@quietsapa/qsl 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.
- package/CHANGELOG.md +74 -0
- package/LICENSE +73 -0
- package/NOTICE +20 -0
- package/README.md +317 -0
- package/dist/qsl.min.js +2 -0
- package/dist/qsl.min.js.map +1 -0
- package/dist/qsl.mjs +1800 -0
- package/dist/qsl.mjs.map +1 -0
- package/dist/qsl.slim.min.js +2 -0
- package/dist/qsl.slim.min.js.map +1 -0
- package/package.json +70 -0
- package/src/core.js +1156 -0
- package/src/index.js +47 -0
- package/src/plugins/circ.js +59 -0
- package/src/plugins/conditions.js +246 -0
- package/src/plugins/dynamic.js +59 -0
- package/src/plugins/events.js +218 -0
- package/src/plugins/logger.js +56 -0
- package/src/plugins/simple-events.js +6 -0
- package/src/plugins/triggers.js +247 -0
- package/src/presets/default.js +7 -0
- package/src/presets/full.js +34 -0
- package/src/types.js +318 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in trigger handlers.
|
|
3
|
+
*
|
|
4
|
+
* A trigger handler receives the raw trigger option and the flow/process it
|
|
5
|
+
* belongs to, and returns a function `(cb) => void` that invokes `cb` once the
|
|
6
|
+
* trigger fires. Returning `null` means "not my trigger" and lets the next
|
|
7
|
+
* registered handler try.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read the argument part of a prefixed trigger option.
|
|
12
|
+
*
|
|
13
|
+
* Uses `slice` rather than `split(':')` so that arguments containing colons
|
|
14
|
+
* (CSS pseudo-classes, media queries, URLs) survive intact.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} opt - Full trigger option, e.g. `hover:.btn:first-child`.
|
|
17
|
+
* @param {string} prefix - Prefix including the colon, e.g. `hover:`.
|
|
18
|
+
* @returns {string} The argument, e.g. `.btn:first-child`.
|
|
19
|
+
*/
|
|
20
|
+
const arg = (opt, prefix) => opt.slice(prefix.length);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check whether an option is a string starting with the given prefix.
|
|
24
|
+
*
|
|
25
|
+
* @param {*} opt - Trigger option.
|
|
26
|
+
* @param {string} prefix - Prefix including the colon.
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
const isPrefixed = (opt, prefix) => typeof opt === 'string' && opt.startsWith(prefix);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve an element now, or once it is inserted into the document.
|
|
33
|
+
*
|
|
34
|
+
* `appears:` and `visible:` may reference elements that are rendered later, so
|
|
35
|
+
* a plain `querySelector` at registration time is not enough.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} selector - CSS selector.
|
|
38
|
+
* @param {Function} cb - Called with the element once it exists.
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
const whenElement = (selector, cb) => {
|
|
42
|
+
let el = null;
|
|
43
|
+
try {
|
|
44
|
+
el = document.querySelector(selector);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
/* Invalid selector: fail open so the flow is never stuck. */
|
|
47
|
+
cb(null);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (el) {
|
|
52
|
+
cb(el);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof MutationObserver !== 'function') {
|
|
57
|
+
cb(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const root = document.body || document.documentElement;
|
|
62
|
+
if (!root) {
|
|
63
|
+
cb(null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const observer = new MutationObserver(() => {
|
|
68
|
+
const found = document.querySelector(selector);
|
|
69
|
+
if (found) {
|
|
70
|
+
observer.disconnect();
|
|
71
|
+
cb(found);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `load` - fires on the window load event.
|
|
79
|
+
* @param {Object} QSL
|
|
80
|
+
*/
|
|
81
|
+
export function loadTrigger(QSL) {
|
|
82
|
+
QSL.triggerHandlers.add(function (opt) {
|
|
83
|
+
if (opt !== 'load') return null;
|
|
84
|
+
return (cb) => {
|
|
85
|
+
if (document.readyState === 'complete') {
|
|
86
|
+
cb();
|
|
87
|
+
} else {
|
|
88
|
+
window.addEventListener('load', () => cb(), { once: true });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `idle` - fires when the browser is idle.
|
|
96
|
+
* @param {Object} QSL
|
|
97
|
+
*/
|
|
98
|
+
export function idleTrigger(QSL) {
|
|
99
|
+
QSL.triggerHandlers.add(function (opt) {
|
|
100
|
+
if (opt !== 'idle') return null;
|
|
101
|
+
return (cb) => {
|
|
102
|
+
if (typeof window.requestIdleCallback === 'function') {
|
|
103
|
+
window.requestIdleCallback(() => cb());
|
|
104
|
+
} else {
|
|
105
|
+
setTimeout(() => cb(), 200);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `domready` - fires on DOMContentLoaded.
|
|
113
|
+
*
|
|
114
|
+
* `interactive` means DOMContentLoaded has already fired, so a listener added
|
|
115
|
+
* at that point would never run.
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} QSL
|
|
118
|
+
*/
|
|
119
|
+
export function domReadyTrigger(QSL) {
|
|
120
|
+
QSL.triggerHandlers.add(function (opt) {
|
|
121
|
+
if (opt !== 'domready') return null;
|
|
122
|
+
return (cb) => {
|
|
123
|
+
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
|
124
|
+
cb();
|
|
125
|
+
} else {
|
|
126
|
+
document.addEventListener('DOMContentLoaded', () => cb(), { once: true });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `delay:<ms>` - fires after a timeout.
|
|
134
|
+
* @param {Object} QSL
|
|
135
|
+
*/
|
|
136
|
+
export function delayTrigger(QSL) {
|
|
137
|
+
QSL.triggerHandlers.add(function (opt) {
|
|
138
|
+
if (!isPrefixed(opt, 'delay:')) return null;
|
|
139
|
+
const ms = parseInt(arg(opt, 'delay:'), 10);
|
|
140
|
+
return (cb) => setTimeout(cb, Number.isFinite(ms) && ms > 0 ? ms : 0);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `hover:<selector>` - fires when the pointer enters the element.
|
|
146
|
+
* @param {Object} QSL
|
|
147
|
+
*/
|
|
148
|
+
export function hoverTrigger(QSL) {
|
|
149
|
+
QSL.triggerHandlers.add(function (opt) {
|
|
150
|
+
if (!isPrefixed(opt, 'hover:')) return null;
|
|
151
|
+
const selector = arg(opt, 'hover:');
|
|
152
|
+
return (cb) => {
|
|
153
|
+
whenElement(selector, (el) => {
|
|
154
|
+
if (!el) {
|
|
155
|
+
cb();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
el.addEventListener('mouseenter', () => cb(), { once: true, passive: true });
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `visible:<selector>` - fires when the element intersects the viewport.
|
|
166
|
+
* @param {Object} QSL
|
|
167
|
+
*/
|
|
168
|
+
export function visibleTrigger(QSL) {
|
|
169
|
+
QSL.triggerHandlers.add(function (opt) {
|
|
170
|
+
if (!isPrefixed(opt, 'visible:')) return null;
|
|
171
|
+
const selector = arg(opt, 'visible:');
|
|
172
|
+
return (cb) => {
|
|
173
|
+
whenElement(selector, (el) => {
|
|
174
|
+
if (!el || typeof IntersectionObserver !== 'function') {
|
|
175
|
+
cb();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const observer = new IntersectionObserver((entries) => {
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (entry.isIntersecting) {
|
|
181
|
+
observer.disconnect();
|
|
182
|
+
cb();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
observer.observe(el);
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `appears:<selector>` - fires when the element is inserted into the DOM.
|
|
195
|
+
* @param {Object} QSL
|
|
196
|
+
*/
|
|
197
|
+
export function appearsTrigger(QSL) {
|
|
198
|
+
QSL.triggerHandlers.add(function (opt) {
|
|
199
|
+
if (!isPrefixed(opt, 'appears:')) return null;
|
|
200
|
+
const selector = arg(opt, 'appears:');
|
|
201
|
+
return (cb) => whenElement(selector, () => cb());
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `media:<query>` - fires when the media query matches, now or later.
|
|
207
|
+
* @param {Object} QSL
|
|
208
|
+
*/
|
|
209
|
+
export function mediaQueryTrigger(QSL) {
|
|
210
|
+
QSL.triggerHandlers.add(function (opt, o) {
|
|
211
|
+
if (!isPrefixed(opt, 'media:')) return null;
|
|
212
|
+
const query = arg(opt, 'media:');
|
|
213
|
+
return (cb) => {
|
|
214
|
+
if (!query.length || typeof window.matchMedia !== 'function') {
|
|
215
|
+
if (o) o.skipped = true;
|
|
216
|
+
cb();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const mql = window.matchMedia(query);
|
|
220
|
+
if (mql.matches) {
|
|
221
|
+
cb();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const handler = (e) => {
|
|
225
|
+
if (!e.matches) return;
|
|
226
|
+
mql.removeEventListener('change', handler);
|
|
227
|
+
cb();
|
|
228
|
+
};
|
|
229
|
+
mql.addEventListener('change', handler);
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Register every built-in trigger handler.
|
|
236
|
+
* @param {Object} QSL
|
|
237
|
+
*/
|
|
238
|
+
export default function (QSL) {
|
|
239
|
+
loadTrigger(QSL);
|
|
240
|
+
idleTrigger(QSL);
|
|
241
|
+
domReadyTrigger(QSL);
|
|
242
|
+
delayTrigger(QSL);
|
|
243
|
+
hoverTrigger(QSL);
|
|
244
|
+
visibleTrigger(QSL);
|
|
245
|
+
appearsTrigger(QSL);
|
|
246
|
+
mediaQueryTrigger(QSL);
|
|
247
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import core from '../core.js';
|
|
2
|
+
|
|
3
|
+
import { Script, InlineScript, InlineStyle, Stylesheet, Pixel, Shadow, HTML } from '../types.js';
|
|
4
|
+
|
|
5
|
+
import { mediaQueryCondition, languageCondition, timezoneCondition, urlCondition, userAgentCondition } from '../plugins/conditions.js';
|
|
6
|
+
import { loadTrigger, idleTrigger, domReadyTrigger, delayTrigger, hoverTrigger, visibleTrigger, appearsTrigger, mediaQueryTrigger } from '../plugins/triggers.js';
|
|
7
|
+
|
|
8
|
+
import logger from '../plugins/logger.js';
|
|
9
|
+
import events from '../plugins/events.js';
|
|
10
|
+
import circ from '../plugins/circ.js';
|
|
11
|
+
|
|
12
|
+
core
|
|
13
|
+
.registerTypes([Script, InlineScript, InlineStyle, Stylesheet, Pixel, Shadow, HTML])
|
|
14
|
+
|
|
15
|
+
.use(logger)
|
|
16
|
+
.use(events)
|
|
17
|
+
.use(circ)
|
|
18
|
+
|
|
19
|
+
.use(mediaQueryCondition)
|
|
20
|
+
.use(languageCondition)
|
|
21
|
+
.use(timezoneCondition)
|
|
22
|
+
.use(urlCondition)
|
|
23
|
+
.use(userAgentCondition)
|
|
24
|
+
|
|
25
|
+
.use(loadTrigger)
|
|
26
|
+
.use(idleTrigger)
|
|
27
|
+
.use(domReadyTrigger)
|
|
28
|
+
.use(delayTrigger)
|
|
29
|
+
.use(hoverTrigger)
|
|
30
|
+
.use(visibleTrigger)
|
|
31
|
+
.use(appearsTrigger)
|
|
32
|
+
.use(mediaQueryTrigger)
|
|
33
|
+
|
|
34
|
+
.init();
|
package/src/types.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get cache-bypass suffix for a URL.
|
|
3
|
+
*/
|
|
4
|
+
const bypassSuffix = (source, bypassCache) =>{
|
|
5
|
+
return bypassCache ? ( ( source.includes('?') ? '&' : '?') + Date.now() ) : '';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Log a message.
|
|
10
|
+
*/
|
|
11
|
+
const log = (detail) => {
|
|
12
|
+
window.dispatchEvent(new CustomEvent('QSL:log', { detail }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Log an error.
|
|
17
|
+
*/
|
|
18
|
+
const error = (detail) => {
|
|
19
|
+
window.dispatchEvent(new CustomEvent('QSL:error', { detail }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render an element.
|
|
24
|
+
*/
|
|
25
|
+
const render = (config, callbacks = {}) => {
|
|
26
|
+
return new Promise(async (resolve) => {
|
|
27
|
+
const { flowId, tag, id, delay, data, onBeforeStart, onComplete, onError, footer, dom, onElement, onCustomResolve } = config;
|
|
28
|
+
if (!flowId || !tag || !id) {
|
|
29
|
+
resolve();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
if (onBeforeStart) await onBeforeStart(config);
|
|
34
|
+
log({ tag, type: 'PROCESS_STARTED', config });
|
|
35
|
+
if (delay) await new Promise(res => setTimeout(res, delay));
|
|
36
|
+
let el = document.createElement(tag);
|
|
37
|
+
onElement?.(el, config);
|
|
38
|
+
|
|
39
|
+
callbacks.registerProcessElement?.(el, config);
|
|
40
|
+
|
|
41
|
+
if (data && typeof data === 'object') {
|
|
42
|
+
for (const [key, value] of Object.entries(data)) {
|
|
43
|
+
el.setAttribute(`data-${key.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/[^a-z0-9_-]/g, '').replace(/^-/, '')}`, String(value));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!onCustomResolve) {
|
|
48
|
+
el.onload = () => {
|
|
49
|
+
log({ tag, type: 'PROCESS_COMPLETED', config });
|
|
50
|
+
onComplete?.();
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
el.onerror = (e) => {
|
|
55
|
+
error({ tag, type: 'PROCESS_FAILED', config });
|
|
56
|
+
onError?.(e);
|
|
57
|
+
resolve(e);
|
|
58
|
+
};
|
|
59
|
+
if (dom) {
|
|
60
|
+
(footer ? document.body : document.head).appendChild(el);
|
|
61
|
+
}
|
|
62
|
+
onCustomResolve?.({ el, config, resolve });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
error({ tag, type: 'PROCESS_FAILED', config, error: e });
|
|
65
|
+
onError?.(e);
|
|
66
|
+
resolve(e);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render an inline script.
|
|
73
|
+
*/
|
|
74
|
+
const InlineScript = {
|
|
75
|
+
type: 'inline-script',
|
|
76
|
+
handler: (config, callbacks) => {
|
|
77
|
+
let storedResolve = null;
|
|
78
|
+
let alreadyFired = false;
|
|
79
|
+
const eventName = `QSL:inline-script:completed:${config.id}`;
|
|
80
|
+
const resolveProcess = () => {
|
|
81
|
+
log({ tag: 'inline-script', type: 'INLINE_SCRIPT_SUCCESS', config });
|
|
82
|
+
log({ tag: 'inline-script', type: 'PROCESS_COMPLETED', config });
|
|
83
|
+
config?.onComplete?.();
|
|
84
|
+
storedResolve?.();
|
|
85
|
+
};
|
|
86
|
+
const normalizedConfig = {
|
|
87
|
+
...config,
|
|
88
|
+
code: config.code?.replace(/<script.*?>|<\/script>/gi, '')
|
|
89
|
+
};
|
|
90
|
+
return render({
|
|
91
|
+
...normalizedConfig,
|
|
92
|
+
tag: 'script',
|
|
93
|
+
dom: true,
|
|
94
|
+
onElement: (el, config) => {
|
|
95
|
+
const { code, module, id, flowId } = config;
|
|
96
|
+
if (code) el.textContent = code;
|
|
97
|
+
if (module) {
|
|
98
|
+
el.type = 'module';
|
|
99
|
+
const originalCode = el.textContent;
|
|
100
|
+
const fid = JSON.stringify(String(flowId));
|
|
101
|
+
const pid = JSON.stringify(String(id));
|
|
102
|
+
const evt = JSON.stringify(String(eventName));
|
|
103
|
+
const wrappedCode = `(function(){window.__QSL__.currentProcessPerFlow.set(${fid},${pid});try{${originalCode}}finally{window.__QSL__.currentProcessPerFlow.delete(${fid});window.dispatchEvent(new Event(${evt}));}})();`;
|
|
104
|
+
el.textContent = wrappedCode;
|
|
105
|
+
const handler = () => {
|
|
106
|
+
window.removeEventListener(eventName, handler);
|
|
107
|
+
if (storedResolve) {
|
|
108
|
+
resolveProcess();
|
|
109
|
+
} else {
|
|
110
|
+
alreadyFired = true;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
window.addEventListener(eventName, handler);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
onCustomResolve: ({ el, config, resolve }) => {
|
|
117
|
+
const { module } = config;
|
|
118
|
+
storedResolve = resolve;
|
|
119
|
+
if (module && alreadyFired) {
|
|
120
|
+
resolveProcess();
|
|
121
|
+
} else if (!module) {
|
|
122
|
+
queueMicrotask(() => {
|
|
123
|
+
if (storedResolve === resolve) resolveProcess();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}, callbacks);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render a script element.
|
|
133
|
+
*/
|
|
134
|
+
const Script = {
|
|
135
|
+
type: 'script',
|
|
136
|
+
handler: (config, callbacks) => {
|
|
137
|
+
return render({
|
|
138
|
+
...config,
|
|
139
|
+
tag: 'script',
|
|
140
|
+
dom: true,
|
|
141
|
+
onElement: (el, { src, module, async, defer, crossOrigin, integrity, bypassCache }) => {
|
|
142
|
+
if (module) el.type = 'module';
|
|
143
|
+
if (async) el.async = async;
|
|
144
|
+
if (defer) el.defer = defer;
|
|
145
|
+
if (crossOrigin) el.crossOrigin = crossOrigin;
|
|
146
|
+
if (integrity) el.integrity = integrity;
|
|
147
|
+
if (src) el.src = src + bypassSuffix(src, bypassCache);
|
|
148
|
+
}
|
|
149
|
+
}, callbacks);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Render an inline style.
|
|
155
|
+
*/
|
|
156
|
+
const InlineStyle = {
|
|
157
|
+
type: 'style',
|
|
158
|
+
handler: (config, callbacks) => {
|
|
159
|
+
const normalizedConfig = {
|
|
160
|
+
...config,
|
|
161
|
+
code: config.code?.replace(/<style.*?>|<\/style>/gi, '')
|
|
162
|
+
};
|
|
163
|
+
return render({
|
|
164
|
+
...normalizedConfig,
|
|
165
|
+
tag: 'style',
|
|
166
|
+
dom: true,
|
|
167
|
+
onElement: (el, { code }) => {
|
|
168
|
+
if (code) el.textContent = code;
|
|
169
|
+
},
|
|
170
|
+
onCustomResolve: ({ el, config, resolve }) => {
|
|
171
|
+
const { tag, onComplete } = config;
|
|
172
|
+
log({ tag, type: 'INLINE_STYLE_SUCCESS', config });
|
|
173
|
+
log({ tag, type: 'PROCESS_COMPLETED', config });
|
|
174
|
+
onComplete?.();
|
|
175
|
+
resolve();
|
|
176
|
+
}
|
|
177
|
+
}, callbacks);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Render a stylesheet.
|
|
183
|
+
*/
|
|
184
|
+
const Stylesheet = {
|
|
185
|
+
type: 'stylesheet',
|
|
186
|
+
handler: (config, callbacks) => {
|
|
187
|
+
return render({
|
|
188
|
+
...config,
|
|
189
|
+
tag: 'link',
|
|
190
|
+
dom: true,
|
|
191
|
+
onElement: (el, { href, crossOrigin, bypassCache }) => {
|
|
192
|
+
el.rel = 'stylesheet';
|
|
193
|
+
if (crossOrigin) el.crossOrigin = crossOrigin;
|
|
194
|
+
el.href = href + bypassSuffix(href, bypassCache);
|
|
195
|
+
}
|
|
196
|
+
}, callbacks);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Render a pixel.
|
|
202
|
+
*/
|
|
203
|
+
const Pixel = {
|
|
204
|
+
type: 'pixel',
|
|
205
|
+
handler: (config, callbacks) => {
|
|
206
|
+
return render({
|
|
207
|
+
...config,
|
|
208
|
+
tag: 'img',
|
|
209
|
+
dom: true,
|
|
210
|
+
onElement: (el, { style = { display: 'none' }, dom, src, bypassCache }) => {
|
|
211
|
+
if ( ! dom ) el = new window.Image();
|
|
212
|
+
el.src = src + bypassSuffix(src, bypassCache);
|
|
213
|
+
el.width = 1;
|
|
214
|
+
el.height = 1;
|
|
215
|
+
if ( style && typeof style === 'object' ) {
|
|
216
|
+
for (const [key, value] of Object.entries(style)) {
|
|
217
|
+
el.style.setProperty(key, value);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
onCustomResolve: ({ el, config, resolve }) => {
|
|
222
|
+
const { tag, dom, onComplete } = config;
|
|
223
|
+
const resolver = () => {
|
|
224
|
+
log({ tag, type: 'IMAGE_LOADED', config });
|
|
225
|
+
log({ tag, type: 'PROCESS_COMPLETED', config });
|
|
226
|
+
onComplete?.();
|
|
227
|
+
resolve();
|
|
228
|
+
};
|
|
229
|
+
! dom ? resolver() : el.onload = () => resolver();
|
|
230
|
+
}
|
|
231
|
+
}, callbacks);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Render a custom element.
|
|
237
|
+
*/
|
|
238
|
+
const Shadow = {
|
|
239
|
+
type: 'shadow',
|
|
240
|
+
handler: (config, callbacks) => {
|
|
241
|
+
return render({
|
|
242
|
+
...config,
|
|
243
|
+
onBeforeStart: async ({ tag }) => await window.customElements.whenDefined(tag),
|
|
244
|
+
onElement: (el, { shadowData }) => {
|
|
245
|
+
el.data = shadowData || {};
|
|
246
|
+
if (shadowData?.hidden) el.setAttribute('hidden', '');
|
|
247
|
+
},
|
|
248
|
+
onCustomResolve: ({ el, config, resolve }) => {
|
|
249
|
+
const { tag, shadowData, onComplete, onError } = config;
|
|
250
|
+
const resolver = () => {
|
|
251
|
+
const selector = shadowData?.container;
|
|
252
|
+
let container = null;
|
|
253
|
+
if (selector === 'body') {
|
|
254
|
+
container = document.body;
|
|
255
|
+
} else if (typeof selector === 'string' && selector) {
|
|
256
|
+
try {
|
|
257
|
+
container = document.querySelector(selector);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
container = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!container) {
|
|
263
|
+
const err = new Error(`Container not found: ${selector}`);
|
|
264
|
+
error({ tag, type: 'SHADOW_FAILED', config, error: err });
|
|
265
|
+
onError?.(err);
|
|
266
|
+
resolve(err);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
shadowData?.position === 'top' ? container.insertBefore(el, container.firstChild) : container.appendChild(el);
|
|
270
|
+
log({ tag, type: 'SHADOW_SUCCESS', config });
|
|
271
|
+
log({ tag, type: 'PROCESS_COMPLETED', config });
|
|
272
|
+
onComplete?.();
|
|
273
|
+
resolve();
|
|
274
|
+
};
|
|
275
|
+
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
|
276
|
+
resolver();
|
|
277
|
+
} else {
|
|
278
|
+
document.addEventListener('DOMContentLoaded', resolver, { once: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}, callbacks);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Render an HTML element.
|
|
287
|
+
*/
|
|
288
|
+
const HTML = {
|
|
289
|
+
type: 'html',
|
|
290
|
+
handler: (config, callbacks) => {
|
|
291
|
+
return render({
|
|
292
|
+
...config,
|
|
293
|
+
dom: true,
|
|
294
|
+
onElement: (el, { html = '', id = '', className = '', style = {} }) => {
|
|
295
|
+
if (html) el.innerHTML = html;
|
|
296
|
+
if (id) el.id = id;
|
|
297
|
+
if (className) el.className = Array.isArray(className) ? className.join(' ') : className;
|
|
298
|
+
if ( style && typeof style === 'object' ) {
|
|
299
|
+
for (const [key, value] of Object.entries(style)) {
|
|
300
|
+
el.style.setProperty(key, value);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
onCustomResolve: ({ el, config, resolve }) => {
|
|
305
|
+
const { tag, onComplete } = config;
|
|
306
|
+
log({ tag, type: 'HTML_SUCCESS', config });
|
|
307
|
+
log({ tag, type: 'PROCESS_COMPLETED', config });
|
|
308
|
+
onComplete?.();
|
|
309
|
+
resolve();
|
|
310
|
+
}
|
|
311
|
+
}, callbacks);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Default types for QSL.
|
|
317
|
+
*/
|
|
318
|
+
export { Script, Stylesheet, InlineScript, InlineStyle, Pixel, Shadow, HTML };
|