@myxo-victor/chexjs 6.0.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.
- package/ChexJs/.htaccess +9 -0
- package/ChexJs/Chex.js +712 -0
- package/ChexJs/Chex.php +354 -0
- package/ChexJs/Chex_notify.php +68 -0
- package/ChexJs/LICENSE +22 -0
- package/ChexJs/README.md +121 -0
- package/ChexJs/api/read.txt +4 -0
- package/ChexJs/app/api/api.txt +4 -0
- package/ChexJs/app/components/main.js +4 -0
- package/ChexJs/app/index.css +12 -0
- package/ChexJs/app/index.html +28 -0
- package/ChexJs/app/logic/app.js +4 -0
- package/ChexJs/components/demo.js +45 -0
- package/ChexJs/components/main.js +609 -0
- package/ChexJs/images/logo.png +0 -0
- package/ChexJs/index.css +118 -0
- package/ChexJs/index.html +44 -0
- package/ChexJs/libs/modal.js +617 -0
- package/ChexJs/libs/orbit.js +217 -0
- package/ChexJs/libs/racket.js +374 -0
- package/ChexJs/libs/rinx.js +88 -0
- package/ChexJs/libs/scrollEcho.js +212 -0
- package/ChexJs/libs/skeleton.js +341 -0
- package/ChexJs/libs/smooth.js +647 -0
- package/ChexJs/logic/app.js +36 -0
- package/ChexJs/notification_handler.txt +1 -0
- package/ChexJs/sw.js +67 -0
- package/package.json +12 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smooth.js - Seamless, Fluid Continuous Infinite Ticker Slider Engine
|
|
3
|
+
* * Unlike traditional carousels that transition and pause, Smooth.js uses
|
|
4
|
+
* high-performance requestAnimationFrame delta-time rendering to run a
|
|
5
|
+
* completely seamless, non-stutter, continuous carousel loop.
|
|
6
|
+
* * Supports cloning rich DOM nodes, fallback image URLs, responsive column count,
|
|
7
|
+
* customizable speeds, and on-the-fly direction switching.
|
|
8
|
+
* * Responsive Breakpoints:
|
|
9
|
+
* - Desktop (>= 768px): Displays 3 slides at once
|
|
10
|
+
* - Mobile (< 768px): Displays 2 slides at once
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class Smooth {
|
|
14
|
+
static defaultStylesInjected = false;
|
|
15
|
+
static activeAutoCarousels = new Map();
|
|
16
|
+
static autoInitTimer = null;
|
|
17
|
+
static autoLifecycleBound = false;
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
this.rawInputList = []; // Holds element selectors/IDs or image URLs
|
|
21
|
+
this.slides = []; // Resolved slide nodes and content elements
|
|
22
|
+
this.pixelsPerSecond = 60; // Standard ticker speed (px/sec)
|
|
23
|
+
this.directionVector = 'left'; // Horizontal scroll direction ('left' | 'right')
|
|
24
|
+
this.container = null; // Active mount viewport element
|
|
25
|
+
this.viewport = null; // Internal masked viewport element
|
|
26
|
+
this.track = null; // Ticker movement rail element
|
|
27
|
+
|
|
28
|
+
this.isPlaying = false; // Playback running state
|
|
29
|
+
this.isHovered = false; // Kept for backwards-compatible telemetry
|
|
30
|
+
this.pauseOnHover = false; // Hover suspension is disabled
|
|
31
|
+
|
|
32
|
+
this.scrollOffset = 0; // Raw scroll distance accumulator (pixels)
|
|
33
|
+
this.lastTimestamp = null; // Frame delta tracker
|
|
34
|
+
this.animationFrameId = null; // Animation loop reference handle
|
|
35
|
+
this.changeCallback = null; // Active telemetry update listeners
|
|
36
|
+
|
|
37
|
+
this.lastItemsPerPage = this.getItemsPerPage(); // Cache width state
|
|
38
|
+
this.resizeBound = false; // Guards against duplicate window listeners
|
|
39
|
+
this.itemsWidthSum = 0; // Total physical width of unique slide pack (one full loop)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds baseline carousel styling once per page so images look polished even
|
|
44
|
+
* when the user does not provide custom CSS.
|
|
45
|
+
*/
|
|
46
|
+
injectDefaultStyles() {
|
|
47
|
+
if (Smooth.defaultStylesInjected || typeof document === 'undefined') return;
|
|
48
|
+
|
|
49
|
+
const style = document.createElement('style');
|
|
50
|
+
style.id = 'smooth-default-styles';
|
|
51
|
+
style.textContent = `
|
|
52
|
+
.smooth-viewport {
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
width: 100%;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.smooth-track {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
width: max-content;
|
|
61
|
+
will-change: transform;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.smooth-slide-item {
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
flex-shrink: 0;
|
|
70
|
+
padding: 8px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.smooth-media-frame {
|
|
74
|
+
width: 100%;
|
|
75
|
+
aspect-ratio: var(--smooth-image-ratio, 4 / 3);
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
border-radius: var(--smooth-image-radius, 12px);
|
|
78
|
+
background: var(--smooth-image-background, #f1f5f9);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.smooth-media-image {
|
|
82
|
+
display: block;
|
|
83
|
+
width: 100%;
|
|
84
|
+
height: 100%;
|
|
85
|
+
object-fit: var(--smooth-image-fit, cover);
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
document.head.appendChild(style);
|
|
90
|
+
Smooth.defaultStylesInjected = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Gives cloned image nodes the same default frame used by URL images.
|
|
95
|
+
* @param {HTMLImageElement} image
|
|
96
|
+
* @returns {HTMLDivElement}
|
|
97
|
+
*/
|
|
98
|
+
createImageFrame(image) {
|
|
99
|
+
const imgWrapper = document.createElement('div');
|
|
100
|
+
imgWrapper.className = 'smooth-media-frame';
|
|
101
|
+
|
|
102
|
+
image.classList.add('smooth-media-image');
|
|
103
|
+
image.style.display = '';
|
|
104
|
+
image.removeAttribute('id'); // Avoid ID duplication conflicts
|
|
105
|
+
|
|
106
|
+
imgWrapper.appendChild(image);
|
|
107
|
+
return imgWrapper;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Starts one or many carousels from simple config objects.
|
|
112
|
+
* @param {Object|Object[]} configs
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
static start(configs) {
|
|
116
|
+
const list = Array.isArray(configs) ? configs : [configs];
|
|
117
|
+
Smooth.autoConfigs = list.filter(Boolean);
|
|
118
|
+
Smooth.setupAutoLifecycle();
|
|
119
|
+
Smooth.scheduleAutoInit();
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Instance-friendly alias for Smooth.start().
|
|
125
|
+
* @param {Object|Object[]} configs
|
|
126
|
+
* @returns {Smooth}
|
|
127
|
+
*/
|
|
128
|
+
start(configs) {
|
|
129
|
+
Smooth.start(configs);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Binds page lifecycle events once so declarative configs survive delayed
|
|
135
|
+
* rendering and route changes.
|
|
136
|
+
*/
|
|
137
|
+
static setupAutoLifecycle() {
|
|
138
|
+
if (Smooth.autoLifecycleBound || typeof window === 'undefined') return;
|
|
139
|
+
Smooth.autoLifecycleBound = true;
|
|
140
|
+
|
|
141
|
+
if (document.readyState === 'loading') {
|
|
142
|
+
document.addEventListener('DOMContentLoaded', Smooth.scheduleAutoInit);
|
|
143
|
+
} else {
|
|
144
|
+
Smooth.scheduleAutoInit();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
window.addEventListener('load', Smooth.scheduleAutoInit);
|
|
148
|
+
window.addEventListener('hashchange', Smooth.scheduleAutoInit);
|
|
149
|
+
window.addEventListener('popstate', Smooth.scheduleAutoInit);
|
|
150
|
+
|
|
151
|
+
const observer = new MutationObserver(Smooth.scheduleAutoInit);
|
|
152
|
+
observer.observe(document.documentElement, {
|
|
153
|
+
childList: true,
|
|
154
|
+
subtree: true
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Debounces automatic setup while frameworks are rendering.
|
|
160
|
+
*/
|
|
161
|
+
static scheduleAutoInit() {
|
|
162
|
+
clearTimeout(Smooth.autoInitTimer);
|
|
163
|
+
Smooth.autoInitTimer = setTimeout(Smooth.initConfiguredCarousels, 50);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Initializes every carousel declared through Smooth.start().
|
|
168
|
+
* @returns {boolean}
|
|
169
|
+
*/
|
|
170
|
+
static initConfiguredCarousels() {
|
|
171
|
+
const configs = Smooth.autoConfigs || [];
|
|
172
|
+
return configs.map(Smooth.initConfiguredCarousel).every(Boolean);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Stops a managed carousel when its container leaves the DOM.
|
|
177
|
+
* @param {string} selector
|
|
178
|
+
*/
|
|
179
|
+
static stopConfiguredCarousel(selector) {
|
|
180
|
+
const active = Smooth.activeAutoCarousels.get(selector);
|
|
181
|
+
if (!active) return;
|
|
182
|
+
|
|
183
|
+
active.carousel.stop();
|
|
184
|
+
Smooth.activeAutoCarousels.delete(selector);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolves item IDs/selectors from a declarative carousel config.
|
|
189
|
+
* @param {HTMLElement} container
|
|
190
|
+
* @param {Object} config
|
|
191
|
+
* @returns {string[]}
|
|
192
|
+
*/
|
|
193
|
+
static getConfiguredItems(container, config) {
|
|
194
|
+
if (Array.isArray(config.items)) return config.items;
|
|
195
|
+
if (Array.isArray(config.images)) return config.images;
|
|
196
|
+
|
|
197
|
+
const itemSelector = config.itemSelector || 'img';
|
|
198
|
+
const idPrefix = config.idPrefix || `smooth-item-${Math.random().toString(36).slice(2)}`;
|
|
199
|
+
const items = Array.from(container.querySelectorAll(itemSelector));
|
|
200
|
+
|
|
201
|
+
return items.map((item, index) => {
|
|
202
|
+
if (config.idPrefix || !item.id) item.id = `${idPrefix}-${index + 1}`;
|
|
203
|
+
item.style.display = 'none';
|
|
204
|
+
return item.id;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Initializes a single declarative carousel config.
|
|
210
|
+
* @param {Object} config
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
static initConfiguredCarousel(config) {
|
|
214
|
+
if (!config || !config.selector) return false;
|
|
215
|
+
|
|
216
|
+
const container = document.querySelector(config.selector);
|
|
217
|
+
const active = Smooth.activeAutoCarousels.get(config.selector);
|
|
218
|
+
|
|
219
|
+
if (!container) {
|
|
220
|
+
Smooth.stopConfiguredCarousel(config.selector);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (active?.container === container) return true;
|
|
225
|
+
|
|
226
|
+
Smooth.stopConfiguredCarousel(config.selector);
|
|
227
|
+
|
|
228
|
+
const items = Smooth.getConfiguredItems(container, config);
|
|
229
|
+
if (items.length === 0) return false;
|
|
230
|
+
|
|
231
|
+
const carousel = new Smooth();
|
|
232
|
+
carousel
|
|
233
|
+
.mount(container)
|
|
234
|
+
.items(items)
|
|
235
|
+
.speed(config.speed || 60)
|
|
236
|
+
.direction(config.direction || 'left');
|
|
237
|
+
|
|
238
|
+
if (config.autoPlay !== false) carousel.play();
|
|
239
|
+
|
|
240
|
+
Smooth.activeAutoCarousels.set(config.selector, { carousel, container });
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolves IDs and CSS selectors without throwing on plain image URLs.
|
|
246
|
+
* @param {string} value
|
|
247
|
+
* @returns {HTMLElement|null}
|
|
248
|
+
*/
|
|
249
|
+
resolveInputElement(value) {
|
|
250
|
+
const trimmed = value.trim();
|
|
251
|
+
const idMatch = document.getElementById(trimmed);
|
|
252
|
+
if (idMatch) return idMatch;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
return document.querySelector(trimmed);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Detects the optimal layout mode based on viewport width.
|
|
263
|
+
* Responsive: Desktop (>= 768px) shows 3, Mobile (< 768px) shows 2.
|
|
264
|
+
* @returns {number}
|
|
265
|
+
*/
|
|
266
|
+
getItemsPerPage() {
|
|
267
|
+
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
|
268
|
+
return 2;
|
|
269
|
+
}
|
|
270
|
+
return 3;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Anchors the carousel wrapper to the targeted element.
|
|
275
|
+
* @param {string|HTMLElement} selector
|
|
276
|
+
* @returns {Smooth}
|
|
277
|
+
*/
|
|
278
|
+
mount(selector) {
|
|
279
|
+
if (Array.isArray(selector) || (selector && typeof selector === 'object' && selector.selector)) {
|
|
280
|
+
return this.start(selector);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.container = typeof selector === 'string'
|
|
284
|
+
? document.querySelector(selector)
|
|
285
|
+
: selector;
|
|
286
|
+
|
|
287
|
+
if (!this.container) {
|
|
288
|
+
console.error(`Smooth.js: Mounting container "${selector}" was not resolved.`);
|
|
289
|
+
} else {
|
|
290
|
+
this.setupResizeEvent();
|
|
291
|
+
}
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Hover pausing is disabled; kept as a no-op for backwards compatibility.
|
|
297
|
+
*/
|
|
298
|
+
setupHoverEvents() {
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Sets up highly responsive window resize listeners to reconstruct
|
|
304
|
+
* track sizes and boundaries.
|
|
305
|
+
*/
|
|
306
|
+
setupResizeEvent() {
|
|
307
|
+
if (this.resizeBound || typeof window === 'undefined') return;
|
|
308
|
+
this.resizeBound = true;
|
|
309
|
+
|
|
310
|
+
let resizeTimeout;
|
|
311
|
+
window.addEventListener('resize', () => {
|
|
312
|
+
clearTimeout(resizeTimeout);
|
|
313
|
+
resizeTimeout = setTimeout(() => {
|
|
314
|
+
const currentBreakpointVal = this.getItemsPerPage();
|
|
315
|
+
if (currentBreakpointVal !== this.lastItemsPerPage) {
|
|
316
|
+
this.lastItemsPerPage = currentBreakpointVal;
|
|
317
|
+
this.render(); // Breakpoint changed: requires full reconstruction
|
|
318
|
+
} else {
|
|
319
|
+
this.recalculateDimensions(); // Just adapt sizing on same layout mode
|
|
320
|
+
}
|
|
321
|
+
}, 150);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Accepts array list representing raw image IDs, selectors, or direct URLs.
|
|
327
|
+
* @param {Array} list
|
|
328
|
+
* @returns {Smooth}
|
|
329
|
+
*/
|
|
330
|
+
items(list) {
|
|
331
|
+
this.rawInputList = Array.isArray(list) ? list : [list];
|
|
332
|
+
this.render();
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Alias interface for items() to ensure backwards compatibility.
|
|
338
|
+
* @param {Array} list
|
|
339
|
+
* @returns {Smooth}
|
|
340
|
+
*/
|
|
341
|
+
images(list) {
|
|
342
|
+
return this.items(list);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Sets continuous motion speed (pixels scrolled per second).
|
|
347
|
+
* @param {number} rate
|
|
348
|
+
* @returns {Smooth}
|
|
349
|
+
*/
|
|
350
|
+
speed(rate) {
|
|
351
|
+
this.pixelsPerSecond = Math.max(1, Number(rate) || 60);
|
|
352
|
+
this.updateCallback();
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Adjusts the moving direction of the carousel loop.
|
|
358
|
+
* @param {string} dir ('left' | 'right')
|
|
359
|
+
* @returns {Smooth}
|
|
360
|
+
*/
|
|
361
|
+
direction(dir) {
|
|
362
|
+
if (dir === 'left' || dir === 'right') {
|
|
363
|
+
this.directionVector = dir;
|
|
364
|
+
this.updateCallback();
|
|
365
|
+
}
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Hover suspension is disabled; kept chainable for existing calls.
|
|
371
|
+
* @param {boolean} value
|
|
372
|
+
* @returns {Smooth}
|
|
373
|
+
*/
|
|
374
|
+
setPauseOnHover(value) {
|
|
375
|
+
this.pauseOnHover = false;
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Resolves selector nodes, clones DOM elements, constructs circular buffer blocks,
|
|
381
|
+
* and mounts the elements into the track.
|
|
382
|
+
*/
|
|
383
|
+
render() {
|
|
384
|
+
this.slides = [];
|
|
385
|
+
this.injectDefaultStyles();
|
|
386
|
+
|
|
387
|
+
this.rawInputList.forEach(item => {
|
|
388
|
+
if (typeof item === 'string') {
|
|
389
|
+
const trimmed = item.trim();
|
|
390
|
+
const element = this.resolveInputElement(trimmed);
|
|
391
|
+
if (element) {
|
|
392
|
+
element.style.display = 'none'; // Hide static layout source elements
|
|
393
|
+
this.slides.push({ type: 'dom', node: element });
|
|
394
|
+
} else {
|
|
395
|
+
this.slides.push({ type: 'url', url: item });
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
this.slides.push({ type: 'url', url: String(item) });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (this.slides.length === 0) return;
|
|
403
|
+
|
|
404
|
+
// Automatic container parent matching fallback
|
|
405
|
+
if (!this.container) {
|
|
406
|
+
const firstDom = this.slides.find(s => s.type === 'dom');
|
|
407
|
+
if (firstDom && firstDom.node.parentElement) {
|
|
408
|
+
this.container = firstDom.node.parentElement;
|
|
409
|
+
this.setupResizeEvent();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!this.container) return;
|
|
414
|
+
|
|
415
|
+
// Set styling baseline to mask content
|
|
416
|
+
this.container.style.position = 'relative';
|
|
417
|
+
this.container.style.overflow = 'hidden';
|
|
418
|
+
|
|
419
|
+
// Establish viewport layer
|
|
420
|
+
this.viewport = this.container.querySelector('.smooth-viewport');
|
|
421
|
+
if (!this.viewport) {
|
|
422
|
+
this.viewport = document.createElement('div');
|
|
423
|
+
this.viewport.className = 'smooth-viewport';
|
|
424
|
+
this.viewport.style.overflow = 'hidden';
|
|
425
|
+
this.viewport.style.width = '100%';
|
|
426
|
+
this.container.appendChild(this.viewport);
|
|
427
|
+
} else {
|
|
428
|
+
this.viewport.innerHTML = ''; // Fresh DOM compilation
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Horizontal sliding track rail
|
|
432
|
+
this.track = document.createElement('div');
|
|
433
|
+
this.track.className = 'smooth-track';
|
|
434
|
+
this.track.style.display = 'flex';
|
|
435
|
+
this.track.style.width = 'max-content';
|
|
436
|
+
this.track.style.willChange = 'transform';
|
|
437
|
+
this.viewport.appendChild(this.track);
|
|
438
|
+
|
|
439
|
+
const itemsPerPage = this.getItemsPerPage();
|
|
440
|
+
|
|
441
|
+
// Triple-duplication technique:
|
|
442
|
+
// We render: [Buffer Left Block] [Main Content Block] [Buffer Right Block]
|
|
443
|
+
// Allowing the window to continuously warp offsets seamlessly without blank gaps.
|
|
444
|
+
const originalSet = [...this.slides];
|
|
445
|
+
const tripleSets = [...originalSet, ...originalSet, ...originalSet];
|
|
446
|
+
|
|
447
|
+
tripleSets.forEach((slide) => {
|
|
448
|
+
const itemWrapper = document.createElement('div');
|
|
449
|
+
itemWrapper.className = 'smooth-slide-item';
|
|
450
|
+
itemWrapper.style.width = `${this.viewport.clientWidth / itemsPerPage}px`;
|
|
451
|
+
itemWrapper.style.flexShrink = '0';
|
|
452
|
+
itemWrapper.style.boxSizing = 'border-box';
|
|
453
|
+
itemWrapper.style.padding = '8px'; // Fluid spacing gutters
|
|
454
|
+
|
|
455
|
+
if (slide.type === 'dom') {
|
|
456
|
+
const clone = slide.node.cloneNode(true);
|
|
457
|
+
|
|
458
|
+
if (clone.tagName === 'IMG') {
|
|
459
|
+
itemWrapper.appendChild(this.createImageFrame(clone));
|
|
460
|
+
} else {
|
|
461
|
+
clone.style.display = ''; // Clear display hidden from source
|
|
462
|
+
clone.style.width = '100%';
|
|
463
|
+
clone.removeAttribute('id'); // Avoid ID duplication conflicts
|
|
464
|
+
itemWrapper.appendChild(clone);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// Fallback placeholder image element
|
|
468
|
+
const imgWrapper = document.createElement('div');
|
|
469
|
+
imgWrapper.className = 'smooth-media-frame';
|
|
470
|
+
|
|
471
|
+
const img = document.createElement('img');
|
|
472
|
+
img.src = slide.url;
|
|
473
|
+
img.className = 'smooth-media-image';
|
|
474
|
+
imgWrapper.appendChild(img);
|
|
475
|
+
itemWrapper.appendChild(imgWrapper);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.track.appendChild(itemWrapper);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
this.recalculateDimensions();
|
|
482
|
+
|
|
483
|
+
// Align starting scroll position cleanly onto the middle (main) copy
|
|
484
|
+
this.scrollOffset = this.itemsWidthSum;
|
|
485
|
+
this.updatePositionDOM();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Recalculates individual item widths and updates the boundary parameters.
|
|
490
|
+
*/
|
|
491
|
+
recalculateDimensions() {
|
|
492
|
+
if (!this.viewport || !this.track) return;
|
|
493
|
+
const itemsPerPage = this.getItemsPerPage();
|
|
494
|
+
const slideWidth = this.viewport.clientWidth / itemsPerPage;
|
|
495
|
+
|
|
496
|
+
const slideNodes = this.track.querySelectorAll('.smooth-slide-item');
|
|
497
|
+
slideNodes.forEach(node => {
|
|
498
|
+
node.style.width = `${slideWidth}px`;
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Compute absolute scroll length of 1 unique set iteration
|
|
502
|
+
this.itemsWidthSum = slideWidth * this.slides.length;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Triggers hardware-accelerated transform manipulations directly on the track.
|
|
507
|
+
*/
|
|
508
|
+
updatePositionDOM() {
|
|
509
|
+
if (!this.track) return;
|
|
510
|
+
this.track.style.transform = `translate3d(-${this.scrollOffset}px, 0, 0)`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Animation frame loop calculation using delta time variables to maintain
|
|
515
|
+
* uniform rate values on variable device configurations (60Hz, 120Hz, 144Hz monitors).
|
|
516
|
+
* @param {DOMHighResTimeStamp} timestamp
|
|
517
|
+
*/
|
|
518
|
+
tick(timestamp) {
|
|
519
|
+
if (!this.isPlaying) return;
|
|
520
|
+
|
|
521
|
+
if (!this.lastTimestamp) {
|
|
522
|
+
this.lastTimestamp = timestamp;
|
|
523
|
+
this.animationFrameId = requestAnimationFrame((t) => this.tick(t));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const deltaTime = (timestamp - this.lastTimestamp) / 1000;
|
|
528
|
+
this.lastTimestamp = timestamp;
|
|
529
|
+
|
|
530
|
+
// Safeguard frame skip jumps if user changes background tabs
|
|
531
|
+
if (deltaTime < 0.1) {
|
|
532
|
+
const deltaScroll = this.pixelsPerSecond * deltaTime;
|
|
533
|
+
|
|
534
|
+
if (this.directionVector === 'left') {
|
|
535
|
+
this.scrollOffset += deltaScroll;
|
|
536
|
+
} else {
|
|
537
|
+
this.scrollOffset -= deltaScroll;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Bound checking wrapping resets
|
|
541
|
+
if (this.scrollOffset >= this.itemsWidthSum * 2) {
|
|
542
|
+
this.scrollOffset -= this.itemsWidthSum; // Wrap backwards
|
|
543
|
+
} else if (this.scrollOffset <= this.itemsWidthSum) {
|
|
544
|
+
this.scrollOffset += this.itemsWidthSum; // Wrap forwards
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this.updatePositionDOM();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
this.updateCallback();
|
|
551
|
+
this.animationFrameId = requestAnimationFrame((t) => this.tick(t));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Starts the continuous loop motion.
|
|
556
|
+
* @returns {Smooth}
|
|
557
|
+
*/
|
|
558
|
+
play() {
|
|
559
|
+
if (this.isPlaying) return this;
|
|
560
|
+
this.isPlaying = true;
|
|
561
|
+
this.lastTimestamp = null;
|
|
562
|
+
this.animationFrameId = requestAnimationFrame((t) => this.tick(t));
|
|
563
|
+
this.updateCallback();
|
|
564
|
+
return this;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Halts the continuous loop motion immediately.
|
|
569
|
+
* @returns {Smooth}
|
|
570
|
+
*/
|
|
571
|
+
stop() {
|
|
572
|
+
this.isPlaying = false;
|
|
573
|
+
if (this.animationFrameId) {
|
|
574
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
575
|
+
this.animationFrameId = null;
|
|
576
|
+
}
|
|
577
|
+
this.updateCallback();
|
|
578
|
+
return this;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Registers a callback hook to track carousel position and telemetry updates in real-time.
|
|
583
|
+
* @param {function} fn
|
|
584
|
+
* @returns {Smooth}
|
|
585
|
+
*/
|
|
586
|
+
onUpdate(fn) {
|
|
587
|
+
this.changeCallback = fn;
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Broadcasts engine states downstream.
|
|
593
|
+
*/
|
|
594
|
+
updateCallback() {
|
|
595
|
+
if (this.changeCallback) {
|
|
596
|
+
this.changeCallback({
|
|
597
|
+
scrollOffset: this.scrollOffset,
|
|
598
|
+
direction: this.directionVector,
|
|
599
|
+
speed: this.pixelsPerSecond,
|
|
600
|
+
isPlaying: this.isPlaying,
|
|
601
|
+
isHoverSuspended: this.isHovered && this.pauseOnHover,
|
|
602
|
+
itemsPerPage: this.getItemsPerPage()
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Global Exposure
|
|
609
|
+
if (typeof window !== 'undefined') {
|
|
610
|
+
window.Smooth = Smooth;
|
|
611
|
+
window.smooth = new Smooth();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Module Support
|
|
615
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
616
|
+
module.exports = Smooth;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/*
|
|
620
|
+
Example usage:
|
|
621
|
+
|
|
622
|
+
smooth.start([
|
|
623
|
+
{
|
|
624
|
+
selector: '#carousel-container',
|
|
625
|
+
itemSelector: '.carousol-image',
|
|
626
|
+
idPrefix: 'impact-carousel-image',
|
|
627
|
+
speed: 80,
|
|
628
|
+
direction: 'left'
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
selector: '#schools-carousel-container',
|
|
632
|
+
itemSelector: '.partner-logo',
|
|
633
|
+
idPrefix: 'schools-carousel-logo',
|
|
634
|
+
speed: 70,
|
|
635
|
+
direction: 'right'
|
|
636
|
+
}
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
You can also use the classic chainable API:
|
|
640
|
+
|
|
641
|
+
smooth
|
|
642
|
+
.mount('#carousel-container')
|
|
643
|
+
.items(['img1', 'img2', 'img3', 'img4'])
|
|
644
|
+
.speed(100)
|
|
645
|
+
.direction('left')
|
|
646
|
+
.play();
|
|
647
|
+
*/
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const app = document.getElementById("app");
|
|
2
|
+
|
|
3
|
+
const routeLink = (label, route) => Chex.button({
|
|
4
|
+
class: window.router.path.value === route ? "nav-btn active" : "nav-btn",
|
|
5
|
+
onclick: () => window.router.navigate(route)
|
|
6
|
+
}, label);
|
|
7
|
+
|
|
8
|
+
const App = () => Chex.div({ class: "app" }, [
|
|
9
|
+
Chex.h1({ class: "hero-title" }, "Chex Multi-Page Demo"),
|
|
10
|
+
Chex.p({ class: "hero-copy" }, "Each page lives in its own file under components/."),
|
|
11
|
+
Chex.div({ class: "nav" }, [
|
|
12
|
+
routeLink("Home", "/home"),
|
|
13
|
+
routeLink("About", "/about"),
|
|
14
|
+
routeLink("Contact", "/contact"),
|
|
15
|
+
routeLink("Demo", "/demo")
|
|
16
|
+
]),
|
|
17
|
+
window.router.view()
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
Chex.render(app, App);
|
|
21
|
+
|
|
22
|
+
if (window.router.path.value === "/") {
|
|
23
|
+
window.router.navigate("/home");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Chex.animate('.hero-title', {
|
|
27
|
+
slideFrom: "top",
|
|
28
|
+
duration: 700,
|
|
29
|
+
opacity: [0, 1]
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Chex.animate('.page', {
|
|
33
|
+
slideFrom: "bottom",
|
|
34
|
+
duration: 500,
|
|
35
|
+
opacity: [0, 1]
|
|
36
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|