jekyll-page-asset 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ ## Run crappy demo site locally
2
+ ```
3
+ bundle install
4
+ bundle exec jekyll serve --livereload
5
+ ```
6
+ # jekyll-page-asset
7
+
8
+ The `page_asset` plugin designed for a project portfolio style ( although it might well work with blog/post style layouts) to make it easy to put assets that relate to a page, onto that page.
9
+
10
+ It's essentially a convenience tag for adding components that reference media files into project pages. The media files are stored in the `assets/project_media/<project-slug>/` folder and can be referenced by just a relative path to that folder.
11
+
12
+ The intent is to make it easy to add media components into project pages by defining a simple set of the most likely needed. Users should be able to stay in markdown writing flow and not think too much about page structure.
13
+
14
+
15
+ ## Supported components
16
+ ### image
17
+ Standard image. This delegates to the `jekyll_picture_tag` plugin which handles responsive images. Any args past the filename are passed through to that plugin, so you can use any of the options it supports.
18
+
19
+ `{% page_asset image <image-filename> [alt="<alt-text>"] %}`
20
+
21
+ ### foreground video
22
+ i.e. a video that is embedded in the page and can be played by the user.
23
+
24
+ `{% page_asset video <video-filename> %}`
25
+
26
+ ### background video
27
+ i.e. video that loads and autoplays muted by default.
28
+
29
+ `{% page_asset video <video-filename> display=background %}`
30
+
31
+ ### scroll scrub video
32
+ Video that loads muted and advances as the user scrolls the page.
33
+
34
+ `{% page_asset scroll_scrub_video <video-filename> %}`
35
+
36
+ ### gallery
37
+ Pass a folder name in the media asset directory and all images in that folder will be displayed in a gallery.
38
+ The generated html and script for this will also write tags to two target mount points in the site, if found.
39
+ `project-asset-gallery-lightbox` is the target for the lightbox html. By default this is styled to fill the viewport area of the component that it is mounted in to act as a full area takeover.
40
+ `project-asset-gallery-controls` is the target for the gallery controls - to move to next/previous images and close the lightbox. This appears when the lightbox is open.
41
+ `{% page_asset gallery <gallery-folder-name> %}`
42
+
43
+ ### fountain script reader
44
+ Simple scrollable script reader for [fountain](https://fountain.io/) formatted scripts.
45
+
46
+ `{% page_asset fountain <fountain-filename> %}`
47
+
48
+ # Styling
49
+ Page asset components styles are exported by this gem in `_sass/_page-asset.scss`, but this is not a theme gem, so you need to import them manually.
50
+
51
+ copy `_sass/_page-asset.scss` to your project. You can also update styles in that file to customize the look of the components.
52
+
53
+ In your project, update your `assets/css/main.scss` to import the page asset styles:
54
+
55
+ ``` scss
56
+ @use "page-asset" as *;
57
+ ```
58
+
59
+ # Configuration
60
+ The plugin can be configured in `_config.yml` with the following options:
61
+
62
+ ``` yaml
63
+ page_asset:
64
+ asset_root: assets/project_media
65
+ ```
66
+ where asset_root is the root folder for all project media assets.
67
+
68
+
69
+ # Custom components
70
+ Custom components are defined by adding:
71
+
72
+ A custom type renderer in `_lib/page_asset/`
73
+
74
+ OR
75
+
76
+ A custom html template in `_includes/page_asset/`
77
+
78
+
79
+ or both. There are examples each kind in the current supported types.
80
+
81
+ ## Custom renderer
82
+ A custom renderer is a class that inherits from `PageAsset::Renderer`, sets `register_type <type>` and implements a `render(context)` method.
83
+
84
+ A renderer can directly return html:
85
+ ``` ruby
86
+ module Jekyll
87
+ module PageAsset
88
+ class DummyRenderer < Renderer
89
+ register_type 'dummy'
90
+
91
+ def render(context)
92
+ <<~HTML
93
+ <div style="background-color: red; color: white; padding: 1rem;">
94
+ full path is #{@target_asset_path}
95
+ </div>
96
+ HTML
97
+ end
98
+ end
99
+ end
100
+ end
101
+ ```
102
+ and has the following variables available:
103
+
104
+ | @type | component type string |
105
+ | @asset_path_arg | requested path argument from the tag |
106
+ | @rest | all the rest of the args from the tag (useful for passing through) |
107
+ | @params | A hash of any tag args that were specified like key=value |
108
+ | @target_asset_paths | An array of the full resolved paths to assets (for single files this is an array with one item) |
109
+ | @target_asset_path | convenience - the first item from the above array |
110
+
111
+ Or it can load a custom html template from `_includes/page_asset/` and pass variables to it. The above variables are available in `include` but you can derive and pass any other variable.
112
+
113
+ ``` ruby
114
+ module Jekyll
115
+ module PageAsset
116
+ class DummyRenderer < Renderer
117
+ register_type 'dummy'
118
+
119
+ def render(context)
120
+ include_vars = build_include_vars(
121
+ "dummy_string" => @target_asset_path + " is the full path to the asset"
122
+ )
123
+ render_template(context, include_vars)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ ```
129
+ This is expecting to find a template file `_includes/page_asset/dummy.html`. e.g.:
130
+
131
+ ``` html
132
+ <div style="background-color: red; color: white; padding: 1rem;">
133
+ {{ include.dummy_string }}
134
+ </div>
135
+ ```
136
+
137
+ If there is no custom renderer, but there is a custom template, the template will be rendered with the above variables available in `include`.
138
+
139
+ Follow convention to put any styles for the custom component in `_sass/page_asset.scss`.
@@ -0,0 +1,13 @@
1
+ <script type="text/fountain" id="script-source">
2
+ {{ include.script_text }}
3
+ </script>
4
+
5
+ <div class="script-container screenplay" id="script-output"></div>
6
+ <script type="module">
7
+ import { Fountain } from 'https://cdn.jsdelivr.net/npm/fountain-js/+esm';
8
+ const sourceText = document.getElementById('script-source').textContent;
9
+ const fountain = new Fountain();
10
+ const result = fountain.parse(sourceText);
11
+ // Render the generated HTML into the output container
12
+ document.getElementById('script-output').innerHTML = result.html.script;
13
+ </script>
@@ -0,0 +1,306 @@
1
+ <script>
2
+ (function () {
3
+ var MOUNTED_ATTR = "data-gallery-mounted";
4
+
5
+ function mountGallery() {
6
+ var lightboxMount = document.getElementById("page-asset-gallery-lightbox");
7
+ var controlsMount = document.getElementById("page-asset-gallery-controls");
8
+
9
+ if (lightboxMount && lightboxMount.hasAttribute(MOUNTED_ATTR)) {
10
+ return;
11
+ }
12
+
13
+ if (lightboxMount) {
14
+ lightboxMount.innerHTML =
15
+ '<div class="gallery-lightbox" id="gallery-lightbox" role="dialog" aria-modal="true" aria-label="Image viewer" hidden>' +
16
+ '<div class="gallery-lightbox-inner" tabindex="-1">' +
17
+ '<img class="gallery-lightbox-image" id="gallery-lightbox-image" src="" alt="">' +
18
+ '</div>' +
19
+ '</div>';
20
+ lightboxMount.setAttribute(MOUNTED_ATTR, "true");
21
+ }
22
+
23
+ if (controlsMount) {
24
+ controlsMount.innerHTML =
25
+ '<div class="gallery-controls" id="gallery-controls" hidden aria-label="Gallery controls">' +
26
+ '<button class="btn gallery-control-button" type="button" id="gallery-prev" aria-label="Previous image">&lt;&lt;</button>' +
27
+ '<button class="btn gallery-control-button" type="button" id="gallery-close">Show Thumbnails</button>' +
28
+ '<button class="btn gallery-control-button" type="button" id="gallery-next" aria-label="Next image">&gt;&gt;</button>' +
29
+ '</div>';
30
+ controlsMount.setAttribute(MOUNTED_ATTR, "true");
31
+ }
32
+
33
+ initLightbox();
34
+ initControls();
35
+ }
36
+
37
+ function initLightbox() {
38
+ var lightbox = document.getElementById("gallery-lightbox");
39
+ var lightboxImage = document.getElementById("gallery-lightbox-image");
40
+ var lightboxMount = document.getElementById("page-asset-gallery-lightbox");
41
+ var focusableSelector = ".gallery-controls .gallery-control-button";
42
+
43
+ if (!lightbox || !lightboxImage) {
44
+ return;
45
+ }
46
+
47
+ var state = {
48
+ open: false,
49
+ galleryId: null,
50
+ index: 0,
51
+ items: [],
52
+ trigger: null
53
+ };
54
+
55
+ function applyVisibleBounds() {
56
+
57
+ var rect = lightboxMount.getBoundingClientRect();
58
+ var left = Math.max(0, rect.left);
59
+ var top = Math.max(0, rect.top);
60
+ var right = Math.min(window.innerWidth, rect.right);
61
+ var bottom = Math.min(window.innerHeight, rect.bottom);
62
+ var width = Math.max(0, right - left);
63
+ var height = Math.max(0, bottom - top);
64
+
65
+ lightbox.style.left = left + "px";
66
+ lightbox.style.top = top + "px";
67
+ lightbox.style.width = width + "px";
68
+ lightbox.style.height = height + "px";
69
+ }
70
+
71
+ function onViewportChange() {
72
+ if (!state.open) {
73
+ return;
74
+ }
75
+ applyVisibleBounds();
76
+ }
77
+
78
+ function getGalleryItems(galleryId) {
79
+ var container = document.querySelector("[data-gallery='" + galleryId + "']");
80
+ if (!container) {
81
+ return [];
82
+ }
83
+
84
+ return Array.prototype.slice
85
+ .call(container.querySelectorAll("[data-gallery-thumb='true']"))
86
+ .map(function (thumb) {
87
+ return {
88
+ src: thumb.getAttribute("data-gallery-src"),
89
+ alt: thumb.getAttribute("data-gallery-alt") || "Gallery image"
90
+ };
91
+ });
92
+ }
93
+
94
+ function emitState() {
95
+ document.dispatchEvent(new CustomEvent("gallerylightbox:statechange", {
96
+ detail: {
97
+ open: state.open,
98
+ galleryId: state.galleryId,
99
+ index: state.index,
100
+ total: state.items.length
101
+ }
102
+ }));
103
+ }
104
+
105
+ function renderImage() {
106
+ if (!state.open || state.items.length === 0) {
107
+ return;
108
+ }
109
+
110
+ var current = state.items[state.index];
111
+ lightboxImage.src = current.src;
112
+ lightboxImage.alt = current.alt;
113
+ emitState();
114
+ }
115
+
116
+ function focusFirstControl() {
117
+ var focusable = document.querySelectorAll(focusableSelector);
118
+ if (focusable.length > 0) {
119
+ focusable[0].focus();
120
+ return;
121
+ }
122
+
123
+ var inner = lightbox.querySelector(".gallery-lightbox-inner");
124
+ if (inner) {
125
+ inner.focus();
126
+ }
127
+ }
128
+
129
+ function open(galleryId, index, trigger) {
130
+ var items = getGalleryItems(galleryId);
131
+ if (items.length === 0) {
132
+ return;
133
+ }
134
+
135
+ state.open = true;
136
+ state.galleryId = galleryId;
137
+ state.items = items;
138
+ state.index = Math.max(0, Math.min(index, items.length - 1));
139
+ state.trigger = trigger || document.activeElement;
140
+
141
+ applyVisibleBounds();
142
+ lightbox.hidden = false;
143
+ document.body.classList.add("gallery-lightbox-open");
144
+ renderImage();
145
+ focusFirstControl();
146
+ }
147
+
148
+ function close() {
149
+ if (!state.open) {
150
+ return;
151
+ }
152
+
153
+ var trigger = state.trigger;
154
+ state.open = false;
155
+ state.galleryId = null;
156
+ state.items = [];
157
+ state.index = 0;
158
+ state.trigger = null;
159
+
160
+ lightbox.hidden = true;
161
+ lightbox.style.left = "";
162
+ lightbox.style.top = "";
163
+ lightbox.style.width = "";
164
+ lightbox.style.height = "";
165
+ lightboxImage.src = "";
166
+ lightboxImage.alt = "";
167
+ document.body.classList.remove("gallery-lightbox-open");
168
+ emitState();
169
+
170
+ if (trigger && typeof trigger.focus === "function") {
171
+ trigger.focus();
172
+ }
173
+ }
174
+
175
+ function goNext() {
176
+ if (!state.open || state.items.length === 0) {
177
+ return;
178
+ }
179
+
180
+ state.index = (state.index + 1) % state.items.length;
181
+ renderImage();
182
+ }
183
+
184
+ function goPrevious() {
185
+ if (!state.open || state.items.length === 0) {
186
+ return;
187
+ }
188
+
189
+ state.index = (state.index - 1 + state.items.length) % state.items.length;
190
+ renderImage();
191
+ }
192
+
193
+ document.addEventListener("click", function (event) {
194
+ var thumb = event.target.closest("[data-gallery-thumb='true']");
195
+ if (!thumb) {
196
+ return;
197
+ }
198
+
199
+ event.preventDefault();
200
+ open(
201
+ thumb.getAttribute("data-gallery-id"),
202
+ Number(thumb.getAttribute("data-gallery-index")) || 0,
203
+ thumb
204
+ );
205
+ });
206
+
207
+ lightbox.addEventListener("wheel", function (event) {
208
+ if (state.open) {
209
+ event.preventDefault();
210
+ }
211
+ }, { passive: false });
212
+
213
+ lightbox.addEventListener("touchmove", function (event) {
214
+ if (state.open) {
215
+ event.preventDefault();
216
+ }
217
+ }, { passive: false });
218
+
219
+ document.addEventListener("keydown", function (event) {
220
+ if (!state.open) {
221
+ return;
222
+ }
223
+
224
+ if (event.key === "Escape") {
225
+ event.preventDefault();
226
+ close();
227
+ return;
228
+ }
229
+
230
+ if (event.key === "ArrowRight") {
231
+ event.preventDefault();
232
+ goNext();
233
+ return;
234
+ }
235
+
236
+ if (event.key === "ArrowLeft") {
237
+ event.preventDefault();
238
+ goPrevious();
239
+ return;
240
+ }
241
+ });
242
+
243
+ window.addEventListener("resize", onViewportChange);
244
+ window.addEventListener("scroll", onViewportChange, { passive: true });
245
+
246
+ window.__galleryLightbox = {
247
+ open: open,
248
+ close: close,
249
+ next: goNext,
250
+ previous: goPrevious,
251
+ getState: function () {
252
+ return {
253
+ open: state.open,
254
+ galleryId: state.galleryId,
255
+ index: state.index,
256
+ total: state.items.length
257
+ };
258
+ }
259
+ };
260
+
261
+ emitState();
262
+ }
263
+
264
+ function initControls() {
265
+ var controls = document.getElementById("gallery-controls");
266
+ var prevButton = document.getElementById("gallery-prev");
267
+ var nextButton = document.getElementById("gallery-next");
268
+ var closeButton = document.getElementById("gallery-close");
269
+
270
+ if (!controls || !prevButton || !nextButton || !closeButton) {
271
+ return;
272
+ }
273
+
274
+ function callLightbox(methodName) {
275
+ if (!window.__galleryLightbox || typeof window.__galleryLightbox[methodName] !== "function") {
276
+ return;
277
+ }
278
+
279
+ window.__galleryLightbox[methodName]();
280
+ }
281
+
282
+ prevButton.addEventListener("click", function () {
283
+ callLightbox("previous");
284
+ });
285
+
286
+ nextButton.addEventListener("click", function () {
287
+ callLightbox("next");
288
+ });
289
+
290
+ closeButton.addEventListener("click", function () {
291
+ callLightbox("close");
292
+ });
293
+
294
+ document.addEventListener("gallerylightbox:statechange", function (event) {
295
+ var isOpen = Boolean(event.detail && event.detail.open);
296
+ controls.hidden = !isOpen;
297
+ });
298
+ }
299
+
300
+ if (document.readyState === "loading") {
301
+ document.addEventListener("DOMContentLoaded", mountGallery);
302
+ } else {
303
+ mountGallery();
304
+ }
305
+ })();
306
+ </script>
@@ -0,0 +1,242 @@
1
+ <div
2
+ class="scroll-scrub-sticky-container"
3
+ style="position: relative; width: 100%;"
4
+ data-pixels-per-second="{{ include.pixels_per_second }}"
5
+ data-sticky-top="{{ include.sticky_top }}"
6
+ >
7
+ <div class="scroll-scrub-sticky-inner" style="position: sticky; top: 0; width: 100%;">
8
+ <video class="scroll-scrub" style="display: block; width: 100%; height: auto;" muted playsinline preload="auto">
9
+ <source src="{{ include.video_path | relative_url }}" type="video/mp4">
10
+ Your browser does not support the video tag.
11
+ </video>
12
+ </div>
13
+ </div>
14
+
15
+ <script>
16
+ (function () {
17
+ if (window.__scrollScrubStickyInitialized) {
18
+ if (typeof window.__scrollScrubStickyRebuild === 'function') {
19
+ window.__scrollScrubStickyRebuild();
20
+ }
21
+ return;
22
+ }
23
+
24
+ window.__scrollScrubStickyInitialized = true;
25
+
26
+ var updateQueued = false;
27
+ var controllers = [];
28
+
29
+ function isSeekableVideo(video) {
30
+ return Number.isFinite(video.duration) && video.duration > 0;
31
+ }
32
+
33
+ function clamp(value, min, max) {
34
+ return Math.min(Math.max(value, min), max);
35
+ }
36
+
37
+ function requestVideoSeek(video, targetTime) {
38
+ if (!isSeekableVideo(video) || !Number.isFinite(targetTime)) return;
39
+
40
+ var clampedTarget = clamp(targetTime, 0, video.duration);
41
+ video._scrubTargetTime = clampedTarget;
42
+
43
+ if (video._scrubSeeking) return;
44
+
45
+ var currentTarget = video._scrubTargetTime;
46
+ if (typeof currentTarget !== 'number') return;
47
+ if (Math.abs(video.currentTime - currentTarget) < 0.001) return;
48
+
49
+ video._scrubSeeking = true;
50
+ if (typeof video.fastSeek === 'function') {
51
+ video.fastSeek(currentTarget);
52
+ } else {
53
+ video.currentTime = currentTarget;
54
+ }
55
+ }
56
+
57
+ function onVideoSeeked(video) {
58
+ video._scrubSeeking = false;
59
+
60
+ var target = video._scrubTargetTime;
61
+ if (typeof target !== 'number') return;
62
+
63
+ if (Math.abs(video.currentTime - target) >= 0.01) {
64
+ requestVideoSeek(video, target);
65
+ }
66
+ }
67
+
68
+ function getRenderedVideoHeight(video) {
69
+ var rect = video.getBoundingClientRect();
70
+ if (rect.height > 0) return rect.height;
71
+
72
+ if (video.videoWidth > 0 && video.videoHeight > 0) {
73
+ var parent = video.parentElement;
74
+ var width = rect.width
75
+ || video.clientWidth
76
+ || video.offsetWidth
77
+ || (parent ? (parent.clientWidth || parent.offsetWidth) : 0)
78
+ || 0;
79
+ if (width > 0) {
80
+ return width * (video.videoHeight / video.videoWidth);
81
+ }
82
+ }
83
+
84
+ return 0;
85
+ }
86
+
87
+ function getDocumentHeight() {
88
+ var body = document.body;
89
+ var docEl = document.documentElement;
90
+ if (!body || !docEl) return 0;
91
+
92
+ return Math.max(
93
+ body.scrollHeight,
94
+ body.offsetHeight,
95
+ docEl.clientHeight,
96
+ docEl.scrollHeight,
97
+ docEl.offsetHeight
98
+ );
99
+ }
100
+
101
+ function computeEffectiveStickyTop(controller, renderedHeight) {
102
+ if (!Number.isFinite(renderedHeight) || renderedHeight <= 0) {
103
+ return controller.stickyTop;
104
+ }
105
+
106
+ var containerRect = controller.container.getBoundingClientRect();
107
+ var scrollY = window.pageYOffset || window.scrollY || 0;
108
+ var containerBottomInDocument = containerRect.bottom + scrollY;
109
+ var documentHeight = getDocumentHeight();
110
+ var trailingContent = Math.max(0, documentHeight - containerBottomInDocument);
111
+ var requiredStickyTop = window.innerHeight - renderedHeight - trailingContent;
112
+
113
+ return Math.max(controller.stickyTop, requiredStickyTop);
114
+ }
115
+
116
+ function ensureControllers() {
117
+ var containers = Array.prototype.slice.call(document.querySelectorAll('.scroll-scrub-sticky-container'));
118
+
119
+ for (var i = 0; i < containers.length; i += 1) {
120
+ var container = containers[i];
121
+ var known = false;
122
+ for (var c = 0; c < controllers.length; c += 1) {
123
+ if (controllers[c].container === container) {
124
+ known = true;
125
+ break;
126
+ }
127
+ }
128
+ if (known) continue;
129
+
130
+ var inner = container.querySelector('.scroll-scrub-sticky-inner');
131
+ var video = container.querySelector('video.scroll-scrub');
132
+ if (!inner || !video) continue;
133
+
134
+ var pixelsPerSecond = parseFloat(container.getAttribute('data-pixels-per-second'));
135
+ if (!Number.isFinite(pixelsPerSecond) || pixelsPerSecond <= 0) {
136
+ pixelsPerSecond = 50;
137
+ }
138
+
139
+ var stickyTop = parseFloat(container.getAttribute('data-sticky-top'));
140
+ if (!Number.isFinite(stickyTop)) {
141
+ stickyTop = parseFloat(window.getComputedStyle(inner).top) || 120;
142
+ }
143
+ inner.style.top = stickyTop + 'px';
144
+
145
+ controllers.push({
146
+ container: container,
147
+ inner: inner,
148
+ video: video,
149
+ pixelsPerSecond: pixelsPerSecond,
150
+ extraHeight: 0,
151
+ stickyTop: stickyTop,
152
+ effectiveStickyTop: stickyTop,
153
+ });
154
+
155
+
156
+
157
+ video.pause();
158
+ video._scrubSeeking = false;
159
+ video._scrubTargetTime = 0;
160
+ video.addEventListener('loadedmetadata', rebuildControllers);
161
+ video.addEventListener('loadedmetadata', function () {
162
+
163
+ });
164
+ video.addEventListener('loadeddata', function () {
165
+
166
+ });
167
+ video.addEventListener('canplay', function () {
168
+
169
+ });
170
+ video.addEventListener('error', function () {
171
+ var mediaError = video.error;
172
+
173
+ });
174
+ video.addEventListener('seeked', (function (boundVideo) {
175
+ return function () {
176
+ onVideoSeeked(boundVideo);
177
+ };
178
+ })(video));
179
+
180
+ if (video.readyState === 0 && typeof video.load === 'function') {
181
+ video.load();
182
+ }
183
+ }
184
+ }
185
+
186
+ function rebuildControllers() {
187
+ ensureControllers();
188
+
189
+ for (var i = 0; i < controllers.length; i += 1) {
190
+ var controller = controllers[i];
191
+ var renderedHeight = getRenderedVideoHeight(controller.video);
192
+ var extraHeight = isSeekableVideo(controller.video)
193
+ ? Math.max(0, Math.round(controller.video.duration * controller.pixelsPerSecond))
194
+ : 0;
195
+
196
+ controller.extraHeight = extraHeight;
197
+ if (renderedHeight > 0) {
198
+ controller.effectiveStickyTop = computeEffectiveStickyTop(controller, renderedHeight);
199
+ controller.container.style.height = (renderedHeight + extraHeight) + 'px';
200
+ controller.inner.style.height = renderedHeight + 'px';
201
+
202
+ } else {
203
+ controller.effectiveStickyTop = controller.stickyTop;
204
+
205
+ }
206
+ }
207
+
208
+ queueApply();
209
+ }
210
+
211
+ function applyFromWindowScroll() {
212
+ for (var i = 0; i < controllers.length; i += 1) {
213
+ var controller = controllers[i];
214
+ if (controller.extraHeight <= 0 || !isSeekableVideo(controller.video)) continue;
215
+
216
+ var containerRect = controller.container.getBoundingClientRect();
217
+ var clampedProgress = clamp(controller.effectiveStickyTop - containerRect.top, 0, controller.extraHeight);
218
+
219
+ var targetTime = (clampedProgress / controller.extraHeight) * controller.video.duration;
220
+ requestVideoSeek(controller.video, targetTime);
221
+ }
222
+ }
223
+
224
+ function queueApply() {
225
+ if (updateQueued) return;
226
+ updateQueued = true;
227
+
228
+ window.requestAnimationFrame(function () {
229
+ updateQueued = false;
230
+ applyFromWindowScroll();
231
+ });
232
+ }
233
+
234
+ window.__scrollScrubStickyRebuild = rebuildControllers;
235
+
236
+ window.addEventListener('scroll', queueApply, { passive: true });
237
+ window.addEventListener('resize', rebuildControllers);
238
+ window.addEventListener('load', rebuildControllers);
239
+
240
+ rebuildControllers();
241
+ })();
242
+ </script>