├── .npmignore ├── docs └── favicon.ico ├── example ├── content │ ├── code │ │ ├── handle-clone-hover.css │ │ ├── handle-dom-change.js │ │ ├── cloning-event-listeners.html │ │ ├── external-scroll-engine.js │ │ ├── event-table.md │ │ ├── styles.css │ │ ├── public-fields.md │ │ ├── table.md │ │ ├── markup.html │ │ ├── initialization.js │ │ ├── event-table.html │ │ ├── public-fields.html │ │ └── table.html │ └── emoji.html ├── favicon │ ├── favicon.gif │ ├── favicon.ico │ ├── favicon.jpg │ ├── favicon.png │ └── favicon.svg ├── styles │ ├── shared │ │ ├── _globals.scss │ │ ├── _transitions.scss │ │ ├── _colors.scss │ │ ├── _breakpoints.scss │ │ └── _grid.scss │ ├── components │ │ ├── sulk.scss │ │ ├── header.scss │ │ ├── footer.scss │ │ ├── menu.scss │ │ ├── about.scss │ │ ├── logo.scss │ │ ├── emoji.scss │ │ ├── pager.scss │ │ ├── language.scss │ │ ├── highlighter.scss │ │ ├── fixed.scss │ │ ├── scrollbar.scss │ │ ├── typography.scss │ │ ├── common.scss │ │ └── code-highlight.scss │ └── main.scss ├── emoji │ ├── render-tongue.ts │ ├── render-hand.ts │ ├── render-glasses.ts │ ├── render-left-brow.ts │ ├── render-mouth-ellipse.ts │ ├── paths.ts │ ├── render-open-eye.ts │ ├── render-hp-bar.ts │ ├── mix-config-by-progress.ts │ ├── render-emoji-face.ts │ ├── select-emoji-nodes.ts │ ├── config.ts │ ├── sulking │ │ ├── phrases.ru.ts │ │ ├── phrases.en.ts │ │ └── init-sulking.ts │ └── animation.ts ├── svg │ ├── options.svg │ ├── why-immerser.svg │ ├── how-it-works-face.svg │ ├── how-it-works-hand.svg │ ├── how-to-use.svg │ └── possibilities.svg └── main.ts ├── .gitignore ├── tsconfig.eslint.json ├── .babelrc ├── tsconfig.json ├── .vscode └── settings.json ├── .github └── workflows │ └── publish.yml ├── api-extractor.json ├── webpack.config.js ├── agents.md ├── scripts ├── post-build.js ├── build-source-code.js ├── build-public-fields.js ├── readme.js ├── build-events-table.js └── build-options-table.js ├── TODO.md ├── src ├── utils.ts ├── options.ts └── types.ts ├── .eslintrc ├── package.json ├── webpack.config.docs.js ├── changelog.md ├── dist └── immerser.min.d.ts ├── i18n ├── en.js └── ru.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | !README.md 2 | !dist/tsdoc-metadata.json 3 | *.map 4 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /example/content/code/handle-clone-hover.css: -------------------------------------------------------------------------------- 1 | a:hover, 2 | a._hover { 3 | color: magenta; 4 | } -------------------------------------------------------------------------------- /example/favicon/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/HEAD/example/favicon/favicon.gif -------------------------------------------------------------------------------- /example/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/HEAD/example/favicon/favicon.ico -------------------------------------------------------------------------------- /example/favicon/favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/HEAD/example/favicon/favicon.jpg -------------------------------------------------------------------------------- /example/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dubaua/immerser/HEAD/example/favicon/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dev_dist/ 4 | /.cache 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | dist/types 10 | dist/tsdoc-metadata.json 11 | -------------------------------------------------------------------------------- /example/styles/shared/_globals.scss: -------------------------------------------------------------------------------- 1 | @forward './breakpoints'; 2 | @forward './colors'; 3 | @forward './grid'; 4 | @forward './transitions'; 5 | 6 | $base: 16px; 7 | $cyrillic-modifier: '.font-cyrillic'; 8 | $contrast-surface: '.background--contrast'; 9 | -------------------------------------------------------------------------------- /example/content/code/handle-dom-change.js: -------------------------------------------------------------------------------- 1 | // <%= getTranslation('recipes-changing-dom') %> 2 | document.appendChild(someNode); 3 | document.removeChild(anotherNode); 4 | 5 | // <%= getTranslation('recipes-redraw-immerser') %> 6 | immerserInstance.render(); 7 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "declaration": false, 6 | "emitDeclarationOnly": false, 7 | "rootDir": "." 8 | }, 9 | "include": ["src/**/*.ts", "example/**/*.ts"], 10 | "exclude": ["dist", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /example/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /example/emoji/render-tongue.ts: -------------------------------------------------------------------------------- 1 | const tongueMaxShiftX = 102; 2 | const tongueMaxShiftY = 72; 3 | 4 | export function renderTongue(valueX: number, valueY: number, node: SVGPathElement): void { 5 | const shiftX = tongueMaxShiftX * valueX; 6 | const shiftY = tongueMaxShiftY * valueY; 7 | node.setAttribute('transform', `translate(${shiftX} ${shiftY})`); 8 | } 9 | -------------------------------------------------------------------------------- /example/styles/components/sulk.scss: -------------------------------------------------------------------------------- 1 | @use '../shared/globals.scss' as *; 2 | 3 | .sulk { 4 | overflow: visible; 5 | &__phrase { 6 | position: absolute; 7 | top: 39px; 8 | right: $col-width * 0.5; 9 | width: 250px; 10 | text-align: right; 11 | pointer-events: none; 12 | touch-action: none; 13 | user-select: none; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | "@babel/preset-typescript" 13 | ], 14 | "plugins": ["@babel/plugin-proposal-class-properties"] 15 | } 16 | -------------------------------------------------------------------------------- /example/content/code/cloning-event-listeners.html: -------------------------------------------------------------------------------- 1 |
| <%= getTranslation('event') %> | 5 |<%= getTranslation('arguments') %> | 6 |<%= getTranslation('description') %> | 7 |
|---|---|---|
| init | 12 |immerser: Immerser | 13 |<%= getTranslation('event-init') %> | 14 |
| bind | 17 |immerser: Immerser | 18 |<%= getTranslation('event-bind') %> | 19 |
| unbind | 22 |immerser: Immerser | 23 |<%= getTranslation('event-unbind') %> | 24 |
| destroy | 27 |immerser: Immerser | 28 |<%= getTranslation('event-destroy') %> | 29 |
| activeLayerChange | 32 |layerIndex: number, immerser: Immerser | 33 |<%= getTranslation('event-activeLayerChange') %> | 34 |
| layersUpdate | 37 |layersProgress: number[], immerser: Immerser | 38 |<%= getTranslation('event-layersUpdate') %> | 39 |
| <%= getTranslation('name') %> | 5 |<%= getTranslation('type') %> | 6 |<%= getTranslation('description') %> | 7 |
|---|---|---|
| debug | 12 |property | 13 |<%= getTranslation('public-field-debug') %> | 14 |
| bind | 17 |method | 18 |<%= getTranslation('public-field-bind') %> | 19 |
| unbind | 22 |method | 23 |<%= getTranslation('public-field-unbind') %> | 24 |
| destroy | 27 |method | 28 |<%= getTranslation('public-field-destroy') %> | 29 |
| render | 32 |method | 33 |<%= getTranslation('public-field-render') %> | 34 |
| syncScroll | 37 |method | 38 |<%= getTranslation('public-field-syncScroll') %> | 39 |
| on | 42 |method | 43 |<%= getTranslation('public-field-on') %> | 44 |
| once | 47 |method | 48 |<%= getTranslation('public-field-once') %> | 49 |
| off | 52 |method | 53 |<%= getTranslation('public-field-off') %> | 54 |
| activeIndex | 57 |getter | 58 |<%= getTranslation('public-field-activeIndex') %> | 59 |
| isBound | 62 |getter | 63 |<%= getTranslation('public-field-isBound') %> | 64 |
| rootNode | 67 |getter | 68 |<%= getTranslation('public-field-rootNode') %> | 69 |
| layerProgressArray | 72 |getter | 73 |<%= getTranslation('public-field-layerProgressArray') %> | 74 |
| <%= getTranslation('option') %> | 5 |<%= getTranslation('type') %> | 6 |<%= getTranslation('default') %> | 7 |<%= getTranslation('description') %> | 8 |
|---|---|---|---|
| solidClassnameArray | 13 |array | 14 |[] | 15 |<%= getTranslation('option-solidClassnameArray') %> | 16 |
| fromViewportWidth | 19 |number | 20 |0 | 21 |<%= getTranslation('option-fromViewportWidth') %> | 22 |
| pagerThreshold | 25 |number | 26 |0.5 | 27 |<%= getTranslation('option-pagerThreshold') %> | 28 |
| hasToUpdateHash | 31 |boolean | 32 |false | 33 |<%= getTranslation('option-hasToUpdateHash') %> | 34 |
| scrollAdjustThreshold | 37 |number | 38 |0 | 39 |<%= getTranslation('option-scrollAdjustThreshold') %> | 40 |
| scrollAdjustDelay | 43 |number | 44 |600 | 45 |<%= getTranslation('option-scrollAdjustDelay') %> | 46 |
| pagerLinkActiveClassname | 49 |string | 50 |pager-link-active | 51 |<%= getTranslation('option-pagerLinkActiveClassname') %> | 52 |
| isScrollHandled | 55 |boolean | 56 |true | 57 |<%= getTranslation('option-isScrollHandled') %> | 58 |
| debug | 61 |boolean | 62 |false | 63 |<%= getTranslation('option-debug') %> | 64 |
| on | 67 |object | 68 |{} | 69 |<%= getTranslation('option-on') %> | 70 |
${highlighted}`;
56 | }
57 |
58 | function buildSourceCode() {
59 | const html = fs.readFileSync(indexPath, 'utf8');
60 | let replacements = 0;
61 |
62 | const nextHtml = html.replace(blockRE, (match, startComment, sourcePath, flagStr, _content, endComment) => {
63 | replacements += 1;
64 | const flags = (flagStr || '')
65 | .split(',')
66 | .map((s) => s.trim().toLowerCase())
67 | .filter(Boolean);
68 | const skipPrism = flags.includes('raw') || flags.includes('plain') || flags.includes('no-prism');
69 |
70 | const codeBlock = renderSource(sourcePath, { skipPrism });
71 |
72 | return `${startComment}\n${codeBlock}\n${endComment}`;
73 | });
74 |
75 | if (replacements === 0) {
76 | console.warn('No @build-source-code blocks found.');
77 | return;
78 | }
79 |
80 | fs.writeFileSync(indexPath, nextHtml, 'utf8');
81 | console.log(`Replaced ${replacements} @build-source-code block(s) in ${indexPath}`);
82 | }
83 |
84 | buildSourceCode();
85 |
--------------------------------------------------------------------------------
/example/styles/shared/_grid.scss:
--------------------------------------------------------------------------------
1 | @use 'sass:math';
2 | @use './breakpoints' as *;
3 |
4 | $alpha-width: 0;
5 | $alpha-width--xl: 4;
6 |
7 | $beta-width: 10;
8 | $beta-width--xl: 7;
9 |
10 | $gamma-width: 14;
11 | $gamma-width--xl: 13;
12 |
13 | $grid-width: $alpha-width + $beta-width + $gamma-width;
14 |
15 | $col-width: math.div(1, $grid-width) * 100vw;
16 |
17 | .grid {
18 | @include from('md') {
19 | display: flex;
20 | }
21 |
22 | &__content {
23 | padding-left: $col-width * 2;
24 | padding-right: $col-width * 2;
25 | @include from('sm') {
26 | padding-left: $col-width;
27 | padding-right: $col-width;
28 | }
29 | @include from('lg') {
30 | padding-left: $col-width * 0.5;
31 | padding-right: $col-width * 0.5;
32 | }
33 | }
34 |
35 | &__col {
36 | &--alpha {
37 | display: none;
38 | }
39 |
40 | @include from('md') {
41 | &--alpha-beta {
42 | $width: #{math.div($alpha-width + $beta-width, $grid-width) * 100}vw;
43 | flex-basis: $width;
44 | max-width: $width;
45 | }
46 |
47 | &--alpha-beta-gamma {
48 | $width: #{math.div($alpha-width + $beta-width + $gamma-width, $grid-width) * 100}vw;
49 | flex-basis: $width;
50 | max-width: $width;
51 | }
52 |
53 | &--beta {
54 | $width: #{math.div($beta-width, $grid-width) * 100}vw;
55 | flex-basis: $width;
56 | max-width: $width;
57 | }
58 |
59 | &--beta-gamma {
60 | $width: #{math.div($beta-width + $gamma-width, $grid-width) * 100}vw;
61 | flex-basis: $width;
62 | max-width: $width;
63 | }
64 |
65 | &--gamma {
66 | $width: #{math.div($gamma-width, $grid-width) * 100}vw;
67 | flex-basis: $width;
68 | max-width: $width;
69 | }
70 | }
71 | @include from('lg') {
72 | &--alpha {
73 | display: block;
74 | $width: #{math.div($alpha-width--xl, $grid-width) * 100}vw;
75 | flex-basis: $width;
76 | max-width: $width;
77 | }
78 |
79 | &--alpha-beta {
80 | $width: #{math.div($alpha-width--xl + $beta-width--xl, $grid-width) * 100}vw;
81 | flex-basis: $width;
82 | max-width: $width;
83 | }
84 |
85 | &--alpha-beta-gamma {
86 | $width: #{math.div($alpha-width--xl + $beta-width--xl + $gamma-width--xl, $grid-width) * 100}vw;
87 | flex-basis: $width;
88 | max-width: $width;
89 | }
90 |
91 | &--beta {
92 | $width: #{math.div($beta-width--xl, $grid-width) * 100}vw;
93 | flex-basis: $width;
94 | max-width: $width;
95 | }
96 |
97 | &--beta-gamma {
98 | $width: #{math.div($beta-width--xl + $gamma-width--xl, $grid-width) * 100}vw;
99 | flex-basis: $width;
100 | max-width: $width;
101 | }
102 |
103 | &--gamma {
104 | $width: #{math.div($gamma-width--xl, $grid-width) * 100}vw;
105 | flex-basis: $width;
106 | max-width: $width;
107 | }
108 | }
109 | }
110 | }
111 | $grid-width: $alpha-width + $beta-width + $gamma-width;
112 | $col-width: math.div(1, $grid-width) * 100vw;
113 |
--------------------------------------------------------------------------------
/src/options.ts:
--------------------------------------------------------------------------------
1 | import type { OptionConfig } from '@dubaua/merge-options';
2 | import type { EventName, Options } from './types';
3 |
4 | const CLASSNAME_REGEX = /^[a-z_-][a-z\d_-]*$/i;
5 |
6 | export const INITIAL_DEBUG = process.env.NODE_ENV === 'development';
7 |
8 | /** @public All available immerser event names. */
9 | export const EVENT_NAMES = ['init', 'bind', 'unbind', 'destroy', 'activeLayerChange', 'layersUpdate'] as const;
10 |
11 | function classnameValidator(str: string): boolean {
12 | return typeof str === 'string' && str !== '' && CLASSNAME_REGEX.test(str);
13 | }
14 |
15 | function onOptionValidator(on?: Options['on']): boolean {
16 | if (on === undefined) {
17 | return true;
18 | }
19 | if (!on || typeof on !== 'object' || Array.isArray(on)) {
20 | return false;
21 | }
22 | return Object.keys(on).every(
23 | (eventName) =>
24 | EVENT_NAMES.includes(eventName as EventName) &&
25 | (on as Record| <%= getTranslation('name') %> | `, 90 | `<%= getTranslation('type') %> | `, 91 | `<%= getTranslation('description') %> | `, 92 | '
|---|
| <%= getTranslation('event') %> | `, 121 | `<%= getTranslation('arguments') %> | `, 122 | `<%= getTranslation('description') %> | `, 123 | '
|---|
| <%= getTranslation(\'option\') %> | ', 124 | '<%= getTranslation(\'type\') %> | ', 125 | '<%= getTranslation(\'default\') %> | ', 126 | '<%= getTranslation(\'description\') %> | ', 127 | '
|---|
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 typescript. 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 |
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.
Next place absolutely positioned children into the immerser parent and add data-immerser-solid="solid-id" to each.
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.
Also feel free to add data-immerser-pager to create a pager for your layers.
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 event', 81 | 'callback-on-bind': 'callback on bind event', 82 | 'callback-on-unbind': 'callback on unbind event', 83 | 'callback-on-destroy': 'callback on destroy event', 84 | 'callback-on-active-layer-change': 'callback on active layer change event', 85 | 'callback-on-layers-update': 'callback on layers update event', 86 | 87 | 'how-it-works-title': 'How it Works', 88 | 'how-it-works-content': ` 89 |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.
90 |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.
91 |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.
92 |On scroll, immerser moves a mask of solids to show part of each solid group according to the layer below.
93 | `, 94 | 95 | 'options-title': 'Options', 96 | 'options-content': ` 97 |98 | You can pass options to immerser as data-attributes on layers or as object as function parameter. Data-attributes are 99 | processed last, so they override the options passed to the function. 100 |
101 | `, 102 | 103 | option: 'option', 104 | event: 'event', 105 | type: 'type', 106 | arguments: 'arguments', 107 | default: 'default', 108 | description: 'description', 109 | name: 'name', 110 | 111 | 'option-solidClassnameArray': 112 | 'Array of layer class configurations. Overriding by config passed in data-immerser-layer-config for corresponding layer. Configuration example is shown above', 113 | 'option-fromViewportWidth': 'A viewport width, from which immerser will init', 114 | 'option-pagerThreshold': 'How much next layer should be in viewport to trigger pager', 115 | 'option-hasToUpdateHash': 'Flag to control changing hash on pager active state change', 116 | 'option-scrollAdjustThreshold': 117 | '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', 118 | 'option-scrollAdjustDelay': 'Delay after user interaction and before scroll adjust', 119 | 'option-pagerLinkActiveClassname': 'Added to each pager link pointing to active', 120 | 'option-isScrollHandled': "Binds scroll listener if true. Set to false if you're using remote scroll controller", 121 | 'option-debug': 'Enables logging warnings and errors. Defaults to true in development, false otherwise', 122 | 'option-on': 'Initial event handlers map keyed by event name', 123 | 'events-title': 'Events', 124 | 'events-content': 125 | 'You can subscribe to events via the on option or by calling the on or once method on an immerser instance.
153 | Since immerser cloning nested nodes by default, all event listeners and data bound on nodes will be lost after 154 | init. Fortunately, you can markup the immerser yourself. It can be useful when you have event listeners 155 | on solids, reactive logic or more than classname switching. All you need is to place the number 156 | of nested immerser masks equal to the number of the layers. Look how I change the smiley emoji 157 | on the right in this page source. 158 |
159 | `, 160 | 161 | 'your-markup': 'your markup', 162 | 163 | 'handle-clone-hover-title': 'Handle Clone Hover', 164 | 'handle-clone-hover-content': ` 165 |
166 | As mentioned above, immerser cloning nested nodes to achieve changing on scroll. Therefore if you
167 | hover a partially visible element, only the visible part will change. If you want to synchronize all cloned links, just
168 | pass
169 | data-immerser-synchro-hover="hoverId" attribute. It will share _hover class between all
170 | nodes with this hoverId when the mouse is over one of them. Add _hover selector alongside your
171 | :hover pseudoselector to style your interactive elements.
172 |
177 | Immerser is not aware of changes in DOM, if you dynamically add or remove nodes. If you change height of the document
178 | and want immerser to recalculate and redraw solids, call render method on the immerser instance.
179 |
184 | If you drive scrolling with a custom scroll engine, for example Locomotive Scroll, disable immerser scroll listener with
185 | isScrollHandled=false flag and call syncScroll method every time the engine updates position.
186 | Immerser will only redraw masks without attaching another scroll handler. Keep in mind that immerser will not optimize calls this way, and performance optimization is client responsibility.
187 |
The core of the library was written in 2019 and significantly improved in 2022, before AI-assisted programming became a thing. In later iterations, AI was used as a supporting tool for infrastructure tasks, documentation updates, and generation of code generation.
196 |For me, AI is just another tool alongside linters, bundlers, and other means of speeding up and simplifying work. I am lazy, and my laziness pushes me toward inventing better tools.
197 |I use AI openly and consider it important to state this explicitly, because for some people it can be a deciding factor.
`, 198 | }; 199 | -------------------------------------------------------------------------------- /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 |
Через npm:
', 48 | 'install-yarn-label': 'Через yarn:
', 49 | 'install-browser-label': 'Или если вы хотите использовать иммёрсер в браузере как глобальную переменную:
', 50 | 51 | 'prepare-your-markup-title': 'Подготовьте разметку', 52 | 'prepare-your-markup-content': ` 53 |Сначала настройте свой фиксированный контейнер как корневой элемент иммёрсера, добавив атрибут data-immerser
Затем расположите в нем абсолютно позиционированные дочерние элементы и добавьте каждому атрибут data-immerser-solid="solid-id" с идентификатором блока.
Добавьте каждому слою атрибут data-immerser-layer. Передайте конфигурацию в виде JSON в каждый слой с помощью атрибута
56 | data-immerser-layer-config='{"solid-id": "classname-modifier"}'.
57 | Также вы можете передать конфигурацию всех слоев массивом в параметре solidClassnameArray настроек.
58 | Конфигурация должна содержать описание классов для блоков, когда они находятся поверх слоя.
Так же вы можете добавить элемент с атрибутом data-immerser-pager для создания навигации.
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 | 'callback-on-layers-update': 'колбек события обновления прогресса слоёв', 83 | 84 | 'how-it-works-title': 'Принцип работы', 85 | 'how-it-works-content': ` 86 |Сначала иммёрсер собирает информацию о слоях, блоках, окне и документе. Затем скрипт создает карту состояний для каждого слоя. Карта содержит размеры слоя, блоков и позиции их пересечений при скролле.
87 |После сбора информации скрипт копирует все блоки в маскирующий контейнер и применяет к каждому классы, переданные в настройках. Если вы добавили навигацию, то иммёрсер создаст ссылки на каждый слой.
88 |Затем иммёрсер подписывается на события скролла документа и изменения размеров окна.
89 |При скролле иммёрсер двигает маскирующий контейнер так, чтобы показывать часть каждой группы блоков для каждого слоя под ними. При изменении размеров окна скрипт рассчитает карту состояний заново.
90 | `, 91 | 92 | 'options-title': 'Настройки', 93 | 'options-content': ` 94 |95 | Вы можете передать настройки параметром функции конструктора или дата-атрибутом в документе. 96 | Дата-аттрибут обрабатывается последним, поэтому он переопределит настройки, переданные в конструктор. 97 |
98 | `, 99 | 100 | option: 'параметр', 101 | event: 'событие', 102 | type: 'тип', 103 | arguments: 'аргументы', 104 | default: 'значение по умолчанию', 105 | description: 'описание', 106 | name: 'название', 107 | 108 | 'option-solidClassnameArray': 109 | 'Массив настроек слоев. Конфигурация, переданная в data-immerser-layer-config перезапишет эту настройку для соответствующего слоя. Пример конфигурации показан выше', 110 | 'option-fromViewportWidth': 'Минимальная ширина окна для инициализации иммёрсера', 111 | 'option-pagerThreshold': 'Насколько должен следующий слой быть видим в окне, чтобы он стал активен в навигации', 112 | 'option-hasToUpdateHash': 'Флаг, контролирующий обновление хеша страницы', 113 | 'option-scrollAdjustThreshold': 114 | 'Дистанция до верха или низа окна браузера в пикселях. Если текущая дистанция меньше переданного значения, то скрипт подстроит положение скролла', 115 | 'option-scrollAdjustDelay': 'Сколько ждать бездействия пользователя, чтобы начать подстройку скролла', 116 | 'option-pagerLinkActiveClassname': 'Применяется, к каждой ссылке пейджера, ссылающуюся на активный слой', 117 | 'option-isScrollHandled': 118 | 'Подписывается на событие прокрутки, если включено. Выключите, если используете внешний контроллер скролла', 119 | 'option-debug': 'Включает логирование предупреждений и ошибок. По умолчанию true в режиме разработки, иначе false', 120 | 'option-on': 'Начальные обработчики событий, сгруппированные по имени события', 121 | 122 | 'events-title': 'События', 123 | 'events-content': 124 | 'На события можно подписаться через поле on в настройках конструктора или с помощью вызова метода on или once у экземпляра иммёрсера.
153 | Вы уже знаете, что иммёрсер клонирует элементы. 154 | Подписчики событий и данные, привязанные к нодам, не клонируются вместе с элементом. 155 | К счастью, вы можете разметить иммёрсер самостоятельно. 156 | Для этого разместите внутри корневого элемента маскирующие контейнеры для блоков по числу слоев. 157 | В таком случае скрипт не будет клонировать элементы. Подписчики и реактивная логика останутся нетронутыми. 158 | В примере на этой странице я создаю подписчик на клик по смайлу справа до инициализации. 159 |
160 | `, 161 | 162 | 'your-markup': 'ваша разметка', 163 | 164 | 'handle-clone-hover-title': 'Обработка наведения', 165 | 'handle-clone-hover-content': ` 166 |
167 | Если вы наведете мышь на элемент, находящийся на границе слоев,
168 | то псевдоселектор :hover сработает только на одну часть.
169 | Чтобы наведение сработало на все клоны элемента, задайте идентификатор наведения в атрибуте data-immerser-synchro-hover="hoverId".
170 | При наведении мыши на такой элемент, ко всем его клонам добавится класс _hover.
171 | Стилизуйте по этому селектору вместе с псевдоселектором :hover, чтобы добиться нужного эффекта.
172 |
177 | Иммёрсер не отслеживает изменения документа, если вы динамически добавляете или удаляете ноды. Если вы меняете высоту документа,
178 | и хотите, чтобы иммёрсер пересчитал и перерисовал блоки, вызовите метод render у экземпляра иммёрсера.
179 |
184 | Если прокруткой управляет кастомный движок, например, Locomotive Scroll, выключите обработчик скролла
185 | иммёрсера флагом isScrollHandled=false и вызывайте метод syncScroll при каждом обновлении позиции движком.
186 | Иммёрсер только перерисует маски и не будет вешать свой обработчик. Иммёрсер не оптимизирует вызовы в этом режиме — оптимизация производительности остается на стороне клиента.
187 |
Ядро библиотеки было написано в 2019 году и существенно доработано в 2022 году до появления программирования с ИИ. В более поздних итерациях ИИ использовался как вспомогательный инструмент для инфраструктурных задач, обновления документации и генерации обслуживающего кода.
196 |Для меня ИИ — это инструмент наравне с линтерами, сборщиками и другими средствами ускорения и упрощения работы. Я ленивый, и моя лень толкает меня на изобретения.
197 |Я использую ИИ открыто и считаю важным прямо об этом говорить, потому что для кого-то это может иметь решающее значение.
`, 198 | }; 199 | -------------------------------------------------------------------------------- /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 typescript. Only 6.63Kb 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 |