@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
package/src/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QSL public entry point.
|
|
3
|
+
*
|
|
4
|
+
* Side-effect free: importing this module registers nothing and starts
|
|
5
|
+
* nothing. Compose what you need, then call `init()` yourself. For a
|
|
6
|
+
* batteries-included build see `src/presets/full.js`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { default as core } from './core.js';
|
|
10
|
+
export { default } from './core.js';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
Script,
|
|
14
|
+
Stylesheet,
|
|
15
|
+
InlineScript,
|
|
16
|
+
InlineStyle,
|
|
17
|
+
Pixel,
|
|
18
|
+
Shadow,
|
|
19
|
+
HTML,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
default as conditions,
|
|
24
|
+
mediaQueryCondition,
|
|
25
|
+
languageCondition,
|
|
26
|
+
timezoneCondition,
|
|
27
|
+
urlCondition,
|
|
28
|
+
userAgentCondition,
|
|
29
|
+
} from './plugins/conditions.js';
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
default as triggers,
|
|
33
|
+
loadTrigger,
|
|
34
|
+
idleTrigger,
|
|
35
|
+
domReadyTrigger,
|
|
36
|
+
delayTrigger,
|
|
37
|
+
hoverTrigger,
|
|
38
|
+
visibleTrigger,
|
|
39
|
+
appearsTrigger,
|
|
40
|
+
mediaQueryTrigger,
|
|
41
|
+
} from './plugins/triggers.js';
|
|
42
|
+
|
|
43
|
+
export { default as logger } from './plugins/logger.js';
|
|
44
|
+
export { default as events } from './plugins/events.js';
|
|
45
|
+
export { default as circ } from './plugins/circ.js';
|
|
46
|
+
export { default as dynamic } from './plugins/dynamic.js';
|
|
47
|
+
export { default as simpleEvents } from './plugins/simple-events.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check for circular dependencies in flows and processes.
|
|
3
|
+
* @param {*} QSL
|
|
4
|
+
*/
|
|
5
|
+
export default function(QSL) {
|
|
6
|
+
QSL.loadActions.add(function() {
|
|
7
|
+
/**
|
|
8
|
+
* Helper function to detect circular dependencies
|
|
9
|
+
*/
|
|
10
|
+
const detectCircularDependency = (id, getDeps, visited = new Set()) => {
|
|
11
|
+
if (visited.has(id)) return true;
|
|
12
|
+
visited.add(id);
|
|
13
|
+
const deps = getDeps(id) || [];
|
|
14
|
+
for (const depId of deps) {
|
|
15
|
+
if (detectCircularDependency(depId, getDeps, new Set(visited))) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check for circular dependencies in flows
|
|
22
|
+
*/
|
|
23
|
+
const getFlowDeps = id => (this.flowOptions.get(this.normalizeFlowId(id))?.depends) || [];
|
|
24
|
+
for (const [fid, options] of this.flowOptions.entries()) {
|
|
25
|
+
if (Array.isArray(options.depends) && options.depends.length) {
|
|
26
|
+
if (detectCircularDependency(fid, getFlowDeps)) {
|
|
27
|
+
this.log('CIRC_FLOW_DEP_SKIPPED', fid, options.depends);
|
|
28
|
+
this.setFlowOptions({ status: this.FLOW_STATE.COMPLETED }, fid); /* Mark as completed */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check for circular dependencies in processes
|
|
35
|
+
*/
|
|
36
|
+
const getProcDeps = id => {
|
|
37
|
+
for (const flow of this.flows.values()) {
|
|
38
|
+
const proc = flow.find(p => p.id === id);
|
|
39
|
+
if (proc && Array.isArray(proc.depends)) return proc.depends.map(dep => this.PREFIX + dep);
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
};
|
|
43
|
+
for (const flow of this.flows.values()) {
|
|
44
|
+
for (const process of flow) {
|
|
45
|
+
if (Array.isArray(process.depends) && process.depends.length) {
|
|
46
|
+
if (detectCircularDependency(process.id, getProcDeps)) {
|
|
47
|
+
this.log('CIRC_PROCESS_DEP_SKIPPED', process.id, process.depends);
|
|
48
|
+
process.condition = false; /* Turn off condition */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if ( QSL.logger && QSL.logger.VERSION === 'qsl-logger' ) {
|
|
56
|
+
QSL.logger.LOG.CIRC_FLOW_DEP_SKIPPED = '[QSL] Circular flow dependency skipped:';
|
|
57
|
+
QSL.logger.LOG.CIRC_PROCESS_DEP_SKIPPED = '[QSL] Circular process dependency skipped:';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media query condition handler
|
|
3
|
+
* @param {*} QSL
|
|
4
|
+
*/
|
|
5
|
+
export function mediaQueryCondition(QSL) {
|
|
6
|
+
QSL.conditionHandlers.add(function(opt) {
|
|
7
|
+
if (typeof opt !== 'string' || !opt.startsWith('media:')) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return !window.matchMedia(opt.slice('media:'.length)).matches;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Language condition handler
|
|
16
|
+
* @param {*} QSL
|
|
17
|
+
*/
|
|
18
|
+
export function languageCondition(QSL) {
|
|
19
|
+
QSL.conditionHandlers.add(function(opt) {
|
|
20
|
+
if (typeof opt !== 'string' || !opt.startsWith('lang:')) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const p = opt.split(':');
|
|
24
|
+
const t = p[1] || 'equals';
|
|
25
|
+
const v = p.slice(2).join(':');
|
|
26
|
+
const l = navigator.language || navigator.languages?.[0] || '';
|
|
27
|
+
|
|
28
|
+
switch (t) {
|
|
29
|
+
case 'equals':
|
|
30
|
+
case 'is':
|
|
31
|
+
return l !== v;
|
|
32
|
+
case 'contains':
|
|
33
|
+
return !l.includes(v);
|
|
34
|
+
case 'startsWith':
|
|
35
|
+
return !l.startsWith(v);
|
|
36
|
+
case 'in':
|
|
37
|
+
const ls = v.split(',').map(ll => ll.trim());
|
|
38
|
+
return !ls.includes(l);
|
|
39
|
+
default:
|
|
40
|
+
return true; // Unknown type, fail condition
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Timezone condition handler
|
|
47
|
+
* @param {*} QSL
|
|
48
|
+
*/
|
|
49
|
+
export function timezoneCondition(QSL) {
|
|
50
|
+
QSL.conditionHandlers.add(function(opt) {
|
|
51
|
+
if (typeof opt !== 'string' ||
|
|
52
|
+
(!opt.startsWith('tz:') && !opt.startsWith('timezone:'))) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const p = opt.split(':');
|
|
56
|
+
const t = p[1] || 'equals';
|
|
57
|
+
const v = p.slice(2).join(':');
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
61
|
+
const o = -new Date().getTimezoneOffset() / 60;
|
|
62
|
+
|
|
63
|
+
switch (t) {
|
|
64
|
+
case 'equals':
|
|
65
|
+
case 'is':
|
|
66
|
+
return tz !== v;
|
|
67
|
+
case 'contains':
|
|
68
|
+
return !tz.includes(v);
|
|
69
|
+
case 'offset':
|
|
70
|
+
return o !== parseInt(v);
|
|
71
|
+
default:
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* URL condition handler
|
|
82
|
+
* @param {*} QSL
|
|
83
|
+
*/
|
|
84
|
+
export function urlCondition(QSL) {
|
|
85
|
+
QSL.conditionHandlers.add(function(opt) {
|
|
86
|
+
if (typeof opt !== 'string' || !opt.startsWith('url:')) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const lc = window.location;
|
|
90
|
+
const hf = lc.href;
|
|
91
|
+
const pn = lc.pathname;
|
|
92
|
+
const se = lc.search;
|
|
93
|
+
const hn = lc.hostname;
|
|
94
|
+
|
|
95
|
+
const p = opt.split(':');
|
|
96
|
+
const t = p[1];
|
|
97
|
+
const v = p.slice(2).join(':');
|
|
98
|
+
|
|
99
|
+
if (!t) return true;
|
|
100
|
+
|
|
101
|
+
switch (t) {
|
|
102
|
+
case 'contains':
|
|
103
|
+
return !hf.includes(v);
|
|
104
|
+
case 'path':
|
|
105
|
+
return !pn.includes(v);
|
|
106
|
+
case 'pathStartsWith':
|
|
107
|
+
return !pn.startsWith(v);
|
|
108
|
+
case 'pathEndsWith':
|
|
109
|
+
return !pn.endsWith(v);
|
|
110
|
+
case 'query':
|
|
111
|
+
if (se) {
|
|
112
|
+
const q = new URLSearchParams(se);
|
|
113
|
+
if (v.includes('=')) {
|
|
114
|
+
const [key, val] = v.split('=');
|
|
115
|
+
return q.get(key) !== val;
|
|
116
|
+
} else {
|
|
117
|
+
return !q.has(v);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
case 'hostname':
|
|
123
|
+
return !hn.includes(v);
|
|
124
|
+
case 'matches':
|
|
125
|
+
try {
|
|
126
|
+
return !(new RegExp(v)).test(hf);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
case 'pathMatches':
|
|
131
|
+
try {
|
|
132
|
+
return !(new RegExp(v)).test(pn);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return true; // Error parsing regex, fail condition
|
|
135
|
+
}
|
|
136
|
+
default:
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* User agent condition handler
|
|
144
|
+
* @param {*} QSL
|
|
145
|
+
*/
|
|
146
|
+
export function userAgentCondition(QSL) {
|
|
147
|
+
QSL.conditionHandlers.add(function(opt) {
|
|
148
|
+
if (typeof opt !== 'string' ||
|
|
149
|
+
(!opt.startsWith('ua:') && !opt.startsWith('userAgent:'))) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const p = opt.split(':');
|
|
153
|
+
const t = p[1] || 'contains';
|
|
154
|
+
const v = p.slice(2).join(':');
|
|
155
|
+
const ua = navigator.userAgent || '';
|
|
156
|
+
const uaLower = ua.toLowerCase();
|
|
157
|
+
const vLower = v.toLowerCase();
|
|
158
|
+
|
|
159
|
+
switch (t) {
|
|
160
|
+
case 'contains':
|
|
161
|
+
if (!uaLower.includes(vLower)) return true;
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case 'equals':
|
|
165
|
+
case 'is':
|
|
166
|
+
if (ua !== v) return true;
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case 'matches':
|
|
170
|
+
// Regex pattern matching
|
|
171
|
+
try {
|
|
172
|
+
if (!(new RegExp(v, 'i')).test(ua)) return true;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return true; // Error parsing regex, fail condition
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'browser':
|
|
179
|
+
// Detect browser
|
|
180
|
+
const bMap = {
|
|
181
|
+
'chrome': /chrome/i.test(ua) && !/edg|opr/i.test(ua),
|
|
182
|
+
'firefox': /firefox/i.test(ua),
|
|
183
|
+
'safari': /safari/i.test(ua) && !/chrome|chromium|edg|opr/i.test(ua),
|
|
184
|
+
'edge': /edg/i.test(ua),
|
|
185
|
+
'opera': /opr/i.test(ua),
|
|
186
|
+
'ie': /msie|trident/i.test(ua),
|
|
187
|
+
'chromium': /chromium/i.test(ua)
|
|
188
|
+
};
|
|
189
|
+
const bKey = vLower;
|
|
190
|
+
if (!bMap[bKey]) return true;
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case 'device':
|
|
194
|
+
// Detect device type
|
|
195
|
+
const isM = /mobile|android|iphone|ipod|blackberry|iemobile|opera mini/i.test(ua);
|
|
196
|
+
const isT = /tablet|ipad|playbook|silk/i.test(ua) || (isM && /android/i.test(ua) && !/mobile/i.test(ua));
|
|
197
|
+
const isD = !isM && !isT;
|
|
198
|
+
|
|
199
|
+
switch (vLower) {
|
|
200
|
+
case 'mobile': if (!isM) return true; break;
|
|
201
|
+
case 'tablet': if (!isT) return true; break;
|
|
202
|
+
case 'desktop': if (!isD) return true; break;
|
|
203
|
+
default: return true;
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case 'os':
|
|
208
|
+
case 'platform':
|
|
209
|
+
// Detect operating system
|
|
210
|
+
const oMap = {
|
|
211
|
+
'windows': /win/i.test(ua),
|
|
212
|
+
'mac': /mac/i.test(ua),
|
|
213
|
+
'ios': /iphone|ipad|ipod/i.test(ua),
|
|
214
|
+
'android': /android/i.test(ua),
|
|
215
|
+
'linux': /linux/i.test(ua) && !/android/i.test(ua),
|
|
216
|
+
'unix': /unix/i.test(ua),
|
|
217
|
+
'chromeos': /cros/i.test(ua)
|
|
218
|
+
};
|
|
219
|
+
if (!oMap[vLower]) return true;
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
default:
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If we reach here, condition passed (none of the cases returned true)
|
|
227
|
+
return false;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default function(QSL) {
|
|
232
|
+
// Media query handler
|
|
233
|
+
mediaQueryCondition(QSL);
|
|
234
|
+
|
|
235
|
+
// Language handler
|
|
236
|
+
languageCondition(QSL);
|
|
237
|
+
|
|
238
|
+
// Timezone handler
|
|
239
|
+
timezoneCondition(QSL);
|
|
240
|
+
|
|
241
|
+
// URL handler
|
|
242
|
+
urlCondition(QSL);
|
|
243
|
+
|
|
244
|
+
// User agent handler
|
|
245
|
+
userAgentCondition(QSL);
|
|
246
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle dynamic flow assignment when resources are added after load has started.
|
|
3
|
+
* @param {*} QSL
|
|
4
|
+
*/
|
|
5
|
+
export default function(QSL) {
|
|
6
|
+
/**
|
|
7
|
+
* Handle dynamic flow assignment when resources are added after load has started.
|
|
8
|
+
*/
|
|
9
|
+
QSL.addProcessFilters.add(function(flowId, config) {
|
|
10
|
+
if (this.hasStarted && !flowId) {
|
|
11
|
+
flowId = 'dynamic-' + Math.random().toString(36).slice(2);
|
|
12
|
+
config.paused = true;
|
|
13
|
+
}
|
|
14
|
+
return [flowId, config];
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Filter out dynamic flows from initial processing.
|
|
19
|
+
*/
|
|
20
|
+
QSL.flowIdFilters.add(function(flowIds) {
|
|
21
|
+
return flowIds.filter(fid => !fid.includes('dynamic'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Process dynamic flows after all regular flows complete.
|
|
26
|
+
* Checks if all non-dynamic flows are completed, then processes dynamic flows.
|
|
27
|
+
*/
|
|
28
|
+
QSL.completedFlowsActions.add(function(flowsDone, flows, flowOptions) {
|
|
29
|
+
if (!flowsDone || !flows || !flowOptions) return true;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if all non-dynamic flows are completed
|
|
33
|
+
*/
|
|
34
|
+
const nonDynamicFlowsDone = [...flowOptions.entries()]
|
|
35
|
+
.filter(([fid]) => !fid.includes('dynamic'))
|
|
36
|
+
.every(([, opt]) => opt.status === this.FLOW_STATE.COMPLETED);
|
|
37
|
+
|
|
38
|
+
if (!nonDynamicFlowsDone) return false;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Process dynamic flows
|
|
42
|
+
*/
|
|
43
|
+
const dynamicFlowIds = [...flows.keys()].filter(fid => fid.includes('dynamic'));
|
|
44
|
+
if (dynamicFlowIds.length === 0) return true;
|
|
45
|
+
|
|
46
|
+
for (const dynamicFlowId of dynamicFlowIds) {
|
|
47
|
+
const dynamicOptions = flowOptions.get(dynamicFlowId);
|
|
48
|
+
|
|
49
|
+
if (dynamicOptions && dynamicOptions.status !== this.FLOW_STATE.COMPLETED) {
|
|
50
|
+
if (dynamicOptions.status === this.FLOW_STATE.READY) {
|
|
51
|
+
this.setFlowOptions({ status: this.FLOW_STATE.RUNNING }, dynamicFlowId);
|
|
52
|
+
this.runFlow(dynamicFlowId);
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track events when a process completes.
|
|
3
|
+
* @param {*} QSL
|
|
4
|
+
*/
|
|
5
|
+
export default function(QSL) {
|
|
6
|
+
const customEvents = new Map();
|
|
7
|
+
const processElementMap = new WeakMap();
|
|
8
|
+
let documentListener = null;
|
|
9
|
+
let windowListener = null;
|
|
10
|
+
let isIntercepting = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize script URL for comparison (remove protocol, domain, query params, hash).
|
|
14
|
+
* @param {string} url - Script URL or path
|
|
15
|
+
* @returns {string} - Normalized path
|
|
16
|
+
*/
|
|
17
|
+
const normalizeScriptPath = (u) => {
|
|
18
|
+
if (!u) return '';
|
|
19
|
+
try {
|
|
20
|
+
return u.includes('://') ? new URL(u).pathname : u.split('?')[0].split('#')[0];
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return u.split('?')[0].split('#')[0];
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize event interception when QSL loads.
|
|
28
|
+
*/
|
|
29
|
+
QSL.loadActions.add(function() {
|
|
30
|
+
if (isIntercepting) return;
|
|
31
|
+
|
|
32
|
+
documentListener = document.addEventListener;
|
|
33
|
+
windowListener = window.addEventListener;
|
|
34
|
+
isIntercepting = true;
|
|
35
|
+
|
|
36
|
+
const iterator = (type, eventType, changeEventName) => {
|
|
37
|
+
let currentScriptId = null;
|
|
38
|
+
let targetFlowId = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Priority 1: Check document.currentScript for current process ID and flow ID.
|
|
42
|
+
*/
|
|
43
|
+
if (document.currentScript) {
|
|
44
|
+
const processElement = processElementMap.get(document.currentScript);
|
|
45
|
+
if (processElement) {
|
|
46
|
+
currentScriptId = processElement.processId;
|
|
47
|
+
targetFlowId = processElement.flowId;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Priority 2: Check currentProcessPerFlow map for current process ID and flow ID.
|
|
53
|
+
*/
|
|
54
|
+
if (!currentScriptId && this.currentProcessPerFlow.size) {
|
|
55
|
+
for (const [flowId, processId] of this.currentProcessPerFlow) {
|
|
56
|
+
if (processId) {
|
|
57
|
+
currentScriptId = processId;
|
|
58
|
+
targetFlowId = flowId;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Priority 3: Use Error stack trace only when currentScript is null.
|
|
66
|
+
*/
|
|
67
|
+
if (!currentScriptId) {
|
|
68
|
+
const stack = new Error().stack;
|
|
69
|
+
if (stack) {
|
|
70
|
+
const lines = stack.split('\n');
|
|
71
|
+
// Skip first 2 lines (Error and iterator function)
|
|
72
|
+
for (let i = 2; i < lines.length; i++) {
|
|
73
|
+
const match = lines[i].match(/([^()\s]+\.js(?:\?[^:)]*)?):\d+(?::\d+)?/);
|
|
74
|
+
if (match) {
|
|
75
|
+
const file = match[1];
|
|
76
|
+
const qIndex = file.indexOf('?');
|
|
77
|
+
const stackFile = qIndex === -1 ? file.trim() : file.slice(0, qIndex).trim();
|
|
78
|
+
|
|
79
|
+
if (stackFile) {
|
|
80
|
+
// Normalize: remove query/hash, extract pathname from URL
|
|
81
|
+
const qIdx = stackFile.indexOf('?');
|
|
82
|
+
const hIdx = stackFile.indexOf('#');
|
|
83
|
+
const endIdx = qIdx === -1 ? (hIdx === -1 ? stackFile.length : hIdx) : (hIdx === -1 ? qIdx : Math.min(qIdx, hIdx));
|
|
84
|
+
let normalizedFile = stackFile.slice(0, endIdx);
|
|
85
|
+
try {
|
|
86
|
+
if (normalizedFile.includes('://')) {
|
|
87
|
+
normalizedFile = new URL(normalizedFile).pathname;
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {}
|
|
90
|
+
const lastSlash = normalizedFile.lastIndexOf('/');
|
|
91
|
+
const fileName = lastSlash === -1 ? normalizedFile : normalizedFile.slice(lastSlash + 1);
|
|
92
|
+
|
|
93
|
+
for (const [flowId, processes] of this.flows) {
|
|
94
|
+
for (const process of processes) {
|
|
95
|
+
if (process.src && process.type === 'script') {
|
|
96
|
+
const normalizedSrc = normalizeScriptPath(process.src);
|
|
97
|
+
const srcLastSlash = normalizedSrc.lastIndexOf('/');
|
|
98
|
+
const srcFileName = srcLastSlash === -1 ? normalizedSrc : normalizedSrc.slice(srcLastSlash + 1);
|
|
99
|
+
|
|
100
|
+
if (normalizedFile === normalizedSrc || (fileName && fileName === srcFileName)) {
|
|
101
|
+
currentScriptId = process.id;
|
|
102
|
+
targetFlowId = flowId;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (currentScriptId) break;
|
|
108
|
+
}
|
|
109
|
+
if (currentScriptId) break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* If current script ID and flow ID are found, track event.
|
|
118
|
+
* Only change event name if event already fired (changeEventName = true).
|
|
119
|
+
*/
|
|
120
|
+
if (currentScriptId && targetFlowId) {
|
|
121
|
+
const flow = this.flows.get(targetFlowId);
|
|
122
|
+
const process = flow?.find(p => p.id === currentScriptId || p.id === this.PREFIX + currentScriptId);
|
|
123
|
+
|
|
124
|
+
if (process) {
|
|
125
|
+
const shouldFireEvents = process.fireEvents !== false && (this.flowOptions.get(targetFlowId)?.fireEvents !== false);
|
|
126
|
+
if (shouldFireEvents) {
|
|
127
|
+
const eventName = `${type}:${process.id}`;
|
|
128
|
+
if (!customEvents.has(process.id)) customEvents.set(process.id, []);
|
|
129
|
+
const events = customEvents.get(process.id);
|
|
130
|
+
if (!events.some(e => e.name === eventName)) events.push({ type: eventType, name: eventName });
|
|
131
|
+
if (changeEventName) type = eventName;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return type;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
document.addEventListener = (type, listener, opts) => {
|
|
139
|
+
if (type === 'DOMContentLoaded' && this.LIFECYCLE.DOMREADY) {
|
|
140
|
+
const trackedType = iterator.call(this, type, this.EVENTS.DOMREADY, true);
|
|
141
|
+
if (trackedType !== type) {
|
|
142
|
+
type = trackedType;
|
|
143
|
+
queueMicrotask(() => document.dispatchEvent(new Event(trackedType)));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return documentListener.call(document, type, listener, opts);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
window.addEventListener = (type, listener, opts) => {
|
|
150
|
+
if (type === 'load' && this.LIFECYCLE.LOADED) {
|
|
151
|
+
const trackedType = iterator.call(this, type, this.EVENTS.LOADED, true);
|
|
152
|
+
if (trackedType !== type) {
|
|
153
|
+
type = trackedType;
|
|
154
|
+
queueMicrotask(() => window.dispatchEvent(new Event(trackedType)));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return windowListener.call(window, type, listener, opts);
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Provide handler callbacks for process execution.
|
|
163
|
+
* Register process elements in processElementMap.
|
|
164
|
+
*/
|
|
165
|
+
QSL.handlerCallbacksFilters.add(function(process) {
|
|
166
|
+
return {
|
|
167
|
+
registerProcessElement: (el, config) => {
|
|
168
|
+
processElementMap.set(el, { flowId: config.flowId, processId: config.id });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fire tracked events when a process completes.
|
|
175
|
+
* Use processCompleteActions instead of patching execute.
|
|
176
|
+
*/
|
|
177
|
+
QSL.processCompleteActions.add(function(process) {
|
|
178
|
+
const events = customEvents.get(process.id);
|
|
179
|
+
if (events && Array.isArray(events)) {
|
|
180
|
+
for (const event of events) {
|
|
181
|
+
if (event.type === this.EVENTS.DOMREADY) {
|
|
182
|
+
if (this.LIFECYCLE.DOMREADY) {
|
|
183
|
+
document.dispatchEvent(new Event(event.name));
|
|
184
|
+
} else {
|
|
185
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
186
|
+
document.dispatchEvent(new Event(event.name));
|
|
187
|
+
}, { once: true });
|
|
188
|
+
}
|
|
189
|
+
} else if (event.type === this.EVENTS.LOADED) {
|
|
190
|
+
if (this.LIFECYCLE.LOADED) {
|
|
191
|
+
window.dispatchEvent(new Event(event.name));
|
|
192
|
+
} else {
|
|
193
|
+
window.addEventListener('load', () => {
|
|
194
|
+
window.dispatchEvent(new Event(event.name));
|
|
195
|
+
}, { once: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Cleanup: restore original addEventListener functions on reset.
|
|
204
|
+
* Use resetActions instead of patching reset.
|
|
205
|
+
*/
|
|
206
|
+
QSL.resetActions.add(function() {
|
|
207
|
+
if (isIntercepting && documentListener && windowListener) {
|
|
208
|
+
document.addEventListener = documentListener;
|
|
209
|
+
window.addEventListener = windowListener;
|
|
210
|
+
isIntercepting = false;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Store custom events map.
|
|
216
|
+
*/
|
|
217
|
+
QSL.customEvents = customEvents;
|
|
218
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export default function(QSL) {
|
|
2
|
+
|
|
3
|
+
QSL.initActions.add(function() {
|
|
4
|
+
|
|
5
|
+
QSL.logger = {
|
|
6
|
+
VERSION: 'qsl-logger',
|
|
7
|
+
LOG: {
|
|
8
|
+
LOGGER_LOADED: '[QSL] Logger loaded',
|
|
9
|
+
LOGGER_LOAD_ERROR: '[QSL] Logger load error:',
|
|
10
|
+
UNKNOWN_TYPE: '[QSL] Unknown type:',
|
|
11
|
+
PROCESS_STARTED: '[QSL] Process started:',
|
|
12
|
+
PROCESS_COMPLETED: '[QSL] Process completed:',
|
|
13
|
+
PROCESS_FAILED: '[QSL] Process failed:',
|
|
14
|
+
STYLESHEET_STARTED: '[QSL] Stylesheet loading:',
|
|
15
|
+
STYLESHEET_LOADED: '[QSL] Stylesheet loaded:',
|
|
16
|
+
STYLESHEET_FAILED: '[QSL] Stylesheet failed to load:',
|
|
17
|
+
INLINE_SCRIPT_STARTED: '[QSL] Inline script loading:',
|
|
18
|
+
INLINE_SCRIPT_SUCCESS: '[QSL] Inline script loaded:',
|
|
19
|
+
INLINE_SCRIPT_ERROR: '[QSL] Inline script load error:',
|
|
20
|
+
INLINE_STYLE_STARTED: '[QSL] Inline style loading:',
|
|
21
|
+
INLINE_STYLE_SUCCESS: '[QSL] Inline style loaded:',
|
|
22
|
+
INLINE_STYLE_ERROR: '[QSL] Inline style load error:',
|
|
23
|
+
IMAGE_STARTED: '[QSL] Image pixel loading:',
|
|
24
|
+
IMAGE_LOADED: '[QSL] Image pixel loaded:',
|
|
25
|
+
IMAGE_FAILED: '[QSL] Image pixel failed:',
|
|
26
|
+
SHADOW_STARTED: '[QSL] Shadow element loading:',
|
|
27
|
+
SHADOW_SUCCESS: '[QSL] Shadow element loaded:',
|
|
28
|
+
SHADOW_FAILED: '[QSL] Shadow element failed:',
|
|
29
|
+
HTML_STARTED: '[QSL] HTML element loading:',
|
|
30
|
+
HTML_SUCCESS: '[QSL] HTML element loaded:',
|
|
31
|
+
HTML_FAILED: '[QSL] HTML element failed:',
|
|
32
|
+
PRELOAD_ERROR: '[QSL] Preload error:',
|
|
33
|
+
FLOW_DEP_SKIPPED: '[QSL] Flow dependency missed:',
|
|
34
|
+
DEP_NOT_FOUND: '[QSL] Dependency not found:',
|
|
35
|
+
RESET: '[QSL] Global reset',
|
|
36
|
+
ALL_COMPLETED: '[QSL] Loading is completed',
|
|
37
|
+
},
|
|
38
|
+
log(type, ...args) {
|
|
39
|
+
if (this.LOG[type]) {
|
|
40
|
+
console.log(this.LOG[type], ...args, { timestamp: Date.now() });
|
|
41
|
+
} else {
|
|
42
|
+
console.log('[QSL] ' + type, ...args);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
error(type, ...args) {
|
|
46
|
+
if (this.LOG[type]) {
|
|
47
|
+
console.error(this.LOG[type], ...args);
|
|
48
|
+
} else {
|
|
49
|
+
console.error('[QSL] ' + type, ...args);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
}
|