@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 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
- <head>
4
- <title>Plugins API Demo</title>
5
- <meta charset="utf-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <link rel="manifest" href="/manifest.webmanifest" />
8
-
9
- <!-- Bootstrap 5 CSS for Demo Presentation -->
10
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
11
-
12
- <style>
13
- body { background-color: #f8f9fa; padding-bottom: 50px; }
14
- .demo-card { margin-bottom: 2rem; border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
15
- .card-header { background-color: #fff; border-bottom: 1px solid #eee; padding: 1.5rem; }
16
- .operator-badge { font-family: monospace; background: #e9ecef; color: #d63384; padding: 2px 6px; border-radius: 4px; }
17
- </style>
18
- </head>
19
- <body>
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
- <div class="container py-5">
22
- <div class="row mb-4">
23
- <div class="col-12 text-center">
24
- <h1 class="display-5 fw-bold text-primary">CoCreate Plugins API</h1>
25
- <p class="lead text-muted">A declarative, attribute-based system for initializing and configuring JavaScript libraries.</p>
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
- <!-- SECTION 1: THE BASICS -->
30
- <div class="row">
31
- <div class="col-12">
32
- <div class="card demo-card">
33
- <div class="card-header">
34
- <h4 class="m-0">1. The Basics: Declarative Initialization</h4>
35
- </div>
36
- <div class="card-body">
37
- <p>Initialize any supported plugin using the <code>plugin="PluginName"</code> attribute. Configure it using the lowercase attribute matching the plugin name (e.g., <code>pluginname="{ options }"</code>).</p>
38
-
39
- <div class="row align-items-center">
40
- <div class="col-md-6">
41
- <h6 class="text-muted text-uppercase fs-7 fw-bold mb-3">Live Demo</h6>
42
- <!-- Toastify Example -->
43
- <button
44
- type="button"
45
- class="btn btn-success"
46
- plugin="Toastify"
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">&lt;button
61
- plugin="Toastify"
62
- toastify='{
63
- "text": "Hello!",
64
- "duration": 3000,
65
- "style": { "background": "#198754" }
66
- }'
67
- onclick="this.Toastify.showToast()"&gt;
68
- Click Me
69
- &lt;/button&gt;</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
- <!-- SECTION 2: OPERATORS & VARIABLES -->
78
- <div class="row">
79
- <div class="col-12">
80
- <div class="card demo-card">
81
- <div class="card-header">
82
- <h4 class="m-0">2. Variable Operators</h4>
83
- </div>
84
- <div class="card-body">
85
- <p>The API supports special variables to reference DOM elements and global objects dynamically within your JSON configuration.</p>
86
-
87
- <div class="table-responsive mb-4">
88
- <table class="table table-bordered">
89
- <thead class="table-light">
90
- <tr>
91
- <th>Operator</th>
92
- <th>Description</th>
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">&lt;div
129
- plugin="raterJs"
130
- raterjs='[{
131
- "element": "$this",
132
- "starSize": 32,
133
- "rating": 3.5
134
- }]'&gt;
135
- &lt;/div&gt;</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
- <!-- SECTION 3: CONFIGURATION PATTERNS -->
146
- <div class="row">
147
- <div class="col-12">
148
- <div class="card demo-card">
149
- <div class="card-header">
150
- <h4 class="m-0">3. Configuration Patterns (Array vs. Object)</h4>
151
- </div>
152
- <div class="card-body">
153
- <p>Plugins have different constructor signatures. The API handles both by inspecting the JSON structure.</p>
154
-
155
- <div class="row">
156
- <div class="col-md-6">
157
- <div class="card h-100 bg-light border-0">
158
- <div class="card-body">
159
- <h6 class="fw-bold">Pattern A: Single Config Object</h6>
160
- <p class="small text-muted">Used when the plugin accepts a single object containing all settings (e.g., <code>raterJs({ element: div })</code>).</p>
161
- <hr>
162
- <code>config='[{ "element": "$this", "opt": "val" }]'</code>
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
- <!-- SECTION 4: ADVANCED (Callbacks) -->
184
- <div class="row">
185
- <div class="col-12">
186
- <div class="card demo-card">
187
- <div class="card-header">
188
- <h4 class="m-0">4. Advanced: Callbacks & Global Functions</h4>
189
- </div>
190
- <div class="card-body">
191
- <p>Use <code>$window</code> or specific global names (like <code>$Swal</code>) to pass functions into configuration attributes.</p>
192
-
193
- <div class="row align-items-center">
194
- <div class="col-md-6">
195
- <!-- SweetAlert Example -->
196
- <button
197
- type="button"
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">&lt;button
211
- onclick="Swal.fire({
212
- title: 'Ready?',
213
- preConfirm: '$window.myCustomFunction'
214
- }) &gt;
215
- Launch
216
- &lt;/button&gt;</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
- <!-- CoCreate Engine -->
227
- <script src="https://CoCreate.app/dist/CoCreate.js"></script>
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
- </body>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/plugins",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "CoCreate plugins",
5
5
  "author": "CoCreate LLC",
6
6
  "license": "AGPL-3.0",
package/src/index.js CHANGED
@@ -1,592 +1,248 @@
1
1
  import Observer from "@cocreate/observer";
2
- import {dotNotationToObject} from "@cocreate/utils";
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. Can be strings (URLs) or objects with src, integrity, etc
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
- function init(elements) {
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 an individual element to detect, load resources for, and execute plugins.
62
- * Reads the 'plugin' attribute (e.g., plugin="chart, map") to identify targets.
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
- if (!rawAttr) return;
70
-
71
- const pluginNames = rawAttr.split(',').map(s => s.trim());
34
+ const pluginNames = rawAttr ? rawAttr.split(',').map(s => s.trim()) : [];
72
35
 
73
- for (const pluginName of pluginNames) {
74
- const pluginDef = plugins[pluginName];
75
-
76
- // Only attempt to load resources if a configuration exists
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.href = href;
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
- const integrity = typeof item === 'object' ? item.integrity : null;
115
- const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
116
-
53
+ if (!src) continue;
117
54
  if (!scriptCache.has(src)) {
118
- const scriptPromise = new Promise((resolve, reject) => {
119
- // Check if already in DOM (manual load)
120
- const existing = document.querySelector(`script[src="${src}"]`);
121
- if (existing) {
122
- if (existing.dataset.loaded === "true") {
123
- resolve();
124
- } else {
125
- const prevOnload = existing.onload;
126
- existing.onload = () => {
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
- } catch (err) {
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
- * Helper to determine if a function should be called with 'new'.
197
- * Uses heuristics like ES6 class syntax, lack of prototype (arrow function), or PascalCase naming.
198
- * * @param {Function} func - The function to check.
199
- * @param {string} [name] - The property name associated with the function (for casing check).
200
- * @returns {boolean} True if the function appears to be a constructor.
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
- * Executes the logic for a generic plugin on a specific element.
215
- * Handles:
216
- * 1. Resolving the target class/function from window.
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
- try {
239
- rawData[key] = JSON.parse(attr.value);
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
- // 2. Resolve parameters (Token Resolver)
247
- let resolved = processParams(el, rawData);
248
- resolved = dotNotationToObject(resolved);
92
+ try {
93
+ let val = attr.value.trim();
94
+ if (val === "") val = "true";
249
95
 
250
- let Plugin = window[name] || window[prefix];
251
- if (!Plugin) {
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
- // Iterate over resolved object.
257
- // Since we use dotNotationToObject, keys like "swiper.effect" are already nested as { swiper: { effect: ... } }
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
- // Pass context: Window as parent, Plugin Name as property (for potential context binding)
268
- // el and name used to store the result on the element.
269
- update(Target, val, window, name, el, name, el);
270
-
271
- console.log(`Processed ${name}`);
272
- } catch (e) {
273
- console.error(`Error processing ${name}:`, e);
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
- for (const candidate of candidates) {
348
- if (!candidate.root) continue;
349
- const { parent: resolvedParent, value } = resolvePathWithParent(candidate.root, candidate.path);
350
- if (typeof value === "function") {
351
- return { fn: value, context: resolvedParent, methodName };
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
- if (methodName) {
356
- const instances = getPluginInstancesFromElement(hostElement || parent);
357
- for (const instance of instances) {
358
- if (instance && typeof instance[methodName] === "function") {
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
- function createFunctionAdapter(refPath, parent, property, hostElement) {
368
- const normalizedRefPath = normalizeReferencePath(refPath);
369
- const methodName = normalizedRefPath.split(".").pop();
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
- return function (...args) {
372
- const resolved = resolveCallableReference(normalizedRefPath, parent, hostElement);
373
- const fn = resolved.fn;
374
- const context = resolved.context;
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
- if (typeof fn !== "function") {
377
- console.error(`Plugin adapter failed: "${normalizedRefPath}" did not resolve to a function for ${property}.`);
378
- return;
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
- if (property === "setValue") {
382
- const payload = normalizeCrudPayload(args[0]);
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
- if (methodName === "addEventSource" && context && typeof context.getEventSources === "function") {
385
- const sources = context.getEventSources();
386
- if (Array.isArray(sources)) {
387
- sources.forEach(source => source && typeof source.remove === "function" && source.remove());
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
- return fn.call(context || this, payload);
392
- }
393
-
394
- return fn.apply(context || this, args);
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
- let instance;
414
- if (typeof Target === 'function') {
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
- if (!isConstructor(Target, property)) {
423
- // Call as a function (method or standalone)
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
- if (Array.isArray(val)) {
433
- instance = Target(...val);
434
- } else {
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
- if (instance && instance.el && typeof instance.el === "object") {
453
- if (!instance.el.__cocreatePluginInstances) instance.el.__cocreatePluginInstances = {};
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
- } else if (typeof Target === 'object' && Target !== null && typeof val === 'object' && val !== null && !Array.isArray(val)) {
459
- // Prepare the next level of the element structure
460
- if (elParent && elProperty) {
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
- // If it's not a function, we are setting a value on the plugin object
479
- parent[property] = val;
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
- * Generic Parameter Processor
492
- * Handles:
493
- * - $this / $this.children
494
- * - $window.path.to.function(arg)
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
- // 3. Check for standalone globals like $document or $window
534
- const globalKey = params.substring(1);
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
- // --- STARTUP LOGIC ---
554
-
228
+ // Global Startup
555
229
  if (typeof document !== 'undefined') {
556
- // Dynamic Import: Loads config if available, handles error if missing.
557
- // Works with 'npm start' (Bundlers) by creating a code-split chunk.
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
- // LOGIC: Merge exports into plugins object
561
- if (Config.plugins) {
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((err) => {
573
- // Optional: fail silently for optional config
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 };