@cocreate/plugins 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/index.js +170 -300
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.1.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.0.3...v1.1.0) (2026-01-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * add marker to indicate where css files will be inserted ([6b4b030](https://github.com/CoCreate-app/CoCreate-plugins/commit/6b4b0301acdabff19b8cca5968cc67619e417c92))
7
+
8
+ ## [1.0.3](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.0.2...v1.0.3) (2025-12-26)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * update worklow ([8976d08](https://github.com/CoCreate-app/CoCreate-plugins/commit/8976d080f8972e840ff5530d5dfb15f839e9ac5b))
14
+
1
15
  ## [1.0.2](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.0.1...v1.0.2) (2025-12-25)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/plugins",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "CoCreate plugins",
5
5
  "author": "CoCreate LLC",
6
6
  "license": "AGPL-3.0",
package/src/index.js CHANGED
@@ -6,17 +6,14 @@ const plugins = {
6
6
  Toastify: {
7
7
  js: [{ src: "https://cdn.jsdelivr.net/npm/toastify-js", crossOrigin: "anonymous" }],
8
8
  css: ["https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css"]
9
- // attribute prefix: toastify-
10
9
  },
11
10
  Choices: {
12
11
  js: [{ src: "https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js", crossOrigin: "anonymous" }],
13
12
  css: ["https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css"]
14
- // attribute prefix: choices-
15
13
  },
16
14
  flatpickr: {
17
15
  js: [{ src: "https://cdn.jsdelivr.net/npm/flatpickr", crossOrigin: "anonymous" }],
18
16
  css: ["https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"]
19
- // attribute prefix: flatpickr-
20
17
  },
21
18
  Quill: {
22
19
  js: [{ src: "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.min.js", crossOrigin: "anonymous" }],
@@ -25,47 +22,37 @@ const plugins = {
25
22
  "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css",
26
23
  "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.bubble.css"
27
24
  ]
28
- // attribute prefix: quill-
29
25
  },
30
26
  ClassicEditor: {
31
27
  js: [{ src: "https://cdn.ckeditor.com/ckeditor5/41.2.0/classic/ckeditor.js", crossOrigin: "anonymous" }]
32
- // attribute prefix: classiceditor-
33
28
  },
34
29
  Dropzone: {
35
30
  js: [{ src: "https://unpkg.com/dropzone@5/dist/min/dropzone.min.js", crossOrigin: "anonymous" }],
36
31
  css: ["https://unpkg.com/dropzone@5/dist/min/dropzone.min.css"]
37
- // attribute prefix: dropzone-
38
32
  },
39
33
  SimpleBar: {
40
34
  js: [{ src: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js", crossOrigin: "anonymous" }],
41
35
  css: ["https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css"]
42
- // attribute prefix: simplebar-
43
36
  },
44
37
  GLightbox: {
45
38
  js: [{ src: "https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js", crossOrigin: "anonymous" }],
46
39
  css: ["https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css"]
47
- // attribute prefix: glightbox-
48
40
  },
49
41
  FgEmojiPicker: {
50
42
  js: [{ src: "https://cdn.jsdelivr.net/npm/fg-emoji-picker/fgEmojiPicker.js", crossOrigin: "anonymous" }]
51
- // attribute prefix: fgemojipicker-
52
43
  },
53
44
  bootstrap: {
54
45
  js: [{ src: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js", crossOrigin: "anonymous" }]
55
- // attribute prefix: bootstrap-
56
46
  },
57
47
  Waves: {
58
48
  js: [{ src: "https://cdn.jsdelivr.net/npm/node-waves/dist/waves.min.js", crossOrigin: "anonymous" }],
59
49
  css: ["https://cdn.jsdelivr.net/npm/node-waves/dist/waves.min.css"]
60
- // attribute prefix: waves-
61
50
  },
62
51
  feather: {
63
52
  js: [{ src: "https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js", crossOrigin: "anonymous" }]
64
- // attribute prefix: feather-
65
53
  },
66
54
  ApexCharts: {
67
55
  js: [{ src: "https://cdn.jsdelivr.net/npm/apexcharts", crossOrigin: "anonymous" }]
68
- // attribute prefix: apexcharts-
69
56
  },
70
57
  jsVectorMap: {
71
58
  js: [
@@ -73,41 +60,33 @@ const plugins = {
73
60
  { src: "https://cdn.jsdelivr.net/npm/jsvectormap/dist/maps/world.js", crossOrigin: "anonymous" }
74
61
  ],
75
62
  css: ["https://cdn.jsdelivr.net/npm/jsvectormap/dist/css/jsvectormap.min.css"]
76
- // attribute prefix: jsvectormap-
77
63
  },
78
64
  Swiper: {
79
65
  js: [{ src: "https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js", crossOrigin: "anonymous" }],
80
66
  css: ["https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"]
81
- // attribute prefix: swiper-
82
67
  },
83
68
  List: {
84
69
  js: [
85
70
  { src: "https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js", crossOrigin: "anonymous" },
86
71
  { src: "https://cdnjs.cloudflare.com/ajax/libs/list.pagination.js/0.1.1/list.pagination.min.js", crossOrigin: "anonymous" }
87
72
  ]
88
- // attribute prefix: list-
89
73
  },
90
74
  Swal: {
91
75
  js: [{ src: "https://cdn.jsdelivr.net/npm/sweetalert2@11", crossOrigin: "anonymous" }],
92
76
  css: ["https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css"]
93
- // attribute prefix: swal-
94
77
  },
95
78
  FullCalendar: {
96
79
  js: [{ src: "https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js", crossOrigin: "anonymous" }]
97
- // attribute prefix: fullcalendar-
98
80
  },
99
81
  Cleave: {
100
82
  js: [{ src: "https://cdn.jsdelivr.net/npm/cleave.js/dist/cleave.min.js", crossOrigin: "anonymous" }]
101
- // attribute prefix: cleave-
102
83
  },
103
84
  noUiSlider: {
104
85
  js: [{ src: "https://cdn.jsdelivr.net/npm/nouislider/dist/nouislider.min.js", crossOrigin: "anonymous" }],
105
86
  css: ["https://cdn.jsdelivr.net/npm/nouislider/dist/nouislider.min.css"]
106
- // attribute prefix: nouislider-
107
87
  },
108
88
  wNumb: {
109
89
  js: [{ src: "https://cdn.jsdelivr.net/npm/wnumb/wNumb.min.js", crossOrigin: "anonymous" }]
110
- // attribute prefix: wnumb-
111
90
  },
112
91
  Grid: {
113
92
  js: [
@@ -115,7 +94,6 @@ const plugins = {
115
94
  { src: "https://unpkg.com/gridjs/plugins/selection/dist/selection.umd.js", crossOrigin: "anonymous" }
116
95
  ],
117
96
  css: ["https://unpkg.com/gridjs/dist/theme/mermaid.min.css"]
118
- // attribute prefix: grid-
119
97
  },
120
98
  FilePond: {
121
99
  js: [
@@ -129,83 +107,74 @@ const plugins = {
129
107
  "https://unpkg.com/filepond/dist/filepond.min.css",
130
108
  "https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css"
131
109
  ]
132
- // attribute prefix: filepond-
133
110
  },
134
111
  Prism: {
135
112
  js: [{ src: "https://cdn.jsdelivr.net/npm/prismjs/prism.min.js", crossOrigin: "anonymous" }],
136
113
  css: ["https://cdn.jsdelivr.net/npm/prismjs/themes/prism.min.css"]
137
- // attribute prefix: prism-
138
114
  },
139
115
  Isotope: {
140
116
  js: [{ src: "https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.min.js", crossOrigin: "anonymous" }]
141
- // attribute prefix: isotope-
142
117
  },
143
118
  particlesJS: {
144
119
  js: [{ src: "https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js", crossOrigin: "anonymous" }]
145
- // attribute prefix: particlesjs-
146
120
  },
147
121
  dragula: {
148
122
  js: [{ src: "https://cdn.jsdelivr.net/npm/dragula/dist/dragula.min.js", crossOrigin: "anonymous" }],
149
123
  css: ["https://cdn.jsdelivr.net/npm/dragula/dist/dragula.min.css"]
150
- // attribute prefix: dragula-
151
124
  },
152
125
  DomAutoscroller: {
153
126
  js: [{ src: "https://cdn.jsdelivr.net/npm/dom-autoscroller", crossOrigin: "anonymous" }]
154
- // attribute prefix: domautoscroller-
155
127
  },
156
128
  Card: {
157
129
  js: [{ src: "https://cdn.jsdelivr.net/npm/card/dist/card.js", crossOrigin: "anonymous" }]
158
- // attribute prefix: card-
159
130
  },
160
131
  Chart: {
161
132
  js: [{ src: "https://cdn.jsdelivr.net/npm/chart.js", crossOrigin: "anonymous" }]
162
- // attribute prefix: chart-
163
133
  },
164
134
  echarts: {
165
135
  js: [{ src: "https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js", crossOrigin: "anonymous" }]
166
- // attribute prefix: echarts-
167
136
  },
168
137
  Multi: {
169
138
  js: [{ src: "https://cdn.jsdelivr.net/npm/multi.js/dist/multi.min.js", crossOrigin: "anonymous" }],
170
139
  css: ["https://cdn.jsdelivr.net/npm/multi.js/dist/multi.min.css"]
171
- // attribute prefix: multi-
172
140
  },
173
141
  autoComplete: {
174
142
  js: [{ src: "https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/autoComplete.min.js", crossOrigin: "anonymous" }],
175
143
  css: ["https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/css/autoComplete.01.min.css"]
176
- // attribute prefix: autocomplete-
177
144
  },
178
145
  Pickr: {
179
146
  js: [{ src: "https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js", crossOrigin: "anonymous" }],
180
147
  css: ["https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/classic.min.css"]
181
- // attribute prefix: pickr-
182
148
  },
183
149
  Shepherd: {
184
150
  js: [{ src: "https://cdn.jsdelivr.net/npm/shepherd.js/dist/js/shepherd.min.js", crossOrigin: "anonymous" }],
185
151
  css: ["https://cdn.jsdelivr.net/npm/shepherd.js/dist/css/shepherd.css"]
186
- // attribute prefix: shepherd-
187
152
  },
188
153
  GMaps: {
189
154
  js: [{ src: "https://cdn.jsdelivr.net/npm/gmaps/gmaps.min.js", crossOrigin: "anonymous" }]
190
- // attribute prefix: gmaps-
191
155
  },
192
156
  L: {
193
157
  js: [{ src: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js", crossOrigin: "anonymous" }],
194
158
  css: ["https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"]
195
- // attribute prefix: l-
196
159
  },
197
160
  Masonry: {
198
161
  js: [{ src: "https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js", crossOrigin: "anonymous" }]
199
- // attribute prefix: masonry-
200
162
  },
201
163
  Rater: {
202
164
  js: [{ src: "https://cdn.jsdelivr.net/npm/rater-js/index.js", crossOrigin: "anonymous" }]
203
- // attribute prefix: rater-
165
+ },
166
+ Anime: {
167
+ js: [{ src: "https://cdn.jsdelivr.net/npm/animejs@3.2.2/lib/anime.min.js", crossOrigin: "anonymous" }]
204
168
  }
205
169
  };
206
170
 
207
171
  // --- CORE ENGINE ---
208
172
 
173
+ // Global Cache for script promises to prevent race conditions and duplicate loads
174
+ const scriptCache = new Map();
175
+ // Cache the CSS marker once on load
176
+ const cssMarker = typeof document !== 'undefined' ? document.querySelector('link[plugins]') : null;
177
+
209
178
  /**
210
179
  * Global Initialization Function
211
180
  * Processes one or more elements to attach plugins.
@@ -214,323 +183,219 @@ const plugins = {
214
183
  export function init(elements) {
215
184
  if (!elements) return;
216
185
 
217
- // 1. Detect Type & Normalize to Array
218
- // Normalize string handling (removed), Element, NodeList, HTMLCollection, Array
219
186
  let collection = [];
220
-
221
187
  if (elements instanceof HTMLElement || elements instanceof Element) {
222
188
  collection = [elements];
223
189
  } else if (elements.length !== undefined && typeof elements !== 'function') {
224
- // Handle Array-like objects (NodeList, HTMLCollection, Array)
225
190
  collection = Array.from(elements);
226
191
  } else {
227
- // Fallback for single object that isn't element (unlikely given spec, but safe)
228
192
  collection = [elements];
229
193
  }
230
194
 
231
- // 2. Iterate and Process
232
195
  collection.forEach(el => {
233
- // Ensure valid element before processing
234
196
  if (el && typeof el.getAttribute === 'function') {
235
197
  processPlugin(el);
236
198
  }
237
199
  });
238
200
  }
239
201
 
240
- // Auto-Init on load
241
- if (typeof document !== 'undefined') {
242
- document.addEventListener('DOMContentLoaded', () => {
243
- init(document.querySelectorAll("[plugin]"));
244
- });
245
- }
246
-
247
202
  async function processPlugin(el) {
248
203
  const rawAttr = el.getAttribute("plugin");
249
204
  if (!rawAttr) return;
250
205
 
251
- const pluginName = rawAttr.split(',')[0].trim();
206
+ const pluginNames = rawAttr.split(',').map(s => s.trim());
252
207
 
253
- // SECURITY CHECK:
254
- // Look up the definition in our hardcoded allowlist.
255
- const pluginDef = plugins[pluginName];
256
-
257
- if (pluginDef) {
258
- console.log(`[Plugin System] Loading ${pluginName}...`);
208
+ for (const pluginName of pluginNames) {
209
+ const pluginDef = plugins[pluginName];
210
+ if (!pluginDef) continue;
259
211
 
260
- // Load CSS
261
- if(pluginDef.css) pluginDef.css.forEach(href => {
262
- if (!document.querySelector(`link[href="${href}"]`)) {
212
+ // Load CSS
213
+ if (pluginDef.css) pluginDef.css.forEach(href => {
214
+ if (!document.querySelector(`link[href="${href}"]`)) {
263
215
  const link = document.createElement("link");
264
- link.rel="stylesheet"; link.href=href; document.head.appendChild(link);
216
+ link.rel = "stylesheet";
217
+ link.href = href;
218
+
219
+ // CSS INJECTION STRATEGY:
220
+ // 1. Priority: Check for a specific marker element <link plugin>
221
+ // (Cached globally in cssMarker)
222
+
223
+ if (cssMarker) {
224
+ // Insert before the marker
225
+ cssMarker.parentNode.insertBefore(link, cssMarker);
226
+ } else {
227
+ // 2. Fallback: Prepend before existing CSS
228
+ // To allow custom CSS to easily override plugin defaults, we must ensure
229
+ // plugin CSS loads BEFORE user CSS.
230
+ const firstStyle = document.head.querySelector('link[rel="stylesheet"], style');
231
+
232
+ if (firstStyle) {
233
+ document.head.insertBefore(link, firstStyle);
234
+ } else {
235
+ // If no CSS exists yet, appending is safe
236
+ document.head.appendChild(link);
237
+ }
265
238
  }
266
- });
267
-
268
- // Load JS with Integrity Checks
269
- if(pluginDef.js) {
270
- for(const item of pluginDef.js) {
271
- const src = typeof item === 'string' ? item : item.src;
272
- const integrity = typeof item === 'object' ? item.integrity : null;
273
- const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
274
-
275
- if (document.querySelector(`script[src="${src}"]`)) continue;
276
-
277
- await new Promise((resolve, reject) => {
278
- const s = document.createElement("script");
279
- s.src = src;
280
- if (integrity) {
281
- s.integrity = integrity;
282
- s.crossOrigin = crossOrigin || "anonymous";
239
+ }
240
+ });
241
+
242
+ // Load JS with Promise Cache
243
+ if (pluginDef.js) {
244
+ for (const item of pluginDef.js) {
245
+ const src = typeof item === 'string' ? item : item.src;
246
+ const integrity = typeof item === 'object' ? item.integrity : null;
247
+ const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
248
+
249
+ if (!scriptCache.has(src)) {
250
+ const scriptPromise = new Promise((resolve, reject) => {
251
+ // Check if already in DOM (manual load)
252
+ const existing = document.querySelector(`script[src="${src}"]`);
253
+ if (existing) {
254
+ if (existing.dataset.loaded === "true") {
255
+ resolve();
256
+ } else {
257
+ const prevOnload = existing.onload;
258
+ existing.onload = () => {
259
+ if (prevOnload) prevOnload();
260
+ existing.dataset.loaded = "true";
261
+ resolve();
262
+ };
263
+ existing.onerror = reject;
264
+ }
265
+ } else {
266
+ const s = document.createElement("script");
267
+ s.src = src;
268
+ if (integrity) {
269
+ s.integrity = integrity;
270
+ s.crossOrigin = crossOrigin || "anonymous";
271
+ }
272
+ s.onload = () => {
273
+ s.dataset.loaded = "true";
274
+ resolve();
275
+ };
276
+ s.onerror = reject;
277
+ document.head.appendChild(s);
283
278
  }
284
- s.onload = resolve;
285
- s.onerror = reject;
286
- document.head.appendChild(s);
287
279
  });
280
+ scriptCache.set(src, scriptPromise);
288
281
  }
289
- }
290
-
291
- // Initialize after load
292
- executeGenericPlugin(el, pluginName);
293
282
 
294
- } else {
295
- console.warn(`[Plugin System] Blocked: Plugin "${pluginName}" is not in the allowlist.`);
283
+ try {
284
+ await scriptCache.get(src);
285
+ } catch (e) {
286
+ console.error(`Failed to load script: ${src}`, e);
287
+ }
288
+ }
289
+ }
290
+
291
+ executeGenericPlugin(el, pluginName);
296
292
  }
297
293
  }
298
294
 
299
- async function executeGenericPlugin(el, pluginName) {
300
- // MODIFIED: Removed 'data-' prefix.
301
- // Now looks for attributes like `quill=""` and `quill-theme=""`.
302
- const prefix = pluginName.toLowerCase();
295
+ function executeGenericPlugin(el, name) {
296
+ const prefix = name.toLowerCase();
303
297
  const mainAttr = el.getAttribute(prefix);
298
+ let rawData;
304
299
 
305
- // 1. CONFIG GATHERING
306
- const config = {};
307
- const attributes = Array.from(el.attributes);
308
-
309
- attributes.forEach(attr => {
310
- if (!attr.name.startsWith(prefix + '-')) return;
311
- const key = attr.name.substring(prefix.length + 1);
312
-
313
- if (key.startsWith('$')) return;
300
+ try {
301
+ rawData = JSON.parse(mainAttr);
302
+ } catch(e) {
303
+ rawData = {};
304
+ }
314
305
 
315
- let val = attr.value;
316
- try { val = JSON.parse(val); } catch(e) {}
306
+ // 1. Gather configuration from attributes
307
+ if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {
308
+ Array.from(el.attributes).forEach(attr => {
309
+ if (!attr.name.startsWith(prefix + '-')) return;
310
+ let key = attr.name.substring(prefix.length + 1);
311
+ if (key.startsWith('$')) key = key.substring(1);
312
+ let val = attr.value;
313
+ try { val = JSON.parse(val); } catch (e) { }
314
+ rawData[key] = val;
315
+ });
316
+ }
317
317
 
318
- if (key.includes('-')) {
319
- const parts = key.split('-');
320
- let target = config;
321
- for(let i=0; i<parts.length-1; i++) {
322
- if (!target[parts[i]]) target[parts[i]] = {};
323
- target = target[parts[i]];
324
- }
325
- target[parts[parts.length-1]] = val;
326
- } else {
327
- config[key] = val;
328
- }
329
- });
318
+ // 2. Resolve parameters (Token Resolver)
319
+ const resolved = processParams(el, rawData);
330
320
 
331
- // 2. INSTANTIATION
332
- const Constructor = window[pluginName];
333
-
334
- if (!Constructor) {
335
- console.error(`Constructor for ${pluginName} not found on window.`);
321
+ const Target = window[name] || window[prefix];
322
+ if (!Target) {
323
+ console.error(`Constructor for ${name} not found on window.`);
336
324
  return;
337
325
  }
338
326
 
339
- let instance = null;
327
+ let instance;
340
328
  try {
341
- let args = [el, config];
342
- if (mainAttr) {
343
- try {
344
- const json = JSON.parse(mainAttr);
345
- if (Array.isArray(json)) {
346
- args = processParams(el, json, config, null, pluginName);
347
- }
348
- } catch(e) {}
329
+ // 3. Determine if we call as a function (Anime) or as a Constructor (Swiper)
330
+ if (typeof Target === 'function' && (!Target.prototype || name === 'Anime')) {
331
+ instance = Target(resolved);
332
+ } else {
333
+ if (Array.isArray(resolved)) {
334
+ instance = new Target(...resolved);
335
+ } else {
336
+ instance = new Target(resolved);
337
+ }
349
338
  }
350
- instance = new Constructor(...args);
351
- } catch(e) {
352
- console.error(`Init failed for ${pluginName}`, e);
353
- return;
354
- }
355
-
356
- if (instance) el[pluginName] = instance;
357
- else return;
358
-
359
- // 3. OPERATOR PIPELINE
360
- const opPipeline = [];
361
- attributes.forEach(attr => {
362
- if (!attr.name.startsWith(prefix + '-')) return;
363
- let key = attr.name.substring(prefix.length + 1);
364
- if (!key.startsWith('$')) return;
365
- let val = attr.value;
366
- try { val = JSON.parse(val); } catch(e) {}
367
- let opName = key;
368
- opPipeline.push(opName, [val]);
369
- });
370
-
371
- if (opPipeline.length > 0) {
372
- await executePipeline(el, opPipeline, pluginName, instance, config);
339
+ console.log(`Initialized ${name}`);
340
+ } catch (e) {
341
+ console.error(`Error initializing ${name}:`, e);
373
342
  }
343
+ if (instance) el[name] = instance;
374
344
  }
375
345
 
376
- async function executePipeline(element, pipelineArray, pluginName, currentCtx, config = {}) {
377
- let parentCtx = window;
378
- const instance = element[pluginName] || currentCtx;
379
-
380
- for (let i = 0; i < pipelineArray.length; i++) {
381
- const item = pipelineArray[i];
382
- if (Array.isArray(item)) continue;
383
-
384
- if (typeof item === 'string') {
385
- let opName = item;
386
-
387
- if ((typeof currentCtx === 'undefined' || currentCtx === null) && instance) {
388
- currentCtx = instance;
389
- }
390
-
391
- if (opName.toLowerCase() === '$this') {
392
- const args = (i + 1 < pipelineArray.length && Array.isArray(pipelineArray[i + 1])) ? pipelineArray[i + 1] : [];
393
- const mapDef = args[0];
394
- if (Array.isArray(mapDef)) {
395
- mapDef.forEach(def => {
396
- applyMapping(element, instance, def);
397
- });
398
- }
399
- continue;
400
- }
401
-
402
- if (opName.startsWith('$')) {
403
- if (!currentCtx) {
404
- if (element[pluginName]) currentCtx = element[pluginName];
405
- if (!currentCtx) {
406
- console.warn(`Context undefined for operator ${opName}. Skipping.`);
407
- continue;
408
- }
409
- }
410
-
411
- let cleanName = opName.substring(1);
412
- if (!cleanName.includes('.')) {
413
- cleanName = cleanName.replace(/-/g, '');
414
- }
415
-
416
- let realKey = cleanName;
417
- if (!cleanName.includes('.') && currentCtx && (typeof currentCtx === 'object' || typeof currentCtx === 'function')) {
418
- try {
419
- const keys = Object.keys(currentCtx.constructor?.prototype || currentCtx);
420
- const found = keys.find(k => k.toLowerCase() === cleanName.toLowerCase());
421
- if (found) realKey = found;
422
- else {
423
- const directFound = Object.keys(currentCtx).find(k => k.toLowerCase() === cleanName.toLowerCase());
424
- if(directFound) realKey = directFound;
425
- }
426
- } catch (e) {}
346
+ /**
347
+ * Generic Parameter Processor
348
+ * Handles:
349
+ * - $this / $this.children
350
+ * - $window.path.to.function(arg)
351
+ * - $anime.stagger(100)
352
+ */
353
+ function processParams(el, params) {
354
+ if (typeof params === 'string' && params.startsWith('$')) {
355
+ try {
356
+ // 1. Check for Method Call: $root.path.to.func(arg)
357
+ const callMatch = params.match(/^\$([^.]+)\.(.+)\((.*)\)$/);
358
+ if (callMatch) {
359
+ const [_, root, path, arg] = callMatch;
360
+ const obj = (root === 'this') ? el : window[root];
361
+
362
+ // If root object exists, drill down
363
+ if (obj) {
364
+ const func = path.split('.').reduce((o, k) => (o || {})[k], obj);
365
+ if (typeof func === 'function') {
366
+ // Parse argument if JSON-like, else string
367
+ const parsedArg = arg ? (function() { try { return JSON.parse(arg); } catch(e) { return arg; } })() : undefined;
368
+ return func(parsedArg);
369
+ }
427
370
  }
428
- opName = realKey;
429
371
  }
430
-
431
- const [parent, val] = resolvePath(i === 0 ? window : currentCtx, opName);
432
372
 
433
- if (typeof val === 'undefined') {
434
- console.warn(`Method/Prop "${opName}" not found on context.`);
435
- continue;
373
+ // 2. Check for Property Access: $root.path.to.prop or just $root
374
+ const propMatch = params.match(/^\$([^.]+)(?:\.(.+))?$/);
375
+ if (propMatch) {
376
+ const [_, root, path] = propMatch;
377
+ const obj = (root === 'this') ? el : window[root];
378
+ if (!obj) return params; // Return raw string if root not found
379
+ if (!path) return obj;
380
+
381
+ const val = path.split('.').reduce((o, k) => (o || {})[k], obj);
382
+ // Convert HTMLCollections to Arrays
383
+ return (val instanceof HTMLCollection) ? Array.from(val) : val;
436
384
  }
437
-
438
- parentCtx = parent;
439
- currentCtx = val;
440
-
441
- if (typeof currentCtx === 'function') {
442
- let args = [];
443
- if (i + 1 < pipelineArray.length && Array.isArray(pipelineArray[i + 1])) {
444
- args = processParams(element, pipelineArray[i + 1], config, instance, pluginName);
445
- }
446
-
447
- try {
448
- const finalArgs = (args.length === 1) ? [args[0]] : args;
449
- try {
450
- currentCtx = await currentCtx.apply(parentCtx, finalArgs);
451
- } catch (err) {
452
- currentCtx = new currentCtx(...finalArgs);
453
- parentCtx = currentCtx;
454
- }
455
- } catch(e) {
456
- console.error(`Exec error ${opName}`, e);
457
- }
458
- }
459
- }
460
- }
461
- return currentCtx;
462
- }
463
-
464
- // --- HELPERS ---
465
- function applyMapping(element, instance, def) {
466
- if (def.includes('=')) {
467
- const [lhs, rhs] = def.split('=').map(s => s.trim());
468
- const pathRaw = rhs;
469
- const match = lhs.match(/^[$]?([\w\.]+)(\(\))?$/);
470
- if (!match) return;
471
- const alias = match[1];
472
- const isFunction = match[2] === '()';
473
-
474
- if (isFunction) {
475
- element[alias] = function(val) {
476
- if (arguments.length > 0) return assignDeep(instance, pathRaw, val);
477
- else return resolveDeep(instance, pathRaw);
478
- };
479
- } else {
480
- Object.defineProperty(element, alias, {
481
- get: () => resolveDeep(instance, pathRaw),
482
- set: (v) => assignDeep(instance, pathRaw, v),
483
- configurable: true
484
- });
485
- }
486
- } else {
487
- if (typeof instance[def] === 'function') {
488
- element[def] = instance[def].bind(instance);
385
+ } catch (e) {
386
+ console.warn("Failed to resolve dynamic token:", params);
489
387
  }
490
388
  }
491
- }
492
-
493
- function processParams(element, params, config, instance, pluginName) {
494
- if (params === "$this") return element;
495
- if (params === "$config") return config;
496
- if (params === "$root" || params === "$instance") return instance;
497
- if (typeof params === 'string' && params === `$${pluginName}`) return instance;
498
- if (typeof params === 'string' && params.startsWith('$this.')) return resolveDeep(element, params.substring(6));
499
- if (Array.isArray(params)) return params.map(p => processParams(element, p, config, instance, pluginName));
389
+
390
+ if (Array.isArray(params)) return params.map(p => processParams(el, p));
500
391
  if (typeof params === 'object' && params !== null) {
501
392
  const res = {};
502
- for(let k in params) res[k] = processParams(element, params[k], config, instance, pluginName);
393
+ for (let k in params) res[k] = processParams(el, params[k]);
503
394
  return res;
504
395
  }
505
396
  return params;
506
397
  }
507
398
 
508
- function resolveDeep(obj, path) {
509
- if (!obj || !path) return undefined;
510
- return path.split('.').reduce((o, k) => (o || {})[k], obj);
511
- }
512
- function assignDeep(obj, path, val) {
513
- if (!obj || !path) return;
514
- const parts = path.split('.');
515
- let target = obj;
516
- for(let i=0; i<parts.length-1; i++) target = target[parts[i]];
517
- const last = parts[parts.length-1];
518
- if(typeof target[last] === 'function') target[last](val);
519
- else target[last] = val;
520
- }
521
- function resolvePath(obj, path) {
522
- if (!obj || !path) return [null, undefined];
523
- const parts = path.split('.');
524
- let current = obj;
525
- let parent = null;
526
- for (const part of parts) {
527
- if (!current) return [null, undefined];
528
- parent = current;
529
- current = current[part];
530
- }
531
- return [parent, current];
532
- }
533
-
534
399
  // --- OBSERVER INTEGRATION ---
535
400
  Observer.init({
536
401
  name: "plugin",
@@ -540,4 +405,9 @@ Observer.init({
540
405
  callback: (mutation) => {
541
406
  init(mutation.target);
542
407
  }
543
- });
408
+ });
409
+
410
+ // Auto-init for existing elements
411
+ if (typeof document !== 'undefined') {
412
+ init(document.querySelectorAll("[plugin]"));
413
+ }