@cocreate/events 1.33.0 → 1.36.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 CHANGED
@@ -13,7 +13,7 @@ Fork the [project on Github](https://github.com/CoCreate-app/CoCreate-toggle) an
13
13
  ```
14
14
  git toggle https://github.com/contributor/CoCreate-toggle.git
15
15
  cd CoCreate-toggle
16
- git remote add upstream https://github.com/CoCreate-app/CoCreate-toggle.git
16
+ git remote add upstream https://gitlab.com/cocreatejs/CoCreate-toggle.git
17
17
  ```
18
18
 
19
19
  ## Create a Topic Branch
package/README.md CHANGED
@@ -1,91 +1,474 @@
1
- # CoCreate-toggle
1
+ # @cocreate/events
2
+
3
+ A declarative, event-driven DOM engine configured entirely through HTML5 attributes. Bind any DOM event, observer, or custom event to one or many target elements, then assign values to their properties, attributes, styles, classes, or markup without writing JavaScript. Every binding follows a single, predictable grammar: an **event prefix** declares when to run, **dash options** declare how and where to run, and **dot assignments** declare what to change.
4
+
5
+ ---
6
+
7
+ ## Documentation
8
+
9
+ For a complete guide, live playground, and working demos refer to the [documentation](https://cocreate.app/docs/events).
10
+
11
+ ## Table of Contents
12
+
13
+ * [Features](#features)
14
+ * [Installation](#installation)
15
+ * [Usage](#usage)
16
+ * [How it Works](#how-it-works)
17
+ * [Anatomy of a Binding](#anatomy-of-a-binding)
18
+ * [Event Prefixes](#event-prefixes)
19
+ * [Assignment Targets](#assignment-targets)
20
+ * [Value Semantics](#value-semantics)
21
+ * [Targeting Elements](#targeting-elements)
22
+ * [Options Reference](#options-reference)
23
+ * [Observers](#observers)
24
+ * [Groups](#groups)
25
+ * [Custom Events](#custom-events)
26
+ * [Actions Integration](#actions-integration)
27
+ * [Behavior Notes](#behavior-notes)
28
+ * [Announcements](#announcements)
29
+ * [Roadmap](#roadmap)
30
+ * [How to Contribute](#how-to-contribute)
31
+ * [About](#about)
32
+ * [License](#license)
33
+
34
+ ---
35
+
36
+ ## Features
37
+
38
+ * **Attribute-Driven Bindings:** Wire clicks, hovers, inputs, focus, mutations, resizes, and intersections without writing JavaScript.
39
+ * **Universal Assignment:** Set any DOM property, HTML attribute, style declaration, class, or adjacent markup on a target element.
40
+ * **Multi-Target Dispatch:** A single event can update many elements resolved through relational selectors such as `$parent`, `$next`, `$closest`, and `$document`.
41
+ * **Value Rotation:** Comma separated values cycle on each trigger, producing toggles, tri-states, and round-robin behavior from a single attribute.
42
+ * **Grouped Selection:** `-group` links elements so that activating one deactivates its peers, giving tabs, accordions, and radio behavior for free.
43
+ * **Observer Support:** Mutation, intersection, and resize observers are declared with the same grammar as ordinary DOM events.
44
+ * **Custom Events:** Register application-specific event names and drive them from markup or from the Actions pipeline.
45
+ * **Timing Controls:** Debounce, delay, repeat, gate, and one-shot behavior via `-set-timeout`, `-set-interval`, `-on`, and `-once`.
46
+ * **Actions Integration:** Every prefix is registered as an action, so bindings can be chained and sequenced with other CoCreate modules.
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ npm install @cocreate/events
54
+ ```
2
55
 
3
- A simple toggle component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API. Take it for a spin in our [playground!](https://cocreate.app/docs/toggle)
56
+ ```bash
57
+ yarn add @cocreate/events
58
+ ```
4
59
 
5
- ![minified](https://img.badgesize.io/https://cdn.cocreate.app/toggle/latest/CoCreate-toggle.min.js?style=flat-square&label=minified&color=orange)
6
- ![gzip](https://img.badgesize.io/https://cdn.cocreate.app/toggle/latest/CoCreate-toggle.min.js?compression=gzip&style=flat-square&label=gzip&color=yellow)
7
- ![brotli](https://img.badgesize.io/https://cdn.cocreate.app/toggle/latest/CoCreate-toggle.min.js?compression=brotli&style=flat-square&label=brotli)
8
- ![GitHub latest release](https://img.shields.io/github/v/release/CoCreate-app/CoCreate-toggle?style=flat-square)
9
- ![License](https://img.shields.io/github/license/CoCreate-app/CoCreate-toggle?style=flat-square)
10
- ![Hiring](https://img.shields.io/static/v1?style=flat-square&label=&message=Hiring&color=blueviolet)
60
+ ### CDN
11
61
 
12
- ![CoCreate-toggle](https://cdn.cocreate.app/docs/CoCreate-toggle.gif)
62
+ ```html
63
+ <script src="https://cdn.cocreate.app/events/latest/CoCreate-events.min.js"></script>
64
+ ```
13
65
 
14
- ## [Docs & Demo](https://cocreate.app/docs/toggle)
66
+ ---
15
67
 
16
- For a complete guide and working demo refer to the [doumentation](https://cocreate.app/docs/toggle)
68
+ ## Usage
17
69
 
18
- ## CDN
70
+ ### Toggle an Attribute
19
71
 
20
72
  ```html
21
- <script src="https://cdn.cocreate.app/toggle/latest/CoCreate-toggle.min.js"></script>
73
+ <button toggle-query="#panel" toggle.hidden="true,false">Toggle panel</button>
74
+
75
+ <div id="panel">Panel content</div>
22
76
  ```
23
77
 
78
+ ### Set a Property on Click
79
+
24
80
  ```html
25
- <script src="https://cdn.cocreate.app/toggle/latest/CoCreate-toggle.min.css"></script>
81
+ <button click-query="$next" click.readonly="">Enable editing</button>
82
+ <textarea readonly></textarea>
26
83
  ```
27
84
 
28
- ## NPM
85
+ ### Copy a Value Between Elements
29
86
 
30
- ```shell
31
- $ npm i @cocreate/toggle
87
+ ```html
88
+ <input input-query="$document #preview" input.value="$value" />
89
+ <span id="preview"></span>
32
90
  ```
33
91
 
34
- ## yarn
92
+ ### Apply Styles and Classes
35
93
 
36
- ```shell
37
- $ yarn install @cocreate/toggle
94
+ ```html
95
+ <button
96
+ click-query="#card"
97
+ click.style="background:tomato,background:seagreen"
98
+ click.class="is-active"></button>
38
99
  ```
39
100
 
40
- # Table of Contents
101
+ ### Reveal on Hover
41
102
 
42
- - [Table of Contents](#table-of-contents)
43
- - [Announcements](#announcements)
44
- - [Roadmap](#roadmap)
45
- - [How to Contribute](#how-to-contribute)
46
- - [About](#about)
47
- - [License](#license)
103
+ ```html
104
+ <div hover-query=".tools" hover.hidden="">
105
+ <div class="tools" hidden>Tools</div>
106
+ </div>
107
+ ```
48
108
 
49
- <a name="announcements"></a>
109
+ ### Insert Markup
110
+
111
+ ```html
112
+ <textarea click-query="#output" click.insertAdjacentHTML="beforeend"></textarea>
113
+ ```
50
114
 
51
- # Announcements
115
+ The inserted content is read from the source element's value.
52
116
 
53
- All updates to this library are documented in our [CHANGELOG](https://github.com/CoCreate-app/CoCreate-toggle/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-toggle/releases). You may also subscribe to email for releases and breaking changes.
117
+ ---
54
118
 
55
- <a name="roadmap"></a>
119
+ ## How it Works
56
120
 
57
- # Roadmap
121
+ 1. On load the module registers a set of built-in event prefixes and scans the document for matching elements.
122
+ 2. An element is initialized when it declares the bare prefix, a `-query`, or a `-group` attribute.
123
+ 3. Listeners are attached for the DOM events mapped to that prefix, or for the events named in `-events`.
124
+ 4. When the event fires, source elements are resolved: the element itself, or every member of its `-group`.
125
+ 5. Timing options such as `-on`, `-set-timeout`, `-set-interval`, and `-once` are evaluated.
126
+ 6. Target elements are resolved from `-query` using relational selectors.
127
+ 7. Every `prefix.target` assignment on the source element is applied to each target element.
128
+ 8. A `prefixEnd` event is dispatched on the element so that action chains can continue.
129
+ 9. A `MutationObserver` initializes elements added to the DOM later, so dynamically rendered markup is bound automatically.
58
130
 
59
- If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/CoCreate-app/CoCreate-toggle/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-toggle/pulls). We would love to hear your feedback.
131
+ ---
60
132
 
61
- <a name="about"></a>
133
+ ## Anatomy of a Binding
62
134
 
63
- # About
135
+ Every binding is composed of three parts. The separator character determines the role of the attribute.
64
136
 
65
- CoCreate-toggle is guided and supported by the CoCreate Developer Experience Team.
137
+ ```html
138
+ <button
139
+ click-query="$document #chat-body"
140
+ click-once
141
+ click.scroll-to="bottom"></button>
142
+ ```
66
143
 
67
- Please Email the Developer Experience Team [here](mailto:develop@cocreate.app) in case of any queries.
144
+ | Part | Separator | Purpose | Example |
145
+ | ---- | --------- | ------- | ------- |
146
+ | Prefix | none | Declares the event that triggers the binding | `click` |
147
+ | Option | dash `-` | Configures how and where the binding runs | `click-query`, `click-once` |
148
+ | Assignment | dot `.` | Declares what is written to the target | `click.value`, `click.hidden` |
149
+
150
+ > Dash attributes are always configuration. Dot attributes are always assignments. Any attribute using a dash is ignored by the assignment engine, and vice versa.
151
+
152
+ ---
153
+
154
+ ## Event Prefixes
155
+
156
+ Each prefix listens to one or more native DOM events.
157
+
158
+ | Prefix | DOM Events | Typical Use |
159
+ | ------ | ---------- | ----------- |
160
+ | `click` | `click` | Buttons, links, menu items |
161
+ | `toggle` | `click` | Show/hide, on/off, rotating states |
162
+ | `selected` | `click` | Tabs and selection lists, usually with `-group` |
163
+ | `hover` | `mouseover`, `mouseout` | Reveal on hover, auto-reverting |
164
+ | `mouseover` | `mouseover` | Enter-only behavior |
165
+ | `mouseout` | `mouseout` | Leave-only behavior |
166
+ | `input` | `input` | Live binding as the user types |
167
+ | `change` | `change` | Commit on change, selects and checkboxes |
168
+ | `focus` | `focus` | Focus styling and hints |
169
+ | `blur` | `blur` | Validation and cleanup |
170
+ | `onload` | runs immediately | Initialization on parse or insertion |
171
+ | `observe` | `MutationObserver` | React to DOM changes |
172
+ | `intersection` | `IntersectionObserver` | Scroll reveals, lazy loading |
173
+ | `resize` | `ResizeObserver`, load | Responsive behavior |
174
+ | `localstorage` | runs immediately | Hydrate from stored state |
175
+
176
+ Override the events for any prefix with `-events`:
177
+
178
+ ```html
179
+ <div click-events="dblclick, keyup" click-query="#panel" click.hidden=""></div>
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Assignment Targets
185
+
186
+ The name after the dot determines what is written.
187
+
188
+ | Assignment | Behavior |
189
+ | ---------- | -------- |
190
+ | `prefix.value` | Uses `setValue()` when available so bound modules receive an `input` event, otherwise sets `.value` |
191
+ | `prefix.style` | Applies `property:value` declarations, removing the previous declaration when rotating |
192
+ | `prefix.class` | Adds and removes class names, supporting multiple classes per value |
193
+ | `prefix.insertAdjacentHTML` | Inserts the source value at the given position, integrating with CoCreate rich text when inside `[contenteditable]` |
194
+ | `prefix.insertAdjacentText` | Same as above, as plain text |
195
+ | `prefix.<property>` | Any existing DOM property, for example `hidden`, `readonly`, `disabled`, `checked`, `textContent` |
196
+ | `prefix.$<method>` | Invokes a DOM method with the resolved value; use `focus.$focus="$undefined"` for an optional argument |
197
+ | `prefix.<attribute>` | Any attribute when no matching property exists, for example `click.data-state` or `click.aria-expanded` |
198
+
199
+ Valid positions for the insert assignments are `beforebegin`, `afterbegin`, `beforeend`, and `afterend`.
200
+
201
+ ```html
202
+ <button click-query="#box" click.aria-expanded="true,false">Expand</button>
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Value Semantics
208
+
209
+ ### Static values
210
+
211
+ ```html
212
+ <button click-query="#status" click.textContent="Saved">Save</button>
213
+ ```
214
+
215
+ ### Dynamic values
216
+
217
+ `$value` resolves to the source element's current value at trigger time.
218
+
219
+ ```html
220
+ <select change-query="#preview" change.textContent="$value">
221
+ <option>One</option>
222
+ <option>Two</option>
223
+ </select>
224
+ ```
225
+
226
+ ### Rotation
227
+
228
+ Comma separated values rotate on each trigger. The engine reads the target's current value, finds its position in the list, and applies the next entry, wrapping at the end.
229
+
230
+ ```html
231
+ <button click-query="#panel" click.data-state="open,half,closed"></button>
232
+ ```
233
+
234
+ Sequence: `open` → `half` → `closed` → `open`.
235
+
236
+ ---
237
+
238
+ ## Targeting Elements
239
+
240
+ `-query` accepts standard CSS selectors, comma separated selector lists, and relational operators. When omitted, the source element is its own target.
241
+
242
+ | Operator | Resolves To |
243
+ | -------- | ----------- |
244
+ | `$document` | The owning document |
245
+ | `$parent` | The parent element |
246
+ | `$next` | The next element sibling |
247
+ | `$previous` | The previous element sibling |
248
+ | `$closest <selector>` | Nearest matching ancestor |
249
+ | `$frame` | The frame element of the current document |
250
+ | `$top` | The top-level document |
251
+
252
+ Operators compose with ordinary selectors, and a trailing `[]` selects **all** matches instead of the first.
253
+
254
+ ```html
255
+ <span click-query="$closest .card .title[]" click.contentEditable="true"></span>
256
+ ```
257
+
258
+ Selectors also support media suffixes, so a binding can be limited to a viewport range:
259
+
260
+ ```html
261
+ <div click-query="#menu@xs@sm" click.hidden=""></div>
262
+ ```
263
+
264
+ Available ranges are `xs`, `sm`, `md`, `lg`, and `xl`.
265
+
266
+ ---
267
+
268
+ ## Options Reference
269
+
270
+ | Option | Description |
271
+ | ------ | ----------- |
272
+ | `prefix-query` | Selector resolving the target elements |
273
+ | `prefix-element` | Selector resolving the element to move with `prefix.insertAdjacentElement` |
274
+ | `prefix-selector` | Selector observed by `observe`, keeping `-query` free for targets |
275
+ | `prefix-group` | Links elements so activating one deactivates its peers |
276
+ | `prefix-events` | Comma separated DOM events replacing the prefix defaults |
277
+ | `prefix-once` | Runs the binding a single time |
278
+ | `prefix-set-timeout` | Delay in milliseconds before running |
279
+ | `prefix-set-interval` | Repeat interval in milliseconds, `0` clears it |
280
+ | `prefix-on` | Waits for the named document event before running |
281
+ | `prefix-prevent-default` | Calls `preventDefault()` |
282
+ | `prefix-stop-propagation` | Calls `stopPropagation()` |
283
+ | `prefix-stop-immediate-propagation` | Calls `stopImmediatePropagation()` |
284
+ | `prefix-types` | Mutation types observed by `observe` |
285
+ | `prefix-attribute-filter` | Attribute names observed by `observe` |
286
+ | `prefix-root` | Root element selector for `intersection` |
287
+ | `prefix-root-margin` | Root margin for `intersection` |
288
+ | `prefix-threshold` | JSON array of thresholds for `intersection` |
289
+ | `prefix-trigger` | `visible`, `not-visible`, or `both` for `intersection` |
290
+ | `<event>-bubbles` | Listener option, named after the DOM event |
291
+ | `<event>-cancelable` | Listener option, named after the DOM event |
292
+ | `<event>-composed` | Listener option, named after the DOM event |
293
+
294
+ The module also writes status attributes you can bind against: `prefix-is-visible` during intersection changes and `prefix-if-value` carrying the observed width during resizes.
295
+
296
+ `prefix.insertAdjacentElement` moves the first element matched by `prefix-element` to the requested position on the `prefix-query` target. Unlike the HTML/text variants, it passes the element itself rather than reading its value:
297
+
298
+ ```html
299
+ <button
300
+ click-query="$document #preview-container"
301
+ click.insertAdjacentElement="beforeend"
302
+ click-element="$document #preview">
303
+ Show preview
304
+ </button>
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Observers
310
+
311
+ ### Mutation
312
+
313
+ Use `-selector` for what to watch and `-query` for what to update.
314
+
315
+ ```html
316
+ <div
317
+ observe-selector=".message"
318
+ observe.scroll-to="bottom"></div>
319
+ ```
320
+
321
+ Watch attributes instead of insertions:
322
+
323
+ ```html
324
+ <div
325
+ observe-selector="#editor"
326
+ observe-attribute-filter="data-state"
327
+ observe-query="#indicator"
328
+ observe.textContent="changed"></div>
329
+ ```
330
+
331
+ ### Intersection
332
+
333
+ ```html
334
+ <img
335
+ intersection
336
+ intersection-threshold="[0.25]"
337
+ intersection-trigger="visible"
338
+ intersection-once
339
+ intersection.class="fade-in" />
340
+ ```
341
+
342
+ ### Resize
343
+
344
+ ```html
345
+ <div resize resize-query="$parent" resize.data-width="$width"></div>
346
+ ```
347
+
348
+ The observed width, including its `px` unit, is available as `$width` and can
349
+ be embedded in assignments:
350
+
351
+ ```html
352
+ <aside
353
+ resize
354
+ resize-query="#page-topbar, .main-content"
355
+ resize.style="margin-right:$width"></aside>
356
+ ```
357
+
358
+ ---
68
359
 
69
- CoCreate-toggle is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
360
+ ## Groups
70
361
 
71
- <a name="contribute"></a>
362
+ Elements sharing a `-group` value act as a single unit. When one is triggered it applies its assignments, while every other member is deactivated and has its assignments cleared. This produces tab bars, accordions, and radio groups without JavaScript.
363
+
364
+ ```html
365
+ <nav>
366
+ <button selected-group="tabs" selected-query="#tab-1" selected.hidden="false" class="tab">One</button>
367
+ <button selected-group="tabs" selected-query="#tab-2" selected.hidden="false" class="tab">Two</button>
368
+ </nav>
369
+
370
+ <section id="tab-1">First</section>
371
+ <section id="tab-2" hidden>Second</section>
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Custom Events
377
+
378
+ Declare an application specific prefix with `event-name`. Multiple names are comma separated, and the prefix becomes available to markup and to the Actions pipeline.
379
+
380
+ ```html
381
+ <div event-name="publish, archive"></div>
382
+
383
+ <button publish-query="#doc" publish.data-status="published">Publish</button>
384
+ ```
385
+
386
+ Dispatch it from JavaScript or from an action chain:
387
+
388
+ ```javascript
389
+ import events from "@cocreate/events";
390
+
391
+ events.init("publish", ["click"]);
392
+ ```
72
393
 
73
- # How to Contribute
394
+ ---
74
395
 
75
- We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/CoCreate-app/CoCreate-toggle/blob/master/CONTRIBUTING.md) guide for details.
396
+ ## Actions Integration
76
397
 
77
- We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/CoCreate-app/CoCreate-toggle/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-toggle/pulls) or merely upvote or comment on existing issues or pull requests.
398
+ Every prefix is registered with `@cocreate/actions`, so bindings participate in sequenced action chains and can be invoked by name.
399
+
400
+ ```html
401
+ <button actions="save, click">Save and continue</button>
402
+ ```
403
+
404
+ When an element's `actions` attribute already includes the prefix, the direct DOM listener stands down and the action chain drives the binding, preventing duplicate execution.
405
+
406
+ Each run dispatches a `prefixEnd` event on the element, which is how the action pipeline knows the binding has completed and the next action may start.
407
+
408
+ Actions may also pass a selector as a parameter, which is used when the element has no `-query`:
409
+
410
+ ```html
411
+ <button actions="click(#panel)"></button>
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Behavior Notes
417
+
418
+ * An element is only initialized if it declares the bare prefix, `-query`, or `-group`. An element carrying assignments alone is not bound.
419
+ * Rotation compares the target's current value as a string. Boolean DOM properties such as `hidden` and `readonly` report real booleans, so prefer explicit two-value rotations or attribute assignments when precise toggling matters.
420
+ * `hover` defers its revert until the pointer actually leaves the target, so bindings do not flicker while the cursor remains over the element.
421
+ * Assignments are applied in attribute order, allowing a single event to compose several changes on the same target.
422
+ * Dynamically inserted markup is initialized automatically, so templates and rendered lists behave the same as static markup.
423
+
424
+ ---
425
+
426
+ ## Announcements
427
+
428
+ All updates to this library are documented in our [CHANGELOG](https://github.com/CoCreate-app/CoCreate-events/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-events/releases). You may also subscribe to email for releases and breaking changes.
429
+
430
+ ---
431
+
432
+ ## Roadmap
433
+
434
+ Upcoming enhancements include:
435
+
436
+ * Development mode diagnostics for unrecognized and legacy attributes
437
+ * Typed value coercion for boolean properties
438
+ * Value source queries for assignments sourced from other elements
439
+ * Expanded observer configuration
440
+ * Devtools inspection of bindings attached to an element
441
+
442
+ If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/CoCreate-app/CoCreate-events/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-events/pulls). We would love to hear your feedback.
443
+
444
+ ---
445
+
446
+ ## How to Contribute
447
+
448
+ We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/CoCreate-app/CoCreate-events/blob/master/CONTRIBUTING.md) guide for details.
449
+
450
+ We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/CoCreate-app/CoCreate-events/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-events/pulls) or merely upvote or comment on existing issues or pull requests.
78
451
 
79
452
  We appreciate your continued support, thank you!
80
453
 
81
- <a name="license"></a>
454
+ ---
455
+
456
+ ## About
457
+
458
+ CoCreate-events is guided and supported by the CoCreate Developer Experience Team.
459
+
460
+ Please Email the Developer Experience Team [here](mailto:develop@cocreate.app) in case of any queries.
461
+
462
+ CoCreate-events is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
463
+
464
+ ---
82
465
 
83
- # License
466
+ ## License
84
467
 
85
468
  This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
86
469
 
87
- - **Open Source Use**: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the [LICENSE file](https://github.com/CoCreate-app/CoCreate-events/blob/master/LICENSE).
470
+ * **Open Source Use**: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the [LICENSE file](https://github.com/CoCreate-app/CoCreate-events/blob/master/LICENSE).
88
471
 
89
- - **Commercial Use**: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our [website](https://cocreate.app). This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
472
+ * **Commercial Use**: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our [website](https://cocreate.app). This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
90
473
 
91
474
  If you have not purchased a commercial license and intend to use this software for commercial purposes, you are required to sign up for an API key on our website.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/events",
3
- "version": "1.33.0",
3
+ "version": "1.36.0",
4
4
  "description": "A simple events component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "events",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/CoCreate-app/CoCreate-events.git"
25
+ "url": "git+https://gitlab.com/cocreatejs/CoCreate-events.git"
26
26
  },
27
27
  "author": "CoCreate LLC",
28
28
  "license": "AGPL-3.0",
@@ -36,13 +36,13 @@
36
36
  },
37
37
  "main": "./src/index.js",
38
38
  "dependencies": {
39
- "@cocreate/actions": "^1.23.0",
40
- "@cocreate/element-prototype": "^1.34.0",
41
- "@cocreate/observer": "^1.21.0",
42
- "@cocreate/utils": "^1.50.3"
39
+ "@cocreate/actions": "^1.24.0",
40
+ "@cocreate/element-prototype": "^1.35.0",
41
+ "@cocreate/observer": "^1.23.0",
42
+ "@cocreate/utils": "^1.54.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@cocreate/webpack": "^1.7.0"
45
+ "@cocreate/webpack": "^1.9.0"
46
46
  },
47
47
  "allowScripts": {
48
48
  "@cocreate/actions@1.21.4": true,
package/src/index.js CHANGED
@@ -29,6 +29,27 @@ import { getAttribute } from "@cocreate/element-prototype/src/getAttribute.js";
29
29
  const CoCreateEvents = {
30
30
  elements: new Map(),
31
31
 
32
+ // Known targets initialize for every prefix without an additional marker.
33
+ // Arbitrary prefix.* properties and methods still opt in through
34
+ // [prefix], [prefix-group], or [prefix-query].
35
+ knownTargets: [
36
+ "class",
37
+ "src",
38
+ "href",
39
+ "value",
40
+ "hidden",
41
+ "disabled",
42
+ "checked",
43
+ "selected",
44
+ "aria-expanded",
45
+ "aria-hidden",
46
+ "aria-pressed",
47
+ "aria-current",
48
+ "textContent",
49
+ "innerHTML",
50
+ "save"
51
+ ],
52
+
32
53
  init: function (prefix, events) {
33
54
  if (prefix && events) {
34
55
  this.initPrefix(prefix, events);
@@ -48,7 +69,8 @@ const CoCreateEvents = {
48
69
  "resize": ["onload", "resize"],
49
70
  "localstorage": ["onload"],
50
71
  "focus": ["focus"],
51
- "blur": ["blur"]
72
+ "blur": ["blur"],
73
+ "error": ["error"]
52
74
  };
53
75
 
54
76
  for (const [p, e] of Object.entries(defaults)) {
@@ -59,11 +81,11 @@ const CoCreateEvents = {
59
81
  // Initialize from existing elements with [event-name]
60
82
  const customEventEls = document.querySelectorAll("[event-name]");
61
83
  const processedNames = new Set();
62
-
84
+
63
85
  for (const customEventEl of customEventEls) {
64
86
  let names = customEventEl.getAttribute("event-name");
65
87
  if (!names) continue;
66
-
88
+
67
89
  names.split(",").forEach(name => {
68
90
  name = name.trim();
69
91
  if (name && !processedNames.has(name)) {
@@ -105,7 +127,14 @@ const CoCreateEvents = {
105
127
  });
106
128
 
107
129
  // Updated selector to use dash for options: prefix-group, prefix-query
108
- const selector = `[${prefix}], [${prefix}-group], [${prefix}-query]`;
130
+ const knownTargets = this.knownTargets.map(target => `[${prefix}\\.${target}]`);
131
+ const selector = [
132
+ `[${prefix}]`,
133
+ `[${prefix}-group]`,
134
+ `[${prefix}-query]`,
135
+ `[${prefix}-element]`,
136
+ ...knownTargets
137
+ ].join(", ");
109
138
 
110
139
  observer.init({
111
140
  name: "CoCreateEventAttributes",
@@ -213,7 +242,7 @@ const CoCreateEvents = {
213
242
  prefixData.prev = event.type;
214
243
 
215
244
  // Check for 'actions' blocking
216
- const actionsAttr = target.getAttribute("actions") || "";
245
+ const actionsAttr = target.attributes.getNamedItem("actions")?.value || "";
217
246
  if (actionsAttr.includes(prefix)) return;
218
247
 
219
248
  // Handle lifecycle controls (using dash)
@@ -222,7 +251,7 @@ const CoCreateEvents = {
222
251
  { attr: "stop-propagation", method: "stopPropagation" },
223
252
  { attr: "stop-immediate-propagation", method: "stopImmediatePropagation" }
224
253
  ];
225
-
254
+
226
255
  for (const { attr, method } of lifecycleAttributes) {
227
256
  const attrValue = target.getAttribute(`${prefix}-${attr}`);
228
257
  if (attrValue !== null && attrValue !== "false") {
@@ -240,7 +269,7 @@ const CoCreateEvents = {
240
269
  let attributeFilter = (el.getAttribute(`${prefix}-attribute-filter`) || "")
241
270
  .split(/\s*,\s*/)
242
271
  .filter(item => item);
243
-
272
+
244
273
  let types = (el.getAttribute(`${prefix}-types`) || "")
245
274
  .split(/\s*,\s*/)
246
275
  .filter(item => item);
@@ -273,8 +302,10 @@ const CoCreateEvents = {
273
302
  const resizeObserver = new ResizeObserver((entries) => {
274
303
  const lastEntry = entries[entries.length - 1];
275
304
  const { width } = lastEntry.contentRect;
276
- // Keeps dash style for this status attribute
277
- el.setAttribute(`${prefix}-if-value`, width + "px");
305
+ const widthValue = width + "px";
306
+ el.width = widthValue;
307
+ // Keeps dash style for conditional resize bindings.
308
+ el.setAttribute(`${prefix}-if-value`, widthValue);
278
309
  this.__updateElements(el, prefix);
279
310
  });
280
311
  resizeObserver.observe(el);
@@ -289,8 +320,8 @@ const CoCreateEvents = {
289
320
  const trigger = el.getAttribute(`${prefix}-trigger`) || "both";
290
321
 
291
322
  const root = rootSelector ? document.querySelector(rootSelector) : null;
292
- const validThreshold = Array.isArray(threshold)
293
- ? threshold.filter(v => v >= 0 && v <= 1)
323
+ const validThreshold = Array.isArray(threshold)
324
+ ? threshold.filter(v => v >= 0 && v <= 1)
294
325
  : [0];
295
326
 
296
327
  const once = el.getAttribute(`${prefix}-once`);
@@ -327,14 +358,14 @@ const CoCreateEvents = {
327
358
  // 1. Determine targets based on prefix-group
328
359
  let sourceElements = [rootElement];
329
360
  const sourceGroup = rootElement.getAttribute(`${prefix}-group`);
330
-
361
+
331
362
  if (sourceGroup) {
332
363
  sourceElements = document.querySelectorAll(`[${prefix}-group="${sourceGroup}"]`);
333
364
  }
334
365
 
335
366
  // 2. Iterate through sourceElements
336
367
  for (const sourceElement of sourceElements) {
337
-
368
+
338
369
  // --- Global Options (Dash) ---
339
370
 
340
371
  // onEvent
@@ -353,18 +384,18 @@ const CoCreateEvents = {
353
384
  const intervalAttr = sourceElement.getAttribute(`${prefix}-set-interval`);
354
385
  if (intervalAttr !== null) {
355
386
  const intervalMs = parseInt(intervalAttr, 10) || 0;
356
-
387
+
357
388
  if (!sourceElement.eventsInterval) {
358
389
  sourceElement.eventsInterval = {};
359
390
  }
360
-
391
+
361
392
  const currentInterval = sourceElement.eventsInterval[prefix];
362
393
 
363
394
  if (intervalMs > 0) {
364
395
  // Start or update interval
365
396
  if (!currentInterval || currentInterval.delay !== intervalMs) {
366
397
  if (currentInterval) clearInterval(currentInterval.id);
367
-
398
+
368
399
  sourceElement.eventsInterval[prefix] = {
369
400
  delay: intervalMs,
370
401
  id: setInterval(() => {
@@ -401,7 +432,7 @@ const CoCreateEvents = {
401
432
 
402
433
  // once
403
434
  const onceAttr = sourceElement.getAttribute(`${prefix}-once`);
404
- if (onceAttr !== null) {
435
+ if (onceAttr !== null) {
405
436
  if (!sourceElement.eventsOnce) {
406
437
  sourceElement.eventsOnce = [prefix];
407
438
  } else if (sourceElement.eventsOnce.includes(prefix)) {
@@ -412,7 +443,9 @@ const CoCreateEvents = {
412
443
  }
413
444
 
414
445
  // --- Resolve Targets ---
415
- const querySelector = sourceElement.getAttribute(`${prefix}-query`);
446
+ // const querySelector = sourceElement.getAttribute(`${prefix}-query`);
447
+ const querySelector =
448
+ sourceElement.attributes[`${prefix}-query`]?.value;
416
449
  let targetElements = [sourceElement];
417
450
 
418
451
  if (querySelector || params) {
@@ -460,44 +493,65 @@ const CoCreateEvents = {
460
493
  // Iterate Attributes
461
494
  for (const attribute of sourceElement.attributes) {
462
495
  const name = attribute.name;
463
-
496
+
464
497
  // Optimization: Skip if shorter than prefix + dot + min char
465
498
  if (name.length <= prefix.length + 1) continue;
466
-
499
+
467
500
  // STRICT RULE: Only process attributes with DOT separator.
468
501
  // All options use Dash, so they will be ignored here.
469
502
  if (!name.startsWith(prefix) || name[prefix.length] !== '.') continue;
470
503
 
471
- // Extract target name (e.g., 'value', 'style', 'src', 'data-something')
472
- const targetName = name.substring(prefix.length + 1);
504
+ // Extract target name (e.g., 'value', 'style', 'src', 'data-something')
505
+ let targetName = name.substring(prefix.length + 1);
506
+ const explicitMethod = targetName.startsWith("$");
473
507
 
474
- // Get Value
475
- let value = sourceElement.getAttribute(name);
476
- if (value === "$value" && sourceElement.getValue) {
477
- value = await sourceElement.getValue();
478
- }
479
-
480
- // Parse CSV for rotation
481
508
  let values = [];
482
- if (typeof value === 'string') {
483
- values = value.split(',').map(v => v.trim());
484
- } else if (Array.isArray(value)) {
485
- values = value;
509
+ let value = attribute.value;
510
+ if (value.startsWith("(") && value.endsWith(")")) {
511
+ let depth = 0;
512
+ let start = 0;
513
+ for (let i = 0; i < value.length; i++) {
514
+ const char = value.charCodeAt(i);
515
+ if (char === 40) {
516
+ depth++;
517
+ } else if (char === 41) {
518
+ depth--;
519
+ } else if (char === 44 && depth === 0) {
520
+ values.push(value.slice(start, i).trim().slice(1, -1));
521
+ start = i + 1;
522
+ }
523
+ }
524
+ values.push(value.slice(start).trim().slice(1, -1));
486
525
  } else {
487
- values = [value];
526
+ value = sourceElement.getAttribute(name);
527
+ // Parse CSV for rotation
528
+ if (typeof value === 'string') {
529
+ values = value.split(',').map(v => v.trim());
530
+ } else if (Array.isArray(value)) {
531
+ values = value;
532
+ } else {
533
+ values = [value];
534
+ }
488
535
  }
536
+ // getAttribute has AST built in and resolves a wide range of operators.
489
537
 
490
538
  let oldValue = "";
491
539
  let newValue = "";
492
540
 
493
541
  switch (targetName) {
542
+ case 'type':
543
+ oldValue = targetElement.getAttribute('type') || '';
544
+ newValue = this.__getNextValue(values, oldValue, deactivate);
545
+ targetElement.setAttribute('type', newValue);
546
+ break;
547
+
494
548
  case 'style':
495
549
  if (targetElement.getAttribute("style")) {
496
550
  // Normalize style string for comparison
497
551
  const styles = targetElement.getAttribute("style")
498
552
  .split(";")
499
553
  .map(x => x.trim().replace(/\s/g, ""));
500
-
554
+
501
555
  oldValue = values.find(x => styles.includes(x.replace(/\s/g, ""))) || "";
502
556
  }
503
557
  newValue = this.__getNextValue(values, oldValue, deactivate);
@@ -527,7 +581,7 @@ const CoCreateEvents = {
527
581
 
528
582
  if (oldValue) {
529
583
  oldValue.split(" ").forEach(c => targetElement.classList.remove(c));
530
- if (values.length === 1 && !newValue) return;
584
+ if (values.length === 1 && !newValue) return;
531
585
  }
532
586
 
533
587
  if (newValue) {
@@ -535,11 +589,11 @@ const CoCreateEvents = {
535
589
  }
536
590
  break;
537
591
 
538
- case 'insertadjacenthtml':
539
- case 'insertadjacenttext':
540
- case 'insertAdjacentHTML':
541
- case 'insertAdjacentText':
542
- const position = sourceElement.getAttribute(name);;
592
+ case 'insertadjacenthtml':
593
+ case 'insertadjacenttext':
594
+ case 'insertAdjacentHTML':
595
+ case 'insertAdjacentText':
596
+ const position = sourceElement.getAttribute(name);;
543
597
  if (!position) continue;
544
598
 
545
599
  let content = "";
@@ -564,10 +618,34 @@ const CoCreateEvents = {
564
618
  } else {
565
619
  targetElement.insertAdjacentText(position, content);
566
620
  }
567
- }
568
- break;
569
-
570
- case 'value':
621
+ }
622
+ break;
623
+
624
+ case 'insertadjacentelement':
625
+ case 'insertAdjacentElement':
626
+ const elementPosition = sourceElement.getAttribute(name);
627
+ const elementSelector =
628
+ sourceElement.attributes.getNamedItem(`${prefix}-element`)?.value;
629
+ if (!elementPosition || !elementSelector) continue;
630
+
631
+ const elementsToInsert = queryElements({
632
+ element: sourceElement,
633
+ prefix,
634
+ selector: elementSelector
635
+ });
636
+ const elementToInsert = elementsToInsert[0];
637
+ if (
638
+ elementToInsert &&
639
+ typeof targetElement.insertAdjacentElement === 'function'
640
+ ) {
641
+ targetElement.insertAdjacentElement(
642
+ elementPosition,
643
+ elementToInsert
644
+ );
645
+ }
646
+ break;
647
+
648
+ case 'value':
571
649
  if (targetElement.getValue) {
572
650
  oldValue = await targetElement.getValue();
573
651
  } else {
@@ -586,25 +664,35 @@ const CoCreateEvents = {
586
664
 
587
665
  default:
588
666
  // Generic handling: Try Property first, then Attribute
589
-
667
+
590
668
  // 1. Try Property
591
- if (targetElement[targetName] !== undefined) {
669
+ if (targetElement[targetName] !== undefined || explicitMethod) {
670
+ if (explicitMethod)
671
+ targetName = targetName.substring(1)
592
672
  oldValue = targetElement[targetName];
593
673
  if (!values.includes(oldValue) && typeof oldValue !== 'function') oldValue = "";
594
-
595
- newValue = this.__getNextValue(values, oldValue, deactivate);
596
674
 
597
- if (typeof targetElement[targetName] === 'function') {
598
- targetElement[targetName](newValue);
599
- } else {
675
+ newValue = this.__getNextValue(values, oldValue, deactivate);
676
+
677
+ if (typeof targetElement[targetName] === 'function') {
678
+ // A non-$ method binding with value=false disables
679
+ // the action. Explicit $method bindings may still
680
+ // intentionally pass false as an argument.
681
+ if (
682
+ explicitMethod ||
683
+ (newValue !== false && newValue !== "false")
684
+ ) {
685
+ targetElement[targetName](newValue);
686
+ }
687
+ } else {
600
688
  targetElement[targetName] = newValue;
601
689
  }
602
- }
690
+ }
603
691
  // 2. Fallback to Attribute
604
692
  else {
605
693
  oldValue = targetElement.getAttribute(targetName);
606
694
  if (!values.includes(oldValue)) oldValue = "";
607
-
695
+
608
696
  newValue = this.__getNextValue(values, oldValue, deactivate);
609
697
 
610
698
  // Handle Namespaced Attributes (e.g. svg:href) if encoded in name
@@ -629,4 +717,4 @@ const CoCreateEvents = {
629
717
 
630
718
  CoCreateEvents.init();
631
719
 
632
- export default CoCreateEvents;
720
+ export default CoCreateEvents;
@@ -0,0 +1,135 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const vm = require("node:vm");
5
+ const test = require("node:test");
6
+
7
+ const code = fs.readFileSync(path.join(__dirname, "../src/index.js"), "utf8")
8
+ .replace(/^import .*;\r?\n/gm, "")
9
+ .replace("CoCreateEvents.init();", "")
10
+ .replace("export default CoCreateEvents;", "globalThis.CoCreateEvents = CoCreateEvents;");
11
+
12
+ const context = {
13
+ action: {},
14
+ observer: {},
15
+ queryElements: () => [],
16
+ getAttribute: () => undefined
17
+ };
18
+ vm.runInNewContext(code, context);
19
+
20
+ test("$undefined is passed to a method as undefined", async () => {
21
+ let args;
22
+ const source = {
23
+ attributes: [{ name: "focus.$focus", value: "$undefined" }],
24
+ getAttribute: () => undefined
25
+ };
26
+ const target = {
27
+ focus(...received) {
28
+ args = received;
29
+ }
30
+ };
31
+
32
+ await context.CoCreateEvents.setValue(source, source, target, "focus");
33
+ assert.deepEqual(args, [undefined]);
34
+ });
35
+
36
+ test("an explicit method attribute preserves a provided argument", async () => {
37
+ let args;
38
+ const source = {
39
+ attributes: [{ name: "click.$method", value: "value" }],
40
+ getAttribute: () => "value"
41
+ };
42
+ const target = {
43
+ method(...received) {
44
+ args = received;
45
+ }
46
+ };
47
+
48
+ await context.CoCreateEvents.setValue(source, source, target, "click");
49
+ assert.deepEqual(args, ["value"]);
50
+ });
51
+
52
+ test("insertAdjacentElement moves the queried element without reading its value", async () => {
53
+ const movedElement = { id: "preview" };
54
+ let query;
55
+ let inserted;
56
+ const attributes = [
57
+ { name: "click.insertAdjacentElement", value: "beforeend" },
58
+ { name: "click-element", value: "$document #preview" }
59
+ ];
60
+ attributes.getNamedItem = name => attributes.find(attribute => attribute.name === name);
61
+ context.queryElements = options => {
62
+ query = options;
63
+ return [movedElement];
64
+ };
65
+ const source = {
66
+ attributes,
67
+ getAttribute(name) {
68
+ if (name === "click.insertAdjacentElement") return "beforeend";
69
+ if (name === "click-element") return "$document #preview";
70
+ return null;
71
+ },
72
+ getValue() {
73
+ throw new Error("insertAdjacentElement must use the element itself");
74
+ }
75
+ };
76
+ const target = {
77
+ insertAdjacentElement(position, element) {
78
+ inserted = { position, element };
79
+ }
80
+ };
81
+
82
+ await context.CoCreateEvents.setValue(source, source, target, "click");
83
+
84
+ assert.equal(query.prefix, "click");
85
+ assert.equal(query.selector, "$document #preview");
86
+ assert.deepEqual(inserted, { position: "beforeend", element: movedElement });
87
+ });
88
+
89
+ test("an empty method attribute preserves an empty-string argument", async () => {
90
+ let args;
91
+ const source = {
92
+ attributes: [{ name: "click.$method", value: "" }],
93
+ getAttribute: () => ""
94
+ };
95
+ const target = {
96
+ method(...received) {
97
+ args = received;
98
+ }
99
+ };
100
+
101
+ await context.CoCreateEvents.setValue(source, source, target, "click");
102
+ assert.deepEqual(args, [""]);
103
+ });
104
+
105
+ test("a false-valued implicit method binding does not invoke the method", async () => {
106
+ let calls = 0;
107
+ const source = {
108
+ attributes: [{ name: "input.save", value: "false" }],
109
+ getAttribute: () => false
110
+ };
111
+ const target = {
112
+ save() {
113
+ calls++;
114
+ }
115
+ };
116
+
117
+ await context.CoCreateEvents.setValue(source, source, target, "input");
118
+ assert.equal(calls, 0);
119
+ });
120
+
121
+ test("an explicit $method binding can pass false intentionally", async () => {
122
+ let args;
123
+ const source = {
124
+ attributes: [{ name: "input.$method", value: "false" }],
125
+ getAttribute: () => false
126
+ };
127
+ const target = {
128
+ method(...received) {
129
+ args = received;
130
+ }
131
+ };
132
+
133
+ await context.CoCreateEvents.setValue(source, source, target, "input");
134
+ assert.deepEqual(args, [false]);
135
+ });