@cocreate/plugins 1.1.1 → 1.2.1

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,23 @@
1
+ ## [1.2.1](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.2.0...v1.2.1) (2026-03-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Handle case-insensitive plugin global mapping and add support for function reference assignments in plugin properties. ([904a947](https://github.com/CoCreate-app/CoCreate-plugins/commit/904a947abc868937b6e650b62e080d00341272a1))
7
+
8
+ # [1.2.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.1.1...v1.2.0) (2026-02-15)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * improve key validation in executeGenericPlugin function ([447f6d2](https://github.com/CoCreate-app/CoCreate-plugins/commit/447f6d209adea010110c3bc52a17f8b45cb302ea))
14
+
15
+
16
+ ### Features
17
+
18
+ * enhance demo page with improved layout and Bootstrap integration ([a74615e](https://github.com/CoCreate-app/CoCreate-plugins/commit/a74615e27be7c2f40529c5242e6c6e9b839ad020))
19
+ * refactor plugin management and enhance dynamic loading capabilities ([4d91958](https://github.com/CoCreate-app/CoCreate-plugins/commit/4d919582934418cfb7bfa9497b6d0dcaf3bf8d10))
20
+
1
21
  ## [1.1.1](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.1.0...v1.1.1) (2026-02-09)
2
22
 
3
23
 
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.1",
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,419 @@ 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
+ const preWindowKeys = (typeof window !== 'undefined') ? new Set(Object.keys(window)) : new Set();
241
111
 
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();
112
+ for (const item of pluginDef.js) {
113
+ 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
+
117
+ 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
+ }
256
133
  } else {
257
- const prevOnload = existing.onload;
258
- existing.onload = () => {
259
- if (prevOnload) prevOnload();
260
- existing.dataset.loaded = "true";
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";
261
142
  resolve();
262
143
  };
263
- existing.onerror = reject;
144
+ s.onerror = reject;
145
+ document.head.appendChild(s);
264
146
  }
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);
278
- }
279
- });
280
- scriptCache.set(src, scriptPromise);
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
+ }
281
156
  }
282
157
 
158
+ // After loading JS files, map newly-added globals to the expected plugin name.
159
+ // Exact (case-insensitive) matching only.
283
160
  try {
284
- await scriptCache.get(src);
285
- } catch (e) {
286
- console.error(`Failed to load script: ${src}`, e);
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}`);
182
+ }
183
+ }
184
+ } catch (err) {
185
+ // Non-fatal
287
186
  }
288
187
  }
289
188
  }
290
189
 
190
+ // Attempt to execute plugin even if no config was found (it might be on window already)
291
191
  executeGenericPlugin(el, pluginName);
292
192
  }
293
193
  }
294
194
 
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
+ };
212
+
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
+ */
295
223
  function executeGenericPlugin(el, name) {
296
224
  const prefix = name.toLowerCase();
297
225
  const mainAttr = el.getAttribute(prefix);
298
- let rawData;
226
+ let rawData = {};
299
227
 
300
- try {
301
- rawData = JSON.parse(mainAttr);
302
- } catch(e) {
303
- rawData = {};
304
- }
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
236
+ }
305
237
 
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
- }
238
+ try {
239
+ rawData[key] = JSON.parse(attr.value);
240
+ } catch(e) {
241
+ rawData[key] = attr.value;
242
+ }
243
+
244
+ };
317
245
 
318
246
  // 2. Resolve parameters (Token Resolver)
319
- const resolved = processParams(el, rawData);
247
+ let resolved = processParams(el, rawData);
248
+ resolved = dotNotationToObject(resolved);
320
249
 
321
- const Target = window[name] || window[prefix];
322
- if (!Target) {
323
- console.error(`Constructor for ${name} not found on window.`);
250
+ let Plugin = window[name] || window[prefix];
251
+ if (!Plugin) {
252
+ console.error(`Plugin for ${name} not found on window.`);
324
253
  return;
325
254
  }
326
255
 
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];
266
+
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
+ }
346
+
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
+ }
354
+
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 };
360
+ }
361
+ }
362
+ }
363
+
364
+ return { fn: undefined, context: undefined, methodName };
365
+ }
366
+
367
+ function createFunctionAdapter(refPath, parent, property, hostElement) {
368
+ const normalizedRefPath = normalizeReferencePath(refPath);
369
+ const methodName = normalizedRefPath.split(".").pop();
370
+
371
+ return function (...args) {
372
+ const resolved = resolveCallableReference(normalizedRefPath, parent, hostElement);
373
+ const fn = resolved.fn;
374
+ const context = resolved.context;
375
+
376
+ if (typeof fn !== "function") {
377
+ console.error(`Plugin adapter failed: "${normalizedRefPath}" did not resolve to a function for ${property}.`);
378
+ return;
379
+ }
380
+
381
+ if (property === "setValue") {
382
+ const payload = normalizeCrudPayload(args[0]);
383
+
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());
388
+ }
389
+ }
390
+
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;
409
+ }
410
+ }
411
+ }
412
+
327
413
  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);
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
+ }
421
+
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
+ }
431
+ } else {
432
+ if (Array.isArray(val)) {
433
+ instance = Target(...val);
434
+ } else {
435
+ instance = Target(val);
436
+ }
437
+ }
332
438
  } else {
333
- if (Array.isArray(resolved)) {
334
- instance = new Target(...resolved);
439
+ // Call as a Constructor
440
+ if (Array.isArray(val)) {
441
+ instance = new Target(...val);
335
442
  } else {
336
- instance = new Target(resolved);
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
+
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
+ }
457
+
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);
337
468
  }
338
469
  }
339
- console.log(`Initialized ${name}`);
340
- } catch (e) {
341
- console.error(`Error initializing ${name}:`, e);
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
+
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;
484
+ }
485
+
486
+ console.log(`Set plugin property ${property} to`, val);
342
487
  }
343
- if (instance) el[name] = instance;
344
488
  }
345
489
 
346
490
  /**
@@ -349,6 +493,7 @@ function executeGenericPlugin(el, name) {
349
493
  * - $this / $this.children
350
494
  * - $window.path.to.function(arg)
351
495
  * - $anime.stagger(100)
496
+ * - Global access: $document, $window, etc.
352
497
  */
353
498
  function processParams(el, params) {
354
499
  if (typeof params === 'string' && params.startsWith('\u0024')) {
@@ -375,13 +520,22 @@ function processParams(el, params) {
375
520
  if (propMatch) {
376
521
  const [_, root, path] = propMatch;
377
522
  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
523
 
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;
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
+ }
531
+ }
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];
384
537
  }
538
+
385
539
  } catch (e) {
386
540
  console.warn("Failed to resolve dynamic token:", params);
387
541
  }
@@ -396,20 +550,43 @@ function processParams(el, params) {
396
550
  return params;
397
551
  }
398
552
 
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
- });
553
+ // --- STARTUP LOGIC ---
409
554
 
410
- // Auto-init for existing elements
411
555
  if (typeof document !== 'undefined') {
412
- init(document.querySelectorAll("[plugin]"));
556
+ // Dynamic Import: Loads config if available, handles error if missing.
557
+ // Works with 'npm start' (Bundlers) by creating a code-split chunk.
558
+ import("./CoCreate.config.js")
559
+ .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
+ }
571
+ })
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
+ });
413
590
  }
414
591
 
415
- export default { init }
592
+ export default { init, plugins }