├── .babelrc ├── .editorconfig ├── .gitignore ├── license ├── package.json ├── readme.md ├── rollup.config.js └── src └── main.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [{*.yml,*.json}] 15 | indent_size = 2 16 | indent_style = space 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Antoine Boulanger 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. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modularload", 3 | "version": "1.2.8", 4 | "description": "Dead simple page transitions and lazy loading.", 5 | "repository": "modularorg/modularload", 6 | "author": "Antoine Boulanger (https://antoineboulanger.com)", 7 | "license": "MIT", 8 | "main": "dist/main.cjs.js", 9 | "module": "dist/main.esm.js", 10 | "scripts": { 11 | "build": "rollup -c" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.7.2", 15 | "@babel/preset-env": "^7.7.1", 16 | "rollup": "^1.27.0", 17 | "rollup-plugin-babel": "^4.3.3", 18 | "rollup-plugin-node-resolve": "^5.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 |

modularLoad

7 |

Dead simple page transitions and lazy loading.

8 | 9 | ## Installation 10 | ```sh 11 | npm install modularload 12 | ``` 13 | 14 | ## Why 15 | - Simple 16 | - Lightweight 17 | - Minimal configuration 18 | - No dependencies 19 | 20 | ## Usage 21 | ```js 22 | import modularLoad from 'modularload'; 23 | 24 | this.load = new modularLoad({ 25 | enterDelay: 300 26 | }); 27 | ``` 28 | ```html 29 |
30 |

Hello

31 | Read more 32 |
33 | ``` 34 | 35 | #### With custom transitions 36 | ```js 37 | import modularLoad from 'modularload'; 38 | 39 | this.load = new modularLoad({ 40 | enterDelay: 300, 41 | transitions: { 42 | transitionName: { 43 | enterDelay: 450 44 | }, 45 | transitionTwoName: { 46 | enterDelay: 600 47 | } 48 | } 49 | }); 50 | ``` 51 | ```html 52 | 53 | 54 | 57 |
58 |

Hello

59 | Read more 60 |
61 | 62 | 63 | ``` 64 | 65 | #### With custom container 66 | ```js 67 | import modularLoad from 'modularload'; 68 | 69 | this.load = new modularLoad({ 70 | enterDelay: 600, 71 | transitions: { 72 | article: { 73 | enterDelay: 300 74 | } 75 | } 76 | }); 77 | ``` 78 | ```html 79 |
80 |
81 |

Article One

82 |

Text

83 |
84 | Article One 85 | Article Two 86 |
87 | ``` 88 | 89 | #### With lazy images 90 | ```js 91 | import modularLoad from 'modularload'; 92 | 93 | this.load = new modularLoad(); 94 | ``` 95 | ```html 96 |
97 |
98 |

Hello

99 |
100 |
101 | 102 | Read more 103 |
104 |
105 | ``` 106 | 107 | #### With events 108 | ```js 109 | import modularLoad from 'modularload'; 110 | 111 | this.load = new modularLoad(); 112 | 113 | this.load.on('loaded', (transition, oldContainer, newContainer) => { 114 | console.log('👌'); 115 | 116 | if (transition == 'transitionName') { 117 | console.log('🤙'); 118 | } 119 | }); 120 | ``` 121 | 122 | #### With methods 123 | ```js 124 | import modularLoad from 'modularload'; 125 | 126 | this.load = new modularLoad(); 127 | 128 | this.load.goTo('/page', 'transitionName'); 129 | ``` 130 | 131 | ## Options 132 | | Option | Type | Default | Description | 133 | | ------ | ---- | ------- | ----------- | 134 | | `name` | `string` | `'load'` | Data attributes name | 135 | | `loadingClass` | `string` | `'is-loading'` | Class when a link is clicked. | 136 | | `loadedClass` | `string` | `'is-loaded'` | Class when the new container enters. | 137 | | `readyClass` | `string` | `'is-ready'` | Class when the old container exits. | 138 | | `transitionsPrefix` | `string` | `'is-'` | Custom transitions class prefix. | 139 | | `transitionsHistory` | `boolean` | `true` | Redo the custom transitions while using the back button. | 140 | | `enterDelay` | `number` | `0` | Minimum delay before the new container enters. | 141 | | `exitDelay` | `number` | `0` | Delay before the old container exists after the new enters. | 142 | | `loadedDelay` | `number` | `0` | Delay before adding the loaded class. For example, to wait for your JS DOM updates. | 143 | | `transitions` | `object` | `{}` | Custom transitions options. | 144 | 145 | ## Attributes 146 | | Attribute | Values | Description | 147 | | --------- | ------ | ----------- | 148 | | `data-load-container` | ` `, `string` | Container you want to load with optional string. | 149 | | `data-load` | `string`, `false` | Transition name or disable transition. | 150 | | `data-load-url` | `boolean` | Update url without loading container. | 151 | | `data-load-src` | `string` | Lazy load src attribute. | 152 | | `data-load-srcset` | `string` | Lazy load srcset attribute. | 153 | | `data-load-style` | `string` | Lazy load style attribute. | 154 | | `data-load-href` | `string` | Lazy load href attribute. | 155 | 156 | ## Events 157 | | Event | Arguments | Description | 158 | | ----- | --------- | ----------- | 159 | | `loading` | `transition`, `oldContainer` | On link click. | 160 | | `loaded` | `transition`, `oldContainer`, `newContainer` | On new container enter. | 161 | | `ready` | `transition`, `newContainer` | On old container exit. | 162 | | `images` | | On all images load. | 163 | 164 | ## Methods 165 | | Method | Description | 166 | | ------ | ----------- | 167 | | `goTo('href'[, 'transition'][, true])` | Go to href. With optional transition name and boolean for url update only. | 168 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | 4 | export default [{ 5 | input: 'src/main.js', 6 | output: [ 7 | { 8 | file: 'dist/main.cjs.js', 9 | format: 'cjs' 10 | }, 11 | { 12 | file: 'dist/main.esm.js', 13 | format: 'esm' 14 | } 15 | ], 16 | plugins: [ 17 | resolve(), 18 | babel({ 19 | exclude: 'node_modules/**' 20 | }) 21 | ] 22 | }]; 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(options) { 3 | this.defaults = { 4 | name: 'load', 5 | loadingClass: 'is-loading', 6 | loadedClass: 'is-loaded', 7 | readyClass: 'is-ready', 8 | transitionsPrefix: 'is-', 9 | transitionsHistory: true, 10 | enterDelay: 0, 11 | exitDelay: 0, 12 | loadedDelay: 0, 13 | isLoaded: false, 14 | isEntered: false, 15 | isUrl: false, 16 | transitionContainer: null, 17 | popstateIgnore: false 18 | } 19 | 20 | Object.assign(this, this.defaults, options); 21 | 22 | this.options = options; 23 | this.namespace = 'modular'; 24 | this.html = document.documentElement; 25 | this.href = window.location.href; 26 | this.container = 'data-' + this.name + '-container'; 27 | this.subContainer = false; 28 | this.prevTransition = null; 29 | this.loadAttributes = ['src', 'srcset', 'style', 'href']; 30 | this.isInserted = false; 31 | this.isLoading = false; 32 | this.enterTimeout = false; 33 | this.controller = new AbortController(); 34 | 35 | this.classContainer = this.html; 36 | 37 | this.isChrome = (navigator.userAgent.indexOf("Chrome") != -1) ? true : false; 38 | 39 | this.init(); 40 | } 41 | 42 | init() { 43 | window.addEventListener('popstate', (e) => this.checkState(e), false); 44 | this.html.addEventListener('click', (e) => this.checkClick(e), false); 45 | 46 | this.loadEls(document); 47 | } 48 | 49 | checkClick(e) { 50 | if (!e.ctrlKey && !e.metaKey) { 51 | let target = e.target; 52 | 53 | while (target && target !== document) { 54 | if (target.matches('a') && target.getAttribute('download') == null) { 55 | const href = target.getAttribute('href'); 56 | 57 | if (!href.startsWith('#') && !href.startsWith('mailto:') && !href.startsWith('tel:')) { 58 | e.preventDefault(); 59 | 60 | this.reset(); 61 | this.getClickOptions(target); 62 | } 63 | 64 | break; 65 | } 66 | 67 | target = target.parentNode; 68 | }; 69 | } 70 | } 71 | 72 | checkState() { 73 | if((typeof this.popstateIgnore === 'string') && window.location.href.indexOf(this.popstateIgnore) > -1) { 74 | return 75 | } 76 | 77 | this.reset(); 78 | this.getStateOptions(); 79 | } 80 | 81 | reset() { 82 | if (this.isLoading) { 83 | this.controller.abort(); 84 | this.isLoading = false; 85 | this.controller = new AbortController(); 86 | } 87 | 88 | window.clearTimeout(this.enterTimeout); 89 | 90 | if (this.isInserted) { 91 | this.removeContainer(); 92 | } 93 | 94 | this.classContainer = this.html; 95 | Object.assign(this, this.defaults, this.options); 96 | } 97 | 98 | getClickOptions(link) { 99 | this.transition = link.getAttribute('data-' + this.name); 100 | this.isUrl = link.getAttribute('data-' + this.name + '-url'); 101 | const href = link.getAttribute('href'); 102 | const target = link.getAttribute('target'); 103 | 104 | if (target == '_blank') { 105 | window.open(href, '_blank'); 106 | return; 107 | } 108 | 109 | if (this.transition == 'false') { 110 | window.location = href; 111 | return; 112 | } 113 | 114 | this.setOptions(href, true); 115 | } 116 | 117 | getStateOptions() { 118 | if (this.transitionsHistory) { 119 | this.transition = history.state; 120 | } else { 121 | this.transition = false; 122 | } 123 | 124 | const href = window.location.href; 125 | this.setOptions(href); 126 | } 127 | 128 | goTo(href, transition, isUrl) { 129 | this.reset(); 130 | this.transition = transition; 131 | this.isUrl = isUrl; 132 | 133 | this.setOptions(href, true); 134 | } 135 | 136 | setOptions(href, push) { 137 | let container = '[' + this.container + ']'; 138 | let oldContainer; 139 | 140 | if (this.transition && this.transition != 'true') { 141 | this.transitionContainer = '[' + this.container + '="' + this.transition + '"]'; 142 | this.loadingClass = this.transitions[this.transition].loadingClass || this.loadingClass; 143 | this.loadedClass = this.transitions[this.transition].loadedClass || this.loadedClass; 144 | this.readyClass = this.transitions[this.transition].readyClass || this.readyClass; 145 | this.transitionsPrefix = this.transitions[this.transition].transitionsPrefix || this.transitionsPrefix; 146 | this.enterDelay = this.transitions[this.transition].enterDelay || this.enterDelay; 147 | this.exitDelay = this.transitions[this.transition].exitDelay || this.exitDelay; 148 | this.loadedDelay = this.transitions[this.transition].loadedDelay || this.loadedDelay; 149 | 150 | oldContainer = document.querySelector(this.transitionContainer); 151 | } 152 | 153 | if (oldContainer) { 154 | container = this.transitionContainer; 155 | this.oldContainer = oldContainer; 156 | this.classContainer = this.oldContainer.parentNode; 157 | 158 | if (!this.subContainer) { 159 | history.replaceState(this.transition, null, this.href); 160 | } 161 | 162 | this.subContainer = true; 163 | } else { 164 | this.oldContainer = document.querySelector(container); 165 | 166 | if (this.subContainer) { 167 | history.replaceState(this.prevTransition, null, this.href); 168 | } 169 | 170 | this.subContainer = false; 171 | } 172 | 173 | this.href = href; 174 | this.parentContainer = this.oldContainer.parentNode; 175 | 176 | if (this.isUrl === '' || this.isUrl != null && this.isUrl != 'false' && this.isUrl != false) { 177 | history.pushState(this.transition, null, href); 178 | } else { 179 | this.oldContainer.classList.add('is-old'); 180 | 181 | this.setLoading(); 182 | this.startEnterDelay(); 183 | this.loadHref(href, container, push); 184 | } 185 | } 186 | 187 | setLoading() { 188 | this.classContainer.classList.remove(this.loadedClass, this.readyClass); 189 | this.classContainer.classList.add(this.loadingClass); 190 | 191 | this.classContainer.classList.remove(this.transitionsPrefix + this.prevTransition); 192 | if (this.transition) { 193 | this.classContainer.classList.add(this.transitionsPrefix + this.transition); 194 | } 195 | 196 | if (!this.subContainer) { 197 | this.prevTransition = this.transition; 198 | } 199 | 200 | const loadingEvent = new Event(this.namespace + 'loading'); 201 | window.dispatchEvent(loadingEvent); 202 | } 203 | 204 | startEnterDelay() { 205 | this.enterTimeout = window.setTimeout(() => { 206 | this.isEntered = true; 207 | 208 | if (this.isLoaded) { 209 | this.transitionContainers(); 210 | } 211 | }, this.enterDelay); 212 | } 213 | 214 | loadHref(href, container, push) { 215 | this.isLoading = true; 216 | const signal = this.controller.signal; 217 | 218 | fetch(href, {signal}) 219 | .then(response => response.text()) 220 | .then(data => { 221 | if (push) { 222 | history.pushState(this.transition, null, href); 223 | } 224 | 225 | const parser = new DOMParser(); 226 | this.data = parser.parseFromString(data, 'text/html'); 227 | 228 | this.newContainer = this.data.querySelector(container); 229 | this.newContainer.classList.add('is-new'); 230 | this.parentNewContainer = this.newContainer.parentNode; 231 | 232 | this.hideContainer(); 233 | 234 | this.parentContainer.insertBefore(this.newContainer, this.oldContainer); 235 | this.isInserted = true; 236 | 237 | this.setSvgs(); 238 | 239 | this.isLoaded = true; 240 | 241 | if (this.isEntered) { 242 | this.transitionContainers(); 243 | } 244 | 245 | this.loadEls(this.newContainer); 246 | this.isLoading = false; 247 | }) 248 | .catch(err => { 249 | window.location = href; 250 | }) 251 | } 252 | 253 | transitionContainers() { 254 | this.setAttributes(); 255 | this.showContainer(); 256 | this.setLoaded(); 257 | 258 | setTimeout(() => { 259 | this.removeContainer(); 260 | this.setReady(); 261 | }, this.exitDelay); 262 | } 263 | 264 | setSvgs() { 265 | if (this.isChrome) { 266 | const svgs = this.newContainer.querySelectorAll('use'); 267 | 268 | if (svgs.length) { 269 | svgs.forEach((svg) => { 270 | const xhref = svg.getAttribute('xlink:href'); 271 | if (xhref) { 272 | svg.parentNode.innerHTML = ''; 273 | } else { 274 | const href = svg.getAttribute('href'); 275 | if (href) svg.parentNode.innerHTML = ''; 276 | } 277 | }); 278 | } 279 | } 280 | } 281 | 282 | setAttributes() { 283 | const title = this.data.getElementsByTagName('title')[0]; 284 | const newDesc = this.data.head.querySelector('meta[name="description"]'); 285 | const oldDesc = document.head.querySelector('meta[name="description"]'); 286 | let container; 287 | let newContainer; 288 | 289 | if (this.subContainer) { 290 | newContainer = this.parentNewContainer; 291 | container = document.querySelector(this.transitionContainer).parentNode; 292 | } else { 293 | newContainer = this.data.querySelector('html'); 294 | container = document.querySelector('html'); 295 | } 296 | 297 | const datas = Object.assign({}, newContainer.dataset); 298 | 299 | if (title) document.title = title.innerText; 300 | if (oldDesc && newDesc) oldDesc.setAttribute('content', newDesc.getAttribute('content')); 301 | if (datas) { 302 | Object.entries(datas).forEach(([key, val]) => { 303 | container.setAttribute('data-' + this.toDash(key), val); 304 | }); 305 | } 306 | } 307 | 308 | toDash(str) { 309 | return str.split(/(?=[A-Z])/).join('-').toLowerCase(); 310 | } 311 | 312 | hideContainer() { 313 | this.newContainer.style.visibility = 'hidden'; 314 | this.newContainer.style.height = 0; 315 | this.newContainer.style.overflow = 'hidden'; 316 | } 317 | 318 | showContainer() { 319 | this.newContainer.style.visibility = ''; 320 | this.newContainer.style.height = ''; 321 | this.newContainer.style.overflow = ''; 322 | } 323 | 324 | loadEls(container) { 325 | let promises = []; 326 | 327 | this.loadAttributes.forEach((attr) => { 328 | const data = 'data-' + this.name + '-' + attr; 329 | const els = container.querySelectorAll('[' + data + ']'); 330 | 331 | if (els.length) { 332 | els.forEach((el) => { 333 | const elData = el.getAttribute(data); 334 | el.setAttribute(attr, elData); 335 | 336 | if (attr == 'src' || attr == 'srcset') { 337 | const promise = new Promise(resolve => { 338 | el.onload = () => resolve(el); 339 | }) 340 | promises.push(promise); 341 | } 342 | }); 343 | } 344 | }); 345 | 346 | Promise.all(promises).then(val => { 347 | const imagesEvent = new Event(this.namespace + 'images'); 348 | window.dispatchEvent(imagesEvent); 349 | }); 350 | } 351 | 352 | setLoaded() { 353 | this.classContainer.classList.remove(this.loadingClass); 354 | 355 | setTimeout(() => { 356 | this.classContainer.classList.add(this.loadedClass); 357 | }, this.loadedDelay); 358 | 359 | const loadedEvent = new Event(this.namespace + 'loaded'); 360 | window.dispatchEvent(loadedEvent); 361 | } 362 | 363 | removeContainer() { 364 | this.parentContainer.removeChild(this.oldContainer); 365 | this.newContainer.classList.remove('is-new'); 366 | this.isInserted = false; 367 | } 368 | 369 | setReady() { 370 | this.classContainer.classList.add(this.readyClass); 371 | 372 | const readyEvent = new Event(this.namespace + 'ready'); 373 | window.dispatchEvent(readyEvent); 374 | } 375 | 376 | on(event, func) { 377 | window.addEventListener(this.namespace + event, () => { 378 | switch (event) { 379 | case 'loading': 380 | return func(this.transition, this.oldContainer); 381 | case 'loaded': 382 | return func(this.transition, this.oldContainer, this.newContainer); 383 | case 'ready': 384 | return func(this.transition, this.newContainer); 385 | default: 386 | return func(); 387 | } 388 | }, false); 389 | } 390 | } 391 | --------------------------------------------------------------------------------