@cocreate/plugins 1.1.1 → 1.2.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,16 @@
1
+ # [1.2.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.1.1...v1.2.0) (2026-02-15)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * improve key validation in executeGenericPlugin function ([447f6d2](https://github.com/CoCreate-app/CoCreate-plugins/commit/447f6d209adea010110c3bc52a17f8b45cb302ea))
7
+
8
+
9
+ ### Features
10
+
11
+ * enhance demo page with improved layout and Bootstrap integration ([a74615e](https://github.com/CoCreate-app/CoCreate-plugins/commit/a74615e27be7c2f40529c5242e6c6e9b839ad020))
12
+ * refactor plugin management and enhance dynamic loading capabilities ([4d91958](https://github.com/CoCreate-app/CoCreate-plugins/commit/4d919582934418cfb7bfa9497b6d0dcaf3bf8d10))
13
+
1
14
  ## [1.1.1](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.1.0...v1.1.1) (2026-02-09)
2
15
 
3
16
 
package/demo/index.html CHANGED
@@ -1,12 +1,230 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
- <head>
4
- <title>plugins demo</title>
5
- <link rel="manifest" href="/manifest.webmanifest" />
6
- </head>
7
- <body>
8
- <!--<script src="../dist/CoCreate-plugins.js"></script>-->
9
- <!-- <script src="https://CoCreate.app/dist/CoCreate.js" ></script> -->
10
- <script src="https://CoCreate.app/dist/CoCreate.js"></script>
11
- </body>
12
- </html>
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>
20
+
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
+
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>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
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>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
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>
179
+ </div>
180
+ </div>
181
+ </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>
221
+ </div>
222
+ </div>
223
+
224
+ </div>
225
+
226
+ <!-- CoCreate Engine -->
227
+ <script src="https://CoCreate.app/dist/CoCreate.js"></script>
228
+
229
+ </body>
230
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/plugins",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "CoCreate plugins",
5
5
  "author": "CoCreate LLC",
6
6
  "license": "AGPL-3.0",
package/src/index.js CHANGED
@@ -1,184 +1,42 @@
1
1
  import Observer from "@cocreate/observer";
2
+ import {dotNotationToObject} from "@cocreate/utils";
3
+
4
+ /**
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.
7
+ * @property {Array<string>} [css] - List of CSS files to load.
8
+ */
2
9
 
3
10
  // --- CONFIGURATION ---
4
- // SECURITY: STRICT ALLOWLIST
5
- const plugins = {
6
- Toastify: {
7
- js: [{ src: "https://cdn.jsdelivr.net/npm/toastify-js", crossOrigin: "anonymous" }],
8
- css: ["https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css"]
9
- },
10
- Choices: {
11
- js: [{ src: "https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js", crossOrigin: "anonymous" }],
12
- css: ["https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css"]
13
- },
14
- flatpickr: {
15
- js: [{ src: "https://cdn.jsdelivr.net/npm/flatpickr", crossOrigin: "anonymous" }],
16
- css: ["https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"]
17
- },
18
- Quill: {
19
- js: [{ src: "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.min.js", crossOrigin: "anonymous" }],
20
- css: [
21
- "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.core.css",
22
- "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css",
23
- "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.bubble.css"
24
- ]
25
- },
26
- ClassicEditor: {
27
- js: [{ src: "https://cdn.ckeditor.com/ckeditor5/41.2.0/classic/ckeditor.js", crossOrigin: "anonymous" }]
28
- },
29
- Dropzone: {
30
- js: [{ src: "https://unpkg.com/dropzone@5/dist/min/dropzone.min.js", crossOrigin: "anonymous" }],
31
- css: ["https://unpkg.com/dropzone@5/dist/min/dropzone.min.css"]
32
- },
33
- SimpleBar: {
34
- js: [{ src: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js", crossOrigin: "anonymous" }],
35
- css: ["https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css"]
36
- },
37
- GLightbox: {
38
- js: [{ src: "https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js", crossOrigin: "anonymous" }],
39
- css: ["https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css"]
40
- },
41
- FgEmojiPicker: {
42
- js: [{ src: "https://cdn.jsdelivr.net/npm/fg-emoji-picker/fgEmojiPicker.js", crossOrigin: "anonymous" }]
43
- },
44
- bootstrap: {
45
- js: [{ src: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js", crossOrigin: "anonymous" }]
46
- },
47
- Waves: {
48
- js: [{ src: "https://cdn.jsdelivr.net/npm/node-waves/dist/waves.min.js", crossOrigin: "anonymous" }],
49
- css: ["https://cdn.jsdelivr.net/npm/node-waves/dist/waves.min.css"]
50
- },
51
- feather: {
52
- js: [{ src: "https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js", crossOrigin: "anonymous" }]
53
- },
54
- ApexCharts: {
55
- js: [{ src: "https://cdn.jsdelivr.net/npm/apexcharts", crossOrigin: "anonymous" }]
56
- },
57
- jsVectorMap: {
58
- js: [
59
- { src: "https://cdn.jsdelivr.net/npm/jsvectormap", crossOrigin: "anonymous" },
60
- { src: "https://cdn.jsdelivr.net/npm/jsvectormap/dist/maps/world.js", crossOrigin: "anonymous" }
61
- ],
62
- css: ["https://cdn.jsdelivr.net/npm/jsvectormap/dist/css/jsvectormap.min.css"]
63
- },
64
- Swiper: {
65
- js: [{ src: "https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js", crossOrigin: "anonymous" }],
66
- css: ["https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"]
67
- },
68
- List: {
69
- js: [
70
- { src: "https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js", crossOrigin: "anonymous" },
71
- { src: "https://cdnjs.cloudflare.com/ajax/libs/list.pagination.js/0.1.1/list.pagination.min.js", crossOrigin: "anonymous" }
72
- ]
73
- },
74
- Swal: {
75
- js: [{ src: "https://cdn.jsdelivr.net/npm/sweetalert2@11", crossOrigin: "anonymous" }],
76
- css: ["https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css"]
77
- },
78
- FullCalendar: {
79
- js: [{ src: "https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js", crossOrigin: "anonymous" }]
80
- },
81
- Cleave: {
82
- js: [{ src: "https://cdn.jsdelivr.net/npm/cleave.js/dist/cleave.min.js", crossOrigin: "anonymous" }]
83
- },
84
- noUiSlider: {
85
- js: [{ src: "https://cdn.jsdelivr.net/npm/nouislider/dist/nouislider.min.js", crossOrigin: "anonymous" }],
86
- css: ["https://cdn.jsdelivr.net/npm/nouislider/dist/nouislider.min.css"]
87
- },
88
- wNumb: {
89
- js: [{ src: "https://cdn.jsdelivr.net/npm/wnumb/wNumb.min.js", crossOrigin: "anonymous" }]
90
- },
91
- Grid: {
92
- js: [
93
- { src: "https://unpkg.com/gridjs/dist/gridjs.umd.js", crossOrigin: "anonymous" },
94
- { src: "https://unpkg.com/gridjs/plugins/selection/dist/selection.umd.js", crossOrigin: "anonymous" }
95
- ],
96
- css: ["https://unpkg.com/gridjs/dist/theme/mermaid.min.css"]
97
- },
98
- FilePond: {
99
- js: [
100
- { src: "https://unpkg.com/filepond/dist/filepond.min.js", crossOrigin: "anonymous" },
101
- { src: "https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.js", crossOrigin: "anonymous" },
102
- { src: "https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.min.js", crossOrigin: "anonymous" },
103
- { src: "https://unpkg.com/filepond-plugin-image-exif-orientation/dist/filepond-plugin-image-exif-orientation.min.js", crossOrigin: "anonymous" },
104
- { src: "https://unpkg.com/filepond-plugin-file-encode/dist/filepond-plugin-file-encode.min.js", crossOrigin: "anonymous" }
105
- ],
106
- css: [
107
- "https://unpkg.com/filepond/dist/filepond.min.css",
108
- "https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css"
109
- ]
110
- },
111
- Prism: {
112
- js: [{ src: "https://cdn.jsdelivr.net/npm/prismjs/prism.min.js", crossOrigin: "anonymous" }],
113
- css: ["https://cdn.jsdelivr.net/npm/prismjs/themes/prism.min.css"]
114
- },
115
- Isotope: {
116
- js: [{ src: "https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.min.js", crossOrigin: "anonymous" }]
117
- },
118
- particlesJS: {
119
- js: [{ src: "https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js", crossOrigin: "anonymous" }]
120
- },
121
- dragula: {
122
- js: [{ src: "https://cdn.jsdelivr.net/npm/dragula/dist/dragula.min.js", crossOrigin: "anonymous" }],
123
- css: ["https://cdn.jsdelivr.net/npm/dragula/dist/dragula.min.css"]
124
- },
125
- DomAutoscroller: {
126
- js: [{ src: "https://cdn.jsdelivr.net/npm/dom-autoscroller", crossOrigin: "anonymous" }]
127
- },
128
- Card: {
129
- js: [{ src: "https://cdn.jsdelivr.net/npm/card/dist/card.js", crossOrigin: "anonymous" }]
130
- },
131
- Chart: {
132
- js: [{ src: "https://cdn.jsdelivr.net/npm/chart.js", crossOrigin: "anonymous" }]
133
- },
134
- echarts: {
135
- js: [{ src: "https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js", crossOrigin: "anonymous" }]
136
- },
137
- Multi: {
138
- js: [{ src: "https://cdn.jsdelivr.net/npm/multi.js/dist/multi.min.js", crossOrigin: "anonymous" }],
139
- css: ["https://cdn.jsdelivr.net/npm/multi.js/dist/multi.min.css"]
140
- },
141
- autoComplete: {
142
- js: [{ src: "https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/autoComplete.min.js", crossOrigin: "anonymous" }],
143
- css: ["https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/css/autoComplete.01.min.css"]
144
- },
145
- Pickr: {
146
- js: [{ src: "https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js", crossOrigin: "anonymous" }],
147
- css: ["https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/classic.min.css"]
148
- },
149
- Shepherd: {
150
- js: [{ src: "https://cdn.jsdelivr.net/npm/shepherd.js/dist/js/shepherd.min.js", crossOrigin: "anonymous" }],
151
- css: ["https://cdn.jsdelivr.net/npm/shepherd.js/dist/css/shepherd.css"]
152
- },
153
- GMaps: {
154
- js: [{ src: "https://cdn.jsdelivr.net/npm/gmaps/gmaps.min.js", crossOrigin: "anonymous" }]
155
- },
156
- L: {
157
- js: [{ src: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js", crossOrigin: "anonymous" }],
158
- css: ["https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"]
159
- },
160
- Masonry: {
161
- js: [{ src: "https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js", crossOrigin: "anonymous" }]
162
- },
163
- Rater: {
164
- js: [{ src: "https://cdn.jsdelivr.net/npm/rater-js/index.js", crossOrigin: "anonymous" }]
165
- },
166
- Anime: {
167
- js: [{ src: "https://cdn.jsdelivr.net/npm/animejs@3.2.2/lib/anime.min.js", crossOrigin: "anonymous" }]
168
- }
169
- };
11
+
12
+ /**
13
+ * @type {Object.<string, PluginDefinition>}
14
+ * Configuration object containing plugin definitions.
15
+ * Populated dynamically from CoCreate.config.js or defaults.
16
+ */
17
+ const plugins = {};
170
18
 
171
19
  // --- CORE ENGINE ---
172
20
 
173
- // Global Cache for script promises to prevent race conditions and duplicate loads
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
+ */
174
26
  const scriptCache = new Map();
175
- // Cache the CSS marker once on load
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
+ */
176
33
  const cssMarker = typeof document !== 'undefined' ? document.querySelector('link[plugins]') : null;
177
34
 
178
35
  /**
179
- * Global Initialization Function
180
- * Processes one or more elements to attach plugins.
181
- * @param {HTMLElement|NodeList|Array} elements - Element, or Collection of Elements
36
+ * 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}
182
40
  */
183
41
  function init(elements) {
184
42
  if (!elements) return;
@@ -199,6 +57,13 @@ const cssMarker = typeof document !== 'undefined' ? document.querySelector('link
199
57
  });
200
58
  }
201
59
 
60
+ /**
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.
66
+ */
202
67
  async function processPlugin(el) {
203
68
  const rawAttr = el.getAttribute("plugin");
204
69
  if (!rawAttr) return;
@@ -207,140 +72,248 @@ async function processPlugin(el) {
207
72
 
208
73
  for (const pluginName of pluginNames) {
209
74
  const pluginDef = plugins[pluginName];
210
- if (!pluginDef) continue;
211
-
212
- // Load CSS
213
- if (pluginDef.css) pluginDef.css.forEach(href => {
214
- if (!document.querySelector(`link[href="${href}"]`)) {
215
- const link = document.createElement("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');
75
+
76
+ // Only attempt to load resources if a configuration exists
77
+ if (pluginDef) {
78
+ // Load CSS
79
+ if (pluginDef.css) pluginDef.css.forEach(href => {
80
+ if (!document.querySelector(`link[href="${href}"]`)) {
81
+ 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)
231
88
 
232
- if (firstStyle) {
233
- document.head.insertBefore(link, firstStyle);
89
+ if (cssMarker) {
90
+ // Insert before the marker
91
+ cssMarker.parentNode.insertBefore(link, cssMarker);
234
92
  } else {
235
- // If no CSS exists yet, appending is safe
236
- document.head.appendChild(link);
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
+ }
237
104
  }
238
105
  }
239
- }
240
- });
106
+ });
107
+
108
+ // Load JS with Promise Cache
109
+ if (pluginDef.js) {
110
+ for (const item of pluginDef.js) {
111
+ const src = typeof item === 'string' ? item : item.src;
112
+ const integrity = typeof item === 'object' ? item.integrity : null;
113
+ const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
241
114
 
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();
115
+ if (!scriptCache.has(src)) {
116
+ const scriptPromise = new Promise((resolve, reject) => {
117
+ // Check if already in DOM (manual load)
118
+ const existing = document.querySelector(`script[src="${src}"]`);
119
+ if (existing) {
120
+ if (existing.dataset.loaded === "true") {
121
+ resolve();
122
+ } else {
123
+ const prevOnload = existing.onload;
124
+ existing.onload = () => {
125
+ if (prevOnload) prevOnload();
126
+ existing.dataset.loaded = "true";
127
+ resolve();
128
+ };
129
+ existing.onerror = reject;
130
+ }
256
131
  } else {
257
- const prevOnload = existing.onload;
258
- existing.onload = () => {
259
- if (prevOnload) prevOnload();
260
- existing.dataset.loaded = "true";
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";
261
140
  resolve();
262
141
  };
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";
142
+ s.onerror = reject;
143
+ document.head.appendChild(s);
271
144
  }
272
- s.onload = () => {
273
- s.dataset.loaded = "true";
274
- resolve();
275
- };
276
- s.onerror = reject;
277
- document.head.appendChild(s);
278
- }
279
- });
280
- scriptCache.set(src, scriptPromise);
281
- }
145
+ });
146
+ scriptCache.set(src, scriptPromise);
147
+ }
282
148
 
283
- try {
284
- await scriptCache.get(src);
285
- } catch (e) {
286
- console.error(`Failed to load script: ${src}`, e);
149
+ try {
150
+ await scriptCache.get(src);
151
+ } catch (e) {
152
+ console.error(`Failed to load script: ${src}`, e);
153
+ }
287
154
  }
288
155
  }
289
156
  }
290
157
 
158
+ // Attempt to execute plugin even if no config was found (it might be on window already)
291
159
  executeGenericPlugin(el, pluginName);
292
160
  }
293
161
  }
294
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
+
181
+ /**
182
+ * Executes the logic for a generic plugin on a specific element.
183
+ * Handles:
184
+ * 1. Resolving the target class/function from window.
185
+ * 2. Initializing the base instance.
186
+ * 3. Processing attribute paths and nested JSON objects to execute methods or set properties.
187
+ * * @param {HTMLElement} el - The target element.
188
+ * @param {string} name - The name of the plugin (case-insensitive identifier).
189
+ * @returns {void}
190
+ */
295
191
  function executeGenericPlugin(el, name) {
296
192
  const prefix = name.toLowerCase();
297
193
  const mainAttr = el.getAttribute(prefix);
298
- let rawData;
194
+ let rawData = {};
299
195
 
300
- try {
301
- rawData = JSON.parse(mainAttr);
302
- } catch(e) {
303
- rawData = {};
304
- }
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
204
+ }
305
205
 
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
- }
206
+ try {
207
+ rawData[key] = JSON.parse(attr.value);
208
+ } catch(e) {
209
+ rawData[key] = attr.value;
210
+ }
211
+
212
+ };
317
213
 
318
214
  // 2. Resolve parameters (Token Resolver)
319
- const resolved = processParams(el, rawData);
215
+ let resolved = processParams(el, rawData);
216
+ resolved = dotNotationToObject(resolved);
320
217
 
321
- const Target = window[name] || window[prefix];
322
- if (!Target) {
323
- console.error(`Constructor for ${name} not found on window.`);
218
+ let Plugin = window[name] || window[prefix];
219
+ if (!Plugin) {
220
+ console.error(`Plugin for ${name} not found on window.`);
324
221
  return;
325
222
  }
326
223
 
224
+ // Iterate over resolved object.
225
+ // Since we use dotNotationToObject, keys like "swiper.effect" are already nested as { swiper: { effect: ... } }
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];
234
+
235
+ // Pass context: Window as parent, Plugin Name as property (for potential context binding)
236
+ // el and name used to store the result on the element.
237
+ update(Target, val, window, name, el, name);
238
+
239
+ console.log(`Processed ${name}`);
240
+ } catch (e) {
241
+ console.error(`Error processing ${name}:`, e);
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
+ }
261
+
327
262
  let instance;
328
- try {
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);
263
+ if (typeof Target === 'function') {
264
+ if (!isConstructor(Target, property)) {
265
+ // Call as a function (method or standalone)
266
+ // Use 'parent' as context (this) if available to maintain class references
267
+ if (parent) {
268
+ if (Array.isArray(val)) {
269
+ instance = Target.apply(parent, val);
270
+ } else {
271
+ instance = Target.call(parent, val);
272
+ }
273
+ } else {
274
+ if (Array.isArray(val)) {
275
+ instance = Target(...val);
276
+ } else {
277
+ instance = Target(val);
278
+ }
279
+ }
332
280
  } else {
333
- if (Array.isArray(resolved)) {
334
- instance = new Target(...resolved);
281
+ // Call as a Constructor
282
+ if (Array.isArray(val)) {
283
+ instance = new Target(...val);
335
284
  } else {
336
- instance = new Target(resolved);
285
+ instance = new Target(val);
286
+ }
287
+ }
288
+
289
+ // Assign the result to the element structure
290
+ if (elParent && elProperty) {
291
+ elParent[elProperty] = instance;
292
+ }
293
+
294
+ } else if (typeof Target === 'object' && Target !== null && typeof val === 'object' && val !== null && !Array.isArray(val)) {
295
+ // Prepare the next level of the element structure
296
+ if (elParent && elProperty) {
297
+ if (!elParent[elProperty]) {
298
+ elParent[elProperty] = {};
299
+ }
300
+ const nextElParent = elParent[elProperty];
301
+
302
+ for (let key in val) {
303
+ update(Target[key], val[key], Target, key, nextElParent, key);
337
304
  }
338
305
  }
339
- console.log(`Initialized ${name}`);
340
- } catch (e) {
341
- console.error(`Error initializing ${name}:`, e);
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);
342
316
  }
343
- if (instance) el[name] = instance;
344
317
  }
345
318
 
346
319
  /**
@@ -349,6 +322,7 @@ function executeGenericPlugin(el, name) {
349
322
  * - $this / $this.children
350
323
  * - $window.path.to.function(arg)
351
324
  * - $anime.stagger(100)
325
+ * - Global access: $document, $window, etc.
352
326
  */
353
327
  function processParams(el, params) {
354
328
  if (typeof params === 'string' && params.startsWith('\u0024')) {
@@ -375,13 +349,22 @@ function processParams(el, params) {
375
349
  if (propMatch) {
376
350
  const [_, root, path] = propMatch;
377
351
  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
352
 
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;
353
+ if (obj) {
354
+ if (!path) return (obj instanceof HTMLCollection) ? Array.from(obj) : obj;
355
+
356
+ const val = path.split('.').reduce((o, k) => (o || {})[k], obj);
357
+ // Convert HTMLCollections to Arrays
358
+ return (val instanceof HTMLCollection) ? Array.from(val) : val;
359
+ }
384
360
  }
361
+
362
+ // 3. Check for standalone globals like $document or $window
363
+ const globalKey = params.substring(1);
364
+ if (window[globalKey]) {
365
+ return window[globalKey];
366
+ }
367
+
385
368
  } catch (e) {
386
369
  console.warn("Failed to resolve dynamic token:", params);
387
370
  }
@@ -396,20 +379,43 @@ function processParams(el, params) {
396
379
  return params;
397
380
  }
398
381
 
399
- // --- OBSERVER INTEGRATION ---
400
- Observer.init({
401
- name: "plugin",
402
- types: ["addedNodes", "attributes"],
403
- selector: "[plugin]",
404
- attributeFilter: ["plugin"],
405
- callback: (mutation) => {
406
- init(mutation.target);
407
- }
408
- });
382
+ // --- STARTUP LOGIC ---
409
383
 
410
- // Auto-init for existing elements
411
384
  if (typeof document !== 'undefined') {
412
- init(document.querySelectorAll("[plugin]"));
385
+ // Dynamic Import: Loads config if available, handles error if missing.
386
+ // Works with 'npm start' (Bundlers) by creating a code-split chunk.
387
+ import("./CoCreate.config.js")
388
+ .then((Config) => {
389
+ // LOGIC: Merge exports into plugins object
390
+ if (Config.plugins) {
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
403
+ })
404
+ .finally(() => {
405
+ // Start Observer
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
+ });
413
419
  }
414
420
 
415
- export default { init }
421
+ export default { init, plugins }