@cocreate/plugins 1.2.0 → 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 +19 -0
- package/demo/index.html +110 -209
- package/package.json +1 -1
- package/src/index.js +168 -341
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
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
|
+
|
|
13
|
+
## [1.2.1](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.2.0...v1.2.1) (2026-03-02)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Handle case-insensitive plugin global mapping and add support for function reference assignments in plugin properties. ([904a947](https://github.com/CoCreate-app/CoCreate-plugins/commit/904a947abc868937b6e650b62e080d00341272a1))
|
|
19
|
+
|
|
1
20
|
# [1.2.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.1.1...v1.2.0) (2026-02-15)
|
|
2
21
|
|
|
3
22
|
|
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,421 +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
51
|
for (const item of pluginDef.js) {
|
|
111
52
|
const src = typeof item === 'string' ? item : item.src;
|
|
112
|
-
|
|
113
|
-
const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
|
|
114
|
-
|
|
53
|
+
if (!src) continue;
|
|
115
54
|
if (!scriptCache.has(src)) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (prevOnload) prevOnload();
|
|
126
|
-
existing.dataset.loaded = "true";
|
|
127
|
-
resolve();
|
|
128
|
-
};
|
|
129
|
-
existing.onerror = reject;
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
const s = document.createElement("script");
|
|
133
|
-
s.src = src;
|
|
134
|
-
if (integrity) {
|
|
135
|
-
s.integrity = integrity;
|
|
136
|
-
s.crossOrigin = crossOrigin || "anonymous";
|
|
137
|
-
}
|
|
138
|
-
s.onload = () => {
|
|
139
|
-
s.dataset.loaded = "true";
|
|
140
|
-
resolve();
|
|
141
|
-
};
|
|
142
|
-
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();
|
|
143
64
|
document.head.appendChild(s);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
scriptCache.set(src, scriptPromise);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
await scriptCache.get(src);
|
|
151
|
-
} catch (e) {
|
|
152
|
-
console.error(`Failed to load script: ${src}`, e);
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
153
67
|
}
|
|
68
|
+
await scriptCache.get(src);
|
|
154
69
|
}
|
|
155
70
|
}
|
|
156
71
|
}
|
|
157
|
-
|
|
158
|
-
// Attempt to execute plugin even if no config was found (it might be on window already)
|
|
159
|
-
executeGenericPlugin(el, pluginName);
|
|
160
72
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Helper to determine if a function should be called with 'new'.
|
|
165
|
-
* Uses heuristics like ES6 class syntax, lack of prototype (arrow function), or PascalCase naming.
|
|
166
|
-
* * @param {Function} func - The function to check.
|
|
167
|
-
* @param {string} [name] - The property name associated with the function (for casing check).
|
|
168
|
-
* @returns {boolean} True if the function appears to be a constructor.
|
|
169
|
-
*/
|
|
170
|
-
const isConstructor = (func, name) => {
|
|
171
|
-
try {
|
|
172
|
-
if (typeof func !== 'function') return false;
|
|
173
|
-
if (/^\s*class\s+/.test(func.toString())) return true;
|
|
174
|
-
if (!func.prototype) return false;
|
|
175
|
-
const n = name || func.name;
|
|
176
|
-
if (n && /^[A-Z]/.test(n)) return true;
|
|
177
|
-
} catch(e) {}
|
|
178
|
-
return false;
|
|
179
|
-
};
|
|
180
73
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const prefix = name.toLowerCase();
|
|
193
|
-
const mainAttr = el.getAttribute(prefix);
|
|
194
|
-
let rawData = {};
|
|
195
|
-
|
|
196
|
-
for (let attr of el.attributes) {
|
|
197
|
-
let key = attr.name;
|
|
198
|
-
if (key === prefix) {
|
|
199
|
-
key = name;
|
|
200
|
-
} else if (key.startsWith(prefix + '-')) {
|
|
201
|
-
key = key.replaceAll("-", ".");
|
|
202
|
-
} else if (!key.startsWith(prefix + '.')) {
|
|
203
|
-
continue
|
|
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();
|
|
80
|
+
|
|
81
|
+
const isDirectThis = attrName.startsWith('$this.');
|
|
82
|
+
let cleanAttrName = attrLower;
|
|
83
|
+
if (isDirectThis) {
|
|
84
|
+
cleanAttrName = attrLower.substring(6); // Strip '$this.' for plugin matching
|
|
204
85
|
}
|
|
205
86
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} catch(e) {
|
|
209
|
-
rawData[key] = attr.value;
|
|
210
|
-
}
|
|
87
|
+
// Router: Find if this matches a listed plugin
|
|
88
|
+
const activePluginName = pluginNames.find(p => cleanAttrName === p.toLowerCase() || cleanAttrName.startsWith(p.toLowerCase() + '.'));
|
|
211
89
|
|
|
212
|
-
|
|
90
|
+
if (!activePluginName && !isDirectThis) continue;
|
|
213
91
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
92
|
+
try {
|
|
93
|
+
let val = attr.value.trim();
|
|
94
|
+
if (val === "") val = "true";
|
|
217
95
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
console.error(`Plugin for ${name} not found on window.`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
96
|
+
// BOOTSTRAPPING (Initialize on First Sight via Operator Engine)
|
|
97
|
+
let existingInstance = activePluginName && el[activePluginName];
|
|
223
98
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
for (let key in resolved) {
|
|
227
|
-
// We generally expect the root key to match the plugin name (e.g., 'swiper')
|
|
228
|
-
// We unwrap this root key to pass the actual config to the Plugin.
|
|
229
|
-
if (key === name || key.toLowerCase() === prefix) {
|
|
230
|
-
try {
|
|
231
|
-
// Determine Target: Use existing instance on element if available, else use Window Plugin
|
|
232
|
-
let Target = el[name] || Plugin;
|
|
233
|
-
let val = resolved[key];
|
|
99
|
+
if (activePluginName && !existingInstance) {
|
|
100
|
+
const initVal = (cleanAttrName === activePluginName.toLowerCase()) ? val : null;
|
|
234
101
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function update(Target, val, parent, property, elParent, elProperty) {
|
|
248
|
-
// RESOLUTION: Handle case-insensitivity before processing targets.
|
|
249
|
-
// If Target is missing, check parent for a property matching 'property' (case-insensitive).
|
|
250
|
-
if (!Target && parent && property) {
|
|
251
|
-
const lowerProp = String(property).toLowerCase();
|
|
252
|
-
for (const key in parent) {
|
|
253
|
-
if (key.toLowerCase() === lowerProp) {
|
|
254
|
-
Target = parent[key];
|
|
255
|
-
property = key;
|
|
256
|
-
if (elProperty) elProperty = key; // Update element structure key to match real property
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
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
|
+
}
|
|
261
113
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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;
|
|
272
124
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
instance = Target(val);
|
|
125
|
+
|
|
126
|
+
// If this attribute was the base initializer, we're done processing it
|
|
127
|
+
if (cleanAttrName === activePluginName.toLowerCase()) {
|
|
128
|
+
continue;
|
|
278
129
|
}
|
|
279
130
|
}
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
131
|
+
|
|
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;
|
|
137
|
+
|
|
138
|
+
if (isDirectThis) {
|
|
139
|
+
resolvedParts.push('$this');
|
|
140
|
+
pointer = el;
|
|
284
141
|
} else {
|
|
285
|
-
|
|
142
|
+
resolvedParts.push(activePluginName);
|
|
143
|
+
pointer = existingInstance || el; // Fallback to element if instance isn't populated
|
|
286
144
|
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Assign the result to the element structure
|
|
290
|
-
if (elParent && elProperty) {
|
|
291
|
-
elParent[elProperty] = instance;
|
|
292
|
-
}
|
|
293
145
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
const nextElParent = elParent[elProperty];
|
|
301
|
-
|
|
302
|
-
for (let key in val) {
|
|
303
|
-
update(Target[key], val[key], Target, key, nextElParent, key);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
} else if (parent && property) {
|
|
307
|
-
// If it's not a function, we are setting a value on the plugin object
|
|
308
|
-
parent[property] = val;
|
|
309
|
-
|
|
310
|
-
// Map the value to the element structure
|
|
311
|
-
if (elParent && elProperty) {
|
|
312
|
-
elParent[elProperty] = val;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
console.log(`Set plugin property ${property} to`, val);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
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;
|
|
318
151
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Parse argument if JSON-like, else string
|
|
341
|
-
const parsedArg = arg ? (function() { try { return JSON.parse(arg); } catch(e) { return arg; } })() : undefined;
|
|
342
|
-
return func(parsedArg);
|
|
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
|
+
}
|
|
169
|
+
|
|
170
|
+
if (realKey) {
|
|
171
|
+
matchedKey = isMethod ? realKey + '()' : realKey;
|
|
172
|
+
}
|
|
343
173
|
}
|
|
174
|
+
pointer = pointer[matchedKey.replace('()', '')];
|
|
344
175
|
}
|
|
176
|
+
resolvedParts.push(matchedKey);
|
|
345
177
|
}
|
|
346
178
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
179
|
+
let correctedAttrName = resolvedParts.join('.');
|
|
180
|
+
|
|
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;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const isMethodCall = correctedAttrName.includes('(') || correctedAttrName.endsWith('()');
|
|
189
|
+
let expression = "";
|
|
190
|
+
|
|
191
|
+
if (isMethodCall) {
|
|
192
|
+
expression = prefixedAttrName;
|
|
193
|
+
} else {
|
|
194
|
+
let safeValue = val;
|
|
195
|
+
if (!val.includes('$') && isNaN(val) && val !== 'true' && val !== 'false' && val !== 'null') {
|
|
196
|
+
if (!val.startsWith("'") && !val.startsWith('"')) safeValue = `'${val}'`;
|
|
359
197
|
}
|
|
198
|
+
expression = `${prefixedAttrName} = ${safeValue}`;
|
|
360
199
|
}
|
|
361
200
|
|
|
362
|
-
//
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
201
|
+
// Execute using the existing processOperatorsAsync system.
|
|
202
|
+
const result = await processOperatorsAsync(el, expression, [], null, [], new Map([["$this", el]]));
|
|
203
|
+
|
|
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;
|
|
366
207
|
}
|
|
367
208
|
|
|
368
209
|
} catch (e) {
|
|
369
|
-
console.warn(
|
|
210
|
+
console.warn(`[Plugin System] Sequential Execution Error (${attrName}):`, e);
|
|
370
211
|
}
|
|
371
212
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
213
|
+
|
|
214
|
+
// 3. FALLBACK BOOT
|
|
215
|
+
for (const name of pluginNames) {
|
|
216
|
+
if (!el[name]) {
|
|
217
|
+
if (window[name]) {
|
|
218
|
+
el[name] = window[name];
|
|
219
|
+
}
|
|
220
|
+
const initResult = await processOperatorsAsync(el, `$${name}()`, [], null, [], new Map([["$this", el]]));
|
|
221
|
+
if (initResult !== undefined && initResult !== null && initResult !== "") {
|
|
222
|
+
el[name] = initResult;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
378
225
|
}
|
|
379
|
-
return params;
|
|
380
226
|
}
|
|
381
227
|
|
|
382
|
-
//
|
|
383
|
-
|
|
228
|
+
// Global Startup
|
|
384
229
|
if (typeof document !== 'undefined') {
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
|
|
387
239
|
import("./CoCreate.config.js")
|
|
388
240
|
.then((Config) => {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
Object.assign(plugins, Config.plugins);
|
|
392
|
-
}
|
|
393
|
-
else if (Config.default) {
|
|
394
|
-
if (Config.default.plugins) {
|
|
395
|
-
Object.assign(plugins, Config.default.plugins);
|
|
396
|
-
} else {
|
|
397
|
-
Object.assign(plugins, Config.default);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
})
|
|
401
|
-
.catch((err) => {
|
|
402
|
-
// Optional: fail silently for optional config
|
|
241
|
+
const data = Config.plugins || Config.default?.plugins || Config.default || {};
|
|
242
|
+
Object.assign(plugins, data);
|
|
403
243
|
})
|
|
404
|
-
.
|
|
405
|
-
|
|
406
|
-
Observer.init({
|
|
407
|
-
name: "plugin",
|
|
408
|
-
types: ["addedNodes", "attributes"],
|
|
409
|
-
selector: "[plugin]",
|
|
410
|
-
attributeFilter: ["plugin"],
|
|
411
|
-
callback: (mutation) => {
|
|
412
|
-
init(mutation.target);
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// Initial Init
|
|
417
|
-
init(document.querySelectorAll("[plugin]"));
|
|
418
|
-
});
|
|
244
|
+
.catch(() => {})
|
|
245
|
+
.finally(() => init(document.querySelectorAll(selector)));
|
|
419
246
|
}
|
|
420
247
|
|
|
421
|
-
export default { init, plugins }
|
|
248
|
+
export default { init, plugins };
|