source: Klonkt/src/assets/vendor/lenis-snap.mjs@ 3ccca13

main
Last change on this file since 3ccca13 was 88838bc, checked in by Robin <roboburr@…>, 3 weeks ago

Opruimen rond Lenis: een kopie, de juiste map, en de licenties genoteerd

Robins vraag of er iets met de MIT-licentie moest, en het antwoord bleek
groter dan de vraag.

LENIS ZAT ER AL. src/assets/js/lenis.min.js (1.1.13) plus een
smooth-scroll.js die er een instantie mee maakte. Ik heb daar gisteren een
tweede kopie naast gezet (1.3.26, ESM) zonder te kijken -- twee versies van
dezelfde bibliotheek in twee mappen. Had ik moeten opzoeken voor ik ging
vendoren.

Het gevaar dat ik vreesde was er niet: smooth-scroll.js wordt door geen
enkel sjabloon ingeladen, dus er draaiden geen twee instanties. Het was dode
code. Wel leerzaam: die oude opzet stond op duration 1.0 met easeOutExpo,
precies de trage instelling die Robin gisteren als "te zweverig" herkende.

Opgeruimd:

  • lenis.min.js (1.1.13) en smooth-scroll.js weg -- ongebruikt.
  • Mijn ESM-kopie verhuisd van src/assets/js/vendor/ naar src/assets/vendor/, waar cropper en simplewebauthn al stonden. Een tweede vendor-map naast de bestaande was mijn eigen verzinsel.
  • data-lenis-prevent in topnav.ejs blijft staan: dat werkt weer, want er draait nu daadwerkelijk een Lenis. Het hoorde bij de oude poging en is nu toevallig weer zinvol -- vandaar deze regel, zodat de volgende lezer niet denkt dat het een restant is.

LICENTIES. MIT eist dat de copyrightvermelding meereist met de code, en die
staat in src/assets/vendor/lenis-LICENSE.txt. Maar de repo heeft een
THIRD-PARTY-NOTICES.md waar de README naar verwijst, en daar ontbrak een
sectie voor gevendorde browser-bibliotheken -- niet alleen Lenis, ook
cropper, simplewebauthn en de losse htmx-kopie stonden er niet in. Die
sectie is er nu, met versie en licentie per bestand.

Alles nagekeken in de bestanden zelf en via npm view, niet uit het hoofd:
Lenis 1.3.26 MIT, Cropper.js 1.6.2 MIT, @simplewebauthn/browser 13.3.0 MIT,
htmx 0BSD. Klonkt is AGPL-3.0-or-later en MIT-code mag daarin.

MOD_V naar 20 (het importpad in read.js is verhuisd).

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 10.0 KB
Line 
1//#region packages/snap/src/debounce.ts
2function debounce(callback, delay) {
3 let timer;
4 return function(...args) {
5 clearTimeout(timer);
6 timer = setTimeout(() => {
7 timer = void 0;
8 callback.apply(this, args);
9 }, delay);
10 };
11}
12//#endregion
13//#region packages/snap/src/element.ts
14function removeParentSticky(element) {
15 if (getComputedStyle(element).position === "sticky") {
16 element.style.setProperty("position", "static");
17 element.dataset.sticky = "true";
18 }
19 if (element.offsetParent) removeParentSticky(element.offsetParent);
20}
21function addParentSticky(element) {
22 if (element?.dataset?.sticky === "true") {
23 element.style.removeProperty("position");
24 delete element.dataset.sticky;
25 }
26 if (element.offsetParent) addParentSticky(element.offsetParent);
27}
28function offsetTop(element, accumulator = 0) {
29 const top = accumulator + element.offsetTop;
30 if (element.offsetParent) return offsetTop(element.offsetParent, top);
31 return top;
32}
33function offsetLeft(element, accumulator = 0) {
34 const left = accumulator + element.offsetLeft;
35 if (element.offsetParent) return offsetLeft(element.offsetParent, left);
36 return left;
37}
38function scrollTop(element, accumulator = 0) {
39 const top = accumulator + element.scrollTop;
40 if (element.offsetParent) return scrollTop(element.offsetParent, top);
41 return top + window.scrollY;
42}
43function scrollLeft(element, accumulator = 0) {
44 const left = accumulator + element.scrollLeft;
45 if (element.offsetParent) return scrollLeft(element.offsetParent, left);
46 return left + window.scrollX;
47}
48var SnapElement = class {
49 element;
50 options;
51 align;
52 rect = {};
53 wrapperResizeObserver;
54 resizeObserver;
55 debouncedWrapperResize;
56 constructor(element, { align = ["start"], ignoreSticky = true, ignoreTransform = false } = {}) {
57 this.element = element;
58 this.options = {
59 align,
60 ignoreSticky,
61 ignoreTransform
62 };
63 this.align = [align].flat();
64 this.debouncedWrapperResize = debounce(this.onWrapperResize, 500);
65 this.wrapperResizeObserver = new ResizeObserver(this.debouncedWrapperResize);
66 this.wrapperResizeObserver.observe(document.body);
67 this.onWrapperResize();
68 this.resizeObserver = new ResizeObserver(this.onResize);
69 this.resizeObserver.observe(this.element);
70 this.setRect({
71 width: this.element.offsetWidth,
72 height: this.element.offsetHeight
73 });
74 }
75 destroy() {
76 this.wrapperResizeObserver.disconnect();
77 this.resizeObserver.disconnect();
78 }
79 setRect({ top, left, width, height, element } = {}) {
80 top = top ?? this.rect.top;
81 left = left ?? this.rect.left;
82 width = width ?? this.rect.width;
83 height = height ?? this.rect.height;
84 element = element ?? this.rect.element;
85 if (top === this.rect.top && left === this.rect.left && width === this.rect.width && height === this.rect.height && element === this.rect.element) return;
86 this.rect.top = top;
87 this.rect.y = top;
88 this.rect.width = width;
89 this.rect.height = height;
90 this.rect.left = left;
91 this.rect.x = left;
92 this.rect.bottom = top + height;
93 this.rect.right = left + width;
94 }
95 onWrapperResize = () => {
96 let top;
97 let left;
98 if (this.options.ignoreSticky) removeParentSticky(this.element);
99 if (this.options.ignoreTransform) {
100 top = offsetTop(this.element);
101 left = offsetLeft(this.element);
102 } else {
103 const rect = this.element.getBoundingClientRect();
104 top = rect.top + scrollTop(this.element);
105 left = rect.left + scrollLeft(this.element);
106 }
107 if (this.options.ignoreSticky) addParentSticky(this.element);
108 this.setRect({
109 top,
110 left
111 });
112 };
113 onResize = ([entry]) => {
114 if (!entry?.borderBoxSize[0]) return;
115 const width = entry.borderBoxSize[0].inlineSize;
116 const height = entry.borderBoxSize[0].blockSize;
117 this.setRect({
118 width,
119 height
120 });
121 };
122};
123//#endregion
124//#region packages/snap/src/uid.ts
125let index = 0;
126function uid() {
127 return index++;
128}
129//#endregion
130//#region packages/snap/src/snap.ts
131/**
132* Snap class to handle the snap functionality
133*
134* @example
135* const snap = new Snap(lenis, {
136* type: 'mandatory', // 'mandatory', 'proximity' or 'lock'
137* onSnapStart: (snap) => {
138* console.log('onSnapStart', snap)
139* },
140* onSnapComplete: (snap) => {
141* console.log('onSnapComplete', snap)
142* },
143* })
144*
145* snap.add(500) // snap at 500px
146*
147* const removeSnap = snap.add(500)
148*
149* if (someCondition) {
150* removeSnap()
151* }
152*/
153var Snap = class {
154 options;
155 elements = /* @__PURE__ */ new Map();
156 snaps = /* @__PURE__ */ new Map();
157 viewport = {
158 width: window.innerWidth,
159 height: window.innerHeight
160 };
161 isStopped = false;
162 onSnapDebounced;
163 currentSnapIndex;
164 constructor(lenis, { type = "proximity", lerp, easing, duration, distanceThreshold = "50%", debounce: debounceDelay = 500, onSnapStart, onSnapComplete } = {}) {
165 this.lenis = lenis;
166 if (!window.lenis) window.lenis = {};
167 window.lenis.snap = true;
168 this.options = {
169 type,
170 lerp,
171 easing,
172 duration,
173 distanceThreshold,
174 debounce: debounceDelay,
175 onSnapStart,
176 onSnapComplete
177 };
178 this.onWindowResize();
179 window.addEventListener("resize", this.onWindowResize);
180 this.onSnapDebounced = debounce(this.onSnap, this.options.debounce);
181 this.lenis.on("virtual-scroll", this.onSnapDebounced);
182 }
183 /**
184 * Destroy the snap instance
185 */
186 destroy() {
187 this.lenis.off("virtual-scroll", this.onSnapDebounced);
188 window.removeEventListener("resize", this.onWindowResize);
189 this.elements.forEach((element) => {
190 element.destroy();
191 });
192 }
193 /**
194 * Start the snap after it has been stopped
195 */
196 start() {
197 this.isStopped = false;
198 }
199 /**
200 * Stop the snap
201 */
202 stop() {
203 this.isStopped = true;
204 }
205 /**
206 * Add a snap to the snap instance
207 *
208 * @param value The value to snap to
209 * @param userData User data that will be forwarded through the snap event
210 * @returns Unsubscribe function
211 */
212 add(value) {
213 const id = uid();
214 this.snaps.set(id, { value });
215 return () => this.snaps.delete(id);
216 }
217 /**
218 * Add an element to the snap instance
219 *
220 * @param element The element to add
221 * @param options The options for the element
222 * @returns Unsubscribe function
223 */
224 addElement(element, options = {}) {
225 const id = uid();
226 this.elements.set(id, new SnapElement(element, options));
227 return () => this.elements.delete(id);
228 }
229 addElements(elements, options = {}) {
230 const map = [...elements].map((element) => this.addElement(element, options));
231 return () => {
232 map.forEach((remove) => {
233 remove();
234 });
235 };
236 }
237 onWindowResize = () => {
238 this.viewport.width = window.innerWidth;
239 this.viewport.height = window.innerHeight;
240 };
241 computeSnaps = () => {
242 const { isHorizontal } = this.lenis;
243 let snaps = [...this.snaps.values()];
244 this.elements.forEach(({ rect, align }) => {
245 let value;
246 align.forEach((align) => {
247 if (align === "start") value = rect.top;
248 else if (align === "center") value = isHorizontal ? rect.left + rect.width / 2 - this.viewport.width / 2 : rect.top + rect.height / 2 - this.viewport.height / 2;
249 else if (align === "end") value = isHorizontal ? rect.left + rect.width - this.viewport.width : rect.top + rect.height - this.viewport.height;
250 if (typeof value === "number") snaps.push({ value: Math.ceil(value) });
251 });
252 });
253 snaps = snaps.sort((a, b) => Math.abs(a.value) - Math.abs(b.value));
254 return snaps;
255 };
256 previous() {
257 this.goTo((this.currentSnapIndex ?? 0) - 1);
258 }
259 next() {
260 this.goTo((this.currentSnapIndex ?? 0) + 1);
261 }
262 goTo(index) {
263 const snaps = this.computeSnaps();
264 if (snaps.length === 0) return;
265 this.currentSnapIndex = Math.max(0, Math.min(index, snaps.length - 1));
266 const currentSnap = snaps[this.currentSnapIndex];
267 if (currentSnap === void 0) return;
268 this.lenis.scrollTo(currentSnap.value, {
269 duration: this.options.duration,
270 easing: this.options.easing,
271 lerp: this.options.lerp,
272 lock: this.options.type === "lock",
273 userData: { initiator: "snap" },
274 onStart: () => {
275 this.options.onSnapStart?.({
276 index: this.currentSnapIndex,
277 ...currentSnap
278 });
279 },
280 onComplete: () => {
281 this.options.onSnapComplete?.({
282 index: this.currentSnapIndex,
283 ...currentSnap
284 });
285 }
286 });
287 }
288 get distanceThreshold() {
289 let distanceThreshold = Number.POSITIVE_INFINITY;
290 if (this.options.type === "mandatory") return Number.POSITIVE_INFINITY;
291 const { isHorizontal } = this.lenis;
292 const axis = isHorizontal ? "width" : "height";
293 if (typeof this.options.distanceThreshold === "string" && this.options.distanceThreshold.endsWith("%")) distanceThreshold = Number(this.options.distanceThreshold.replace("%", "")) / 100 * this.viewport[axis];
294 else if (typeof this.options.distanceThreshold === "number") distanceThreshold = this.options.distanceThreshold;
295 else distanceThreshold = this.viewport[axis];
296 return distanceThreshold;
297 }
298 onSnap = (e) => {
299 if (this.isStopped) return;
300 if (e.event.type === "touchmove") return;
301 if (this.options.type === "lock" && this.lenis.userData?.initiator === "snap") return;
302 let { scroll, isHorizontal } = this.lenis;
303 const delta = isHorizontal ? e.deltaX : e.deltaY;
304 scroll = Math.ceil(this.lenis.scroll + delta);
305 const snaps = this.computeSnaps();
306 if (snaps.length === 0) return;
307 let snapIndex;
308 const prevSnapIndex = snaps.findLastIndex(({ value }) => value < scroll);
309 const nextSnapIndex = snaps.findIndex(({ value }) => value > scroll);
310 if (this.options.type === "lock") {
311 if (delta > 0) snapIndex = nextSnapIndex;
312 else if (delta < 0) snapIndex = prevSnapIndex;
313 } else {
314 const prevSnap = snaps[prevSnapIndex];
315 const distanceToPrevSnap = prevSnap ? Math.abs(scroll - prevSnap.value) : Number.POSITIVE_INFINITY;
316 const nextSnap = snaps[nextSnapIndex];
317 snapIndex = distanceToPrevSnap < (nextSnap ? Math.abs(scroll - nextSnap.value) : Number.POSITIVE_INFINITY) ? prevSnapIndex : nextSnapIndex;
318 }
319 if (snapIndex === void 0) return;
320 if (snapIndex === -1) return;
321 snapIndex = Math.max(0, Math.min(snapIndex, snaps.length - 1));
322 const snap = snaps[snapIndex];
323 if (Math.abs(scroll - snap.value) <= this.distanceThreshold) this.goTo(snapIndex);
324 };
325 resize() {
326 this.elements.forEach((element) => {
327 element.onWrapperResize();
328 });
329 }
330};
331//#endregion
332export { Snap as default };
333
334//# sourceMappingURL=lenis-snap.mjs.map
Note: See TracBrowser for help on using the repository browser.