├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .idea ├── codeStyleSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── copyright │ ├── adrien.xml │ ├── profiles_settings.xml │ └── rz.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── pageblock.iml ├── vcs.xml └── watcherTasks.xml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── dist ├── main.amd.js ├── main.cjs.js ├── main.e6.js ├── main.esm.js ├── main.iife.js └── main.umd.js ├── esdoc.json ├── examples ├── src │ ├── css │ │ └── demo.css │ ├── img │ │ ├── test1.jpg │ │ ├── test2.jpg │ │ ├── test2.webp │ │ ├── test3.jpg │ │ ├── test4.jpg │ │ └── test5.jpg │ ├── js │ │ ├── ExampleNav.js │ │ ├── api │ │ │ └── Api.js │ │ ├── app.js │ │ ├── blocks │ │ │ ├── DefaultBlock.js │ │ │ ├── InViewBlock.js │ │ │ └── UsersBlock.js │ │ ├── config │ │ │ └── config.example.js │ │ ├── factories │ │ │ └── TransitionFactory.js │ │ ├── pages │ │ │ ├── DefaultPage.js │ │ │ └── HomePage.js │ │ ├── services │ │ │ ├── Splashscreen.js │ │ │ └── WebpackAsyncBlockBuilder.js │ │ ├── transitions │ │ │ ├── BigTransition.js │ │ │ ├── DefaultTransition.js │ │ │ ├── FadeTransition.js │ │ │ └── SlideTransition.js │ │ └── utils │ │ │ └── utils.js │ └── views │ │ ├── index.html │ │ ├── page1.html │ │ └── partial.html └── webpack │ ├── build │ ├── base.js │ ├── environments.js │ └── index.js │ ├── config │ ├── base.js │ ├── environments.js │ └── index.js │ └── modules │ └── HtmlWebpackMultiBuildPlugin.js ├── package.json ├── rollup.config.js ├── src ├── StartingBlocks.js ├── abstracts │ ├── AbstractBlock.js │ ├── AbstractBlockBuilder.js │ ├── AbstractBootableService.js │ ├── AbstractInViewBlock.js │ ├── AbstractPage.js │ ├── AbstractService.js │ ├── AbstractSplashscreen.js │ ├── AbstractTransition.js │ └── AbstractTransitionFactory.js ├── bundle.js ├── dispatcher │ └── Dispatcher.js ├── errors │ ├── DependencyNotFulfilledException.js │ └── UnknownServiceException.js ├── pages │ └── DefaultPage.js ├── services │ ├── BlockBuilder.js │ ├── CacheProvider.js │ ├── Dom.js │ ├── History.js │ ├── PageBuilder.js │ ├── Pjax.js │ └── Prefetch.js ├── transitions │ └── DefaultTransition.js ├── types │ └── EventTypes.js └── utils │ ├── BootstrapMedia.js │ ├── Logger.js │ ├── Scroll.js │ ├── Utils.js │ ├── debounce.js │ ├── gaTrackErrors.js │ └── polyfills.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "useBuiltIns": "usage" 5 | }] 6 | ], 7 | "plugins": [ 8 | "@babel/plugin-transform-runtime", 9 | "@babel/plugin-syntax-dynamic-import", 10 | "@babel/plugin-syntax-import-meta", 11 | "@babel/plugin-proposal-class-properties", 12 | "@babel/plugin-proposal-json-strings", 13 | [ 14 | "@babel/plugin-proposal-decorators", 15 | { 16 | "legacy": true 17 | } 18 | ], 19 | "@babel/plugin-proposal-function-sent", 20 | "@babel/plugin-proposal-export-namespace-from", 21 | "@babel/plugin-proposal-numeric-separator", 22 | "@babel/plugin-proposal-throw-expressions", 23 | "@babel/plugin-proposal-export-default-from", 24 | "@babel/plugin-proposal-logical-assignment-operators", 25 | "@babel/plugin-proposal-optional-chaining", 26 | [ 27 | "@babel/plugin-proposal-pipeline-operator", 28 | { 29 | "proposal": "minimal" 30 | } 31 | ], 32 | "@babel/plugin-proposal-nullish-coalescing-operator", 33 | "@babel/plugin-proposal-do-expressions", 34 | "@babel/plugin-proposal-function-bind" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Unix-style newlines with a newline ending every file 2 | [*] 3 | indent_style = space 4 | indent_size = 4 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [package.json] 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true 9 | }, 10 | "extends": ["standard"], 11 | "plugins": [ 12 | "html", 13 | "dependencies" 14 | ], 15 | "rules": { 16 | // enable additional rules 17 | "indent": [ 18 | "warn", 19 | 4 20 | ], 21 | "dependencies/case-sensitive": 1, 22 | "dependencies/no-cycles": 1, 23 | "dependencies/require-json-ext": 1 24 | }, 25 | "globals" : { 26 | "DEVELOPMENT": false, 27 | "PRODUCTION": false, 28 | "ENVIRONMENT": false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/app*.js 2 | /doc 3 | /examples/.cache 4 | /examples/dist/js/app.js 5 | /main.js 6 | 7 | # Created by https://www.gitignore.io/api/npm,node 8 | 9 | #!! ERROR: npm is undefined. Use list command to see defined gitignore types !!# 10 | 11 | ### Node ### 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules 43 | jspm_packages 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | yarn.lock 60 | 61 | # End of https://www.gitignore.io/api/npm,node 62 | 63 | 64 | # Created by https://www.gitignore.io/api/phpstorm 65 | 66 | ### PhpStorm ### 67 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 68 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 69 | 70 | # User-specific stuff: 71 | .idea/**/workspace.xml 72 | .idea/**/tasks.xml 73 | .idea/dictionaries 74 | 75 | # Sensitive or high-churn files: 76 | .idea/**/dataSources/ 77 | .idea/**/dataSources.ids 78 | .idea/**/dataSources.xml 79 | .idea/**/dataSources.local.xml 80 | .idea/**/sqlDataSources.xml 81 | .idea/**/dynamic.xml 82 | .idea/**/uiDesigner.xml 83 | 84 | # Gradle: 85 | .idea/**/gradle.xml 86 | .idea/**/libraries 87 | 88 | # CMake 89 | cmake-build-debug/ 90 | 91 | # Mongo Explorer plugin: 92 | .idea/**/mongoSettings.xml 93 | 94 | ## File-based project format: 95 | *.iws 96 | 97 | ## Plugin-specific files: 98 | 99 | # IntelliJ 100 | /out/ 101 | 102 | # mpeltonen/sbt-idea plugin 103 | .idea_modules/ 104 | 105 | # JIRA plugin 106 | atlassian-ide-plugin.xml 107 | 108 | # Cursive Clojure plugin 109 | .idea/replstate.xml 110 | 111 | # Crashlytics plugin (for Android Studio and IntelliJ) 112 | com_crashlytics_export_strings.xml 113 | crashlytics.properties 114 | crashlytics-build.properties 115 | fabric.properties 116 | 117 | ### PhpStorm Patch ### 118 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 119 | 120 | # *.iml 121 | # modules.xml 122 | # .idea/misc.xml 123 | # *.ipr 124 | 125 | # Sonarlint plugin 126 | .idea/sonarlint 127 | 128 | # End of https://www.gitignore.io/api/phpstorm 129 | 130 | # Config & Examples dist 131 | /examples/src/js/config/config.js 132 | /examples/dist/ 133 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 35 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 18 | 19 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/copyright/adrien.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/copyright/rz.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pageblock.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - lts/* 5 | - '10' 6 | - '8' 7 | script: 8 | - cp examples/src/js/config/config.example.js examples/src/js/config/config.js 9 | - echo "Generating bundle.js…" 10 | - npm run build 11 | - npm run demo 12 | skip_cleanup: true 13 | jobs: 14 | include: 15 | - stage: npm release 16 | node_js: "lts/*" 17 | script: 18 | # Copy config for demo 19 | - cp examples/src/js/config/config.example.js examples/src/js/config/config.js 20 | - echo "Generating bundle.js…" 21 | # Only generate bundle, not demo 22 | - npm run build 23 | # Only generate demo 24 | - npm run demo 25 | - echo "Deploying to npm…" 26 | deploy: 27 | on: 28 | tags: true 29 | # Do not reset files to send bundle.js 30 | # to NPMJS 31 | skip_cleanup: true 32 | email: ambroise@rezo-zero.com 33 | provider: npm 34 | api_key: 35 | secure: ghCDQ6wIjCRhYb3pfCMydSETFyJFkZ6B74NnyRFMP/7RtWCFv7K+g9xbvzta4ru/Y1lnM+GVYIpd9DkiwkrOi10foFGqSOnYfqDzwXN/DgdM1sgM2hMwWnh3cXljmJzU3MHJuu/QzxJDeadt3hMBo6GrYh5HXwAwAZ5egrnFcJC2x0szuORtGCFHJxV9dxQGyIw6LTwi9tU7sUY+0BNy/IjiMUDlOvSLIguL7VdZnq2/3o9YU+7EWgpCMSm9nKqdA4ChQ0lCKyom5JBTqZjE8O2krvVMbkEcIi9XBYLy8dbtZXZ1DXoA7HAuUgReVf3b6qra8rMCwbrsO7pzwseLvXcAFq4ZM0ZA1Ty04kZlyOmVJMhxDHjHX1mWuPrMCLau18M/E8qpyeUWArSTb2/hW51OKwqTZ2iSaiZVUmkyYpK8o5gI6OSWzMi/9EPAvZe4hl2e3fnYr7Hn4FCYB58lXGfh299KCZSSNuinAmGcRl5euJk/ZLKmpbGaLFJMulhNQcNnxMFsK/2q5m9Uqgv6UM+vF3IuKrWKyanR0PnPedHMhqo/6jWqIAcZ+kwXvC965y0xHFQpElZ45u36NSoqmQTMTr0wM+R6RTgT5808LviegHzSbMgXdDj9rweQphkWK2XnNTsLlFooQSdiXwBmXWgM+VRhPmVLD7eCS9vnI6w= 36 | 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## version 5.0.0 2018-03-06 4 | 5 | - Drop jquery and waitForImages 6 | - Remove `onLoad` method in `AbstractPage` and `AbstractBlock` 7 | 8 | ## Version 4.2.0: 2018-03-06 9 | 10 | - Change `classFactory.getBlockInstance()` method parameters order (`page, $cont, nodeType`). Make sure to update your `classFactory` file 11 | - You can now switch `classFactory.getBlockInstance()` to `async` and dynamically load your modules with `import('yourModule').then()`, see example for more details. 12 | - Optional web worker feature to load pages content. Add `workerEnabled` router option to true to enable 13 | 14 | ## Version 4.1.0: 2018-02-20 15 | 16 | - Change `classFactory.getPageInstance()` method parameters order and removed `isHome` parameter 17 | - Added new `data` object parameter in history `add` method if you want to manually add specific data 18 | 19 | ## Version 4.0.0: 2018-02-08 20 | 21 | - Big refactoring of all classes 22 | - No more need to bind manually all **links** in Pages, Blocks or Nav, everything is handled by *Pjax* class 23 | - Removed all Router constructor arguments except for `options` array 24 | - `AbstractNav` has been removed, do not extend it anymore. 25 | - Starting-blocks wrapper default ID is now `sb-wrapper`, you can change it in your `Router` options (i.e. `ajaxWrapperId: 'main-container'`) 26 | - *jquery*, *loglevel* and *jquery.waitforimages* are declared external. You should provide them manually as file or as CDN in your project. 27 | - *Pjax* is now using native `window.fetch` methods to perform XHR requests. Make sure to use necessary polyfills. **If window.fetch is not supported, ajax will be disabled.** 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rezo Zero 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Make commands for page block 3 | # 4 | 5 | # Ignore existing folders for these commands 6 | .PHONY: build doc update clean push-doc uninstall 7 | 8 | # 9 | # Default task 10 | # 11 | all: install build 12 | 13 | install : node_modules 14 | 15 | update : 16 | npm update; 17 | 18 | uninstall : clean 19 | rm -rf node_modules; 20 | 21 | watch : 22 | npm run dev; 23 | 24 | build : 25 | NODE_ENV=production TARGET=all npm run build; 26 | # 27 | # Clean generated files 28 | # 29 | clean : 30 | rm -rf doc; 31 | # 32 | # Generate documentation 33 | # 34 | doc : 35 | npm run doc; 36 | # 37 | # Push generated Documentation on Rezo Zero host. 38 | # Credentials required, of course 39 | push-doc : doc 40 | rsync -avcz -e "ssh -p 39001" doc/ core@startingblocks.rezo-zero.com:~/http/; 41 | 42 | node_modules: 43 | npm install; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starting Blocks 2 | 3 | *Starting Blocks* is a framework for ajax page, transitions and blocks written in vanilla JS, 4 | made by [Rezo Zero](https://www.rezo-zero.com/). 5 | 6 | [![npm](https://img.shields.io/npm/l/starting-blocks.svg)](https://www.npmjs.com/package/starting-blocks) 7 | [![npm](https://img.shields.io/npm/v/starting-blocks.svg)](https://www.npmjs.com/package/starting-blocks) 8 | [![Build Status](https://travis-ci.org/rezozero/starting-blocks.svg?branch=master)](https://travis-ci.org/rezozero/starting-blocks) 9 | 10 | * [Features](#features) 11 | * [Install with Yarn or NPM](#install-with-yarn-or-npm) 12 | * [Usage](#usage) 13 | * [Services](#services) 14 | - [Pjax](#pjax) 15 | - [Splashscreen](#splashscreen) 16 | - [TransitionFactory](#transitionfactory) 17 | * [DOM structure](#dom-structure) 18 | * [Page](#page) 19 | - [Page overridable methods](#page-overridable-methods) 20 | * [Block](#block) 21 | - [Common block overridable methods](#common-block-overridable-methods) 22 | - [In view block overrivable methods](#in-view-block-overrivable-methods) 23 | * [Options](#options) 24 | * [Events](#events) 25 | * [Docs](#docs) 26 | * [Naming conventions](#naming-conventions) 27 | * [Improving Starting Blocks](#improving-starting-blocks) 28 | * [Compatibility](#compatibility) 29 | * [Demo](#demo) 30 | * [Go further with Starting Blocks](#go-further-with-starting-blocks) 31 | * [Contributors](#contributors) 32 | 33 | ## Features 34 | 35 | - *Pjax* : fetch pages and handling `History` state 36 | - *Transition manager* : enhance your website with custom page transitions 37 | - *Page/Blocks* pattern : build dynamic and interactive sections (map, slideshow, Ajax forms, canvas…) 38 | 39 | And more... 40 | 41 | - In view detection 42 | - Splashscreen 43 | - Prefetch 44 | - Cache 45 | 46 | ## Install with Yarn or NPM 47 | 48 | ```shell 49 | yarn add starting-blocks 50 | ``` 51 | 52 | ```shell 53 | npm install starting-blocks --save 54 | ``` 55 | 56 | ## Usage 57 | 58 | *Starting Blocks* is bundled with *NPM* in order to use native *ES6* `import` syntax. 59 | We highly recommend to use a module bundler and to write your code in *ES6* syntax. If you use a bundler like 60 | ***Webpack***, it will be able to remove *dead code* (i.e. when a *Starting Blocks* service is not used) if you use *curly brace* syntax. 61 | 62 | Minimal configuration : 63 | 64 | ```js 65 | import StartingBlocks from 'starting-blocks' 66 | import HomePage from './pages/HomePage' 67 | import UsersBlock from './blocks/UsersBlock' 68 | 69 | // Create a new Starting Blocks instance 70 | const startingBlocks = new StartingBlocks({ 71 | // ...options 72 | }) 73 | 74 | // For each page or block sections: you must map the service name 75 | // with the data-node-type attribute (see DOM Structure section). 76 | // «c» parameter stands for Starting-Blocks service container 77 | 78 | // Register page services factory 79 | startingBlocks.instanceFactory('HomePage', c => { 80 | return new HomePage(c) 81 | }) 82 | 83 | // Register block services factory 84 | startingBlocks.instanceFactory('UsersBlock', c => { 85 | return new UsersBlock(c) 86 | }) 87 | 88 | // 🚀 Boot the whole thing 89 | startingBlocks.boot() 90 | ``` 91 | 92 | ## Services 93 | 94 | *Starting Blocks* use a dependency injection container to delegate object creation and handle dependencies. 95 | You can use or provide these standard services to enhance your website : 96 | 97 | | Service name | Init type | Ready to use | Dependencies | Description 98 | | --- | --- | --- | --- | --- | 99 | | `Pjax` | `bootableProvider` | true | `History` | Enable Ajax navigation on all your website internal links 100 | | `History` | `provider` | true | | | Helps to keep track of the navigation state 101 | | `Prefetch` | `bootableProvider` | true | `Pjax` | Prefetch links on mouse enter (useful with `Pjax`) 102 | | `CacheProvider` | `provider` | true | | Cache ajax requests (useful with `Pjax`) 103 | | `Splashscreen` | `bootableProvider` | false | | Add a splash screen for the first init 104 | | `TransitionFactory` | `bootableProvider` | false | | Instantiate page transitions objects according to your *Pjax* context 105 | 106 | #### Pjax 107 | 108 | ```js 109 | import StartingBlocks, { History, Pjax } from 'starting-blocks' 110 | 111 | // ...Instantiate starting blocks 112 | 113 | // Add service 114 | startingBlocks.provider('History', History) 115 | startingBlocks.bootableProvider('Pjax', Pjax) 116 | 117 | // ...Boot 118 | ``` 119 | 120 | ⚠️ Don't forget to prepare your DOM adding specific *data attributes* and required *classes*, see [DOM structure section](#dom-structure) 121 | 122 | #### Splashscreen 123 | 124 | We implemented a *Splashscreen* service that is triggered at the first website launch. 125 | Create your own class that extends our `AbstractSplashscreen`: 126 | 127 | ```js 128 | import { AbstractSplashscreen, Dispatcher, EventTypes } from 'starting-blocks' 129 | 130 | export default class Splashscreen extends AbstractSplashscreen { 131 | constructor (container) { 132 | super(container, 'Splashscreen') 133 | //... custom values 134 | } 135 | 136 | // You need to override this method 137 | hide () { 138 | return new Promise(resolve => { 139 | // custom logic, animations... 140 | resolve() 141 | }) 142 | } 143 | } 144 | ``` 145 | 146 | ```js 147 | import Splashscreen from './Splashscreen' 148 | 149 | // ...Instantiate starting blocks 150 | 151 | // Add service 152 | startingBlocks.bootableProvider('Splashscreen', Splashscreen) 153 | 154 | // ...Boot 155 | ``` 156 | 157 | #### TransitionFactory 158 | 159 | When AJAX navigation is enabled, transitions are triggered to manage animations between pages. 160 | To choose a custom transition, you can set `data-transition` attribute with a transition name on each link: 161 | ```html 162 | Contact 163 | ``` 164 | 165 | Then, create a `TransitionFactory` class and register it into **Starting Blocks** as a service. 166 | In this class, you must implement `getTransition (previousState, state)` method. 167 | This method will be called on each transition and will give you access to `history` state informations: 168 | 169 | - `previousState` and `state` 170 | - **transitionName** : `data-transition` attributes of the clicked link 171 | - **context** : equal to `"history"`, `"link"` 172 | 173 | Example: 174 | 175 | ```javascript 176 | // src/factories/TransitionFactory.js 177 | import DefaultTransition from './transitions/DefaultTransition'; 178 | import FadeTransition from './transitions/FadeTransition'; 179 | 180 | export default class TransitionFactory { 181 | getTransition (previousState, state) { 182 | switch (state.transitionName) { 183 | case 'fade': 184 | return new FadeTransition() 185 | default: 186 | return new DefaultTransition() 187 | } 188 | } 189 | } 190 | ``` 191 | 192 | How to register your Transition factory service? 193 | 194 | ```js 195 | import TransitionFactory from '../factories/TransitionFactory' 196 | 197 | // ...Instantiate starting blocks 198 | 199 | // Add service 200 | startingBlocks.provider('TransitionFactory', TransitionFactory) 201 | 202 | // ...Boot 203 | ``` 204 | 205 | To create a new transition you need to write a new class extending our `AbstractTransition` boilerplate. Implement `start()` method and use `Promises` to manage your animation timeline. 206 | ⚠️ Be careful, you have to wait for `this.newPageLoading` Promise resolution to make sure the new page DOM is ready. 207 | Then, you have access to old and new `Page` instances. 208 | 209 | Example with *fade* animation (we use *TweenLite* from [gsap](https://github.com/greensock/GreenSock-JS) for this example): 210 | 211 | ```javascript 212 | // src/transitions/FadeTransition.js 213 | import AbstractTransition from '../AbstractTransition' 214 | import { TweenLite } from 'gsap' 215 | 216 | /** 217 | * Fade Transition example. Fade Out / Fade In between old and new pages. 218 | */ 219 | export default class FadeTransition extends AbstractTransition { 220 | /** 221 | * Entry point of the animation 222 | * Automatically called on init() 223 | */ 224 | start () { 225 | // Wait new content and the end of fadeOut animation 226 | // this.newPageLoading is a Promise which is resolved when the new content is loaded 227 | Promise.all([this.newPageLoading, this.fadeOut()]) 228 | // then fadeIn the new content 229 | .then(this.fadeIn.bind(this)) 230 | } 231 | 232 | /** 233 | * Fade out the old content. 234 | * @returns {Promise} 235 | */ 236 | fadeOut () { 237 | return new Promise(resolve => { 238 | TweenLite.to(this.oldPage.rootElement, 0.4, { 239 | alpha: 0, 240 | onComplete: resolve 241 | }) 242 | }) 243 | } 244 | 245 | /** 246 | * Fade in the new content 247 | */ 248 | fadeIn () { 249 | // Add display: none on the old container 250 | this.oldPage.rootElement.style.display = 'none' 251 | 252 | // Prepare new content css properties for the fade animation 253 | this.newPage.rootElement.style.visibility = 'visible' 254 | this.newPage.rootElement.style.opacity = '0' 255 | 256 | // Scroll to the top 257 | document.documentElement.scrollTop = 0 258 | document.body.scrollTop = 0 259 | 260 | // fadeIn the new content container 261 | TweenLite.to(this.newPage.rootElement, 0.4, { 262 | autoAlpha: 1, 263 | onComplete: () => { 264 | // IMPORTANT: Call this method at the end 265 | this.done() 266 | } 267 | }) 268 | } 269 | } 270 | ``` 271 | 272 | ## DOM structure 273 | 274 | This ES6 javascript framework has been designed to handle either complete HTML responses or 275 | *partial* HTML responses to lighten backend process and bandwidth. 276 | One of the most useful Page and Block property is `rootElement` that will always refer to the current page/block main-section. 277 | 278 | **In a page context,** `rootElement` is the DOM element which is extracted at the end of each completed AJAX requests. 279 | When it detects that HTTP response is *partial*, it initializes `rootElement` using the whole response content. Every new DOM content will be appended to the `#sb-wrapper` HTML section in order to enable cross-transitions between old and new content. 280 | 281 | **In a block context,** `rootElement` will store the DOM element with `[data-node-type]` attribute. 282 | 283 | When `Pjax` service is used and `window.fetch` supported, **all links inside document body** are listened to fetch pages asynchronously and make transitions. 284 | 285 | To declare a partial DOM section as the `rootElement` you must add some classes and 286 | data to your HTML tags. 287 | 288 | ```html 289 | 290 |
296 | 297 | 298 | 303 |
304 | ``` 305 | 306 | - `id` *attribute* is obviously mandatory as it will be used to update your navigation and some other parts of your website. **Make sure that your ID is not the same as to your `data-node-name`.** 307 | - `page-content` *class* is essential in order to extract your DOM element during AJAX request. You can customize this class name in [`StartingBlock` options](#options) (`pageClass: "page-content"`). 308 | - `data-node-type` *attribute* will be used to map your element to the matching JS class (in this example: `HomePage.js`). **Every class must extend the `AbstractPage` or `AbstractBlock` class**. Then you have to declare your pages and blocks services in your *Starting Blocks* instance via `instanceFactory` method. 309 | - `data-node-name` is used to name your page object **and to rename body class and ID after it.** 310 | - `data-is-home`: 0 or 1 311 | - `data-meta-title` *attribute* will be used to change your new page *title* (`document.title`), it can be used in other cases such as some social network modules which require a clean page-title. 312 | 313 | You’ll find `index.html` and `page1.html` examples files. You can even test them 314 | by spawning a simple server with `npm run serve` command. 315 | Then go to your favorite browser and type `http://localhost:8080`. 316 | 317 | ## Page 318 | 319 | Each custom page needs to extend our `AbstractPage` class to be registered as a service in your *Starting Blocks* instance. 320 | Be careful that `data-node-type` attribute matches with your service declaration. 321 | By default *Starting Blocks* will instantiate the `DefaultPage` class if no `data-node-type` attribute is found. 322 | 323 | **Best practice** : Create your own `DefaultPage` with your common features then override the default 324 | service to use it as a common base for your custom pages : 325 | 326 | ```js 327 | import { AbstractPage } from 'starting-blocks' 328 | 329 | export default class CustomDefaultPage extends AbstractPage { 330 | //... common methods 331 | } 332 | 333 | // Custom page example 334 | export default class HomePage extends CustomDefaultPage { 335 | //... home page custom methods 336 | } 337 | ``` 338 | 339 | ```js 340 | import CustomDefaultPage from './pages/CustomDefaultPage' 341 | 342 | // ...Instantiate starting blocks 343 | 344 | // Override the DefaultPage service with your own 345 | startingBlocks.instanceFactory('DefaultPage', c => { 346 | return new CustomDefaultPage(c) 347 | }) 348 | // ...Boot 349 | ``` 350 | 351 | #### Page overridable methods 352 | 353 | | Method | Description | 354 | | --- | --- | 355 | | `onResize` | On viewport resize, this method is debounced. | 356 | 357 | ## Block 358 | 359 | A block is a section of your page. It can be a `Slideshow`, an ajax form, a map... 360 | *Starting Blocks* automatically maps those DOM elements with a custom ES6 class in the same way the future [`CustomElementRegistry`](https://developer.mozilla.org/fr/docs/Web/API/CustomElementRegistry) will perform. 361 | Create your own class extending our `AbstractBlock` or `AbstractInViewBlock` then register them as a service. 362 | `data-node-type` attribute content and your service name must match. 363 | 364 | #### Common block overridable methods in AbstractBlock 365 | 366 | | Method | Description | 367 | | --- | --- | 368 | | `onResize` | On viewport resize, this method is *debounced* of 50ms. | 369 | | `onPageReady` | Triggered once all page blocks have been created. | 370 | 371 | #### In-view block overridable methods in AbstractInViewBlock 372 | 373 | | Method | Description | 374 | | --- | --- | 375 | | `onIntersectionCallback` | Triggered when in view block state changed (in or out). | 376 | | `onScreen` | Called when block is **in** the viewport. | 377 | | `offScreen` | Called when block is **out** of the viewport. | 378 | 379 | `AbstractInViewBlock` extends `AbstractBlock` and thus implements each of its methods too. 380 | 381 | ## Options 382 | 383 | You can pass some options when instantiating `StartingBlocks` object: 384 | 385 | | Parameter | Type | Default | Description | 386 | | --- | --- | --- | --- | 387 | | wrapperId | string | 'sb-wrapper' | 388 | | pageBlockClass | string | 'page-block' | 389 | | pageClass | string | 'page-content' | 390 | | objectTypeAttr | string | 'data-node-type' | 391 | | noAjaxLinkClass | string | 'no-ajax-link' | 392 | | noPrefetchClass | string | 'no-prefetch' | 393 | | manualDomAppend | boolean | false | To manually manage page build directly in transition instances 394 | 395 | ## Events 396 | 397 | | Const name | Event name | Description | 398 | | --- | --- | --- | 399 | | `BEFORE_PAGE_LOAD` | `SB_BEFORE_PAGE_LOAD` | Before Router initialize XHR request to load new page. | 400 | | `AFTER_PAGE_LOAD` | `SB_AFTER_PAGE_LOAD` | After `window.fetch` XHR request succeeded. | 401 | | `AFTER_DOM_APPENDED` | `SB_AFTER_DOM_APPENDED` | After Router appended new page DOM to `wrapperId`. | 402 | | `AFTER_PAGE_BOOT` | `SB_AFTER_PAGE_BOOT` | After Router create new page instance. | 403 | | `CONTAINER_READY` | `SB_CONTAINER_READY` | | 404 | | `TRANSITION_START` | `SB_TRANSITION_START` | | 405 | | `TRANSITION_COMPLETE` | `SB_TRANSITION_COMPLETE` | | 406 | | `BEFORE_SPLASHSCREEN_HIDE` | `SB_BEFORE_SPLASHSCREEN_HIDE` | | 407 | | `AFTER_SPLASHSCREEN_HIDE` | `SB_AFTER_SPLASHSCREEN_HIDE` | | 408 | 409 | ## Docs 410 | 411 | To generate documentation, you’ll at least NodeJS v4.4 and to install ESDoc. 412 | 413 | ```bash 414 | npm run doc; 415 | ``` 416 | 417 | Documentation will be available in `doc/` folder. 418 | 419 | ## Naming conventions 420 | 421 | We dropped *jQuery* in Starting-blocks v5 and we changed several variables names. 422 | We suffixed every `DOMElement` variable with `Element`, `Container`, `Elements` or `Containers`. 423 | 424 | Examples: 425 | 426 | ```js 427 | let mainContainer = document.getElementById('main-container') 428 | let imageContainer = document.getElementById('image-container') 429 | let imageElements = imageContainer.querySelectorAll('.image') 430 | ``` 431 | 432 | ## Improving Starting Blocks 433 | 434 | To work locally on *Starting Blocks*, you’ll find some HTML files in `examples/` folder. 435 | 436 | - Install dependencies: `yarn`. 437 | - Type `npm run dev` to improve *Starting Blocks* locally. 438 | - Type `npm run build` to optimize project in one file as: `main.js`. 439 | - Type `npm run demo` to build demo project in `examples/` folder. 440 | 441 | ## Compatibility 442 | 443 | *Starting Blocks* use native `Promise`, `fetch`, `IntersectionObserver` and `MutationObserver` browser features. 444 | **Don't forget to use some polyfills for old browsers.** 445 | 446 | ## Demo 447 | 448 | To launch the example you need to change the `examples/srv/js/config/config.example.js` file with your own informations. 449 | 450 | ## Go further with Starting Blocks 451 | 452 | If you use *Webpack* you will be able to dynamically lazy-load your blocks with a custom `BlockBuilder`. 453 | Create a custom *BlockBuilder* and override the default one: 454 | 455 | ```js 456 | import { AbstractBlockBuilder } from 'starting-blocks' 457 | 458 | export default class WebpackAsyncBlockBuilder extends AbstractBlockBuilder { 459 | // Dynamic import 460 | async getBlockInstance (nodeTypeName) { 461 | try { 462 | const Block = await this.getModule(nodeTypeName) 463 | 464 | if (!this.hasService(nodeTypeName)) { 465 | this.container.$register({ 466 | $name: nodeTypeName, 467 | $type: 'instanceFactory', 468 | $value: c => { 469 | return new Block(c) 470 | } 471 | }) 472 | } 473 | 474 | return this.getService(nodeTypeName).instance() 475 | } catch (e) { 476 | console.error(e.message) 477 | return null 478 | } 479 | } 480 | 481 | async getModule (nodeTypeName) { 482 | return import(`../blocks/${nodeTypeName}` /* webpackChunkName: "block-" */) 483 | .then(block => { 484 | return block.default 485 | }) 486 | } 487 | } 488 | ``` 489 | 490 | Then override the default `PageBuilder` service: 491 | 492 | ```js 493 | // Custom block builder (dynamic import) 494 | startingBlocks.provider('BlockBuilder', WebpackAsyncBlockBuilder) 495 | ``` 496 | 497 | ## Contributors 498 | 499 | - [Adrien Scholaert](https://github.com/Gouterman) 500 | - [Ambroise Maupate](https://github.com/ambroisemaupate) 501 | - [Quentin Neyraud](https://github.com/quentinneyraud) 502 | - [Maxime Bérard](https://github.com/maximeberard) 503 | -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "excludes": ["bundle.js"], 4 | "destination": "./doc", 5 | "plugins": [ 6 | {"name": "esdoc-exclude-source-plugin"}, 7 | {"name": "esdoc-standard-plugin"}, 8 | { 9 | "name": "esdoc-ecmascript-proposal-plugin", 10 | "option": { 11 | "classProperties": true, 12 | "objectRestSpread": true, 13 | "doExpressions": true, 14 | "functionBind": true, 15 | "functionSent": true, 16 | "asyncGenerators": true, 17 | "decorators": true, 18 | "exportExtensions": true, 19 | "dynamicImport": true 20 | } 21 | }, 22 | { 23 | "name": "esdoc-brand-plugin", 24 | "option": { 25 | "title": "Starting Blocks JS framework", 26 | "description": "A page transition and blocks ES6 framework by REZO ZERO", 27 | "repository": "https://github.com/rezozero/starting-blocks", 28 | "site": "https://startingblocks.rezo-zero.com/", 29 | "author": "https://www.rezo-zero.com/" 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /examples/src/css/demo.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | html, 7 | body { 8 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 9 | } 10 | 11 | body { 12 | padding-top: 75px; 13 | margin-bottom: 60px; 14 | } 15 | 16 | .box-shadow { 17 | box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, .05); 18 | } 19 | 20 | .text-white-50 { 21 | color: rgba(255, 255, 255, .5); 22 | } 23 | 24 | /** Users Blocks **/ 25 | .usersblock .avatar-cont { 26 | margin-right: 10px; 27 | } 28 | 29 | .usersblock .avatar-cont, 30 | .usersblock .avatar-cont img { 31 | width: 48px; 32 | height: 48px; 33 | } 34 | 35 | .usersblock__intro { 36 | color: #000; 37 | } 38 | 39 | #splashscreen { 40 | position: fixed; 41 | z-index: 1040; 42 | top: 0; 43 | right: 0; 44 | bottom: 0; 45 | left: 0; 46 | display: flex; 47 | align-content: center; 48 | align-items: center; 49 | justify-content: center; 50 | } 51 | 52 | .splashscreen-inner { 53 | position: relative; 54 | z-index: 11; 55 | display: block; 56 | } 57 | 58 | .splashscreen-bg { 59 | position: absolute; 60 | z-index: 10; 61 | top: 0; 62 | right: 0; 63 | bottom: 0; 64 | left: 0; 65 | background-color: #eeeeee; 66 | } 67 | 68 | .splashscreen-text { 69 | position: relative; 70 | z-index: 11; 71 | font-size: 16px; 72 | margin-top: 150px; 73 | letter-spacing: 5px; 74 | } 75 | 76 | .spinner { 77 | position: absolute; 78 | z-index: 12; 79 | top: 50%; 80 | left: 50%; 81 | width: 66px; 82 | height: 66px; 83 | margin-top: -33px; 84 | margin-left: -33px; 85 | -webkit-animation: contanim 2s linear infinite; 86 | animation: contanim 2s linear infinite; 87 | } 88 | 89 | .spinner svg { 90 | display: block; 91 | margin: 0; padding: 0; 92 | width: 100%; 93 | height: 100%; 94 | left: 0; 95 | top: 0; 96 | position: absolute; 97 | -webkit-transform: rotate(-90deg); 98 | transform: rotate(-90deg); 99 | } 100 | 101 | .spinner svg:nth-child(1) circle { 102 | stroke: #84EBBD; 103 | stroke-dasharray: 1, 300; 104 | stroke-dashoffset: 0; 105 | -webkit-animation: strokeanim 3s calc(.2s * (1)) ease infinite; 106 | animation: strokeanim 3s calc(.2s * (1)) ease infinite; 107 | -webkit-transform-origin: center center; 108 | transform-origin: center center; 109 | } 110 | 111 | .spinner svg:nth-child(2) circle { 112 | stroke: #4977EC; 113 | stroke-dasharray: 1, 300; 114 | stroke-dashoffset: 0; 115 | -webkit-animation: strokeanim 3s calc(.2s * (2)) ease infinite; 116 | animation: strokeanim 3s calc(.2s * (2)) ease infinite; 117 | -webkit-transform-origin: center center; 118 | transform-origin: center center; 119 | } 120 | 121 | .spinner svg:nth-child(3) circle { 122 | stroke: #F6BB67; 123 | stroke-dasharray: 1, 300; 124 | stroke-dashoffset: 0; 125 | -webkit-animation: strokeanim 3s calc(.2s * (3)) ease infinite; 126 | animation: strokeanim 3s calc(.2s * (3)) ease infinite; 127 | -webkit-transform-origin: center center; 128 | transform-origin: center center; 129 | } 130 | 131 | .spinner svg:nth-child(4) circle { 132 | stroke: #333841; 133 | stroke-dasharray: 1, 300; 134 | stroke-dashoffset: 0; 135 | -webkit-animation: strokeanim 3s calc(.2s * (4)) ease infinite; 136 | animation: strokeanim 3s calc(.2s * (4)) ease infinite; 137 | -webkit-transform-origin: center center; 138 | transform-origin: center center; 139 | } 140 | 141 | @-webkit-keyframes strokeanim { 142 | 0% { 143 | stroke-dasharray: 1, 300; 144 | stroke-dashoffset: 0; 145 | } 146 | 50% { 147 | stroke-dasharray: 120, 300; 148 | stroke-dashoffset: -58.548324585; 149 | } 150 | 100% { 151 | stroke-dasharray: 120, 300; 152 | stroke-dashoffset: -175.6449737549; 153 | } 154 | } 155 | 156 | @keyframes strokeanim { 157 | 0% { 158 | stroke-dasharray: 1, 300; 159 | stroke-dashoffset: 0; 160 | } 161 | 50% { 162 | stroke-dasharray: 120, 300; 163 | stroke-dashoffset: -58.548324585; 164 | } 165 | 100% { 166 | stroke-dasharray: 120, 300; 167 | stroke-dashoffset: -175.6449737549; 168 | } 169 | } 170 | 171 | @-webkit-keyframes contanim { 172 | 100% { 173 | -webkit-transform: rotate(360deg); 174 | transform: rotate(360deg); 175 | } 176 | } 177 | 178 | @keyframes contanim { 179 | 100% { 180 | -webkit-transform: rotate(360deg); 181 | transform: rotate(360deg); 182 | } 183 | } 184 | 185 | /** Big transition **/ 186 | #big-transition { 187 | position: fixed; 188 | z-index: 2000; 189 | top: 0; 190 | left: 0; 191 | right: 0; 192 | bottom: 0; 193 | display: flex; 194 | opacity: 0; 195 | visibility: hidden; 196 | } 197 | 198 | #big-transition #big-transition-vertical, 199 | #big-transition #big-transition-horizontal { 200 | position: absolute; 201 | top: 0; 202 | left: 0; 203 | right: 0; 204 | bottom: 0; 205 | display: flex; 206 | } 207 | 208 | #big-transition #big-transition-vertical > div { 209 | height: 100%; 210 | flex: 1; 211 | background: #eee; 212 | transform-origin: 0 0; 213 | transform: scaleY(0); 214 | } 215 | 216 | #big-transition #big-transition-horizontal { 217 | flex-flow: column; 218 | } 219 | 220 | #big-transition #big-transition-horizontal > div { 221 | width: 100%; 222 | flex: 1 1; 223 | background: #8f8f8f; 224 | transform-origin: 0 0; 225 | transform: scaleX(0); 226 | } 227 | 228 | /** Footer **/ 229 | .footer { 230 | position: absolute; 231 | bottom: 0; 232 | width: 100%; 233 | height: 60px; 234 | line-height: 60px; 235 | background-color: #f5f5f5; 236 | } 237 | -------------------------------------------------------------------------------- /examples/src/img/test1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezozero/starting-blocks/9ebd53c8527e7ee6bade1b7beef48044d430c24a/examples/src/img/test1.jpg -------------------------------------------------------------------------------- /examples/src/img/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezozero/starting-blocks/9ebd53c8527e7ee6bade1b7beef48044d430c24a/examples/src/img/test2.jpg -------------------------------------------------------------------------------- /examples/src/img/test2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezozero/starting-blocks/9ebd53c8527e7ee6bade1b7beef48044d430c24a/examples/src/img/test2.webp -------------------------------------------------------------------------------- /examples/src/img/test3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezozero/starting-blocks/9ebd53c8527e7ee6bade1b7beef48044d430c24a/examples/src/img/test3.jpg -------------------------------------------------------------------------------- /examples/src/img/test4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezozero/starting-blocks/9ebd53c8527e7ee6bade1b7beef48044d430c24a/examples/src/img/test4.jpg -------------------------------------------------------------------------------- /examples/src/img/test5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezozero/starting-blocks/9ebd53c8527e7ee6bade1b7beef48044d430c24a/examples/src/img/test5.jpg -------------------------------------------------------------------------------- /examples/src/js/ExampleNav.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018. 3 | * @file ExampleNav.js 4 | * @author Adrien Scholaert 5 | */ 6 | 7 | import { 8 | EventTypes 9 | } from 'starting-blocks' 10 | 11 | /** 12 | * An example nav which binds links for AJAX use. 13 | */ 14 | export default class ExampleNav { 15 | constructor () { 16 | /** 17 | * @type {HTMLElement | null} 18 | */ 19 | this.container = document.getElementById('main-nav') 20 | 21 | /** 22 | * @type {Array} 23 | */ 24 | this.linkElements = [...this.container.querySelectorAll('a')] 25 | 26 | // Binded methods 27 | this.onAfterPageBoot = this.onAfterPageBoot.bind(this) 28 | } 29 | 30 | init () { 31 | this.initEvents() 32 | } 33 | 34 | initEvents () { 35 | window.addEventListener(EventTypes.AFTER_PAGE_BOOT, this.onAfterPageBoot) 36 | } 37 | 38 | onAfterPageBoot () { 39 | // Remove all active class 40 | for (const linkElement of this.linkElements) { 41 | linkElement.classList.remove('active') 42 | 43 | if (linkElement.href === window.location.href) { 44 | linkElement.classList.add('active') 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/src/js/api/Api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file Api.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | import config from '../config/config' 30 | 31 | export async function getData (sourceUrl, params = {}) { 32 | const url = new URL(sourceUrl) 33 | const mergedParams = { ...params, access_token: config.token } 34 | 35 | Object.keys(mergedParams).forEach(key => url.searchParams.append(key, mergedParams[key])) 36 | 37 | const result = await window.fetch(url.toString()) 38 | const json = await result.json() 39 | 40 | return json 41 | } 42 | -------------------------------------------------------------------------------- /examples/src/js/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file app.js 5 | * @author Adrien Scholaert 6 | * @author Ambroise Maupate 7 | */ 8 | 9 | import StartingBlocks, { 10 | Pjax, 11 | History, 12 | Prefetch, 13 | CacheProvider, 14 | polyfills 15 | } from 'starting-blocks' 16 | import WebpackAsyncBlockBuilder from './services/WebpackAsyncBlockBuilder' 17 | import Splashscreen from './services/Splashscreen' 18 | import TransitionFactory from './factories/TransitionFactory' 19 | import HomePage from './pages/HomePage' 20 | import ExampleNav from './ExampleNav' 21 | import 'gsap/CSSPlugin' 22 | import 'lazysizes' 23 | 24 | (() => { 25 | // BEING IMPORTANT (Bug Safari 10.1) 26 | // DO NOT REMOVE 27 | if (window.MAIN_EXECUTED) { 28 | throw new Error('Safari 10') 29 | } 30 | 31 | window.MAIN_EXECUTED = true 32 | // END IMPORTANT 33 | 34 | /** 35 | * Declare polyfills 36 | */ 37 | polyfills() 38 | 39 | /** 40 | * Build nav 41 | * @type {ExampleNav} 42 | */ 43 | const nav = new ExampleNav() 44 | 45 | // console.log(StartingBlocks) 46 | 47 | /** 48 | * Build a new starting blocks 49 | */ 50 | const startingBlocks = new StartingBlocks({ 51 | manualDomAppend: true, 52 | debug: 1 53 | }) 54 | 55 | // Add services 56 | startingBlocks.provider('TransitionFactory', TransitionFactory) 57 | startingBlocks.provider('History', History) 58 | startingBlocks.provider('CacheProvider', CacheProvider) 59 | 60 | // Custom block builder (dynamic import) 61 | startingBlocks.provider('BlockBuilder', WebpackAsyncBlockBuilder) 62 | 63 | // Add bootable services 64 | startingBlocks.bootableProvider('Prefetch', Prefetch) 65 | startingBlocks.bootableProvider('Pjax', Pjax) 66 | startingBlocks.bootableProvider('Splashscreen', Splashscreen) 67 | 68 | // Register pages 69 | startingBlocks.instanceFactory('HomePage', c => { 70 | return new HomePage(c) 71 | }) 72 | 73 | // If you want to use standard block import 74 | // startingBlocks.instanceFactory('UsersBlock', c => { 75 | // return new UsersBlock(c) 76 | // }) 77 | 78 | nav.init() 79 | startingBlocks.boot() 80 | })() 81 | -------------------------------------------------------------------------------- /examples/src/js/blocks/DefaultBlock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file DefaultBlock.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | import { AbstractBlock } from 'starting-blocks' 30 | 31 | export default class DefaultBlock extends AbstractBlock { 32 | init () { 33 | return super.init() 34 | } 35 | 36 | initEvents () { 37 | return super.initEvents() 38 | } 39 | 40 | destroy () { 41 | return super.destroy() 42 | } 43 | 44 | destroyEvents () { 45 | return super.destroyEvents() 46 | } 47 | 48 | onResize () { 49 | return super.onResize() 50 | } 51 | 52 | onPageReady () { 53 | return super.onPageReady() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/src/js/blocks/InViewBlock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file InViewBlock.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import { AbstractInViewBlock } from 'starting-blocks' 9 | import { TweenMax, Power4 } from 'gsap' 10 | 11 | export default class InViewBlock extends AbstractInViewBlock { 12 | constructor (container) { 13 | super(container, 'InViewBlock') 14 | 15 | // Prepare values 16 | this.imgs = [] 17 | this.direction = 'right' 18 | this.xPercent = 30 19 | 20 | this.observerOptions = { 21 | ...this.observerOptions, 22 | threshold: 0.1 23 | } 24 | } 25 | 26 | init () { 27 | super.init() 28 | 29 | if (this.rootElement.hasAttribute('data-direction')) { 30 | this.direction = this.rootElement.getAttribute('data-direction') 31 | } 32 | 33 | if (this.direction === 'left') { 34 | this.xPercent = -this.xPercent 35 | } 36 | 37 | // Find elements 38 | this.imgs = [...this.rootElement.querySelectorAll('img')] 39 | } 40 | 41 | initEvents () { 42 | super.initEvents() 43 | 44 | for (const img of this.imgs) { 45 | img.addEventListener('load', () => { 46 | console.log('img loaded') 47 | }) 48 | } 49 | } 50 | 51 | onScreen (entry) { 52 | TweenMax.to(this.rootElement, 1.5, { 53 | xPercent: 0, 54 | alpha: 1, 55 | delay: 0.15, 56 | ease: Power4.easeOut 57 | }) 58 | } 59 | 60 | offScreen () { 61 | TweenMax.set(this.rootElement, { 62 | xPercent: this.xPercent, 63 | alpha: 0 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/src/js/blocks/UsersBlock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file UsersBlock.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | import { AbstractBlock } from 'starting-blocks' 30 | import * as Api from '../api/Api' 31 | import * as Utils from '../utils/utils' 32 | 33 | export default class UsersBlock extends AbstractBlock { 34 | constructor (container) { 35 | super(container, 'UsersBlock') 36 | 37 | // Elements 38 | this.avatarContainer = null 39 | this.contributorsListingContainer = null 40 | 41 | // Values 42 | this.data = null 43 | this.owner = null 44 | this.contributors = [] 45 | this.initialUrl = 'https://api.github.com/repos/rezozero/starting-blocks' 46 | } 47 | 48 | async init () { 49 | super.init() 50 | 51 | // Elements 52 | this.avatarContainer = this.page.rootElement.querySelectorAll('.avatar-cont')[0] 53 | this.contributorsListingContainer = this.page.rootElement.querySelectorAll('.usersblock__contributors-list')[0] 54 | 55 | // Init request 56 | this.data = await Api.getData(this.initialUrl) 57 | 58 | this.fillData(this.data) 59 | } 60 | 61 | initEvents () { 62 | return super.initEvents() 63 | } 64 | 65 | destroy () { 66 | return super.destroy() 67 | } 68 | 69 | destroyEvents () { 70 | return super.destroyEvents() 71 | } 72 | 73 | async fillData () { 74 | if (!this.data) return 75 | 76 | this.contributors = await Api.getData(this.data.contributors_url) 77 | this.owner = this.data.owner 78 | 79 | this.setAvatar() 80 | this.setContributors() 81 | } 82 | 83 | setContributors () { 84 | for (let contributor of this.contributors) { 85 | const tpl = ` 86 |
87 |
88 | @${contributor.login} 92 |
93 |
@${contributor.login}
94 |

95 | Contributions: ${contributor.contributions}
96 | See more 97 |

98 |
99 |
100 |
` 101 | 102 | this.contributorsListingContainer.insertAdjacentHTML('afterbegin', tpl) 103 | } 104 | } 105 | 106 | async setAvatar () { 107 | if (!this.owner.avatar_url) return 108 | const img = await Utils.loadImage(this.owner.avatar_url) 109 | img.classList.add('img-thumbnail') 110 | this.avatarContainer.appendChild(img) 111 | } 112 | 113 | onResize () { 114 | return super.onResize() 115 | } 116 | 117 | onPageReady () { 118 | return super.onPageReady() 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /examples/src/js/config/config.example.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file config.example.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | export default { 30 | token: 'your_token_to_me' 31 | } 32 | -------------------------------------------------------------------------------- /examples/src/js/factories/TransitionFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file TransitionFactory.js 23 | * @author Quentin Neyraud 24 | * @author Adrien Scholaert 25 | */ 26 | 27 | import { AbstractTransitionFactory } from 'starting-blocks' 28 | import FadeTransition from '../transitions/FadeTransition' 29 | import SlideTransition from '../transitions/SlideTransition' 30 | import BigTransition from '../transitions/BigTransition' 31 | 32 | /** 33 | * Transition mapper class. 34 | * 35 | * This class maps your `data-transition` with your *ES6* classes. 36 | * 37 | * **You must define your own ClassFactory for each of your projects.**. 38 | */ 39 | export default class TransitionFactory extends AbstractTransitionFactory { 40 | /** 41 | * Get Transition 42 | * 43 | * @param {Object} previousState 44 | * @param {Object} state 45 | * @returns {AbstractTransition} 46 | */ 47 | getTransition (previousState, state) { 48 | /** 49 | * You can customise transition logic with the previousState and the new state 50 | * 51 | * Ex: when back or prev button its pressed we use FadeTransition 52 | */ 53 | 54 | let transition 55 | 56 | if (state && state.context === 'history') { 57 | transition = new FadeTransition() 58 | transition.serviceName = 'FadeTransition' 59 | } else { 60 | switch (state.transitionName) { 61 | case 'slide': 62 | transition = new SlideTransition() 63 | transition.serviceName = 'SlideTransition' 64 | break 65 | case 'fade': 66 | transition = new FadeTransition() 67 | transition.serviceName = 'FadeTransition' 68 | break 69 | default: 70 | transition = new BigTransition() 71 | transition.serviceName = 'BigTransition' 72 | 73 | break 74 | } 75 | } 76 | 77 | transition.container = this.container 78 | 79 | return transition 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/src/js/pages/DefaultPage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file Page.js 23 | * @author Ambroise Maupate 24 | */ 25 | 26 | import { AbstractPage } from 'starting-blocks' 27 | 28 | /** 29 | * Some example "page". 30 | * 31 | * @extends {AbstractPage} 32 | * @class 33 | */ 34 | export default class DefaultPage extends AbstractPage { 35 | constructor (container) { 36 | super(container, 'DefaultPage') 37 | 38 | // Values 39 | 40 | /** 41 | * @type {HTMLElement} 42 | */ 43 | this.duplicateButtonElement = null 44 | 45 | // Bind methods 46 | this.onButtonClick = this.onButtonClick.bind(this) 47 | } 48 | 49 | async init () { 50 | await super.init() 51 | 52 | this.duplicateButtonElement = this.rootElement.querySelectorAll('a.duplicate-last')[0] 53 | } 54 | 55 | initEvents () { 56 | super.initEvents() 57 | 58 | if (this.duplicateButtonElement) { 59 | this.duplicateButtonElement.addEventListener('click', this.onButtonClick) 60 | } 61 | } 62 | 63 | destroyEvents () { 64 | super.destroyEvents() 65 | 66 | if (this.duplicateButtonElement) { 67 | this.duplicateButtonElement.removeEventListener('click', this.onButtonClick) 68 | } 69 | } 70 | 71 | onButtonClick (e) { 72 | e.preventDefault() 73 | let newBlockElement = this.blockElements[this.blockElements.length - 1].cloneNode(true) 74 | newBlockElement.setAttribute('id', `block-${this.blockElements.length + 1}`) 75 | this.rootElement.appendChild(newBlockElement) 76 | return false 77 | } 78 | 79 | onLazyImageProcessed (index) { 80 | super.onLazyImageProcessed(index) 81 | } 82 | 83 | onResize () { 84 | super.onResize() 85 | } 86 | 87 | onLazyImageSet (element) { 88 | super.onLazyImageSet(element) 89 | } 90 | 91 | onLazyImageLoad (element) { 92 | super.onLazyImageLoad(element) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /examples/src/js/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file Home.js 23 | * @author Ambroise Maupate 24 | */ 25 | 26 | import { AbstractPage, EventTypes } from 'starting-blocks' 27 | import { TweenMax, Power3 } from 'gsap' 28 | 29 | /** 30 | * Some example "home" page. 31 | * 32 | * @extends {AbstractPage} 33 | * @class 34 | */ 35 | export default class HomePage extends AbstractPage { 36 | constructor (container) { 37 | super(container, 'HomePage') 38 | 39 | // Elements 40 | this.elements = [] 41 | 42 | // Bind methods 43 | this.prepareShow = this.prepareShow.bind(this) 44 | this.show = this.show.bind(this) 45 | } 46 | 47 | async init () { 48 | await super.init() 49 | 50 | this.elements = [...this.rootElement.querySelectorAll('.bg-white')] 51 | } 52 | 53 | initEvents () { 54 | super.initEvents() 55 | 56 | window.addEventListener(EventTypes.BEFORE_SPLASHSCREEN_HIDE, this.prepareShow) 57 | window.addEventListener(EventTypes.START_SPLASHSCREEN_HIDE, this.show) 58 | } 59 | 60 | destroyEvents () { 61 | super.destroyEvents() 62 | 63 | window.removeEventListener(EventTypes.BEFORE_SPLASHSCREEN_HIDE, this.prepareShow) 64 | window.removeEventListener(EventTypes.START_SPLASHSCREEN_HIDE, this.show) 65 | } 66 | 67 | prepareShow () { 68 | TweenMax.set(this.elements, { 69 | alpha: 0, 70 | y: 200 71 | }) 72 | } 73 | 74 | show () { 75 | TweenMax.staggerTo(this.elements, 1.2, { 76 | alpha: 1, 77 | delay: 0.2, 78 | ease: Power3.easeOut, 79 | y: 0 80 | }, 0.2) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/src/js/services/Splashscreen.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file Splashscreen.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import { AbstractSplashscreen, Dispatcher, EventTypes } from 'starting-blocks' 9 | import { TweenMax, Power3 } from 'gsap' 10 | 11 | export default class Splashscreen extends AbstractSplashscreen { 12 | constructor (container) { 13 | super(container, 'Splashscreen') 14 | 15 | // Elements 16 | this.rootElement = document.getElementById('splashscreen') 17 | this.innerEl = this.rootElement.querySelector('.splashscreen-inner') 18 | this.bgEl = this.rootElement.querySelector('.splashscreen-bg') 19 | 20 | // Values 21 | this.minimalDuration = 2000 // ms 22 | this.minimalDurationPromise = new Promise(resolve => { 23 | window.setTimeout(() => { 24 | resolve() 25 | }, this.minimalDuration) 26 | }) 27 | } 28 | 29 | hide () { 30 | return Promise 31 | .all([this.minimalDurationPromise]) 32 | .then(() => this.launchHideAnimation()) 33 | } 34 | 35 | launchHideAnimation () { 36 | return new Promise(resolve => { 37 | Dispatcher.commit(EventTypes.START_SPLASHSCREEN_HIDE) 38 | 39 | TweenMax.to(this.innerEl, 0.5, { 40 | alpha: 0 41 | }) 42 | 43 | TweenMax.to(this.rootElement, 1.2, { 44 | yPercent: 100, 45 | ease: Power3.easeInOut, 46 | onComplete: () => { 47 | TweenMax.set(this.rootElement, { 48 | display: 'none' 49 | }) 50 | 51 | resolve() 52 | } 53 | }) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/src/js/services/WebpackAsyncBlockBuilder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file WebpackAsyncBlockBuilder.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import { AbstractBlockBuilder } from 'starting-blocks' 9 | 10 | export default class WebpackAsyncBlockBuilder extends AbstractBlockBuilder { 11 | // Dynamic import 12 | async getBlockInstance (nodeTypeName) { 13 | try { 14 | const Block = await this.getModule(nodeTypeName) 15 | 16 | if (!this.hasService(nodeTypeName)) { 17 | this.container.$register({ 18 | $name: nodeTypeName, 19 | $type: 'instanceFactory', 20 | $value: c => { 21 | return new Block(c) 22 | } 23 | }) 24 | } 25 | 26 | return this.getService(nodeTypeName).instance() 27 | } catch (e) { 28 | console.error(e.message) 29 | return null 30 | } 31 | } 32 | 33 | async getModule (nodeTypeName) { 34 | return import(`../blocks/${nodeTypeName}` /* webpackChunkName: "block-" */) 35 | .then(block => { 36 | return block.default 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/src/js/transitions/BigTransition.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 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 furnished 10 | * to do so, subject to the following conditions: 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * Except as contained in this notice, the name of the ROADIZ shall not 23 | * be used in advertising or otherwise to promote the sale, use or other dealings 24 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 25 | * 26 | * @file BigTransition.js 27 | * @author Adrien Scholaert 28 | */ 29 | 30 | import { AbstractTransition } from 'starting-blocks' 31 | import TweenMax from 'gsap/TweenMax' 32 | 33 | /** 34 | * Fade Transition class example. Fade Out / Fade In content. 35 | * 36 | * @extends {AbstractTransition} 37 | */ 38 | export default class FadeTransition extends AbstractTransition { 39 | constructor () { 40 | super() 41 | this.mainElement = document.getElementById('big-transition') 42 | this.verticalElements = [...document.getElementById('big-transition-vertical').querySelectorAll('div')] 43 | this.horizontalElements = [...document.getElementById('big-transition-horizontal').querySelectorAll('div')] 44 | } 45 | 46 | /** 47 | * Entry point of the animation 48 | * Automatically called on init() 49 | */ 50 | start () { 51 | // Wait new content and the end of fadeOut animation 52 | // this.newPageLoading is a Promise which is resolved when the new content is loaded 53 | Promise.all([this.newPageLoading, this.startAnim()]) 54 | // then fadeIn the new content 55 | .then(this.endAnim.bind(this)) 56 | } 57 | 58 | /** 59 | * Fade out the old content. 60 | * @returns {Promise} 61 | */ 62 | startAnim () { 63 | return new Promise((resolve) => { 64 | TweenMax.to(this.oldPage.rootElement, 0.75, { 65 | alpha: 0 66 | }) 67 | 68 | TweenMax.set(this.mainElement, { 69 | autoAlpha: 1, 70 | onComplete: () => { 71 | TweenMax.staggerTo(this.horizontalElements, 0.5, { 72 | scaleX: 1 73 | }, 0.1) 74 | 75 | TweenMax.staggerTo(this.verticalElements.reverse(), 0.5, { 76 | scaleY: 1 77 | }, 0.1, resolve) 78 | } 79 | }) 80 | }) 81 | } 82 | 83 | /** 84 | * Fade in the new content 85 | */ 86 | async endAnim () { 87 | // Destroy old page 88 | this.destroyOldPage() 89 | 90 | // Manually append and build the new page 91 | await this.buildNewPage() 92 | 93 | // Scroll top 94 | this.scrollTop() 95 | 96 | // Fade animation 97 | TweenMax.fromTo(this.newPage.rootElement, 0.75, { 98 | autoAlpha: 0 99 | }, { 100 | autoAlpha: 1 101 | }) 102 | 103 | TweenMax.staggerTo(this.horizontalElements, 0.5, { 104 | scaleX: 0 105 | }, 0.1) 106 | 107 | TweenMax.staggerTo(this.verticalElements, 0.5, { 108 | scaleY: 0 109 | }, 0.1, () => { 110 | TweenMax.set(this.mainElement, { 111 | autoAlpha: 0 112 | }) 113 | 114 | // IMPORTANT: Call this method at the end 115 | this.done() 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/src/js/transitions/DefaultTransition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file DefaultTransition.js 26 | * @author Adrien Scholaert 27 | */ 28 | import { AbstractTransition } from 'starting-blocks' 29 | 30 | /** 31 | * Default Transition. Show / Hide content. 32 | * 33 | * @extends {AbstractTransition} 34 | */ 35 | export default class DefaultTransition extends AbstractTransition { 36 | start () { 37 | Promise.all([this.newPageLoading]) 38 | .then(this.finish.bind(this)) 39 | } 40 | 41 | async finish () { 42 | this.destroyOldPage() 43 | this.scrollTop() 44 | await this.buildNewPage() 45 | this.done() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/src/js/transitions/FadeTransition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file FadeTransition.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | import TweenLite from 'gsap/TweenLite' 30 | import { AbstractTransition } from 'starting-blocks' 31 | 32 | /** 33 | * Fade Transition class example. Fade Out / Fade In content. 34 | * 35 | * @extends {AbstractTransition} 36 | */ 37 | export default class FadeTransition extends AbstractTransition { 38 | /** 39 | * Entry point of the animation 40 | * Automatically called on init() 41 | */ 42 | start () { 43 | // Wait new content and the end of fadeOut animation 44 | // this.newPageLoading is a Promise which is resolved when the new content is loaded 45 | Promise.all([this.newPageLoading, this.fadeOut()]) 46 | // then fadeIn the new content 47 | .then(this.fadeIn.bind(this)) 48 | } 49 | 50 | /** 51 | * Fade out the old content. 52 | * @returns {Promise} 53 | */ 54 | fadeOut () { 55 | return new Promise((resolve) => { 56 | TweenLite.to(this.oldPage.rootElement, 0.4, { 57 | alpha: 0, 58 | onComplete: resolve 59 | }) 60 | }) 61 | } 62 | 63 | /** 64 | * Fade in the new content 65 | */ 66 | async fadeIn () { 67 | // Destroy old page 68 | this.destroyOldPage() 69 | 70 | // Manually append and build the new page 71 | await this.buildNewPage() 72 | 73 | // Scroll top 74 | this.scrollTop() 75 | 76 | // fadeIn the new content container 77 | TweenLite.to(this.newPage.rootElement, 0.4, { 78 | autoAlpha: 1, 79 | onComplete: () => { 80 | // IMPORTANT: Call this method at the end 81 | this.done() 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/src/js/transitions/SlideTransition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file SlideTransition.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | import { AbstractTransition } from 'starting-blocks' 30 | import TweenLite from 'gsap/TweenLite' 31 | import { Power4 } from 'gsap/EasePack' 32 | import 'gsap/ScrollToPlugin' 33 | 34 | /** 35 | * Slide Transition class example. 36 | * 37 | * @extends {AbstractTransition} 38 | */ 39 | export default class SlideTransition extends AbstractTransition { 40 | /** 41 | * Entry point of the animation 42 | * Automatically called on init() 43 | */ 44 | start () { 45 | Promise.all([this.newPageLoading, this.slideOut()]) 46 | .then(this.slideIn.bind(this)) 47 | } 48 | 49 | /** 50 | * Slide out the old content. 51 | * @returns {Promise} 52 | */ 53 | slideOut () { 54 | return new Promise((resolve) => { 55 | TweenLite.to(this.oldPage.rootElement, 0.5, { 56 | xPercent: 25, 57 | alpha: 0, 58 | ease: Power4.easeIn, 59 | onComplete: resolve 60 | }) 61 | }) 62 | } 63 | 64 | /** 65 | * Slide in the new content 66 | */ 67 | async slideIn () { 68 | // Destroy old page 69 | this.destroyOldPage() 70 | 71 | // Manually append and build the new page 72 | await this.buildNewPage() 73 | 74 | // Scroll top 75 | this.scrollTop() 76 | 77 | // Animate the new page 78 | TweenLite.fromTo(this.newPage.rootElement, 0.75, { 79 | visibility: 'visible', 80 | alpha: 0, 81 | xPercent: -25 82 | }, { 83 | xPercent: 0, 84 | alpha: 1, 85 | ease: Power4.easeOut, 86 | onComplete: () => { 87 | // IMPORTANT: Call this method at the end 88 | this.done() 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/src/js/utils/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file utils.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | export function loadImage (url) { 30 | return new Promise(resolve => { 31 | const imageElement = new window.Image() 32 | imageElement.addEventListener('load', () => { 33 | resolve(imageElement) 34 | }) 35 | imageElement.src = url 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /examples/src/views/partial.html: -------------------------------------------------------------------------------- 1 | 28 | 29 |
34 | 35 |
36 |

Page partial

37 |

This page only contains a div element with .page-content class.

38 | 39 | 43 | 44 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ex est, maximus aliquam sagittis vel, tempus vel odio. Praesent felis elit, blandit et laoreet nec, hendrerit et arcu. Aenean ligula sapien, sodales vel semper in, pulvinar id nulla. Donec pellentesque non tellus vel imperdiet. Pellentesque posuere, nisl sed rutrum tincidunt, augue augue vestibulum ex, vel ornare justo arcu eu magna. Praesent vel nisl enim. Aenean a odio porttitor, fermentum est et, mattis sem. Suspendisse aliquam, risus a aliquam vestibulum, eros ipsum pulvinar sem, quis posuere tellus ex id velit. Quisque eu pellentesque nisl, sed volutpat purus. Sed auctor convallis sapien sed hendrerit. Vivamus et neque facilisis, ultrices ex non, faucibus velit. Aliquam laoreet sed turpis a blandit. Nulla fringilla massa non fermentum rutrum.

45 | 46 |
47 |

Vivamus dolor massa, aliquet ut leo vel, sodales mattis tortor. Aenean at mauris et ex mollis fringilla. Sed volutpat viverra vehicula. Nullam maximus ligula ac nisl aliquam commodo. Donec sed vestibulum orci. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin ut porttitor lorem, sit amet aliquet dui. Nunc dictum, lorem vitae elementum pharetra, sapien orci dignissim ligula, aliquam interdum erat urna a nibh. Morbi quis tellus maximus, pellentesque erat ac, porttitor nulla. Curabitur rhoncus urna non magna mattis aliquet. Vestibulum non lobortis turpis. Nunc aliquet velit a magna dictum pulvinar vitae laoreet tortor. Duis auctor iaculis arcu ac rutrum. Suspendisse vel odio risus.

48 |
49 |
50 |

Vivamus dolor massa, aliquet ut leo vel, sodales mattis tortor. Aenean at mauris et ex mollis fringilla. Sed volutpat viverra vehicula. Nullam maximus ligula ac nisl aliquam commodo. Donec sed vestibulum orci. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin ut porttitor lorem, sit amet aliquet dui. Nunc dictum, lorem vitae elementum pharetra, sapien orci dignissim ligula, aliquam interdum erat urna a nibh. Morbi quis tellus maximus, pellentesque erat ac, porttitor nulla. Curabitur rhoncus urna non magna mattis aliquet. Vestibulum non lobortis turpis. Nunc aliquet velit a magna dictum pulvinar vitae laoreet tortor. Duis auctor iaculis arcu ac rutrum. Suspendisse vel odio risus.

51 |

Lazyloaded img

57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /examples/webpack/build/base.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import debug from 'debug' 3 | import WebpackNotifierPlugin from 'webpack-notifier' 4 | import CleanTerminalPlugin from 'clean-terminal-webpack-plugin' 5 | import CopyWebpackPlugin from 'copy-webpack-plugin' 6 | import HtmlWebpackPlugin from 'html-webpack-plugin' 7 | import HtmlWebpackMultiBuildPlugin from '../modules/HtmlWebpackMultiBuildPlugin' 8 | 9 | const dbg = debug('StartingBlocks:webpack-config:base ') 10 | dbg.color = debug.colors[3] 11 | 12 | const getWebpackConfigBase = (config) => { 13 | dbg('⚙ Exporting default webpack configuration.') 14 | 15 | const paths = config.utils_paths 16 | 17 | let webpackConfig = { 18 | cache: true, 19 | stats: config.stats, 20 | devtool: config.devtool, 21 | target: 'web', 22 | resolve: config.resolve, 23 | module: { 24 | rules: [{ 25 | test: /\.js$/, 26 | enforce: 'pre', 27 | loader: 'eslint-loader', 28 | exclude: [/node_modules/, /starting-blocks/] 29 | }, { 30 | test: /\.js?$/, 31 | exclude: /(node_modules)/, 32 | loader: 'babel-loader', 33 | query: { 34 | cacheDirectory: true 35 | } 36 | }] 37 | }, 38 | plugins: [ 39 | new CleanTerminalPlugin(), 40 | new webpack.DefinePlugin(config.globals), 41 | new webpack.NoEmitOnErrorsPlugin(), 42 | new WebpackNotifierPlugin({ alwaysNotify: true }), 43 | new CopyWebpackPlugin([{ 44 | from: paths.clientDemo('img'), 45 | to: paths.distDemo('img') 46 | }, { 47 | from: paths.clientDemo('views'), 48 | to: paths.distDemo('') 49 | }, { 50 | from: paths.clientDemo('css'), 51 | to: paths.distDemo('css') 52 | }]), 53 | new HtmlWebpackPlugin({ 54 | filename: paths.distDemo('index.html'), 55 | template: paths.clientDemo('views/index.html'), 56 | cache: false, 57 | inject: false, 58 | refreshOnChange: config.refreshOnChange 59 | }), 60 | new HtmlWebpackMultiBuildPlugin() 61 | ], 62 | externals: config.externals 63 | } 64 | 65 | if (config.bundleAnalyzerReportDemo) { 66 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 67 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 68 | } 69 | 70 | if (config.refreshOnChange) { 71 | webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()) 72 | } 73 | 74 | return webpackConfig 75 | } 76 | 77 | export default getWebpackConfigBase 78 | -------------------------------------------------------------------------------- /examples/webpack/build/environments.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import debug from 'debug' 3 | 4 | const dbg = debug('StartingBlocks:webpack-config:environments ') 5 | dbg.color = debug.colors[5] 6 | 7 | const optimization = { 8 | splitChunks: { 9 | chunks: 'all', 10 | cacheGroups: { 11 | commons: { 12 | name: 'commons', 13 | chunks: 'all', 14 | minChunks: 2, 15 | minSize: 0, 16 | enforce: true 17 | } 18 | } 19 | } 20 | } 21 | 22 | export default { 23 | modern: (base, config) => { 24 | const paths = config.utils_paths 25 | 26 | return { 27 | entry: { 28 | app: paths.clientDemo('js/app.js') 29 | }, 30 | output: { 31 | path: paths.distDemo(), 32 | filename: 'js/modern.[name].js' 33 | }, 34 | module: { 35 | rules: [{ 36 | test: /\.js$/, 37 | enforce: 'pre', 38 | loader: 'eslint-loader', 39 | exclude: [/node_modules/, /starting-blocks/] 40 | }, { 41 | test: /\.js?$/, 42 | exclude: /(node_modules)/, 43 | loader: 'babel-loader', 44 | query: { 45 | cacheDirectory: true, 46 | presets: [ 47 | [ 48 | '@babel/preset-env', { 49 | targets: { 50 | esmodules: true 51 | } 52 | } 53 | ] 54 | ] 55 | } 56 | }] 57 | } 58 | } 59 | }, 60 | 61 | legacy: (base, config) => { 62 | const paths = config.utils_paths 63 | 64 | return { 65 | entry: { 66 | app: [ 67 | 'whatwg-fetch', 68 | 'es6-promise', 69 | 'intersection-observer', 70 | 'url-polyfill', 71 | paths.clientDemo('js/app.js') 72 | ] 73 | }, 74 | output: { 75 | path: paths.distDemo(), 76 | filename: 'js/legacy.[name].js' 77 | } 78 | } 79 | }, 80 | 81 | development: (base, config) => { 82 | return { 83 | mode: 'development', 84 | watch: true, 85 | optimization: { 86 | ...optimization 87 | } 88 | } 89 | }, 90 | 91 | production: (base, config) => { 92 | dbg('🗑 Cleaning assets folder') 93 | dbg('👽 Using UglifyJs') 94 | dbg('🎨 Using PostCss') 95 | 96 | return { 97 | mode: 'production', 98 | plugins: [ 99 | new webpack.DefinePlugin({ 100 | 'process.env': { 101 | NODE_ENV: '"production"' 102 | } 103 | }) 104 | ], 105 | optimization: { 106 | ...optimization 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /examples/webpack/build/index.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import getWebpackConfigBase from './base' 3 | import webpackConfigOverrides from './environments' 4 | import WebpackMerge from 'webpack-merge' 5 | 6 | const dbg = debug('StartingBlocks:webpack-config ') 7 | dbg.color = debug.colors[4] 8 | 9 | const getWebpackConfig = (config) => { 10 | dbg('👷‍♂️ Creating webpack configuration') 11 | 12 | const base = getWebpackConfigBase(config) 13 | const baseModern = WebpackMerge.smart(base, webpackConfigOverrides['modern'](base, config)) 14 | const baseLegacy = WebpackMerge.smart(base, webpackConfigOverrides['legacy'](base, config)) 15 | 16 | dbg(`🕵️‍♂️ Looking for environment overrides for NODE_ENV "${config.env}".`) 17 | 18 | const overrides = webpackConfigOverrides[config.env] 19 | 20 | if (webpackConfigOverrides[config.env]) { 21 | dbg('🙋‍♂️ Found overrides, applying to default configuration.') 22 | 23 | return [ 24 | WebpackMerge.smart(baseModern, overrides(baseModern, config)), 25 | WebpackMerge.smart(baseLegacy, overrides(baseLegacy, config)) 26 | ] 27 | } else { 28 | dbg('🤷‍♂️ No environment overrides found.') 29 | } 30 | } 31 | 32 | export default getWebpackConfig 33 | -------------------------------------------------------------------------------- /examples/webpack/config/base.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import path from 'path' 3 | 4 | const dbg = debug('StartingBlocks:config:base ') 5 | dbg.color = debug.colors[2] 6 | 7 | const getConfig = () => { 8 | let config = { 9 | env: process.env.NODE_ENV || 'development' 10 | } 11 | 12 | config = { 13 | ...config, 14 | devtool: false, 15 | 16 | // ---------------------------------- 17 | // Project Structure 18 | // ---------------------------------- 19 | path_base: path.resolve(__dirname, '..'), 20 | dir_entry_demo: '../src', 21 | dir_dist_demo: '../dist', 22 | 23 | bundleAnalyzerReportDemo: false, 24 | 25 | // ---------------------------------- 26 | // Stats 27 | // ---------------------------------- 28 | // stats: { 29 | // chunks: false, 30 | // chunkModules: false, 31 | // colors: true, 32 | // children: false, 33 | // version: false, 34 | // reasons: false 35 | // }, 36 | stats: 'minimal', 37 | 38 | // ---------------------------------- 39 | // Inputs 40 | // ---------------------------------- 41 | js_vendors: [], 42 | 43 | // ---------------------------------- 44 | // Externals 45 | // ---------------------------------- 46 | externals: { 47 | 48 | }, 49 | 50 | resolve: { 51 | alias: { 52 | 'starting-blocks': path.resolve(__dirname, '../../../') 53 | }, 54 | extensions: ['.js'] 55 | }, 56 | 57 | // ---------------------------------- 58 | // Globals 59 | // ---------------------------------- 60 | // ⚠️ : You have to add all these constants to .eslintrc file 61 | globals: { 62 | 'DEVELOPMENT': JSON.stringify(config.env === 'development'), 63 | 'PRODUCTION': JSON.stringify(config.env === 'production'), 64 | 'ENVIRONMENT': JSON.stringify(config.env) 65 | } 66 | } 67 | 68 | config.public_path = '' 69 | 70 | dbg('⚙ Exporting default configuration.') 71 | return config 72 | } 73 | 74 | export default getConfig 75 | -------------------------------------------------------------------------------- /examples/webpack/config/environments.js: -------------------------------------------------------------------------------- 1 | export default { 2 | development: () => { 3 | return {} 4 | }, 5 | 6 | production: () => ({ 7 | devtool: false 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /examples/webpack/config/index.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | import path from 'path' 3 | import getConfigBase from './base' 4 | import configOverrides from './environments' 5 | 6 | const dbg = debug('StartingBlocks:config ') 7 | dbg.color = debug.colors[1] 8 | 9 | const getConfig = () => { 10 | dbg('👷‍♂️ Creating configuration.') 11 | 12 | let configBase = getConfigBase() 13 | 14 | dbg(`🕵️‍♂️ Looking for environment overrides for NODE_ENV "${configBase.env}".`) 15 | 16 | const overrides = configOverrides[configBase.env] 17 | 18 | if (configOverrides[configBase.env]) { 19 | dbg('🙋‍♂️ Found overrides, applying to default configuration.') 20 | Object.assign(configBase, overrides(configBase)) 21 | } else { 22 | dbg('🤷‍♂️ No environment overrides found.') 23 | } 24 | 25 | // ------------------------------------ 26 | // Utilities 27 | // ------------------------------------ 28 | const resolve = path.resolve 29 | const base = (...args) => 30 | Reflect.apply(resolve, null, [configBase.path_base, ...args]) 31 | 32 | configBase.utils_paths = { 33 | base: base, 34 | clientDemo: base.bind(null, configBase.dir_entry_demo), 35 | distDemo: base.bind(null, configBase.dir_dist_demo) 36 | } 37 | 38 | return configBase 39 | } 40 | 41 | export default getConfig 42 | -------------------------------------------------------------------------------- /examples/webpack/modules/HtmlWebpackMultiBuildPlugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file HtmlWebpackMultiBuildPlugin.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 11 | 12 | function HtmlWebpackMultiBuildPlugin(options) { 13 | this.options = options; 14 | this.js = []; 15 | } 16 | 17 | HtmlWebpackMultiBuildPlugin.prototype = { 18 | apply: function(compiler) { 19 | if (compiler.hooks) { 20 | // webpack 4 support 21 | compiler.hooks.compilation.tap('HtmlWebpackMultiBuildPlugin', compilation => { 22 | HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync( 23 | 'HtmlWebpackMultiBuildPlugin', 24 | this.beforeHtmlGeneration.bind(this), 25 | ); 26 | }); 27 | } else { 28 | compiler.plugin('compilation', compilation => { 29 | compilation.plugin('html-webpack-plugin-before-html-generation', this.beforeHtmlGeneration.bind(this)); 30 | }); 31 | } 32 | }, 33 | 34 | beforeHtmlGeneration: function(data, cb) { 35 | for (var jsName of data.assets.js) { 36 | if (!this.js.includes(jsName)) { 37 | this.js.push(jsName) 38 | } 39 | } 40 | 41 | data.plugin.options.modernScripts = this.js.filter((value) => value.indexOf('legacy') === -1); 42 | data.plugin.options.legacyScripts = this.js.filter((value) => value.indexOf('legacy') > 0); 43 | 44 | cb(null, data); 45 | } 46 | }; 47 | 48 | module.exports = HtmlWebpackMultiBuildPlugin; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starting-blocks", 3 | "version": "5.0.7", 4 | "description": "A JS page and blocks framework written in ES6 to go along Roadiz CMS or any other great CMS.", 5 | "author": "Rezo Zero", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/rezozero/starting-blocks.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/rezozero/starting-blocks/issues" 13 | }, 14 | "homepage": "https://startingblocks.rezo-zero.com", 15 | "scripts": { 16 | "start": "concurrently \"npm run dev\" \"npm run serve\"", 17 | "test": "echo \"No test specified\"", 18 | "dev": "NODE_ENV=development rollup --config -w", 19 | "serve": "http-server ./examples/dist -o", 20 | "build": "NODE_ENV=production rollup --config", 21 | "demo": "NODE_ENV=production TARGET=demo webpack --progress", 22 | "dev-demo": "NODE_ENV=development TARGET=demo webpack --progress", 23 | "doc": "esdoc -c esdoc.json" 24 | }, 25 | "engines": { 26 | "node": ">=8.0.0", 27 | "npm": ">=5.0.0" 28 | }, 29 | "main": "dist/main.umd.js", 30 | "module": "dist/main.esm.js", 31 | "devDependencies": { 32 | "@babel/cli": "7.1.2", 33 | "@babel/core": "7.1.2", 34 | "@babel/plugin-proposal-class-properties": "7.1.0", 35 | "@babel/plugin-proposal-decorators": "7.1.2", 36 | "@babel/plugin-proposal-do-expressions": "7.0.0", 37 | "@babel/plugin-proposal-export-default-from": "7.0.0", 38 | "@babel/plugin-proposal-export-namespace-from": "7.0.0", 39 | "@babel/plugin-proposal-function-bind": "7.0.0", 40 | "@babel/plugin-proposal-function-sent": "7.1.0", 41 | "@babel/plugin-proposal-logical-assignment-operators": "7.0.0", 42 | "@babel/plugin-proposal-nullish-coalescing-operator": "7.0.0", 43 | "@babel/plugin-proposal-numeric-separator": "7.0.0", 44 | "@babel/plugin-proposal-optional-chaining": "7.0.0", 45 | "@babel/plugin-proposal-pipeline-operator": "7.0.0", 46 | "@babel/plugin-proposal-throw-expressions": "7.0.0", 47 | "@babel/plugin-syntax-dynamic-import": "7.0.0", 48 | "@babel/plugin-syntax-import-meta": "7.0.0", 49 | "@babel/plugin-transform-runtime": "7.1.0", 50 | "@babel/polyfill": "7.0.0", 51 | "@babel/preset-env": "7.1.0", 52 | "@babel/register": "7.0.0", 53 | "@babel/runtime": "7.1.2", 54 | "babel-eslint": "9.0.0", 55 | "babel-loader": "8.0.4", 56 | "bottlejs": "1.7.1", 57 | "clean-terminal-webpack-plugin": "1.1.0", 58 | "concurrently": "4.0.1", 59 | "copy-webpack-plugin": "4.5.4", 60 | "es6-promise": "4.2.5", 61 | "esdoc": "1.1.0", 62 | "esdoc-brand-plugin": "1.0.1", 63 | "esdoc-ecmascript-proposal-plugin": "1.0.0", 64 | "esdoc-exclude-source-plugin": "1.0.0", 65 | "esdoc-standard-plugin": "1.0.0", 66 | "eslint": "5.7.0", 67 | "eslint-config-standard": "12.0.0", 68 | "eslint-loader": "2.1.1", 69 | "eslint-plugin-dependencies": "2.4.0", 70 | "eslint-plugin-html": "4.0.6", 71 | "eslint-plugin-import": "2.14.0", 72 | "eslint-plugin-node": "7.0.1", 73 | "eslint-plugin-promise": "4.0.1", 74 | "eslint-plugin-standard": "4.0.0", 75 | "event-hooks-webpack-plugin": "2.1.0", 76 | "extract-text-webpack-plugin": "4.0.0-beta.0", 77 | "gsap": "2.0.2", 78 | "html-webpack-include-assets-plugin": "1.0.6", 79 | "html-webpack-multi-build-plugin": "1.0.0", 80 | "html-webpack-plugin": "4.0.0-beta.2", 81 | "http-server": "0.11.1", 82 | "imports-loader": "0.8.0", 83 | "intersection-observer": "0.5.1", 84 | "lazysizes": "4.1.4", 85 | "post-compile-webpack-plugin": "0.1.2", 86 | "rollup": "0.66.6", 87 | "rollup-plugin-alias": "1.4.0", 88 | "rollup-plugin-babel": "4.0.3", 89 | "rollup-plugin-commonjs": "9.2.0", 90 | "rollup-plugin-node-resolve": "3.4.0", 91 | "rollup-plugin-terser": "3.0.0", 92 | "rollup-plugin-uglify": "6.0.0", 93 | "script-ext-html-webpack-plugin": "2.0.1", 94 | "terser-webpack-plugin": "1.1.0", 95 | "uglifyjs-webpack-plugin": "2.0.1", 96 | "url-polyfill": "1.1.0", 97 | "webpack": "4.22.0", 98 | "webpack-bundle-analyzer": "2.13.1", 99 | "webpack-cli": "3.1.2", 100 | "webpack-merge": "4.1.4", 101 | "webpack-notifier": "1.7.0", 102 | "whatwg-fetch": "3.0.0", 103 | "worker-loader": "1.1.1" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file rollup.config.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import commonjs from 'rollup-plugin-commonjs' 9 | import resolve from 'rollup-plugin-node-resolve' 10 | import babel from 'rollup-plugin-babel' 11 | 12 | const normalBundles = { 13 | input: 'src/bundle.js', 14 | output: [{ 15 | file: 'dist/main.esm.js', 16 | format: 'esm' 17 | }, { 18 | file: 'dist/main.cjs.js', 19 | format: 'cjs' 20 | }, { 21 | file: 'dist/main.amd.js', 22 | format: 'amd' 23 | }, { 24 | file: 'dist/main.umd.js', 25 | format: 'umd', 26 | name: 'starting-blocks' 27 | }], 28 | plugins: [ 29 | commonjs(), // prise en charge de require 30 | resolve(), // prise en charge des modules depuis node_modules 31 | babel({ // transpilation 32 | runtimeHelpers: true, 33 | exclude: 'node_modules/**' 34 | }) 35 | ] 36 | } 37 | 38 | const es6Bundle = { 39 | input: 'src/bundle.js', 40 | output: [{ 41 | file: 'dist/main.e6.js', 42 | format: 'esm' 43 | }], 44 | plugins: [ 45 | commonjs(), // prise en charge de require 46 | resolve(), // prise en charge des modules depuis node_modules 47 | babel({ 48 | runtimeHelpers: true, 49 | exclude: 'node_modules/**', 50 | presets: [ 51 | ['@babel/preset-env', { 52 | targets: { 53 | esmodules: true 54 | } 55 | }] 56 | ] 57 | }) 58 | ] 59 | } 60 | 61 | export default [ 62 | normalBundles, 63 | es6Bundle 64 | ] 65 | -------------------------------------------------------------------------------- /src/StartingBlocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file StartingBlocks.js 5 | * @author Adrien Scholaert 6 | */ 7 | import Bottle from 'bottlejs' 8 | import PageBuilder from './services/PageBuilder' 9 | import BlockBuilder from './services/BlockBuilder' 10 | import Dom from './services/Dom' 11 | import DefaultPage from './pages/DefaultPage' 12 | 13 | /** 14 | * @namespace 15 | * @type {Object} defaults - Default config 16 | * @property {String} defaults.wrapperId - Id of the main wrapper 17 | * @property {String} defaults.pageBlockClass 18 | * @property {String} defaults.pageClass - Class name used to identify the containers 19 | * @property {String} defaults.objectTypeAttr - The data attribute name to find the node type 20 | * @property {String} defaults.noAjaxLinkClass 21 | * @property {String} defaults.noPrefetchClass - Class name used to ignore prefetch on links. 22 | * @property {boolean} defaults.manualDomAppend 23 | * @const 24 | * @default 25 | */ 26 | const CONFIG = { 27 | defaults: { 28 | wrapperId: 'sb-wrapper', 29 | pageBlockClass: 'page-block', 30 | pageClass: 'page-content', 31 | objectTypeAttr: 'data-node-type', 32 | noAjaxLinkClass: 'no-ajax-link', 33 | noPrefetchClass: 'no-prefetch', 34 | manualDomAppend: false, 35 | debug: 0 36 | } 37 | } 38 | 39 | export default class StartingBlocks { 40 | constructor (config = {}) { 41 | this.bottle = new Bottle() 42 | this.bootables = [] 43 | 44 | this.bottle.value('Config', { 45 | ...CONFIG.defaults, 46 | ...config 47 | }) 48 | 49 | window.startingBlocksDebugLevel = this.bottle.container.Config.debug 50 | 51 | this.provider('Dom', Dom) 52 | this.provider('BlockBuilder', BlockBuilder) 53 | this.instanceFactory('DefaultPage', c => { 54 | return new DefaultPage(c) 55 | }) 56 | } 57 | 58 | provider (id, ClassName, ...args) { 59 | if (!id || !ClassName) { 60 | throw new Error('A parameter is missing') 61 | } 62 | 63 | this.bottle.provider(id, function () { 64 | this.$get = container => { 65 | return new ClassName(container, ...args) 66 | } 67 | }) 68 | } 69 | 70 | factory (id, f) { 71 | this.bottle.factory(id, f) 72 | } 73 | 74 | instanceFactory (id, f) { 75 | this.bottle.instanceFactory(id, f) 76 | } 77 | 78 | bootableProvider (id, ClassName, ...args) { 79 | this.provider(id, ClassName, ...args) 80 | this.bootables.push(id) 81 | } 82 | 83 | boot () { 84 | this.bootableProvider('PageBuilder', PageBuilder) 85 | 86 | for (const serviceName of this.bootables) { 87 | if (this.bottle.container.hasOwnProperty(serviceName)) { 88 | this.bottle.container[serviceName].boot() 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/abstracts/AbstractBlock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file AbstractBlock.js 23 | * @author Ambroise Maupate 24 | * @author Adrien Scholaert 25 | */ 26 | 27 | import AbstractService from './AbstractService' 28 | import { debug } from '../utils/Logger' 29 | 30 | /** 31 | * Base class for creating block implementations. 32 | * 33 | * **Do not instanciate this class directly, create a sub-class**. 34 | * 35 | * @abstract 36 | */ 37 | export default class AbstractBlock extends AbstractService { 38 | /** 39 | * Abstract block constructor. 40 | * 41 | * It‘s better to extend this class by using `init` method instead 42 | * of extending `constructor`. 43 | * 44 | * @param {Object} container 45 | * @param {String} blockName 46 | * @constructor 47 | */ 48 | constructor (container, blockName = 'AbstractBlock') { 49 | super(container, blockName) 50 | 51 | /** 52 | * Node Type block name type 53 | * 54 | * @type {String|null} 55 | */ 56 | this.type = null 57 | 58 | /** 59 | * Current page instance 60 | * 61 | * @type {AbstractPage|null} 62 | */ 63 | this.page = null 64 | 65 | /** 66 | * Container 67 | * Root container HTMLElement for current block. 68 | * 69 | * @type {HTMLElement|null} 70 | */ 71 | this.rootElement = null 72 | 73 | /** 74 | * Block id 75 | * 76 | * @type {String|null} 77 | */ 78 | this.id = null 79 | 80 | /** 81 | * Node name 82 | * 83 | * @type {String} 84 | */ 85 | this.name = null 86 | } 87 | 88 | /** 89 | * Basic members initialization for children classes. 90 | * Do not search for page blocks here, use `onPageReady` method instead 91 | * 92 | * @abstract 93 | */ 94 | init () { 95 | debug('\t✳️ #' + this.id + ' %c[' + this.type + ']', 'color:grey') 96 | } 97 | 98 | /** 99 | * Bind load and resize events for this specific block. 100 | * 101 | * Do not forget to call `super.initEvents();` while extending this method. 102 | * 103 | * @abstract 104 | */ 105 | initEvents () {} 106 | 107 | /** 108 | * Destroy current block. 109 | * 110 | * Do not forget to call `super.destroy();` while extending this method. 111 | */ 112 | destroy () { 113 | debug('\t🗑️ #' + this.id + ' %c[' + this.type + ']', 'color:grey') 114 | this.destroyEvents() 115 | } 116 | 117 | /** 118 | * Unbind event block events. 119 | * 120 | * Make sure you’ve used binded methods to be able to 121 | * `off` them correctly. 122 | * 123 | * Do not forget to call `super.destroyEvents();` while extending this method. 124 | * 125 | * @abstract 126 | */ 127 | destroyEvents () {} 128 | 129 | /** 130 | * Called on window resize 131 | * 132 | * @abstract 133 | */ 134 | onResize () {} 135 | 136 | /** 137 | * Called once all page blocks have been created. 138 | * 139 | * @abstract 140 | */ 141 | onPageReady () {} 142 | } 143 | -------------------------------------------------------------------------------- /src/abstracts/AbstractBlockBuilder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file AbstractBlockBuilder.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import AbstractService from './AbstractService' 9 | 10 | export default class AbstractBlockBuilder extends AbstractService { 11 | /** 12 | * Returns an `AbstractBlock` child class instance 13 | * according to the nodeTypeName or an AbstractBlock as default. 14 | * 15 | * Comment out the default case if you don’t want a default block to be instantiated 16 | * for each block. 17 | * 18 | * @param {String} blockType 19 | * @return {AbstractBlock|null} 20 | */ 21 | async getBlockInstance (blockType) { return null } 22 | } 23 | -------------------------------------------------------------------------------- /src/abstracts/AbstractBootableService.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file AbstractBootableService.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import AbstractService from './AbstractService' 9 | 10 | export default class AbstractBootableService extends AbstractService { 11 | boot () {} 12 | } 13 | -------------------------------------------------------------------------------- /src/abstracts/AbstractInViewBlock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file AbstractInViewBlock.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import AbstractBlock from './AbstractBlock' 9 | 10 | export default class AbstractInViewBlock extends AbstractBlock { 11 | constructor (container, blockName = 'AbstractInViewBlock') { 12 | super(container, blockName) 13 | 14 | // Values 15 | this.observer = null 16 | this.observerOptions = { 17 | root: null, 18 | rootMargin: '0px', 19 | threshold: 0 20 | } 21 | 22 | // Bind method 23 | this.onIntersectionCallback = this.onIntersectionCallback.bind(this) 24 | } 25 | 26 | init () { 27 | super.init() 28 | 29 | // Create an observer 30 | this.observer = new window.IntersectionObserver(this.onIntersectionCallback, this.observerOptions) 31 | 32 | // Add block rootElement in the observer 33 | this.observer.observe(this.rootElement) 34 | } 35 | 36 | destroyEvents () { 37 | super.destroyEvents() 38 | this.unobserve() 39 | } 40 | 41 | onIntersectionCallback (entries) { 42 | for (const entry of entries) { 43 | if (entry.intersectionRatio > 0) { 44 | this.onScreen(entry) 45 | } else { 46 | this.offScreen(entry) 47 | } 48 | } 49 | } 50 | 51 | onScreen (entry) {} 52 | 53 | offScreen (entry) {} 54 | 55 | unobserve () { 56 | this.observer.unobserve(this.rootElement) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/abstracts/AbstractPage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file AbstractPage.js 23 | * @author Ambroise Maupate 24 | * @author Adrien Scholaert 25 | */ 26 | 27 | import debounce from '../utils/debounce' 28 | import AbstractService from './AbstractService' 29 | import { debug, warn } from '../utils/Logger' 30 | 31 | /** 32 | * Base class for creating page implementations. 33 | * 34 | * **Do not instanciate this class directly, create a sub-class**. 35 | * 36 | * @abstract 37 | */ 38 | export default class AbstractPage extends AbstractService { 39 | /** 40 | * Base constructor for Pages. 41 | * @constructor 42 | */ 43 | constructor (container) { 44 | super(container, 'AbstractPage') 45 | 46 | /** 47 | * Container element 48 | * 49 | * @type {HTMLElement} 50 | */ 51 | this.rootElement = null 52 | 53 | /** 54 | * Page id 55 | * 56 | * @type {String|null} 57 | */ 58 | this.id = null 59 | 60 | /** 61 | * Page context (static or ajax) 62 | * 63 | * @type {String|null} 64 | */ 65 | this.context = null 66 | 67 | /** 68 | * Page type 69 | * 70 | * @type {String|null} 71 | */ 72 | this.type = null 73 | 74 | /** 75 | * Is home ? 76 | * 77 | * @type {boolean} 78 | */ 79 | this.isHome = null 80 | 81 | /** 82 | * AbstractBlock collection. 83 | * 84 | * @type {Array} 85 | */ 86 | this.blocks = [] 87 | 88 | /** 89 | * Node name 90 | * 91 | * @type {String|null} 92 | */ 93 | this.name = null 94 | 95 | /** 96 | * Meta title 97 | * @type {String|null} 98 | */ 99 | this.metaTitle = null 100 | 101 | // Bind methods 102 | this.onResize = this.onResize.bind(this) 103 | this.onResizeDebounce = debounce(this.onResize, 50, false) 104 | this.bindedUpdateBlocks = debounce(this.updateBlocks.bind(this), 50, false) 105 | } 106 | 107 | /** 108 | * Initialize page. 109 | * 110 | * You should always extends this method in your child implementations instead 111 | * of extending page constructor. 112 | */ 113 | async init () { 114 | // Debug 115 | debug('✳️ #' + this.id + ' %c[' + this.type + '] [' + this.context + ']', 'color:grey') 116 | 117 | /** 118 | * HTMLElement blocks collection. 119 | * 120 | * @type {Array} 121 | */ 122 | this.blockElements = [...this.rootElement.querySelectorAll(`.${this.getService('Config').pageBlockClass}`)] 123 | 124 | /** 125 | * @type {Number} 126 | */ 127 | this.blockLength = this.blockElements.length 128 | 129 | if (this.blockLength) { 130 | await this.initBlocks() 131 | } 132 | 133 | this.initEvents() 134 | } 135 | 136 | /** 137 | * Destroy current page and all its blocks. 138 | */ 139 | destroy () { 140 | debug('🗑️ #' + this.id + ' %c[' + this.type + ']', 'color:grey') 141 | this.rootElement.parentNode.removeChild(this.rootElement) 142 | this.destroyEvents() 143 | 144 | // Do not remove name class on body if destroyed page is the same as current one. 145 | if (this.getService('PageBuilder').page !== null && this.getService('PageBuilder').page.name !== this.name) { 146 | document.body.classList.remove(this.name) 147 | } 148 | 149 | // Do not remove type class on body if destroyed page is the same as current one. 150 | if (this.getService('PageBuilder').page !== null && this.getService('PageBuilder').page.type !== this.type) { 151 | document.body.classList.remove(this.type) 152 | } 153 | 154 | // Blocks 155 | if (this.blocks !== null) { 156 | for (let blockIndex in this.blocks) { 157 | if (this.blocks.hasOwnProperty(blockIndex)) { 158 | this.blocks[blockIndex].destroy() 159 | } 160 | } 161 | } 162 | } 163 | 164 | /** 165 | * Initialize basic events. 166 | */ 167 | initEvents () { 168 | window.addEventListener('resize', this.onResizeDebounce) 169 | 170 | this.domObserver = new window.MutationObserver(this.bindedUpdateBlocks) 171 | this.domObserver.observe(this.rootElement, { 172 | childList: true, 173 | attributes: false, 174 | characterData: false, 175 | subtree: true 176 | }) 177 | } 178 | 179 | /** 180 | * Destroy events 181 | */ 182 | destroyEvents () { 183 | window.removeEventListener('resize', this.onResizeDebounce) 184 | this.domObserver.disconnect() 185 | } 186 | 187 | /** 188 | * Initialize page blocks on page. 189 | */ 190 | async initBlocks () { 191 | for (let blockIndex = 0; blockIndex < this.blockLength; blockIndex++) { 192 | /** 193 | * New Block. 194 | * 195 | * @type {AbstractBlock} 196 | */ 197 | let block = await this.initSingleBlock(this.blockElements[blockIndex]) 198 | 199 | // Prevent undefined blocks to be appended to block collection. 200 | if (block) { 201 | this.blocks.push(block) 202 | } 203 | } 204 | 205 | // Notify all blocks that page init is over. 206 | for (let i = this.blocks.length - 1; i >= 0; i--) { 207 | if (typeof this.blocks[i].onPageReady === 'function') this.blocks[i].onPageReady() 208 | } 209 | } 210 | 211 | /** 212 | * Append new blocks which were not present at init. 213 | */ 214 | async updateBlocks () { 215 | debug('\t📯 Page DOM changed…') 216 | 217 | // Create new blocks 218 | this.blockElements = this.rootElement.querySelectorAll(`.${this.getService('Config').pageBlockClass}`) 219 | this.blockLength = this.blockElements.length 220 | 221 | for (let blockIndex = 0; blockIndex < this.blockLength; blockIndex++) { 222 | let blockElement = this.blockElements[blockIndex] 223 | const existingBlock = this.getBlockById(blockElement.id) 224 | 225 | if (!blockElement.id) break 226 | 227 | if (existingBlock === null) { 228 | try { 229 | let block = await this.initSingleBlock(this.blockElements[blockIndex]) 230 | 231 | if (block) { 232 | this.blocks.push(block) 233 | block.onPageReady() 234 | } 235 | } catch (e) { 236 | warn(e.message) 237 | } 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * @param {HTMLElement} blockElement 244 | * @return {AbstractBlock} 245 | */ 246 | async initSingleBlock (blockElement) { 247 | if (!blockElement.id) return null 248 | 249 | let blockType = blockElement.getAttribute(this.getService('Config').objectTypeAttr) 250 | let blockInstance = await this.getService('BlockBuilder').getBlockInstance(blockType) 251 | 252 | if (!blockInstance) { 253 | return null 254 | } 255 | 256 | // Set values 257 | blockInstance.type = blockType 258 | blockInstance.page = this 259 | blockInstance.rootElement = blockElement 260 | blockInstance.id = blockElement.id 261 | blockInstance.name = blockElement.hasAttribute('data-node-name') ? blockElement.getAttribute('data-node-name') : '' 262 | 263 | // Init everything 264 | blockInstance.init() 265 | blockInstance.initEvents() 266 | 267 | return blockInstance 268 | } 269 | 270 | /** 271 | * Get a page block instance from its `id`. 272 | * 273 | * @param {String} id 274 | * @return {AbstractBlock|null} 275 | */ 276 | getBlockById (id) { 277 | for (const block of this.blocks) { 278 | if (block.id && block.id === id) { 279 | return block 280 | } 281 | } 282 | 283 | return null 284 | } 285 | 286 | /** 287 | * Get a page block index from its `id`. 288 | * 289 | * @param {String} id 290 | * @return {*|null} 291 | */ 292 | getBlockIndexById (id) { 293 | const l = this.blocks.length 294 | 295 | for (let i = 0; i < l; i++) { 296 | if (this.blocks[i].id && this.blocks[i].id === id) { 297 | return i 298 | } 299 | } 300 | 301 | return null 302 | } 303 | 304 | /** 305 | * Get the first page block instance from its `type`. 306 | * 307 | * @param {String} type 308 | * @return {AbstractBlock|null} 309 | */ 310 | getFirstBlockByType (type) { 311 | const index = this.getFirstBlockIndexByType(type) 312 | if (this.blocks[index]) { 313 | return this.blocks[index] 314 | } 315 | 316 | return null 317 | } 318 | 319 | /** 320 | * Get the first page block index from its `type`. 321 | * 322 | * @param {String} type 323 | * @return {*|null} 324 | */ 325 | getFirstBlockIndexByType (type) { 326 | const l = this.blocks.length 327 | 328 | for (let i = 0; i < l; i++) { 329 | if (this.blocks[i].type && this.blocks[i].type === type) { 330 | return i 331 | } 332 | } 333 | 334 | return null 335 | } 336 | 337 | /** 338 | * @abstract 339 | */ 340 | onResize () { 341 | for (const block of this.blocks) { 342 | block.onResize() 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/abstracts/AbstractService.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file AbstractService.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import UnknownServiceException from '../errors/UnknownServiceException' 9 | import DependencyNotFulfilledException from '../errors/DependencyNotFulfilledException' 10 | 11 | export default class AbstractService { 12 | constructor (container = {}, serviceName = 'AbstractService', dependencies = ['Config']) { 13 | this._container = container 14 | this._serviceName = serviceName 15 | 16 | this.checkDependencies(dependencies) 17 | } 18 | 19 | init () {} 20 | 21 | hasService (serviceName) { 22 | return this._container.hasOwnProperty(serviceName) 23 | } 24 | 25 | checkDependencies (dependencies = []) { 26 | for (const serviceName of dependencies) { 27 | if (!this.hasService(serviceName)) { 28 | throw new DependencyNotFulfilledException(this._serviceName, serviceName) 29 | } 30 | } 31 | } 32 | 33 | getService (serviceName) { 34 | if (!this.hasService(serviceName)) { 35 | throw new UnknownServiceException(serviceName) 36 | } 37 | 38 | return this._container[serviceName] 39 | } 40 | 41 | get serviceName () { 42 | return this._serviceName 43 | } 44 | 45 | set serviceName (value) { 46 | this._serviceName = value 47 | } 48 | 49 | get container () { 50 | return this._container 51 | } 52 | 53 | set container (value) { 54 | this._container = value 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/abstracts/AbstractSplashscreen.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file AbstractSplashscreen.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import AbstractBootableService from './AbstractBootableService' 9 | 10 | export default class AbstractSplashscreen extends AbstractBootableService { 11 | constructor (container, serviceName = 'AbstractSplashscreen') { 12 | super(container, serviceName) 13 | 14 | this._splashscreenHidden = false 15 | } 16 | 17 | set splashscreenHidden (value) { 18 | this._splashscreenHidden = value 19 | } 20 | 21 | get splashscreenHidden () { 22 | return this._splashscreenHidden 23 | } 24 | 25 | hide () { 26 | return Promise.resolve() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/abstracts/AbstractTransition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file AbstractTransition.js 23 | * @author Quentin Neyraud 24 | * @author Adrien Scholaert 25 | */ 26 | import Utils from '../utils/Utils' 27 | import AbstractService from './AbstractService' 28 | 29 | /** 30 | * Base class for creating transition. 31 | * 32 | * @abstract 33 | */ 34 | export default class AbstractTransition extends AbstractService { 35 | /** 36 | * Constructor. 37 | * Do not override this method. 38 | * 39 | * @constructor 40 | */ 41 | constructor (container, serviceName = 'Transition', dependencies = []) { 42 | super(container, serviceName, dependencies) 43 | 44 | /** 45 | * @type {AbstractPage|null} old Page instance 46 | */ 47 | this.oldPage = null 48 | 49 | /** 50 | * @type {AbstractPage|null} 51 | */ 52 | this.newPage = null 53 | 54 | /** 55 | * @type {Promise|null} 56 | */ 57 | this.newPageLoading = null 58 | 59 | /** 60 | * @type {(HTMLElement|null)} 61 | */ 62 | this.originElement = null 63 | 64 | /** 65 | * @type {(Object|null)} 66 | */ 67 | this.cursorPosition = null 68 | } 69 | 70 | /** 71 | * Initialize transition. 72 | * Do not override this method. 73 | * 74 | * @param {AbstractPage} oldPage 75 | * @param {Promise} newPagePromise 76 | * @param {(HTMLElement|null)} el The html element where the transition has been launched 77 | * @param {Object} cursorPosition The cursor position when the transition has been launched 78 | * @returns {Promise} 79 | */ 80 | init (oldPage, newPagePromise, el, cursorPosition) { 81 | this.oldPage = oldPage 82 | this._newPagePromise = newPagePromise 83 | this.originElement = el 84 | this.cursorPosition = cursorPosition 85 | this.deferred = Utils.deferred() 86 | this.newPageReady = Utils.deferred() 87 | this.newPageLoading = this.newPageReady.promise 88 | 89 | this.start() 90 | 91 | this._newPagePromise.then(newPage => { 92 | this.newPage = newPage 93 | this.newPageReady.resolve() 94 | }) 95 | 96 | return this.deferred.promise 97 | } 98 | 99 | /** 100 | * Call this function when the Transition is finished. 101 | */ 102 | done () { 103 | this.destroyOldPage() 104 | 105 | const visibility = this.newPage.rootElement.style.visibility 106 | if (visibility !== 'inherit' || visibility !== 'hidden') { 107 | this.newPage.rootElement.style.visibility = 'visible' 108 | } 109 | 110 | this.deferred.resolve() 111 | } 112 | 113 | scrollTop () { 114 | if (document.scrollingElement) { 115 | document.scrollingElement.scrollTop = 0 116 | document.scrollingElement.scrollTo(0, 0) 117 | } else { 118 | document.body.scrollTop = 0 119 | document.documentElement.scrollTop = 0 120 | window.scrollTo(0, 0) 121 | } 122 | } 123 | 124 | destroyOldPage () { 125 | if (this.oldPage) { 126 | this.oldPage.destroy() 127 | this.oldPage = null 128 | } 129 | } 130 | 131 | async buildNewPage () { 132 | if (this.container) { 133 | const pjaxService = this.getService('Pjax') 134 | const domService = this.getService('Dom') 135 | const pageBuilderService = this.getService('PageBuilder') 136 | 137 | // Add the new dom 138 | domService.putContainer(pjaxService.containerElement) 139 | // Build the new page 140 | this.newPage = await pageBuilderService.buildPage(pjaxService.containerElement) 141 | // Then notify 142 | pjaxService.onNewPageLoaded(this.newPage) 143 | } 144 | } 145 | 146 | /** 147 | * Entry point to create a custom Transition. 148 | * @abstract 149 | */ 150 | start () {} 151 | } 152 | -------------------------------------------------------------------------------- /src/abstracts/AbstractTransitionFactory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file AbstractTransitionFactory.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import AbstractService from './AbstractService' 9 | 10 | /** 11 | * Abstract Transition mapper class. 12 | * 13 | * This class maps your `data-transition` with your *ES6* classes. 14 | * 15 | * **You must define your own ClassFactory for each of your projects.**. 16 | * @abstract 17 | */ 18 | export default class AbstractTransitionFactory extends AbstractService { 19 | /** 20 | * Get Transition 21 | * 22 | * @param {Object} previousState 23 | * @param {Object} state 24 | * @returns {AbstractTransition} 25 | * @abstract 26 | */ 27 | getTransition (previousState, state) {} 28 | } 29 | -------------------------------------------------------------------------------- /src/bundle.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @name Starting Blocks 3 | * @license MIT 4 | * @copyright Copyright © 2018, Rezo Zero 5 | * @version 5.0.0 6 | * @author Adrien Scholaert 7 | * @author Ambroise Maupate 8 | */ 9 | 10 | import * as EventTypes from './types/EventTypes' 11 | import StartingBlocks from './StartingBlocks' 12 | 13 | export { default as PageBuilder } from './services/PageBuilder' 14 | export { default as BlockBuilder } from './services/BlockBuilder' 15 | export { default as Pjax } from './services/Pjax' 16 | export { default as History } from './services/History' 17 | export { default as Prefetch } from './services/Prefetch' 18 | export { default as CacheProvider } from './services/CacheProvider' 19 | export { default as AbstractPage } from './abstracts/AbstractPage' 20 | export { default as AbstractBlock } from './abstracts/AbstractBlock' 21 | export { default as AbstractInViewBlock } from './abstracts/AbstractInViewBlock' 22 | export { default as AbstractBlockBuilder } from './abstracts/AbstractBlockBuilder' 23 | export { default as AbstractService } from './abstracts/AbstractService' 24 | export { default as AbstractSplashscreen } from './abstracts/AbstractSplashscreen' 25 | export { default as AbstractTransitionFactory } from './abstracts/AbstractTransitionFactory' 26 | export { default as AbstractTransition } from './abstracts/AbstractTransition' 27 | export { default as DefaultTransition } from './transitions/DefaultTransition' 28 | export { default as Utils } from './utils/Utils' 29 | export { default as Scroll } from './utils/Scroll' 30 | export { default as polyfills } from './utils/polyfills' 31 | export { default as gaTrackErrors } from './utils/gaTrackErrors' 32 | export { default as debounce } from './utils/debounce' 33 | export { default as BootstrapMedia } from './utils/BootstrapMedia' 34 | export { default as Dispatcher } from './dispatcher/Dispatcher' 35 | export { EventTypes } 36 | 37 | export default StartingBlocks 38 | -------------------------------------------------------------------------------- /src/dispatcher/Dispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file Events.js 23 | * @author Ambroise Maupate 24 | */ 25 | 26 | import { debug } from '../utils/Logger' 27 | 28 | /** 29 | * Event dispatcher. 30 | */ 31 | export default class Dispatcher { 32 | static commit (eventType, detail) { 33 | const event = new window.CustomEvent(eventType, { detail }) 34 | debug('🚩 Dispatched ' + eventType) 35 | window.dispatchEvent(event) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/errors/DependencyNotFulfilledException.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file DependencyNotFulfilledException.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | export default class DependencyNotFulfilledException extends Error { 9 | constructor (firstServiceName, secondeServiceName) { 10 | super(`Object of type "${firstServiceName}" needs "${secondeServiceName}" service`) 11 | this.name = `DependencyNotFulfilledException` 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/errors/UnknownServiceException.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright © 2017, Rezo Zero 4 | * 5 | * @file UnknownServiceException.js 6 | * @author Adrien Scholaert 7 | */ 8 | 9 | export default class UnknownServiceException extends Error { 10 | constructor (id) { 11 | super(`Service "${id}" is not defined`) 12 | this.name = `UnknownServiceException` 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/DefaultPage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file DefaultPage.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import AbstractPage from '../abstracts/AbstractPage' 9 | 10 | export default class DefaultPage extends AbstractPage { 11 | constructor (container) { 12 | super(container, 'DefaultPage') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/services/BlockBuilder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file BlockBuilder.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | import AbstractBlockBuilder from '../abstracts/AbstractBlockBuilder' 9 | import { debug } from '../utils/Logger' 10 | 11 | export default class BlockBuilder extends AbstractBlockBuilder { 12 | constructor (container, serviceName = 'BlockBuilder') { 13 | super(container, serviceName) 14 | 15 | debug(`☕️ ${serviceName} awake`) 16 | } 17 | 18 | /** 19 | * Returns an `AbstractBlock` child class instance 20 | * according to the nodeTypeName or an AbstractBlock as default. 21 | * 22 | * Comment out the default case if you don’t want a default block to be instantiated 23 | * for each block. 24 | * 25 | * @param {String} blockType 26 | * @return {AbstractBlock} 27 | */ 28 | async getBlockInstance (blockType) { 29 | if (this.hasService(blockType)) { 30 | return this.getService(blockType).instance() 31 | } 32 | 33 | return null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/services/CacheProvider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file CacheProvider.js 23 | * @author Ambroise Maupate 24 | * @author Adrien Scholaert 25 | */ 26 | 27 | import AbstractService from '../abstracts/AbstractService' 28 | import { debug } from '../utils/Logger' 29 | 30 | /** 31 | * Cache provider class. 32 | * 33 | * This class stores Ajax response in memory. 34 | */ 35 | export default class CacheProvider extends AbstractService { 36 | constructor (container, serviceName = 'CacheProvider') { 37 | super(container, serviceName) 38 | 39 | debug(`☕️ ${serviceName} awake`) 40 | 41 | this.data = {} 42 | } 43 | 44 | /** 45 | * @param {String} key 46 | * @return {Boolean} 47 | */ 48 | exists (key) { 49 | return key in this.data 50 | } 51 | 52 | /** 53 | * @param {String} href 54 | * @return {Object} 55 | */ 56 | get (href) { 57 | return this.data[href] 58 | } 59 | 60 | /** 61 | * @param {String} key 62 | * @param {Object} data 63 | * @return {CacheProvider} this 64 | */ 65 | set (key, data) { 66 | this.data[key] = data 67 | return this 68 | } 69 | 70 | /** 71 | * Flush the cache 72 | */ 73 | reset () { 74 | this.data = {} 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/services/Dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017. Ambroise Maupate and Julien Blanchet 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * The above copyright notice and this permission notice shall be included in all 11 | * copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. 20 | * 21 | * Except as contained in this notice, the name of the ROADIZ shall not 22 | * be used in advertising or otherwise to promote the sale, use or other dealings 23 | * in this Software without prior written authorization from Ambroise Maupate and Julien Blanchet. 24 | * 25 | * @file Dom.js 26 | * @author Adrien Scholaert 27 | */ 28 | 29 | import AbstractService from '../abstracts/AbstractService' 30 | import { debug } from '../utils/Logger' 31 | import Dispatcher from '../dispatcher/Dispatcher' 32 | import { AFTER_DOM_APPENDED } from '../types/EventTypes' 33 | 34 | /** 35 | * Class that is going to deal with DOM parsing/manipulation. 36 | */ 37 | export default class Dom extends AbstractService { 38 | /** 39 | * Constructor. 40 | * 41 | * @param {Object} container 42 | * @param {String} serviceName 43 | */ 44 | constructor (container, serviceName = 'Dom') { 45 | super(container, serviceName) 46 | 47 | debug(`☕️ ${serviceName} awake`) 48 | 49 | /** 50 | * Full HTML String of the current page. 51 | * By default is the innerHTML of the initial loaded page. 52 | * 53 | * Each time a new page is loaded, the value is the response of the ajax call. 54 | * 55 | * @type {String} 56 | * @default 57 | */ 58 | this.currentHTML = document.documentElement.innerHTML 59 | } 60 | 61 | /** 62 | * Parse the responseText obtained from the ajax call. 63 | * 64 | * @param {String} responseText 65 | * @return {HTMLElement} 66 | */ 67 | parseResponse (responseText) { 68 | this.currentHTML = responseText 69 | 70 | const wrapper = document.createElement('div') 71 | wrapper.innerHTML = responseText 72 | 73 | return this.getContainer(wrapper) 74 | } 75 | 76 | /** 77 | * Get the main wrapper by the ID `wrapperId`. 78 | * 79 | * @return {HTMLElement} element 80 | */ 81 | getWrapper () { 82 | const wrapper = document.getElementById(this.getService('Config').wrapperId) 83 | 84 | if (!wrapper) { 85 | throw new Error('Starting Blocks: Wrapper not found!') 86 | } 87 | 88 | return wrapper 89 | } 90 | 91 | /** 92 | * Return node type. 93 | * 94 | * @param container 95 | * @returns {string} 96 | */ 97 | getNodeType (container) { 98 | return container.getAttribute(this.getService('Config').objectTypeAttr) 99 | } 100 | 101 | /** 102 | * Get the container on the current DOM, 103 | * or from an HTMLElement passed via argument. 104 | * 105 | * @param {HTMLElement|null} element 106 | * @return {HTMLElement} 107 | */ 108 | getContainer (element = null) { 109 | if (!element) { element = document.body } 110 | 111 | if (!element) { 112 | throw new Error('Starting Blocks: DOM not ready!') 113 | } 114 | 115 | const container = this.parseContainer(element) 116 | 117 | if (!container) { 118 | throw new Error(`Starting Blocks: container not found! Did you use at least 119 | one dom element with ".${this.getService('Config').pageClass}" class and "data-node-type" attribute?`) 120 | } 121 | 122 | return container 123 | } 124 | 125 | /** 126 | * Put the container on the page. 127 | * 128 | * @param {HTMLElement} element 129 | */ 130 | putContainer (element) { 131 | element.style.visibility = 'hidden' 132 | const wrapper = this.getWrapper() 133 | wrapper.appendChild(element) 134 | 135 | // Dispatch an event 136 | Dispatcher.commit(AFTER_DOM_APPENDED, { 137 | element, 138 | currentHTML: this.getService('Dom').currentHTML 139 | }) 140 | } 141 | 142 | /** 143 | * Get container selector. 144 | * 145 | * @param {HTMLElement} element 146 | * @return {HTMLElement} element 147 | */ 148 | parseContainer (element) { 149 | return element.querySelector(`.${this.getService('Config').pageClass}[data-node-type]`) 150 | } 151 | 152 | /** 153 | * Update body attributes. 154 | * 155 | * @param {AbstractPage} page 156 | */ 157 | updateBodyAttributes (page) { 158 | // Change body class and id 159 | if (page.name) { 160 | document.body.id = page.name 161 | document.body.classList.add(page.name) 162 | } 163 | 164 | document.body.classList.add(page.type) 165 | 166 | if (page.isHome) { 167 | document.body.setAttribute('data-is-home', '1') 168 | } else { 169 | document.body.setAttribute('data-is-home', '0') 170 | } 171 | } 172 | 173 | /** 174 | * Update page title. 175 | * 176 | * @param {AbstractPage} page 177 | */ 178 | updatePageTitle (page) { 179 | if (page.metaTitle) { 180 | document.title = page.metaTitle 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/services/History.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file History.js 23 | * @author Adrien Scholaert 24 | */ 25 | 26 | import AbstractService from '../abstracts/AbstractService' 27 | import { debug } from '../utils/Logger' 28 | 29 | /** 30 | * HistoryManager helps to keep track of the navigation. 31 | * 32 | * @type {Object} 33 | */ 34 | export default class History extends AbstractService { 35 | constructor (container, serviceName = 'History') { 36 | super(container, serviceName) 37 | 38 | debug(`☕️ ${serviceName} awake`) 39 | 40 | /** 41 | * Keep track of the status in historic order. 42 | * 43 | * @readOnly 44 | * @type {Array} 45 | */ 46 | this.history = [] 47 | } 48 | 49 | /** 50 | * Add a new set of url and namespace. 51 | * 52 | * @param {String} url 53 | * @param {String} transitionName 54 | * @param {String} context (ajax, history) 55 | * @param {Object} data (optional data) 56 | * 57 | * @return {Object} 58 | */ 59 | add (url, transitionName, context, data = {}) { 60 | const state = { url, transitionName, context, data } 61 | this.history.push(state) 62 | return state 63 | } 64 | 65 | /** 66 | * Return information about the current status. 67 | * 68 | * @return {Object} 69 | */ 70 | currentStatus () { 71 | return this.history[this.history.length - 1] 72 | } 73 | 74 | /** 75 | * Return information about the previous status. 76 | * 77 | * @return {Object} 78 | */ 79 | prevStatus () { 80 | const history = this.history 81 | 82 | if (history.length < 2) { return null } 83 | 84 | return history[history.length - 2] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/services/PageBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file PageBuilder.js 3 | * @author Ambroise Maupate 4 | * @author Adrien Scholaert 5 | */ 6 | 7 | import Dispatcher from '../dispatcher/Dispatcher' 8 | import { 9 | AFTER_PAGE_BOOT, 10 | BEFORE_SPLASHSCREEN_HIDE, 11 | AFTER_SPLASHSCREEN_HIDE 12 | } from '../types/EventTypes' 13 | import AbstractBootableService from '../abstracts/AbstractBootableService' 14 | import { debug } from '../utils/Logger' 15 | 16 | /** 17 | * PageBuilder. 18 | */ 19 | export default class PageBuilder extends AbstractBootableService { 20 | constructor (container, serviceName = 'PageBuilder') { 21 | super(container, serviceName, ['Dom']) 22 | 23 | debug(`☕️ ${serviceName} awake`) 24 | 25 | if (!window.location.origin) { 26 | window.location.origin = window.location.protocol + '//' + window.location.host 27 | } 28 | 29 | /** 30 | * Page instance 31 | * @type {(AbstractPage|null)} 32 | */ 33 | this.page = null 34 | 35 | // Bind methods 36 | this.buildPage = this.buildPage.bind(this) 37 | } 38 | 39 | boot () { 40 | super.boot() 41 | 42 | // Build first page with static context 43 | this.buildPage(this.getService('Dom').getContainer(), 'static') 44 | } 45 | 46 | /** 47 | * Build a new page instance. 48 | * 49 | * @param {HTMLElement} rootElement 50 | * @param {String} context 51 | * @returns {AbstractPage|null} 52 | */ 53 | async buildPage (rootElement, context = 'ajax') { 54 | let nodeTypeName = this.getService('Dom').getNodeType(rootElement) 55 | 56 | if (this.hasService(nodeTypeName)) { 57 | this.page = this.getService(nodeTypeName).instance() 58 | } else { 59 | nodeTypeName = 'DefaultPage' 60 | this.page = this.getService('DefaultPage').instance() 61 | } 62 | 63 | // Set some values 64 | this.page.type = nodeTypeName 65 | this.page.context = context 66 | this.page.id = rootElement.id 67 | this.page.rootElement = rootElement 68 | this.page.name = rootElement.hasAttribute('data-node-name') ? rootElement.getAttribute('data-node-name') : '' 69 | this.page.metaTitle = rootElement.hasAttribute('data-meta-title') ? rootElement.getAttribute('data-meta-title') : '' 70 | this.page.isHome = rootElement.getAttribute('data-is-home') === '1' 71 | 72 | await this.page.init() 73 | 74 | // Dispatch an event to inform that the new page is ready 75 | Dispatcher.commit(AFTER_PAGE_BOOT, this.page) 76 | 77 | if (this.hasService('Splashscreen') && !this.getService('Splashscreen').splashscreenHidden) { 78 | Dispatcher.commit(BEFORE_SPLASHSCREEN_HIDE, this.page) 79 | 80 | this.getService('Splashscreen') 81 | .hide() 82 | .then(() => { 83 | Dispatcher.commit(AFTER_SPLASHSCREEN_HIDE, this.page) 84 | this.getService('Splashscreen').splashscreenHidden = true 85 | }) 86 | } 87 | 88 | return this.page 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/services/Pjax.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2017, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file Pjax.js 23 | * @author Adrien Scholaert 24 | */ 25 | 26 | import Utils from '../utils/Utils' 27 | import Dispatcher from '../dispatcher/Dispatcher' 28 | import { 29 | CONTAINER_READY, 30 | AFTER_PAGE_LOAD, 31 | TRANSITION_START, 32 | TRANSITION_COMPLETE, 33 | BEFORE_PAGE_LOAD 34 | } from '../types/EventTypes' 35 | import AbstractBootableService from '../abstracts/AbstractBootableService' 36 | import DefaultTransition from '../transitions/DefaultTransition' 37 | import { debug } from '../utils/Logger' 38 | 39 | /** 40 | * Pjax. 41 | */ 42 | export default class Pjax extends AbstractBootableService { 43 | constructor (container, serviceName = 'Pjax') { 44 | super(container, serviceName, ['Dom', 'Config', 'History', 'PageBuilder']) 45 | 46 | debug(`☕️ ${serviceName} awake`) 47 | 48 | /** 49 | * Indicate if there is an animation in progress. 50 | * 51 | * @readOnly 52 | * @type {Boolean} 53 | */ 54 | this.transitionProgress = false 55 | 56 | /** 57 | * @type {(HTMLElement|null)} 58 | * The latest page content loaded 59 | */ 60 | this.containerElement = null 61 | 62 | // Bind methods 63 | this.onNewPageLoaded = this.onNewPageLoaded.bind(this) 64 | this.onTransitionEnd = this.onTransitionEnd.bind(this) 65 | this.onLinkClick = this.onLinkClick.bind(this) 66 | this.onStateChange = this.onStateChange.bind(this) 67 | } 68 | 69 | /** 70 | * Init the events. 71 | * 72 | * @private 73 | */ 74 | boot () { 75 | super.boot() 76 | 77 | const wrapper = this.getService('Dom').getWrapper() 78 | wrapper.setAttribute('aria-live', 'polite') 79 | 80 | this.currentState = this.getService('History').add(this.getCurrentUrl(), null, 'static') 81 | 82 | this.bindEvents() 83 | } 84 | 85 | /** 86 | * Attach event listeners. 87 | * 88 | * @private 89 | */ 90 | bindEvents () { 91 | document.addEventListener('click', this.onLinkClick) 92 | window.addEventListener('popstate', this.onStateChange) 93 | } 94 | 95 | /** 96 | * Return the currentURL cleaned. 97 | * 98 | * @return {String} currentUrl 99 | */ 100 | getCurrentUrl () { 101 | // TODO, clean from what? currenturl do not takes hash.. 102 | return Utils.cleanLink(Utils.getCurrentUrl()) 103 | } 104 | 105 | /** 106 | * Change the URL with push state and trigger the state change. 107 | * 108 | * @param {String} url 109 | * @param {String} transitionName 110 | * @param {HTMLElement} element The element 111 | * @param {Object} cursorPosition 112 | */ 113 | goTo (url, transitionName, element, cursorPosition) { 114 | const currentPosition = window.scrollY 115 | window.history.pushState(null, null, url) 116 | window.scrollTo(0, currentPosition) 117 | this.onStateChange(transitionName, true, element, cursorPosition) 118 | } 119 | 120 | /** 121 | * Force the browser to go to a certain url. 122 | * 123 | * @param {String} url 124 | * @private 125 | */ 126 | forceGoTo (url) { 127 | window.location = url 128 | } 129 | 130 | /** 131 | * Load an url, will start an ajax request or load from the cache. 132 | * 133 | * @private 134 | * @param {String} url 135 | * @return {Promise} 136 | */ 137 | load (url) { 138 | const deferred = Utils.deferred() 139 | 140 | // Check cache 141 | let request = null 142 | 143 | if (this.hasService('CacheProvider')) { 144 | request = this.getService('CacheProvider').get(url) 145 | } 146 | 147 | // If no cache, make request 148 | if (!request) { 149 | request = Utils.request(url) 150 | 151 | // If cache provider, cache the request 152 | if (this.hasService('CacheProvider')) { 153 | this.getService('CacheProvider').set(url, request) 154 | } 155 | } 156 | 157 | // When data are loaded 158 | request 159 | .then(async data => { 160 | this.containerElement = this.getService('Dom').parseResponse(data) 161 | 162 | // Dispatch an event 163 | Dispatcher.commit(AFTER_PAGE_LOAD, { 164 | container: this.containerElement, 165 | currentHTML: this.getService('Dom').currentHTML 166 | }) 167 | 168 | // Add new container to the DOM if manual DOM Append is disable 169 | if (!this.getService('Config').manualDomAppend) { 170 | this.getService('Dom').putContainer(this.containerElement) 171 | 172 | // Build page 173 | const page = await this.getService('PageBuilder').buildPage(this.containerElement) 174 | 175 | deferred.resolve(page) 176 | } else { 177 | deferred.resolve(null) 178 | } 179 | }) 180 | .catch((err) => { 181 | console.error(err) 182 | this.forceGoTo(url) 183 | deferred.reject() 184 | }) 185 | 186 | return deferred.promise 187 | } 188 | 189 | /** 190 | * Get the .href parameter out of a link element 191 | * 192 | * @private 193 | * @param {HTMLElement} el 194 | * @return {String|undefined} href 195 | */ 196 | getHref (el) { 197 | if (!el) { 198 | return undefined 199 | } 200 | 201 | // Check if has a href and if it's a link element 202 | if (typeof el.href === 'string' && el.tagName.toUpperCase() === 'A') { 203 | return el.href 204 | } 205 | 206 | return undefined 207 | } 208 | 209 | /** 210 | * Get transition name from HTMLElement attribute (data-transition). 211 | * 212 | * @param {HTMLElement} el 213 | * @returns {String|undefined} The transition name 214 | */ 215 | getTransitionName (el) { 216 | if (!el) { 217 | return null 218 | } 219 | 220 | if (el.getAttribute && typeof el.getAttribute('data-transition') === 'string') { 221 | return el.getAttribute('data-transition') 222 | } 223 | 224 | return null 225 | } 226 | 227 | /** 228 | * Callback called from click event. 229 | * 230 | * @private 231 | * @param {MouseEvent} evt 232 | */ 233 | onLinkClick (evt) { 234 | /** 235 | * @type {HTMLElement|Node|EventTarget} 236 | */ 237 | let el = evt.target 238 | 239 | // Go up in the node list until we 240 | // find something with an href 241 | while (el && !this.getHref(el)) { 242 | el = el.parentNode 243 | } 244 | 245 | if (this.preventCheck(evt, el)) { 246 | evt.preventDefault() 247 | 248 | this.linkHash = el.hash.split('#')[1] 249 | 250 | const href = this.getHref(el) 251 | const transitionName = this.getTransitionName(el) 252 | const cursorPosition = { 253 | x: evt.clientX, 254 | y: evt.clientY 255 | } 256 | this.goTo(href, transitionName, el, cursorPosition) 257 | } 258 | } 259 | 260 | /** 261 | * Determine if the link should be followed. 262 | * 263 | * @param {MouseEvent} evt 264 | * @param {HTMLElement} element 265 | * @return {Boolean} 266 | */ 267 | preventCheck (evt, element) { 268 | if (!window.history.pushState) { return false } 269 | 270 | const href = this.getHref(element) 271 | 272 | // User 273 | if (!element || !href) { return false } 274 | 275 | // Middle click, cmd click, and ctrl click 276 | if (evt.which > 1 || evt.metaKey || evt.ctrlKey || evt.shiftKey || evt.altKey) { return false } 277 | 278 | // Ignore target with _blank target 279 | if (element.target && element.target === '_blank') { return false } 280 | 281 | // Check if it's the same domain 282 | if (window.location.protocol !== element.protocol || window.location.hostname !== element.hostname) { return false } 283 | 284 | // Check if the port is the same 285 | if (Utils.getPort() !== Utils.getPort(element.port)) { return false } 286 | 287 | // Ignore case when a hash is being tacked on the current URL 288 | // if (href.indexOf('#') > -1) 289 | // return false; 290 | 291 | // Ignore case where there is download attribute 292 | if (element.getAttribute && typeof element.getAttribute('download') === 'string') { return false } 293 | 294 | // In case you're trying to load the same page 295 | if (Utils.cleanLink(href) === Utils.cleanLink(window.location.href)) { return false } 296 | 297 | return !element.classList.contains(this.getService('Config').noAjaxLinkClass) 298 | } 299 | 300 | /** 301 | * Return a transition object. 302 | * 303 | * @param {object} prev historyManager 304 | * @param {object} current historyManager 305 | * @return {AbstractTransition} Transition object 306 | */ 307 | getTransition (prev, current) { 308 | if (this.hasService('TransitionFactory')) { 309 | return this.getService('TransitionFactory').getTransition(prev, current) 310 | } else { 311 | return new DefaultTransition() 312 | } 313 | } 314 | 315 | /** 316 | * Method called after a 'popstate' or from .goTo(). 317 | * 318 | * @private 319 | */ 320 | onStateChange (transitionName = null, isAjax = false, el = null, cursorPosition = null) { 321 | const newUrl = this.getCurrentUrl() 322 | 323 | if (this.transitionProgress) { this.forceGoTo(newUrl) } 324 | 325 | if (this.getService('History').currentStatus().url === newUrl) { return false } 326 | 327 | // If transition name is a string, a link have been click 328 | // Otherwise back/forward buttons have been pressed 329 | if (typeof transitionName === 'string' || isAjax) { 330 | this.currentState = this.getService('History').add(newUrl, transitionName, 'ajax') 331 | } else { 332 | this.currentState = this.getService('History').add(newUrl, null, '_history') 333 | } 334 | 335 | // Dispatch an event to inform that the page is being load 336 | Dispatcher.commit(BEFORE_PAGE_LOAD, { 337 | currentStatus: this.getService('History').currentStatus(), 338 | prevStatus: this.getService('History').prevStatus() 339 | }) 340 | 341 | // Load the page with the new url (promise is return) 342 | const newPagePromise = this.load(newUrl) 343 | 344 | // Get the page transition instance (from prev and current state) 345 | const transition = this.getTransition( 346 | this.getService('History').prevStatus(), 347 | this.getService('History').currentStatus() 348 | ) 349 | 350 | this.transitionProgress = true 351 | 352 | // Dispatch an event that the transition is started 353 | Dispatcher.commit(TRANSITION_START, { 354 | transition: transition, 355 | currentStatus: this.getService('History').currentStatus(), 356 | prevStatus: this.getService('History').prevStatus() 357 | }) 358 | 359 | // Start the transition (with the current page, and the new page load promise) 360 | const transitionPromise = transition.init( 361 | this.getService('PageBuilder').page, 362 | newPagePromise, 363 | el, 364 | cursorPosition 365 | ) 366 | 367 | newPagePromise.then(this.onNewPageLoaded) 368 | transitionPromise.then(this.onTransitionEnd) 369 | } 370 | 371 | /** 372 | * Function called as soon the new page is ready. 373 | * 374 | * @private 375 | * @param {AbstractPage} page 376 | */ 377 | onNewPageLoaded (page) { 378 | if (page) { 379 | const currentStatus = this.getService('History').currentStatus() 380 | 381 | // Update body attributes (class, id, data-attributes 382 | this.getService('Dom').updateBodyAttributes(page) 383 | // Update the page title 384 | this.getService('Dom').updatePageTitle(page) 385 | // Send google analytic data 386 | Utils.trackGoogleAnalytics() 387 | 388 | // Update the current state 389 | if (this.currentState && page) { 390 | if (!this.currentState.data.title && page.metaTitle) { 391 | this.currentState.data.title = page.metaTitle 392 | } 393 | } 394 | 395 | Dispatcher.commit(CONTAINER_READY, { 396 | currentStatus, 397 | prevStatus: this.getService('History').prevStatus(), 398 | currentHTML: this.getService('Dom').currentHTML, 399 | page 400 | }) 401 | } 402 | } 403 | 404 | /** 405 | * Function called as soon the transition is finished. 406 | * 407 | * @private 408 | */ 409 | onTransitionEnd () { 410 | this.transitionProgress = false 411 | 412 | if (this.linkHash) { 413 | window.location.hash = '' 414 | window.location.hash = this.linkHash 415 | 416 | this.linkHash = null 417 | } 418 | 419 | Dispatcher.commit(TRANSITION_COMPLETE, { 420 | currentStatus: this.getService('History').currentStatus(), 421 | prevStatus: this.getService('History').prevStatus() 422 | }) 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/services/Prefetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2017, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file Prefetch.js 23 | * @author Adrien Scholaert 24 | */ 25 | 26 | import Utils from '../utils/Utils' 27 | import AbstractBootableService from '../abstracts/AbstractBootableService' 28 | import { debug } from '../utils/Logger' 29 | 30 | /** 31 | * Prefetch. 32 | * 33 | * @type {Object} 34 | */ 35 | export default class Prefetch extends AbstractBootableService { 36 | constructor (container, serviceName = 'Prefetch') { 37 | super(container, serviceName, ['Pjax', 'Config']) 38 | 39 | debug(`☕️ ${serviceName} awake`) 40 | } 41 | 42 | boot () { 43 | super.boot() 44 | 45 | if (!window.history.pushState) { 46 | return false 47 | } 48 | 49 | document.body.addEventListener('mouseover', this.onLinkEnter.bind(this)) 50 | document.body.addEventListener('touchstart', this.onLinkEnter.bind(this)) 51 | } 52 | 53 | onLinkEnter (evt) { 54 | let el = evt.target 55 | 56 | while (el && !this.getService('Pjax').getHref(el)) { 57 | el = el.parentNode 58 | } 59 | 60 | if (!el || el.classList.contains(this.getService('Config').noPrefetchClass)) { 61 | return 62 | } 63 | 64 | let url = this.getService('Pjax').getHref(el) 65 | 66 | // Check if the link is eligible for Pjax 67 | if (this.getService('Pjax').preventCheck(evt, el)) { 68 | if (this.hasService('CacheProvider') && this.getService('CacheProvider').get(url)) { 69 | return 70 | } 71 | 72 | let xhr = Utils.request(url) 73 | 74 | if (this.hasService('CacheProvider')) { 75 | this.getService('CacheProvider').set(url, xhr) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/transitions/DefaultTransition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file DefaultTransition.js 23 | * @author Quentin Neyraud 24 | * @author Adrien Scholaert 25 | */ 26 | import AbstractTransition from '../abstracts/AbstractTransition' 27 | 28 | /** 29 | * Default Transition. Show / Hide content. 30 | * 31 | * @extends {AbstractTransition} 32 | */ 33 | export default class DefaultTransition extends AbstractTransition { 34 | start () { 35 | Promise.all([this.newPageLoading]) 36 | .then(this.finish.bind(this)) 37 | } 38 | 39 | finish () { 40 | document.body.scrollTop = 0 41 | this.done() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/types/EventTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file EventTypes.js 23 | * @author Ambroise Maupate 24 | * @author Adrien Scholaert 25 | */ 26 | 27 | /** 28 | * Before initialize XHR request to load new page. 29 | * 30 | * @type {String} 31 | */ 32 | export const BEFORE_PAGE_LOAD = 'SB_BEFORE_PAGE_LOAD' 33 | 34 | /** 35 | * After XHR request succeeded. 36 | * 37 | * @type {String} 38 | */ 39 | export const AFTER_PAGE_LOAD = 'SB_AFTER_PAGE_LOAD' 40 | 41 | /** 42 | * After Dom service appended new page DOM to page-container. 43 | * 44 | * @type {String} 45 | */ 46 | export const AFTER_DOM_APPENDED = 'SB_AFTER_DOM_APPENDED' 47 | 48 | /** 49 | * When new page container is ready. 50 | * 51 | * @type {String} 52 | */ 53 | export const CONTAINER_READY = 'SB_CONTAINER_READY' 54 | 55 | /** 56 | * After PageBuilder create new page instance. 57 | * 58 | * @type {String} 59 | */ 60 | export const AFTER_PAGE_BOOT = 'SB_AFTER_PAGE_BOOT' 61 | 62 | /** 63 | * Before page transition begin. 64 | * 65 | * @type {String} 66 | */ 67 | export const TRANSITION_START = 'SB_TRANSITION_START' 68 | 69 | /** 70 | * After page transition completed. 71 | * 72 | * @type {String} 73 | */ 74 | export const TRANSITION_COMPLETE = 'SB_TRANSITION_COMPLETE' 75 | 76 | /** 77 | * Before splashscreen begin to hide. 78 | * 79 | * @type {String} 80 | */ 81 | export const BEFORE_SPLASHSCREEN_HIDE = 'SB_BEFORE_SPLASHSCREEN_HIDE' 82 | 83 | /** 84 | * When splashscreen start to hide. 85 | * 86 | * @type {String} 87 | */ 88 | export const START_SPLASHSCREEN_HIDE = 'SB_START_SPLASHSCREEN_HIDE' 89 | 90 | /** 91 | * After splashscreen hiding animation. 92 | * 93 | * @type {String} 94 | */ 95 | export const AFTER_SPLASHSCREEN_HIDE = 'SB_AFTER_SPLASHSCREEN_HIDE' 96 | -------------------------------------------------------------------------------- /src/utils/BootstrapMedia.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright REZO ZERO 2016 3 | * 4 | * 5 | * 6 | * @file bootstrapMedia.js 7 | * @copyright REZO ZERO 2016 8 | * @author Ambroise Maupate 9 | * @author Adrien Scholaert 10 | */ 11 | 12 | import Utils from './Utils' 13 | 14 | /** 15 | * Static class to get bootstrap breakpoints. 16 | */ 17 | export default class BootstrapMedia { 18 | constructor () { 19 | // Values 20 | this.viewportSize = null 21 | this.breakpoints = { 22 | xs: 480, 23 | sm: 768, 24 | md: 992, 25 | lg: 1200, 26 | xl: 1920 27 | } 28 | 29 | // Binded methods 30 | this.setValues = this.setValues.bind(this) 31 | 32 | // Init 33 | this.init() 34 | } 35 | 36 | init () { 37 | window.addEventListener('resize', this.setValues) 38 | this.setValues() 39 | } 40 | 41 | setValues () { 42 | this.viewportSize = Utils.getViewportSize() 43 | } 44 | 45 | resize () { 46 | this.setValues() 47 | } 48 | 49 | isMin (breakpoint) { 50 | if (!this.breakpoints[breakpoint]) { 51 | const errorMessage = `Breakpoint '${breakpoint}' do not exist` 52 | console.error(errorMessage) 53 | throw new Error(errorMessage) 54 | } 55 | 56 | return this.viewportSize.width >= this.breakpoints[breakpoint] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/Logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017, Rezo Zero 3 | * 4 | * @file debug.js 5 | * @author Adrien Scholaert 6 | */ 7 | 8 | export function log (...args) { 9 | if (window.startingBlocksDebugLevel === 1) { 10 | console.log('[SB]', ...args) 11 | } 12 | } 13 | 14 | export function debug (message, color = '') { 15 | if (window.startingBlocksDebugLevel === 1) { 16 | console.debug(`%c[SB] %c${message}`, 'color:#749f73', 'color:debug', color) 17 | } 18 | } 19 | 20 | export function info (...args) { 21 | if (window.startingBlocksDebugLevel === 1) { 22 | console.info('[SB]', ...args) 23 | } 24 | } 25 | 26 | export function warn (...args) { 27 | if (window.startingBlocksDebugLevel === 1) { 28 | console.warn('[SB]', ...args) 29 | } 30 | } 31 | 32 | export function error (...args) { 33 | if (window.startingBlocksDebugLevel === 1) { 34 | console.error('[SB]', ...args) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/Scroll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file Scroll.js 23 | * @author Ambroise Maupate 24 | */ 25 | 26 | /** 27 | * @static 28 | */ 29 | export default class Scroll { 30 | /** 31 | * 32 | * @param e 33 | * @private 34 | */ 35 | static _preventDefault (e) { 36 | e = e || window.event 37 | if (e.preventDefault) { e.preventDefault() } 38 | e.returnValue = false 39 | } 40 | 41 | /** 42 | * 43 | * @param e 44 | * @private 45 | */ 46 | static _keydown (e) { 47 | // left: 37, up: 38, right: 39, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 48 | let keys = [37, 38, 39, 40, 33, 34, 35] 49 | for (let i = keys.length; i--;) { 50 | if (e.keyCode === keys[i]) { 51 | Scroll._preventDefault(e) 52 | return 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * 59 | * @param e 60 | * @private 61 | */ 62 | static _wheel (e) { 63 | Scroll._preventDefault(e) 64 | } 65 | 66 | /** 67 | * Disable scroll. 68 | * 69 | * @return {void} 70 | */ 71 | static disable () { 72 | if (window.addEventListener) { 73 | window.addEventListener('DOMMouseScroll', Scroll._wheel, false) 74 | } 75 | window.onmousewheel = document.onmousewheel = Scroll._wheel 76 | document.onkeydown = Scroll._keydown 77 | } 78 | 79 | /** 80 | * Enable scroll again. 81 | * 82 | * @return {void} 83 | */ 84 | static enable () { 85 | if (window.removeEventListener) { 86 | window.removeEventListener('DOMMouseScroll', Scroll._wheel, false) 87 | } 88 | window.onmousewheel = document.onmousewheel = document.onkeydown = null 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/utils/Utils.js: -------------------------------------------------------------------------------- 1 | import { debug } from './Logger' 2 | 3 | /** 4 | * Utils class 5 | */ 6 | export default class Utils { 7 | /** 8 | * @param {String} str 9 | * @return {String} 10 | */ 11 | static stripTrailingSlash (str) { 12 | if (str.substr(-1) === '/') { 13 | return str.substr(0, str.length - 1) 14 | } 15 | return str 16 | } 17 | 18 | /** 19 | * Get port 20 | * 21 | * @param p 22 | * @returns {*} 23 | */ 24 | static getPort (p) { 25 | const port = typeof p !== 'undefined' ? p : window.location.port 26 | const protocol = window.location.protocol 27 | 28 | if (port !== '') { return parseInt(port) } 29 | if (protocol === 'http:') { return 80 } 30 | if (protocol === 'https:') { return 443 } 31 | } 32 | 33 | static cleanLink (url) { 34 | return url.replace(/#.*/, '') 35 | } 36 | 37 | /** 38 | * Get current url 39 | * 40 | * @returns {string} 41 | */ 42 | static getCurrentUrl () { 43 | return window.location.protocol + '//' + 44 | window.location.host + 45 | window.location.pathname + 46 | window.location.search 47 | } 48 | 49 | /** 50 | * Request timeout (in ms) 51 | * 52 | * @returns {number} 53 | */ 54 | static requestTimeout () { 55 | return 10000 56 | } 57 | 58 | /** 59 | * Start a fetch request 60 | * 61 | * @param {String} url 62 | * @return {Promise} 63 | */ 64 | static request (url) { 65 | const dfd = Utils.deferred() 66 | const timeout = window.setTimeout(() => { 67 | window.clearTimeout(timeout) 68 | dfd.reject('timeout!') 69 | }, Utils.requestTimeout()) 70 | 71 | const headers = new window.Headers() 72 | headers.append('X-Starting-Blocks', 'yes') 73 | headers.append('X-Allow-Partial', 'yes') 74 | headers.append('X-Requested-With', 'XMLHttpRequest') 75 | 76 | window.fetch(url, { 77 | method: 'GET', 78 | headers: headers, 79 | cache: 'default', 80 | credentials: 'same-origin' 81 | }).then(res => { 82 | window.clearTimeout(timeout) 83 | 84 | if (res.status >= 200 && res.status < 300) { 85 | return dfd.resolve(res.text()) 86 | } 87 | 88 | const err = new Error(res.statusText || res.status) 89 | return dfd.reject(err) 90 | }).catch(err => { 91 | window.clearTimeout(timeout) 92 | dfd.reject(err) 93 | }) 94 | 95 | return dfd.promise 96 | } 97 | 98 | /** 99 | * Log credits to console for code lovers. 100 | * 101 | * @param {String} siteName 102 | * @param {String} bgColor 103 | * @param {Array} creditsList 104 | * @param {Array} thanksList 105 | * @param {String} textColor (optional) 106 | */ 107 | static logCredits (siteName, bgColor, creditsList, thanksList, textColor) { 108 | let color = '#fff' 109 | if (typeof textColor !== 'undefined') color = textColor 110 | 111 | console.log('%c ', 'font-size:3px;') 112 | console.log('%c' + siteName, 'background:' + bgColor + '; color: ' + color + '; font-size:14px; padding:5px 10px;') 113 | console.log('%c ', 'font-size:3px;') 114 | 115 | if (creditsList !== null) { 116 | let creditsLength = creditsList.length 117 | if (creditsLength) { 118 | for (let indexCredit = 0; indexCredit < creditsLength; indexCredit++) { 119 | console.log(creditsList[indexCredit].name + ' - ' + creditsList[indexCredit].website) 120 | } 121 | } 122 | } 123 | 124 | if (thanksList !== null) { 125 | let thanksLength = thanksList.length 126 | if (thanksLength) { 127 | console.log('-') 128 | console.log('Thanks to') 129 | for (let indexThanks = 0; indexThanks < thanksLength; indexThanks++) { 130 | console.log(thanksList[indexThanks].name + ' (' + thanksList[indexThanks].website + ')') 131 | } 132 | } 133 | } 134 | 135 | console.log('-') 136 | console.log(' ') 137 | } 138 | 139 | /** 140 | * Get random number. 141 | * 142 | * @param {Number} min [min value] 143 | * @param {Number} max [max value] 144 | * @param {Number} decimal 145 | * @return {Number} 146 | */ 147 | static getRandomNumber (min, max, decimal) { 148 | const result = Math.random() * (max - min) + min 149 | 150 | if (typeof decimal !== 'undefined') { 151 | return Number(result.toFixed(decimal)) 152 | } else return result 153 | } 154 | 155 | /** 156 | * Get random integer. 157 | * 158 | * @param {Number} min [min value] 159 | * @param {Number} max [max value] 160 | * @return {Number} 161 | */ 162 | static getRandomInt (min, max) { 163 | return Math.floor(Math.random() * (max - min + 1)) + min 164 | } 165 | 166 | /** 167 | * Send a GA page view event when context is AJAX. 168 | */ 169 | static trackGoogleAnalytics () { 170 | if (typeof window.ga !== 'undefined') { 171 | debug('🚩 Push Analytics for: ' + window.location.pathname) 172 | window.ga('send', 'pageview', { 173 | 'page': window.location.pathname, 174 | 'title': document.title 175 | }) 176 | } 177 | } 178 | 179 | /** 180 | * Match CSS media queries and JavaScript window width. 181 | * 182 | * @see http://stackoverflow.com/a/11310353 183 | * @return {Object} 184 | */ 185 | static getViewportSize () { 186 | let e = window 187 | let a = 'inner' 188 | if (!('innerWidth' in window)) { 189 | a = 'client' 190 | e = document.documentElement || document.body 191 | } 192 | return { width: e[ a + 'Width' ], height: e[ a + 'Height' ] } 193 | } 194 | 195 | /** 196 | * Get a css property with the vendor prefix. 197 | * 198 | * @param {String} property the css property 199 | * @return {String} the prefixed property 200 | */ 201 | static prefixProperty (property) { 202 | const prefixes = ['', 'ms', 'Webkit', 'Moz', 'O'] 203 | const numPrefixes = prefixes.length 204 | const tmp = document.createElement('div') 205 | 206 | for (let i = 0; i < numPrefixes; i++) { 207 | let prefix = prefixes[i] 208 | property = prefix === '' ? property : property.charAt(0).toUpperCase() + property.substring(1).toLowerCase() 209 | const prop = prefix + property 210 | 211 | if (typeof tmp.style[prop] !== 'undefined') { 212 | return prop 213 | } 214 | } 215 | } 216 | 217 | /** 218 | * Gets normalized ratio of value inside range. 219 | * 220 | * from https://github.com/mout/mout/blob/master/src/math/norm.js 221 | * 222 | * @param {Number} val 223 | * @param {Number} min 224 | * @param {Number} max 225 | * @return {Number} 226 | */ 227 | static getNormRatio (val, min, max) { 228 | if (val < min) return 0 229 | if (val > max) return 1 230 | 231 | return val === max ? 1 : (val - min) / (max - min) 232 | } 233 | 234 | /** 235 | * Return a new "Deferred" object 236 | * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred 237 | * 238 | * @return {Deferred} 239 | */ 240 | static deferred () { 241 | return new function () { 242 | this.resolve = null 243 | this.reject = null 244 | 245 | this.promise = new Promise((resolve, reject) => { 246 | this.resolve = resolve 247 | this.reject = reject 248 | }) 249 | }() 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a function, that, as long as it continues to be invoked, will not 3 | * be triggered. 4 | * 5 | * The function will be called after it stops being called for 6 | * N milliseconds. If `immediate` is passed, trigger the function on the 7 | * leading edge, instead of the trailing. 8 | * 9 | * @see http://davidwalsh.name/javascript-debounce-function 10 | * @param {Function} func [function to debounce] 11 | * @param {Number} wait [time to wait] 12 | * @param {Boolean} immediate [] 13 | */ 14 | export default function debounce (func, wait, immediate) { 15 | let timeout 16 | return function () { 17 | let context = this 18 | let args = arguments 19 | let later = function () { 20 | timeout = null 21 | if (!immediate) func.apply(context, args) 22 | } 23 | const callNow = immediate && !timeout 24 | clearTimeout(timeout) 25 | timeout = setTimeout(later, wait) 26 | if (callNow) func.apply(context, args) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/gaTrackErrors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Method to track JS errors if your Google Analytics account. 3 | */ 4 | export default function gaTrackErrors () { 5 | if (typeof ga !== 'undefined') { 6 | // Pure JavaScript errors handler 7 | window.addEventListener('error', function (err) { 8 | const lineAndColumnInfo = err.colno ? ' line:' + err.lineno + ', column:' + err.colno : ' line:' + err.lineno 9 | window.ga( 10 | 'send', 11 | 'event', 12 | 'JavaScript Error', 13 | err.message, 14 | err.filename + lineAndColumnInfo + ' -> ' + navigator.userAgent, 15 | 0, 16 | true 17 | ) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/polyfills.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2016, Ambroise Maupate 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is furnished 9 | * to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | * IN THE SOFTWARE. 21 | * 22 | * @file polyfills.js 23 | * @author Ambroise Maupate 24 | */ 25 | 26 | /** 27 | * Execute some polyfill for older and crappy browsers. 28 | * 29 | * - window.requestAnimFrame 30 | * - window.cancelAnimFrame 31 | * - Avoid `console` errors in browsers that lack a console. 32 | */ 33 | export default function polyfills () { 34 | window.requestAnimFrame = (function () { 35 | return ( 36 | window.requestAnimationFrame || 37 | window.webkitRequestAnimationFrame || 38 | window.mozRequestAnimationFrame || 39 | window.oRequestAnimationFrame || 40 | window.msRequestAnimationFrame || 41 | function (/* function */ callback) { 42 | window.setTimeout(callback, 1000 / 60) // 60fps 43 | } 44 | ) 45 | }()) 46 | 47 | window.cancelAnimFrame = (function () { 48 | return ( 49 | window.cancelAnimationFrame || 50 | window.webkitCancelAnimationFrame || 51 | window.mozCancelAnimationFrame || 52 | window.oCancelAnimationFrame || 53 | window.msCancelAnimationFrame || 54 | function (id) { 55 | window.clearTimeout(id) 56 | } 57 | ) 58 | }()) 59 | 60 | /* 61 | * Avoid `console` errors in browsers that lack a console. 62 | * @return {[type]} [description] 63 | */ 64 | { 65 | let method 66 | const noop = () => {} 67 | const methods = [ 68 | 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 69 | 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 70 | 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 71 | 'timeStamp', 'trace', 'warn' 72 | ] 73 | let length = methods.length 74 | const console = (window.console = window.console || {}) 75 | 76 | while (length--) { 77 | method = methods[length] 78 | 79 | // Only stub undefined methods. 80 | if (!console[method]) { 81 | console[method] = noop 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Array.from polyfill 88 | */ 89 | if (!Array.from) { 90 | Array.from = function (object) { 91 | 'use strict' 92 | return [].slice.call(object) 93 | } 94 | } 95 | 96 | /** 97 | * Array.find polyfill 98 | */ 99 | if (!Array.prototype.find) { 100 | // eslint-disable-next-line no-extend-native 101 | Object.defineProperty(Array.prototype, 'find', { 102 | value: function (predicate) { 103 | if (this === null) { 104 | throw new TypeError('Array.prototype.find called on null or undefined') 105 | } 106 | 107 | if (typeof predicate !== 'function') { 108 | throw new TypeError('predicate must be a function') 109 | } 110 | 111 | let list = Object(this) 112 | let length = list.length >>> 0 113 | let thisArg = arguments[1] 114 | let value 115 | 116 | for (let i = 0; i < length; i++) { 117 | value = list[i] 118 | if (predicate.call(thisArg, value, i, list)) { 119 | return value 120 | } 121 | } 122 | 123 | return undefined 124 | } 125 | }) 126 | } 127 | 128 | /** 129 | * FindIndex polyfill 130 | */ 131 | if (!Array.prototype.findIndex) { 132 | // eslint-disable-next-line no-extend-native 133 | Object.defineProperty(Array.prototype, 'findIndex', { 134 | value: function (predicate) { 135 | // 1. Let O be ? ToObject(this value). 136 | if (this === null) { 137 | throw new TypeError('"this" is null or not defined') 138 | } 139 | 140 | let o = Object(this) 141 | 142 | // 2. Let len be ? ToLength(? Get(O, "length")). 143 | let len = o.length >>> 0 144 | 145 | // 3. If IsCallable(predicate) is false, throw a TypeError exception. 146 | if (typeof predicate !== 'function') { 147 | throw new TypeError('predicate must be a function') 148 | } 149 | 150 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 151 | let thisArg = arguments[1] 152 | 153 | // 5. Let k be 0. 154 | let k = 0 155 | 156 | // 6. Repeat, while k < len 157 | while (k < len) { 158 | // a. Let Pk be ! ToString(k). 159 | // b. Let kValue be ? Get(O, Pk). 160 | // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). 161 | // d. If testResult is true, return k. 162 | let kValue = o[k] 163 | if (predicate.call(thisArg, kValue, k, o)) { 164 | return k 165 | } 166 | // e. Increase k by 1. 167 | k++ 168 | } 169 | 170 | // 7. Return -1. 171 | return -1 172 | } 173 | }) 174 | } 175 | 176 | /** 177 | * Custom event support for IE 178 | */ 179 | (function () { 180 | if (typeof window.CustomEvent === 'function') return false 181 | 182 | function CustomEvent (event, params) { 183 | params = params || { bubbles: false, cancelable: false, detail: undefined } 184 | let evt = document.createEvent('CustomEvent') 185 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) 186 | return evt 187 | } 188 | 189 | CustomEvent.prototype = window.Event.prototype 190 | 191 | window.CustomEvent = CustomEvent 192 | })() 193 | } 194 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import getConfig from './examples/webpack/config' 2 | import getWebpackConfig from './examples/webpack/build' 3 | 4 | module.exports = getWebpackConfig(getConfig()) 5 | --------------------------------------------------------------------------------