@cocreate/plugins 1.8.0 → 1.9.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/CONTRIBUTING.md +1 -1
- package/README.md +432 -92
- package/package.json +6 -5
- package/src/index.js +169 -56
- package/webpack.config.js +10 -6
package/CONTRIBUTING.md
CHANGED
|
@@ -13,7 +13,7 @@ Fork the [project on Github](https://github.com/CoCreate-app/CoCreate-plugins) a
|
|
|
13
13
|
```
|
|
14
14
|
git clone https://github.com/contributor/CoCreate-plugins.git
|
|
15
15
|
cd CoCreate-plugins
|
|
16
|
-
git remote add upstream https://
|
|
16
|
+
git remote add upstream https://gitlab.com/cocreatejs/CoCreate-plugins.git
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Create a Topic Branch
|
package/README.md
CHANGED
|
@@ -1,92 +1,432 @@
|
|
|
1
|
-
# CoCreate
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
# CoCreate Plugins
|
|
2
|
+
|
|
3
|
+
A declarative browser plugin loader and runtime for CoCreate. It loads registered JavaScript and CSS resources, exposes their browser globals to the CoCreate AST, initializes plugin instances on DOM elements, and connects those instances to standard element value APIs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
For interactive examples, visit the [CoCreate Plugins documentation](https://cocreate.app/docs/plugins).
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Architectural Overview](#architectural-overview)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Registering Plugin Resources](#registering-plugin-resources)
|
|
16
|
+
- [Using the `plugin` Attribute](#using-the-plugin-attribute)
|
|
17
|
+
- [AST Execution Model](#ast-execution-model)
|
|
18
|
+
- [Element Value Integration](#element-value-integration)
|
|
19
|
+
- [Programmatic API](#programmatic-api)
|
|
20
|
+
- [Resource Loading](#resource-loading)
|
|
21
|
+
- [Error Handling](#error-handling)
|
|
22
|
+
- [Security](#security)
|
|
23
|
+
- [Announcements](#announcements)
|
|
24
|
+
- [Roadmap](#roadmap)
|
|
25
|
+
- [How to Contribute](#how-to-contribute)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Architectural Overview
|
|
31
|
+
|
|
32
|
+
`@cocreate/plugins` connects three parts of the browser runtime:
|
|
33
|
+
|
|
34
|
+
1. A plugin definition in `window.CoCreateConfig.plugins` identifies the JavaScript and CSS resources.
|
|
35
|
+
2. A DOM element's `plugin` attribute contains an AST configuration.
|
|
36
|
+
3. The resolved plugin instance and other top-level AST values are attached to that element.
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
CoCreateConfig.plugins
|
|
40
|
+
│
|
|
41
|
+
├── load JavaScript and CSS
|
|
42
|
+
│
|
|
43
|
+
▼
|
|
44
|
+
window.PluginName
|
|
45
|
+
│
|
|
46
|
+
├── AST executes plugin configuration
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
element.PluginName
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Plugin configurations execute sequentially. A constructor result created by one top-level key is therefore available to every key that follows it.
|
|
53
|
+
|
|
54
|
+
The module observes `[plugin]` elements automatically, including elements added or updated after the initial page load.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
### NPM
|
|
61
|
+
|
|
62
|
+
```shell
|
|
63
|
+
npm install @cocreate/plugins
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Yarn
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
yarn add @cocreate/plugins
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### CDN
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<script src="https://cdn.cocreate.app/plugins/latest/CoCreate-plugins.min.js"></script>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When using the package as a module:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
import plugins from "@cocreate/plugins";
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Registering Plugin Resources
|
|
87
|
+
|
|
88
|
+
Plugin resource definitions live under `window.CoCreateConfig.plugins`. The property name should match the global name created by the external library.
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
window.CoCreateConfig = {
|
|
92
|
+
plugins: {
|
|
93
|
+
SimpleBar: {
|
|
94
|
+
js: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js",
|
|
95
|
+
css: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css"
|
|
96
|
+
},
|
|
97
|
+
FullCalendar: {
|
|
98
|
+
js: "https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Both `js` and `css` accept one resource or an array of resources. A resource can be a URL string or an object:
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
window.CoCreateConfig.plugins.Example = {
|
|
108
|
+
js: [
|
|
109
|
+
{
|
|
110
|
+
src: "https://cdn.example.com/example.min.js",
|
|
111
|
+
crossOrigin: "anonymous",
|
|
112
|
+
integrity: "sha384-..."
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
css: [
|
|
116
|
+
{
|
|
117
|
+
href: "https://cdn.example.com/example.min.css",
|
|
118
|
+
crossOrigin: "anonymous",
|
|
119
|
+
integrity: "sha384-..."
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Plugins can also be registered at runtime:
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
import { registerPlugin } from "@cocreate/plugins";
|
|
129
|
+
|
|
130
|
+
registerPlugin("SimpleBar", {
|
|
131
|
+
js: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js",
|
|
132
|
+
css: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css"
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Registration describes where a library is loaded from. Initialization behavior belongs in the element's `plugin` attribute.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Using the `plugin` Attribute
|
|
141
|
+
|
|
142
|
+
### Constructor or Function
|
|
143
|
+
|
|
144
|
+
The simplest object form calls the registered global and assigns its result to the element under the same name.
|
|
145
|
+
|
|
146
|
+
```html
|
|
147
|
+
<div
|
|
148
|
+
plugin='{
|
|
149
|
+
"SimpleBar": ["$this", {}]
|
|
150
|
+
}'
|
|
151
|
+
style="max-height: 300px">
|
|
152
|
+
</div>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This is conceptually equivalent to:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
element.SimpleBar = new SimpleBar(element, {});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`$this` resolves to the element containing the `plugin` attribute.
|
|
162
|
+
|
|
163
|
+
For a global function that receives a single options object:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<button
|
|
167
|
+
plugin='{
|
|
168
|
+
"Toastify": {
|
|
169
|
+
"text": "Saved successfully",
|
|
170
|
+
"duration": 3000
|
|
171
|
+
},
|
|
172
|
+
"Toastify.showToast": []
|
|
173
|
+
}'>
|
|
174
|
+
Show notification
|
|
175
|
+
</button>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Nested Constructor
|
|
179
|
+
|
|
180
|
+
Some libraries expose constructors as properties of a global namespace. A single nested member identifies the constructor to invoke:
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<div
|
|
184
|
+
id="calendar"
|
|
185
|
+
plugin='{
|
|
186
|
+
"FullCalendar": {
|
|
187
|
+
"Calendar": [
|
|
188
|
+
"$this",
|
|
189
|
+
{
|
|
190
|
+
"initialView": "dayGridMonth",
|
|
191
|
+
"editable": true
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
"FullCalendar.render": []
|
|
196
|
+
}'>
|
|
197
|
+
</div>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The first key creates the instance:
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
element.FullCalendar = new window.FullCalendar.Calendar(element, options);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The next key operates on that resolved instance:
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
element.FullCalendar.render();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Bare Plugin Name
|
|
213
|
+
|
|
214
|
+
A plain attribute value invokes the registered global without arguments:
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<div plugin="ExamplePlugin"></div>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use the JSON form whenever the plugin requires the element, options, or additional operations.
|
|
221
|
+
|
|
222
|
+
### Multiple Configuration Blocks
|
|
223
|
+
|
|
224
|
+
The attribute can contain an array. Each object is processed in order:
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<div
|
|
228
|
+
plugin='[
|
|
229
|
+
{ "FirstPlugin": ["$this"] },
|
|
230
|
+
{ "SecondPlugin": ["$this", {}] }
|
|
231
|
+
]'>
|
|
232
|
+
</div>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## AST Execution Model
|
|
238
|
+
|
|
239
|
+
Each configuration object is converted into one AST program and evaluated sequentially.
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"FullCalendar": {
|
|
244
|
+
"Calendar": ["$this", {}]
|
|
245
|
+
},
|
|
246
|
+
"FullCalendar.render": []
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Conceptually becomes:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"FullCalendar": {
|
|
255
|
+
"$FullCalendar.Calendar": ["$this", {}]
|
|
256
|
+
},
|
|
257
|
+
"FullCalendar.render": {
|
|
258
|
+
"$FullCalendar.render": []
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Execution follows these rules:
|
|
264
|
+
|
|
265
|
+
- Plugin globals are placed in the AST sandbox.
|
|
266
|
+
- `$this` is the element being initialized.
|
|
267
|
+
- Arrays are passed as positional arguments.
|
|
268
|
+
- An object is passed as a single argument.
|
|
269
|
+
- Native classes are instantiated with `new`.
|
|
270
|
+
- Functions are called normally.
|
|
271
|
+
- A resolved top-level value becomes available to subsequent keys.
|
|
272
|
+
- Resolved top-level keys without dot notation are attached to the element.
|
|
273
|
+
- Dotted keys represent operations and are not attached as element properties.
|
|
274
|
+
|
|
275
|
+
Ordinary non-plugin keys remain part of the AST program, so they can store intermediate results or participate in later operations.
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<div
|
|
279
|
+
plugin='{
|
|
280
|
+
"ExamplePlugin": ["$this"],
|
|
281
|
+
"items": { "$ExamplePlugin.getItems": [] },
|
|
282
|
+
"count": "$items.length"
|
|
283
|
+
}'>
|
|
284
|
+
</div>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
After evaluation, `element.ExamplePlugin`, `element.items`, and `element.count` contain the corresponding resolved values.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Element Value Integration
|
|
292
|
+
|
|
293
|
+
The reserved `getValue` and `setValue` keys connect a plugin instance to CoCreate's standard element value interface. Unlike initialization operations, these AST expressions are stored and evaluated only when the corresponding element method is called.
|
|
294
|
+
|
|
295
|
+
```html
|
|
296
|
+
<div
|
|
297
|
+
id="calendar"
|
|
298
|
+
component
|
|
299
|
+
plugin='{
|
|
300
|
+
"FullCalendar": {
|
|
301
|
+
"Calendar": ["$this", { "events": [] }]
|
|
302
|
+
},
|
|
303
|
+
"FullCalendar.render": [],
|
|
304
|
+
"getValue": "$FullCalendar.getEvents",
|
|
305
|
+
"setValue": {
|
|
306
|
+
"$FullCalendar.addEvent": "$event.value"
|
|
307
|
+
}
|
|
308
|
+
}'>
|
|
309
|
+
</div>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Reading a Value
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
const events = document.querySelector("#calendar").getValue();
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The string method reference is invoked with no arguments. Asynchronous consumers can use:
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
const events = await document.querySelector("#calendar").getValueAsync();
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Setting a Value
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
327
|
+
document.querySelector("#calendar").setValue({
|
|
328
|
+
title: "Planning meeting",
|
|
329
|
+
start: "2026-09-01"
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The runtime scope exposes:
|
|
334
|
+
|
|
335
|
+
| AST value | Description |
|
|
336
|
+
| --- | --- |
|
|
337
|
+
| `$event.value` | Value passed to `element.setValue(value)` |
|
|
338
|
+
| `$event.dispatch` | Optional dispatch argument passed to `setValue` |
|
|
339
|
+
| `$this` | Element containing the plugin instance |
|
|
340
|
+
| `$PluginName` | Resolved plugin instance |
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Programmatic API
|
|
345
|
+
|
|
346
|
+
### `registerPlugin(name, config)`
|
|
347
|
+
|
|
348
|
+
Registers or replaces a resource definition in `window.CoCreateConfig.plugins`.
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
registerPlugin("ExamplePlugin", {
|
|
352
|
+
js: "https://cdn.example.com/plugin.js",
|
|
353
|
+
css: "https://cdn.example.com/plugin.css"
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### `init(elements)`
|
|
358
|
+
|
|
359
|
+
Finds and initializes plugin configurations. It accepts an element, collection, array, or selector and returns a promise.
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
import { init } from "@cocreate/plugins";
|
|
363
|
+
|
|
364
|
+
await init(document.querySelector("#calendar"));
|
|
365
|
+
await init(document.querySelectorAll("[plugin]"));
|
|
366
|
+
await init("[plugin]");
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Calling `init()` directly is generally unnecessary because the module observes matching elements automatically. It is useful when application code needs to wait for plugin initialization to complete.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Resource Loading
|
|
374
|
+
|
|
375
|
+
- JavaScript resources are cached by URL and loaded once.
|
|
376
|
+
- CSS resources already present in the document are not inserted again.
|
|
377
|
+
- Multiple resources are loaded concurrently with `Promise.allSettled()`.
|
|
378
|
+
- String resource definitions default to `crossOrigin="anonymous"`.
|
|
379
|
+
- Object definitions can override `crossOrigin` or provide `integrity`.
|
|
380
|
+
- After loading, the runtime waits briefly for the expected browser global.
|
|
381
|
+
|
|
382
|
+
The configured name should match the library's browser global. A capitalized fallback is supported, so a configured `simpleBar` can resolve `window.SimpleBar`, but using the exact global name is recommended.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Error Handling
|
|
387
|
+
|
|
388
|
+
Resource failures are logged as warnings. Initialization fails when the expected global is still unavailable after loading:
|
|
389
|
+
|
|
390
|
+
```text
|
|
391
|
+
[Plugin System] Plugin 'ExamplePlugin' is not available on window.
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Invalid JSON, AST failures, constructor errors, and method errors are reported with the original `plugin` attribute for debugging.
|
|
395
|
+
|
|
396
|
+
Because initialization is asynchronous, application code that requires a completed instance should await `init()`.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Security
|
|
401
|
+
|
|
402
|
+
Plugin scripts execute with the same browser privileges as the application. Only register resources from trusted locations.
|
|
403
|
+
|
|
404
|
+
For third-party CDN resources:
|
|
405
|
+
|
|
406
|
+
- Pin versions instead of relying on floating tags such as `latest`.
|
|
407
|
+
- Provide Subresource Integrity metadata when available.
|
|
408
|
+
- Use an appropriate Content Security Policy.
|
|
409
|
+
- Review the library's data access and network behavior.
|
|
410
|
+
|
|
411
|
+
AST configuration controls how an already trusted plugin is initialized; it does not isolate untrusted JavaScript.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Announcements
|
|
416
|
+
|
|
417
|
+
Updates are documented in the project [CHANGELOG](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-plugins/releases).
|
|
418
|
+
|
|
419
|
+
## Roadmap
|
|
420
|
+
|
|
421
|
+
See the open [issues](https://github.com/CoCreate-app/CoCreate-plugins/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-plugins/pulls) for current development priorities.
|
|
422
|
+
|
|
423
|
+
## How to Contribute
|
|
424
|
+
|
|
425
|
+
Contributions are welcome. See [CONTRIBUTING.md](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/CONTRIBUTING.md) before opening an issue or pull request.
|
|
426
|
+
|
|
427
|
+
## License
|
|
428
|
+
|
|
429
|
+
This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
|
|
430
|
+
|
|
431
|
+
- **Open-source use:** Modifications and derivative works must comply with the AGPLv3. See the [LICENSE](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/LICENSE).
|
|
432
|
+
- **Commercial use:** A commercial license is available from [CoCreate](https://cocreate.app).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "CoCreate plugins",
|
|
5
5
|
"author": "CoCreate LLC",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "git+https://
|
|
17
|
+
"url": "git+https://gitlab.com/cocreatejs/CoCreate-plugins.git"
|
|
18
18
|
},
|
|
19
19
|
"bugs": {
|
|
20
20
|
"url": "https://github.com/CoCreate-app/CoCreate-plugins/issues"
|
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
"type": "module",
|
|
28
28
|
"main": "./src/index.js",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cocreate/observer": "^1.
|
|
30
|
+
"@cocreate/observer": "^1.23.0",
|
|
31
|
+
"@cocreate/utils": "^1.54.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@cocreate/webpack": "^1.
|
|
34
|
+
"@cocreate/webpack": "^1.9.0"
|
|
34
35
|
},
|
|
35
36
|
"allowScripts": {
|
|
36
37
|
"@cocreate/observer@1.19.0": true,
|
|
37
|
-
"@cocreate/utils@1.
|
|
38
|
+
"@cocreate/utils@1.52.0": true,
|
|
38
39
|
"@cocreate/webpack@1.4.3": true
|
|
39
40
|
}
|
|
40
41
|
}
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import Observer from '@cocreate/observer';
|
|
|
2
2
|
import { astAsync } from '@cocreate/utils';
|
|
3
3
|
|
|
4
4
|
const scriptCache = new Map();
|
|
5
|
-
const
|
|
5
|
+
const pendingElements = new WeakSet();
|
|
6
|
+
const initializedPlugins = new WeakMap();
|
|
7
|
+
const cssMarker = typeof document !== 'undefined'
|
|
6
8
|
? document.head.querySelector('meta[name="viewport"]') || document.head.lastChild
|
|
7
9
|
: null;
|
|
8
10
|
|
|
@@ -110,7 +112,8 @@ function loadCSS(item) {
|
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
114
|
* Preloads JS scripts and CSS stylesheets declared in global plugins config.
|
|
113
|
-
*
|
|
115
|
+
* Loads each plugin's JavaScript resources in declared order while allowing
|
|
116
|
+
* independent plugins and stylesheets to load in parallel.
|
|
114
117
|
* @param {Array<string>} pluginNames - Names of target plugins to preload
|
|
115
118
|
*/
|
|
116
119
|
async function preloadResources(pluginNames) {
|
|
@@ -126,7 +129,11 @@ async function preloadResources(pluginNames) {
|
|
|
126
129
|
}
|
|
127
130
|
if (config.js) {
|
|
128
131
|
const jsFiles = Array.isArray(config.js) ? config.js : [config.js];
|
|
129
|
-
|
|
132
|
+
promises.push((async () => {
|
|
133
|
+
for (const src of jsFiles) {
|
|
134
|
+
await loadScript(src);
|
|
135
|
+
}
|
|
136
|
+
})());
|
|
130
137
|
}
|
|
131
138
|
}
|
|
132
139
|
}
|
|
@@ -171,6 +178,91 @@ function getPluginLibrary(pluginName) {
|
|
|
171
178
|
return null;
|
|
172
179
|
}
|
|
173
180
|
|
|
181
|
+
function getPluginNames(configBlock) {
|
|
182
|
+
const configuredPlugins = getPluginsConfig();
|
|
183
|
+
const pluginNames = new Set();
|
|
184
|
+
|
|
185
|
+
function collect(value) {
|
|
186
|
+
if (!value || typeof value !== 'object') return;
|
|
187
|
+
|
|
188
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
189
|
+
const name = key.replace(/^\$/, '').split('.')[0];
|
|
190
|
+
if (configuredPlugins[name] || getPluginLibrary(name)) {
|
|
191
|
+
pluginNames.add(name);
|
|
192
|
+
}
|
|
193
|
+
collect(childValue);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
collect(configBlock);
|
|
198
|
+
|
|
199
|
+
return pluginNames;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isClassLike(value) {
|
|
203
|
+
if (typeof value !== 'function') return false;
|
|
204
|
+
|
|
205
|
+
const source = Function.prototype.toString.call(value);
|
|
206
|
+
if (/^class\s|^class{/.test(source)) return true;
|
|
207
|
+
|
|
208
|
+
return !!value.prototype && Object.getOwnPropertyNames(value.prototype)
|
|
209
|
+
.some(member => member !== 'constructor');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getPluginMember(library, path) {
|
|
213
|
+
let value = library;
|
|
214
|
+
|
|
215
|
+
for (const property of path) {
|
|
216
|
+
if (value === null || value === undefined || !(property in Object(value))) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
value = value[property];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function createPluginOperations(configBlock, pluginNames, libraries) {
|
|
226
|
+
const operations = [];
|
|
227
|
+
|
|
228
|
+
for (const [key, value] of Object.entries(configBlock)) {
|
|
229
|
+
const normalizedKey = key.replace(/^\$/, '');
|
|
230
|
+
const pluginName = normalizedKey.split('.')[0];
|
|
231
|
+
|
|
232
|
+
if (!pluginNames.has(pluginName)) {
|
|
233
|
+
operations.push({ ast: { [key]: value } });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let operator = normalizedKey;
|
|
238
|
+
let parameters = value;
|
|
239
|
+
const library = libraries['$' + pluginName];
|
|
240
|
+
const memberPath = normalizedKey.split('.').slice(1);
|
|
241
|
+
const pluginMember = getPluginMember(library, memberPath);
|
|
242
|
+
const members = value && typeof value === 'object' && !Array.isArray(value)
|
|
243
|
+
? Object.entries(value)
|
|
244
|
+
: [];
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
!normalizedKey.includes('.') &&
|
|
248
|
+
typeof library !== 'function' &&
|
|
249
|
+
members.length === 1
|
|
250
|
+
) {
|
|
251
|
+
const [member, memberParameters] = members[0];
|
|
252
|
+
operator = `${pluginName}.${member.replace(/^\$/, '')}`;
|
|
253
|
+
parameters = memberParameters;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
operations.push({
|
|
257
|
+
ast: { ['$' + operator]: parameters },
|
|
258
|
+
pluginName,
|
|
259
|
+
attach: !normalizedKey.includes('.') || isClassLike(pluginMember)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return operations;
|
|
264
|
+
}
|
|
265
|
+
|
|
174
266
|
/**
|
|
175
267
|
* Parses element 'plugin' attribute, executes AST configuration via astAsync,
|
|
176
268
|
* sets core library on window, and attaches instance onto element property.
|
|
@@ -183,6 +275,11 @@ async function processPlugin(el) {
|
|
|
183
275
|
const rawPluginAttr = pluginAttrNode?.value?.trim();
|
|
184
276
|
if (!rawPluginAttr) return;
|
|
185
277
|
|
|
278
|
+
// Added-node and attribute observers can discover the same element during
|
|
279
|
+
// separate mutation cycles. Do not initialize an unchanged plugin twice.
|
|
280
|
+
if (initializedPlugins.get(el) === rawPluginAttr) return;
|
|
281
|
+
initializedPlugins.set(el, rawPluginAttr);
|
|
282
|
+
|
|
186
283
|
try {
|
|
187
284
|
let configs = [];
|
|
188
285
|
|
|
@@ -193,62 +290,64 @@ async function processPlugin(el) {
|
|
|
193
290
|
configs = [{ [rawPluginAttr]: [] }];
|
|
194
291
|
}
|
|
195
292
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
293
|
+
const configPlans = configs.map(configBlock => ({
|
|
294
|
+
configBlock,
|
|
295
|
+
pluginNames: getPluginNames(configBlock)
|
|
296
|
+
}));
|
|
297
|
+
const targetPlugins = new Set(
|
|
298
|
+
configPlans.flatMap(({ pluginNames }) => Array.from(pluginNames))
|
|
299
|
+
);
|
|
204
300
|
|
|
205
301
|
await preloadResources(Array.from(targetPlugins));
|
|
206
302
|
|
|
207
|
-
for (const configBlock of
|
|
208
|
-
|
|
209
|
-
// Poll window to ensure script execution has completed
|
|
210
|
-
const pluginLib = await waitForPluginOnWindow(pluginName);
|
|
303
|
+
for (const { configBlock, pluginNames } of configPlans) {
|
|
304
|
+
const libraries = {};
|
|
211
305
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
306
|
+
for (const pluginName of pluginNames) {
|
|
307
|
+
const library = await waitForPluginOnWindow(pluginName);
|
|
308
|
+
if (!library) {
|
|
309
|
+
throw new Error(`Plugin '${pluginName}' is not available on window.`);
|
|
215
310
|
}
|
|
311
|
+
libraries['$' + pluginName] = library;
|
|
312
|
+
}
|
|
216
313
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
return new pluginLib(...args);
|
|
226
|
-
} catch (e) {
|
|
227
|
-
return pluginLib(...args);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return pluginLib;
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// Provide operator wrapper in context for AST registry lookup
|
|
234
|
-
const context = {
|
|
235
|
-
[operatorName]: opWrapper
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// Execute AST with the $ operator key so astAsync invokes operatorKey logic
|
|
239
|
-
const result = await astAsync({
|
|
240
|
-
node: el,
|
|
241
|
-
sandbox: context,
|
|
242
|
-
executeAST: { [operatorName]: astValue }
|
|
314
|
+
const scope = { ...libraries, $this: el };
|
|
315
|
+
|
|
316
|
+
if (Object.prototype.hasOwnProperty.call(configBlock, '$return')) {
|
|
317
|
+
const value = await astAsync({
|
|
318
|
+
executeAST: configBlock,
|
|
319
|
+
sandbox: scope,
|
|
320
|
+
node: el
|
|
243
321
|
});
|
|
244
322
|
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
el[pluginName] =
|
|
323
|
+
if (value !== undefined && pluginNames.size === 1) {
|
|
324
|
+
const [pluginName] = pluginNames;
|
|
325
|
+
el[pluginName] = value;
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const operations = createPluginOperations(
|
|
331
|
+
configBlock,
|
|
332
|
+
pluginNames,
|
|
333
|
+
libraries
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
for (const operation of operations) {
|
|
337
|
+
const value = await astAsync({
|
|
338
|
+
executeAST: operation.ast,
|
|
339
|
+
sandbox: scope,
|
|
340
|
+
node: el
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (operation.attach && value !== undefined) {
|
|
344
|
+
scope['$' + operation.pluginName] = value;
|
|
345
|
+
el[operation.pluginName] = value;
|
|
248
346
|
}
|
|
249
347
|
}
|
|
250
348
|
}
|
|
251
349
|
} catch (err) {
|
|
350
|
+
initializedPlugins.delete(el);
|
|
252
351
|
console.error('[Plugin System] Failed to parse and execute plugin configuration:', err, rawPluginAttr);
|
|
253
352
|
}
|
|
254
353
|
}
|
|
@@ -258,21 +357,33 @@ async function processPlugin(el) {
|
|
|
258
357
|
* @param {Element|NodeList|Array|string} elements - Target element(s) or DOM query selector
|
|
259
358
|
*/
|
|
260
359
|
export function init(elements) {
|
|
261
|
-
if (!elements) return;
|
|
360
|
+
if (!elements) return Promise.resolve([]);
|
|
262
361
|
|
|
263
362
|
if (typeof elements === 'string') {
|
|
264
363
|
elements = document.querySelectorAll(elements);
|
|
265
364
|
}
|
|
266
365
|
|
|
267
366
|
if (elements instanceof Element) {
|
|
268
|
-
processPlugin(elements);
|
|
367
|
+
return processPlugin(elements);
|
|
269
368
|
} else if (elements.length !== undefined || Array.isArray(elements)) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
369
|
+
return Promise.all(
|
|
370
|
+
Array.from(elements)
|
|
371
|
+
.filter(el => el instanceof Element)
|
|
372
|
+
.map(el => processPlugin(el))
|
|
373
|
+
);
|
|
275
374
|
}
|
|
375
|
+
|
|
376
|
+
return Promise.resolve([]);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function scheduleInit(element) {
|
|
380
|
+
if (!element || pendingElements.has(element)) return;
|
|
381
|
+
|
|
382
|
+
pendingElements.add(element);
|
|
383
|
+
requestAnimationFrame(() => {
|
|
384
|
+
pendingElements.delete(element);
|
|
385
|
+
if (element.isConnected) init(element);
|
|
386
|
+
});
|
|
276
387
|
}
|
|
277
388
|
|
|
278
389
|
if (typeof document !== 'undefined') {
|
|
@@ -282,10 +393,12 @@ if (typeof document !== 'undefined') {
|
|
|
282
393
|
types: ["addedNodes", "attributes"],
|
|
283
394
|
selector: selector,
|
|
284
395
|
attributeFilter: ["plugin"],
|
|
285
|
-
callback: (mutation) =>
|
|
396
|
+
callback: (mutation) => {
|
|
397
|
+
scheduleInit(mutation.target);
|
|
398
|
+
}
|
|
286
399
|
});
|
|
287
400
|
|
|
288
401
|
init(document.querySelectorAll(selector));
|
|
289
402
|
}
|
|
290
403
|
|
|
291
|
-
export default { init, registerPlugin };
|
|
404
|
+
export default { init, registerPlugin };
|
package/webpack.config.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
|
4
|
+
import { EsbuildPlugin } from "esbuild-loader";
|
|
5
|
+
import webpackPackage from "@cocreate/webpack";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const { FileUploader } = webpackPackage;
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
export default async (env, argv) => {
|
|
7
11
|
const isProduction = argv && argv.mode === "production";
|
|
8
12
|
const config = {
|
|
9
13
|
entry: {
|
|
@@ -52,7 +56,7 @@ module.exports = async (env, argv) => {
|
|
|
52
56
|
})
|
|
53
57
|
],
|
|
54
58
|
splitChunks: {
|
|
55
|
-
|
|
59
|
+
cacheGroups: {
|
|
56
60
|
defaultVendors: false
|
|
57
61
|
}
|
|
58
62
|
}
|