├── .babelrc ├── .eslintrc ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── TODO.md ├── changelog.md ├── dist ├── immerser.min.js └── immerser.min.js.map ├── docs ├── favicon.ico ├── index.html ├── main.css ├── main.js ├── main.js.map └── ru.html ├── example ├── content │ ├── code │ │ ├── cloning-event-listeners.html │ │ ├── handle-clone-hover.css │ │ ├── handle-dom-change.js │ │ ├── initialization.js │ │ ├── markup.html │ │ ├── styles.css │ │ ├── table.html │ │ └── table.md │ ├── highlighted-cloning-event-listeners.html │ ├── highlighted-handle-clone-hover.html │ ├── highlighted-handle-dom-change.html │ ├── highlighted-initialization.html │ ├── highlighted-markup.html │ └── highlighted-styles.html ├── favicon │ ├── favicon.gif │ ├── favicon.ico │ ├── favicon.jpg │ ├── favicon.png │ └── favicon.svg ├── index.html ├── main.js ├── styles │ ├── components │ │ ├── about.scss │ │ ├── code-highlight.scss │ │ ├── common.scss │ │ ├── emoji.scss │ │ ├── fixed.scss │ │ ├── footer.scss │ │ ├── header.scss │ │ ├── highlighter.scss │ │ ├── language.scss │ │ ├── logo.scss │ │ ├── menu.scss │ │ ├── pager.scss │ │ └── typography.scss │ ├── main.scss │ └── shared │ │ ├── _breakpoints.scss │ │ ├── _colors.scss │ │ ├── _globals.scss │ │ ├── _grid.scss │ │ └── _transitions.scss └── svg │ ├── how-it-works-face.svg │ ├── how-it-works-hand.svg │ ├── how-to-use.svg │ ├── options.svg │ ├── possibilities.svg │ └── why-immerser.svg ├── generateOptionsTables.js ├── how-to-highlight.md ├── i18n ├── en.js └── ru.js ├── jsconfig.json ├── package.json ├── postBuild.js ├── readme.js ├── src ├── defaults.js ├── immerser.js └── utils.js ├── webpack.config.docs.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8", "ie >= 11"] 9 | } 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // Это наш общий стайлгайд. Он будет дополняться 2 | { 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "parserOptions": { 10 | "parser": "babel-eslint", 11 | "sourceType": "module", 12 | "ecmaVersion": 2018 13 | }, 14 | "extends": ["eslint:recommended"], 15 | "rules": { 16 | // пробелы внутри квадратных скобок массива 17 | "array-bracket-spacing": ["error", "never"], 18 | // стрелка арроу функции обрамляется пробелами с обеих сторон 19 | "arrow-spacing": ["error", { "before": true, "after": true }], 20 | // Хотим, чтобы в конце строки многострочного массива или объека всегда была запятая 21 | // Чтобы при мультивыделении был единообразный конец строк 22 | "comma-dangle": ["error", "always-multiline"], 23 | // не дропаем кудрявые скобки в блоках 24 | "curly": ["error", "all"], 25 | // отступы в 2 пробела 26 | "indent": ["error", 2, { "SwitchCase": 1 }], 27 | // ключевые слова всегда отбиваем пробелами 28 | "keyword-spacing": ["error", { "before": true, "after": true }], 29 | // не используем alert, prompt, confirm 30 | "no-alert": "error", 31 | // не импортируем из одного файла по нескольку раз, чтобы не путаться в импортах 32 | "no-duplicate-imports": ["error", { "includeExports": true }], 33 | // не плодим больше 2 пустых строк в коде 34 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }], 35 | // не бросаем ошибку, но предупреждаем о ненужном экранировании 36 | "no-useless-escape": "warn", 37 | // используем var по назначению 38 | "no-var": "error", 39 | // всегда отбиваем кудрявые скобки в объектах пробелами 40 | "object-curly-spacing": ["error", "always"], 41 | // каждую переменную, константу отдельно объявляем, так лучше видно 42 | "one-var": ["error", { "var": "never", "let": "never", "const": "never" }], 43 | // стрелочные функции лучше читаются и автоматически получают контекст 44 | "prefer-arrow-callback": ["error"], 45 | // если не переназначаем переменную, лучше использовать константу 46 | "prefer-const": "error", 47 | // кавычки одинарные используем 48 | "quotes": ["error", "single", { "allowTemplateLiterals": true }], 49 | // явно вставляем точку с запятой в концах строк, где они подразумеваются движком 50 | "semi": ["error", "always"], 51 | // сортируем импортируемые модули внутри кудрявых скобок 52 | "sort-imports": ["error", { "ignoreDeclarationSort": true }], 53 | // пробелом отбиваем только асинхронную стелочную функцию 54 | "space-before-function-paren": [ 55 | "error", 56 | { 57 | "anonymous": "never", 58 | "named": "never", 59 | "asyncArrow": "always" 60 | } 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dev_dist/ 4 | /.cache 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.language": "en,ru", 3 | "cSpell.words": [ 4 | "BUNDLESIZE", 5 | "classname", 6 | "dubaua", 7 | "gzipped", 8 | "immerser", 9 | "Lysov", 10 | "pseudoselector", 11 | "rsquo", 12 | "statemap", 13 | "гитхаб", 14 | "джаваскрипт", 15 | "джаваскрипте", 16 | "иммёрсер", 17 | "иммёрсера", 18 | "колбек", 19 | "нодам", 20 | "псевдоселектор", 21 | "псевдоселектором", 22 | "скролле", 23 | "скроллом", 24 | "скроллу" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Library for Switching Fixed Elements on Scroll 2 | 3 | Sometimes designers create complex logic and fix parts of the interface. Also they colour page sections contrasted. How to deal with this mess? 4 | 5 | Immerser comes to help you. It’s a javascript library to change fixed elements on scroll. 6 | 7 | Immerser fast, because it calculates states once on init. Then it watches the scroll position and schedules redraw document in the next event loop tick with requestAnimationFrame. Script changes transform property, so it uses graphic hardware acceleration. 8 | 9 | Immerser is written on vanilla js. Only 5.39Kb gzipped. 10 | 11 | ## Terms 12 | 13 | `Immerser root` — is the parent container for your fixed parts `solids`. Actually, solids are positioned absolutely to fixed immerser root. The `layers` are sections of your page. Also you may want to add `pager` to navigate through layers and indicate active state. 14 | 15 | # How to Use 16 | 17 | ## Install 18 | 19 | Using npm: 20 | 21 | ```shell 22 | npm install immerser 23 | ``` 24 | 25 | Using yarn: 26 | 27 | ```shell 28 | yarn add immerser 29 | ``` 30 | 31 | Or if you want to use immerser in browser as global variable: 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | ## Prepare Your Markup 38 | 39 | First, setup fixed container as the immerser root container, and add the `data-immerser` attribute. 40 | 41 | Next place absolutely positioned children into the immerser parent and add `data-immerser-solid="solid-id"` to each. 42 | 43 | Then add `data-immerser-layer` attribute to each section and pass configuration in `data-immerser-layer-config='{"solid-id": "classname-modifier"}'`. Otherwise, you can pass configuration as `solidClassnameArray` option to immerser. Config should contain JSON describing what class should be applied on each solid element, when it's over a section. 44 | 45 | Also feel free to add `data-immerser-pager` to create a pager for your layers. 46 | 47 | ```html 48 |
49 |
50 | 51 | 58 |
59 | english 60 | по-русски 61 |
62 |
63 | © 2022 — Vladimir Lysov, Chelyabinsk, Russia 64 | github 65 | dubaua@gmail.com 66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 | ``` 75 | 76 | ## Apply styles 77 | 78 | Apply colour and background styles to your layers and solids according to your classname configuration passed in data attribute or options. I’m using [BEM methodology](https://en.bem.info/methodology/) in this example. 79 | 80 | ```css 81 | .fixed { 82 | position: fixed; 83 | top: 2em; 84 | bottom: 3em; 85 | left: 3em; 86 | right: 3em; 87 | z-index: 1; 88 | } 89 | .fixed__pager { 90 | position: absolute; 91 | top: 50%; 92 | left: 0; 93 | transform: translate(0, -50%); 94 | } 95 | .fixed__logo { 96 | position: absolute; 97 | top: 0; 98 | left: 0; 99 | } 100 | .fixed__menu { 101 | position: absolute; 102 | top: 0; 103 | right: 0; 104 | } 105 | .fixed__language { 106 | position: absolute; 107 | bottom: 0; 108 | left: 0; 109 | } 110 | .fixed__about { 111 | position: absolute; 112 | bottom: 0; 113 | right: 0; 114 | } 115 | .pager, 116 | .logo, 117 | .menu, 118 | .language, 119 | .about { 120 | color: black; 121 | } 122 | .pager--contrast, 123 | .logo--contrast, 124 | .menu--contrast, 125 | .language--contrast, 126 | .about--contrast { 127 | color: white; 128 | } 129 | ``` 130 | 131 | ## Initialize Immerser 132 | 133 | Include immerser in your code and create immerser instance with options. 134 | 135 | ```js 136 | // You don't have to import immerser 137 | // if you're using it in browser as global variable 138 | import Immerser from 'immerser'; 139 | 140 | const immerserInstance = new Immerser({ 141 | // this option will be overridden by options 142 | // passed in data-immerser-layer-config attribute in each layer 143 | solidClassnameArray: [ 144 | { 145 | logo: 'logo--contrast-lg', 146 | pager: 'pager--contrast-lg', 147 | language: 'language--contrast-lg', 148 | }, 149 | { 150 | pager: 'pager--contrast-only-md', 151 | menu: 'menu--contrast', 152 | about: 'about--contrast', 153 | }, 154 | { 155 | logo: 'logo--contrast-lg', 156 | pager: 'pager--contrast-lg', 157 | language: 'language--contrast-lg', 158 | }, 159 | { 160 | logo: 'logo--contrast-only-md', 161 | pager: 'pager--contrast-only-md', 162 | language: 'language--contrast-only-md', 163 | menu: 'menu--contrast', 164 | about: 'about--contrast', 165 | }, 166 | { 167 | logo: 'logo--contrast-lg', 168 | pager: 'pager--contrast-lg', 169 | language: 'language--contrast-lg', 170 | }, 171 | ], 172 | hasToUpdateHash: true, 173 | fromViewportWidth: 1024, 174 | pagerLinkActiveClassname: 'pager__link--active', 175 | scrollAdjustThreshold: 50, 176 | scrollAdjustDelay: 600, 177 | onInit(immerser) { 178 | // callback on init 179 | }, 180 | onBind(immerser) { 181 | // callback on bind 182 | }, 183 | onUnbind(immerser) { 184 | // callback on unbind 185 | }, 186 | onDestroy(immerser) { 187 | // callback on destroy 188 | }, 189 | onActiveLayerChange(activeIndex, immerser) { 190 | // callback on active layer change 191 | }, 192 | }); 193 | 194 | ``` 195 | 196 | # How it Works 197 | 198 | First, immerser gathers information about the layers, solids, window and document. Then it creates a statemap for each layer, containing all necessary information, when the layer is partially and fully in viewport. 199 | 200 | After that immerser modifies DOM, cloning all solids into mask containers for each layer and applying the classnames given in configuration. If you have added a pager, immerser also creates links for layers. 201 | 202 | Finally, immerser binds listeners to scroll and resize events. On resize, it will meter layers, the window and document heights again and recalculate the statemap. 203 | 204 | On scroll, immerser moves a mask of solids to show part of each solid group according to the layer below. 205 | 206 | # Options 207 | 208 | You can pass options to immerser as data-attributes on layers or as object as function parameter. Data-attributes are processed last, so they override the options passed to the function. 209 | 210 | | option | type | default | description | 211 | | - | - | - | - | 212 | | solidClassnameArray | `array` | `[]` | Array of layer class configurations. Overriding by config passed in data-immerser-layer-config for corresponding layer. Configuration example [is shown above](#initialize-immerser) | 213 | | fromViewportWidth | `number` | `0` | A viewport width, from which immerser will init | 214 | | pagerThreshold | `number` | `0.5` | How much next layer should be in viewport to trigger pager | 215 | | hasToUpdateHash | `boolean` | `false` | Flag to control changing hash on pager active state change | 216 | | scrollAdjustThreshold | `number` | `0` | A distance from the viewport top or bottom to the section top or bottom edge in pixels. If the current distance is below the threshold, the scroll adjustment will be applied. Will not adjust, if zero passed | 217 | | scrollAdjustDelay | `number` | `600` | Delay after user interaction and before scroll adjust | 218 | | pagerLinkActiveClassname | `string` | `pager-link-active` | Added to each pager link pointing to active | 219 | | isScrollHandled | `boolean` | `true` | Binds scroll listener if true. Set to false if you're using remote scroll controller | 220 | | onInit | `function` | `null` | Fired after initialization. Accept an immerser instance as the only parameter | 221 | | onBind | `function` | `null` | Fired after binding DOM. Accept an immerser instance as the only parameter | 222 | | onUnbind | `function` | `null` | Fired after unbinding DOM. Accept an immerser instance as the only parameter | 223 | | onDestroy | `function` | `null` | Fired after destroy. Accept an immerser instance as the only parameter | 224 | | onActiveLayerChange | `function` | `null` | Fired after active layer change. Accept active layer index as first parameter and an immerser instance as second | 225 | 226 | 227 | # Recipes 228 | 229 | ## Cloning Event Listeners 230 | 231 | Since immerser cloning nested nodes by default, all event listeners and data bound on nodes will be lost after init. Fortunately, you can markup the immerser yourself. It can be useful when you have event listeners on solids, reactive logic or more than classname switching. All you need is to place the number of nested immerser masks equal to the number of the layers. Look how I change the smiley emoji on the right in this page source. 232 | 233 | ```html 234 |
235 |
236 |
237 | 238 |
239 |
240 |
241 |
242 | 243 |
244 |
245 |
246 | ``` 247 | 248 | ## Handle Clone Hover 249 | 250 | As mentioned above, immerser cloning nested nodes to achieve changing on scroll. Therefore if you hover a partially visible element, only the visible part will change. If you want to synchronize all cloned links, just pass `data-immerser-synchro-hover="hoverId"` attribute. It will share `_hover` class between all nodes with this `hoverId` when the mouse is over one of them. Add `_hover` selector alongside your `:hover` pseudoselector to style your interactive elements. 251 | 252 | ```css 253 | a:hover, 254 | a._hover { 255 | color: magenta; 256 | } 257 | ``` 258 | 259 | ## Handle DOM change 260 | 261 | Immerser is not aware of changes in DOM, if you dynamically add or remove nodes. If you change height of the document and want immerser to recalculate and redraw solids, call `onDOMChange` method on the immerser instance. 262 | 263 | ```js 264 | // adding or removing node, that changes DOM height 265 | document.appendChild(someNode); 266 | document.removeChild(anotherNode); 267 | 268 | // then explicitly redraw immerser 269 | immerserInstance.onDOMChange(); 270 | ``` 271 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ~~Switch hashes on activeLayer change~~ 4 | 5 | ~~Responsive styles~~ 6 | 7 | ~~Deal with horizontal scroll~~ 8 | 9 | ~~Draw smily faces~~ 10 | 11 | ~~Add easter egg on click on smily face~~ 12 | 13 | ~~Add favicon~~ 14 | 15 | ~~Fix Edge no changes on scroll~~ 16 | 17 | ~~Listen to resize and bound/unbound immerser~~ 18 | 19 | ~~name onresize and onscroll functions with binded context~~ 20 | 21 | ~~fix less 100vh layer height~~ 22 | 23 | ~~add onBind, onUnbind, onDestroy callbacks~~ 24 | 25 | ~~extract utils~~ 26 | 27 | ~~extract defaults~~ 28 | 29 | Ask for and make code review 30 | 31 | Unique animations on smily faces 32 | 33 | batch animation on all faces 34 | 35 | Inline svg sprites 36 | 37 | ~~Add scroll adjust parameter~~ 38 | 39 | ~~autoupdate version in readme and docs~~ 40 | 41 | ~~autoupdate gzipped size~~ 42 | 43 | add adjustable scroll animation 44 | 45 | ~~add russian translation and language switcher~~ 46 | 47 | ~~move selectors to separate object~~ 48 | 49 | ~~create error and warning configuration~~ 50 | 51 | ~~rewrite custom markup to cloning listeners~~ 52 | 53 | ~~rewrite hover synhcro to handle hover~~ 54 | 55 | Write class JSDoc 56 | 57 | Separate public and private methods 58 | 59 | Rewrite on Typescript -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 3.0.0 2 | 3 | ## Default Options Configuration Prop Names 4 | 5 | According to changes [mergeOptions](https://github.com/dubaua/merge-options) library now default option stored in `default` key instead of former `initial` key. Selectors removed from options. 6 | 7 | ## Pager 8 | 9 | Now pager no longer automaticly created by immerser. Instead you can manually markup you pager as regular solid and mark pager links with `data-immerser-pager-link` selector. The script will add classname passed as pagerLinkActiveClassname options to link when active layer changed. Removed `classnamePager`, `classnamePagerLink`, `classnamePagerLinkActive` options. 10 | 11 | This changed because somebody might need text pager links or more complicated markup. 12 | 13 | ## Breakpoints 14 | 15 | Now `fromViewportWidth` options is 0 by default. Its better to explicitly mark if you don't need init it on mobile screens. 16 | 17 | ## Class Fields Changes 18 | 19 | ### Renamed or changed 20 | - `statemap` => `stateArray` - renamed 21 | - `immerserNode` => `rootNode` - renamed 22 | - `originalChildrenNodeList` => `originalSolidNodeArray` - now contains array of nodes instead of NodeList 23 | - `immerserMaskNodeArray` => `maskNodeArray` - renamed 24 | - `resizeTimerId` => `resizeFrameId` - renamed 25 | - `scrollTimerId` => `scrollFrameId` - renamed 26 | 27 | ### New 28 | - `stateIndexById` - a hashmap with layerId keys and layerIndex values 29 | - `scrollAdjustTimerId` - scroll adjust delay timer id 30 | - `selectors` - object of selectors 31 | - `layerNodeArray` - contains array of layer nodes 32 | - `solidNodeArray` - contains array of solid nodes 33 | - `pagerLinkNodeArray` - contains array of pager link nodes 34 | - `customMaskNodeArray` - contains array of custom mask nodes 35 | - `stopRedrawingPager` - a function to detach pager redraw callback 36 | - `stopUpdatingHash` - a function to detach update hash callback 37 | - `stopFiringActiveLayerChangeCallback` - a function to detach active layer change callback 38 | - `stopTrackingWindowWidth` - a function to detach resize callback 39 | - `stopTrackingSynchroHover` - a function to detach syncro hover callback 40 | - `onSynchroHoverMouseOver` - synchro hover mouse over callback 41 | - `onSynchroHoverMouseOut` - synchro hover mouse out callback 42 | 43 | ### Removed 44 | - `pagerNode` 45 | -------------------------------------------------------------------------------- /dist/immerser.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Immerser=t():e.Immerser=t()}(this,(function(){return(()=>{var e={98:e=>{"use strict";function t(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function r(e){return null!==e&&"object"==typeof e&&!1===Array.isArray(e)}function o(e){var r=e.optionConfig,o=e.userOptions,i=void 0===o?{}:o,n=e.preffix,s=void 0===n?"":n,a=e.suffix,l=void 0===a?"":a,c=e.strict,u=void 0===c||c,d=function(e){return[s,e,l].join(" ")},h={};for(var f in r)if(t(r,f)){var p=r[f],v=p.required,y=p.default,m=p.description,g=p.validator,b=i[f],k=!!t(p,"required")&&("function"==typeof v?v(i):v),A=t(i,f),w=g(b,i);if(k){if(!A)throw new TypeError(d(f+" is required."));if(!w)throw new TypeError(d("Expected "+f+" to be "+m+", got "+typeof b+" "+b+"."));h[f]=b}else if(h[f]=y,A)if(w)h[f]=b;else{if(u)throw new TypeError(d("Expected "+f+" to be "+m+", got "+typeof b+" "+b+"."));console.warn(d("Expected "+f+" to be "+m+", got "+typeof b+" "+b+". Fallback to default value "+y+"."))}}return h}var i="[mergeOptions]:",n="\nCheck out documentation https://github.com/dubaua/merge-options#parameters-and-return";function s(e){throw new TypeError([i,e,n].join(" "))}var a={optionConfig:{required:!0,validator:function(e){for(var o in e)if(t(e,o)){var i=e[o];if(r(i)||s("Expected optionConfig."+o+" to be an object with declarative option configuration, got "+typeof i+" "+i+"."),t(i,"required")){var n=i.required,a=typeof n;"boolean"!==a&&"function"!==a&&s("Expected optionConfig."+o+".required to be either boolean or function, got "+typeof n+" "+n+".")}else t(i,"default")||s("Expected optionConfig."+o+" to either have required or default value.");if(t(i,"default")||t(i,"required")||s("Expected optionConfig."+o+" to either have required or default value."),t(i,"description")){var l=i.description;"string"!=typeof i.description&&s("Expected optionConfig."+o+".description to be a string, got "+typeof l+" "+l+".")}else s("Missing description on optionConfig."+o+" config.");if(t(i,"validator")){var c=i.validator;"function"!=typeof i.validator&&s("Expected optionConfig."+o+".validator to be a function, got "+typeof c+" "+c+".")}else s("Missing validator on optionConfig."+o+" config.")}return r(e)},description:"an object with declarative option configuration"},userOptions:{required:!1,default:{},validator:r,description:"an object"},preffix:{required:!1,default:"",validator:function(e){return"string"==typeof e},description:"a string"},suffix:{required:!1,default:"",validator:function(e){return"string"==typeof e},description:"a string"},strict:{required:!1,default:!0,validator:function(e){return"boolean"==typeof e},description:"a boolean"}};e.exports=function(e){var t=o({optionConfig:a,userOptions:e,preffix:i,suffix:n});return o(t)}},971:e=>{"use strict";e.exports=function(){function e(e){this.callbacks=[],this.internal=e}var t;return e.prototype.subscribe=function(e){var t=this;if("function"!=typeof e)throw new TypeError("[createObservable]: expected callback to be a function.");return this.callbacks.push(e),function(){var r=t.callbacks.indexOf(e);t.callbacks=t.callbacks.slice(0,r).concat(t.callbacks.slice(r+1))}},(t=[{key:"value",get:function(){return this.internal},set:function(e){if(e!==this.internal){var t=this.internal;this.internal=e;for(var r=0;r{var t=/^[a-z_-][a-z\d_-]*$/i;var r={solidClassnameArray:{default:[],description:"non empty array of objects",validator:function(e){return Array.isArray(e)&&0!==e.length}},fromViewportWidth:{default:0,description:"a natural number",validator:function(e){return"number"==typeof e&&0<=e&&e%1==0}},pagerThreshold:{default:.5,description:"a number between 0 and 1",validator:function(e){return"number"==typeof e&&0<=e&&e<=1}},hasToUpdateHash:{default:!1,description:"a boolean",validator:function(e){return"boolean"==typeof e}},scrollAdjustThreshold:{default:0,description:"a number greater than or equal to 0",validator:function(e){return"number"==typeof e&&e>=0}},scrollAdjustDelay:{default:600,description:"a number greater than or equal to 300",validator:function(e){return"number"==typeof e&&e>=300}},pagerLinkActiveClassname:{default:"pager-link-active",description:"valid non empty classname string",validator:function(e){return"string"==typeof e&&""!==e&&t.test(e)}},isScrollHandled:{default:!0,description:"a boolean",validator:function(e){return"boolean"==typeof e}},onInit:{default:null,description:"a function",validator:function(e){return"function"==typeof e}},onBind:{default:null,description:"a function",validator:function(e){return"function"==typeof e}},onUnbind:{default:null,description:"a function",validator:function(e){return"function"==typeof e}},onDestroy:{default:null,description:"a function",validator:function(e){return"function"==typeof e}},onActiveLayerChange:{default:null,description:"a function",validator:function(e){return"function"==typeof e}}};e.exports={OPTION_CONFIG:r,MESSAGE_PREFFIX:"[immmerser:]"}},873:(e,t,r)=>{"use strict";r.d(t,{default:()=>Immerser});var o=r(98),i=r.n(o),n=r(971),s=r.n(n),a=r(376);function l(e,t){for(var r in t)e.style[r]=t[r]}function c(e){var t=e.selector,r=e.parent,o=void 0===r?document:r;if(!o)return[];var i=o.querySelectorAll(t);return[].slice.call(i)}function u(e){var t=e.message,r=e.warning,o=void 0!==r&&r,i=e.docs,n=i||"",s="".concat(a.MESSAGE_PREFFIX," ").concat(t," \nCheck out documentation https://github.com/dubaua/immerser").concat(n);if(!o)throw new Error(s);console.warn(s)}function d(e,t,r){return Math.max(Math.min(e,r),t)}function h(){var e=window.scrollX||document.documentElement.scrollLeft,t=window.scrollY||document.documentElement.scrollTop;return{x:d(e,0,document.documentElement.offsetWidth),y:d(t,0,document.documentElement.offsetHeight)}}function f(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,o)}return r}function p(e){for(var t=1;t=e.options.fromViewportWidth?e.isBound||e.bind():e.isBound&&e.unbind()}))}},{key:"setSizes",value:function(){var e=this;this.documentHeight=document.documentElement.offsetHeight,this.windowHeight=window.innerHeight,this.immerserTop=this.rootNode.offsetTop,this.immerserHeight=this.rootNode.offsetHeight,this.stateArray=this.stateArray.map((function(t){var r=t.layerNode.offsetTop,o=r+t.layerNode.offsetHeight,i=r-e.immerserTop,n=i-e.immerserHeight,s=o-e.immerserTop,a=s-e.immerserHeight;return p(p({},t),{},{layerTop:r,layerBottom:o,beginEnter:n,endEnter:i,beginLeave:a,endLeave:s})})),this.reactiveWindowWidth.value=window.innerWidth}},{key:"addScrollAndResizeListeners",value:function(){this.options.isScrollHandled&&(this.onScroll=this.handleScroll.bind(this),window.addEventListener("scroll",this.onScroll,!1)),this.onResize=this.handleResize.bind(this),window.addEventListener("resize",this.onResize,!1)}},{key:"bind",value:function(){this.createMarkup(),this.initPagerLinks(),this.initHoverSynchro(),this.attachCallbacks(),this.isBound=!0,this.draw(),"function"==typeof this.options.onBind&&this.options.onBind(this)}},{key:"unbind",value:function(){this.detachCallbacks(),this.removeSyncroHoverListeners(),this.clearCustomSectionIds(),this.restoreOriginalSolidNodes(),this.cleanupClonnedMarkup(),this.isBound=!1,"function"==typeof this.options.onUnbind&&this.options.onUnbind(this),this.reactiveActiveLayer.value=void 0}},{key:"destroy",value:function(){this.unbind(),this.stopToggleBindOnRezise(),this.removeScrollAndResizeListeners(),"function"==typeof this.options.onDestroy&&this.options.onDestroy(this),this.initState()}},{key:"createMarkup",value:function(){var e=this;l(this.rootNode,g),this.initCustomMarkup(),this.originalSolidNodeArray=c({selector:this.selectors.solid,parent:this.rootNode}),this.stateArray=this.stateArray.map((function(t,r){var o=e.isCustomMarkup?e.customMaskNodeArray[r]:document.createElement("div");l(o,m);var i=e.isCustomMarkup?o.querySelector(e.selectors.maskInner):document.createElement("div");return l(i,m),e.isCustomMarkup||(o.dataset.immerserMask="",i.dataset.immerserMaskInner=""),e.originalSolidNodeArray.forEach((function(e){var t=e.cloneNode(!0);l(t,b),t.__immerserClonned=!0,i.appendChild(t)})),function(e,t){for(var r=0;r0&&!this.isCustomMarkup&&u({message:"You're trying use custom markup, but count of your immerser masks doesn't equal layers count.",warning:!0,docs:"#cloning-event-listeners"}),this.customMaskNodeArray.forEach((function(t){for(var r=t.querySelector(e.selectors.maskInner).children,o=0;o0&&(this.stopRedrawingPager=this.reactiveActiveLayer.subscribe(this.drawPagerLinks.bind(this))),this.options.hasToUpdateHash&&(this.stopUpdatingHash=this.reactiveActiveLayer.subscribe(this.drawHash.bind(this))),"function"==typeof this.options.onActiveLayerChange&&(this.stopFiringActiveLayerChangeCallback=this.reactiveActiveLayer.subscribe((function(t){e.options.onActiveLayerChange(t,e)}))),this.synchroHoverNodeArray.length>0&&(this.stopTrackingSynchroHover=this.reactiveSynchroHoverId.subscribe(this.drawSynchroHover.bind(this)))}},{key:"detachCallbacks",value:function(){"function"==typeof this.stopRedrawingPager&&this.stopRedrawingPager(),"function"==typeof this.stopUpdatingHash&&this.stopUpdatingHash(),"function"==typeof this.stopFiringActiveLayerChangeCallback&&this.stopFiringActiveLayerChangeCallback(),"function"==typeof this.stopTrackingSynchroHover&&this.stopTrackingSynchroHover()}},{key:"removeSyncroHoverListeners",value:function(){var e=this;this.synchroHoverNodeArray.forEach((function(t){t.removeEventListener("mouseover",e.onSynchroHoverMouseOver),t.removeEventListener("mouseout",e.onSynchroHoverMouseOut)}))}},{key:"clearCustomSectionIds",value:function(){this.stateArray.forEach((function(e){e.layerNode.__immerserCustomId&&e.layerNode.removeAttribute("id")}))}},{key:"restoreOriginalSolidNodes",value:function(){var e=this;this.originalSolidNodeArray.forEach((function(t){e.rootNode.appendChild(t)}))}},{key:"cleanupClonnedMarkup",value:function(){var e=this;this.maskNodeArray.forEach((function(t){if(e.isCustomMarkup){t.removeAttribute("style"),t.removeAttribute("aria-hidden");var r=t.querySelector(e.selectors.maskInner);r.removeAttribute("style"),c({selector:e.selectors.solid,parent:r}).forEach((function(e){e.__immerserClonned&&r.removeChild(e)}))}else e.rootNode.removeChild(t)}))}},{key:"removeScrollAndResizeListeners",value:function(){this.options.isScrollHandled&&window.removeEventListener("scroll",this.onScroll,!1),window.removeEventListener("resize",this.onResize,!1)}},{key:"draw",value:function(e){var t=this,r=void 0!==e?e:h().y;this.stateArray.forEach((function(e,o){var i,n=e.beginEnter,s=e.endEnter,a=e.beginLeave,l=e.endLeave,c=e.maskNode,u=e.maskInnerNode,d=e.layerTop,h=e.layerBottom;n>r?i=t.immerserHeight:n<=r&&r=l&&(i=-t.immerserHeight),c.style.transform="translateY(".concat(i,"px)"),u.style.transform="translateY(".concat(-i,"px)");var f=r+t.windowHeight*(1-t.options.pagerThreshold);d<=f&&f0&&(e.scrollAdjustTimerId&&(clearTimeout(e.scrollAdjustTimerId),e.scrollAdjustTimerId=null),e.scrollAdjustTimerId=setTimeout(e.adjustScroll.bind(e),e.options.scrollAdjustDelay))}),this.options.scrollAdjustDelay))}},{key:"handleResize",value:function(){var e=this;this.resizeFrameId&&(window.cancelAnimationFrame(this.resizeFrameId),this.resizeFrameId=null),this.resizeFrameId=window.requestAnimationFrame((function(){e.setSizes(),e.draw()}))}}])&&y(e.prototype,t),r&&y(e,r),Immerser}()}},t={};function r(o){if(t[o])return t[o].exports;var i=t[o]={exports:{}};return e[o](i,i.exports,r),i.exports}return r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r(873)})().default})); 2 | //# sourceMappingURL=immerser.min.js.map -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/docs/favicon.ico -------------------------------------------------------------------------------- /docs/main.css: -------------------------------------------------------------------------------- 1 | [data-simplebar]{position:relative;flex-direction:column;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:flex-start}.simplebar-wrapper{overflow:hidden;width:inherit;height:inherit;max-width:inherit;max-height:inherit}.simplebar-mask{direction:inherit;overflow:hidden;width:auto!important;height:auto!important;z-index:0}.simplebar-mask,.simplebar-offset{position:absolute;padding:0;margin:0;left:0;top:0;bottom:0;right:0}.simplebar-offset{direction:inherit!important;box-sizing:inherit!important;resize:none!important;-webkit-overflow-scrolling:touch}.simplebar-content-wrapper{direction:inherit;box-sizing:border-box!important;position:relative;display:block;height:100%;width:auto;max-width:100%;max-height:100%;scrollbar-width:none;-ms-overflow-style:none}.simplebar-content-wrapper::-webkit-scrollbar,.simplebar-hide-scrollbar::-webkit-scrollbar{width:0;height:0}.simplebar-content:after,.simplebar-content:before{content:" ";display:table}.simplebar-placeholder{max-height:100%;max-width:100%;width:100%;pointer-events:none}.simplebar-height-auto-observer-wrapper{box-sizing:inherit!important;height:100%;width:100%;max-width:1px;position:relative;float:left;max-height:1px;overflow:hidden;z-index:-1;padding:0;margin:0;pointer-events:none;flex-grow:inherit;flex-shrink:0;flex-basis:0}.simplebar-height-auto-observer{box-sizing:inherit;display:block;opacity:0;top:0;left:0;height:1000%;width:1000%;min-height:1px;min-width:1px;z-index:-1}.simplebar-height-auto-observer,.simplebar-track{position:absolute;overflow:hidden;pointer-events:none}.simplebar-track{z-index:1;right:0;bottom:0}[data-simplebar].simplebar-dragging .simplebar-content{pointer-events:none;user-select:none;-webkit-user-select:none}[data-simplebar].simplebar-dragging .simplebar-track{pointer-events:all}.simplebar-scrollbar{position:absolute;left:0;right:0;min-height:10px}.simplebar-scrollbar:before{position:absolute;content:"";background:#000;border-radius:7px;left:2px;right:2px;opacity:0;transition:opacity .2s linear}.simplebar-scrollbar.simplebar-visible:before{opacity:.5;transition:opacity 0s linear}.simplebar-track.simplebar-vertical{top:0;width:11px}.simplebar-track.simplebar-vertical .simplebar-scrollbar:before{top:2px;bottom:2px}.simplebar-track.simplebar-horizontal{left:0;height:11px}.simplebar-track.simplebar-horizontal .simplebar-scrollbar:before{height:100%;left:2px;right:2px}.simplebar-track.simplebar-horizontal .simplebar-scrollbar{right:auto;left:0;top:2px;height:7px;min-height:0;min-width:10px;width:auto}[data-simplebar-direction=rtl] .simplebar-track.simplebar-vertical{right:auto;left:0}.hs-dummy-scrollbar-size{direction:rtl;position:fixed;opacity:0;visibility:hidden;height:500px;width:500px;overflow-y:hidden;overflow-x:scroll}.simplebar-hide-scrollbar{position:fixed;left:0;visibility:hidden;overflow-y:scroll;scrollbar-width:none;-ms-overflow-style:none} 2 | 3 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.about{padding-bottom:14px;margin:0 -16px;color:#000}.font-cyrillic .about{padding-bottom:15px;font-size:14px;letter-spacing:-.01em}.about--contrast{color:#fff}.about a,.about span{display:inline-block;padding-left:16px;color:inherit;transition-property:color;transition-duration:.2s;transition-timing-function:cubic-bezier(.25,.1,0,1);transition-delay:0s}@media screen and (min-width:1600px){.about a,.about span{padding-right:16px}}.code-highlight{padding:24px 0;color:#394646}.code-highlight--inline{margin-top:0;padding-bottom:0;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace}.code-highlight pre{margin:0;font-size:16px;line-height:24px}.code-highlight code{font-size:16px!important;line-height:24px}code[class*=language-],pre[class*=language-]{background:none;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:23px;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.namespace{opacity:.7}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.atrule{color:red}.token.attr-name{color:#3f8d8d}.language-css.token.string .style.token.string,.token.attr-value,.token.boolean{color:#1a2626}.token.builtin{color:red}.token.cdata{color:#b0b0b0}.token.char{color:red}.token.class-name{color:#1a2626}.token.comment{color:#b0b0b0}.token.constant{color:red}.token.deleted{color:#f0f}.token.doctype{color:#b0b0b0}.token.entity{color:red}.token.function,.token.important{color:#f0f}.token.inserted{color:red}.token.keyword{color:#d85ad8}.token.number{color:#1a2626}.token.operator{color:#394646}.token.prolog{color:#b0b0b0}.token.property{color:#3f8d8d}.token.punctuation{color:#394646}.token.regex{color:red}.token.selector{color:#d85ad8}.token.string{color:#1a2626}.token.symbol{color:red}.token.tag{color:#d85ad8}.token.url,.token.variable{color:red}.code-highlight--contrast{color:#dce2e2;background-color:#000}.code-highlight--contrast code[class*=language-],.code-highlight--contrast pre[class*=language-]{color:#dce2e2}.code-highlight--contrast .token.bold,.code-highlight--contrast .token.important{font-weight:700}.code-highlight--contrast .token.italic{font-style:italic}.code-highlight--contrast .token.entity{cursor:help}.code-highlight--contrast .token.atrule{color:red}.code-highlight--contrast .token.attr-name{color:#3f8d8d}.code-highlight--contrast .language-css.token.string .style.token.string,.code-highlight--contrast .token.attr-value,.code-highlight--contrast .token.boolean{color:#c5d8d8}.code-highlight--contrast .token.builtin{color:red}.code-highlight--contrast .token.cdata{color:#404040}.code-highlight--contrast .token.char{color:red}.code-highlight--contrast .token.class-name{color:#c5d8d8}.code-highlight--contrast .token.comment{color:#404040}.code-highlight--contrast .token.constant{color:red}.code-highlight--contrast .token.deleted{color:#dd6edd}.code-highlight--contrast .token.doctype{color:#404040}.code-highlight--contrast .token.entity{color:red}.code-highlight--contrast .token.function,.code-highlight--contrast .token.important{color:#dd6edd}.code-highlight--contrast .token.inserted{color:red}.code-highlight--contrast .token.keyword{color:#8d3f8d}.code-highlight--contrast .token.number{color:#c5d8d8}.code-highlight--contrast .token.operator{color:#dce2e2}.code-highlight--contrast .token.prolog{color:#404040}.code-highlight--contrast .token.property{color:#3f8d8d}.code-highlight--contrast .token.punctuation{color:#dce2e2}.code-highlight--contrast .token.regex{color:red}.code-highlight--contrast .token.selector{color:#8d3f8d}.code-highlight--contrast .token.string{color:#c5d8d8}.code-highlight--contrast .token.symbol{color:red}.code-highlight--contrast .token.tag{color:#8d3f8d}.code-highlight--contrast .token.url,.code-highlight--contrast .token.variable{color:red}html{scroll-behavior:smooth;font-family:Questrial,Montserrat,sans-serif;letter-spacing:.01em}html.font-cyrillic{font-family:Montserrat,sans-serif}::selection{background:#ff0;color:#000}::-moz-selection{background:#ff0;color:#000}@media screen and (min-width:1024px){.tall{box-sizing:border-box;min-height:100vh}.start{padding-top:78px}}@media screen and (min-width:1024px) and (min-width:1600px){.start{padding-top:102px}}@media screen and (min-width:1024px){.end{padding-bottom:80px}.as-if-title{height:96px}}.background{background:#fff;color:#000}.background--contrast{background:#000;color:#fff}.code-background{background-color:#f5eff5}@media screen and (min-width:1024px){.code-background{background-color:transparent}}.scroller-x{overflow-y:hidden}.scroller-x.background .simplebar-scrollbar:before{background:#e1bce1}.scroller-x.background .simplebar-scrollbar.simplebar-hover:before{background:#e7c3e7}.scroller-x.background--contrast .simplebar-scrollbar:before{background:#1e4343}.scroller-x.background--contrast .simplebar-scrollbar.simplebar-hover:before{background:#1f4e4e}.scroller-x .simplebar-track.simplebar-horizontal{height:16px}.scroller-x .simplebar-track.simplebar-horizontal .simplebar-scrollbar{height:16px;top:0}.scroller-x .simplebar-track.simplebar-horizontal .simplebar-scrollbar.simplebar-visible:before{border-radius:0;height:16px;left:0;right:0;opacity:1}a._hover,a:hover{color:#f0f!important}.rulers{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;display:none;background-image:repeating-linear-gradient(180deg,transparent,transparent 23px,rgba(255,0,255,.3) 0,rgba(255,0,255,.3) 24px)}.rulers--active{display:block}.rulers:after{content:"";position:fixed;z-index:1;top:0;height:100%;left:0;width:100%;pointer-events:none;background-image:repeating-linear-gradient(90deg,transparent,transparent calc(2.08333% - 1px),rgba(255,0,255,.3) calc(2.08333% - 1px),rgba(255,0,255,.3) 2.08333%)}.emoji{position:relative}.emoji__face{display:block}.emoji__hand{position:absolute;left:0;top:0}.emoji[data-emoji-animating=true] .emoji__face{animation:spinning .62s ease-in-out}.emoji[data-emoji-animating=true] .emoji__hand{transform-origin:left bottom;animation:hang .62s ease-in-out}@keyframes spinning{to{transform:rotate(1turn)}}@keyframes hang{50%{transform:rotate(-3deg)}}.fixed{display:none}@media screen and (min-width:1024px){.fixed{position:fixed;display:block;top:32px;bottom:32px;left:2.08333vw;right:2.08333vw;z-index:2}.fixed__pager{position:absolute;top:50%;right:0;transform:translateY(-50%)}}@media screen and (min-width:1024px) and (min-width:1600px){.fixed__pager{right:auto;left:0}}@media screen and (min-width:1024px){.fixed__logo{position:absolute;top:0;left:0}.fixed__menu{position:absolute;top:0;right:0}.fixed__language{position:absolute;bottom:0;left:0}.fixed__about{position:absolute;bottom:0;right:0}.fixed__emoji{position:absolute;right:0;top:50%;transform:translateY(-50%)}}.footer{margin-top:24px;padding-top:24px;padding-bottom:24px;line-height:24px;display:none}@media screen and (max-width:1023px){.footer{display:inherit;padding-left:8.33333vw;padding-right:8.33333vw}}@media screen and (max-width:1023px) and (min-width:586px){.footer{padding-left:4.16667vw;padding-right:4.16667vw}}.header{padding-top:32px;padding-bottom:4.48px;display:none}@media screen and (max-width:1023px){.header{display:inherit;padding-left:8.33333vw;padding-right:8.33333vw}}@media screen and (max-width:1023px) and (min-width:586px){.header{padding-left:4.16667vw;padding-right:4.16667vw}}.language{margin:0 -16px;color:#000}@media screen and (min-width:1024px){.language{padding-bottom:14px}}.font-cyrillic .language{font-size:14px;letter-spacing:-.01em}@media screen and (min-width:1600px){.font-cyrillic .language{padding-bottom:15px}}.language--contrast a{color:#fff}@media screen and (min-width:1600px){.language--contrast-lg{color:#fff}}@media screen and (min-width:1024px) and (max-width:1599px){.language--contrast-only-md{color:#fff}}.language__link{color:inherit;transition-property:color;transition-duration:.2s;transition-timing-function:cubic-bezier(.25,.1,0,1);transition-delay:0s;display:inline-block;padding-left:16px;font-size:14px}@media screen and (min-width:1600px){.language__link{padding-right:16px}}.language__link--active{font-size:16px}.font-cyrillic .language__link--active{font-size:14px}.logo{padding-top:9.92px;font-size:48px;line-height:32px;color:#000;display:block;text-decoration:none;transition-property:color;transition-duration:.2s;transition-timing-function:cubic-bezier(.25,.1,0,1);transition-delay:0s}.font-cyrillic .logo{font-size:44px;transform:translateY(-3px);letter-spacing:-.03em}.logo--contrast{color:#fff}@media screen and (min-width:1600px){.logo--contrast-lg{color:#fff}}@media screen and (min-width:1024px) and (max-width:1599px){.logo--contrast-only-md{color:#fff}}.menu{display:flex;margin:0 -16px}.menu__link{color:#000;display:block;font-size:16px;line-height:1;text-decoration:none;padding:26px 16px;transition-property:color;transition-duration:.2s;transition-timing-function:cubic-bezier(.25,.1,0,1);transition-delay:0s}.font-cyrillic .menu__link{font-size:14px;letter-spacing:-.03em;padding-top:28px}.menu--contrast .menu__link{color:#fff}.pager__link{display:block;margin:12px 0;width:12px;height:12px;box-sizing:border-box;border-radius:50%;color:#000;border:1.5px solid;transition-property:color,border-width;transition-duration:.2s;transition-timing-function:cubic-bezier(.25,.1,0,1);transition-delay:0s}.pager__link._hover,.pager__link:hover{color:#f0f}.pager__link--active{border-width:6px}.pager--contrast .pager__link{color:#fff}@media screen and (min-width:1600px){.pager--contrast-lg .pager__link{color:#fff}}@media screen and (min-width:1024px) and (max-width:1599px){.pager--contrast-only-md .pager__link{color:#fff}}.typography *{margin-bottom:0}.typography h1{font-size:32px;line-height:32px;margin-top:64px;font-weight:400}.font-cyrillic .typography h1{letter-spacing:-.03em;transform:translateY(-1px)}.typography p{line-height:24px;margin:24px 0}.font-cyrillic .typography p{font-size:14px;letter-spacing:-.01em}.typography p code{background-color:#f5eff5;border-radius:3px;margin:0;padding:4px 6px 2px;font-size:15px;vertical-align:bottom}.typography a{transition-property:color;transition-duration:.2s;transition-timing-function:cubic-bezier(.25,.1,0,1);transition-delay:0s;color:inherit}.typography strong{font-weight:400}.typography table{border-spacing:0}.typography tbody,.typography thead,.typography tr{padding:0;border:0;margin:0}.typography td,.typography th{vertical-align:top;font-weight:400;text-align:left;line-height:24px;padding:0 32px 24px 0;white-space:nowrap}@media screen and (min-width:1024px){.typography td,.typography th{white-space:normal}.typography td.nowrap,.typography th.nowrap{white-space:nowrap}}.typography abbr[title]{text-decoration:none}@media screen and (min-width:1024px){.grid{display:flex}}.grid__content{padding-left:8.33333vw;padding-right:8.33333vw}@media screen and (min-width:586px){.grid__content{padding-left:4.16667vw;padding-right:4.16667vw}}@media screen and (min-width:1600px){.grid__content{padding-left:2.08333vw;padding-right:2.08333vw}}.grid__col--alpha{display:none}@media screen and (min-width:1024px){.grid__col--alpha-beta{flex-basis:41.66667vw;max-width:41.66667vw}.grid__col--alpha-beta-gamma{flex-basis:100vw;max-width:100vw}.grid__col--beta{flex-basis:41.66667vw;max-width:41.66667vw}.grid__col--beta-gamma{flex-basis:100vw;max-width:100vw}.grid__col--gamma{flex-basis:58.33333vw;max-width:58.33333vw}}@media screen and (min-width:1600px){.grid__col--alpha{display:block;flex-basis:16.66667vw;max-width:16.66667vw}.grid__col--alpha-beta{flex-basis:45.83333vw;max-width:45.83333vw}.grid__col--alpha-beta-gamma{flex-basis:100vw;max-width:100vw}.grid__col--beta{flex-basis:29.16667vw;max-width:29.16667vw}.grid__col--beta-gamma{flex-basis:83.33333vw;max-width:83.33333vw}.grid__col--gamma{flex-basis:54.16667vw;max-width:54.16667vw}}@media screen and (min-width:1024px){.highlighter,p code.highlighter{cursor:pointer;text-decoration:underline;text-decoration-line:underline;text-decoration-style:double;text-decoration-color:#0ff;font-size:inherit;font-family:inherit;padding:0;background:none}.highlighter-animation-active{animation:highlight 1.5s linear infinite}.highlighter-animation-active[data-immerser-layer]{position:relative;animation:none}.highlighter-animation-active[data-immerser-layer]:after{content:"";position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;animation:highlight 1.5s linear infinite}@keyframes highlight{25%{background:rgba(0,255,255,.5)}50%{background:transparent}75%{background:rgba(0,255,255,.5)}}} -------------------------------------------------------------------------------- /example/content/code/cloning-event-listeners.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 |
-------------------------------------------------------------------------------- /example/content/code/handle-clone-hover.css: -------------------------------------------------------------------------------- 1 | a:hover, 2 | a._hover { 3 | color: magenta; 4 | } -------------------------------------------------------------------------------- /example/content/code/handle-dom-change.js: -------------------------------------------------------------------------------- 1 | // adding or removing node, that changes DOM height 2 | document.appendChild(someNode); 3 | document.removeChild(anotherNode); 4 | 5 | // then explicitly redraw immerser 6 | immerserInstance.onDOMChange(); -------------------------------------------------------------------------------- /example/content/code/initialization.js: -------------------------------------------------------------------------------- 1 | // <%= getTranslation('dont-import-if-umd-line-1') %> 2 | // <%= getTranslation('dont-import-if-umd-line-2') %> 3 | import Immerser from 'immerser'; 4 | 5 | const immerserInstance = new Immerser({ 6 | // <%= getTranslation('data-attribute-will-override-this-option-line-1') %> 7 | // <%= getTranslation('data-attribute-will-override-this-option-line-2') %> 8 | solidClassnameArray: [ 9 | { 10 | logo: 'logo--contrast-lg', 11 | pager: 'pager--contrast-lg', 12 | language: 'language--contrast-lg', 13 | }, 14 | { 15 | pager: 'pager--contrast-only-md', 16 | menu: 'menu--contrast', 17 | about: 'about--contrast', 18 | }, 19 | { 20 | logo: 'logo--contrast-lg', 21 | pager: 'pager--contrast-lg', 22 | language: 'language--contrast-lg', 23 | }, 24 | { 25 | logo: 'logo--contrast-only-md', 26 | pager: 'pager--contrast-only-md', 27 | language: 'language--contrast-only-md', 28 | menu: 'menu--contrast', 29 | about: 'about--contrast', 30 | }, 31 | { 32 | logo: 'logo--contrast-lg', 33 | pager: 'pager--contrast-lg', 34 | language: 'language--contrast-lg', 35 | }, 36 | ], 37 | hasToUpdateHash: true, 38 | fromViewportWidth: 1024, 39 | pagerLinkActiveClassname: 'pager__link--active', 40 | scrollAdjustThreshold: 50, 41 | scrollAdjustDelay: 600, 42 | onInit(immerser) { 43 | // <%= getTranslation('callback-on-init') %> 44 | }, 45 | onBind(immerser) { 46 | // <%= getTranslation('callback-on-bind') %> 47 | }, 48 | onUnbind(immerser) { 49 | // <%= getTranslation('callback-on-unbind') %> 50 | }, 51 | onDestroy(immerser) { 52 | // <%= getTranslation('callback-on-destroy') %> 53 | }, 54 | onActiveLayerChange(activeIndex, immerser) { 55 | // <%= getTranslation('callback-on-active-layer-change') %> 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /example/content/code/markup.html: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |
24 |
25 |
26 |
-------------------------------------------------------------------------------- /example/content/code/styles.css: -------------------------------------------------------------------------------- 1 | .fixed { 2 | position: fixed; 3 | top: 2em; 4 | bottom: 3em; 5 | left: 3em; 6 | right: 3em; 7 | z-index: 1; 8 | } 9 | .fixed__pager { 10 | position: absolute; 11 | top: 50%; 12 | left: 0; 13 | transform: translate(0, -50%); 14 | } 15 | .fixed__logo { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | } 20 | .fixed__menu { 21 | position: absolute; 22 | top: 0; 23 | right: 0; 24 | } 25 | .fixed__language { 26 | position: absolute; 27 | bottom: 0; 28 | left: 0; 29 | } 30 | .fixed__about { 31 | position: absolute; 32 | bottom: 0; 33 | right: 0; 34 | } 35 | .pager, 36 | .logo, 37 | .menu, 38 | .language, 39 | .about { 40 | color: black; 41 | } 42 | .pager--contrast, 43 | .logo--contrast, 44 | .menu--contrast, 45 | .language--contrast, 46 | .about--contrast { 47 | color: white; 48 | } -------------------------------------------------------------------------------- /example/content/code/table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
<%= getTranslation('option') %><%= getTranslation('type') %><%= getTranslation('default') %><%= getTranslation('description') %>
solidClassnameArrayarray[]<%= getTranslation('option-solidClassnameArray') %>
fromViewportWidthnumber0<%= getTranslation('option-fromViewportWidth') %>
pagerThresholdnumber0.5<%= getTranslation('option-pagerThreshold') %>
hasToUpdateHashbooleanfalse<%= getTranslation('option-hasToUpdateHash') %>
scrollAdjustThresholdnumber0<%= getTranslation('option-scrollAdjustThreshold') %>
scrollAdjustDelaynumber600<%= getTranslation('option-scrollAdjustDelay') %>
pagerLinkActiveClassnamestringpager-link-active<%= getTranslation('option-pagerLinkActiveClassname') %>
isScrollHandledbooleantrue<%= getTranslation('option-isScrollHandled') %>
onInitfunctionnull<%= getTranslation('option-onInit') %>
onBindfunctionnull<%= getTranslation('option-onBind') %>
onUnbindfunctionnull<%= getTranslation('option-onUnbind') %>
onDestroyfunctionnull<%= getTranslation('option-onDestroy') %>
onActiveLayerChangefunctionnull<%= getTranslation('option-onActiveLayerChange') %>
91 | -------------------------------------------------------------------------------- /example/content/code/table.md: -------------------------------------------------------------------------------- 1 | | option | type | default | description | 2 | | - | - | - | - | 3 | | solidClassnameArray | `array` | `[]` | Array of layer class configurations. Overriding by config passed in data-immerser-layer-config for corresponding layer. Configuration example [is shown above](#initialize-immerser) | 4 | | fromViewportWidth | `number` | `0` | A viewport width, from which immerser will init | 5 | | pagerThreshold | `number` | `0.5` | How much next layer should be in viewport to trigger pager | 6 | | hasToUpdateHash | `boolean` | `false` | Flag to control changing hash on pager active state change | 7 | | scrollAdjustThreshold | `number` | `0` | A distance from the viewport top or bottom to the section top or bottom edge in pixels. If the current distance is below the threshold, the scroll adjustment will be applied. Will not adjust, if zero passed | 8 | | scrollAdjustDelay | `number` | `600` | Delay after user interaction and before scroll adjust | 9 | | pagerLinkActiveClassname | `string` | `pager-link-active` | Added to each pager link pointing to active | 10 | | isScrollHandled | `boolean` | `true` | Binds scroll listener if true. Set to false if you're using remote scroll controller | 11 | | onInit | `function` | `null` | Fired after initialization. Accept an immerser instance as the only parameter | 12 | | onBind | `function` | `null` | Fired after binding DOM. Accept an immerser instance as the only parameter | 13 | | onUnbind | `function` | `null` | Fired after unbinding DOM. Accept an immerser instance as the only parameter | 14 | | onDestroy | `function` | `null` | Fired after destroy. Accept an immerser instance as the only parameter | 15 | | onActiveLayerChange | `function` | `null` | Fired after active layer change. Accept active layer index as first parameter and an immerser instance as second | 16 | -------------------------------------------------------------------------------- /example/content/highlighted-cloning-event-listeners.html: -------------------------------------------------------------------------------- 1 |
<div class="fixed" data-immerser>
 2 |   <div data-immerser-mask>
 3 |     <div data-immerser-mask-inner>
 4 |       <!-- <%= getTranslation('your-markup') %> -->
 5 |     </div>
 6 |   </div>
 7 |   <div data-immerser-mask>
 8 |     <div data-immerser-mask-inner>
 9 |       <!-- <%= getTranslation('your-markup') %> -->
10 |     </div>
11 |   </div>
12 | </div>
-------------------------------------------------------------------------------- /example/content/highlighted-handle-clone-hover.html: -------------------------------------------------------------------------------- 1 |
a:hover,
2 | a._hover {
3 |   color: magenta;
4 | }
-------------------------------------------------------------------------------- /example/content/highlighted-handle-dom-change.html: -------------------------------------------------------------------------------- 1 |
// adding or removing node, that changes DOM height
2 | document.appendChild(someNode);
3 | document.removeChild(anotherNode);
4 | 
5 | // then explicitly redraw immerser
6 | immerserInstance.onDOMChange();
-------------------------------------------------------------------------------- /example/content/highlighted-initialization.html: -------------------------------------------------------------------------------- 1 |
// <%= getTranslation('dont-import-if-umd-line-1') %>
 2 | // <%= getTranslation('dont-import-if-umd-line-2') %>
 3 | import Immerser from 'immerser';
 4 | 
 5 | const immerserInstance = new Immerser({
 6 |   // <%= getTranslation('data-attribute-will-override-this-option-line-1') %>
 7 |   // <%= getTranslation('data-attribute-will-override-this-option-line-2') %>
 8 |   solidClassnameArray: [
 9 |     {
10 |       logo: 'logo--contrast-lg',
11 |       pager: 'pager--contrast-lg',
12 |       language: 'language--contrast-lg',
13 |     },
14 |     {
15 |       pager: 'pager--contrast-only-md',
16 |       menu: 'menu--contrast',
17 |       about: 'about--contrast',
18 |     },
19 |     {
20 |       logo: 'logo--contrast-lg',
21 |       pager: 'pager--contrast-lg',
22 |       language: 'language--contrast-lg',
23 |     },
24 |     {
25 |       logo: 'logo--contrast-only-md',
26 |       pager: 'pager--contrast-only-md',
27 |       language: 'language--contrast-only-md',
28 |       menu: 'menu--contrast',
29 |       about: 'about--contrast',
30 |     },
31 |     {
32 |       logo: 'logo--contrast-lg',
33 |       pager: 'pager--contrast-lg',
34 |       language: 'language--contrast-lg',
35 |     },
36 |   ],
37 |   hasToUpdateHash: true,
38 |   fromViewportWidth: 1024,
39 |   pagerLinkActiveClassname: 'pager__link--active',
40 |   scrollAdjustThreshold: 50,
41 |   scrollAdjustDelay: 600,
42 |   onInit(immerser) {
43 |     // <%= getTranslation('callback-on-init') %>
44 |   },
45 |   onBind(immerser) {
46 |     // <%= getTranslation('callback-on-bind') %>
47 |   },
48 |   onUnbind(immerser) {
49 |     // <%= getTranslation('callback-on-unbind') %>
50 |   },
51 |   onDestroy(immerser) {
52 |     // <%= getTranslation('callback-on-destroy') %>
53 |   },
54 |   onActiveLayerChange(activeIndex, immerser) {
55 |     // <%= getTranslation('callback-on-active-layer-change') %>
56 |   },
57 | });
58 | 
-------------------------------------------------------------------------------- /example/content/highlighted-markup.html: -------------------------------------------------------------------------------- 1 |
<div class="fixed" data-immerser>
 2 |   <div class="fixed__pager pager" data-immerser-pager data-immerser-solid="pager"></div>
 3 |   <a href="#reasoning" class="fixed__logo logo" data-immerser-solid="logo"><%= getTranslation('immerser') %></a>
 4 |   <div class="fixed__menu menu" data-immerser-solid="menu">
 5 |     <a href="#reasoning" class="menu__link"><%= getTranslation('menu-link-reasoning') %></a>
 6 |     <a href="#how-to-use" class="menu__link"><%= getTranslation('menu-link-how-to-use') %></a>
 7 |     <a href="#how-it-works" class="menu__link"><%= getTranslation('menu-link-how-it-works') %></a>
 8 |     <a href="#options" class="menu__link"><%= getTranslation('menu-link-options') %></a>
 9 |     <a href="#recipes" class="menu__link"><%= getTranslation('menu-link-recipes') %></a>
10 |   </div>
11 |   <div class="fixed__language language" data-immerser-solid="language">
12 |     <a href="/" class="language__link">english</a>
13 |     <a href="/ru.html" class="language__link">по-русски</a>
14 |   </div>
15 |   <div class="fixed__about about" data-immerser-solid="about">
16 |     <%= getTranslation('copyright') %>
17 |     <a href="https://github.com/dubaua/immerser"><%= getTranslation('github') %></a>
18 |     <a href="mailto:dubaua@gmail.com">dubaua@gmail.com</a>
19 |   </div>
20 | </div>
21 | 
22 | <div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="reasoning"></div>
23 | <div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="how-to-use"></div>
24 | <div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="how-it-works"></div>
25 | <div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="options"></div>
26 | <div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="recipes"></div>
27 | 
-------------------------------------------------------------------------------- /example/content/highlighted-styles.html: -------------------------------------------------------------------------------- 1 |
.fixed {
 2 |   position: fixed;
 3 |   top: 2em;
 4 |   bottom: 3em;
 5 |   left: 3em;
 6 |   right: 3em;
 7 |   z-index: 1;
 8 | }
 9 | .fixed__pager {
10 |   position: absolute;
11 |   top: 50%;
12 |   left: 0;
13 |   transform: translate(0, -50%);
14 | }
15 | .fixed__logo {
16 |   position: absolute;
17 |   top: 0;
18 |   left: 0;
19 | }
20 | .fixed__menu {
21 |   position: absolute;
22 |   top: 0;
23 |   right: 0;
24 | }
25 | .fixed__language {
26 |   position: absolute;
27 |   bottom: 0;
28 |   left: 0;
29 | }
30 | .fixed__about {
31 |   position: absolute;
32 |   bottom: 0;
33 |   right: 0;
34 | }
35 | .pager,
36 | .logo,
37 | .menu,
38 | .language,
39 | .about {
40 |   color: black;
41 | }
42 | .pager--contrast,
43 | .logo--contrast,
44 | .menu--contrast,
45 | .language--contrast,
46 | .about--contrast {
47 |   color: white;
48 | }
49 | 
-------------------------------------------------------------------------------- /example/favicon/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.gif -------------------------------------------------------------------------------- /example/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.ico -------------------------------------------------------------------------------- /example/favicon/favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.jpg -------------------------------------------------------------------------------- /example/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.png -------------------------------------------------------------------------------- /example/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import Immerser from '../dist/immerser.min.js'; 2 | import SimpleBar from 'simplebar'; 3 | import './styles/main.scss'; 4 | // import Prism from 'prismjs'; 5 | 6 | const scrollbarNodeList = document.querySelectorAll('.scroller-x'); 7 | for (let i = 0; i < scrollbarNodeList.length; i++) { 8 | const scrollbarNode = scrollbarNodeList[i]; 9 | new SimpleBar(scrollbarNode, { autoHide: false }); 10 | } 11 | 12 | const immerserInstance = new Immerser({ 13 | solidClassnameArray: [ 14 | { 15 | logo: 'logo--contrast-lg', 16 | pager: 'pager--contrast-lg', 17 | language: 'language--contrast-lg', 18 | }, 19 | { 20 | pager: 'pager--contrast-only-md', 21 | menu: 'menu--contrast', 22 | about: 'about--contrast', 23 | }, 24 | { 25 | logo: 'logo--contrast-lg', 26 | pager: 'pager--contrast-lg', 27 | language: 'language--contrast-lg', 28 | }, 29 | { 30 | logo: 'logo--contrast-only-md', 31 | pager: 'pager--contrast-only-md', 32 | language: 'language--contrast-only-md', 33 | menu: 'menu--contrast', 34 | about: 'about--contrast', 35 | }, 36 | { 37 | logo: 'logo--contrast-lg', 38 | pager: 'pager--contrast-lg', 39 | language: 'language--contrast-lg', 40 | }, 41 | ], 42 | fromViewportWidth: 1024, 43 | pagerLinkActiveClassname: 'pager__link--active', 44 | scrollAdjustThreshold: 50, 45 | scrollAdjustDelay: 600, 46 | onInit(immerser) { 47 | window.imm = immerser; 48 | console.log('onInit', immerser); 49 | }, 50 | onBind(immerser) { 51 | console.log('onBind', immerser); 52 | }, 53 | onUnbind(immerser) { 54 | console.log('onUnbind', immerser); 55 | }, 56 | onDestroy(immerser) { 57 | console.log('onDestroy', immerser); 58 | }, 59 | onActiveLayerChange(activeIndex, immerser) { 60 | console.log('onActiveLayerChange', activeIndex, immerser); 61 | }, 62 | }); 63 | 64 | const highlighterNodeList = document.querySelectorAll('[data-highlighter]'); 65 | const highlighterAnimationClassname = 'highlighter-animation-active'; 66 | 67 | function highlight(highlighterNode) { 68 | return () => { 69 | if (!immerserInstance.isBound) { 70 | return; 71 | } 72 | const targetSelector = highlighterNode.dataset.highlighter; 73 | const targetNodeList = document.querySelectorAll(targetSelector); 74 | for (let j = 0; j < targetNodeList.length; j++) { 75 | const targetNode = targetNodeList[j]; 76 | if (!targetNode.isHighlighting) { 77 | targetNode.isHighlighting = true; 78 | targetNode.classList.add(highlighterAnimationClassname); 79 | const timerId = setTimeout(() => { 80 | targetNode.classList.remove(highlighterAnimationClassname); 81 | clearTimeout(timerId); 82 | targetNode.isHighlighting = false; 83 | }, 1500); 84 | } 85 | } 86 | }; 87 | } 88 | 89 | for (let i = 0; i < highlighterNodeList.length; i++) { 90 | const highlighterNode = highlighterNodeList[i]; 91 | highlighterNode.addEventListener('mouseover', highlight(highlighterNode)); 92 | highlighterNode.addEventListener('click', highlight(highlighterNode)); 93 | } 94 | 95 | const emojiNodeList = document.querySelectorAll('[data-emoji-animating]'); 96 | for (let i = 0; i < emojiNodeList.length; i++) { 97 | const emojiNode = emojiNodeList[i]; 98 | emojiNode.addEventListener('click', () => { 99 | if (emojiNode.dataset.emojiAnimating === 'false') { 100 | emojiNode.dataset.emojiAnimating = 'true'; 101 | setTimeout(() => { 102 | emojiNode.dataset.emojiAnimating = 'false'; 103 | }, 620); 104 | } 105 | }); 106 | } 107 | 108 | const rulersNode = document.getElementById('rulers'); 109 | document.addEventListener('keydown', ({ altKey, code, keyCode }) => { 110 | const isR = code === 'KeyR' || keyCode === 82; 111 | if (altKey && isR) { 112 | rulersNode.classList.toggle('rulers--active'); 113 | } 114 | }); 115 | 116 | console.log('welcome here, fella. Press Alt+R to see vertical rhythm'); 117 | 118 | window.immerserInstance = immerserInstance; 119 | -------------------------------------------------------------------------------- /example/styles/components/about.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .about { 4 | padding-bottom: $base - 2px; 5 | margin: 0 $base * -1; 6 | 7 | @at-root #{$cyrillic-modifier} & { 8 | padding-bottom: $base - 1px; 9 | font-size: 14px; 10 | letter-spacing: -0.01em; 11 | } 12 | color: $color-text; 13 | &--contrast { 14 | color: $color-text--contrast; 15 | } 16 | span, a { 17 | display: inline-block; 18 | padding-left: $base; 19 | color: inherit; 20 | @include transition('color'); 21 | @include from('lg') { 22 | padding-right: $base; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/styles/components/code-highlight.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | $code-color-muted: #b0b0b0; 4 | $code-color-muted--contrast: #404040; 5 | 6 | $code-color-sintax: scale-color($color-accent, $lightness: 20%, $saturation: -38%); 7 | $code-color-sintax--contrast: scale-color($color-accent, $lightness: -20%, $saturation: -62%); 8 | 9 | $code-color-name: scale-color($color-highlight, $lightness: -20%, $saturation: -62%); 10 | $code-color-name--contrast: scale-color($color-highlight, $lightness: -20%, $saturation: -62%); 11 | 12 | $code-color-punctuation: scale-color($color-highlight, $lightness: -50%, $saturation: -90%); 13 | $code-color-punctuation--contrast: scale-color($color-highlight, $lightness: 75%, $saturation: -90%); 14 | 15 | $code-color-text: scale-color($color-highlight, $lightness: -50%, $saturation: -90%); 16 | $code-color-text--contrast: scale-color($color-highlight, $lightness: 75%, $saturation: -90%); 17 | 18 | $code-color-value: scale-color($color-highlight, $lightness: -75%, $saturation: -80%); 19 | $code-color-value--contrast: scale-color($color-highlight, $lightness: 62%, $saturation: -80%); 20 | 21 | $code-color-function: scale-color($color-accent, $lightness: 0%, $saturation: 0%); 22 | $code-color-function--contrast: scale-color($color-accent, $lightness: 30%, $saturation: -38%); 23 | 24 | 25 | $theme-prolog: $code-color-muted; 26 | $theme-doctype: $code-color-muted; 27 | $theme-comment: $code-color-muted; 28 | $theme-cdata: $code-color-muted; 29 | 30 | $theme-attr-name: $code-color-name; 31 | $theme-property: $code-color-name; 32 | 33 | $theme-selector: $code-color-sintax; 34 | $theme-tag: $code-color-sintax; 35 | $theme-keyword: $code-color-sintax; 36 | 37 | $theme-attr-value: $code-color-value; 38 | $theme-boolean: $code-color-value; 39 | $theme-string: $code-color-value; 40 | $theme-class-name: $code-color-value; 41 | $theme-number: $code-color-value; 42 | 43 | $theme-punctuation: $code-color-punctuation; 44 | $theme-operator: $code-color-punctuation; 45 | 46 | $theme-function: $code-color-function; 47 | $theme-deleted: $code-color-function; 48 | $theme-important: $code-color-function; 49 | 50 | $theme-text: $code-color-text; 51 | 52 | $theme-atrule: red; 53 | $theme-builtin: red; 54 | $theme-char: red; 55 | $theme-constant: red; 56 | $theme-entity: red; 57 | $theme-inserted: red; 58 | $theme-regex: red; 59 | $theme-symbol: red; 60 | $theme-url: red; 61 | $theme-variable: red; 62 | 63 | .code-highlight { 64 | padding: $base * 1.5 0; 65 | color: $theme-text; 66 | 67 | &--inline { 68 | margin-top: 0; 69 | padding-bottom: 0; 70 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 71 | } 72 | pre { 73 | margin: 0; 74 | font-size: $base; 75 | line-height: $base * 1.5; 76 | } 77 | code { 78 | font-size: $base !important; 79 | line-height: $base * 1.5; 80 | } 81 | } 82 | 83 | code[class*='language-'], 84 | pre[class*='language-'] { 85 | background: none; 86 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 87 | font-size: 1em; 88 | text-align: left; 89 | white-space: pre; 90 | word-spacing: normal; 91 | word-break: normal; 92 | word-wrap: normal; 93 | line-height: $base * 1.5 - 1px; 94 | 95 | -moz-tab-size: 4; 96 | -o-tab-size: 4; 97 | tab-size: 4; 98 | 99 | -webkit-hyphens: none; 100 | -moz-hyphens: none; 101 | -ms-hyphens: none; 102 | hyphens: none; 103 | } 104 | 105 | /* Code blocks */ 106 | pre[class*='language-'] { 107 | padding: 1em; 108 | margin: 0.5em 0; 109 | overflow: auto; 110 | border-radius: 0.3em; 111 | } 112 | 113 | :not(pre) > code[class*='language-'], 114 | pre[class*='language-'] { 115 | background: #272822; 116 | } 117 | 118 | /* Inline code */ 119 | :not(pre) > code[class*='language-'] { 120 | padding: 0.1em; 121 | border-radius: 0.3em; 122 | white-space: normal; 123 | } 124 | 125 | .namespace { 126 | opacity: 0.7; 127 | } 128 | 129 | .token.important, 130 | .token.bold { 131 | font-weight: bold; 132 | } 133 | 134 | .token.italic { 135 | font-style: italic; 136 | } 137 | 138 | .token.entity { 139 | cursor: help; 140 | } 141 | 142 | .token.atrule { 143 | color: $theme-atrule; 144 | } 145 | .token.attr-name { 146 | color: $theme-attr-name; 147 | } 148 | .token.attr-value, 149 | .language-css.token.string .style.token.string { 150 | color: $theme-attr-value; 151 | } 152 | .token.boolean { 153 | color: $theme-boolean; 154 | } 155 | .token.builtin { 156 | color: $theme-builtin; 157 | } 158 | .token.cdata { 159 | color: $theme-cdata; 160 | } 161 | .token.char { 162 | color: $theme-char; 163 | } 164 | .token.class-name { 165 | color: $theme-class-name; 166 | } 167 | .token.comment { 168 | color: $theme-comment; 169 | } 170 | .token.constant { 171 | color: $theme-constant; 172 | } 173 | .token.deleted { 174 | color: $theme-deleted; 175 | } 176 | .token.doctype { 177 | color: $theme-doctype; 178 | } 179 | .token.entity { 180 | color: $theme-entity; 181 | } 182 | .token.function { 183 | color: $theme-function; 184 | } 185 | .token.important { 186 | color: $theme-important; 187 | } 188 | .token.inserted { 189 | color: $theme-inserted; 190 | } 191 | .token.keyword { 192 | color: $theme-keyword; 193 | } 194 | .token.number { 195 | color: $theme-number; 196 | } 197 | .token.operator { 198 | color: $theme-operator; 199 | } 200 | .token.prolog { 201 | color: $theme-prolog; 202 | } 203 | .token.property { 204 | color: $theme-property; 205 | } 206 | .token.punctuation { 207 | color: $theme-punctuation; 208 | } 209 | .token.regex { 210 | color: $theme-regex; 211 | } 212 | .token.selector { 213 | color: $theme-selector; 214 | } 215 | .token.string { 216 | color: $theme-string; 217 | } 218 | .token.symbol { 219 | color: $theme-symbol; 220 | } 221 | .token.tag { 222 | color: $theme-tag; 223 | } 224 | .token.url { 225 | color: $theme-url; 226 | } 227 | .token.variable { 228 | color: $theme-variable; 229 | } 230 | 231 | .code-highlight--contrast { 232 | $theme-prolog: $code-color-muted--contrast; 233 | $theme-doctype: $code-color-muted--contrast; 234 | $theme-comment: $code-color-muted--contrast; 235 | $theme-cdata: $code-color-muted--contrast; 236 | 237 | $theme-attr-name: $code-color-name--contrast; 238 | $theme-property: $code-color-name--contrast; 239 | 240 | $theme-selector: $code-color-sintax--contrast; 241 | $theme-tag: $code-color-sintax--contrast; 242 | $theme-keyword: $code-color-sintax--contrast; 243 | 244 | $theme-attr-value: $code-color-value--contrast; 245 | $theme-boolean: $code-color-value--contrast; 246 | $theme-string: $code-color-value--contrast; 247 | $theme-class-name: $code-color-value--contrast; 248 | $theme-number: $code-color-value--contrast; 249 | 250 | $theme-punctuation: $code-color-punctuation--contrast; 251 | $theme-operator: $code-color-punctuation--contrast; 252 | 253 | $theme-function: $code-color-function--contrast; 254 | $theme-deleted: $code-color-function--contrast; 255 | $theme-important: $code-color-function--contrast; 256 | 257 | $theme-text: $code-color-text--contrast; 258 | 259 | color: $theme-text; 260 | background-color: $color-background--contrast; 261 | 262 | code[class*='language-'], 263 | pre[class*='language-'] { 264 | color: $theme-text; 265 | } 266 | 267 | .token.important, 268 | .token.bold { 269 | font-weight: bold; 270 | } 271 | 272 | .token.italic { 273 | font-style: italic; 274 | } 275 | 276 | .token.entity { 277 | cursor: help; 278 | } 279 | 280 | .token.atrule { 281 | color: $theme-atrule; 282 | } 283 | .token.attr-name { 284 | color: $theme-attr-name; 285 | } 286 | .token.attr-value, 287 | .language-css.token.string .style.token.string { 288 | color: $theme-attr-value; 289 | } 290 | .token.boolean { 291 | color: $theme-boolean; 292 | } 293 | .token.builtin { 294 | color: $theme-builtin; 295 | } 296 | .token.cdata { 297 | color: $theme-cdata; 298 | } 299 | .token.char { 300 | color: $theme-char; 301 | } 302 | .token.class-name { 303 | color: $theme-class-name; 304 | } 305 | .token.comment { 306 | color: $theme-comment; 307 | } 308 | .token.constant { 309 | color: $theme-constant; 310 | } 311 | .token.deleted { 312 | color: $theme-deleted; 313 | } 314 | .token.doctype { 315 | color: $theme-doctype; 316 | } 317 | .token.entity { 318 | color: $theme-entity; 319 | } 320 | .token.function { 321 | color: $theme-function; 322 | } 323 | .token.important { 324 | color: $theme-important; 325 | } 326 | .token.inserted { 327 | color: $theme-inserted; 328 | } 329 | .token.keyword { 330 | color: $theme-keyword; 331 | } 332 | .token.number { 333 | color: $theme-number; 334 | } 335 | .token.operator { 336 | color: $theme-operator; 337 | } 338 | .token.prolog { 339 | color: $theme-prolog; 340 | } 341 | .token.property { 342 | color: $theme-property; 343 | } 344 | .token.punctuation { 345 | color: $theme-punctuation; 346 | } 347 | .token.regex { 348 | color: $theme-regex; 349 | } 350 | .token.selector { 351 | color: $theme-selector; 352 | } 353 | .token.string { 354 | color: $theme-string; 355 | } 356 | .token.symbol { 357 | color: $theme-symbol; 358 | } 359 | .token.tag { 360 | color: $theme-tag; 361 | } 362 | .token.url { 363 | color: $theme-url; 364 | } 365 | .token.variable { 366 | color: $theme-variable; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /example/styles/components/common.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | html { 4 | scroll-behavior: smooth; 5 | font-family: 'Questrial', 'Montserrat', sans-serif; 6 | letter-spacing: 0.01em; 7 | 8 | &#{$cyrillic-modifier} { 9 | font-family: 'Montserrat', sans-serif; 10 | } 11 | } 12 | 13 | *::selection { 14 | background: $color-marker; 15 | color: $color-secondary; 16 | } 17 | *::-moz-selection { 18 | background: $color-marker; 19 | color: $color-secondary; 20 | } 21 | 22 | @include from('md') { 23 | .tall { 24 | box-sizing: border-box; 25 | min-height: 100vh; 26 | } 27 | 28 | .start { 29 | padding-top: $base * 4.875; 30 | @include from('lg') { 31 | padding-top: $base * 6.375; 32 | } 33 | } 34 | 35 | .end { 36 | padding-bottom: $base * 5; 37 | } 38 | 39 | .as-if-title { 40 | height: $base * 6; 41 | } 42 | } 43 | 44 | .background { 45 | background: $color-background; 46 | color: $color-background--contrast; 47 | &--contrast { 48 | background: $color-background--contrast; 49 | color: $color-text--contrast; 50 | } 51 | } 52 | 53 | .code-background { 54 | background-color: $color-code-background; 55 | @include from('md') { 56 | background-color: transparent; 57 | } 58 | } 59 | 60 | .scroller-x { 61 | overflow-y: hidden; 62 | 63 | &.background .simplebar-scrollbar:before { 64 | background: scale-color($color-accent, $lightness: 62%, $saturation: -62%); 65 | } 66 | &.background .simplebar-scrollbar.simplebar-hover:before { 67 | background: scale-color($color-accent, $lightness: 62%+5%, $saturation: -62%+5%); 68 | } 69 | 70 | &.background--contrast .simplebar-scrollbar:before { 71 | background: scale-color($color-highlight, $lightness: -62%, $saturation: -62%); 72 | } 73 | &.background--contrast .simplebar-scrollbar.simplebar-hover:before { 74 | background: scale-color($color-highlight, $lightness: -62%+5%, $saturation: -62%+5%); 75 | } 76 | 77 | .simplebar-track.simplebar-horizontal { 78 | height: $base; 79 | .simplebar-scrollbar { 80 | height: $base; 81 | top: 0; 82 | &.simplebar-visible:before { 83 | border-radius: 0; 84 | height: $base; 85 | left: 0; 86 | right: 0; 87 | opacity: 1; 88 | } 89 | } 90 | } 91 | } 92 | 93 | a:hover, 94 | a._hover { 95 | color: $color-active !important; 96 | } 97 | 98 | .rulers { 99 | $ruler-color: rgba(magenta, 0.3); 100 | $horizontal-ruler-gutter: $base * 1.5; 101 | $vertical-ruler-gutter: 4.1666666666666664%; 102 | position: fixed; 103 | top: 0; 104 | right: 0; 105 | bottom: 0; 106 | left: 0; 107 | pointer-events: none; 108 | 109 | display: none; 110 | &--active { 111 | display: block; 112 | } 113 | 114 | background-image: repeating-linear-gradient( 115 | to bottom, 116 | transparent, 117 | transparent $horizontal-ruler-gutter - 1, 118 | $ruler-color $horizontal-ruler-gutter - 1, 119 | $ruler-color $horizontal-ruler-gutter 120 | ); 121 | 122 | &:after { 123 | content: ''; 124 | position: fixed; 125 | z-index: 1; 126 | top: 0; 127 | height: 100%; 128 | left: 0; 129 | width: 100%; 130 | pointer-events: none; 131 | background-image: repeating-linear-gradient( 132 | to right, 133 | transparent, 134 | transparent calc(#{$vertical-ruler-gutter / 2} - 1px), 135 | $ruler-color calc(#{$vertical-ruler-gutter / 2} - 1px), 136 | $ruler-color $vertical-ruler-gutter / 2 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /example/styles/components/emoji.scss: -------------------------------------------------------------------------------- 1 | .emoji { 2 | $emoji: &; 3 | position: relative; 4 | &__face { 5 | display: block; 6 | } 7 | &__hand { 8 | position: absolute; 9 | left: 0; 10 | top: 0; 11 | } 12 | &[data-emoji-animating='true'] #{$emoji}__face { 13 | animation: spinning 0.62s ease-in-out; 14 | } 15 | &[data-emoji-animating='true'] #{$emoji}__hand { 16 | transform-origin: left bottom; 17 | animation: hang 0.62s ease-in-out; 18 | } 19 | } 20 | 21 | @keyframes spinning { 22 | 100% { 23 | transform: rotate(360deg); 24 | } 25 | } 26 | 27 | @keyframes hang { 28 | 50% { 29 | transform: rotate(-3deg); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/styles/components/fixed.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .fixed { 4 | display: none; 5 | @include from('md') { 6 | position: fixed; 7 | display: block; 8 | top: $base * 2; 9 | bottom: $base * 2; 10 | left: $col-width * 0.5; 11 | right: $col-width * 0.5; 12 | z-index: 2; 13 | 14 | &__pager { 15 | position: absolute; 16 | top: 50%; 17 | right: 0; 18 | @include from('lg') { 19 | right: auto; 20 | left: 0; 21 | } 22 | transform: translate(0, -50%); 23 | } 24 | 25 | &__logo { 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | } 30 | 31 | &__menu { 32 | position: absolute; 33 | top: 0; 34 | right: 0; 35 | } 36 | 37 | &__language { 38 | position: absolute; 39 | bottom: 0; 40 | left: 0; 41 | } 42 | 43 | &__about { 44 | position: absolute; 45 | bottom: 0; 46 | right: 0; 47 | } 48 | 49 | &__emoji { 50 | position: absolute; 51 | right: 0; 52 | top: 50%; 53 | transform: translate(0, -50%); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/styles/components/footer.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .footer { 4 | margin-top: $base * 1.5; 5 | padding-top: $base * 1.5; 6 | padding-bottom: $base * 1.5; 7 | line-height: $base * 1.5; 8 | @include show-from-to('xs', 'md') { 9 | padding-left: $col-width * 2; 10 | padding-right: $col-width * 2; 11 | @include from('sm') { 12 | padding-left: $col-width; 13 | padding-right: $col-width; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/styles/components/header.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .header { 4 | padding-top: $base * 2; 5 | padding-bottom: $base * 0.28; 6 | @include show-from-to('xs', 'md') { 7 | padding-left: $col-width * 2; 8 | padding-right: $col-width * 2; 9 | @include from('sm') { 10 | padding-left: $col-width; 11 | padding-right: $col-width; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/styles/components/highlighter.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | @include from('md') { 4 | .highlighter, 5 | p code.highlighter { 6 | cursor: pointer; 7 | text-decoration: underline; 8 | text-decoration-line: underline; 9 | text-decoration-style: double; 10 | text-decoration-color: $color-highlight; 11 | font-size: inherit; 12 | font-family: inherit; 13 | padding: 0; 14 | background: none; 15 | } 16 | 17 | .highlighter-animation-active { 18 | animation: highlight 1.5s linear infinite; 19 | &[data-immerser-layer] { 20 | position: relative; 21 | animation: none; 22 | &:after { 23 | content: ''; 24 | position: absolute; 25 | top: 0; 26 | bottom: 0; 27 | left: 0; 28 | right: 0; 29 | pointer-events: none; 30 | animation: highlight 1.5s linear infinite; 31 | } 32 | } 33 | } 34 | 35 | @keyframes highlight { 36 | 25% { 37 | background: rgba($color-highlight, 0.5); 38 | } 39 | 50% { 40 | background: transparent; 41 | } 42 | 75% { 43 | background: rgba($color-highlight, 0.5); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/styles/components/language.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .language { 4 | @include from('md') { 5 | padding-bottom: $base - 2px; 6 | } 7 | margin: 0 $base * -1; 8 | 9 | @at-root #{$cyrillic-modifier} & { 10 | @include from('lg') { 11 | padding-bottom: $base - 1px; 12 | } 13 | font-size: 14px; 14 | letter-spacing: -0.01em; 15 | } 16 | 17 | color: $color-text; 18 | &--contrast a { 19 | color: $color-text--contrast; 20 | } 21 | @include from('lg') { 22 | &--contrast-lg { 23 | color: $color-text--contrast; 24 | } 25 | } 26 | @include from-to('md', 'lg') { 27 | &--contrast-only-md { 28 | color: $color-text--contrast; 29 | } 30 | } 31 | &__link { 32 | color: inherit; 33 | @include transition('color'); 34 | display: inline-block; 35 | padding-left: $base; 36 | @include from('lg') { 37 | padding-right: $base; 38 | } 39 | font-size: 14px; 40 | 41 | &--active { 42 | font-size: 16px; 43 | @at-root #{$cyrillic-modifier} & { 44 | font-size: 14px; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/styles/components/logo.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .logo { 4 | padding-top: $base * 0.62; 5 | font-size: $base * 3; 6 | @at-root #{$cyrillic-modifier} & { 7 | font-size: $base * 2.75; 8 | transform: translate(0px, -3px); 9 | letter-spacing: -0.03em; 10 | } 11 | line-height: $base * 2; 12 | color: $color-text; 13 | display: block; 14 | text-decoration: none; 15 | @include transition('color'); 16 | 17 | &--contrast { 18 | color: $color-text--contrast; 19 | } 20 | 21 | @include from('lg') { 22 | &--contrast-lg { 23 | color: $color-text--contrast; 24 | } 25 | } 26 | 27 | @include from-to('md', 'lg') { 28 | &--contrast-only-md { 29 | color: $color-text--contrast; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/styles/components/menu.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .menu { 4 | $menu: &; 5 | display: flex; 6 | margin: 0 $base * -1; 7 | 8 | &__link { 9 | color: black; 10 | display: block; 11 | font-size: $base; 12 | line-height: 1; 13 | text-decoration: none; 14 | padding: 26px $base; 15 | @include transition('color'); 16 | @at-root #{$cyrillic-modifier} & { 17 | font-size: 14px; 18 | letter-spacing: -0.03em; 19 | padding-top: 28px; 20 | } 21 | } 22 | 23 | &--contrast #{$menu}__link { 24 | color: $color-text--contrast; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/styles/components/pager.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .pager { 4 | $pager: &; 5 | $pager-size: $base * 0.75; 6 | $border-width: 1.5px; 7 | $border-width--active: $pager-size * 0.5; 8 | 9 | &__link { 10 | display: block; 11 | margin: $pager-size 0; 12 | width: $pager-size; 13 | height: $pager-size; 14 | box-sizing: border-box; 15 | border-radius: 50%; 16 | color: $color-text; 17 | border: $border-width solid; 18 | @include transition('color, border-width'); 19 | 20 | &:hover, 21 | &._hover { 22 | color: $color-active; 23 | } 24 | 25 | &--active { 26 | border-width: $border-width--active; 27 | } 28 | } 29 | 30 | &--contrast #{$pager}__link { 31 | color: $color-text--contrast; 32 | } 33 | 34 | @include from('lg') { 35 | &--contrast-lg #{$pager}__link { 36 | color: $color-text--contrast; 37 | } 38 | } 39 | 40 | @include from-to('md', 'lg') { 41 | &--contrast-only-md #{$pager}__link { 42 | color: $color-text--contrast; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/styles/components/typography.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/globals.scss'; 2 | 3 | .typography { 4 | * { 5 | margin-bottom: 0; 6 | } 7 | h1 { 8 | font-size: $base * 2; 9 | line-height: $base * 2; 10 | margin-top: $base * 4; 11 | font-weight: normal; 12 | @at-root #{$cyrillic-modifier} & { 13 | letter-spacing: -0.03em; 14 | transform: translate(0, -1px); 15 | } 16 | } 17 | p { 18 | line-height: $base * 1.5; 19 | margin: $base * 1.5 0; 20 | @at-root #{$cyrillic-modifier} & { 21 | font-size: 14px; 22 | letter-spacing: -0.01em; 23 | } 24 | code { 25 | background-color: $color-code-background; 26 | border-radius: 3px; 27 | margin: 0; 28 | padding: 4px 6px 2px; 29 | font-size: 15px; 30 | vertical-align: bottom; 31 | } 32 | } 33 | a { 34 | @include transition('color'); 35 | color: inherit; 36 | } 37 | strong { 38 | font-weight: normal; 39 | } 40 | table { 41 | border-spacing: 0; 42 | } 43 | thead, 44 | tbody, 45 | tr { 46 | padding: 0; 47 | border: 0; 48 | margin: 0; 49 | } 50 | td, 51 | th { 52 | vertical-align: top; 53 | font-weight: normal; 54 | text-align: left; 55 | line-height: $base * 1.5; 56 | padding: 0 $base * 2 $base * 1.5 0; 57 | white-space: nowrap; 58 | @include from('md') { 59 | white-space: normal; 60 | &.nowrap { 61 | white-space: nowrap; 62 | } 63 | } 64 | } 65 | abbr[title] { 66 | text-decoration: none; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import '~normalize.css'; 2 | @import '~simplebar/dist/simplebar.css'; 3 | @import './components/about.scss'; 4 | @import './components/code-highlight.scss'; 5 | @import './components/common.scss'; 6 | @import './components/emoji.scss'; 7 | @import './components/fixed.scss'; 8 | @import './components/footer.scss'; 9 | @import './components/header.scss'; 10 | @import './components/language.scss'; 11 | @import './components/logo.scss'; 12 | @import './components/menu.scss'; 13 | @import './components/pager.scss'; 14 | @import './components/typography.scss'; 15 | @import './components/highlighter.scss'; 16 | -------------------------------------------------------------------------------- /example/styles/shared/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: ( 2 | xs: 0, 3 | sm: 586px, 4 | md: 1024px, 5 | lg: 1600px, 6 | ); 7 | 8 | @mixin from($breakpoint) { 9 | $size: map-get($breakpoints, $breakpoint); 10 | @if ($size == 0) { 11 | @content; 12 | } @else { 13 | @media screen and (min-width: $size) { 14 | @content; 15 | } 16 | } 17 | } 18 | 19 | @mixin from-to($from, $to) { 20 | $min: map-get($breakpoints, $from); 21 | $max: map-get($breakpoints, $to) - 1px; 22 | 23 | @if ($min == 0) { 24 | @media screen and (max-width: $max) { 25 | @content; 26 | } 27 | } @else { 28 | @media screen and (min-width: $min) and (max-width: $max) { 29 | @content; 30 | } 31 | } 32 | } 33 | 34 | @mixin show-from($breakpoint) { 35 | $size: map-get($breakpoints, $breakpoint); 36 | @if ($size != 0) { 37 | display: none; 38 | @media screen and (min-width: $size) { 39 | display: inherit; 40 | @content; 41 | } 42 | } @else { 43 | @content; 44 | } 45 | } 46 | 47 | @mixin show-from-to($from, $to) { 48 | $min: map-get($breakpoints, $from); 49 | $max: map-get($breakpoints, $to) - 1px; 50 | 51 | display: none; 52 | @if ($min == 0) { 53 | @media screen and (max-width: $max) { 54 | display: inherit; 55 | @content; 56 | } 57 | } @else { 58 | @media screen and (min-width: $min) and (max-width: $max) { 59 | display: inherit; 60 | @content; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/styles/shared/_colors.scss: -------------------------------------------------------------------------------- 1 | $color-primary: white; 2 | $color-secondary: black; 3 | $color-accent: magenta; 4 | $color-highlight: cyan; 5 | $color-marker: yellow; 6 | $color-primary--muted: #999; 7 | $color-secondary--muted: #666; 8 | 9 | $color-background: $color-primary; 10 | $color-background--contrast: $color-secondary; 11 | $color-text: $color-secondary; 12 | $color-text--contrast: $color-primary; 13 | $color-active: $color-accent; 14 | 15 | $color-code-background: scale-color($color-accent, $lightness: 90%, $saturation: -75%); -------------------------------------------------------------------------------- /example/styles/shared/_globals.scss: -------------------------------------------------------------------------------- 1 | @import './breakpoints.scss'; 2 | @import './colors.scss'; 3 | @import './grid.scss'; 4 | @import './transitions.scss'; 5 | 6 | $base: 16px; 7 | $cyrillic-modifier: '.font-cyrillic'; 8 | -------------------------------------------------------------------------------- /example/styles/shared/_grid.scss: -------------------------------------------------------------------------------- 1 | $alpha-width: 0; 2 | $alpha-width--xl: 4; 3 | 4 | $beta-width: 10; 5 | $beta-width--xl: 7; 6 | 7 | $gamma-width: 14; 8 | $gamma-width--xl: 13; 9 | 10 | $grid-width: $alpha-width + $beta-width + $gamma-width; 11 | 12 | $col-width: 1 / $grid-width * 100vw; 13 | 14 | .grid { 15 | @include from('md') { 16 | display: flex; 17 | } 18 | 19 | &__content { 20 | padding-left: $col-width * 2; 21 | padding-right: $col-width * 2; 22 | @include from('sm') { 23 | padding-left: $col-width; 24 | padding-right: $col-width; 25 | } 26 | @include from('lg') { 27 | padding-left: $col-width * 0.5; 28 | padding-right: $col-width * 0.5; 29 | } 30 | } 31 | 32 | &__col { 33 | &--alpha { 34 | display: none; 35 | } 36 | 37 | @include from('md') { 38 | &--alpha-beta { 39 | $width: #{($alpha-width + $beta-width) / $grid-width * 100}vw; 40 | flex-basis: $width; 41 | max-width: $width; 42 | } 43 | 44 | &--alpha-beta-gamma { 45 | $width: #{($alpha-width + $beta-width + $gamma-width) / $grid-width * 100}vw; 46 | flex-basis: $width; 47 | max-width: $width; 48 | } 49 | 50 | &--beta { 51 | $width: #{$beta-width / $grid-width * 100}vw; 52 | flex-basis: $width; 53 | max-width: $width; 54 | } 55 | 56 | &--beta-gamma { 57 | $width: #{($beta-width + $gamma-width) / $grid-width * 100}vw; 58 | flex-basis: $width; 59 | max-width: $width; 60 | } 61 | 62 | &--gamma { 63 | $width: #{$gamma-width / $grid-width * 100}vw; 64 | flex-basis: $width; 65 | max-width: $width; 66 | } 67 | } 68 | @include from('lg') { 69 | &--alpha { 70 | display: block; 71 | $width: #{$alpha-width--xl / $grid-width * 100}vw; 72 | flex-basis: $width; 73 | max-width: $width; 74 | } 75 | 76 | &--alpha-beta { 77 | $width: #{($alpha-width--xl + $beta-width--xl) / $grid-width * 100}vw; 78 | flex-basis: $width; 79 | max-width: $width; 80 | } 81 | 82 | &--alpha-beta-gamma { 83 | $width: #{($alpha-width--xl + $beta-width--xl + $gamma-width--xl) / $grid-width * 100}vw; 84 | flex-basis: $width; 85 | max-width: $width; 86 | } 87 | 88 | &--beta { 89 | $width: #{$beta-width--xl / $grid-width * 100}vw; 90 | flex-basis: $width; 91 | max-width: $width; 92 | } 93 | 94 | &--beta-gamma { 95 | $width: #{($beta-width--xl + $gamma-width--xl) / $grid-width * 100}vw; 96 | flex-basis: $width; 97 | max-width: $width; 98 | } 99 | 100 | &--gamma { 101 | $width: #{$gamma-width--xl / $grid-width * 100}vw; 102 | flex-basis: $width; 103 | max-width: $width; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /example/styles/shared/_transitions.scss: -------------------------------------------------------------------------------- 1 | @mixin transition( 2 | $property: all, 3 | $duration: 0.2s, 4 | $timing-function: cubic-bezier(0.25, 0.1, 0, 1), 5 | $delay: 0s 6 | ) { 7 | transition-property: #{$property}; 8 | transition-duration: $duration; 9 | transition-timing-function: $timing-function; 10 | transition-delay: $delay; 11 | } 12 | -------------------------------------------------------------------------------- /example/svg/how-it-works-face.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/svg/how-it-works-hand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /example/svg/how-to-use.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/svg/options.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/svg/possibilities.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/svg/why-immerser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /generateOptionsTables.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const kindOf = require('kind-of'); 3 | const TurndownService = require('turndown'); 4 | const en = require('./i18n/en.js'); 5 | const { OPTION_CONFIG } = require('./src/defaults.js'); 6 | 7 | const turndownService = new TurndownService(); 8 | 9 | function getClassNames(type) { 10 | switch (type.toLowerCase()) { 11 | case 'array': 12 | case 'object': 13 | return 'punctuation'; 14 | case 'number': 15 | return 'number'; 16 | case 'boolean': 17 | return 'boolean'; 18 | case 'string': 19 | return 'string nowrap'; 20 | case 'function': 21 | case 'null': 22 | case 'undefined': 23 | return 'keyword'; 24 | } 25 | } 26 | 27 | const options = Object.keys(OPTION_CONFIG).map((optionName) => { 28 | let defaultValue = OPTION_CONFIG[optionName].default; 29 | let type = kindOf(defaultValue); 30 | if (type === 'null' && optionName.startsWith('on')) { 31 | type = 'function'; 32 | } 33 | if (type === 'array') { 34 | defaultValue = '[]'; 35 | } 36 | return { 37 | optionName, 38 | type, 39 | defaultValue, 40 | }; 41 | }); 42 | 43 | const HTMLRowsMarkup = options.map(({ optionName, type, defaultValue }) => { 44 | return ` 45 | ${optionName} 46 | ${type} 47 | ${defaultValue} 48 | <%= getTranslation('option-${optionName}') %> 49 | `; 50 | }); 51 | 52 | const HTMLTableMarkup = ` 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ${HTMLRowsMarkup.join('\n')} 63 | 64 |
<%= getTranslation('option') %><%= getTranslation('type') %><%= getTranslation('default') %><%= getTranslation('description') %>
65 | `; 66 | 67 | fs.writeFileSync('./example/content/code/table.html', HTMLTableMarkup); 68 | 69 | const markdownTable = `| option | type | default | description | 70 | | - | - | - | - | 71 | ${options 72 | .map( 73 | ({ optionName, type, defaultValue }) => 74 | `| ${optionName} | \`${type}\` | \`${defaultValue}\` | ${turndownService.turndown(en['option-' + optionName])} |`, 75 | ) 76 | .join('\n')} 77 | `; 78 | 79 | fs.writeFileSync('./example/content/code/table.md', markdownTable); 80 | -------------------------------------------------------------------------------- /how-to-highlight.md: -------------------------------------------------------------------------------- 1 | # copy contents /example/content/code to /dev_dist/code folder 2 | # turn on prism 3 | # highlight in local browser uncommenting `
` in example/index.html and commenting `include` tag
4 | # copy markup from the page and insert into following `include` tag src. clean starting `pre` attributes
5 | # uncomment `include` tag and comment `pre` tag
6 | # turn off prism
7 | 


--------------------------------------------------------------------------------
/i18n/en.js:
--------------------------------------------------------------------------------
  1 | module.exports = {
  2 |   'language-code': 'en',
  3 |   'document-title': 'immerser — Javascript Library for Switching Fixed Elements on Scroll',
  4 |   'readme-title': 'Library for Switching Fixed Elements on Scroll',
  5 |   immerser: 'immerser',
  6 |   'menu-link-reasoning': 'Reasoning',
  7 |   'menu-link-how-to-use': 'How to Use',
  8 |   'menu-link-how-it-works': 'How it Works',
  9 |   'menu-link-options': 'Options',
 10 |   'menu-link-recipes': 'Recipes',
 11 |   'language-switcher':
 12 |     'englishпо-русски',
 13 |   github: 'github',
 14 |   copyright: '© %%THIS_YEAR%% — Vladimir Lysov, Chelyabinsk, Russia',
 15 |   'custom-font-body-classname': '',
 16 |   'why-immerser-title': 'Why Immerser?',
 17 |   'why-immerser-content': `
 18 | 

19 | Sometimes designers create complex logic and fix parts of the interface. 20 | Also they colour page sections contrasted. How to deal with this mess? 21 |

22 |

23 | Immerser comes to help you. It’s a javascript library to change fixed elements on scroll. 24 |

25 |

26 | Immerser fast, because it calculates states once on init. 27 | Then it watches the scroll position and schedules redraw document in the next event loop tick with requestAnimationFrame. 28 | Script changes transform property, so it uses graphic hardware acceleration. 29 |

30 |

31 | Immerser is written on vanilla js. Only %%BUNDLESIZE%%Kb gzipped. 32 |

33 | `, 34 | 35 | 'terms-title': 'Terms', 36 | 'terms-content': ` 37 |

38 | Immerser root — is the parent 39 | container for your fixed parts solids. 40 | Actually, solids are positioned absolutely to fixed immerser root. The 41 | layers are sections of your page. 42 | Also you may want to add 43 | pager to navigate through layers 44 | and indicate active state. 45 |

46 | `, 47 | 48 | 'install-title': 'Install', 49 | 'install-npm-label': '

Using npm:

', 50 | 'install-yarn-label': '

Using yarn:

', 51 | 'install-browser-label': '

Or if you want to use immerser in browser as global variable:

', 52 | 53 | 'prepare-your-markup-title': 'Prepare Your Markup', 54 | 'prepare-your-markup-content': ` 55 |

First, setup fixed container as the immerser root container, and add the data-immerser attribute.

56 |

Next place absolutely positioned children into the immerser parent and add data-immerser-solid="solid-id" to each.

57 |

Then add data-immerser-layer attribute to each section and pass configuration in 58 | data-immerser-layer-config='{"solid-id": "classname-modifier"}'. Otherwise, you can pass configuration as 59 | solidClassnameArray option to immerser. Config should contain JSON describing what class should be 60 | applied on each solid element, when it's over a section.

61 |

Also feel free to add data-immerser-pager to create a pager for your layers.

62 | `, 63 | 64 | 'apply-styles-title': 'Apply styles', 65 | 'apply-styles-content': ` 66 |

67 | Apply colour and background styles to your layers and solids according to your classname configuration passed in data attribute or options. 68 | I’m using BEM methodology in this example. 69 |

70 | `, 71 | 72 | 'dont-import-if-umd-line-1': `You don't have to import immerser`, 73 | 'dont-import-if-umd-line-2': `if you're using it in browser as global variable`, 74 | 'data-attribute-will-override-this-option-line-1': 'this option will be overridden by options', 75 | 'data-attribute-will-override-this-option-line-2': 'passed in data-immerser-layer-config attribute in each layer', 76 | 77 | 'initialize-immerser-title': 'Initialize Immerser', 78 | 'initialize-immerser-content': `

Include immerser in your code and create immerser instance with options.

`, 79 | 80 | 'callback-on-init': 'callback on init', 81 | 'callback-on-bind': 'callback on bind', 82 | 'callback-on-unbind': 'callback on unbind', 83 | 'callback-on-destroy': 'callback on destroy', 84 | 'callback-on-active-layer-change': 'callback on active layer change', 85 | 86 | 'how-it-works-title': 'How it Works', 87 | 'how-it-works-content': ` 88 |

First, immerser gathers information about the layers, solids, window and document. Then it creates a statemap for each layer, containing all necessary information, when the layer is partially and fully in viewport.

89 |

After that immerser modifies DOM, cloning all solids into mask containers for each layer and applying the classnames given in configuration. If you have added a pager, immerser also creates links for layers.

90 |

Finally, immerser binds listeners to scroll and resize events. On resize, it will meter layers, the window and document heights again and recalculate the statemap.

91 |

On scroll, immerser moves a mask of solids to show part of each solid group according to the layer below.

92 | `, 93 | 94 | 'options-title': 'Options', 95 | 'options-content': ` 96 |

97 | You can pass options to immerser as data-attributes on layers or as object as function parameter. Data-attributes are 98 | processed last, so they override the options passed to the function. 99 |

100 | `, 101 | 102 | option: 'option', 103 | type: 'type', 104 | default: 'default', 105 | description: 'description', 106 | 107 | 'option-solidClassnameArray': 108 | 'Array of layer class configurations. Overriding by config passed in data-immerser-layer-config for corresponding layer. Configuration example is shown above', 109 | 'option-fromViewportWidth': 'A viewport width, from which immerser will init', 110 | 'option-pagerThreshold': 'How much next layer should be in viewport to trigger pager', 111 | 'option-hasToUpdateHash': 'Flag to control changing hash on pager active state change', 112 | 'option-scrollAdjustThreshold': 113 | 'A distance from the viewport top or bottom to the section top or bottom edge in pixels. If the current distance is below the threshold, the scroll adjustment will be applied. Will not adjust, if zero passed', 114 | 'option-scrollAdjustDelay': 'Delay after user interaction and before scroll adjust', 115 | 'option-pagerLinkActiveClassname': 'Added to each pager link pointing to active', 116 | 'option-isScrollHandled': 'Binds scroll listener if true. Set to false if you\'re using remote scroll controller', 117 | 'option-onInit': 'Fired after initialization. Accept an immerser instance as the only parameter', 118 | 'option-onBind': 'Fired after binding DOM. Accept an immerser instance as the only parameter', 119 | 'option-onUnbind': 'Fired after unbinding DOM. Accept an immerser instance as the only parameter', 120 | 'option-onDestroy': 'Fired after destroy. Accept an immerser instance as the only parameter', 121 | 'option-onActiveLayerChange': 122 | 'Fired after active layer change. Accept active layer index as first parameter and an immerser instance as second', 123 | 124 | 'cloning-event-listeners-title': 'Cloning Event Listeners', 125 | 'cloning-event-listeners-content': ` 126 |

127 | Since immerser cloning nested nodes by default, all event listeners and data bound on nodes will be lost after 128 | init. Fortunately, you can markup the immerser yourself. It can be useful when you have event listeners 129 | on solids, reactive logic or more than classname switching. All you need is to place the number 130 | of nested immerser masks equal to the number of the layers. Look how I change the smiley emoji 131 | on the right in this page source. 132 |

133 | `, 134 | 135 | 'your-markup': 'your markup', 136 | 137 | 'handle-clone-hover-title': 'Handle Clone Hover', 138 | 'handle-clone-hover-content': ` 139 |

140 | As mentioned above, immerser cloning nested nodes to achieve changing on scroll. Therefore if you 141 | hover a partially visible element, only the visible part will change. If you want to synchronize all cloned links, just 142 | pass 143 | data-immerser-synchro-hover="hoverId" attribute. It will share _hover class between all 144 | nodes with this hoverId when the mouse is over one of them. Add _hover selector alongside your 145 | :hover pseudoselector to style your interactive elements. 146 |

147 | `, 148 | 'handle-dom-change-title': 'Handle DOM change', 149 | 'handle-dom-change-content': ` 150 |

151 | Immerser is not aware of changes in DOM, if you dynamically add or remove nodes. If you change height of the document 152 | and want immerser to recalculate and redraw solids, call onDOMChange method on the immerser instance. 153 |

154 | `, 155 | }; 156 | -------------------------------------------------------------------------------- /i18n/ru.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'language-code': 'ru', 3 | 'document-title': 'иммёрсер — джаваскрипт библиотека для перекрашивания фиксированных блоков по скроллу', 4 | 'readme-title': 'Библиотека для перекрашивания фиксированных блоков по скроллу', 5 | immerser: 'иммёрсер', 6 | 'menu-link-reasoning': 'Зачем нужен иммёрсер', 7 | 'menu-link-how-to-use': 'Как пользоваться', 8 | 'menu-link-how-it-works': 'Принцип работы', 9 | 'menu-link-options': 'Настройки', 10 | 'menu-link-recipes': 'Рецепты', 11 | 'language-switcher': 12 | 'englishпо-русски', 13 | github: 'гитхаб', 14 | copyright: '© %%THIS_YEAR%% — Владимир Лысов, Челябинск, Россия', 15 | 'custom-font-body-classname': 'font-cyrillic', 16 | 'why-immerser-title': 'Зачем нужен иммёрсер?', 17 | 'why-immerser-content': ` 18 |

19 | Иногда дизайнеры создают сложную логику и фиксируют части интерфейса. 20 | А еще они красят разделы страницы в контрастные цвета. Как с этим справиться? 21 |

22 |

23 | Вам поможет иммёрсер — джаваскрипт библиотека для замены фиксированных элементов при прокрутке страницы. 24 |

25 |

26 | Иммёрсер вычисляет состояния один раз в момент инициализации. 27 | Затем он следит за позицией скролла и планирует перерисовку документа 28 | в следующем такте цикла событий через метод requestAnimationFrame. 29 | Скрипт изменяет свойство transform, это задействует графический ускоритель. 30 |

31 |

32 | Иммёрсер написан на чистом джаваскрипте. Всего %%BUNDLESIZE%%Кб в сжатии gzip. 33 |

34 | `, 35 | 36 | 'terms-title': 'Термины', 37 | 'terms-content': ` 38 |

39 | Корневой элемент иммёрсера — это родительский контейнер для ваших фиксированных блоков. 40 | Фактически они позиционированы абсолютно внутри фиксированного корневого элемента. 41 | Слои — это разделы страницы, окрашенные в разные цвета. 42 | Еще вы наверняка захотите добавить навигацию по разделам, выделяющую активный раздел. 43 |

44 | `, 45 | 46 | 'install-title': 'Установка', 47 | 'install-npm-label': '

Через npm:

', 48 | 'install-yarn-label': '

Через yarn:

', 49 | 'install-browser-label': '

Или если вы хотите использовать иммёрсер в браузере как глобальную переменную:

', 50 | 51 | 'prepare-your-markup-title': 'Подготовьте разметку', 52 | 'prepare-your-markup-content': ` 53 |

Сначала настройте свой фиксированный контейнер как корневой элемент иммёрсера, добавив атрибут data-immerser

54 |

Затем расположите в нем абсолютно позиционированные дочерние элементы и добавьте каждому атрибут data-immerser-solid="solid-id" с идентификатором блока.

55 |

Добавьте каждому слою атрибут data-immerser-layer. Передайте конфигурацию в виде JSON в каждый слой с помощью атрибута 56 | data-immerser-layer-config='{"solid-id": "classname-modifier"}'. 57 | Также вы можете передать конфигурацию всех слоев массивом в параметре solidClassnameArray настроек. 58 | Конфигурация должна содержать описание классов для блоков, когда они находятся поверх слоя.

59 |

Так же вы можете добавить элемент с атрибутом data-immerser-pager для создания навигации.

60 | `, 61 | 62 | 'apply-styles-title': 'Примените стили', 63 | 'apply-styles-content': ` 64 |

65 | Добавьте стили цвета текста и фона на ваши блоки и слои с помощью классов, переданных в дата-атрибут или настройки. 66 | В примере я использую методологию БЭМ. 67 |

68 | `, 69 | 'dont-import-if-umd-line-1': `Вам не нужно импортировать иммёрсер,`, 70 | 'dont-import-if-umd-line-2': `если вы используете его в браузере как глобальную переменную`, 71 | 'data-attribute-will-override-this-option-line-1': 'будет переопределена настройками,', 72 | 'data-attribute-will-override-this-option-line-2': 'переданными в атрибут data-immerser-layer-config каждого слоя', 73 | 74 | 'initialize-immerser-title': 'Инициализируйте иммёрсер', 75 | 'initialize-immerser-content': `

Добавьте иммёрсер в код и создайте экземпляр с настройками.

`, 76 | 77 | 'callback-on-init': 'колбек после инициализации', 78 | 'callback-on-bind': 'колбек после привязки к документу', 79 | 'callback-on-unbind': 'колбек после отвязки от документа', 80 | 'callback-on-destroy': 'колбек после уничтожения', 81 | 'callback-on-active-layer-change': 'колбек после смены активного слоя', 82 | 83 | 'how-it-works-title': 'Принцип работы', 84 | 'how-it-works-content': ` 85 |

Сначала иммёрсер собирает информацию о слоях, блоках, окне и документе. Затем скрипт создает карту состояний для каждого слоя. Карта содержит размеры слоя, блоков и позиции их пересечений при скролле.

86 |

После сбора информации скрипт копирует все блоки в маскирующий контейнер и применяет к каждому классы, переданные в настройках. Если вы добавили навигацию, то иммёрсер создаст ссылки на каждый слой.

87 |

Затем иммёрсер подписывается на события скролла документа и изменения размеров окна.

88 |

При скролле иммёрсер двигает маскирующий контейнер так, чтобы показывать часть каждой группы блоков для каждого слоя под ними. При изменении размеров окна скрипт рассчитает карту состояний заново.

89 | `, 90 | 91 | 'options-title': 'Настройки', 92 | 'options-content': ` 93 |

94 | Вы можете передать настройки параметром функции конструктора или дата-атрибутом в документе. 95 | Дата-аттрибут обрабатывается последним, поэтому он переопределит настройки, переданные в конструктор. 96 |

97 | `, 98 | 99 | option: 'параметр', 100 | type: 'тип', 101 | default: 'значение по умолчанию', 102 | description: 'описание', 103 | 104 | 'option-solidClassnameArray': 105 | 'Массив настроек слоев. Конфигурация, переданная в data-immerser-layer-config перезапишет эту настройку для соответствующего слоя. Пример конфигурации показан выше', 106 | 'option-fromViewportWidth': 'Минимальная ширина окна для инициализации иммёрсера', 107 | 'option-pagerThreshold': 'Насколько должен следующий слой быть видим в окне, чтобы он стал активен в навигации', 108 | 'option-hasToUpdateHash': 'Флаг, контролирующий обновление хеша страницы', 109 | 'option-scrollAdjustThreshold': 110 | 'Дистанция до верха или низа окна браузера в пикселях. Если текущая дистанция меньше переданного значения, то скрипт подстроит положение скролла', 111 | 'option-scrollAdjustDelay': 'Сколько ждать бездействия пользователя, чтобы начать подстройку скролла', 112 | 'option-pagerLinkActiveClassname': 'Применяется, к каждой ссылке пейджера, ссылающуюся на активный слой', 113 | 'option-isScrollHandled': 'Подписывается на событие прокрутки, если включено. Выключите в случае, когда скроллом управляет внешний контроллер', 114 | 'option-onInit': 'Колбек после инициализации. Принимает один параметр — экземпляр иммёрсера', 115 | 'option-onBind': 'Колбек после привязки к документу. Принимает один параметр — экземпляр иммёрсера', 116 | 'option-onUnbind': 'Колбек после отвязки от документа. Принимает один параметр — экземпляр иммёрсера', 117 | 'option-onDestroy': 'Колбек после уничтожения. Принимает один параметр — экземпляр иммёрсера', 118 | 'option-onActiveLayerChange': 119 | 'Колбек после смены активного слоя. Принимает два параметра: индекс следующего слоя и экземпляр иммёрсера', 120 | 121 | 'cloning-event-listeners-title': 'Клонирование подписчиков событий', 122 | 'cloning-event-listeners-content': ` 123 |

124 | Вы уже знаете, что иммёрсер клонирует элементы. 125 | Подписчики событий и данные, привязанные к нодам, не клонируются вместе с элементом. 126 | К счастью, вы можете разметить иммёрсер самостоятельно. 127 | Для этого разместите внутри корневого элемента маскирующие контейнеры для блоков по числу слоев. 128 | В таком случае скрипт не будет клонировать элементы. Подписчики и реактивная логика останутся нетронутыми. 129 | В примере на этой странице я создаю подписчик на клик по смайлу справа до инициализации. 130 |

131 | `, 132 | 133 | 'your-markup': 'ваша разметка', 134 | 135 | 'handle-clone-hover-title': 'Обработка наведения на границах слоев', 136 | 'handle-clone-hover-content': ` 137 |

138 | Если вы наведете мышь на элемент, находящийся на границе слоев, 139 | то псевдоселектор :hover сработает только на одну часть. 140 | Чтобы наведение сработало на все клоны элемента, задайте идентификатор наведения в атрибуте data-immerser-synchro-hover="hoverId". 141 | При наведении мыши на такой элемент, ко всем его клонам добавится класс _hover. 142 | Стилизуйте по этому селектору вместе с псевдоселектором :hover, чтобы добиться нужного эффекта. 143 |

144 | `, 145 | 'handle-dom-change-title': 'Обработка изменения документа', 146 | 'handle-dom-change-content': ` 147 |

148 | Иммёрсер не отслеживает изменения документа, если вы динамически добавляете или удаляете ноды. Если вы меняете высоту документа, 149 | и хотите, чтобы иммёрсер пересчитал и перерисовал блоки, вызовите метод onDOMChange у экземпляра иммёрсера. 150 |

151 | `, 152 | }; 153 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2020", 5 | "baseUrl": "./", 6 | "paths": { 7 | "@/*": ["src/*"] 8 | } 9 | }, 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immerser", 3 | "version": "3.1.1", 4 | "description": "Javascript library for switching fixed elements on scroll through sections. Like Midnight.js, but without jQuery", 5 | "source": "src/immerser.js", 6 | "main": "dist/immerser.min.js", 7 | "umd:main": "dist/immerser.umd.js", 8 | "module": "dist/immerser.mjs", 9 | "keywords": [ 10 | "scroll", 11 | "fixed", 12 | "sticky" 13 | ], 14 | "repository": "git@github.com:dubaua/immerser.git", 15 | "author": "Vladimir Lysov", 16 | "email": "dubaua@gmail.com", 17 | "license": "MIT", 18 | "homepage": "https://dubaua.github.io/immerser/", 19 | "devDependencies": { 20 | "@babel/core": "^7.12.3", 21 | "@babel/preset-env": "^7.12.1", 22 | "@zainulbr/i18n-webpack-plugin": "^2.0.3", 23 | "babel-eslint": "^10.1.0", 24 | "babel-loader": "^8.2.1", 25 | "css-loader": "^5.0.1", 26 | "cz-conventional-changelog": "^3.3.0", 27 | "eslint": "^7.13.0", 28 | "eslint-config-standard": "^16.0.2", 29 | "file-loader": "^6.2.0", 30 | "gzip-size": "^6.0.0", 31 | "html-loader": "^1.3.2", 32 | "html-webpack-plugin": "^4.5.0", 33 | "kind-of": "^6.0.3", 34 | "mini-css-extract-plugin": "^1.3.1", 35 | "node-sass": "^5.0.0", 36 | "optimize-css-assets-webpack-plugin": "^5.0.4", 37 | "sass-loader": "^10.1.0", 38 | "turndown": "^7.0.0", 39 | "webpack": "^5.4.0", 40 | "webpack-cli": "^4.2.0", 41 | "webpack-dev-server": "^3.11.0", 42 | "yarn-audit-fix": "^7.0.4" 43 | }, 44 | "scripts": { 45 | "build:lib": "webpack --mode production", 46 | "build:docs": "webpack --config ./webpack.config.docs.js --mode production", 47 | "post-build": "node postBuild.js", 48 | "build": "yarn lint && node generateOptionsTables.js && node readme.js && yarn build:lib && yarn build:docs && yarn post-build", 49 | "dev": "webpack serve --config ./webpack.config.docs.js --mode development --open 'Google Chrome'", 50 | "lint": "eslint ./src && eslint ./example/main.js" 51 | }, 52 | "dependencies": { 53 | "@dubaua/merge-options": "^2.0.0", 54 | "@dubaua/observable": "^2.0.0", 55 | "normalize.css": "^8.0.1", 56 | "prismjs": "^1.22.0", 57 | "simplebar": "^5.3.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postBuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const gzipSize = require('gzip-size'); 3 | const packageJSON = require('./package.json'); 4 | 5 | const bundle = fs.readFileSync('./dist/immerser.min.js', 'utf8'); 6 | 7 | const bungleSize = (Math.round(gzipSize.sync(bundle) / 1000 * 100) / 100).toString(); 8 | 9 | const version = packageJSON.version; 10 | 11 | function replacer(content) { 12 | let result = content; 13 | const thisYear = new Date().getFullYear(); 14 | result = result.replace(/%%BUNDLESIZE%%/g, bungleSize); 15 | result = result.replace(/%%VERSION%%/g, version); 16 | result = result.replace(/%%THIS_YEAR%%/g, thisYear); 17 | return result; 18 | } 19 | 20 | function replaceInFile(path, replacer) { 21 | const content = fs.readFileSync(path, 'utf8'); 22 | const result = replacer(content); 23 | fs.writeFileSync(path, result); 24 | } 25 | 26 | replaceInFile('./README.md', replacer); 27 | replaceInFile('./docs/index.html', replacer); 28 | replaceInFile('./docs/ru.html', replacer); -------------------------------------------------------------------------------- /readme.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const TurndownService = require('turndown'); 3 | const en = require('./i18n/en.js'); 4 | 5 | const turndownService = new TurndownService(); 6 | 7 | function getTranslationFromTemplate(fileContent) { 8 | return fileContent.replace(/<%= getTranslation\('(.*)'\) %>/gm, (_, capture) => 9 | Object.prototype.hasOwnProperty.call(en, capture) ? en[capture] : 'TRANSLATION_NOT_FOUND!', 10 | ); 11 | } 12 | 13 | const markupCode = getTranslationFromTemplate(fs.readFileSync('./example/content/code/markup.html', 'utf8')); 14 | const styleCode = getTranslationFromTemplate(fs.readFileSync('./example/content/code/styles.css', 'utf8')); 15 | const initializationCode = getTranslationFromTemplate( 16 | fs.readFileSync('./example/content/code/initialization.js', 'utf8'), 17 | ); 18 | const optionsTable = fs.readFileSync('./example/content/code/table.md', 'utf8'); 19 | const cloningEventListenersCode = getTranslationFromTemplate( 20 | fs.readFileSync('./example/content/code/cloning-event-listeners.html', 'utf8'), 21 | ); 22 | const handleCloneHoverCode = getTranslationFromTemplate( 23 | fs.readFileSync('./example/content/code/handle-clone-hover.css', 'utf8'), 24 | ); 25 | const handleDOMChangeCode = getTranslationFromTemplate( 26 | fs.readFileSync('./example/content/code/handle-dom-change.js', 'utf8'), 27 | ); 28 | 29 | const readmeContent = `# ${en['readme-title']} 30 | 31 | ${turndownService.turndown(en['why-immerser-content'])} 32 | 33 | ## ${en['terms-title']} 34 | 35 | ${turndownService.turndown(en['terms-content'])} 36 | 37 | # ${en['menu-link-how-to-use']} 38 | 39 | ## ${en['install-title']} 40 | 41 | ${turndownService.turndown(en['install-npm-label'])} 42 | 43 | \`\`\`shell 44 | npm install immerser 45 | \`\`\` 46 | 47 | ${turndownService.turndown(en['install-yarn-label'])} 48 | 49 | \`\`\`shell 50 | yarn add immerser 51 | \`\`\` 52 | 53 | ${turndownService.turndown(en['install-browser-label'])} 54 | 55 | \`\`\`html 56 | 57 | \`\`\` 58 | 59 | ## ${en['prepare-your-markup-title']} 60 | 61 | ${turndownService.turndown(en['prepare-your-markup-content'])} 62 | 63 | \`\`\`html 64 | ${markupCode} 65 | \`\`\` 66 | 67 | ## ${en['apply-styles-title']} 68 | 69 | ${turndownService.turndown(en['apply-styles-content'])} 70 | 71 | \`\`\`css 72 | ${styleCode} 73 | \`\`\` 74 | 75 | ## ${en['initialize-immerser-title']} 76 | 77 | ${turndownService.turndown(en['initialize-immerser-content'])} 78 | 79 | \`\`\`js 80 | ${initializationCode} 81 | \`\`\` 82 | 83 | # ${en['how-it-works-title']} 84 | 85 | ${turndownService.turndown(en['how-it-works-content'])} 86 | 87 | # ${en['options-title']} 88 | 89 | ${turndownService.turndown(en['options-content'])} 90 | 91 | ${optionsTable} 92 | 93 | # ${en['menu-link-recipes']} 94 | 95 | ## ${en['cloning-event-listeners-title']} 96 | 97 | ${turndownService.turndown(en['cloning-event-listeners-content'])} 98 | 99 | \`\`\`html 100 | ${cloningEventListenersCode} 101 | \`\`\` 102 | 103 | ## ${en['handle-clone-hover-title']} 104 | 105 | ${turndownService.turndown(en['handle-clone-hover-content'])} 106 | 107 | \`\`\`css 108 | ${handleCloneHoverCode} 109 | \`\`\` 110 | 111 | ## ${en['handle-dom-change-title']} 112 | 113 | ${turndownService.turndown(en['handle-dom-change-content'])} 114 | 115 | \`\`\`js 116 | ${handleDOMChangeCode} 117 | \`\`\` 118 | `; 119 | 120 | fs.writeFileSync('README.md', readmeContent); 121 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | const CLASSNAME_REGEX = /^[a-z_-][a-z\d_-]*$/i; 2 | 3 | function classnameValidator(string) { 4 | return typeof string === 'string' && string !== '' && CLASSNAME_REGEX.test(string); 5 | } 6 | 7 | const OPTION_CONFIG = { 8 | solidClassnameArray: { 9 | default: [], 10 | description: 'non empty array of objects', 11 | validator: (x) => Array.isArray(x) && x.length !== 0, 12 | }, 13 | fromViewportWidth: { 14 | default: 0, 15 | description: 'a natural number', 16 | validator: (x) => typeof x === 'number' && 0 <= x && x % 1 === 0, 17 | }, 18 | pagerThreshold: { 19 | default: 0.5, 20 | description: 'a number between 0 and 1', 21 | validator: (x) => typeof x === 'number' && 0 <= x && x <= 1, 22 | }, 23 | hasToUpdateHash: { 24 | default: false, 25 | description: 'a boolean', 26 | validator: (x) => typeof x === 'boolean', 27 | }, 28 | scrollAdjustThreshold: { 29 | default: 0, 30 | description: 'a number greater than or equal to 0', 31 | validator: (x) => typeof x === 'number' && x >= 0, 32 | }, 33 | scrollAdjustDelay: { 34 | default: 600, 35 | description: 'a number greater than or equal to 300', 36 | validator: (x) => typeof x === 'number' && x >= 300, 37 | }, 38 | pagerLinkActiveClassname: { 39 | default: 'pager-link-active', 40 | description: 'valid non empty classname string', 41 | validator: classnameValidator, 42 | }, 43 | isScrollHandled: { 44 | default: true, 45 | description: 'a boolean', 46 | validator: (x) => typeof x === 'boolean', 47 | }, 48 | onInit: { 49 | default: null, 50 | description: 'a function', 51 | validator: (x) => typeof x === 'function', 52 | }, 53 | onBind: { 54 | default: null, 55 | description: 'a function', 56 | validator: (x) => typeof x === 'function', 57 | }, 58 | onUnbind: { 59 | default: null, 60 | description: 'a function', 61 | validator: (x) => typeof x === 'function', 62 | }, 63 | onDestroy: { 64 | default: null, 65 | description: 'a function', 66 | validator: (x) => typeof x === 'function', 67 | }, 68 | onActiveLayerChange: { 69 | default: null, 70 | description: 'a function', 71 | validator: (x) => typeof x === 'function', 72 | }, 73 | }; 74 | 75 | const MESSAGE_PREFFIX = '[immmerser:]'; 76 | 77 | module.exports = { 78 | OPTION_CONFIG, 79 | MESSAGE_PREFFIX, 80 | }; 81 | -------------------------------------------------------------------------------- /src/immerser.js: -------------------------------------------------------------------------------- 1 | import mergeOptions from '@dubaua/merge-options'; 2 | import Observable from '@dubaua/observable'; 3 | import { MESSAGE_PREFFIX, OPTION_CONFIG } from '@/defaults.js'; 4 | import { bindStyles, forEachNode, getLastScrollPosition, getNodeArray, isEmpty, showError } from '@/utils.js'; 5 | 6 | const CROPPED_FULL_ABSOLUTE_STYLES = { 7 | position: 'absolute', 8 | top: 0, 9 | right: 0, 10 | bottom: 0, 11 | left: 0, 12 | overflow: 'hidden', 13 | }; 14 | 15 | const NOT_INTERACTIVE_STYLES = { 16 | pointerEvents: 'none', 17 | touchAction: 'none', 18 | }; 19 | 20 | const INTERACTIVE_STYLES = { 21 | pointerEvents: 'all', 22 | touchAction: 'auto', 23 | }; 24 | 25 | export default class Immerser { 26 | constructor(userOptions) { 27 | this.initState(); 28 | this.init(userOptions); 29 | } 30 | 31 | initState() { 32 | this.options = null; 33 | this.selectors = { 34 | root: '[data-immerser]', 35 | layer: '[data-immerser-layer]', 36 | solid: '[data-immerser-solid]', 37 | pagerLink: '[data-immerser-pager-link]', 38 | mask: '[data-immerser-mask]', 39 | maskInner: '[data-immerser-mask-inner]', 40 | synchroHover: '[data-immerser-synchro-hover]', 41 | }; 42 | this.stateArray = []; 43 | this.stateIndexById = {}; 44 | this.isBound = false; 45 | this.rootNode = null; 46 | this.layerNodeArray = []; 47 | this.solidNodeArray = []; 48 | this.pagerLinkNodeArray = []; 49 | this.originalSolidNodeArray = []; 50 | this.maskNodeArray = []; 51 | this.synchroHoverNodeArray = []; 52 | this.isCustomMarkup = false; 53 | this.customMaskNodeArray = []; 54 | this.documentHeight = 0; 55 | this.windowHeight = 0; 56 | this.immerserTop = 0; 57 | this.immerserHeight = 0; 58 | this.resizeFrameId = null; 59 | this.scrollFrameId = null; 60 | this.scrollAdjustTimerId = null; 61 | this.reactiveActiveLayer = new Observable(); 62 | this.reactiveWindowWidth = new Observable(); 63 | this.reactiveSynchroHoverId = new Observable(); 64 | this.stopRedrawingPager = null; 65 | this.stopUpdatingHash = null; 66 | this.stopFiringActiveLayerChangeCallback = null; 67 | this.stopTrackingWindowWidth = null; 68 | this.stopTrackingSynchroHover = null; 69 | this.onResize = null; 70 | this.onScroll = null; 71 | this.onSynchroHoverMouseOver = null; 72 | this.onSynchroHoverMouseOut = null; 73 | } 74 | 75 | init(userOptions) { 76 | this.setNodes(); 77 | this.validateMarkup(); 78 | this.mergeOptions(userOptions); 79 | this.getClassnamesFromMarkup(); 80 | this.validateSolidClassnameArray(); 81 | this.initSectionIds(); 82 | this.initStatemap(); 83 | this.validateClassnames(); 84 | this.toggleBindOnRezise(); 85 | this.setSizes(); 86 | this.addScrollAndResizeListeners(); 87 | if (typeof this.options.onInit === 'function') { 88 | this.options.onInit(this); 89 | } 90 | } 91 | 92 | setNodes() { 93 | this.rootNode = document.querySelector(this.selectors.root); 94 | this.layerNodeArray = getNodeArray({ selector: this.selectors.layer }); 95 | this.solidNodeArray = getNodeArray({ selector: this.selectors.solid, parent: this.rootNode }); 96 | } 97 | 98 | validateMarkup() { 99 | if (!this.rootNode) { 100 | showError({ 101 | message: 'immerser root node not found.', 102 | docs: '#prepare-your-markup', 103 | }); 104 | } 105 | if (this.layerNodeArray.length < 0) { 106 | showError({ 107 | message: 'immerser will not work without layer nodes.', 108 | docs: '#prepare-your-markup', 109 | }); 110 | } 111 | if (this.solidNodeArray.length < 0) { 112 | showError({ 113 | message: 'immerser will not work without solid nodes.', 114 | docs: '#prepare-your-markup', 115 | }); 116 | } 117 | } 118 | 119 | mergeOptions(userOptions) { 120 | this.options = mergeOptions({ 121 | optionConfig: OPTION_CONFIG, 122 | userOptions, 123 | preffix: MESSAGE_PREFFIX, 124 | suffix: '\nCheck out documentation https://github.com/dubaua/immerser#options', 125 | }); 126 | } 127 | 128 | getClassnamesFromMarkup() { 129 | this.layerNodeArray.forEach((layerNode, layerIndex) => { 130 | if (layerNode.dataset.immerserLayerConfig) { 131 | try { 132 | this.options.solidClassnameArray[layerIndex] = JSON.parse(layerNode.dataset.immerserLayerConfig); 133 | } catch (e) { 134 | console.error(MESSAGE_PREFFIX, 'Failed to parse JSON classname configuration.', e); 135 | } 136 | } 137 | }); 138 | } 139 | 140 | validateSolidClassnameArray() { 141 | const layerCount = this.layerNodeArray.length; 142 | const classnamesCount = this.options.solidClassnameArray.length; 143 | if (classnamesCount !== layerCount) { 144 | showError({ 145 | message: 'solidClassnameArray length differs from count of layers', 146 | docs: '#options', 147 | }); 148 | } 149 | } 150 | 151 | initSectionIds() { 152 | this.layerNodeArray.forEach((layerNode, layerIndex) => { 153 | let id = layerNode.id; 154 | if (!id) { 155 | id = `immerser-section-${layerIndex}`; 156 | layerNode.id = id; 157 | layerNode.__immerserCustomId = true; 158 | } 159 | this.stateIndexById[id] = layerIndex; 160 | }); 161 | } 162 | 163 | initStatemap() { 164 | this.stateArray = this.layerNodeArray.map((layerNode, layerIndex) => { 165 | const solidClassnames = this.options.solidClassnameArray[layerIndex]; 166 | const { id } = layerNode; 167 | return { 168 | beginEnter: 0, 169 | beginLeave: 0, 170 | endEnter: 0, 171 | endLeave: 0, 172 | id, 173 | layerBottom: 0, 174 | layerTop: 0, 175 | maskInnerNode: null, 176 | maskNode: null, 177 | layerNode: layerNode, 178 | solidClassnames, 179 | }; 180 | }); 181 | } 182 | 183 | validateClassnames() { 184 | const noClassnameConfigPassed = this.stateArray.every((state) => isEmpty(state.solidClassnames)); 185 | if (noClassnameConfigPassed) { 186 | showError({ 187 | message: 'immerser will do nothing without solid classname configuration.', 188 | docs: '#prepare-your-markup', 189 | }); 190 | } 191 | } 192 | 193 | toggleBindOnRezise() { 194 | this.stopToggleBindOnRezise = this.reactiveWindowWidth.subscribe((nextWindowWidth) => { 195 | if (nextWindowWidth >= this.options.fromViewportWidth) { 196 | if (!this.isBound) { 197 | this.bind(); 198 | } 199 | } else if (this.isBound) { 200 | this.unbind(); 201 | } 202 | }); 203 | } 204 | 205 | setSizes() { 206 | this.documentHeight = document.documentElement.offsetHeight; 207 | this.windowHeight = window.innerHeight; 208 | this.immerserTop = this.rootNode.offsetTop; 209 | this.immerserHeight = this.rootNode.offsetHeight; 210 | 211 | this.stateArray = this.stateArray.map((state) => { 212 | const layerTop = state.layerNode.offsetTop; 213 | const layerBottom = layerTop + state.layerNode.offsetHeight; 214 | 215 | const endEnter = layerTop - this.immerserTop; 216 | const beginEnter = endEnter - this.immerserHeight; 217 | const endLeave = layerBottom - this.immerserTop; 218 | const beginLeave = endLeave - this.immerserHeight; 219 | 220 | return { 221 | ...state, 222 | layerTop, 223 | layerBottom, 224 | beginEnter, 225 | endEnter, 226 | beginLeave, 227 | endLeave, 228 | }; 229 | }); 230 | 231 | this.reactiveWindowWidth.value = window.innerWidth; 232 | } 233 | 234 | addScrollAndResizeListeners() { 235 | if (this.options.isScrollHandled) { 236 | this.onScroll = this.handleScroll.bind(this); 237 | window.addEventListener('scroll', this.onScroll, false); 238 | } 239 | this.onResize = this.handleResize.bind(this); 240 | window.addEventListener('resize', this.onResize, false); 241 | } 242 | 243 | bind() { 244 | this.createMarkup(); 245 | this.initPagerLinks(); 246 | this.initHoverSynchro(); 247 | this.attachCallbacks(); 248 | this.isBound = true; 249 | this.draw(); 250 | if (typeof this.options.onBind === 'function') { 251 | this.options.onBind(this); 252 | } 253 | } 254 | 255 | unbind() { 256 | this.detachCallbacks(); 257 | this.removeSyncroHoverListeners(); 258 | this.clearCustomSectionIds(); 259 | this.restoreOriginalSolidNodes(); 260 | this.cleanupClonnedMarkup(); 261 | this.isBound = false; 262 | if (typeof this.options.onUnbind === 'function') { 263 | this.options.onUnbind(this); 264 | } 265 | this.reactiveActiveLayer.value = undefined; 266 | } 267 | 268 | destroy() { 269 | this.unbind(); 270 | this.stopToggleBindOnRezise(); 271 | this.removeScrollAndResizeListeners(); 272 | if (typeof this.options.onDestroy === 'function') { 273 | this.options.onDestroy(this); 274 | } 275 | this.initState(); 276 | } 277 | 278 | createMarkup() { 279 | bindStyles(this.rootNode, NOT_INTERACTIVE_STYLES); 280 | this.initCustomMarkup(); 281 | this.originalSolidNodeArray = getNodeArray({ selector: this.selectors.solid, parent: this.rootNode }); 282 | 283 | this.stateArray = this.stateArray.map((state, stateIndex) => { 284 | // create or assign existing markup, bind styles 285 | const maskNode = this.isCustomMarkup ? this.customMaskNodeArray[stateIndex] : document.createElement('div'); 286 | bindStyles(maskNode, CROPPED_FULL_ABSOLUTE_STYLES); 287 | 288 | const maskInnerNode = this.isCustomMarkup 289 | ? maskNode.querySelector(this.selectors.maskInner) 290 | : document.createElement('div'); 291 | bindStyles(maskInnerNode, CROPPED_FULL_ABSOLUTE_STYLES); 292 | 293 | // mark created masks with data attributes 294 | if (!this.isCustomMarkup) { 295 | maskNode.dataset.immerserMask = ''; 296 | maskInnerNode.dataset.immerserMaskInner = ''; 297 | } 298 | 299 | // clone solids to innerMask 300 | this.originalSolidNodeArray.forEach((childNode) => { 301 | const clonnedChildNode = childNode.cloneNode(true); 302 | bindStyles(clonnedChildNode, INTERACTIVE_STYLES); 303 | clonnedChildNode.__immerserClonned = true; 304 | maskInnerNode.appendChild(clonnedChildNode); 305 | }); 306 | 307 | // assign class modifiers to cloned solids 308 | const clonedSolidNodeList = maskInnerNode.querySelectorAll(this.selectors.solid); 309 | forEachNode(clonedSolidNodeList, (clonedSolidNode) => { 310 | const solidId = clonedSolidNode.dataset.immerserSolid; 311 | if (state.solidClassnames && Object.prototype.hasOwnProperty.call(state.solidClassnames, solidId)) { 312 | clonedSolidNode.classList.add(state.solidClassnames[solidId]); 313 | } 314 | }); 315 | 316 | // a11y 317 | if (stateIndex !== 0) { 318 | maskNode.setAttribute('aria-hidden', 'true'); 319 | } 320 | 321 | maskNode.appendChild(maskInnerNode); 322 | this.rootNode.appendChild(maskNode); 323 | 324 | this.maskNodeArray.push(maskNode); 325 | 326 | return { ...state, maskNode, maskInnerNode }; 327 | }); 328 | 329 | this.detachOriginalSolidNodes(); 330 | } 331 | 332 | initCustomMarkup() { 333 | this.customMaskNodeArray = getNodeArray({ selector: this.selectors.mask, parent: this.rootNode }); 334 | this.isCustomMarkup = this.customMaskNodeArray.length === this.stateArray.length; 335 | 336 | if (this.customMaskNodeArray.length > 0 && !this.isCustomMarkup) { 337 | // later allow explicitly pass mask index? 338 | showError({ 339 | message: 'You\'re trying use custom markup, but count of your immerser masks doesn\'t equal layers count.', 340 | warning: true, 341 | docs: '#cloning-event-listeners', 342 | }); 343 | } 344 | 345 | // since custom child wrapped in ignoring pointer and touch events immerser mask, we should explicitly set them on 346 | this.customMaskNodeArray.forEach((customMaskNode) => { 347 | const customChildrenHTMLCollection = customMaskNode.querySelector(this.selectors.maskInner).children; 348 | for (let i = 0; i < customChildrenHTMLCollection.length; i++) { 349 | bindStyles(customChildrenHTMLCollection[i], INTERACTIVE_STYLES); 350 | } 351 | }); 352 | } 353 | 354 | detachOriginalSolidNodes() { 355 | this.originalSolidNodeArray.forEach((childNode) => { 356 | this.rootNode.removeChild(childNode); 357 | }); 358 | } 359 | 360 | initPagerLinks() { 361 | // its here, because we need to clone pager link first, then init them 362 | this.pagerLinkNodeArray = getNodeArray({ selector: this.selectors.pagerLink, parent: this.rootNode }); 363 | this.pagerLinkNodeArray.forEach((pagerLinkNode) => { 364 | const { href } = pagerLinkNode; 365 | if (href) { 366 | const layerId = href.split('#')[1]; 367 | if (layerId) { 368 | const layerIndex = this.stateIndexById[layerId]; 369 | pagerLinkNode.dataset.immerserLayerIndex = layerIndex.toString(); 370 | } 371 | } 372 | }); 373 | } 374 | 375 | initHoverSynchro() { 376 | // its here, because we need to clone nodes first, then init them 377 | this.synchroHoverNodeArray = getNodeArray({ selector: this.selectors.synchroHover, parent: this.rootNode }); 378 | 379 | this.onSynchroHoverMouseOver = (e) => { 380 | const synchroHoverId = e.target.dataset.immerserSynchroHover; 381 | this.reactiveSynchroHoverId.value = synchroHoverId; 382 | }; 383 | 384 | this.onSynchroHoverMouseOut = () => { 385 | this.reactiveSynchroHoverId.value = undefined; 386 | }; 387 | 388 | this.synchroHoverNodeArray.forEach((synchroHoverNode) => { 389 | synchroHoverNode.addEventListener('mouseover', this.onSynchroHoverMouseOver); 390 | synchroHoverNode.addEventListener('mouseout', this.onSynchroHoverMouseOut); 391 | }); 392 | } 393 | 394 | attachCallbacks() { 395 | if (this.pagerLinkNodeArray.length > 0) { 396 | this.stopRedrawingPager = this.reactiveActiveLayer.subscribe(this.drawPagerLinks.bind(this)); 397 | } 398 | 399 | if (this.options.hasToUpdateHash) { 400 | this.stopUpdatingHash = this.reactiveActiveLayer.subscribe(this.drawHash.bind(this)); 401 | } 402 | 403 | if (typeof this.options.onActiveLayerChange === 'function') { 404 | this.stopFiringActiveLayerChangeCallback = this.reactiveActiveLayer.subscribe((nextIndex) => { 405 | this.options.onActiveLayerChange(nextIndex, this); 406 | }); 407 | } 408 | 409 | if (this.synchroHoverNodeArray.length > 0) { 410 | this.stopTrackingSynchroHover = this.reactiveSynchroHoverId.subscribe(this.drawSynchroHover.bind(this)); 411 | } 412 | } 413 | 414 | detachCallbacks() { 415 | if (typeof this.stopRedrawingPager === 'function') { 416 | this.stopRedrawingPager(); 417 | } 418 | 419 | if (typeof this.stopUpdatingHash === 'function') { 420 | this.stopUpdatingHash(); 421 | } 422 | 423 | if (typeof this.stopFiringActiveLayerChangeCallback === 'function') { 424 | this.stopFiringActiveLayerChangeCallback(); 425 | } 426 | 427 | if (typeof this.stopTrackingSynchroHover === 'function') { 428 | this.stopTrackingSynchroHover(); 429 | } 430 | } 431 | 432 | removeSyncroHoverListeners() { 433 | this.synchroHoverNodeArray.forEach((synchroHoverNode) => { 434 | synchroHoverNode.removeEventListener('mouseover', this.onSynchroHoverMouseOver); 435 | synchroHoverNode.removeEventListener('mouseout', this.onSynchroHoverMouseOut); 436 | }); 437 | } 438 | 439 | clearCustomSectionIds() { 440 | this.stateArray.forEach((state) => { 441 | if (state.layerNode.__immerserCustomId) { 442 | state.layerNode.removeAttribute('id'); 443 | } 444 | }); 445 | } 446 | 447 | restoreOriginalSolidNodes() { 448 | this.originalSolidNodeArray.forEach((childNode) => { 449 | this.rootNode.appendChild(childNode); 450 | }); 451 | } 452 | 453 | cleanupClonnedMarkup() { 454 | this.maskNodeArray.forEach((immerserMaskNode) => { 455 | if (this.isCustomMarkup) { 456 | immerserMaskNode.removeAttribute('style'); 457 | immerserMaskNode.removeAttribute('aria-hidden'); 458 | const immerserMaskInnerNode = immerserMaskNode.querySelector(this.selectors.maskInner); 459 | immerserMaskInnerNode.removeAttribute('style'); 460 | const clonnedSolidNodeArray = getNodeArray({ selector: this.selectors.solid, parent: immerserMaskInnerNode }); 461 | clonnedSolidNodeArray.forEach((clonnedSolideNode) => { 462 | if (clonnedSolideNode.__immerserClonned) { 463 | immerserMaskInnerNode.removeChild(clonnedSolideNode); 464 | } 465 | }); 466 | } else { 467 | this.rootNode.removeChild(immerserMaskNode); 468 | } 469 | }); 470 | } 471 | 472 | removeScrollAndResizeListeners() { 473 | if (this.options.isScrollHandled) { 474 | window.removeEventListener('scroll', this.onScroll, false); 475 | } 476 | window.removeEventListener('resize', this.onResize, false); 477 | } 478 | 479 | draw(scrollY) { 480 | const y = scrollY !== undefined ? scrollY : getLastScrollPosition().y; 481 | this.stateArray.forEach( 482 | ({ beginEnter, endEnter, beginLeave, endLeave, maskNode, maskInnerNode, layerTop, layerBottom }, layerIndex) => { 483 | let progress; 484 | 485 | if (beginEnter > y) { 486 | progress = this.immerserHeight; 487 | } else if (beginEnter <= y && y < endEnter) { 488 | progress = endEnter - y; 489 | } else if (endEnter <= y && y < beginLeave) { 490 | progress = 0; 491 | } else if (beginLeave <= y && y < endLeave) { 492 | progress = beginLeave - y; 493 | } else if (y >= endLeave) { 494 | progress = -this.immerserHeight; 495 | } 496 | 497 | maskNode.style.transform = `translateY(${progress}px)`; 498 | maskInnerNode.style.transform = `translateY(${-progress}px)`; 499 | 500 | const pagerScrollActivePoint = y + this.windowHeight * (1 - this.options.pagerThreshold); 501 | if (layerTop <= pagerScrollActivePoint && pagerScrollActivePoint < layerBottom) { 502 | this.reactiveActiveLayer.value = layerIndex; 503 | } 504 | }, 505 | ); 506 | } 507 | 508 | drawPagerLinks(layerIndex) { 509 | this.pagerLinkNodeArray.forEach((pagerLinkNode) => { 510 | if (parseInt(pagerLinkNode.dataset.immerserLayerIndex, 10) === layerIndex) { 511 | pagerLinkNode.classList.add(this.options.pagerLinkActiveClassname); 512 | } else { 513 | pagerLinkNode.classList.remove(this.options.pagerLinkActiveClassname); 514 | } 515 | }); 516 | } 517 | 518 | drawHash(layerIndex) { 519 | const { id, layerNode } = this.stateArray[layerIndex]; 520 | // this prevent move to anchor 521 | layerNode.removeAttribute('id'); 522 | window.location.hash = id; 523 | layerNode.setAttribute('id', id); 524 | } 525 | 526 | drawSynchroHover(synchroHoverId) { 527 | this.synchroHoverNodeArray.forEach((synchroHoverNode) => { 528 | if (synchroHoverNode.dataset.immerserSynchroHover === synchroHoverId) { 529 | synchroHoverNode.classList.add('_hover'); 530 | } else { 531 | synchroHoverNode.classList.remove('_hover'); 532 | } 533 | }); 534 | } 535 | 536 | adjustScroll() { 537 | const { layerTop, layerBottom } = this.stateArray[this.reactiveActiveLayer.value]; 538 | const { x, y } = getLastScrollPosition(); 539 | const topThreshold = Math.abs(y - layerTop); 540 | const bottomThreshold = Math.abs(y + this.windowHeight - layerBottom); 541 | 542 | if (topThreshold !== 0 && bottomThreshold !== 0) { 543 | if (topThreshold <= bottomThreshold && topThreshold <= this.options.scrollAdjustThreshold) { 544 | window.scrollTo(x, layerTop); 545 | } else if (bottomThreshold <= topThreshold && bottomThreshold <= this.options.scrollAdjustThreshold) { 546 | window.scrollTo(x, layerBottom - this.windowHeight); 547 | } 548 | } 549 | } 550 | 551 | onDOMChange() { 552 | this.setSizes(); 553 | this.draw(); 554 | } 555 | 556 | handleScroll() { 557 | if (this.isBound) { 558 | if (this.scrollFrameId) { 559 | window.cancelAnimationFrame(this.scrollFrameId); 560 | this.scrollFrameId = null; 561 | } 562 | this.scrollFrameId = window.requestAnimationFrame(() => { 563 | this.draw(); 564 | if (this.options.scrollAdjustThreshold > 0) { 565 | if (this.scrollAdjustTimerId) { 566 | clearTimeout(this.scrollAdjustTimerId); 567 | this.scrollAdjustTimerId = null; 568 | } 569 | this.scrollAdjustTimerId = setTimeout(this.adjustScroll.bind(this), this.options.scrollAdjustDelay); 570 | } 571 | }, this.options.scrollAdjustDelay); 572 | } 573 | } 574 | 575 | handleResize() { 576 | if (this.resizeFrameId) { 577 | window.cancelAnimationFrame(this.resizeFrameId); 578 | this.resizeFrameId = null; 579 | } 580 | this.resizeFrameId = window.requestAnimationFrame(() => { 581 | this.setSizes(); 582 | this.draw(); 583 | }); 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { MESSAGE_PREFFIX } from '@/defaults.js'; 2 | 3 | export function bindStyles(node, styles) { 4 | for (const rule in styles) { 5 | node.style[rule] = styles[rule]; 6 | } 7 | } 8 | 9 | export function forEachNode(nodeList, callback) { 10 | for (let index = 0; index < nodeList.length; index++) { 11 | const node = nodeList[index]; 12 | callback(node, index, nodeList); 13 | } 14 | } 15 | 16 | export function getNodeArray({ selector, parent = document }) { 17 | if (!parent) { 18 | return []; 19 | } 20 | const nodeList = parent.querySelectorAll(selector); 21 | return [].slice.call(nodeList); 22 | } 23 | 24 | export function showError({ message, warning = false, docs }) { 25 | const docsHash = docs ? docs : ''; 26 | const resultMessage = `${MESSAGE_PREFFIX} ${message} \nCheck out documentation https://github.com/dubaua/immerser${docsHash}`; 27 | if (warning) { 28 | console.warn(resultMessage); 29 | } else { 30 | throw new Error(resultMessage); 31 | } 32 | } 33 | 34 | export function isEmpty(obj) { 35 | if (!obj) { 36 | return true; 37 | } 38 | return Object.keys(obj).length === 0 && obj.constructor === Object; 39 | } 40 | 41 | export function limit(number, min, max) { 42 | return Math.max(Math.min(number, max), min); 43 | } 44 | 45 | export function getLastScrollPosition() { 46 | const scrollX = window.scrollX || document.documentElement.scrollLeft; 47 | const scrollY = window.scrollY || document.documentElement.scrollTop; 48 | // limit scroll position between 0 and document height in case of iOS overflow scroll 49 | return { 50 | x: limit(scrollX, 0, document.documentElement.offsetWidth), 51 | y: limit(scrollY, 0, document.documentElement.offsetHeight), 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /webpack.config.docs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCSSExtractPlugin = require('mini-css-extract-plugin'); 4 | const TerserJSPlugin = require('terser-webpack-plugin'); 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const I18nPlugin = require('@zainulbr/i18n-webpack-plugin'); 7 | const languages = { 8 | en: require('./i18n/en.js'), 9 | ru: require('./i18n/ru.js'), 10 | }; 11 | 12 | const isDev = process.env.NODE_ENV !== 'production'; 13 | 14 | module.exports = Object.keys(languages).map((language) => ({ 15 | name: language, 16 | entry: { 17 | main: path.resolve(__dirname, 'example/main.js'), 18 | }, 19 | resolve: { 20 | alias: { 21 | '@': path.resolve(__dirname, 'src'), 22 | }, 23 | }, 24 | output: { 25 | path: __dirname + '/docs', 26 | filename: 'main.js', 27 | }, 28 | devtool: 'source-map', 29 | optimization: { 30 | minimizer: [ 31 | new TerserJSPlugin({ 32 | terserOptions: { 33 | mangle: { 34 | reserved: ['Immerser'], 35 | }, 36 | }, 37 | }), 38 | new OptimizeCSSAssetsPlugin({}), 39 | ], 40 | }, 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.js$/, 45 | use: ['babel-loader'], 46 | exclude: /node_modules/, 47 | }, 48 | { 49 | test: /\.(png|svg|jpg|jpeg|gif)$/, 50 | exclude: /svg[/\\]/, 51 | use: [ 52 | { 53 | loader: 'file-loader', 54 | options: { 55 | name: 'images/[name].[ext]', 56 | }, 57 | }, 58 | ], 59 | }, 60 | { 61 | test: /\.scss$/, 62 | use: [MiniCSSExtractPlugin.loader, { loader: 'css-loader' }, { loader: 'sass-loader' }], 63 | }, 64 | { 65 | test: /\.css$/, 66 | use: [MiniCSSExtractPlugin.loader, { loader: 'css-loader' }], 67 | }, 68 | ], 69 | }, 70 | plugins: [ 71 | new HtmlWebpackPlugin({ 72 | template: './example/index.html', 73 | filename: language === 'en' ? 'index.html' : language + '.html', 74 | favicon: './example/favicon/favicon.ico', 75 | minify: { 76 | collapseWhitespace: true, 77 | removeComments: true, 78 | removeRedundantAttributes: true, 79 | removeScriptTypeAttributes: true, 80 | removeStyleLinkTypeAttributes: true, 81 | useShortDoctype: true, 82 | }, 83 | }), 84 | new MiniCSSExtractPlugin({ 85 | filename: isDev ? '[name].css' : '[name].[hash].css', 86 | chunkFilename: isDev ? '[id].css' : '[id].[hash].css', 87 | }), 88 | new I18nPlugin(languages[language], { 89 | functionName: 'getTranslation', 90 | }), 91 | ], 92 | })); 93 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/immerser.js', 6 | resolve: { 7 | alias: { 8 | '@': path.resolve(__dirname, 'src'), 9 | }, 10 | }, 11 | output: { 12 | filename: 'immerser.min.js', 13 | library: 'Immerser', 14 | libraryTarget: 'umd', 15 | libraryExport: 'default', 16 | globalObject: 'this', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | use: ['babel-loader'], 23 | exclude: /node_modules/, 24 | }, 25 | ], 26 | }, 27 | devtool: 'source-map', 28 | optimization: { 29 | minimize: true, 30 | minimizer: [ 31 | new TerserPlugin({ 32 | terserOptions: { 33 | mangle: { 34 | reserved: ['Immerser'], 35 | }, 36 | }, 37 | }), 38 | ], 39 | }, 40 | }; 41 | --------------------------------------------------------------------------------