@cocreate/plugins 1.2.1 → 1.3.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 +12 -0
- package/demo/index.html +110 -209
- package/package.json +1 -1
- package/src/index.js +159 -503
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.2.1...v1.3.0) (2026-03-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Update demo/index.html to test sequential execution of attributes as logic ([d71e0a5](https://github.com/CoCreate-app/CoCreate-plugins/commit/d71e0a562bc48337746583a04bf230f84134f6c7))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Refactor plugin initialization to sequentially load resources and execute attributes in DOM order. Add support for direct $this references in attributes. Improve error handling and logging during plugin execution. ([08ecdb2](https://github.com/CoCreate-app/CoCreate-plugins/commit/08ecdb2b2bb8bba2cf3950eb2ef5e7ac77916203))
|
|
12
|
+
|
|
1
13
|
## [1.2.1](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.2.0...v1.2.1) (2026-03-02)
|
|
2
14
|
|
|
3
15
|
|
package/demo/index.html
CHANGED
|
@@ -1,230 +1,131 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<title>Plugins API Test Suite</title>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
|
|
8
|
+
<!-- UI Framework for Demo -->
|
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
10
|
+
|
|
11
|
+
<!-- Library CSS (Toastify) -->
|
|
12
|
+
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
--primary-color: #0d6efd;
|
|
17
|
+
}
|
|
18
|
+
body { background-color: #f8fafc; padding-top: 50px; padding-bottom: 100px; font-family: system-ui, -apple-system, sans-serif; }
|
|
19
|
+
.card { border: none; border-radius: 12px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); margin-bottom: 2rem; }
|
|
20
|
+
.card-header { background: #fff; border-bottom: 1px solid #e2e8f0; font-weight: 600; padding: 1.25rem; }
|
|
21
|
+
.code-block { background: #1e293b; color: #f1f5f9; padding: 1rem; border-radius: 8px; font-family: 'Fira Code', monospace; font-size: 0.85rem; margin-top: 1rem; overflow-x: auto; }
|
|
22
|
+
.live-preview { background: #fff; border: 2px dashed #e2e8f0; border-radius: 8px; padding: 2rem; display: flex; align-items: center; justify-content: center; min-height: 140px; }
|
|
23
|
+
.badge-sequential { background-color: #dcfce7; color: #166534; font-size: 0.75rem; padding: 0.25rem 0.6rem; border-radius: 9999px; font-weight: 600; margin-bottom: 0.5rem; display: inline-block; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
+
<div class="container" style="max-width: 800px;">
|
|
29
|
+
<div class="text-center mb-5">
|
|
30
|
+
<h1 class="fw-bold">Linear Execution Test Suite</h1>
|
|
31
|
+
<p class="text-muted">Testing the sequential processing of HTML attributes as logic.</p>
|
|
32
|
+
</div>
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
toastify='{
|
|
48
|
-
"text": "Hello! I am a declarative toast.",
|
|
49
|
-
"duration": 3000,
|
|
50
|
-
"gravity": "top",
|
|
51
|
-
"position": "right",
|
|
52
|
-
"style": { "background": "#198754" }
|
|
53
|
-
}'
|
|
54
|
-
onclick="this.Toastify.showToast()">
|
|
55
|
-
Click me to Show Toast
|
|
56
|
-
</button>
|
|
57
|
-
</div>
|
|
58
|
-
<div class="col-md-6">
|
|
59
|
-
<h6 class="text-muted text-uppercase fs-7 fw-bold mb-3">Code</h6>
|
|
60
|
-
<pre><code class="language-html"><button
|
|
61
|
-
plugin="Toastify"
|
|
62
|
-
toastify='{
|
|
63
|
-
"text": "Hello!",
|
|
64
|
-
"duration": 3000,
|
|
65
|
-
"style": { "background": "#198754" }
|
|
66
|
-
}'
|
|
67
|
-
onclick="this.Toastify.showToast()">
|
|
68
|
-
Click Me
|
|
69
|
-
</button></code></pre>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
34
|
+
<!-- TEST 1: Dot-Notation + Method Execution -->
|
|
35
|
+
<div class="card">
|
|
36
|
+
<div class="card-header">1. Sequential Property & Method Call</div>
|
|
37
|
+
<div class="card-body">
|
|
38
|
+
<p class="text-secondary small">This button sets multiple properties on the Toastify instance and then calls the execution method, all in order.</p>
|
|
39
|
+
<div class="live-preview flex-column">
|
|
40
|
+
<button type="button" class="btn btn-primary px-4"
|
|
41
|
+
plugin="Toastify"
|
|
42
|
+
Toastify.text="Step-by-step logic works!"
|
|
43
|
+
Toastify.duration="4000"
|
|
44
|
+
Toastify.style.background="linear-gradient(to right, #00b09b, #96c93d)"
|
|
45
|
+
Toastify.showToast()="">
|
|
46
|
+
Trigger Sequential Toast
|
|
47
|
+
</button>
|
|
48
|
+
<div class="code-block w-100">
|
|
49
|
+
plugin="Toastify" <br>
|
|
50
|
+
Toastify.text="..." <br>
|
|
51
|
+
Toastify.showToast()=""
|
|
73
52
|
</div>
|
|
74
53
|
</div>
|
|
75
54
|
</div>
|
|
55
|
+
</div>
|
|
76
56
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<th>Usage Example</th>
|
|
94
|
-
</tr>
|
|
95
|
-
</thead>
|
|
96
|
-
<tbody>
|
|
97
|
-
<tr>
|
|
98
|
-
<td><span class="operator-badge">$this</span></td>
|
|
99
|
-
<td>References the current DOM element where the attribute is placed.</td>
|
|
100
|
-
<td><code>"element": "$this"</code></td>
|
|
101
|
-
</tr>
|
|
102
|
-
<tr>
|
|
103
|
-
<td><span class="operator-badge">$window</span></td>
|
|
104
|
-
<td>Accesses the global <code>window</code> object (useful for callbacks).</td>
|
|
105
|
-
<td><code>"callback": "$window.console.log"</code></td>
|
|
106
|
-
</tr>
|
|
107
|
-
</tbody>
|
|
108
|
-
</table>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
<!-- RaterJs Example ($this) -->
|
|
112
|
-
<div class="border rounded p-3 bg-light mb-3">
|
|
113
|
-
<h5 class="mb-3">Example: Using <code>$this</code> for Element Binding</h5>
|
|
114
|
-
<div class="row">
|
|
115
|
-
<div class="col-md-6">
|
|
116
|
-
<!-- RaterJs Implementation -->
|
|
117
|
-
<div dir="ltr"
|
|
118
|
-
plugin="raterJs"
|
|
119
|
-
raterjs='[{
|
|
120
|
-
"element": "$this",
|
|
121
|
-
"starSize": 32,
|
|
122
|
-
"rating": 3.5,
|
|
123
|
-
"step": 0.5
|
|
124
|
-
}]'>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
<div class="col-md-6">
|
|
128
|
-
<pre><code class="language-html"><div
|
|
129
|
-
plugin="raterJs"
|
|
130
|
-
raterjs='[{
|
|
131
|
-
"element": "$this",
|
|
132
|
-
"starSize": 32,
|
|
133
|
-
"rating": 3.5
|
|
134
|
-
}]'>
|
|
135
|
-
</div></code></pre>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
</div>
|
|
57
|
+
<!-- TEST 2: JSON Object + Method Call -->
|
|
58
|
+
<div class="card">
|
|
59
|
+
<div class="card-header">2. Object-Based Initialization</div>
|
|
60
|
+
<div class="card-body">
|
|
61
|
+
<p class="text-secondary small">Initializing using a full JSON object, followed immediately by a method call.</p>
|
|
62
|
+
<div class="live-preview flex-column">
|
|
63
|
+
<button type="button" class="btn btn-dark px-4"
|
|
64
|
+
plugin="Toastify"
|
|
65
|
+
Toastify()='{"text": "JSON Object Booted!", "gravity": "bottom", "position": "left"}'
|
|
66
|
+
Toastify.showToast()="">
|
|
67
|
+
Trigger JSON Boot
|
|
68
|
+
</button>
|
|
69
|
+
<div class="code-block w-100">
|
|
70
|
+
plugin="Toastify" <br>
|
|
71
|
+
Toastify='{"text": "..."}' <br>
|
|
72
|
+
$Toastify.showToast()=""
|
|
141
73
|
</div>
|
|
142
74
|
</div>
|
|
143
75
|
</div>
|
|
76
|
+
</div>
|
|
144
77
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<br><small class="text-danger">*Note: Enclosed in an array to represent arguments list.</small>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
<div class="col-md-6">
|
|
168
|
-
<div class="card h-100 bg-light border-0">
|
|
169
|
-
<div class="card-body">
|
|
170
|
-
<h6 class="fw-bold">Pattern B: Argument List</h6>
|
|
171
|
-
<p class="small text-muted">Used when the plugin accepts multiple distinct arguments (e.g., <code>Swiper(element, options)</code>).</p>
|
|
172
|
-
<hr>
|
|
173
|
-
<code>config='["$this", { "opt": "val" }]'</code>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
78
|
+
<!-- TEST 3: Interleaved $this and Plugin Logic -->
|
|
79
|
+
<div class="card">
|
|
80
|
+
<div class="card-header">3. Interleaved Element & Plugin Logic</div>
|
|
81
|
+
<div class="card-body">
|
|
82
|
+
<p class="text-secondary small">This test changes the button's own style using <code>$this</code> before triggering the plugin logic.</p>
|
|
83
|
+
<div class="live-preview flex-column">
|
|
84
|
+
<button type="button" class="btn btn-outline-danger px-4"
|
|
85
|
+
$this.style.borderRadius="0px"
|
|
86
|
+
$this.innerText="Order Processed"
|
|
87
|
+
plugin="Toastify"
|
|
88
|
+
Toastify.text="Element changed first!"
|
|
89
|
+
Toastify.showToast()>
|
|
90
|
+
Change Me & Toast
|
|
91
|
+
</button>
|
|
92
|
+
<div class="code-block w-100">
|
|
93
|
+
$this.style.borderRadius="0px" <br>
|
|
94
|
+
plugin="Toastify" <br>
|
|
95
|
+
Toastify.showToast()=""
|
|
179
96
|
</div>
|
|
180
97
|
</div>
|
|
181
98
|
</div>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
class="btn btn-primary"
|
|
199
|
-
onclick="Swal.fire({
|
|
200
|
-
title: 'Are you sure?',
|
|
201
|
-
text: 'You can define this entire config in HTML!',
|
|
202
|
-
icon: 'warning',
|
|
203
|
-
showCancelButton: true,
|
|
204
|
-
confirmButtonText: 'Yes, delete it!'
|
|
205
|
-
})">
|
|
206
|
-
Trigger SweetAlert
|
|
207
|
-
</button>
|
|
208
|
-
</div>
|
|
209
|
-
<div class="col-md-6">
|
|
210
|
-
<pre><code class="language-html"><button
|
|
211
|
-
onclick="Swal.fire({
|
|
212
|
-
title: 'Ready?',
|
|
213
|
-
preConfirm: '$window.myCustomFunction'
|
|
214
|
-
}) >
|
|
215
|
-
Launch
|
|
216
|
-
</button></code></pre>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- TEST 4: Input State Manipulation -->
|
|
102
|
+
<div class="card">
|
|
103
|
+
<div class="card-header">4. Input Property Manipulation</div>
|
|
104
|
+
<div class="card-body">
|
|
105
|
+
<p class="text-secondary small">Directly setting properties on an input element on load.</p>
|
|
106
|
+
<div class="live-preview">
|
|
107
|
+
<input type="text" class="form-control"
|
|
108
|
+
$this.value="CoCreate Linear Engine"
|
|
109
|
+
$this.style.border="2px solid #0d6efd"
|
|
110
|
+
$this.placeholder="Wait for it..."
|
|
111
|
+
plugin="Toastify"
|
|
112
|
+
Toastify.text="Input properties applied"
|
|
113
|
+
Toastify.duration="2000"
|
|
114
|
+
Toastify.showToast()="">
|
|
221
115
|
</div>
|
|
222
116
|
</div>
|
|
223
|
-
|
|
224
117
|
</div>
|
|
225
118
|
|
|
226
|
-
|
|
227
|
-
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- CoCreate JS Bundle (Contains plugin.js and operatorEngine.js) -->
|
|
122
|
+
<script src="../../../CoCreateJS/src/dist/CoCreate.js"></script>
|
|
123
|
+
|
|
124
|
+
<!-- External Plugin Library -->
|
|
125
|
+
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
|
228
126
|
|
|
229
|
-
|
|
127
|
+
<script type="module">
|
|
128
|
+
console.log("Test Suite Ready. The 'plugin.js' sequencer will now process attributes in order.");
|
|
129
|
+
</script>
|
|
130
|
+
</body>
|
|
230
131
|
</html>
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,592 +1,248 @@
|
|
|
1
1
|
import Observer from "@cocreate/observer";
|
|
2
|
-
import {
|
|
2
|
+
import { processOperatorsAsync } from "@cocreate/utils";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {Object} PluginDefinition
|
|
6
|
-
* @property {Array<string|Object>} [js] - List of JS files to load.
|
|
6
|
+
* @property {Array<string|Object>} [js] - List of JS files to load.
|
|
7
7
|
* @property {Array<string>} [css] - List of CSS files to load.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
// --- CONFIGURATION ---
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @type {Object.<string, PluginDefinition>}
|
|
14
|
-
* Configuration object containing plugin definitions.
|
|
15
|
-
* Populated dynamically from CoCreate.config.js or defaults.
|
|
16
|
-
*/
|
|
17
11
|
const plugins = {};
|
|
18
12
|
|
|
19
13
|
// --- CORE ENGINE ---
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Global Cache for script promises to prevent race conditions and duplicate loads.
|
|
23
|
-
* Stores the pending Promise for a script source URL.
|
|
24
|
-
* @type {Map<string, Promise<void>>}
|
|
25
|
-
*/
|
|
26
14
|
const scriptCache = new Map();
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Cache the CSS marker once on load to determine injection point.
|
|
30
|
-
* Used to ensure plugin CSS is injected in the correct order relative to user styles.
|
|
31
|
-
* @type {Element|null}
|
|
32
|
-
*/
|
|
33
15
|
const cssMarker = typeof document !== 'undefined' ? document.querySelector('link[plugins]') : null;
|
|
34
16
|
|
|
35
17
|
/**
|
|
36
18
|
* Global Initialization Function.
|
|
37
|
-
* Processes one or more elements to detect and attach plugins.
|
|
38
|
-
* * @param {HTMLElement|NodeList|Array<HTMLElement>} elements - Single Element, NodeList, or Array of Elements to initialize.
|
|
39
|
-
* @returns {void}
|
|
40
19
|
*/
|
|
41
|
-
|
|
20
|
+
function init(elements) {
|
|
42
21
|
if (!elements) return;
|
|
43
|
-
|
|
44
|
-
let collection = [];
|
|
45
|
-
if (elements instanceof HTMLElement || elements instanceof Element) {
|
|
46
|
-
collection = [elements];
|
|
47
|
-
} else if (elements.length !== undefined && typeof elements !== 'function') {
|
|
48
|
-
collection = Array.from(elements);
|
|
49
|
-
} else {
|
|
50
|
-
collection = [elements];
|
|
51
|
-
}
|
|
52
|
-
|
|
22
|
+
let collection = (elements instanceof HTMLElement || elements instanceof Element) ? [elements] : Array.from(elements || []);
|
|
53
23
|
collection.forEach(el => {
|
|
54
|
-
if (el && typeof el.getAttribute === 'function')
|
|
55
|
-
processPlugin(el);
|
|
56
|
-
}
|
|
24
|
+
if (el && typeof el.getAttribute === 'function') processPlugin(el);
|
|
57
25
|
});
|
|
58
26
|
}
|
|
59
27
|
|
|
60
28
|
/**
|
|
61
|
-
* Processes
|
|
62
|
-
*
|
|
63
|
-
* * @async
|
|
64
|
-
* @param {HTMLElement} el - The DOM element to process.
|
|
65
|
-
* @returns {Promise<void>} Resolves when all resources for the plugins are loaded.
|
|
29
|
+
* Processes resources and executes attributes in strict DOM order.
|
|
30
|
+
* The attribute list IS the script. We iterate once and execute everything in sequence.
|
|
66
31
|
*/
|
|
67
32
|
async function processPlugin(el) {
|
|
68
33
|
const rawAttr = el.getAttribute("plugin");
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const pluginNames = rawAttr.split(',').map(s => s.trim());
|
|
34
|
+
const pluginNames = rawAttr ? rawAttr.split(',').map(s => s.trim()) : [];
|
|
72
35
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (pluginDef) {
|
|
78
|
-
// Load CSS
|
|
36
|
+
// 1. RESOURCE PRE-LOADING
|
|
37
|
+
for (const name of pluginNames) {
|
|
38
|
+
const pluginDef = plugins[name];
|
|
39
|
+
|
|
40
|
+
if (pluginDef && !window[name]) {
|
|
79
41
|
if (pluginDef.css) pluginDef.css.forEach(href => {
|
|
80
42
|
if (!document.querySelector(`link[href="${href}"]`)) {
|
|
81
43
|
const link = document.createElement("link");
|
|
82
|
-
link.rel = "stylesheet";
|
|
83
|
-
link
|
|
84
|
-
|
|
85
|
-
// CSS INJECTION STRATEGY:
|
|
86
|
-
// 1. Priority: Check for a specific marker element <link plugin>
|
|
87
|
-
// (Cached globally in cssMarker)
|
|
88
|
-
|
|
89
|
-
if (cssMarker) {
|
|
90
|
-
// Insert before the marker
|
|
91
|
-
cssMarker.parentNode.insertBefore(link, cssMarker);
|
|
92
|
-
} else {
|
|
93
|
-
// 2. Fallback: Prepend before existing CSS
|
|
94
|
-
// To allow custom CSS to easily override plugin defaults, we must ensure
|
|
95
|
-
// plugin CSS loads BEFORE user CSS.
|
|
96
|
-
const firstStyle = document.head.querySelector('link[rel="stylesheet"], style');
|
|
97
|
-
|
|
98
|
-
if (firstStyle) {
|
|
99
|
-
document.head.insertBefore(link, firstStyle);
|
|
100
|
-
} else {
|
|
101
|
-
// If no CSS exists yet, appending is safe
|
|
102
|
-
document.head.appendChild(link);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
44
|
+
link.rel = "stylesheet"; link.href = href;
|
|
45
|
+
if (cssMarker) cssMarker.parentNode.insertBefore(link, cssMarker);
|
|
46
|
+
else document.head.appendChild(link);
|
|
105
47
|
}
|
|
106
48
|
});
|
|
107
49
|
|
|
108
|
-
// Load JS with Promise Cache
|
|
109
50
|
if (pluginDef.js) {
|
|
110
|
-
const preWindowKeys = (typeof window !== 'undefined') ? new Set(Object.keys(window)) : new Set();
|
|
111
|
-
|
|
112
51
|
for (const item of pluginDef.js) {
|
|
113
52
|
const src = typeof item === 'string' ? item : item.src;
|
|
114
|
-
|
|
115
|
-
const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
|
|
116
|
-
|
|
53
|
+
if (!src) continue;
|
|
117
54
|
if (!scriptCache.has(src)) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (prevOnload) prevOnload();
|
|
128
|
-
existing.dataset.loaded = "true";
|
|
129
|
-
resolve();
|
|
130
|
-
};
|
|
131
|
-
existing.onerror = reject;
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
const s = document.createElement("script");
|
|
135
|
-
s.src = src;
|
|
136
|
-
if (integrity) {
|
|
137
|
-
s.integrity = integrity;
|
|
138
|
-
s.crossOrigin = crossOrigin || "anonymous";
|
|
139
|
-
}
|
|
140
|
-
s.onload = () => {
|
|
141
|
-
s.dataset.loaded = "true";
|
|
142
|
-
resolve();
|
|
143
|
-
};
|
|
144
|
-
s.onerror = reject;
|
|
55
|
+
const existingScript = document.querySelector(`script[src*="${src}"]`);
|
|
56
|
+
if (existingScript && (existingScript.dataset.loaded === "true" || window[name])) {
|
|
57
|
+
scriptCache.set(src, Promise.resolve());
|
|
58
|
+
} else {
|
|
59
|
+
const s = document.createElement("script");
|
|
60
|
+
s.src = src;
|
|
61
|
+
scriptCache.set(src, new Promise((resolve) => {
|
|
62
|
+
s.onload = () => { s.dataset.loaded = "true"; resolve(); };
|
|
63
|
+
s.onerror = () => resolve();
|
|
145
64
|
document.head.appendChild(s);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
scriptCache.set(src, scriptPromise);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
await scriptCache.get(src);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
console.error(`Failed to load script: ${src}`, e);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// After loading JS files, map newly-added globals to the expected plugin name.
|
|
159
|
-
// Exact (case-insensitive) matching only.
|
|
160
|
-
try {
|
|
161
|
-
if (typeof window !== 'undefined') {
|
|
162
|
-
const expectedName = pluginName;
|
|
163
|
-
const lower = expectedName.toLowerCase();
|
|
164
|
-
|
|
165
|
-
const allKeys = Object.keys(window);
|
|
166
|
-
const newKeys = allKeys.filter(k => !preWindowKeys.has(k));
|
|
167
|
-
let mappedKey = null;
|
|
168
|
-
|
|
169
|
-
for (const k of newKeys) {
|
|
170
|
-
if (k.toLowerCase() === lower) { mappedKey = k; break; }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (!mappedKey) {
|
|
174
|
-
for (const k of allKeys) {
|
|
175
|
-
if (k.toLowerCase() === lower) { mappedKey = k; break; }
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (mappedKey && !window[expectedName]) {
|
|
180
|
-
window[expectedName] = window[mappedKey];
|
|
181
|
-
console.debug(`Mapped plugin global: window.${expectedName} <- window.${mappedKey}`);
|
|
65
|
+
}));
|
|
182
66
|
}
|
|
183
67
|
}
|
|
184
|
-
|
|
185
|
-
// Non-fatal
|
|
68
|
+
await scriptCache.get(src);
|
|
186
69
|
}
|
|
187
70
|
}
|
|
188
71
|
}
|
|
189
|
-
|
|
190
|
-
// Attempt to execute plugin even if no config was found (it might be on window already)
|
|
191
|
-
executeGenericPlugin(el, pluginName);
|
|
192
72
|
}
|
|
193
|
-
}
|
|
194
73
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
*/
|
|
202
|
-
const isConstructor = (func, name) => {
|
|
203
|
-
try {
|
|
204
|
-
if (typeof func !== 'function') return false;
|
|
205
|
-
if (/^\s*class\s+/.test(func.toString())) return true;
|
|
206
|
-
if (!func.prototype) return false;
|
|
207
|
-
const n = name || func.name;
|
|
208
|
-
if (n && /^[A-Z]/.test(n)) return true;
|
|
209
|
-
} catch(e) {}
|
|
210
|
-
return false;
|
|
211
|
-
};
|
|
74
|
+
// 2. LINEAR ATTRIBUTE SCRIPT EXECUTION
|
|
75
|
+
const attributes = Array.from(el.attributes);
|
|
76
|
+
for (let i = 0; i < attributes.length; i++) {
|
|
77
|
+
const attr = attributes[i];
|
|
78
|
+
const attrName = attr.name;
|
|
79
|
+
const attrLower = attrName.toLowerCase();
|
|
212
80
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
* 2. Initializing the base instance.
|
|
218
|
-
* 3. Processing attribute paths and nested JSON objects to execute methods or set properties.
|
|
219
|
-
* * @param {HTMLElement} el - The target element.
|
|
220
|
-
* @param {string} name - The name of the plugin (case-insensitive identifier).
|
|
221
|
-
* @returns {void}
|
|
222
|
-
*/
|
|
223
|
-
function executeGenericPlugin(el, name) {
|
|
224
|
-
const prefix = name.toLowerCase();
|
|
225
|
-
const mainAttr = el.getAttribute(prefix);
|
|
226
|
-
let rawData = {};
|
|
227
|
-
|
|
228
|
-
for (let attr of el.attributes) {
|
|
229
|
-
let key = attr.name;
|
|
230
|
-
if (key === prefix) {
|
|
231
|
-
key = name;
|
|
232
|
-
} else if (key.startsWith(prefix + '-')) {
|
|
233
|
-
key = key.replaceAll("-", ".");
|
|
234
|
-
} else if (!key.startsWith(prefix + '.')) {
|
|
235
|
-
continue
|
|
81
|
+
const isDirectThis = attrName.startsWith('$this.');
|
|
82
|
+
let cleanAttrName = attrLower;
|
|
83
|
+
if (isDirectThis) {
|
|
84
|
+
cleanAttrName = attrLower.substring(6); // Strip '$this.' for plugin matching
|
|
236
85
|
}
|
|
237
86
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} catch(e) {
|
|
241
|
-
rawData[key] = attr.value;
|
|
242
|
-
}
|
|
87
|
+
// Router: Find if this matches a listed plugin
|
|
88
|
+
const activePluginName = pluginNames.find(p => cleanAttrName === p.toLowerCase() || cleanAttrName.startsWith(p.toLowerCase() + '.'));
|
|
243
89
|
|
|
244
|
-
|
|
90
|
+
if (!activePluginName && !isDirectThis) continue;
|
|
245
91
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
92
|
+
try {
|
|
93
|
+
let val = attr.value.trim();
|
|
94
|
+
if (val === "") val = "true";
|
|
249
95
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
console.error(`Plugin for ${name} not found on window.`);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
96
|
+
// BOOTSTRAPPING (Initialize on First Sight via Operator Engine)
|
|
97
|
+
let existingInstance = activePluginName && el[activePluginName];
|
|
255
98
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
for (let key in resolved) {
|
|
259
|
-
// We generally expect the root key to match the plugin name (e.g., 'swiper')
|
|
260
|
-
// We unwrap this root key to pass the actual config to the Plugin.
|
|
261
|
-
if (key === name || key.toLowerCase() === prefix) {
|
|
262
|
-
try {
|
|
263
|
-
// Determine Target: Use existing instance on element if available, else use Window Plugin
|
|
264
|
-
let Target = el[name] || Plugin;
|
|
265
|
-
let val = resolved[key];
|
|
99
|
+
if (activePluginName && !existingInstance) {
|
|
100
|
+
const initVal = (cleanAttrName === activePluginName.toLowerCase()) ? val : null;
|
|
266
101
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function resolvePathWithParent(root, path) {
|
|
280
|
-
if (!root || !path || typeof path !== "string") return { parent: null, value: undefined };
|
|
281
|
-
const parts = path.split(".").filter(Boolean);
|
|
282
|
-
if (!parts.length) return { parent: null, value: undefined };
|
|
283
|
-
|
|
284
|
-
let parent = null;
|
|
285
|
-
let current = root;
|
|
286
|
-
for (let i = 0; i < parts.length; i++) {
|
|
287
|
-
const part = parts[i];
|
|
288
|
-
if (current == null) return { parent: null, value: undefined };
|
|
289
|
-
parent = current;
|
|
290
|
-
current = current[part];
|
|
291
|
-
}
|
|
292
|
-
return { parent, value: current };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function normalizeCrudPayload(value) {
|
|
296
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
297
|
-
|
|
298
|
-
if (value.type && Array.isArray(value[value.type])) return value[value.type];
|
|
299
|
-
if (value.method && typeof value.method === "string") {
|
|
300
|
-
const type = value.method.split(".")[0];
|
|
301
|
-
if (type && Array.isArray(value[type])) return value[type];
|
|
302
|
-
}
|
|
303
|
-
return value;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function getPluginInstancesFromElement(el) {
|
|
307
|
-
if (!el || !el.__cocreatePluginInstances) return [];
|
|
308
|
-
return Object.values(el.__cocreatePluginInstances).filter(Boolean);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function isReferenceAssignment(val) {
|
|
312
|
-
return typeof val === "string" && val.trim().startsWith("=");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function normalizeReferencePath(refPath) {
|
|
316
|
-
if (typeof refPath !== "string") return "";
|
|
317
|
-
return refPath.trim().replace(/^=\s*/, "");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function resolveCallableReference(refPath, parent, hostElement) {
|
|
321
|
-
const normalized = normalizeReferencePath(refPath);
|
|
322
|
-
if (!normalized) return { fn: undefined, context: undefined, methodName: undefined };
|
|
323
|
-
|
|
324
|
-
const methodName = normalized.split(".").pop();
|
|
325
|
-
const startsWithThis = normalized === "$this" || normalized.startsWith("$this.");
|
|
326
|
-
const startsWithWindow = normalized === "$window" || normalized.startsWith("$window.");
|
|
327
|
-
const startsWithToken = normalized.startsWith("$");
|
|
328
|
-
|
|
329
|
-
const candidates = [];
|
|
330
|
-
if (startsWithThis) {
|
|
331
|
-
const path = normalized.replace(/^\$this\.?/, "");
|
|
332
|
-
candidates.push({ root: hostElement || parent, path });
|
|
333
|
-
} else if (startsWithWindow) {
|
|
334
|
-
const path = normalized.replace(/^\$window\.?/, "");
|
|
335
|
-
candidates.push({ root: window, path });
|
|
336
|
-
} else if (startsWithToken) {
|
|
337
|
-
const path = normalized.replace(/^\$/, "");
|
|
338
|
-
candidates.push({ root: hostElement, path });
|
|
339
|
-
candidates.push({ root: parent, path });
|
|
340
|
-
candidates.push({ root: window, path });
|
|
341
|
-
} else {
|
|
342
|
-
candidates.push({ root: hostElement, path: normalized });
|
|
343
|
-
candidates.push({ root: parent, path: normalized });
|
|
344
|
-
candidates.push({ root: window, path: normalized });
|
|
345
|
-
}
|
|
102
|
+
let safeInit = initVal;
|
|
103
|
+
if (initVal) {
|
|
104
|
+
if (!initVal.includes('$') && isNaN(initVal) && initVal !== 'true' && initVal !== 'false' && initVal !== 'null') {
|
|
105
|
+
if (!initVal.startsWith("'") && !initVal.startsWith('"')) safeInit = `'${initVal}'`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Temporarily expose the library to the element so processOperatorsAsync can find and execute it natively
|
|
110
|
+
if (window[activePluginName]) {
|
|
111
|
+
el[activePluginName] = window[activePluginName];
|
|
112
|
+
}
|
|
346
113
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
114
|
+
// Construct initialization string (e.g., $Toastify(...) )
|
|
115
|
+
let initExpr = safeInit ? `$${activePluginName}(${safeInit})` : `$${activePluginName}()`;
|
|
116
|
+
|
|
117
|
+
// Await initialization from the operator engine
|
|
118
|
+
const initResult = await processOperatorsAsync(el, initExpr, [], null, [], new Map([["$this", el]]));
|
|
119
|
+
|
|
120
|
+
if (initResult !== undefined && initResult !== null && initResult !== "") {
|
|
121
|
+
// Overwrite the temporary library function with the returned instance
|
|
122
|
+
el[activePluginName] = initResult;
|
|
123
|
+
existingInstance = initResult;
|
|
124
|
+
}
|
|
354
125
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return { fn: instance[methodName], context: instance, methodName };
|
|
126
|
+
// If this attribute was the base initializer, we're done processing it
|
|
127
|
+
if (cleanAttrName === activePluginName.toLowerCase()) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
360
130
|
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return { fn: undefined, context: undefined, methodName };
|
|
365
|
-
}
|
|
366
131
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
132
|
+
// --- CASE-SENSITIVE PATH RESOLUTION ---
|
|
133
|
+
// Reconstruct the attribute path with correct casing from the instance/element deeply
|
|
134
|
+
let keyParts = attrName.split('.');
|
|
135
|
+
let resolvedParts = [];
|
|
136
|
+
let pointer = null;
|
|
370
137
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
138
|
+
if (isDirectThis) {
|
|
139
|
+
resolvedParts.push('$this');
|
|
140
|
+
pointer = el;
|
|
141
|
+
} else {
|
|
142
|
+
resolvedParts.push(activePluginName);
|
|
143
|
+
pointer = existingInstance || el; // Fallback to element if instance isn't populated
|
|
144
|
+
}
|
|
375
145
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
146
|
+
for (let j = 1; j < keyParts.length; j++) {
|
|
147
|
+
let part = keyParts[j];
|
|
148
|
+
let isMethod = part.endsWith('()');
|
|
149
|
+
let cleanPart = part.replace('()', '');
|
|
150
|
+
let matchedKey = part;
|
|
380
151
|
|
|
381
|
-
|
|
382
|
-
|
|
152
|
+
if (pointer != null) {
|
|
153
|
+
if (pointer[cleanPart] !== undefined) {
|
|
154
|
+
matchedKey = part;
|
|
155
|
+
} else {
|
|
156
|
+
let lower = cleanPart.toLowerCase();
|
|
157
|
+
let realKey = null;
|
|
158
|
+
|
|
159
|
+
let currentObj = pointer;
|
|
160
|
+
while (currentObj) {
|
|
161
|
+
let props = Object.getOwnPropertyNames(currentObj);
|
|
162
|
+
let found = props.find(p => p.toLowerCase() === lower);
|
|
163
|
+
if (found) {
|
|
164
|
+
realKey = found;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
currentObj = Object.getPrototypeOf(currentObj);
|
|
168
|
+
}
|
|
383
169
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
170
|
+
if (realKey) {
|
|
171
|
+
matchedKey = isMethod ? realKey + '()' : realKey;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
pointer = pointer[matchedKey.replace('()', '')];
|
|
388
175
|
}
|
|
176
|
+
resolvedParts.push(matchedKey);
|
|
389
177
|
}
|
|
178
|
+
|
|
179
|
+
let correctedAttrName = resolvedParts.join('.');
|
|
390
180
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function update(Target, val, parent, property, elParent, elProperty, hostElement) {
|
|
399
|
-
// RESOLUTION: Handle case-insensitivity before processing targets.
|
|
400
|
-
// If Target is missing, check parent for a property matching 'property' (case-insensitive).
|
|
401
|
-
if (!Target && parent && property) {
|
|
402
|
-
const lowerProp = String(property).toLowerCase();
|
|
403
|
-
for (const key in parent) {
|
|
404
|
-
if (key.toLowerCase() === lowerProp) {
|
|
405
|
-
Target = parent[key];
|
|
406
|
-
property = key;
|
|
407
|
-
if (elProperty) elProperty = key; // Update element structure key to match real property
|
|
408
|
-
break;
|
|
181
|
+
// --- DECORATION & EXECUTION ---
|
|
182
|
+
// Plugins must be prefixed with $ for the operator engine's property resolution
|
|
183
|
+
let prefixedAttrName = correctedAttrName;
|
|
184
|
+
if (activePluginName && !isDirectThis) {
|
|
185
|
+
prefixedAttrName = "$" + correctedAttrName;
|
|
409
186
|
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
187
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (isReferenceAssignment(val) && parent && property) {
|
|
416
|
-
instance = createFunctionAdapter(val, parent, property, hostElement);
|
|
417
|
-
parent[property] = instance;
|
|
418
|
-
if (elParent && elProperty) elParent[elProperty] = instance;
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
188
|
+
const isMethodCall = correctedAttrName.includes('(') || correctedAttrName.endsWith('()');
|
|
189
|
+
let expression = "";
|
|
421
190
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// Use 'parent' as context (this) if available to maintain class references
|
|
425
|
-
if (parent) {
|
|
426
|
-
if (Array.isArray(val)) {
|
|
427
|
-
instance = Target.apply(parent, val);
|
|
428
|
-
} else {
|
|
429
|
-
instance = Target.call(parent, val);
|
|
430
|
-
}
|
|
191
|
+
if (isMethodCall) {
|
|
192
|
+
expression = prefixedAttrName;
|
|
431
193
|
} else {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
instance = Target(val);
|
|
194
|
+
let safeValue = val;
|
|
195
|
+
if (!val.includes('$') && isNaN(val) && val !== 'true' && val !== 'false' && val !== 'null') {
|
|
196
|
+
if (!val.startsWith("'") && !val.startsWith('"')) safeValue = `'${val}'`;
|
|
436
197
|
}
|
|
198
|
+
expression = `${prefixedAttrName} = ${safeValue}`;
|
|
437
199
|
}
|
|
438
|
-
} else {
|
|
439
|
-
// Call as a Constructor
|
|
440
|
-
if (Array.isArray(val)) {
|
|
441
|
-
instance = new Target(...val);
|
|
442
|
-
} else {
|
|
443
|
-
instance = new Target(val);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Assign the result to the element structure
|
|
448
|
-
if (elParent && elProperty) {
|
|
449
|
-
elParent[elProperty] = instance;
|
|
450
|
-
}
|
|
451
200
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const key = property || (Target && Target.name) || "instance";
|
|
455
|
-
instance.el.__cocreatePluginInstances[key] = instance;
|
|
456
|
-
}
|
|
201
|
+
// Execute using the existing processOperatorsAsync system.
|
|
202
|
+
const result = await processOperatorsAsync(el, expression, [], null, [], new Map([["$this", el]]));
|
|
457
203
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (!elParent[elProperty]) {
|
|
462
|
-
elParent[elProperty] = {};
|
|
463
|
-
}
|
|
464
|
-
const nextElParent = elParent[elProperty];
|
|
465
|
-
|
|
466
|
-
for (let key in val) {
|
|
467
|
-
update(Target[key], val[key], Target, key, nextElParent, key, hostElement);
|
|
204
|
+
// CAPTURE & ASSIGN: If the element still lacks the instance, save the result of the execution to the element
|
|
205
|
+
if (activePluginName && !el[activePluginName] && result !== undefined && result !== null && result !== "") {
|
|
206
|
+
el[activePluginName] = result;
|
|
468
207
|
}
|
|
469
|
-
}
|
|
470
|
-
} else if (parent && property) {
|
|
471
|
-
if (isReferenceAssignment(val)) {
|
|
472
|
-
const adapter = createFunctionAdapter(val, parent, property, hostElement);
|
|
473
|
-
parent[property] = adapter;
|
|
474
|
-
if (elParent && elProperty) elParent[elProperty] = adapter;
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
208
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Map the value to the element structure
|
|
482
|
-
if (elParent && elProperty) {
|
|
483
|
-
elParent[elProperty] = val;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.warn(`[Plugin System] Sequential Execution Error (${attrName}):`, e);
|
|
484
211
|
}
|
|
485
|
-
|
|
486
|
-
console.log(`Set plugin property ${property} to`, val);
|
|
487
212
|
}
|
|
488
|
-
}
|
|
489
213
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
* - $anime.stagger(100)
|
|
496
|
-
* - Global access: $document, $window, etc.
|
|
497
|
-
*/
|
|
498
|
-
function processParams(el, params) {
|
|
499
|
-
if (typeof params === 'string' && params.startsWith('\u0024')) {
|
|
500
|
-
try {
|
|
501
|
-
// 1. Check for Method Call: $root.path.to.func(arg)
|
|
502
|
-
const callMatch = params.match(/^\u0024([^.]+)\.(.+)\((.*)\)$/);
|
|
503
|
-
if (callMatch) {
|
|
504
|
-
const [_, root, path, arg] = callMatch;
|
|
505
|
-
const obj = (root === 'this') ? el : window[root];
|
|
506
|
-
|
|
507
|
-
// If root object exists, drill down
|
|
508
|
-
if (obj) {
|
|
509
|
-
const func = path.split('.').reduce((o, k) => (o || {})[k], obj);
|
|
510
|
-
if (typeof func === 'function') {
|
|
511
|
-
// Parse argument if JSON-like, else string
|
|
512
|
-
const parsedArg = arg ? (function() { try { return JSON.parse(arg); } catch(e) { return arg; } })() : undefined;
|
|
513
|
-
return func(parsedArg);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// 2. Check for Property Access: $root.path.to.prop or just $root
|
|
519
|
-
const propMatch = params.match(/^\u0024([^.]+)(?:\.(.+))?$/);
|
|
520
|
-
if (propMatch) {
|
|
521
|
-
const [_, root, path] = propMatch;
|
|
522
|
-
const obj = (root === 'this') ? el : window[root];
|
|
523
|
-
|
|
524
|
-
if (obj) {
|
|
525
|
-
if (!path) return (obj instanceof HTMLCollection) ? Array.from(obj) : obj;
|
|
526
|
-
|
|
527
|
-
const val = path.split('.').reduce((o, k) => (o || {})[k], obj);
|
|
528
|
-
// Convert HTMLCollections to Arrays
|
|
529
|
-
return (val instanceof HTMLCollection) ? Array.from(val) : val;
|
|
530
|
-
}
|
|
214
|
+
// 3. FALLBACK BOOT
|
|
215
|
+
for (const name of pluginNames) {
|
|
216
|
+
if (!el[name]) {
|
|
217
|
+
if (window[name]) {
|
|
218
|
+
el[name] = window[name];
|
|
531
219
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (window[globalKey]) {
|
|
536
|
-
return window[globalKey];
|
|
220
|
+
const initResult = await processOperatorsAsync(el, `$${name}()`, [], null, [], new Map([["$this", el]]));
|
|
221
|
+
if (initResult !== undefined && initResult !== null && initResult !== "") {
|
|
222
|
+
el[name] = initResult;
|
|
537
223
|
}
|
|
538
|
-
|
|
539
|
-
} catch (e) {
|
|
540
|
-
console.warn("Failed to resolve dynamic token:", params);
|
|
541
224
|
}
|
|
542
225
|
}
|
|
543
|
-
|
|
544
|
-
if (Array.isArray(params)) return params.map(p => processParams(el, p));
|
|
545
|
-
if (typeof params === 'object' && params !== null) {
|
|
546
|
-
const res = {};
|
|
547
|
-
for (let k in params) res[k] = processParams(el, params[k]);
|
|
548
|
-
return res;
|
|
549
|
-
}
|
|
550
|
-
return params;
|
|
551
226
|
}
|
|
552
227
|
|
|
553
|
-
//
|
|
554
|
-
|
|
228
|
+
// Global Startup
|
|
555
229
|
if (typeof document !== 'undefined') {
|
|
556
|
-
|
|
557
|
-
|
|
230
|
+
const selector = "[plugin]";
|
|
231
|
+
Observer.init({
|
|
232
|
+
name: "plugin",
|
|
233
|
+
types: ["addedNodes", "attributes"],
|
|
234
|
+
selector: selector,
|
|
235
|
+
attributeFilter: ["plugin"],
|
|
236
|
+
callback: (mutation) => init(mutation.target)
|
|
237
|
+
});
|
|
238
|
+
|
|
558
239
|
import("./CoCreate.config.js")
|
|
559
240
|
.then((Config) => {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
Object.assign(plugins, Config.plugins);
|
|
563
|
-
}
|
|
564
|
-
else if (Config.default) {
|
|
565
|
-
if (Config.default.plugins) {
|
|
566
|
-
Object.assign(plugins, Config.default.plugins);
|
|
567
|
-
} else {
|
|
568
|
-
Object.assign(plugins, Config.default);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
241
|
+
const data = Config.plugins || Config.default?.plugins || Config.default || {};
|
|
242
|
+
Object.assign(plugins, data);
|
|
571
243
|
})
|
|
572
|
-
.catch((
|
|
573
|
-
|
|
574
|
-
})
|
|
575
|
-
.finally(() => {
|
|
576
|
-
// Start Observer
|
|
577
|
-
Observer.init({
|
|
578
|
-
name: "plugin",
|
|
579
|
-
types: ["addedNodes", "attributes"],
|
|
580
|
-
selector: "[plugin]",
|
|
581
|
-
attributeFilter: ["plugin"],
|
|
582
|
-
callback: (mutation) => {
|
|
583
|
-
init(mutation.target);
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// Initial Init
|
|
588
|
-
init(document.querySelectorAll("[plugin]"));
|
|
589
|
-
});
|
|
244
|
+
.catch(() => {})
|
|
245
|
+
.finally(() => init(document.querySelectorAll(selector)));
|
|
590
246
|
}
|
|
591
247
|
|
|
592
|
-
export default { init, plugins }
|
|
248
|
+
export default { init, plugins };
|