@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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollEcho.js
|
|
3
|
+
* A high-performance, lightweight scroll-triggered text reveal library.
|
|
4
|
+
* Supports character-by-character and word-by-word reveal cascades.
|
|
5
|
+
* * Initializer Syntax:
|
|
6
|
+
* ScrollEcho.auto('#target', { type: 'char', delay: 20 });
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class ScrollEcho {
|
|
10
|
+
constructor(selector, options = {}) {
|
|
11
|
+
// DOM lookup for target elements
|
|
12
|
+
this.elements = document.querySelectorAll(selector);
|
|
13
|
+
|
|
14
|
+
// Configuration options with fluid defaults
|
|
15
|
+
this.options = {
|
|
16
|
+
type: options.type || 'word', // Reveal type: 'word' or 'char'
|
|
17
|
+
delay: options.delay !== undefined ? options.delay : 40, // Stagger delay gap in milliseconds
|
|
18
|
+
duration: options.duration !== undefined ? options.duration : 400, // Speed of transition animation in ms
|
|
19
|
+
threshold: options.threshold !== undefined ? options.threshold : 0.15, // Viewport visibility scroll threshold (0.0 to 1.0)
|
|
20
|
+
transformY: options.transformY || '12px', // Vertical rise offset displacement
|
|
21
|
+
ease: options.ease || 'cubic-bezier(0.25, 1, 0.5, 1)', // Easing transition timing curve
|
|
22
|
+
onRevealComplete: options.onRevealComplete || null, // Optional callback executed when reveal loop completes
|
|
23
|
+
...options
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Initialize global instance registry for manual retrigger controls
|
|
27
|
+
if (!window.scrollTextTriggerInstances) {
|
|
28
|
+
window.scrollTextTriggerInstances = {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.init();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Static Auto Initializer Endpoint
|
|
36
|
+
* @param {string} selector - Target selector string (e.g. '#target-element')
|
|
37
|
+
* @param {object} options - Configuration overrides
|
|
38
|
+
* @returns {ScrollEcho} Instance of ScrollEcho
|
|
39
|
+
*/
|
|
40
|
+
static auto(selector, options = {}) {
|
|
41
|
+
return new ScrollEcho(selector, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initializes elements by parsing text content and wrapping with spans
|
|
46
|
+
*/
|
|
47
|
+
init() {
|
|
48
|
+
this.elements.forEach((element) => {
|
|
49
|
+
// Guard clause to prevent duplicate parsing initialization on the same node
|
|
50
|
+
if (element.dataset.revealerInitialized) return;
|
|
51
|
+
|
|
52
|
+
const originalText = element.textContent.trim();
|
|
53
|
+
element.innerHTML = ''; // Clear original text nodes safely
|
|
54
|
+
element.dataset.revealerInitialized = "true";
|
|
55
|
+
|
|
56
|
+
// Store original text format for re-trigger calculations
|
|
57
|
+
element.dataset.originalContent = originalText;
|
|
58
|
+
|
|
59
|
+
let splitArray = [];
|
|
60
|
+
if (this.options.type === 'char') {
|
|
61
|
+
splitArray = originalText.split(''); // Split to individual characters
|
|
62
|
+
} else {
|
|
63
|
+
splitArray = originalText.split(/\s+/); // Split on whitespace blocks to get words
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const docFragment = document.createDocumentFragment();
|
|
67
|
+
|
|
68
|
+
splitArray.forEach((item, index) => {
|
|
69
|
+
const span = document.createElement('span');
|
|
70
|
+
|
|
71
|
+
if (this.options.type === 'char') {
|
|
72
|
+
span.className = 'reveal-char';
|
|
73
|
+
// Manage spaces elegantly using non-breaking spaces
|
|
74
|
+
if (item === ' ') {
|
|
75
|
+
span.innerHTML = ' ';
|
|
76
|
+
} else {
|
|
77
|
+
span.innerText = item;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
span.className = 'reveal-word';
|
|
81
|
+
span.innerText = item;
|
|
82
|
+
|
|
83
|
+
// Inject a separation space node to avoid text compression when wrapped inside inline-block elements
|
|
84
|
+
if (index < splitArray.length - 1) {
|
|
85
|
+
const space = document.createTextNode(' ');
|
|
86
|
+
docFragment.appendChild(span);
|
|
87
|
+
docFragment.appendChild(space);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Apply initial hidden layout properties & custom configurations
|
|
93
|
+
span.style.display = 'inline-block';
|
|
94
|
+
span.style.maxWidth = '100%';
|
|
95
|
+
span.style.opacity = '0';
|
|
96
|
+
span.style.transform = `translateY(${this.options.transformY})`;
|
|
97
|
+
span.style.transitionProperty = 'opacity, transform';
|
|
98
|
+
span.style.transitionDuration = `${this.options.duration}ms`;
|
|
99
|
+
span.style.transitionTimingFunction = this.options.ease;
|
|
100
|
+
|
|
101
|
+
docFragment.appendChild(span);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
element.appendChild(docFragment);
|
|
105
|
+
|
|
106
|
+
// Dynamically assign an ID if element does not have one to map callbacks
|
|
107
|
+
const trackerId = element.id || 'el-' + Math.random().toString(36).substr(2, 9);
|
|
108
|
+
if (!element.id) element.id = trackerId;
|
|
109
|
+
|
|
110
|
+
// Map reference inside global window register
|
|
111
|
+
window.scrollTextTriggerInstances[trackerId] = this;
|
|
112
|
+
|
|
113
|
+
// Bind element to the IntersectionObserver API
|
|
114
|
+
this.createObserver(element);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Binds observation triggers using native high-performance IntersectionObserver
|
|
120
|
+
* @param {HTMLElement} element - Target text node element container
|
|
121
|
+
*/
|
|
122
|
+
createObserver(element) {
|
|
123
|
+
const observerOptions = {
|
|
124
|
+
root: null, // Scans parent window viewport bounds
|
|
125
|
+
rootMargin: '0px',
|
|
126
|
+
threshold: this.options.threshold
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const observer = new IntersectionObserver((entries, observer) => {
|
|
130
|
+
entries.forEach(entry => {
|
|
131
|
+
if (entry.isIntersecting) {
|
|
132
|
+
this.animateReveal(entry.target);
|
|
133
|
+
observer.unobserve(entry.target); // Kill observer to fire animation only once
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}, observerOptions);
|
|
137
|
+
|
|
138
|
+
observer.observe(element);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Staggers animation opacity & transformation triggers for elements children spans
|
|
143
|
+
* @param {HTMLElement} element - Parent element containing split reveal spans
|
|
144
|
+
*/
|
|
145
|
+
animateReveal(element) {
|
|
146
|
+
const targetClass = this.options.type === 'char' ? '.reveal-char' : '.reveal-word';
|
|
147
|
+
const children = element.querySelectorAll(targetClass);
|
|
148
|
+
|
|
149
|
+
children.forEach((child, index) => {
|
|
150
|
+
const itemDelay = index * this.options.delay;
|
|
151
|
+
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
child.style.opacity = '1';
|
|
154
|
+
child.style.transform = 'translateY(0)';
|
|
155
|
+
}, itemDelay);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Trigger optional complete callback if defined
|
|
159
|
+
if (typeof this.options.onRevealComplete === 'function') {
|
|
160
|
+
const totalAnimateTime = (children.length * this.options.delay) + this.options.duration;
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
this.options.onRevealComplete(element);
|
|
163
|
+
}, totalAnimateTime);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Public method to reset animations and play the reveal sequence on command
|
|
169
|
+
* @param {string} elementId - ID string of target element to replay
|
|
170
|
+
*/
|
|
171
|
+
retrigger(elementId) {
|
|
172
|
+
const element = document.getElementById(elementId);
|
|
173
|
+
if (!element) return;
|
|
174
|
+
|
|
175
|
+
const targetClass = this.options.type === 'char' ? '.reveal-char' : '.reveal-word';
|
|
176
|
+
const children = element.querySelectorAll(targetClass);
|
|
177
|
+
|
|
178
|
+
// Reset elements silently back to offset position without transitions
|
|
179
|
+
children.forEach(child => {
|
|
180
|
+
child.style.transition = 'none'; // Temporarily bypass CSS transitions
|
|
181
|
+
child.style.opacity = '0';
|
|
182
|
+
child.style.transform = `translateY(${this.options.transformY})`;
|
|
183
|
+
|
|
184
|
+
// Force browser layout reflow/repaint recalculation
|
|
185
|
+
child.offsetHeight;
|
|
186
|
+
|
|
187
|
+
child.style.transition = ''; // Restore style defaults for cascade
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Play the staggered cascade reveal loop
|
|
191
|
+
this.animateReveal(element);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
/*
|
|
198
|
+
**************************
|
|
199
|
+
How to use this library
|
|
200
|
+
**************************
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
ScrollEcho.auto('#target', {
|
|
206
|
+
type: 'char', // 'char' (characters) or 'word' (words)
|
|
207
|
+
delay: 30, // stagger delay between each piece (ms)
|
|
208
|
+
duration: 500, // speed of the reveal animation (ms)
|
|
209
|
+
threshold: 0.15, // viewport enter intersection threshold
|
|
210
|
+
transformY: '12px' // customized rise offset
|
|
211
|
+
});
|
|
212
|
+
*/
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skeleton.js (v1.0.0)
|
|
3
|
+
* Zero-Config DOM-to-Skeleton Compiler
|
|
4
|
+
*
|
|
5
|
+
* Auto-creates highly accurate shimmer layouts directly from your rendered DOM markup
|
|
6
|
+
* to guarantee zero layout drift (CLS) without writing manual skeleton designs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class SkeletonLoader {
|
|
10
|
+
constructor(target, options = {}) {
|
|
11
|
+
// Resolve target selector or HTMLElement directly
|
|
12
|
+
this.target = typeof target === 'string' ? document.querySelector(target) : target;
|
|
13
|
+
|
|
14
|
+
// Default config values
|
|
15
|
+
this.options = {
|
|
16
|
+
speed: options.speed || 1600, // Shimmer speed in milliseconds
|
|
17
|
+
baseColor: options.baseColor || '#1e293b', // Base placeholder background color (dark slate default)
|
|
18
|
+
varyTextLines: options.varyTextLines !== undefined ? options.varyTextLines : true, // Vary sentence endings
|
|
19
|
+
exclude: options.exclude || [], // Selectors to skip morphing
|
|
20
|
+
classToKeep: options.classToKeep || [], // Classes to preserve during layout transfer
|
|
21
|
+
...options
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.skeletonClone = null;
|
|
25
|
+
this.isShowing = false;
|
|
26
|
+
this.originalStyles = {
|
|
27
|
+
display: '',
|
|
28
|
+
visibility: ''
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Calculate light linear gradient contrast shine based on base color
|
|
32
|
+
this.highlightColor = this._computeHighlight(this.options.baseColor);
|
|
33
|
+
|
|
34
|
+
// Inject styles dynamically into document head
|
|
35
|
+
this._injectStyles();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Automatically handles injecting visual shimmer keyframes into head to make library Zero-Config
|
|
40
|
+
*/
|
|
41
|
+
_injectStyles() {
|
|
42
|
+
const styleId = 'skeleton-loader-shimmer-styles';
|
|
43
|
+
let styleTag = document.getElementById(styleId);
|
|
44
|
+
|
|
45
|
+
if (!styleTag) {
|
|
46
|
+
styleTag = document.createElement('style');
|
|
47
|
+
styleTag.id = styleId;
|
|
48
|
+
document.head.appendChild(styleTag);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
styleTag.textContent = `
|
|
52
|
+
@keyframes skeleton-shimmer-sweep {
|
|
53
|
+
0% { background-position: -200% 0; }
|
|
54
|
+
100% { background-position: 200% 0; }
|
|
55
|
+
}
|
|
56
|
+
.sk-shimmer-active {
|
|
57
|
+
background-size: 200% 100% !important;
|
|
58
|
+
animation: skeleton-shimmer-sweep ${this.options.speed}ms infinite linear !important;
|
|
59
|
+
}
|
|
60
|
+
.sk-hidden-original {
|
|
61
|
+
display: none !important;
|
|
62
|
+
}
|
|
63
|
+
.sk-faded-text {
|
|
64
|
+
color: transparent !important;
|
|
65
|
+
user-select: none !important;
|
|
66
|
+
pointer-events: none !important;
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Helper algorithm to calculate slightly brighter color tone for gradients
|
|
73
|
+
*/
|
|
74
|
+
_computeHighlight(hex) {
|
|
75
|
+
try {
|
|
76
|
+
let cleanHex = hex.replace('#', '');
|
|
77
|
+
if (cleanHex.length === 3) {
|
|
78
|
+
cleanHex = cleanHex.split('').map(char => char + char).join('');
|
|
79
|
+
}
|
|
80
|
+
let r = parseInt(cleanHex.substring(0, 2), 16);
|
|
81
|
+
let g = parseInt(cleanHex.substring(2, 4), 16);
|
|
82
|
+
let b = parseInt(cleanHex.substring(4, 6), 16);
|
|
83
|
+
|
|
84
|
+
// Brighten color channel weights
|
|
85
|
+
r = Math.min(255, Math.round(r + 20));
|
|
86
|
+
g = Math.min(255, Math.round(g + 20));
|
|
87
|
+
b = Math.min(255, Math.round(b + 20));
|
|
88
|
+
|
|
89
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return '#334155'; // Fallback highlight
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Core Traversal Engine: Mutates cloned elements into static blocks while maintaining coordinates
|
|
97
|
+
*/
|
|
98
|
+
_compileSkeleton(node) {
|
|
99
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
100
|
+
|
|
101
|
+
// Handle user custom skips or ignored blocks
|
|
102
|
+
if (node.hasAttribute('data-skeleton-ignore') || this._shouldExclude(node)) {
|
|
103
|
+
node.style.visibility = 'hidden';
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 1. Convert Images and heavy vectors to Solid blocks
|
|
108
|
+
if (node.tagName === 'IMG' || node.tagName === 'svg' || node.classList.contains('avatar') || node.hasAttribute('data-skeleton-img')) {
|
|
109
|
+
this._morphToPlaceholderBlock(node, 'image');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Convert Buttons, Pills & small Badges
|
|
114
|
+
if (node.tagName === 'BUTTON' || node.classList.contains('badge') || node.hasAttribute('data-skeleton-badge')) {
|
|
115
|
+
this._morphToPlaceholderBlock(node, 'badge');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 3. Process direct text lines inside child elements
|
|
120
|
+
const children = Array.from(node.childNodes);
|
|
121
|
+
let containsDirectText = false;
|
|
122
|
+
|
|
123
|
+
children.forEach(child => {
|
|
124
|
+
if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim().length > 0) {
|
|
125
|
+
containsDirectText = true;
|
|
126
|
+
} else {
|
|
127
|
+
this._compileSkeleton(child);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 4. Render block text wireframe bands
|
|
132
|
+
if (containsDirectText && node.children.length === 0) {
|
|
133
|
+
this._morphTextPlaceholder(node);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Converts plain text characters into modern, line-scaled shimmer bars
|
|
140
|
+
*/
|
|
141
|
+
_morphTextPlaceholder(element) {
|
|
142
|
+
const originalText = element.textContent.trim();
|
|
143
|
+
element.innerHTML = '';
|
|
144
|
+
element.classList.add('sk-faded-text');
|
|
145
|
+
|
|
146
|
+
// Estimate line generation bounds based on string size
|
|
147
|
+
const charactersCount = originalText.length;
|
|
148
|
+
const linesToGenerate = Math.max(1, Math.min(6, Math.round(charactersCount / 65)));
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < linesToGenerate; i++) {
|
|
151
|
+
const textBar = document.createElement('span');
|
|
152
|
+
textBar.style.display = 'block';
|
|
153
|
+
textBar.style.height = '0.7rem';
|
|
154
|
+
textBar.style.marginTop = i === 0 ? '0px' : '6px';
|
|
155
|
+
textBar.style.borderRadius = '0.25rem';
|
|
156
|
+
textBar.style.backgroundColor = this.options.baseColor;
|
|
157
|
+
|
|
158
|
+
// Calculate layout variance percentages for paragraph look
|
|
159
|
+
if (linesToGenerate > 1 && i === linesToGenerate - 1 && this.options.varyTextLines) {
|
|
160
|
+
textBar.style.width = `${50 + Math.floor(Math.random() * 25)}%`;
|
|
161
|
+
} else if (this.options.varyTextLines && linesToGenerate > 1) {
|
|
162
|
+
textBar.style.width = `${85 + Math.floor(Math.random() * 15)}%`;
|
|
163
|
+
} else {
|
|
164
|
+
textBar.style.width = '100%';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add shimmer transitions
|
|
168
|
+
textBar.classList.add('sk-shimmer-active');
|
|
169
|
+
textBar.style.backgroundImage = `linear-gradient(90deg, ${this.options.baseColor} 25%, ${this.highlightColor} 50%, ${this.options.baseColor} 75%)`;
|
|
170
|
+
|
|
171
|
+
element.appendChild(textBar);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Helper to strip sources, colors, and border classes to enforce neutral shapes
|
|
177
|
+
*/
|
|
178
|
+
_morphToPlaceholderBlock(element, type) {
|
|
179
|
+
element.innerHTML = '';
|
|
180
|
+
element.style.backgroundImage = 'none';
|
|
181
|
+
element.style.borderColor = 'transparent';
|
|
182
|
+
element.style.backgroundColor = this.options.baseColor;
|
|
183
|
+
element.removeAttribute('src');
|
|
184
|
+
|
|
185
|
+
element.classList.add('sk-shimmer-active');
|
|
186
|
+
element.style.backgroundImage = `linear-gradient(90deg, ${this.options.baseColor} 25%, ${this.highlightColor} 50%, ${this.options.baseColor} 75%)`;
|
|
187
|
+
|
|
188
|
+
if (type === 'image') {
|
|
189
|
+
element.style.borderRadius = '0.75rem';
|
|
190
|
+
if (!element.style.height && element.clientHeight === 0) {
|
|
191
|
+
element.style.minHeight = '150px'; // Safe aspect boundary
|
|
192
|
+
}
|
|
193
|
+
} else if (type === 'badge') {
|
|
194
|
+
element.style.borderRadius = '9999px';
|
|
195
|
+
element.style.height = '22px';
|
|
196
|
+
element.style.minWidth = '60px';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_shouldExclude(element) {
|
|
201
|
+
return this.options.exclude.some(selector => element.matches(selector));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Creates the visual skeleton clone and hides original DOM layouts
|
|
206
|
+
*/
|
|
207
|
+
start() {
|
|
208
|
+
if (!this.target) {
|
|
209
|
+
console.warn('SkeletonLoader: Target element not found inside current DOM scope.');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (this.isShowing) return;
|
|
213
|
+
|
|
214
|
+
// 1. Deep clone target markup layout
|
|
215
|
+
this.skeletonClone = this.target.cloneNode(true);
|
|
216
|
+
this.skeletonClone.classList.add('skeleton-loader-clone');
|
|
217
|
+
this.skeletonClone.setAttribute('aria-busy', 'true');
|
|
218
|
+
this.skeletonClone.setAttribute('role', 'alert');
|
|
219
|
+
|
|
220
|
+
// Remove ID tags to prevent duplicate selectors breaking DOM scripts
|
|
221
|
+
this.skeletonClone.removeAttribute('id');
|
|
222
|
+
const childsWithId = this.skeletonClone.querySelectorAll('[id]');
|
|
223
|
+
childsWithId.forEach(el => el.removeAttribute('id'));
|
|
224
|
+
|
|
225
|
+
// 2. Transpile the clone nodes to shimmering frames
|
|
226
|
+
this._compileSkeleton(this.skeletonClone);
|
|
227
|
+
|
|
228
|
+
// 3. Hide original layouts and inject the skeleton directly adjacent
|
|
229
|
+
this.target.classList.add('sk-hidden-original');
|
|
230
|
+
this.target.parentNode.insertBefore(this.skeletonClone, this.target.nextSibling);
|
|
231
|
+
|
|
232
|
+
this.isShowing = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Restores original components cleanly and drops simulated skeleton elements
|
|
237
|
+
*/
|
|
238
|
+
dismiss() {
|
|
239
|
+
if (!this.isShowing) return;
|
|
240
|
+
|
|
241
|
+
if (this.skeletonClone && this.skeletonClone.parentNode) {
|
|
242
|
+
this.skeletonClone.parentNode.removeChild(this.skeletonClone);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.target.classList.remove('sk-hidden-original');
|
|
246
|
+
this.isShowing = false;
|
|
247
|
+
this.skeletonClone = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Global factory interface to fulfill simple API requirement:
|
|
253
|
+
* const sk = skeleton.load('#element', { speed: 1600 });
|
|
254
|
+
* sk.start();
|
|
255
|
+
*/
|
|
256
|
+
const skeleton = {
|
|
257
|
+
load(targetSelector, options = {}) {
|
|
258
|
+
return new SkeletonLoader(targetSelector, options);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Make library accessible to both browser globals & modern module architectures
|
|
263
|
+
if (typeof window !== 'undefined') {
|
|
264
|
+
window.skeleton = skeleton;
|
|
265
|
+
}
|
|
266
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
267
|
+
module.exports = skeleton;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* ==============================================================================
|
|
272
|
+
* SKELETON.JS DEVELOPER MANUAL
|
|
273
|
+
* ==============================================================================
|
|
274
|
+
*
|
|
275
|
+
* skeleton.js is a zero-dependency, automated skeleton placeholder generator.
|
|
276
|
+
* It inspects your actual rendered markup and transforms a clone of it into a
|
|
277
|
+
* proportion-perfect, shimmering skeletal container. This prevents Cumulative
|
|
278
|
+
* Layout Shift (CLS) on dynamic websites.
|
|
279
|
+
*
|
|
280
|
+
* ------------------------------------------------------------------------------
|
|
281
|
+
* 1. QUICKEST START (DEFAULT)
|
|
282
|
+
* ------------------------------------------------------------------------------
|
|
283
|
+
* Include this script file in your project, and execute:
|
|
284
|
+
*
|
|
285
|
+
* const myLoader = skeleton.load('#my-product-card');
|
|
286
|
+
* myLoader.start();
|
|
287
|
+
*
|
|
288
|
+
* ------------------------------------------------------------------------------
|
|
289
|
+
* 2. FULL CONFIGURATION PARAMETERS
|
|
290
|
+
* ------------------------------------------------------------------------------
|
|
291
|
+
* Pass configuration arguments into the second initialization parameter:
|
|
292
|
+
*
|
|
293
|
+
* const myLoader = skeleton.load('#my-product-card', {
|
|
294
|
+
* speed: 1600, // Shimmer sweep animation loop speed (milliseconds)
|
|
295
|
+
* baseColor: '#1e293b', // Background plate color of placeholders (HEX)
|
|
296
|
+
* varyTextLines: true, // Give multi-line text paragraphs varied endings
|
|
297
|
+
* exclude: ['.no-shimmer', 'footer'] // Array of CSS classes/tags to ignore
|
|
298
|
+
* });
|
|
299
|
+
*
|
|
300
|
+
* ------------------------------------------------------------------------------
|
|
301
|
+
* 3. COMPLETE REAL-WORLD WORKFLOW EXAMPLE
|
|
302
|
+
* ------------------------------------------------------------------------------
|
|
303
|
+
* Here is how you run skeleton.js during standard API data fetch calls:
|
|
304
|
+
*
|
|
305
|
+
* // A. Instantiate and show skeleton over container during network wait
|
|
306
|
+
* const profileLoader = skeleton.load('#profile-section', {
|
|
307
|
+
* speed: 1200,
|
|
308
|
+
* baseColor: '#0f172a'
|
|
309
|
+
* });
|
|
310
|
+
*
|
|
311
|
+
* profileLoader.start(); // This clones your component, morphs it, & hides original
|
|
312
|
+
*
|
|
313
|
+
* // B. Call backend resource
|
|
314
|
+
* fetch('/api/user/profile')
|
|
315
|
+
* .then(response => response.json())
|
|
316
|
+
* .then(data => {
|
|
317
|
+
* // C. Populate layout fields with actual incoming records
|
|
318
|
+
* document.querySelector('#user-name').textContent = data.name;
|
|
319
|
+
* document.querySelector('#user-avatar').src = data.avatarUrl;
|
|
320
|
+
* document.querySelector('#user-bio').textContent = data.bio;
|
|
321
|
+
*
|
|
322
|
+
* // D. Simply dismiss the skeleton. The real populated components instantly reappear!
|
|
323
|
+
* profileLoader.dismiss();
|
|
324
|
+
* });
|
|
325
|
+
*
|
|
326
|
+
* ------------------------------------------------------------------------------
|
|
327
|
+
* 4. SPECIFIC SECTOR ATTR OVERRIDES
|
|
328
|
+
* ------------------------------------------------------------------------------
|
|
329
|
+
* If there are elements in your DOM that you want to exclude or control manually,
|
|
330
|
+
* use these simple HTML5 markers inside your template tags:
|
|
331
|
+
*
|
|
332
|
+
* <div data-skeleton-ignore="true">
|
|
333
|
+
* This whole section and its children will become hidden during generation.
|
|
334
|
+
* </div>
|
|
335
|
+
*
|
|
336
|
+
* <div class="custom-card-graphic" data-skeleton-img="true">
|
|
337
|
+
* This node will automatically represent an image placeholder block.
|
|
338
|
+
* </div>
|
|
339
|
+
*
|
|
340
|
+
* ==============================================================================
|
|
341
|
+
*/
|