├── .babelrc ├── .eslintrc ├── .github ├── CONTRIBUTING.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── demo ├── code.js └── styles.css ├── dist └── presentr.min.js ├── index.html ├── logo.png ├── package-lock.json ├── package.json ├── src ├── presentr.js └── utils │ ├── assign-deep.js │ ├── event-listener.js │ ├── query-all.js │ └── wrap-array.js ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-class-properties" 4 | ] 5 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "problems" 6 | ], 7 | "env": { 8 | "browser": true 9 | }, 10 | "globals": { 11 | "VERSION": true 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module" 16 | }, 17 | "rules": { 18 | "no-cond-assign": "off", 19 | "no-console": "error", 20 | "keyword-spacing": "warn", 21 | "eqeqeq": "error" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ### Issue 4 | 5 | 1. Try the [master](https://github.com/Simonwep/presentr/tree/master)-branch, perhaps the problem has been solved. 6 | 2. [Use the search](https://github.com/Simonwep/presentr/search?type=Issues), maybe there is already an answer. 7 | 3. If not found, [create an issue](https://github.com/Simonwep/presentr/issues/new), please dont forget to carefully describe it how to reproduce it / pay attention to the issue-template. 8 | 9 | *** 10 | 11 | ### Pull Request 12 | 13 | 1. Before a Pull request run `npm run build`. 14 | 2. Pull requests only into [master](https://github.com/Simonwep/presentr/tree/master)-branch. 15 | 16 | *** 17 | 18 | ### Setup 19 | 20 | This project requires [npm](https://nodejs.org/en/). 21 | 22 | 1. Fork this repo on [github](https://github.com/Simonwep/presentr). 23 | 2. Clone locally. 24 | 3. From your local repo run `npm install`. 25 | 4. Run local dev server `npm run dev` and go to `http://localhost:3008/` 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### Do you want to request a **feature** or report a **bug**? 4 | 5 | #### What is the current behavior? 6 | 7 | #### If the current behavior is a bug, please provide the steps to reproduce. 8 | 9 | #### What is the expected behavior? 10 | 11 | #### Your environment: 12 | ``` 13 | Presentr-version (see `presentr.version`): 14 | Browser-version: 15 | Operating-system: 16 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Optional npm cache directory 5 | .npm 6 | 7 | # IntelliJ files 8 | /.idea 9 | /_psd -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "stable" 5 | 6 | install: 7 | - npm install --only=dev 8 | 9 | script: 10 | - npm run build 11 | 12 | cache: 13 | directories: 14 | - "node_modules" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 - 2020 Simon Reinisch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo 3 |

4 | 5 |

6 | Minimalistic, Hackable, Fast Presentation Library. 7 |

8 | 9 | 10 |

11 | gzip size 12 | brotli size 13 | Build Status 16 | Download count 19 | No dependencies 20 | JSDelivr download count 23 | Current version 25 | Support me 28 |

29 | 30 |
31 | 32 | ### Features 33 | * No jQuery 34 | * No dependencies 35 | * Hackable / Extensible 36 | * Ultra small 37 | * Native mobile-support 38 | 39 | Why another library to provide the ability to create a presentation in your browser? 40 | Isn't there already Revealjs which is good and reliable? 41 | Yeah, thought the same. But I was looking for a library which I can use in combination with React, Vue, Bootstrap, Materialize or whatever library I want. 42 | Something which only provides the very essential functionalities to control slides and fragments. 43 | 44 | ## Setup 45 | 46 | ### Node 47 | Install package: 48 | ```shell 49 | $ npm install @simonwep/presentr --save 50 | ``` 51 | 52 | Include code and style: 53 | ```js 54 | import presentr from '@simonwep/presentr'; 55 | ``` 56 | --- 57 | ### Browser 58 | 59 | jsdelivr: 60 | ```html 61 | 62 | ``` 63 | 64 | Directly: 65 | ```html 66 | 67 | ``` 68 | 69 | ## Usage 70 | ```javascript 71 | // Simple example, see optional options for more configuration. 72 | const presentr = new Presentr({ 73 | slides: '.slide', 74 | fragments: '.frag' 75 | }); 76 | ``` 77 | 78 | ## Optional options 79 | ```javascript 80 | const presentr = new Presentr({ 81 | 82 | // Query selector to your slides. 83 | slides: '.slide', 84 | 85 | // Query selector for each fragment of the presentaion. 86 | fragments: '.frag', 87 | 88 | /** 89 | * Can be used to group fragments. 90 | * Apply to multiple elements 'g-a' and they will 91 | * all get active until the first element wich this group 92 | * has been reached. 93 | */ 94 | fragmentGroupPrefix: 'g-', 95 | 96 | // Start index. Does not change the slide sequence. 97 | slideIndex: 0, 98 | 99 | // CSS Classes to get control the appereance of slides and fragments. 100 | // It's possible to use arrays 101 | classes: { 102 | previousSlide: 'previous-slide', // Class for slides behind the current one 103 | nextSlide: 'next-slide', // Class for slides after the current one 104 | currentSlide: 'current-slide', // Class which will be added only to the current slide. 105 | 106 | // Same functionality, just for fragments. 107 | activeFragment: 'active-frag', 108 | currentFragment: 'current-frag' 109 | }, 110 | 111 | // Keyboard shortcuts. 112 | shortcuts: { 113 | 114 | // Jump to next / previous slide 115 | nextSlide: ['d', 'D'], 116 | previousSlide: ['a', 'A'], 117 | 118 | // Jump to first / last slide 119 | firstSlide: ['y', 'Y'], 120 | lastSlide: ['x', 'X'], 121 | 122 | // Jumpt to next / previous fragement. If the first or last fragment is reached, 123 | // the next action would jump to the next / previous slide. 124 | nextFragment: ['ArrowRight', 'ArrowDown'], 125 | previousFragment: ['ArrowLeft', 'ArrowUp'] 126 | } 127 | }); 128 | ``` 129 | 130 | ## Events 131 | Since version `1.1.x` Presentr is event-driven. Use the `on(event, cb)` and `off(event, cb)` functions to bind / unbind eventlistener. 132 | 133 | | Event | Description | Arguments | 134 | | -------------- | ----------- | ----------- | 135 | | `action` | Fires both on `slide` and `fragment` | `{presentr: PickrInstance}` | 136 | | `beforeSlide` | Before slide changes | `{presentr: PickrInstance, from: slideIndex, to: slideIndex}` | 137 | | `slide` | Slide changed | `{presentr: PickrInstance}` | 138 | | `beforeFragment` | Before fragment changes | `{presentr: PickrInstance, from: fragmentIndex, to: fragmentIndex}` | 139 | | `fragment` | Fragment changed | `PickrInstance` | 140 | 141 | > Example: 142 | ```js 143 | presentr.on('action', () => { 144 | console.log('action'); 145 | }).on('beforeSlide', obj => { 146 | console.log('beforeSlide', obj); 147 | // Return false explicitly to block slide 148 | }).on('beforeFragment', obj => { 149 | console.log('beforeFragment', obj); 150 | // Return false explicitly to block fragment 151 | }).on('slide', () => { 152 | console.log('slide'); 153 | }).on('fragment', () => { 154 | console.log('fragment'); 155 | }); 156 | ``` 157 | 158 | ## Methods 159 | * presentr.nextSlide() _- Jump to next slide._ 160 | * presentr.previousSlide() _- Jump to previous slide._ 161 | * presentr.firstSlide() _- Jump to first slide._ 162 | * presentr.lastSlide() _- Jump to last slide._ 163 | * presentr.jumpSlide(index`:Number`) _- Jump to a specific slide._ 164 | * presentr.nextFragment() _- Jump to next fragment, if end reached the next slide will be shown._ 165 | * presentr.previousFragment() _- Jump to previous fragment, if start reached the previous slide will be shown._ 166 | * presentr.jumpFragment(index`:Number`) _- Jump to a specific fragment on the current slide._ 167 | * presentr.destroy() _- Destroys the presentr instance and unbinds all event-listeners._ 168 | 169 | ## Getters 170 | * presentr.totalSlides _- Total amount of slides._ 171 | * presentr.totalFragments _- Total amount of fragments in current slide._ 172 | * presentr.slideIndex _- Current slide index._ 173 | * presentr.fragmentIndex _- Current fragment index in slide._ 174 | * presentr.globalFragmentCount _- Total amount of fragments on all slides combined._ 175 | 176 | ## Contributing 177 | If you want to open a issue, create a Pull Request or simply want to know how you can run it on your local machine, please read the [Contributing guide](https://github.com/Simonwep/presentr/blob/master/.github/CONTRIBUTING.md). 178 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | presets: [ 4 | [ 5 | '@babel/preset-env', { 6 | 'targets': { 7 | 'browsers': [ 8 | 'Chrome >= 52', 9 | 'FireFox >= 45', 10 | 'Safari >= 10', 11 | 'edge 15' 12 | ] 13 | }, 14 | 'modules': 'umd' 15 | } 16 | ] 17 | ] 18 | 19 | }; -------------------------------------------------------------------------------- /demo/code.js: -------------------------------------------------------------------------------- 1 | const processbar = document.querySelector('.progress'); 2 | 3 | const presentr = new Presentr({ 4 | slides: '.presentr .slides > section' 5 | }).on('action', () => { 6 | processbar.style.width = `${(100 * (presentr.slideIndex / presentr.totalSlides))}vw`; 7 | }); 8 | -------------------------------------------------------------------------------- /demo/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Montserrat:400,600|Satisfy'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | overflow: hidden; 11 | } 12 | 13 | .presentr { 14 | font-family: 'Montserrat'; 15 | font-size: 1vmax; 16 | } 17 | 18 | .presentr .progress { 19 | position: fixed; 20 | background: #ff4446; 21 | top: 0; 22 | left: 0; 23 | width: 0; 24 | height: 0.2vh; 25 | z-index: 1; 26 | transition: all 1s; 27 | } 28 | 29 | .slides { 30 | transition: all 0.6s; 31 | } 32 | 33 | .slides section { 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | justify-content: center; 38 | filter: grayscale(100%); 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | width: 100vw; 43 | height: 100vh; 44 | } 45 | 46 | .slides section.next-slide { 47 | transform: translateX(100vw); 48 | transition: all 0.3s; 49 | } 50 | 51 | .slides section.current-slide { 52 | transition: all 0.3s; 53 | filter: none; 54 | transform: none; 55 | } 56 | 57 | .slides section.previous-slide { 58 | transition: all 0.3s; 59 | filter: none; 60 | transform: translateX(-100vw); 61 | } 62 | 63 | .slides section h2 { 64 | font-family: 'Satisfy'; 65 | font-size: 8em; 66 | color: #ff4446; 67 | margin-bottom: 5vh; 68 | } 69 | 70 | .slides section h2, 71 | .slides section.welcome h1 { 72 | text-shadow: 2px 2px 0 #862224; 73 | } 74 | 75 | .slides section.welcome .keyboard-shortcuts { 76 | text-align: center; 77 | color: rgba(0, 0, 0, 0.75); 78 | font-size: 1.3em; 79 | } 80 | 81 | .slides section.welcome .keyboard-shortcuts p { 82 | transform: translateY(-0.5vh); 83 | opacity: 0; 84 | margin: 0.3em 0; 85 | animation: normalize 0.7s ease forwards; 86 | } 87 | 88 | .slides section.welcome .keyboard-shortcuts p:nth-child(2) { 89 | animation-delay: 0.5s; 90 | } 91 | 92 | .slides section.welcome .keyboard-shortcuts b { 93 | font-weight: 600; 94 | } 95 | 96 | .slides section.welcome h1 { 97 | font-family: 'Satisfy'; 98 | font-size: 10em; 99 | color: #ff4446; 100 | } 101 | 102 | .slides section.features .list { 103 | display: flex; 104 | flex-direction: column; 105 | align-items: center; 106 | } 107 | 108 | .slides section.features .list p { 109 | color: rgba(0, 0, 0, 0.85); 110 | display: inline-block; 111 | position: relative; 112 | margin: 0.5em 0; 113 | padding: 0.3em 0.4em 0 0.4em; 114 | font-size: 2em; 115 | text-align: center; 116 | } 117 | 118 | .slides section.features .list p::before { 119 | position: absolute; 120 | content: ''; 121 | bottom: -0.2em; 122 | left: 0; 123 | right: 0; 124 | margin: auto; 125 | width: 0; 126 | height: 100%; 127 | background: #ff4446; 128 | transition: all 0.5s; 129 | z-index: -1; 130 | } 131 | 132 | .slides section.features .list p.current-frag { 133 | color: white; 134 | } 135 | 136 | .slides section.features .list p.current-frag::before { 137 | width: 100%; 138 | } 139 | 140 | .slides section.setup .methods { 141 | display: flex; 142 | flex-direction: column; 143 | } 144 | 145 | .slides section.setup .methods .method { 146 | margin-bottom: 3em; 147 | } 148 | 149 | .slides section.setup .methods .method h3 { 150 | font-size: 1.5em; 151 | font-weight: 400; 152 | margin-bottom: 0.8em; 153 | color: rgba(0, 0, 0, 0.85); 154 | } 155 | 156 | .slides section.setup .methods .method code { 157 | background: #f6f8fa; 158 | padding: 0.8em 1.2em; 159 | transition: all 0.5s; 160 | border-bottom: 2px solid #f6f8fa; 161 | } 162 | 163 | .slides section.setup .methods .method.current-frag code { 164 | border-bottom: 2px solid #ff4446; 165 | } 166 | 167 | .slides section.github a { 168 | cursor: pointer; 169 | outline: none; 170 | text-decoration: none; 171 | background: #ff4446; 172 | color: white; 173 | font-size: 3em; 174 | padding: 0.8em 1.5em; 175 | box-shadow: 2px 2px 0 0 #862224, 176 | 1px 1px 0 0 #862224; 177 | transition: all 0.3s; 178 | } 179 | 180 | .slides section.github a:hover { 181 | background: #d73b3d; 182 | } 183 | 184 | .slides section.current-slide { 185 | transform: none; 186 | filter: none; 187 | } 188 | 189 | .slides .frag { 190 | opacity: 0; 191 | transform: translateY(-0.5vh); 192 | transition: all 0.5s; 193 | } 194 | 195 | .slides .frag.active-frag { 196 | transform: none; 197 | opacity: 1; 198 | } 199 | 200 | @keyframes normalize { 201 | to { 202 | transform: none; 203 | opacity: 1; 204 | } 205 | } 206 | 207 | @media screen and (max-width: 1000px) { 208 | .slides section { 209 | padding: 0 2em; 210 | } 211 | } -------------------------------------------------------------------------------- /dist/presentr.min.js: -------------------------------------------------------------------------------- 1 | /*! Presentr 1.2.0 MIT | https://github.com/Simonwep/presentr */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Presentr=t():e.Presentr=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){var n,i,o;"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self&&self,i=[t,r(1),r(2),r(3),r(4)],void 0===(o="function"==typeof(n=function(e,t,r,n,i){"use strict";function o(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function s(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};a(this,"_slideIndex",0),a(this,"_boundEventListener",{slide:[],beforeSlide:[],fragment:[],beforeFragment:[],action:[]}),a(this,"_options",{slides:".presentr .slides > section",fragments:".frag",fragmentGroupPrefix:"g-",slideIndex:0,classes:{previousSlide:"previous-slide",nextSlide:"next-slide",currentSlide:"current-slide",activeFragment:"active-frag",currentFragment:"current-frag"},shortcuts:{nextSlide:["d","D"],previousSlide:["a","A"],firstSlide:["y","Y"],lastSlide:["x","X"],nextFragment:["ArrowRight","ArrowDown"],previousFragment:["ArrowLeft","ArrowUp"]}}),(0,t.default)(this._options,e);var i=this._options.fragmentGroupPrefix,o=(0,n.queryAll)(this._options.slides,document);this._slides=[];var s=!0,u=!1,f=void 0;try{for(var d,c=o[Symbol.iterator]();!(s=(d=c.next()).done);s=!0){for(var p=d.value,y=new Map,v=(0,n.queryAll)(this._options.fragments,p).map(e=>[e]),h=0;he.startsWith(i));if(g)if(y.has(g)){var b=l(v.splice(h,1),1)[0];v[y.get(g)].push(b[0]),h--}else y.set(g,h)}this._slides.push({el:p,fragments:v,fragmentIndex:0})}}catch(e){u=!0,f=e}finally{try{s||null==c.return||c.return()}finally{if(u)throw f}}this._eventListeners=[(0,r.on)(window,"keyup",e=>{var t=t=>t===e.code||t===e.key,r=this._options.shortcuts,n=Object.keys(r).find(e=>{var n=r[e];return Array.isArray(n)?n.find(t):t(n)});n&&["nextSlide","previousSlide","lastSlide","firstSlide","nextFragment","previousFragment"].includes(n)&&this[n]()}),(0,r.on)(window,"touchstart",e=>{(e&&e.touches&&e.touches[0]&&e.touches[0].clientX)>window.innerWidth/2?this.nextFragment():this.previousFragment()})],this.jumpSlide(this._options.slideIndex||0)}_emit(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=!0,n=!1,i=void 0;try{for(var o,l=this._boundEventListener[e][Symbol.iterator]();!(r=(o=l.next()).done);r=!0)if(!1===(0,o.value)(s({presentr:this},t)))return!1}catch(e){n=!0,i=e}finally{try{r||null==l.return||l.return()}finally{if(n)throw i}}return!0}on(e,t){if(!(e in this._boundEventListener))throw new Error("No such event: ".concat(e));if("function"!=typeof t)throw new Error("Callback must be a function");return this._boundEventListener[e].push(t),this}off(e,t){var r=this._boundEventListener[e];if(r){var n=r.indexOf(t);~n&&r.splice(n,1)}return this}firstSlide(){this.jumpSlide(0)}lastSlide(){this.jumpSlide(this._slides.length-1)}nextSlide(){this.jumpSlide(this._slideIndex+1)}previousSlide(){this.jumpSlide(this._slideIndex-1)}jumpSlide(e){var t=this._slides,r=this._options;if(e<0||e>=t.length)return!1;if(this._emit("beforeSlide",{from:this._slideIndex,to:e})){for(var n=r.classes,o=(0,i.wrapArray)(n.currentSlide),s=(0,i.wrapArray)(n.previousSlide),l=(0,i.wrapArray)(n.nextSlide),a=0;ae&&(u.remove(...o),u.add(...l))}return this._slideIndex=e,this._emit("slide"),this._emit("action"),!0}}nextFragment(){this.jumpFragment(this._currentSlide.fragmentIndex+1)}previousFragment(){this.jumpFragment(this._currentSlide.fragmentIndex-1)}jumpFragment(e){var t=this._currentSlide;if(this._emit("beforeFragment",{from:t.fragmentIndex,to:e})){if(e<0)return this.previousSlide();if(e>t.fragments.length)return this.nextSlide();t.fragmentIndex=e;for(var r=this._options.classes,n=r.activeFragment,o=r.currentFragment,s=t.fragments,l=(0,i.wrapArray)(n),a=(0,i.wrapArray)(o),u=0;u(0,r.off)(...e))}get _currentSlide(){return this._slides[this._slideIndex]}get totalSlides(){return this._slides.length}get globalFragmentCount(){return this._slides.reduce((e,t)=>e+t.fragments.length,0)}get totalFragments(){return this._currentSlide.fragments.length}get slideIndex(){return this._slideIndex}get fragmentIndex(){return this._currentSlide.fragmentIndex}}f.version="1.2.0";var d=f;e.default=d})?n.apply(t,i):n)||(e.exports=o)},function(e,t,r){var n,i,o;"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self&&self,i=[t],void 0===(o="function"==typeof(n=function(e){"use strict";function t(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e)){var r=[],n=!0,i=!1,o=void 0;try{for(var s,l=e[Symbol.iterator]();!(n=(s=l.next()).done)&&(r.push(s.value),!t||r.length!==t);n=!0);}catch(e){i=!0,o=e}finally{try{n||null==l.return||l.return()}finally{if(i)throw o}}return r}}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}Object.defineProperty(e,"__esModule",{value:!0}),e.default=function e(){for(var r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1?arguments[1]:void 0,i=0,o=Object.entries(n);i4&&void 0!==arguments[4]?arguments[4]:{};t instanceof HTMLCollection||t instanceof NodeList?t=Array.from(t):Array.isArray(t)||(t=[t]),Array.isArray(n)||(n=[n]);var s=!0,l=!1,a=void 0;try{for(var u,f=t[Symbol.iterator]();!(s=(u=f.next()).done);s=!0){var d=u.value,c=!0,p=!1,y=void 0;try{for(var v,h=n[Symbol.iterator]();!(c=(v=h.next()).done);c=!0){var g=v.value;d[e](g,i,r({capture:!1},o))}}catch(e){p=!0,y=e}finally{try{c||null==h.return||h.return()}finally{if(p)throw y}}}}catch(e){l=!0,a=e}finally{try{s||null==f.return||f.return()}finally{if(l)throw a}}return[t,n,i,o]}e.off=o})?n.apply(t,i):n)||(e.exports=o)},function(e,t,r){var n,i,o;"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self&&self,i=[t],void 0===(o="function"==typeof(n=function(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.queryAll=void 0,e.queryAll=(e,t)=>Array.from(t.querySelectorAll(e))})?n.apply(t,i):n)||(e.exports=o)},function(e,t,r){var n,i,o;"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self&&self,i=[t],void 0===(o="function"==typeof(n=function(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.wrapArray=void 0,e.wrapArray=e=>Array.isArray(e)?e:[e]})?n.apply(t,i):n)||(e.exports=o)}]).default})); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | Presentr 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 |
55 | 56 | 57 |
58 |

Presentr

59 | 60 |
61 |

Use Arrow Left | Right to navigate.

62 |

Or tap on the left / right side of the screen.

63 |
64 |
65 | 66 | 67 |
68 |

Features

69 | 70 |
71 |

Simple usage

72 |

No jQuery

73 |

No dependencies

74 |

Hackable / Extensible

75 |

Ultra small, only 1.8 kB gzipped

76 |
77 |
78 | 79 | 80 |
81 |

Setup

82 | 83 |
84 |
85 |

Install via npm

86 | $ npm install @simonwep/presentr 87 |
88 | 89 |
90 |

Import in node

91 | import Presentr from 'Presentr' 92 |
93 | 94 |
95 |

Or via jsdelivr

96 | <script src="https://cdn.jsdelivr.net/npm/@simonwep/presentr/dist/presentr.min.js"></script> 97 |
98 | 99 |
100 |

Or directly

101 | <script src="node_modules/@simonwep/presentr/dist/presentr.min.js"></script> 102 |
103 |
104 |
105 | 106 | 107 |
108 |

Getting starded!

109 | GitHub 110 |
111 |
112 | 113 |
114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonwep/presentr/37447dc55c5d576a1718e64bf3a8a75f29dcfb84/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simonwep/presentr", 3 | "version": "1.2.0", 4 | "description": "Minimalistic and extensible javascript presentation-library.", 5 | "main": "./dist/presentr.min.js", 6 | "author": "Simon Reinisch", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "webpack --mode production --config webpack.config.prod.js", 10 | "dev": "webpack-dev-server --mode development --config webpack.config.dev.js", 11 | "lint": "eslint ./src --fix" 12 | }, 13 | "keywords": [ 14 | "presentations", 15 | "presentation", 16 | "js-libray", 17 | "slides", 18 | "slideshow" 19 | ], 20 | "devDependencies": { 21 | "@babel/core": "^7.7.7", 22 | "@babel/plugin-proposal-class-properties": "^7.7.4", 23 | "@babel/preset-env": "^7.7.7", 24 | "babel-eslint": "^10.0.3", 25 | "babel-loader": "^8.0.6", 26 | "eslint": "^6.8.0", 27 | "eslint-config-problems": "^3.0.1", 28 | "eslint-loader": "^3.0.3", 29 | "webpack": "^4.41.4", 30 | "webpack-cli": "^3.3.10", 31 | "webpack-dev-server": "^3.10.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/presentr.js: -------------------------------------------------------------------------------- 1 | import assignDeep from './utils/assign-deep'; 2 | import {off, on} from './utils/event-listener'; 3 | import {queryAll} from './utils/query-all'; 4 | import {wrapArray} from './utils/wrap-array'; 5 | 6 | class Presentr { 7 | 8 | // Current slide-index 9 | _slideIndex = 0; 10 | 11 | // Event-listener 12 | _boundEventListener = { 13 | 'slide': [], 14 | 'beforeSlide': [], 15 | 'fragment': [], 16 | 'beforeFragment': [], 17 | 'action': [] 18 | }; 19 | 20 | _options = { 21 | 22 | // Query selectors 23 | slides: '.presentr .slides > section', 24 | fragments: '.frag', 25 | 26 | // CSS Group prefix 27 | fragmentGroupPrefix: 'g-', 28 | 29 | // Start index 30 | slideIndex: 0, 31 | 32 | // CSS classes 33 | classes: { 34 | previousSlide: 'previous-slide', 35 | nextSlide: 'next-slide', 36 | currentSlide: 'current-slide', 37 | activeFragment: 'active-frag', 38 | currentFragment: 'current-frag' 39 | }, 40 | 41 | // Keyboard shortcuts 42 | shortcuts: { 43 | nextSlide: ['d', 'D'], 44 | previousSlide: ['a', 'A'], 45 | 46 | firstSlide: ['y', 'Y'], 47 | lastSlide: ['x', 'X'], 48 | 49 | nextFragment: ['ArrowRight', 'ArrowDown'], 50 | previousFragment: ['ArrowLeft', 'ArrowUp'] 51 | } 52 | }; 53 | 54 | constructor(opt = {}) { 55 | 56 | // Override default options 57 | assignDeep(this._options, opt); 58 | 59 | // Resolve slides and their fragments 60 | const {fragmentGroupPrefix} = this._options; 61 | const slides = queryAll(this._options.slides, document); 62 | 63 | this._slides = []; 64 | for (const slideElement of slides) { 65 | const groupIndexes = new Map(); 66 | const fragments = queryAll(this._options.fragments, slideElement) 67 | .map(v => [v]); 68 | 69 | for (let i = 0; i < fragments.length; i++) { 70 | const group = Array.from(fragments[i][0].classList) 71 | .find(v => v.startsWith(fragmentGroupPrefix)); 72 | 73 | if (group) { 74 | if (groupIndexes.has(group)) { 75 | const [arr] = fragments.splice(i, 1); 76 | fragments[groupIndexes.get(group)].push(arr[0]); 77 | i--; 78 | } else { 79 | groupIndexes.set(group, i); 80 | } 81 | } 82 | } 83 | 84 | this._slides.push({ 85 | el: slideElement, 86 | fragments, 87 | fragmentIndex: 0 88 | }); 89 | } 90 | 91 | // Bind shortcuts 92 | this._eventListeners = [ 93 | 94 | // Listen for key-events 95 | on(window, 'keyup', e => { 96 | const match = cv => cv === e.code || cv === e.key; 97 | const {shortcuts} = this._options; 98 | const fns = ['nextSlide', 'previousSlide', 'lastSlide', 'firstSlide', 'nextFragment', 'previousFragment']; // Available shortcuts 99 | 100 | // Find corresponding shortcut action 101 | const target = Object.keys(shortcuts).find(v => { 102 | const code = shortcuts[v]; 103 | return Array.isArray(code) ? code.find(match) : match(code); 104 | }); 105 | 106 | // Check shortcut was found and execute function 107 | target && fns.includes(target) && this[target](); 108 | }), 109 | 110 | // Listen for touch-events 111 | on(window, 'touchstart', e => { 112 | const x = e && e.touches && e.touches[0] && e.touches[0].clientX; 113 | const halfWidth = window.innerWidth / 2; 114 | x > halfWidth ? this.nextFragment() : this.previousFragment(); 115 | }) 116 | ]; 117 | 118 | // Trigger 119 | this.jumpSlide(this._options.slideIndex || 0); 120 | } 121 | 122 | /* eslint-disable callback-return */ 123 | _emit(event, args = {}) { 124 | for (const cb of this._boundEventListener[event]) { 125 | if (cb({ 126 | presentr: this, 127 | ...args 128 | }) === false) { 129 | return false; 130 | } 131 | } 132 | 133 | return true; 134 | } 135 | 136 | on(event, cb) { 137 | 138 | if (!(event in this._boundEventListener)) { 139 | throw new Error(`No such event: ${event}`); 140 | } else if (typeof cb !== 'function') { 141 | throw new Error(`Callback must be a function`); 142 | } 143 | 144 | this._boundEventListener[event].push(cb); 145 | return this; 146 | } 147 | 148 | off(event, cb) { 149 | const callBacks = this._boundEventListener[event]; 150 | 151 | if (callBacks) { 152 | const index = callBacks.indexOf(cb); 153 | 154 | if (~index) { 155 | callBacks.splice(index, 1); 156 | } 157 | } 158 | 159 | return this; 160 | } 161 | 162 | firstSlide() { 163 | this.jumpSlide(0); 164 | } 165 | 166 | lastSlide() { 167 | this.jumpSlide(this._slides.length - 1); 168 | } 169 | 170 | nextSlide() { 171 | this.jumpSlide(this._slideIndex + 1); 172 | } 173 | 174 | previousSlide() { 175 | this.jumpSlide(this._slideIndex - 1); 176 | } 177 | 178 | jumpSlide(index) { 179 | const {_slides, _options} = this; 180 | 181 | // Validate 182 | if (index < 0 || index >= _slides.length) { 183 | return false; 184 | } 185 | 186 | if (!this._emit('beforeSlide', { 187 | from: this._slideIndex, 188 | to: index 189 | })) return; 190 | 191 | const {classes} = _options; 192 | const cs = wrapArray(classes.currentSlide); 193 | const ps = wrapArray(classes.previousSlide); 194 | const ns = wrapArray(classes.nextSlide); 195 | 196 | for (let i = 0; i < _slides.length; i++) { 197 | const classl = _slides[i].el.classList; 198 | 199 | if (i === index) { 200 | classl.remove(...ps); 201 | classl.remove(...ns); 202 | classl.add(...cs); 203 | } else if (i < index) { 204 | classl.remove(...cs); 205 | classl.add(...ps); 206 | } else if (i > index) { 207 | classl.remove(...cs); 208 | classl.add(...ns); 209 | } 210 | } 211 | 212 | // Apply index 213 | this._slideIndex = index; 214 | 215 | // Fire event 216 | this._emit('slide'); 217 | this._emit('action'); 218 | return true; 219 | } 220 | 221 | nextFragment() { 222 | this.jumpFragment(this._currentSlide.fragmentIndex + 1); 223 | } 224 | 225 | previousFragment() { 226 | this.jumpFragment(this._currentSlide.fragmentIndex - 1); 227 | } 228 | 229 | jumpFragment(index) { 230 | const slide = this._currentSlide; 231 | 232 | if (!this._emit('beforeFragment', { 233 | from: slide.fragmentIndex, 234 | to: index 235 | })) return; 236 | 237 | // Jump to next / previous slide if no further fragments 238 | if (index < 0) { 239 | return this.previousSlide(); 240 | } else if (index > slide.fragments.length) { 241 | return this.nextSlide(); 242 | } 243 | 244 | slide.fragmentIndex = index; 245 | 246 | // Apply class for previous and current fragment(s) 247 | const {activeFragment, currentFragment} = this._options.classes; 248 | const {fragments} = slide; 249 | 250 | const af = wrapArray(activeFragment); 251 | const cf = wrapArray(currentFragment); 252 | for (let i = 0; i < fragments.length; i++) { 253 | for (const sf of fragments[i]) { 254 | sf.classList.remove(...cf); 255 | sf.classList.remove(...af); 256 | 257 | if (i < (index - 1)) { 258 | sf.classList.add(...af); 259 | } else if (i === (index - 1)) { 260 | sf.classList.add(...af); 261 | sf.classList.add(...cf); 262 | } 263 | } 264 | } 265 | 266 | // Fire events 267 | this._emit('fragment'); 268 | this._emit('action'); 269 | return true; 270 | } 271 | 272 | destroy() { 273 | 274 | // Unbind event-listeners 275 | this._eventListeners.forEach(args => off(...args)); 276 | } 277 | 278 | get _currentSlide() { 279 | return this._slides[this._slideIndex]; 280 | } 281 | 282 | get totalSlides() { 283 | return this._slides.length; 284 | } 285 | 286 | get globalFragmentCount() { 287 | return this._slides.reduce((acc, cv) => acc + cv.fragments.length, 0); 288 | } 289 | 290 | get totalFragments() { 291 | return this._currentSlide.fragments.length; 292 | } 293 | 294 | get slideIndex() { 295 | return this._slideIndex; 296 | } 297 | 298 | get fragmentIndex() { 299 | return this._currentSlide.fragmentIndex; 300 | } 301 | } 302 | 303 | // Export version 304 | Presentr.version = VERSION; 305 | 306 | // Export factory function 307 | export default Presentr; 308 | -------------------------------------------------------------------------------- /src/utils/assign-deep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Deep version of apply, doesn't support circular references 3 | * @param source 4 | * @param target 5 | * @returns {*} 6 | */ 7 | export default function assignDeep(target = {}, source) { 8 | 9 | for (const [key, val] of Object.entries(source)) { 10 | const targetValue = target[key]; 11 | 12 | if (targetValue !== null && typeof targetValue === 'object') { 13 | target[key] = assignDeep(targetValue, val); 14 | } else if (typeof val !== 'undefined') { 15 | target[key] = val; 16 | } 17 | } 18 | 19 | return target; 20 | } -------------------------------------------------------------------------------- /src/utils/event-listener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Add event(s) to element(s). 3 | * @param elements DOM-Elements 4 | * @param events Event names 5 | * @param fn Callback 6 | * @param options Optional options 7 | * @return Array passed arguments 8 | */ 9 | export const on = eventListener.bind(null, 'addEventListener'); 10 | 11 | /** 12 | * Remove event(s) from element(s). 13 | * @param elements DOM-Elements 14 | * @param events Event names 15 | * @param fn Callback 16 | * @param options Optional options 17 | * @return Array passed arguments 18 | */ 19 | export const off = eventListener.bind(null, 'removeEventListener'); 20 | 21 | function eventListener(method, elements, events, fn, options = {}) { 22 | 23 | // Normalize array 24 | if (elements instanceof HTMLCollection || elements instanceof NodeList) { 25 | elements = Array.from(elements); 26 | } else if (!Array.isArray(elements)) { 27 | elements = [elements]; 28 | } 29 | 30 | if (!Array.isArray(events)) { 31 | events = [events]; 32 | } 33 | 34 | for (const el of elements) { 35 | for (const ev of events) { 36 | el[method](ev, fn, {capture: false, ...options}); 37 | } 38 | } 39 | 40 | return [elements, events, fn, options]; 41 | } -------------------------------------------------------------------------------- /src/utils/query-all.js: -------------------------------------------------------------------------------- 1 | export const queryAll = (query, base) => Array.from(base.querySelectorAll(query)); -------------------------------------------------------------------------------- /src/utils/wrap-array.js: -------------------------------------------------------------------------------- 1 | export const wrapArray = val => Array.isArray(val) ? val : [val]; -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './src/presentr.js', 5 | 6 | output: { 7 | path: `${__dirname}/dist`, 8 | publicPath: '/dist/', 9 | filename: 'presentr.min.js', 10 | library: 'Presentr', 11 | libraryExport: 'default', 12 | libraryTarget: 'umd' 13 | }, 14 | 15 | devServer: { 16 | host: '0.0.0.0', 17 | port: 3003 18 | }, 19 | 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | VERSION: JSON.stringify('unknown') 23 | }) 24 | ], 25 | 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js$/, 30 | loader: 'babel-loader' 31 | } 32 | ] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const {version} = require('./package'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/presentr.js', 6 | 7 | output: { 8 | path:`${__dirname}/dist`, 9 | filename: 'presentr.min.js', 10 | library: 'Presentr', 11 | libraryExport: 'default', 12 | libraryTarget: 'umd' 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | loader: [ 20 | 'babel-loader', 21 | 'eslint-loader' 22 | ] 23 | } 24 | ] 25 | }, 26 | 27 | plugins: [ 28 | new webpack.BannerPlugin({ 29 | banner: `Presentr ${version} MIT | https://github.com/Simonwep/presentr` 30 | }), 31 | 32 | new webpack.DefinePlugin({ 33 | VERSION: JSON.stringify(version) 34 | }) 35 | ] 36 | }; 37 | --------------------------------------------------------------------------------