@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-09-20
|
|
10
|
+
|
|
11
|
+
First public release. The runtime is extracted from a private codebase, so
|
|
12
|
+
this entry records the differences from that internal version rather than a
|
|
13
|
+
list of new features.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`media:` trigger never worked.** The handler referenced an undeclared
|
|
18
|
+
variable instead of the query string. Because `typeof` on an undeclared
|
|
19
|
+
identifier is safe, it silently took the "skip" branch every time rather than
|
|
20
|
+
throwing. The query is now read from the option and passed to `matchMedia`.
|
|
21
|
+
- **`visible:` and `appears:` triggers hung the run.** Both listened for DOM
|
|
22
|
+
events named `intersection` and `mutation`, which do not exist. When the
|
|
23
|
+
target element was present, the callback never fired, the flow stayed paused
|
|
24
|
+
and the load never completed. They now use `IntersectionObserver` and
|
|
25
|
+
`MutationObserver`. `appears:` also waits for elements that are inserted
|
|
26
|
+
later, instead of firing immediately when the selector does not match yet.
|
|
27
|
+
- **Selectors containing a colon were truncated.** `hover:`, `visible:` and
|
|
28
|
+
`appears:` parsed their argument with `split(':')[1]`, which broke
|
|
29
|
+
`hover:.btn:first-child` and any media query. Arguments are now read by
|
|
30
|
+
prefix length.
|
|
31
|
+
- **`domready` trigger could hang.** It only fired immediately when
|
|
32
|
+
`readyState` was `complete`; at `interactive`, `DOMContentLoaded` had already
|
|
33
|
+
been dispatched, so the listener it added never ran.
|
|
34
|
+
- **`shadow` type threw when its container was missing.** A local
|
|
35
|
+
`const error = new Error(...)` shadowed the module-level `error()` helper, so
|
|
36
|
+
the error path raised a `TypeError` instead of reporting the failure. A
|
|
37
|
+
missing or absent `container` now resolves with an `Error`.
|
|
38
|
+
- **`fire('SKIPPED', ...)` was a silent no-op.** There was no `SKIPPED` entry in
|
|
39
|
+
the event map. Added as `QSL:skipped`.
|
|
40
|
+
- **`registerTypes()` rejected the object form** documented in its JSDoc, and
|
|
41
|
+
both `registerType` and `registerTypes` returned `undefined` on invalid input,
|
|
42
|
+
breaking the chaining contract. The same applied to `runFlow`, `pauseGroup`
|
|
43
|
+
and `runGroup`.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **`reset()` no longer clears plugin registrations.** It previously wiped
|
|
48
|
+
registered types, condition handlers, trigger handlers and every lifecycle
|
|
49
|
+
hook along with the run state. Since `reset()` runs automatically once all
|
|
50
|
+
flows complete, the runtime was effectively dead for any process added
|
|
51
|
+
afterwards. `reset()` now clears run state only; the new `destroy()` performs
|
|
52
|
+
the full teardown.
|
|
53
|
+
- **`reset()` no longer skips its work when a logger is set.** Debugging
|
|
54
|
+
behaviour is controlled by the new `autoReset` property instead.
|
|
55
|
+
- Identifiers interpolated into the `inline-script` module wrapper are
|
|
56
|
+
serialised with `JSON.stringify`, so a hostile `flowId` or process id cannot
|
|
57
|
+
break out of its string literal.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- `destroy()` for a full teardown, and `autoReset` to keep flow state after a
|
|
62
|
+
run.
|
|
63
|
+
- Vitest test suite covering the fixes above.
|
|
64
|
+
- Three published builds: ESM library, full browser bundle, slim browser
|
|
65
|
+
bundle.
|
|
66
|
+
|
|
67
|
+
### Removed
|
|
68
|
+
|
|
69
|
+
- The service-specific build pipeline: manifest generation, CDN deployment and
|
|
70
|
+
the internal bundle-composition map. Those stay in the private repository;
|
|
71
|
+
this one ships only the runtime.
|
|
72
|
+
|
|
73
|
+
[Unreleased]: https://github.com/Quietsapa/qsl/compare/v0.1.0...HEAD
|
|
74
|
+
[0.1.0]: https://github.com/Quietsapa/qsl/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
10
|
+
|
|
11
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
12
|
+
|
|
13
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
14
|
+
|
|
15
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
16
|
+
|
|
17
|
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
18
|
+
|
|
19
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
20
|
+
|
|
21
|
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
22
|
+
|
|
23
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
24
|
+
|
|
25
|
+
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
26
|
+
|
|
27
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
28
|
+
|
|
29
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
30
|
+
|
|
31
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
32
|
+
|
|
33
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
34
|
+
|
|
35
|
+
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
36
|
+
|
|
37
|
+
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
38
|
+
|
|
39
|
+
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
40
|
+
|
|
41
|
+
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
|
42
|
+
|
|
43
|
+
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
44
|
+
|
|
45
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
46
|
+
|
|
47
|
+
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
48
|
+
|
|
49
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
50
|
+
|
|
51
|
+
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
52
|
+
|
|
53
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
54
|
+
|
|
55
|
+
END OF TERMS AND CONDITIONS
|
|
56
|
+
|
|
57
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
58
|
+
|
|
59
|
+
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
|
60
|
+
|
|
61
|
+
Copyright [yyyy] [name of copyright owner]
|
|
62
|
+
|
|
63
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
64
|
+
you may not use this file except in compliance with the License.
|
|
65
|
+
You may obtain a copy of the License at
|
|
66
|
+
|
|
67
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
68
|
+
|
|
69
|
+
Unless required by applicable law or agreed to in writing, software
|
|
70
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
71
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
72
|
+
See the License for the specific language governing permissions and
|
|
73
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
QSL (@quietsapa/qsl)
|
|
2
|
+
Copyright 2026 Quietsapa
|
|
3
|
+
|
|
4
|
+
This product includes software developed as part of the Quietsapa project.
|
|
5
|
+
|
|
6
|
+
"Quietsapa" and "QSL" are names used by the Quietsapa project. This license
|
|
7
|
+
does not grant permission to use them, except as required for describing the
|
|
8
|
+
origin of the work (see section 6 of the License).
|
|
9
|
+
|
|
10
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
you may not use this file except in compliance with the License.
|
|
12
|
+
You may obtain a copy of the License at
|
|
13
|
+
|
|
14
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
15
|
+
|
|
16
|
+
Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
See the License for the specific language governing permissions and
|
|
20
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# QSL
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Quietsapa/qsl/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@quietsapa/qsl)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A dependency-aware orchestration runtime for everything a page loads besides its
|
|
8
|
+
own code: analytics tags, chat widgets, A/B testing snippets, tracking pixels,
|
|
9
|
+
stylesheets and custom elements.
|
|
10
|
+
|
|
11
|
+
Instead of scattering `<script>` tags through a template and hoping the order
|
|
12
|
+
works out, you describe what should load, when it should load, and what it
|
|
13
|
+
depends on. QSL resolves the graph and runs it.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
core
|
|
17
|
+
.add({ id: 'analytics', type: 'script', src: '/a.js' }, 'metrics')
|
|
18
|
+
.add({ id: 'heatmap', type: 'script', src: '/h.js', depends: ['analytics'], trigger: 'idle' }, 'metrics')
|
|
19
|
+
.add({ id: 'chat', type: 'script', src: '/c.js', trigger: 'visible:#footer' }, 'widgets');
|
|
20
|
+
|
|
21
|
+
core.setFlowOptions({ ordered: true }, 'metrics');
|
|
22
|
+
|
|
23
|
+
await core.load();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It is deliberately small, has no runtime dependencies, and does not talk to any
|
|
27
|
+
server. Configuration comes from wherever you want: a static object, your CMS,
|
|
28
|
+
or an API.
|
|
29
|
+
|
|
30
|
+
## Status
|
|
31
|
+
|
|
32
|
+
Pre-1.0. The API described here is what ships today, but it may still change
|
|
33
|
+
between minor versions. Pin an exact version if that matters to you.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm install @quietsapa/qsl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or load a prebuilt bundle straight from a CDN:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!-- everything: all types, conditions, triggers, logging -->
|
|
45
|
+
<script src="https://cdn.jsdelivr.net/npm/@quietsapa/qsl/dist/qsl.min.js"></script>
|
|
46
|
+
|
|
47
|
+
<!-- just ordered script loading -->
|
|
48
|
+
<script src="https://cdn.jsdelivr.net/npm/@quietsapa/qsl/dist/qsl.slim.min.js"></script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Both browser bundles register their types and call `init()` for you, then
|
|
52
|
+
expose the instance as `window.__QSL__`.
|
|
53
|
+
|
|
54
|
+
| Build | Entry | Size (gzip) | Contents |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `dist/qsl.mjs` | `src/index.js` | — | ESM, nothing registered, nothing started |
|
|
57
|
+
| `dist/qsl.min.js` | `src/presets/full.js` | ~8.3 kB | All types, conditions, triggers, logger, events |
|
|
58
|
+
| `dist/qsl.slim.min.js` | `src/presets/default.js` | ~4.4 kB | The `script` type only |
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
The ESM entry is side-effect free: importing it registers nothing. You compose
|
|
63
|
+
what you need and call `init()` yourself.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import core, { Script, Stylesheet, triggers, conditions } from '@quietsapa/qsl';
|
|
67
|
+
|
|
68
|
+
await core
|
|
69
|
+
.registerTypes([Script, Stylesheet])
|
|
70
|
+
.use(triggers)
|
|
71
|
+
.use(conditions)
|
|
72
|
+
.init();
|
|
73
|
+
|
|
74
|
+
core.add({ id: 'gtm', type: 'script', src: 'https://example.com/gtm.js' });
|
|
75
|
+
core.add({ id: 'widget', type: 'script', src: '/widget.js', trigger: 'interaction' });
|
|
76
|
+
|
|
77
|
+
await core.load();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
With a CDN bundle the registration is already done:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<script src="https://cdn.jsdelivr.net/npm/@quietsapa/qsl/dist/qsl.min.js"></script>
|
|
84
|
+
<script>
|
|
85
|
+
var qsl = window.__QSL__;
|
|
86
|
+
qsl.add({ id: 'gtm', type: 'script', src: 'https://example.com/gtm.js' });
|
|
87
|
+
qsl.load();
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Concepts
|
|
92
|
+
|
|
93
|
+
**Process** — one thing to load: a script, a stylesheet, a pixel, an element.
|
|
94
|
+
Described by a plain object whose `type` selects the handler.
|
|
95
|
+
|
|
96
|
+
**Flow** — a named group of processes. Flows run in parallel with each other.
|
|
97
|
+
Inside a flow, processes run in parallel too, unless the flow is `ordered`, in
|
|
98
|
+
which case each one waits for the previous to finish.
|
|
99
|
+
|
|
100
|
+
Two flow names are special: `default` is where processes go when you pass no
|
|
101
|
+
flow, and `ordered` is a ready-made sequential flow. `core.add(config, true)`
|
|
102
|
+
is shorthand for the latter.
|
|
103
|
+
|
|
104
|
+
**Trigger** — when a flow or process is allowed to start. Until the trigger
|
|
105
|
+
fires, it waits.
|
|
106
|
+
|
|
107
|
+
**Condition** — whether it should run at all. A failing condition skips it.
|
|
108
|
+
|
|
109
|
+
**Dependency** — `depends` makes a process or flow wait for others to finish.
|
|
110
|
+
Circular dependencies are detected by the `circ` plugin and broken rather than
|
|
111
|
+
deadlocking; a dependency that does not exist is logged and ignored.
|
|
112
|
+
|
|
113
|
+
## API
|
|
114
|
+
|
|
115
|
+
### `init()`
|
|
116
|
+
|
|
117
|
+
Registers internal listeners and runs `initActions` from plugins. Returns a
|
|
118
|
+
promise resolving to the instance. Safe to call more than once.
|
|
119
|
+
|
|
120
|
+
If the script element that loaded QSL has `?async=true` in its URL, `init()`
|
|
121
|
+
also calls `window.QSLReady()` when it finishes, or a different global named by
|
|
122
|
+
the `callback` query parameter.
|
|
123
|
+
|
|
124
|
+
### `add(config, flowId = null)`
|
|
125
|
+
|
|
126
|
+
Adds a process. `flowId` may be a string, `true` (the `ordered` flow), or
|
|
127
|
+
omitted (the `default` flow). Returns the instance.
|
|
128
|
+
|
|
129
|
+
`config.id` is optional; a random one is generated. Note that ids are stored
|
|
130
|
+
with a `qsl-` prefix internally, but `depends` is written with the unprefixed id
|
|
131
|
+
you passed.
|
|
132
|
+
|
|
133
|
+
### `load(options = {})`
|
|
134
|
+
|
|
135
|
+
Starts everything. Returns a promise that resolves once every flow has
|
|
136
|
+
completed. `options.between` sets a default delay in milliseconds between
|
|
137
|
+
consecutive processes.
|
|
138
|
+
|
|
139
|
+
Calling `load()` a second time while a run is in progress does nothing.
|
|
140
|
+
|
|
141
|
+
### `setFlowOptions(options, flowId = null)`
|
|
142
|
+
|
|
143
|
+
Sets options on a flow, creating it if needed. Merges with previous options.
|
|
144
|
+
|
|
145
|
+
| Option | Default | Meaning |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `ordered` | `false` | Run processes sequentially |
|
|
148
|
+
| `delay` | `0` | Milliseconds to wait before the flow starts |
|
|
149
|
+
| `between` | `null` | Milliseconds between consecutive processes |
|
|
150
|
+
| `priority` | `0` | Higher runs earlier; flows with a trigger always go last |
|
|
151
|
+
| `trigger` | `null` | See [Triggers](#triggers) |
|
|
152
|
+
| `condition` | `null` | See [Conditions](#conditions) |
|
|
153
|
+
| `depends` | `[]` | Flow ids to wait for. Setting this pauses the flow |
|
|
154
|
+
| `group` | `null` | Group name for `pauseGroup` / `runGroup` |
|
|
155
|
+
| `paused` | `false` | Hold the flow until `runFlow()` |
|
|
156
|
+
| `preload` | `false` | Emit `<link rel=preload>` for scripts and styles |
|
|
157
|
+
| `fireEvents` | `true` | Let the `events` plugin re-dispatch lifecycle events |
|
|
158
|
+
|
|
159
|
+
### Other methods
|
|
160
|
+
|
|
161
|
+
- `use(plugin, ...args)` — run a plugin function against the instance.
|
|
162
|
+
- `registerType(type, handler)` / `registerTypes(list)` — add resource types.
|
|
163
|
+
`registerTypes` accepts `[{ type, handler }]` or `{ type: handler }`.
|
|
164
|
+
- `runFlow(flowId, withTrigger = false)` — start a paused flow.
|
|
165
|
+
- `pauseGroup(name)` / `runGroup(name)` — act on every flow in a group.
|
|
166
|
+
- `setLogger(logger)` — supply an object with `log()` and `error()`.
|
|
167
|
+
- `setOnAllComplete(fn)` — callback for the end of a run.
|
|
168
|
+
- `useEvents()` — enable DOM lifecycle events.
|
|
169
|
+
- `reset()` — clear run state. Registered types and plugin handlers survive, so
|
|
170
|
+
a later `add()` and `load()` behave the same as the first run.
|
|
171
|
+
- `destroy()` — clear everything, including types and plugins. `init()` must be
|
|
172
|
+
called again afterwards.
|
|
173
|
+
- `autoReset` — set to `false` to keep flow state after a run, for debugging.
|
|
174
|
+
|
|
175
|
+
## Types
|
|
176
|
+
|
|
177
|
+
Every type is a `{ type, handler }` pair. Handlers receive the process config
|
|
178
|
+
and return a promise.
|
|
179
|
+
|
|
180
|
+
| Type | Key fields |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `script` | `src`, `module`, `async`, `defer`, `crossOrigin`, `integrity`, `bypassCache` |
|
|
183
|
+
| `inline-script` | `code`, `module` |
|
|
184
|
+
| `stylesheet` | `href`, `crossOrigin`, `bypassCache` |
|
|
185
|
+
| `style` | `code` |
|
|
186
|
+
| `pixel` | `src`, `dom`, `style` |
|
|
187
|
+
| `html` | `tag`, `html`, `id`, `className`, `style` |
|
|
188
|
+
| `shadow` | `tag`, `shadowData: { container, position, hidden }` |
|
|
189
|
+
| `console` | `message` — built in, used as the default type |
|
|
190
|
+
|
|
191
|
+
Common fields across types: `id`, `type`, `depends`, `condition`, `trigger`,
|
|
192
|
+
`priority`, `delay`, `data` (rendered as `data-*` attributes), `footer` (append
|
|
193
|
+
to `<body>` instead of `<head>`), and the callbacks `onBeforeStart`,
|
|
194
|
+
`onComplete`, `onError`.
|
|
195
|
+
|
|
196
|
+
Writing your own is a function returning a promise:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
core.registerType('json', (process) =>
|
|
200
|
+
fetch(process.url)
|
|
201
|
+
.then((r) => r.json())
|
|
202
|
+
.then(process.onComplete)
|
|
203
|
+
);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Triggers
|
|
207
|
+
|
|
208
|
+
Set `trigger` on a process or a flow.
|
|
209
|
+
|
|
210
|
+
| Trigger | Fires when |
|
|
211
|
+
| --- | --- |
|
|
212
|
+
| `'load'` | The window `load` event |
|
|
213
|
+
| `'domready'` | `DOMContentLoaded`, or immediately if it already happened |
|
|
214
|
+
| `'idle'` | `requestIdleCallback`, falling back to a 200 ms timeout |
|
|
215
|
+
| `'interaction'` or `true` | First click, keydown, wheel, mousedown, mousemove or touchstart |
|
|
216
|
+
| `'delay:2000'` | After the given number of milliseconds |
|
|
217
|
+
| `'hover:<selector>'` | Pointer enters the element |
|
|
218
|
+
| `'visible:<selector>'` | Element intersects the viewport (`IntersectionObserver`) |
|
|
219
|
+
| `'appears:<selector>'` | Element is inserted into the DOM (`MutationObserver`) |
|
|
220
|
+
| `'media:<query>'` | Media query matches, now or later |
|
|
221
|
+
| a function | You call the callback it receives |
|
|
222
|
+
|
|
223
|
+
Selectors and queries may contain colons; they are parsed by prefix length, not
|
|
224
|
+
by splitting.
|
|
225
|
+
|
|
226
|
+
Combine them with an array (all must fire) or an operator object:
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
{ trigger: ['domready', 'delay:1000'] }
|
|
230
|
+
{ trigger: { operator: 'or', triggers: ['idle', 'interaction'] } }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Conditions
|
|
234
|
+
|
|
235
|
+
Set `condition` on a process or a flow. A failing condition skips it.
|
|
236
|
+
|
|
237
|
+
| Condition | Example |
|
|
238
|
+
| --- | --- |
|
|
239
|
+
| `media:<query>` | `media:(min-width: 768px)` |
|
|
240
|
+
| `lang:<op>:<value>` | `lang:startsWith:ru`, `lang:in:en-US,en-GB` |
|
|
241
|
+
| `tz:<op>:<value>` | `tz:contains:Europe`, `tz:offset:3` |
|
|
242
|
+
| `url:<op>:<value>` | `url:pathStartsWith:/blog`, `url:query:utm_source=ads` |
|
|
243
|
+
| `ua:<op>:<value>` | `ua:device:mobile`, `ua:browser:safari`, `ua:os:ios` |
|
|
244
|
+
| a function | Return `true` to run, `false` to skip |
|
|
245
|
+
| a boolean | `false` skips |
|
|
246
|
+
|
|
247
|
+
Operators: `equals`/`is`, `contains`, `startsWith`, `in` for language;
|
|
248
|
+
`equals`/`is`, `contains`, `offset` for timezone; `contains`, `path`,
|
|
249
|
+
`pathStartsWith`, `pathEndsWith`, `query`, `hostname`, `matches`, `pathMatches`
|
|
250
|
+
for URL; `contains`, `equals`/`is`, `matches`, `browser`, `device`,
|
|
251
|
+
`os`/`platform` for user agent.
|
|
252
|
+
|
|
253
|
+
Combine with an array (all must pass) or an operator object:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
{ condition: ['ua:device:desktop', 'url:pathStartsWith:/app'] }
|
|
257
|
+
{ condition: { operator: 'or', conditions: ['lang:is:ru-RU', 'tz:contains:Europe'] } }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
An unparseable regular expression fails the condition rather than throwing.
|
|
261
|
+
|
|
262
|
+
## Plugins
|
|
263
|
+
|
|
264
|
+
A plugin is a function that receives the instance and registers handlers on it.
|
|
265
|
+
|
|
266
|
+
| Plugin | What it adds |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `conditions` | The five condition handlers above |
|
|
269
|
+
| `triggers` | The eight trigger handlers above |
|
|
270
|
+
| `logger` | A console logger with readable message names |
|
|
271
|
+
| `events` | Re-dispatches `DOMContentLoaded` and `load` per process, so late-loaded third-party scripts that listen for them still initialise |
|
|
272
|
+
| `circ` | Detects circular dependencies and breaks them |
|
|
273
|
+
| `dynamic` | Defers processes added after `load()` into their own flows |
|
|
274
|
+
| `simple-events` | Re-dispatches `DOMContentLoaded` and `load` globally once everything completes |
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
import core, { triggers, conditions, logger } from '@quietsapa/qsl';
|
|
278
|
+
|
|
279
|
+
core.use(triggers).use(conditions).use(logger);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Events
|
|
283
|
+
|
|
284
|
+
Call `useEvents()` to enable them. Each carries the process config as `detail`.
|
|
285
|
+
|
|
286
|
+
`QSL:started`, `QSL:completed`, `QSL:error`, `QSL:skipped`,
|
|
287
|
+
`QSL:all:completed`.
|
|
288
|
+
|
|
289
|
+
## Security
|
|
290
|
+
|
|
291
|
+
**QSL treats its configuration as trusted, privileged input.** The
|
|
292
|
+
`inline-script` type executes code, the `html` type writes to `innerHTML`, and
|
|
293
|
+
several conditions compile regular expressions from strings. Never build a
|
|
294
|
+
configuration from untrusted input.
|
|
295
|
+
|
|
296
|
+
Read [SECURITY.md](SECURITY.md) before deploying. It covers the trust model,
|
|
297
|
+
Content Security Policy, and how to report a vulnerability.
|
|
298
|
+
|
|
299
|
+
## Development
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
npm install
|
|
303
|
+
npm test # vitest + happy-dom
|
|
304
|
+
npm run build # three bundles into dist/
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`npm run check:version` guards against `VERSION` in `src/core.js` drifting away
|
|
308
|
+
from `package.json`.
|
|
309
|
+
|
|
310
|
+
## Contributing
|
|
311
|
+
|
|
312
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports with a reproduction are the
|
|
313
|
+
most useful thing you can send.
|
|
314
|
+
|
|
315
|
+
## License
|
|
316
|
+
|
|
317
|
+
Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
package/dist/qsl.min.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(){"use strict";const e={VERSION:"0.1.0",PREFIX:"qsl-",FLOW_TYPE:{DEFAULT:"default",ORDERED:"ordered"},FLOW_STATE:{READY:"READY",RUNNING:"RUNNING",COMPLETED:"COMPLETED"},FLOW_OPTIONS:{delay:0,priority:0,between:null,trigger:null,condition:null,beforeStart:null,onComplete:null,group:null,paused:!1,preload:!1,fireEvents:!0,depends:[]},EVENTS:{STARTED:"QSL:started",COMPLETED:"QSL:completed",ERROR:"QSL:error",FLOW_STARTED:"QSL:flow:started",FLOW_COMPLETED:"QSL:flow:completed",ALL_COMPLETED:"QSL:all:completed",SKIPPED:"QSL:skipped",DOMREADY:"QSL:domready",LOADED:"QSL:loaded"},LIFECYCLE:{DOMREADY:"interactive"===document.readyState||"complete"===document.readyState,LOADED:!1},CALLBACK:"QSLReady",types:new Map,flows:new Map,flowOptions:new Map,flowGroups:new Map,currentProcessPerFlow:new Map,pendingFlows:new Set,pendingProcesses:new Set,completedProcesses:new Set,loadActions:new Set,initActions:new Set,addProcessFilters:new Set,completedFlowsActions:new Set,flowIdFilters:new Set,conditionHandlers:new Set,triggerHandlers:new Set,processCompleteActions:new Set,allCompleteActions:new Set,resetActions:new Set,handlerCallbacksFilters:new Set,onAllComplete:null,globalResolve:null,logger:null,hasStarted:!1,eventsEnabled:!1,initialized:!1,completing:!1,autoReset:!0,globalBetween:0,async init(){if(this.initialized)return this;if(window.__QSL__=window.__QSL__||this,"interactive"===document.readyState||"complete"===document.readyState?this.LIFECYCLE.DOMREADY=!0:document.addEventListener("DOMContentLoaded",()=>this.LIFECYCLE.DOMREADY=!0,{once:!0}),"complete"===document.readyState?this.LIFECYCLE.LOADED=!0:window.addEventListener("load",()=>this.LIFECYCLE.LOADED=!0,{once:!0}),this.registerType("console",e=>new Promise(t=>{var s;null==(s=e.onBeforeStart)||s.call(e),setTimeout(()=>{var s;e.message&&this.log(e.message,{timestamp:Date.now()}),null==(s=e.onComplete)||s.call(e),t()},e.delay||0)})),window.addEventListener("QSL:log",e=>this.log(e.detail.type,e.detail.config.id)),window.addEventListener("QSL:error",e=>this.error(e.detail.type,e.detail.error,e.detail.config.id)),this.initActions.size)for(const s of this.initActions)"function"==typeof s&&await s.call(this);this.initialized=!0;const e=document.currentScript?new URL(document.currentScript.src,document.baseURI):null;if(!e||"true"!==e.searchParams.get("async"))return this;const t=e.searchParams.get("callback")||this.CALLBACK;return"function"==typeof window[t]&&window[t](),this},use(e,...t){return"function"==typeof e&&e(this,...t),this},add(e,t=null){if(!e||"object"!=typeof e)return this;if(e.id=e.id?this.PREFIX+e.id:this.PREFIX+Math.random().toString(36).slice(2),e.skipped=!1,e.type||(e.type="console"),Array.isArray(e.depends)&&(e.depends=[...new Set(e.depends)]),this.addProcessFilters.size)for(const n of this.addProcessFilters)"function"==typeof n&&([t,e]=n.call(this,t,e));const s=this.normalizeFlowId(t);return e.flowId=s,this.getOrCreateFlow(s).push(e),this},async load({between:e=!1}={}){if(!this.hasStarted){this.hasStarted=!0,this.globalBetween=e;for(const e of this.loadActions)"function"==typeof e&&e.call(this);return new Promise(e=>{if(!this.flows.size)return this.reset(),e();this.globalResolve=e,this.processFlows()})}},reset(){if(this.resetActions.size)for(const e of this.resetActions)"function"==typeof e&&e.call(this);return this.flows.clear(),this.flowOptions.clear(),this.flowGroups.clear(),this.pendingFlows.clear(),this.pendingProcesses.clear(),this.completedProcesses.clear(),this.currentProcessPerFlow.clear(),this.onAllComplete=null,this.globalResolve=null,this.hasStarted=!1,this.globalBetween=0,this.log("RESET"),this},destroy(){return this.reset(),this.types.clear(),this.initActions.clear(),this.loadActions.clear(),this.addProcessFilters.clear(),this.completedFlowsActions.clear(),this.flowIdFilters.clear(),this.conditionHandlers.clear(),this.triggerHandlers.clear(),this.processCompleteActions.clear(),this.allCompleteActions.clear(),this.resetActions.clear(),this.handlerCallbacksFilters.clear(),this.logger=null,this.eventsEnabled=!1,this.initialized=!1,this},setLogger(e){return!e||"function"!=typeof e&&"object"!=typeof e||(this.logger=e),this},setOnAllComplete(e){return this.onAllComplete="function"==typeof e?e:null,this},useEvents(){return this.eventsEnabled=!0,this},log(e,...t){this.logger&&"function"==typeof this.logger.log&&this.logger.log(e,...t)},error(e,...t){this.logger&&"function"==typeof this.logger.error&&this.logger.error(e,...t)},fire(e,t){this.eventsEnabled&&this.EVENTS[e]&&window.dispatchEvent(new CustomEvent(this.EVENTS[e],{detail:t}))},normalizeFlowId(e){return!0===e?e=this.FLOW_TYPE.ORDERED:"string"!=typeof e&&(e=this.FLOW_TYPE.DEFAULT),e},registerType(e,t){return"string"!=typeof e||"function"!=typeof t||this.types.set(e,t),this},registerTypes(e){if(!e||"object"!=typeof e)return this;const t=Array.isArray(e)?e:Object.entries(e).map(([e,t])=>"function"==typeof t?{type:e,handler:t}:t);for(const s of t)s&&this.registerType(s.type,s.handler);return this},pauseGroup(e){const t=this.flowGroups.get(e);if(!t)return this;for(const s of t)this.setFlowOptions({paused:!0},s);return this},runGroup(e){const t=this.flowGroups.get(e);if(!t)return this;for(const s of t)this.setFlowOptions({paused:!1},s),this.runFlow(s);return this},runFlow(e,t=!1){const s=this.flows.get(e),n=this.flowOptions.get(e);return s&&n&&n.status===this.FLOW_STATE.READY?(this.processFlows(e,t),this):this},getOrCreateFlow(e){return this.flows.has(e)||(this.flows.set(e,[]),this.flowOptions.set(e,{...this.FLOW_OPTIONS,ordered:e===this.FLOW_TYPE.ORDERED,status:this.FLOW_STATE.READY})),this.flows.get(e)},setFlowOptions(e={},t=null){if(t=this.normalizeFlowId(t),this.getOrCreateFlow(t),!e||"object"!=typeof e)return this;const s=this.flowOptions.get(t);return Array.isArray(e.depends)&&e.depends.length&&(e.depends=[...new Set(e.depends)],e.paused=!0,this.pendingFlows.add(t)),e.group&&(this.flowGroups.has(e.group)||this.flowGroups.set(e.group,new Set),this.flowGroups.get(e.group).add(t)),this.flowOptions.set(t,{...s,...e}),this},checkPendingFlows(){if(this.pendingFlows.size)for(const e of this.pendingFlows){const t=this.flowOptions.get(e);t&&t.depends&&t.depends.every(e=>{const t=this.flowOptions.get(this.normalizeFlowId(e));return t&&t.status===this.FLOW_STATE.COMPLETED})&&(this.setFlowOptions({paused:!1},e),this.pendingFlows.delete(e),this.runFlow(e))}},checkPendingProcesses(){if(this.pendingProcesses.size)for(const e of this.pendingProcesses){const t=e.depends.filter(e=>{const t=Array.from(this.flows.values()).some(t=>t.some(t=>t.id===this.PREFIX+e));return!this.completedProcesses.has(this.PREFIX+e)&&!t});if(t.length){this.pendingProcesses.delete(e),this.log("DEP_NOT_FOUND",e.id,`${t.join(", ")}`),e._depsResolved=!0,e._triggered=!0,e._waitResolve&&e._waitResolve();continue}e.depends.every(e=>this.completedProcesses.has(this.PREFIX+e))&&(this.pendingProcesses.delete(e),e._depsResolved=!0,e._triggered&&e._waitResolve&&e._waitResolve())}},async maybeComplete(){var e;if(!this.hasStarted)return;if(this.pendingFlows.size)for(const n of this.pendingFlows){const e=this.flowOptions.get(n);e&&e.depends&&e.depends.some(e=>!this.flowOptions.has(this.normalizeFlowId(e)))&&(this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n),this.pendingFlows.delete(n),this.log("FLOW_DEP_SKIPPED",{flow:n,depends:`${e.depends.filter(e=>!this.flowOptions.has(this.normalizeFlowId(e))).join(", ")}`}))}let t=!!this.flowOptions.size&&Array.from(this.flowOptions.values()).every(e=>e.status===this.FLOW_STATE.COMPLETED);if(!t)return;let s=!0;if(this.flows.size&&this.completedFlowsActions.size)for(const n of this.completedFlowsActions)s=n.call(this,t,this.flows,this.flowOptions);if(s&&!this.completing){if(this.completing=!0,this.log("ALL_COMPLETED"),this.fire("ALL_COMPLETED"),null==(e=this.onAllComplete)||e.call(this),null!==this.globalResolve&&this.globalResolve(),this.allCompleteActions.size)for(const e of this.allCompleteActions)"function"==typeof e&&e.call(this);this.autoReset&&this.reset(),this.completing=!1}},processFlows(e=null,t=!1){if(!this.flows.size)return;let s=e?[e]:Array.from(this.flows.keys());if(s.length&&this.flowIdFilters.size)for(const n of this.flowIdFilters)"function"==typeof n&&(s=n.call(this,s));s.sort((e,t)=>{const s=this.flowOptions.get(e),n=this.flowOptions.get(t),i=null!=(null==s?void 0:s.trigger),o=null!=(null==n?void 0:n.trigger);if(i&&!o)return 1;if(!i&&o)return-1;const r=(null==s?void 0:s.priority)||0;return((null==n?void 0:n.priority)||0)-r});for(const n of s){const s=this.flowOptions.get(n);if(s&&s.status!==this.FLOW_STATE.COMPLETED)if(this.getConditionStatus(s.condition))this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n);else if(!s.paused){if((null===e||e&&!t)&&null!=s.trigger){const e=this.getTriggerFunction(s.trigger,s);if(e){this.setFlowOptions({paused:!0},n);let t=!1;e(()=>{t||(t=!0,this.setFlowOptions({paused:!1},n),this.runFlow(n,!0))});continue}}this.setFlowOptions({status:this.FLOW_STATE.RUNNING},n),(async()=>{var e,t;null==(e=s.beforeStart)||e.call(s),s.delay&&s.delay>0&&await new Promise(e=>setTimeout(e,s.delay));const i=(this.flows.get(n)||[]).slice();if(i.sort((e,t)=>(t.priority||0)-(e.priority||0)),(s.preload||s.prefetch)&&!s.trigger)for(const n of i)if(!n.trigger&&("script"===n.type&&n.src||"style"===n.type&&n.href))try{const e=document.createElement("link");e.rel=s.preload?"preload":"prefetch",e.href=n.src||n.href,e.as="script"===n.type?"script":"style",n.crossOrigin&&(e.crossOrigin=n.crossOrigin),document.head.appendChild(e)}catch(r){this.error("PRELOAD_ERROR",r,n.id)}const o=void 0!==s.between&&null!==s.between?s.between:this.globalBetween;if(s.ordered){let e=Promise.resolve();i.forEach((t,s)=>{e=e.then(async()=>{s>0&&o&&await new Promise(e=>setTimeout(e,o)),await this.run(t)})}),await e}else{const e=i.map(async(e,t)=>(t>0&&o&&await new Promise(e=>setTimeout(e,o)),this.run(e)));await Promise.all(e)}this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n),null==(t=s.onComplete)||t.call(s),this.checkPendingFlows(),this.maybeComplete()})()}}this.checkPendingFlows(),this.maybeComplete()},async run(e){if(!e._running&&!this.getConditionStatus(e.condition)){if(e._running=!0,e._waitPromise||(e._triggered=!1,e._depsResolved=!1,e._waitPromise=new Promise(t=>e._waitResolve=t)),null!=e.trigger){const t=this.getTriggerFunction(e.trigger,e);t?t(()=>{e._triggered=!0,e._depsResolved&&e._waitResolve()}):e._triggered=!0}else e._triggered=!0;if(Array.isArray(e.depends)&&e.depends.length){const t=e.depends.filter(e=>{const t=Array.from(this.flows.values()).some(t=>t.some(t=>t.id===this.PREFIX+e));return!this.completedProcesses.has(this.PREFIX+e)&&!t});if(t.length)this.log("DEP_NOT_FOUND",e.id,`${t.join(", ")}`),e._depsResolved=!0,e._triggered=!0,e._waitResolve&&e._waitResolve();else{e.depends.every(e=>this.completedProcesses.has(this.PREFIX+e))?(e._depsResolved=!0,e._triggered&&e._waitResolve()):this.pendingProcesses.add(e)}}else e._depsResolved=!0,e._triggered&&e._waitResolve();return await e._waitPromise,this.execute(e)}},getConditionStatus(e){if(null==e)return!1;if(Array.isArray(e))return e.some(e=>this.getConditionStatus(e));if("object"==typeof e&&e.operator){const{operator:t,conditions:s}=e;return!!Array.isArray(s)&&("or"===t?s.every(e=>this.getConditionStatus(e)):"and"===t&&s.some(e=>this.getConditionStatus(e)))}if("function"==typeof e&&!e())return!0;if("boolean"==typeof e&&!e)return!0;if(this.conditionHandlers.size)for(const t of this.conditionHandlers)if("function"==typeof t){const s=t.call(this,e);if(!0===s||!1===s)return s}return!1},getTriggerFunction(e,t){if(null==e)return null;if("function"==typeof e)return t=>e(t);if(Array.isArray(e))return s=>{const n=e.map(e=>this.getTriggerFunction(e,t)).filter(e=>e);if(0===n.length)return void s();let i=0;const o=()=>{++i===n.length&&s()};n.forEach(e=>e(o))};if("object"==typeof e&&e.operator){const{operator:s,triggers:n}=e;return Array.isArray(n)?"or"===s?e=>{const s=n.map(e=>this.getTriggerFunction(e,t)).filter(e=>e);if(0===s.length)return void e();let i=!1;const o=()=>{i||(i=!0,e())};s.forEach(e=>e(o))}:"and"===s?e=>{const s=n.map(e=>this.getTriggerFunction(e,t)).filter(e=>e);if(0===s.length)return void e();let i=0;const o=()=>{++i===s.length&&e()};s.forEach(e=>e(o))}:null:null}if(this.triggerHandlers.size)for(const s of this.triggerHandlers)if("function"==typeof s){const n=s.call(this,e,t);if(n&&"function"==typeof n)return n}return!0===e||"interaction"===e?e=>this.waitForInteraction(e):null},waitForInteraction(e){["click","keydown","wheel","mousedown","mousemove","touchstart"].forEach(t=>window.addEventListener(t,()=>e(),{once:!0,passive:!0}))},async execute(e){const t=async()=>{if(this.processCompleteActions.size)for(const t of this.processCompleteActions)"function"==typeof t&&t.call(this,e);this.completedProcesses.add(e.id),e._running=!1,this.checkPendingProcesses()};if(e.skipped)return this.fire("SKIPPED",{...e,id:e.id}),void t();const s=this.types.get(e.type);if(!s)return this.log("UNKNOWN_TYPE",e.type,e.id),void t();this.fire("STARTED",{...e,id:e.id});const n={};if(this.handlerCallbacksFilters.size)for(const i of this.handlerCallbacksFilters)if("function"==typeof i){const t=i.call(this,e);t&&"object"==typeof t&&Object.assign(n,t)}return s(e,n).then(()=>{this.fire("COMPLETED",{...e,id:e.id}),t()}).catch(()=>{this.fire("ERROR",{...e,id:e.id}),t()})}},t=(e,t)=>t?(e.includes("?")?"&":"?")+Date.now():"",s=e=>{window.dispatchEvent(new CustomEvent("QSL:log",{detail:e}))},n=e=>{window.dispatchEvent(new CustomEvent("QSL:error",{detail:e}))},i=(e,t={})=>new Promise(async i=>{var o;const{flowId:r,tag:l,id:a,delay:c,data:d,onBeforeStart:u,onComplete:h,onError:f,footer:p,dom:g,onElement:E,onCustomResolve:w}=e;if(r&&l&&a)try{u&&await u(e),s({tag:l,type:"PROCESS_STARTED",config:e}),c&&await new Promise(e=>setTimeout(e,c));let r=document.createElement(l);if(null==E||E(r,e),null==(o=t.registerProcessElement)||o.call(t,r,e),d&&"object"==typeof d)for(const[e,t]of Object.entries(d))r.setAttribute(`data-${e.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/[^a-z0-9_-]/g,"").replace(/^-/,"")}`,String(t));w||(r.onload=()=>{s({tag:l,type:"PROCESS_COMPLETED",config:e}),null==h||h(),i()}),r.onerror=t=>{n({tag:l,type:"PROCESS_FAILED",config:e}),null==f||f(t),i(t)},g&&(p?document.body:document.head).appendChild(r),null==w||w({el:r,config:e,resolve:i})}catch(m){n({tag:l,type:"PROCESS_FAILED",config:e,error:m}),null==f||f(m),i(m)}else i()}),o={type:"inline-script",handler:(e,t)=>{var n;let o=null,r=!1;const l=`QSL:inline-script:completed:${e.id}`,a=()=>{var t;s({tag:"inline-script",type:"INLINE_SCRIPT_SUCCESS",config:e}),s({tag:"inline-script",type:"PROCESS_COMPLETED",config:e}),null==(t=null==e?void 0:e.onComplete)||t.call(e),null==o||o()},c={...e,code:null==(n=e.code)?void 0:n.replace(/<script.*?>|<\/script>/gi,"")};return i({...c,tag:"script",dom:!0,onElement:(e,t)=>{const{code:s,module:n,id:i,flowId:c}=t;if(s&&(e.textContent=s),n){e.type="module";const t=e.textContent,s=JSON.stringify(String(c)),n=`(function(){window.__QSL__.currentProcessPerFlow.set(${s},${JSON.stringify(String(i))});try{${t}}finally{window.__QSL__.currentProcessPerFlow.delete(${s});window.dispatchEvent(new Event(${JSON.stringify(String(l))}));}})();`;e.textContent=n;const d=()=>{window.removeEventListener(l,d),o?a():r=!0};window.addEventListener(l,d)}},onCustomResolve:({el:e,config:t,resolve:s})=>{const{module:n}=t;o=s,n&&r?a():n||queueMicrotask(()=>{o===s&&a()})}},t)}},r={type:"script",handler:(e,s)=>i({...e,tag:"script",dom:!0,onElement:(e,{src:s,module:n,async:i,defer:o,crossOrigin:r,integrity:l,bypassCache:a})=>{n&&(e.type="module"),i&&(e.async=i),o&&(e.defer=o),r&&(e.crossOrigin=r),l&&(e.integrity=l),s&&(e.src=s+t(s,a))}},s)},l={type:"style",handler:(e,t)=>{var n;const o={...e,code:null==(n=e.code)?void 0:n.replace(/<style.*?>|<\/style>/gi,"")};return i({...o,tag:"style",dom:!0,onElement:(e,{code:t})=>{t&&(e.textContent=t)},onCustomResolve:({el:e,config:t,resolve:n})=>{const{tag:i,onComplete:o}=t;s({tag:i,type:"INLINE_STYLE_SUCCESS",config:t}),s({tag:i,type:"PROCESS_COMPLETED",config:t}),null==o||o(),n()}},t)}},a={type:"stylesheet",handler:(e,s)=>i({...e,tag:"link",dom:!0,onElement:(e,{href:s,crossOrigin:n,bypassCache:i})=>{e.rel="stylesheet",n&&(e.crossOrigin=n),e.href=s+t(s,i)}},s)},c={type:"pixel",handler:(e,n)=>i({...e,tag:"img",dom:!0,onElement:(e,{style:s={display:"none"},dom:n,src:i,bypassCache:o})=>{if(n||(e=new window.Image),e.src=i+t(i,o),e.width=1,e.height=1,s&&"object"==typeof s)for(const[t,r]of Object.entries(s))e.style.setProperty(t,r)},onCustomResolve:({el:e,config:t,resolve:n})=>{const{tag:i,dom:o,onComplete:r}=t,l=()=>{s({tag:i,type:"IMAGE_LOADED",config:t}),s({tag:i,type:"PROCESS_COMPLETED",config:t}),null==r||r(),n()};o?e.onload=()=>l():l()}},n)},d={type:"shadow",handler:(e,t)=>i({...e,onBeforeStart:async({tag:e})=>await window.customElements.whenDefined(e),onElement:(e,{shadowData:t})=>{e.data=t||{},(null==t?void 0:t.hidden)&&e.setAttribute("hidden","")},onCustomResolve:({el:e,config:t,resolve:i})=>{const{tag:o,shadowData:r,onComplete:l,onError:a}=t,c=()=>{const c=null==r?void 0:r.container;let d=null;if("body"===c)d=document.body;else if("string"==typeof c&&c)try{d=document.querySelector(c)}catch(u){d=null}if(!d){const e=new Error(`Container not found: ${c}`);return n({tag:o,type:"SHADOW_FAILED",config:t,error:e}),null==a||a(e),void i(e)}"top"===(null==r?void 0:r.position)?d.insertBefore(e,d.firstChild):d.appendChild(e),s({tag:o,type:"SHADOW_SUCCESS",config:t}),s({tag:o,type:"PROCESS_COMPLETED",config:t}),null==l||l(),i()};"interactive"===document.readyState||"complete"===document.readyState?c():document.addEventListener("DOMContentLoaded",c,{once:!0})}},t)},u={type:"html",handler:(e,t)=>i({...e,dom:!0,onElement:(e,{html:t="",id:s="",className:n="",style:i={}})=>{if(t&&(e.innerHTML=t),s&&(e.id=s),n&&(e.className=Array.isArray(n)?n.join(" "):n),i&&"object"==typeof i)for(const[o,r]of Object.entries(i))e.style.setProperty(o,r)},onCustomResolve:({el:e,config:t,resolve:n})=>{const{tag:i,onComplete:o}=t;s({tag:i,type:"HTML_SUCCESS",config:t}),s({tag:i,type:"PROCESS_COMPLETED",config:t}),null==o||o(),n()}},t)};const h=(e,t)=>e.slice(t.length),f=(e,t)=>"string"==typeof e&&e.startsWith(t),p=(e,t)=>{let s=null;try{s=document.querySelector(e)}catch(o){return void t(null)}if(s)return void t(s);if("function"!=typeof MutationObserver)return void t(null);const n=document.body||document.documentElement;if(!n)return void t(null);const i=new MutationObserver(()=>{const s=document.querySelector(e);s&&(i.disconnect(),t(s))});i.observe(n,{childList:!0,subtree:!0})};e.registerTypes([r,o,l,a,c,d,u]).use(function(e){e.initActions.add(function(){e.logger={VERSION:"qsl-logger",LOG:{LOGGER_LOADED:"[QSL] Logger loaded",LOGGER_LOAD_ERROR:"[QSL] Logger load error:",UNKNOWN_TYPE:"[QSL] Unknown type:",PROCESS_STARTED:"[QSL] Process started:",PROCESS_COMPLETED:"[QSL] Process completed:",PROCESS_FAILED:"[QSL] Process failed:",STYLESHEET_STARTED:"[QSL] Stylesheet loading:",STYLESHEET_LOADED:"[QSL] Stylesheet loaded:",STYLESHEET_FAILED:"[QSL] Stylesheet failed to load:",INLINE_SCRIPT_STARTED:"[QSL] Inline script loading:",INLINE_SCRIPT_SUCCESS:"[QSL] Inline script loaded:",INLINE_SCRIPT_ERROR:"[QSL] Inline script load error:",INLINE_STYLE_STARTED:"[QSL] Inline style loading:",INLINE_STYLE_SUCCESS:"[QSL] Inline style loaded:",INLINE_STYLE_ERROR:"[QSL] Inline style load error:",IMAGE_STARTED:"[QSL] Image pixel loading:",IMAGE_LOADED:"[QSL] Image pixel loaded:",IMAGE_FAILED:"[QSL] Image pixel failed:",SHADOW_STARTED:"[QSL] Shadow element loading:",SHADOW_SUCCESS:"[QSL] Shadow element loaded:",SHADOW_FAILED:"[QSL] Shadow element failed:",HTML_STARTED:"[QSL] HTML element loading:",HTML_SUCCESS:"[QSL] HTML element loaded:",HTML_FAILED:"[QSL] HTML element failed:",PRELOAD_ERROR:"[QSL] Preload error:",FLOW_DEP_SKIPPED:"[QSL] Flow dependency missed:",DEP_NOT_FOUND:"[QSL] Dependency not found:",RESET:"[QSL] Global reset",ALL_COMPLETED:"[QSL] Loading is completed"},log(e,...t){this.LOG[e]?console.log(this.LOG[e],...t,{timestamp:Date.now()}):console.log("[QSL] "+e,...t)},error(e,...t){this.LOG[e]?console.error(this.LOG[e],...t):console.error("[QSL] "+e,...t)}}})}).use(function(e){const t=new Map,s=new WeakMap;let n=null,i=null,o=!1;const r=e=>{if(!e)return"";try{return e.includes("://")?new URL(e).pathname:e.split("?")[0].split("#")[0]}catch(t){return e.split("?")[0].split("#")[0]}};e.loadActions.add(function(){if(o)return;n=document.addEventListener,i=window.addEventListener,o=!0;const e=(e,n,i)=>{var o;let l=null,a=null;if(document.currentScript){const e=s.get(document.currentScript);e&&(l=e.processId,a=e.flowId)}if(!l&&this.currentProcessPerFlow.size)for(const[t,s]of this.currentProcessPerFlow)if(s){l=s,a=t;break}if(!l){const e=(new Error).stack;if(e){const t=e.split("\n");for(let e=2;e<t.length;e++){const s=t[e].match(/([^()\s]+\.js(?:\?[^:)]*)?):\d+(?::\d+)?/);if(s){const e=s[1],t=e.indexOf("?"),n=-1===t?e.trim():e.slice(0,t).trim();if(n){const e=n.indexOf("?"),t=n.indexOf("#"),s=-1===e?-1===t?n.length:t:-1===t?e:Math.min(e,t);let i=n.slice(0,s);try{i.includes("://")&&(i=new URL(i).pathname)}catch(c){}const o=i.lastIndexOf("/"),d=-1===o?i:i.slice(o+1);for(const[n,c]of this.flows){for(const e of c)if(e.src&&"script"===e.type){const t=r(e.src),s=t.lastIndexOf("/"),o=-1===s?t:t.slice(s+1);if(i===t||d&&d===o){l=e.id,a=n;break}}if(l)break}if(l)break}}}}}if(l&&a){const s=this.flows.get(a),r=null==s?void 0:s.find(e=>e.id===l||e.id===this.PREFIX+l);if(r){if(!1!==r.fireEvents&&!1!==(null==(o=this.flowOptions.get(a))?void 0:o.fireEvents)){const s=`${e}:${r.id}`;t.has(r.id)||t.set(r.id,[]);const o=t.get(r.id);o.some(e=>e.name===s)||o.push({type:n,name:s}),i&&(e=s)}}}return e};document.addEventListener=(t,s,i)=>{if("DOMContentLoaded"===t&&this.LIFECYCLE.DOMREADY){const s=e.call(this,t,this.EVENTS.DOMREADY,!0);s!==t&&(t=s,queueMicrotask(()=>document.dispatchEvent(new Event(s))))}return n.call(document,t,s,i)},window.addEventListener=(t,s,n)=>{if("load"===t&&this.LIFECYCLE.LOADED){const s=e.call(this,t,this.EVENTS.LOADED,!0);s!==t&&(t=s,queueMicrotask(()=>window.dispatchEvent(new Event(s))))}return i.call(window,t,s,n)}}),e.handlerCallbacksFilters.add(function(e){return{registerProcessElement:(e,t)=>{s.set(e,{flowId:t.flowId,processId:t.id})}}}),e.processCompleteActions.add(function(e){const s=t.get(e.id);if(s&&Array.isArray(s))for(const t of s)t.type===this.EVENTS.DOMREADY?this.LIFECYCLE.DOMREADY?document.dispatchEvent(new Event(t.name)):document.addEventListener("DOMContentLoaded",()=>{document.dispatchEvent(new Event(t.name))},{once:!0}):t.type===this.EVENTS.LOADED&&(this.LIFECYCLE.LOADED?window.dispatchEvent(new Event(t.name)):window.addEventListener("load",()=>{window.dispatchEvent(new Event(t.name))},{once:!0}))}),e.resetActions.add(function(){o&&n&&i&&(document.addEventListener=n,window.addEventListener=i,o=!1)}),e.customEvents=t}).use(function(e){e.loadActions.add(function(){const e=(t,s,n=new Set)=>{if(n.has(t))return!0;n.add(t);const i=s(t)||[];for(const o of i)if(e(o,s,new Set(n)))return!0;return!1},t=e=>{var t;return(null==(t=this.flowOptions.get(this.normalizeFlowId(e)))?void 0:t.depends)||[]};for(const[n,i]of this.flowOptions.entries())Array.isArray(i.depends)&&i.depends.length&&e(n,t)&&(this.log("CIRC_FLOW_DEP_SKIPPED",n,i.depends),this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n));const s=e=>{for(const t of this.flows.values()){const s=t.find(t=>t.id===e);if(s&&Array.isArray(s.depends))return s.depends.map(e=>this.PREFIX+e)}return[]};for(const n of this.flows.values())for(const t of n)Array.isArray(t.depends)&&t.depends.length&&e(t.id,s)&&(this.log("CIRC_PROCESS_DEP_SKIPPED",t.id,t.depends),t.condition=!1)}),e.logger&&"qsl-logger"===e.logger.VERSION&&(e.logger.LOG.CIRC_FLOW_DEP_SKIPPED="[QSL] Circular flow dependency skipped:",e.logger.LOG.CIRC_PROCESS_DEP_SKIPPED="[QSL] Circular process dependency skipped:")}).use(function(e){e.conditionHandlers.add(function(e){return"string"==typeof e&&e.startsWith("media:")?!window.matchMedia(e.slice(6)).matches:null})}).use(function(e){e.conditionHandlers.add(function(e){var t;if("string"!=typeof e||!e.startsWith("lang:"))return null;const s=e.split(":"),n=s[1]||"equals",i=s.slice(2).join(":"),o=navigator.language||(null==(t=navigator.languages)?void 0:t[0])||"";switch(n){case"equals":case"is":return o!==i;case"contains":return!o.includes(i);case"startsWith":return!o.startsWith(i);case"in":return!i.split(",").map(e=>e.trim()).includes(o);default:return!0}})}).use(function(e){e.conditionHandlers.add(function(e){if("string"!=typeof e||!e.startsWith("tz:")&&!e.startsWith("timezone:"))return null;const t=e.split(":"),s=t[1]||"equals",n=t.slice(2).join(":");try{const e=Intl.DateTimeFormat().resolvedOptions().timeZone,t=-(new Date).getTimezoneOffset()/60;switch(s){case"equals":case"is":return e!==n;case"contains":return!e.includes(n);case"offset":return t!==parseInt(n);default:return!0}}catch(i){return!0}})}).use(function(e){e.conditionHandlers.add(function(e){if("string"!=typeof e||!e.startsWith("url:"))return null;const t=window.location,s=t.href,n=t.pathname,i=t.search,o=t.hostname,r=e.split(":"),l=r[1],a=r.slice(2).join(":");if(!l)return!0;switch(l){case"contains":return!s.includes(a);case"path":return!n.includes(a);case"pathStartsWith":return!n.startsWith(a);case"pathEndsWith":return!n.endsWith(a);case"query":if(i){const e=new URLSearchParams(i);if(a.includes("=")){const[t,s]=a.split("=");return e.get(t)!==s}return!e.has(a)}return!0;case"hostname":return!o.includes(a);case"matches":try{return!new RegExp(a).test(s)}catch(c){return!0}case"pathMatches":try{return!new RegExp(a).test(n)}catch(c){return!0}default:return!0}})}).use(function(e){e.conditionHandlers.add(function(e){if("string"!=typeof e||!e.startsWith("ua:")&&!e.startsWith("userAgent:"))return null;const t=e.split(":"),s=t[1]||"contains",n=t.slice(2).join(":"),i=navigator.userAgent||"",o=i.toLowerCase(),r=n.toLowerCase();switch(s){case"contains":if(!o.includes(r))return!0;break;case"equals":case"is":if(i!==n)return!0;break;case"matches":try{if(!new RegExp(n,"i").test(i))return!0}catch(l){return!0}break;case"browser":if(!{chrome:/chrome/i.test(i)&&!/edg|opr/i.test(i),firefox:/firefox/i.test(i),safari:/safari/i.test(i)&&!/chrome|chromium|edg|opr/i.test(i),edge:/edg/i.test(i),opera:/opr/i.test(i),ie:/msie|trident/i.test(i),chromium:/chromium/i.test(i)}[r])return!0;break;case"device":const e=/mobile|android|iphone|ipod|blackberry|iemobile|opera mini/i.test(i),t=/tablet|ipad|playbook|silk/i.test(i)||e&&/android/i.test(i)&&!/mobile/i.test(i),s=!e&&!t;switch(r){case"mobile":if(!e)return!0;break;case"tablet":if(!t)return!0;break;case"desktop":if(!s)return!0;break;default:return!0}break;case"os":case"platform":if(!{windows:/win/i.test(i),mac:/mac/i.test(i),ios:/iphone|ipad|ipod/i.test(i),android:/android/i.test(i),linux:/linux/i.test(i)&&!/android/i.test(i),unix:/unix/i.test(i),chromeos:/cros/i.test(i)}[r])return!0;break;default:return!0}return!1})}).use(function(e){e.triggerHandlers.add(function(e){return"load"!==e?null:e=>{"complete"===document.readyState?e():window.addEventListener("load",()=>e(),{once:!0})}})}).use(function(e){e.triggerHandlers.add(function(e){return"idle"!==e?null:e=>{"function"==typeof window.requestIdleCallback?window.requestIdleCallback(()=>e()):setTimeout(()=>e(),200)}})}).use(function(e){e.triggerHandlers.add(function(e){return"domready"!==e?null:e=>{"interactive"===document.readyState||"complete"===document.readyState?e():document.addEventListener("DOMContentLoaded",()=>e(),{once:!0})}})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"delay:"))return null;const t=parseInt(h(e,"delay:"),10);return e=>setTimeout(e,Number.isFinite(t)&&t>0?t:0)})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"hover:"))return null;const t=h(e,"hover:");return e=>{p(t,t=>{t?t.addEventListener("mouseenter",()=>e(),{once:!0,passive:!0}):e()})}})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"visible:"))return null;const t=h(e,"visible:");return e=>{p(t,t=>{if(!t||"function"!=typeof IntersectionObserver)return void e();const s=new IntersectionObserver(t=>{for(const n of t)if(n.isIntersecting)return s.disconnect(),void e()});s.observe(t)})}})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"appears:"))return null;const t=h(e,"appears:");return e=>p(t,()=>e())})}).use(function(e){e.triggerHandlers.add(function(e,t){if(!f(e,"media:"))return null;const s=h(e,"media:");return e=>{if(!s.length||"function"!=typeof window.matchMedia)return t&&(t.skipped=!0),void e();const n=window.matchMedia(s);if(n.matches)return void e();const i=t=>{t.matches&&(n.removeEventListener("change",i),e())};n.addEventListener("change",i)}})}).init()}();
|
|
2
|
+
//# sourceMappingURL=qsl.min.js.map
|