@w-lfpup/superaction 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ name: Build and Test
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ branches: ["main"]
8
+
9
+ jobs:
10
+ build_and_test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v5
14
+ - uses: actions/setup-node@v4
15
+ - name: Install
16
+ run: npm ci
@@ -0,0 +1,5 @@
1
+ dist/
2
+
3
+ *.js
4
+ *.html
5
+ package-lock.json
package/.prettierrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "trailingComma": "all",
3
+ "useTabs": true,
4
+ "tabWidth": 4
5
+ }
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Taylor Vann
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # SuperAction-js
2
+
3
+ A hypertext extension to dispatch meaningful actions from the DOM.
4
+
5
+ ## Install
6
+
7
+ Install via npm.
8
+
9
+ ```sh
10
+ npm install --save-dev @w-lfpup/superaction
11
+ ```
12
+
13
+ Or install directly from github.
14
+
15
+ ```sh
16
+ npm install --save-dev https://github.com/w-lfpup/superaction-js
17
+ ```
18
+
19
+ ## Setup
20
+
21
+ Create a `SuperAction` instance dispatch action events.
22
+
23
+ The `SuperAction` instance below listens for `click` events. Event listeners are immediately `connected` to the `document`.
24
+
25
+ This enables the DOM to declaratively send meaningful messages to Javascript-land.
26
+
27
+ ```js
28
+ import { SuperAction } from "superaction";
29
+
30
+ const _superAction = new SuperAction({
31
+ host: document,
32
+ connected: true,
33
+ eventNames: ["click"],
34
+ });
35
+ ```
36
+
37
+ ## Declare
38
+
39
+ Add an attribute with the pattern `event:=action`. The `#action` event will dispatch from the `host` element
40
+
41
+ ```html
42
+ <button click:="increment">+</button>
43
+ ```
44
+
45
+ ## Listen
46
+
47
+ Now the `button` will dispatch an `ActionEvent` from the `host` when clicked.
48
+
49
+ Add an event listener to connect action events from the UI to javascript-land.
50
+
51
+ ```js
52
+ document.addEventListener("#action", (e) => {
53
+ let { action, sourceEvent, formData } = e.actionParams;
54
+
55
+ if ("increment" === action) {
56
+ // increment something!
57
+ }
58
+ });
59
+ ```
60
+
61
+ Form data is available when action events originate from form elements.
62
+
63
+ Learn more about action events [here](./action_events.md).
64
+
65
+ ## Typescript
66
+
67
+ I'm not trying to pollute your globals so if you want typed `#action` events, please add the following to your app somewhere thoughtful.
68
+
69
+ ```ts
70
+ import type { ActionEventInterface } from "superaction";
71
+
72
+ declare global {
73
+ interface GlobalEventHandlersEventMap {
74
+ ["#action"]: ActionEventInterface;
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Examples
80
+
81
+ Here are some examples to demonstrate how easy it is to work with `SuperAction-js`:
82
+
83
+ - a simple [counter](https://w-lfpup.github.io/superaction-js/examples/counter/)
84
+ - a small [sketchpad](https://w-lfpup.github.io/superaction-js/examples/sketch/) using an offscreen canvas
85
+
86
+ ## Why do this?
87
+
88
+ `Superaction` is inspired by the [elm](https://elm-lang.org) project.
89
+
90
+ It turns HTML into a declarative and _explicit_ message generator and removes several layers of indirection between UI and app state.
91
+
92
+ `Superaction` is a straightforward way to work with vanilla web technologies and escape the JSX rabbithole.
93
+
94
+ ## License
95
+
96
+ `SuperAction-js` is released under the BSD-3 Clause License.
@@ -0,0 +1,76 @@
1
+ # Action Events
2
+
3
+ ## Event stacking
4
+
5
+ `Superaction-js` listens to any DOM event that bubbles. It also dispatches all actions found along the composed path of a DOM event.
6
+
7
+ Turns out that's [all UI Events](https://www.w3.org/TR/uievents/#events-uievents). Which is a lot of events!
8
+
9
+ Consider the following example:
10
+
11
+ ```html
12
+ <body click:="A">
13
+ <div click:="B">
14
+ <button click:="C">hai :3</button>
15
+ </div>
16
+ </body>
17
+ ```
18
+
19
+ When a person clicks the button above, the order of action events is:
20
+
21
+ - Action "C"
22
+ - Action "B"
23
+ - Action "A"
24
+
25
+ ## Propagation
26
+
27
+ Action events propagate similar to DOM events. Their declarative API reflects their DOM Event counterpart:
28
+
29
+ - `event:prevent-default`
30
+ - `event:stop-propagation`
31
+ - `event:stop-immediate-propagation`
32
+
33
+ Consider the following example:
34
+
35
+ ```html
36
+ <body
37
+ click:="A"
38
+ click:stop-immediate-propagation>
39
+ <form
40
+ click:="B"
41
+ click:prevent-default>
42
+ <button
43
+ type=submit
44
+ click:="C">
45
+ UwU
46
+ </button>
47
+ <button
48
+ type=submit
49
+ click:="D"
50
+ click:stop-propagation>
51
+ ^_^
52
+ </button>
53
+ </form>
54
+ </body>
55
+ ```
56
+
57
+ So when a person clicks the buttons above, the order of actions is:
58
+
59
+ Click button C:
60
+
61
+ - Action "C" dispatched
62
+ - `preventDefault()` is called on the original `HTMLSubmitEvent`
63
+ - Action "B" dispatched
64
+ - Action propagation is stopped similar to `event.stopImmediatePropagation()`
65
+ - Action "A" does _not_ dispatch
66
+
67
+ Click button D:
68
+
69
+ - Action "D" dispatched
70
+ - Action event propagation stopped similar to `event.stopPropagation()`
71
+
72
+ ## Why #action ?
73
+
74
+ The `#action` event name, specifically the `#`, is used to prevent cyclical event disptaches.
75
+
76
+ We can't _dynamically_ add attribtues to elements that start with `#`. And in this way, some of the infinite loop risk is mitigated.
package/dist/mod.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface ActionInterface {
2
+ action: string;
3
+ formData?: FormData;
4
+ sourceEvent: Event;
5
+ }
6
+ export interface ActionEventInterface extends Event {
7
+ actionParams: ActionInterface;
8
+ }
9
+ export interface SuperActionParamsInterface {
10
+ connected?: boolean;
11
+ eventNames: string[];
12
+ host: EventTarget;
13
+ target?: EventTarget;
14
+ }
15
+ export interface SuperActionInterface {
16
+ connect(): void;
17
+ disconnect(): void;
18
+ }
19
+ export declare class ActionEvent extends Event implements ActionEventInterface {
20
+ actionParams: ActionInterface;
21
+ constructor(actionParams: ActionInterface, eventInit?: EventInit);
22
+ }
23
+ export declare class SuperAction implements SuperActionInterface {
24
+ #private;
25
+ constructor(params: SuperActionParamsInterface);
26
+ connect(): void;
27
+ disconnect(): void;
28
+ }
package/dist/mod.js ADDED
@@ -0,0 +1,59 @@
1
+ export class ActionEvent extends Event {
2
+ actionParams;
3
+ constructor(actionParams, eventInit) {
4
+ super("#action", eventInit);
5
+ this.actionParams = actionParams;
6
+ }
7
+ }
8
+ export class SuperAction {
9
+ #connected = false;
10
+ #boundDispatch;
11
+ #params;
12
+ #target;
13
+ constructor(params) {
14
+ this.#params = { ...params };
15
+ this.#target = params.target ?? params.host;
16
+ this.#boundDispatch = this.#dispatch.bind(this);
17
+ if (this.#params.connected)
18
+ this.connect();
19
+ }
20
+ connect() {
21
+ if (this.#connected)
22
+ return;
23
+ this.#connected = true;
24
+ let { host, eventNames } = this.#params;
25
+ for (let name of eventNames) {
26
+ host.addEventListener(name, this.#boundDispatch);
27
+ }
28
+ }
29
+ disconnect() {
30
+ let { host, eventNames } = this.#params;
31
+ for (let name of eventNames) {
32
+ host.removeEventListener(name, this.#boundDispatch);
33
+ }
34
+ }
35
+ #dispatch(sourceEvent) {
36
+ let { type, currentTarget, target } = sourceEvent;
37
+ if (!currentTarget)
38
+ return;
39
+ let formData;
40
+ if (target instanceof HTMLFormElement)
41
+ formData = new FormData(target);
42
+ for (let node of sourceEvent.composedPath()) {
43
+ if (node instanceof Element) {
44
+ if (node.hasAttribute(`${type}:prevent-default`))
45
+ sourceEvent.preventDefault();
46
+ if (node.hasAttribute(`${type}:stop-immediate-propagation`))
47
+ return;
48
+ let action = node.getAttribute(`${type}:`);
49
+ if (action) {
50
+ let composed = node.hasAttribute(`${type}:composed`);
51
+ let event = new ActionEvent({ action, sourceEvent, formData }, { bubbles: true, composed });
52
+ this.#target.dispatchEvent(event);
53
+ }
54
+ if (node.hasAttribute(`${type}:stop-propagation`))
55
+ return;
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <script type="importmap">
7
+ {
8
+ "imports": {
9
+ "superaction": "../../dist/mod.js"
10
+ }
11
+ }
12
+ </script>
13
+ <script type="module" src="./mod.js"></script>
14
+ </head>
15
+ <body>
16
+ <button click:="decrement">-</button>
17
+ <span count>42</span>
18
+ <button click:="increment">+</button>
19
+ </body>
20
+ </html>
@@ -0,0 +1,18 @@
1
+ import { SuperAction } from "superaction";
2
+ const _superAction = new SuperAction({
3
+ host: document,
4
+ connected: true,
5
+ eventNames: ["click"],
6
+ });
7
+ const countEl = document.querySelector("[count]");
8
+ let count = parseFloat(countEl.textContent ?? "");
9
+ addEventListener("#action", function (e) {
10
+ let { action } = e.actionParams;
11
+ if ("increment" === action) {
12
+ count += 1;
13
+ }
14
+ if ("decrement" === action) {
15
+ count -= 1;
16
+ }
17
+ countEl.textContent = count.toString();
18
+ });
@@ -0,0 +1,32 @@
1
+ import type { ActionEventInterface } from "superaction";
2
+
3
+ import { SuperAction } from "superaction";
4
+
5
+ declare global {
6
+ interface GlobalEventHandlersEventMap {
7
+ ["#action"]: ActionEventInterface;
8
+ }
9
+ }
10
+
11
+ const _superAction = new SuperAction({
12
+ host: document,
13
+ connected: true,
14
+ eventNames: ["click"],
15
+ });
16
+
17
+ const countEl = document.querySelector("[count]")!;
18
+ let count = parseFloat(countEl.textContent ?? "");
19
+
20
+ addEventListener("#action", function (e) {
21
+ let { action } = e.actionParams;
22
+
23
+ if ("increment" === action) {
24
+ count += 1;
25
+ }
26
+
27
+ if ("decrement" === action) {
28
+ count -= 1;
29
+ }
30
+
31
+ countEl.textContent = count.toString();
32
+ });
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ </head>
7
+ <body>
8
+ <header></header>
9
+ <main>
10
+ <p>A <a href="./counter/index.html">counter</a> example.</p>
11
+ <p>A <a href="./sketch/index.html">canvas</a> example.</p>
12
+ </main>
13
+ <footer></footer>
14
+ </body>
15
+ </html>
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ export interface CanvasParams {
2
+ top: number;
3
+ left: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ interface PenParams {
9
+ x: number;
10
+ y: number;
11
+ movementX: number;
12
+ movementY: number;
13
+ }
14
+
15
+ interface SetupCanvas {
16
+ action: "setup_canvas";
17
+ offscreenCanvas: OffscreenCanvas;
18
+ }
19
+
20
+ interface SetCanvasParams {
21
+ action: "set_canvas_params";
22
+ params: CanvasParams;
23
+ }
24
+
25
+ interface SetColor {
26
+ action: "set_color";
27
+ color: string;
28
+ }
29
+
30
+ interface MovePen {
31
+ action: "move_pen";
32
+ params: PenParams;
33
+ }
34
+
35
+ interface PressPen {
36
+ action: "press_pen";
37
+ params: PenParams;
38
+ }
39
+
40
+ interface LiftPen {
41
+ action: "lift_pen";
42
+ params: PenParams;
43
+ }
44
+
45
+ export type Actions =
46
+ | SetupCanvas
47
+ | SetCanvasParams
48
+ | SetColor
49
+ | MovePen
50
+ | PressPen
51
+ | LiftPen;
@@ -0,0 +1,49 @@
1
+ <!doctype html>
2
+ <html pointerup:="lift_pen" pointerdown:="press_pen" pointermove:="move_pen">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <script type="importmap">
7
+ {
8
+ "imports": {
9
+ "superaction": "../../dist/mod.js"
10
+ }
11
+ }
12
+ </script>
13
+ <script type="module" src="./mod.js"></script>
14
+ <style>
15
+ :root {
16
+ background-color: #d3d3d3;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ display: grid;
22
+ grid-template-columns: 1fr min(80dvw, 80dvh) 1fr;
23
+ grid-template-rows: 1fr min(80dvw, 80dvh) 1fr;
24
+ block-size: 100dvh;
25
+ }
26
+
27
+ input {
28
+ grid-column-start: 1;
29
+ grid-row-start: 2;
30
+ align-self: flex-start;
31
+ justify-self: end;
32
+ }
33
+
34
+ canvas {
35
+ background-color: white;
36
+ grid-column-start: 2;
37
+ grid-row-start: 2;
38
+ block-size: 100%;
39
+ inline-size: 100%;
40
+ }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <menu>
45
+ <input type="color" input:="set_color" pointerdown:stop-propagation />
46
+ </menu>
47
+ <canvas></canvas>
48
+ </body>
49
+ </html>
@@ -0,0 +1,50 @@
1
+ import { SuperAction } from "superaction";
2
+ const _superAction = new SuperAction({
3
+ host: document,
4
+ connected: true,
5
+ eventNames: ["input", "pointerdown", "pointerup", "pointermove"],
6
+ });
7
+ // Setup workers
8
+ const worker = new Worker("worker.js", { type: "module" });
9
+ const canvas = document.querySelector("canvas");
10
+ const offscreenCanvas = canvas.transferControlToOffscreen();
11
+ const resizeObserver = new ResizeObserver(sendCanvasParams);
12
+ resizeObserver.observe(canvas);
13
+ // Add reactions
14
+ addEventListener("#action", function (e) {
15
+ let { action, sourceEvent } = e.actionParams;
16
+ // send actions to the offscreen canvas worker
17
+ // set color action needs input value
18
+ if ("set_color" === action &&
19
+ sourceEvent.target instanceof HTMLInputElement) {
20
+ worker.postMessage({
21
+ action,
22
+ color: sourceEvent.target.value,
23
+ });
24
+ }
25
+ // other pointer actions
26
+ if (sourceEvent instanceof PointerEvent) {
27
+ let { x, y, movementX, movementY } = sourceEvent;
28
+ worker.postMessage({
29
+ action,
30
+ params: { x, y, movementX, movementY },
31
+ });
32
+ }
33
+ });
34
+ // Initialize offscreen canvas
35
+ function setupCanvas() {
36
+ worker.postMessage({
37
+ action: "setup_canvas",
38
+ offscreenCanvas,
39
+ }, [offscreenCanvas]);
40
+ }
41
+ function sendCanvasParams() {
42
+ let { top, left } = canvas.getBoundingClientRect();
43
+ let { clientWidth, clientHeight } = canvas;
44
+ worker.postMessage({
45
+ action: "set_canvas_params",
46
+ params: { top, left, width: clientWidth, height: clientHeight },
47
+ });
48
+ }
49
+ setupCanvas();
50
+ sendCanvasParams();
@@ -0,0 +1,74 @@
1
+ import { SuperAction, ActionEventInterface } from "superaction";
2
+
3
+ // Setup SuperAction
4
+ declare global {
5
+ interface GlobalEventHandlersEventMap {
6
+ ["#action"]: ActionEventInterface;
7
+ }
8
+ }
9
+
10
+ const _superAction = new SuperAction({
11
+ host: document,
12
+ connected: true,
13
+ eventNames: ["input", "pointerdown", "pointerup", "pointermove"],
14
+ });
15
+
16
+ // Setup workers
17
+ const worker = new Worker("worker.js", { type: "module" });
18
+ const canvas = document.querySelector("canvas")!;
19
+ const offscreenCanvas = canvas.transferControlToOffscreen();
20
+
21
+ const resizeObserver = new ResizeObserver(sendCanvasParams);
22
+ resizeObserver.observe(canvas);
23
+
24
+ // Add reactions
25
+ addEventListener("#action", function (e: ActionEventInterface) {
26
+ let { action, sourceEvent } = e.actionParams;
27
+
28
+ // send actions to the offscreen canvas worker
29
+
30
+ // set color action needs input value
31
+ if (
32
+ "set_color" === action &&
33
+ sourceEvent.target instanceof HTMLInputElement
34
+ ) {
35
+ worker.postMessage({
36
+ action,
37
+ color: sourceEvent.target.value,
38
+ });
39
+ }
40
+
41
+ // other pointer actions
42
+ if (sourceEvent instanceof PointerEvent) {
43
+ let { x, y, movementX, movementY } = sourceEvent;
44
+
45
+ worker.postMessage({
46
+ action,
47
+ params: { x, y, movementX, movementY },
48
+ });
49
+ }
50
+ });
51
+
52
+ // Initialize offscreen canvas
53
+ function setupCanvas() {
54
+ worker.postMessage(
55
+ {
56
+ action: "setup_canvas",
57
+ offscreenCanvas,
58
+ },
59
+ [offscreenCanvas],
60
+ );
61
+ }
62
+
63
+ function sendCanvasParams() {
64
+ let { top, left } = canvas.getBoundingClientRect();
65
+ let { clientWidth, clientHeight } = canvas;
66
+
67
+ worker.postMessage({
68
+ action: "set_canvas_params",
69
+ params: { top, left, width: clientWidth, height: clientHeight },
70
+ });
71
+ }
72
+
73
+ setupCanvas();
74
+ sendCanvasParams();
@@ -0,0 +1,59 @@
1
+ let canvas;
2
+ let ctx;
3
+ let pen_to_paper = false;
4
+ let canvasParams;
5
+ self.addEventListener("message", function (e) {
6
+ let { data } = e;
7
+ if ("setup_canvas" === data.action) {
8
+ canvas = data.offscreenCanvas;
9
+ ctx = canvas.getContext("2d");
10
+ }
11
+ if ("set_canvas_params" === data.action) {
12
+ canvas.width = data.params.width;
13
+ canvas.height = data.params.height;
14
+ canvasParams = data.params;
15
+ if (ctx) {
16
+ ctx.lineWidth = 10;
17
+ ctx.lineCap = "round";
18
+ }
19
+ }
20
+ if ("set_color" === data.action) {
21
+ let { color } = data;
22
+ if (ctx) {
23
+ ctx.strokeStyle = color;
24
+ ctx.fillStyle = color;
25
+ }
26
+ }
27
+ if ("press_pen" === data.action) {
28
+ pen_to_paper = true;
29
+ if (ctx) {
30
+ // create first point
31
+ ctx.beginPath();
32
+ let { top, left } = canvasParams;
33
+ let { x, y } = data.params;
34
+ let dx = x - left;
35
+ let dy = y - top;
36
+ ctx.arc(dx, dy, ctx.lineWidth * 0.5, 0, 2 * Math.PI, true);
37
+ ctx.fill();
38
+ ctx.closePath();
39
+ // start a "line"
40
+ ctx.beginPath();
41
+ }
42
+ }
43
+ if ("move_pen" === data.action) {
44
+ if (ctx && pen_to_paper) {
45
+ let { top, left } = canvasParams;
46
+ let { movementY, movementX, x, y } = data.params;
47
+ let dx = x - left;
48
+ let dy = y - top;
49
+ ctx.moveTo(dx - movementX, dy - movementY);
50
+ ctx.lineTo(dx, dy);
51
+ ctx.stroke();
52
+ }
53
+ }
54
+ if ("lift_pen" === data.action) {
55
+ pen_to_paper = false;
56
+ ctx?.closePath();
57
+ }
58
+ });
59
+ export {};
@@ -0,0 +1,72 @@
1
+ import type { Actions, CanvasParams } from "./actions.ts";
2
+
3
+ let canvas: OffscreenCanvas;
4
+ let ctx: OffscreenCanvasRenderingContext2D | null;
5
+ let pen_to_paper = false;
6
+ let canvasParams: CanvasParams;
7
+
8
+ self.addEventListener("message", function (e: MessageEvent<Actions>) {
9
+ let { data } = e;
10
+
11
+ if ("setup_canvas" === data.action) {
12
+ canvas = data.offscreenCanvas;
13
+ ctx = canvas.getContext("2d");
14
+ }
15
+
16
+ if ("set_canvas_params" === data.action) {
17
+ canvas.width = data.params.width;
18
+ canvas.height = data.params.height;
19
+ canvasParams = data.params;
20
+ if (ctx) {
21
+ ctx.lineWidth = 10;
22
+ ctx.lineCap = "round";
23
+ }
24
+ }
25
+
26
+ if ("set_color" === data.action) {
27
+ let { color } = data;
28
+ if (ctx) {
29
+ ctx.strokeStyle = color;
30
+ ctx.fillStyle = color;
31
+ }
32
+ }
33
+
34
+ if ("press_pen" === data.action) {
35
+ pen_to_paper = true;
36
+ if (ctx) {
37
+ // create first point
38
+ ctx.beginPath();
39
+ let { top, left } = canvasParams;
40
+ let { x, y } = data.params;
41
+
42
+ let dx = x - left;
43
+ let dy = y - top;
44
+
45
+ ctx.arc(dx, dy, ctx.lineWidth * 0.5, 0, 2 * Math.PI, true);
46
+ ctx.fill();
47
+ ctx.closePath();
48
+
49
+ // start a "line"
50
+ ctx.beginPath();
51
+ }
52
+ }
53
+
54
+ if ("move_pen" === data.action) {
55
+ if (ctx && pen_to_paper) {
56
+ let { top, left } = canvasParams;
57
+ let { movementY, movementX, x, y } = data.params;
58
+
59
+ let dx = x - left;
60
+ let dy = y - top;
61
+
62
+ ctx.moveTo(dx - movementX, dy - movementY);
63
+ ctx.lineTo(dx, dy);
64
+ ctx.stroke();
65
+ }
66
+ }
67
+
68
+ if ("lift_pen" === data.action) {
69
+ pen_to_paper = false;
70
+ ctx?.closePath();
71
+ }
72
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": false,
5
+ "paths": {
6
+ "superaction": ["../dist/mod.d.ts"]
7
+ }
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@w-lfpup/superaction",
3
+ "type": "module",
4
+ "main": "dist/mod.js",
5
+ "description": "A hypertext extension to dispatch meaningful actions from the DOM",
6
+ "license": "BSD-3-Clause",
7
+ "version": "0.1.0",
8
+ "scripts": {
9
+ "prepare": "npm run build && npm run build:examples",
10
+ "build": "npx tsc --project ./src",
11
+ "build:examples": "npx tsc --project ./examples",
12
+ "format": "npx prettier ./ --write"
13
+ },
14
+ "devDependencies": {
15
+ "prettier": "^3.2.5",
16
+ "typescript": "^5.4.5"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/w-lfpup/superaction-js.git"
21
+ }
22
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,94 @@
1
+ export interface ActionInterface {
2
+ action: string;
3
+ formData?: FormData;
4
+ sourceEvent: Event;
5
+ }
6
+
7
+ export interface ActionEventInterface extends Event {
8
+ actionParams: ActionInterface;
9
+ }
10
+
11
+ export interface SuperActionParamsInterface {
12
+ connected?: boolean;
13
+ eventNames: string[];
14
+ host: EventTarget;
15
+ target?: EventTarget;
16
+ }
17
+
18
+ export interface SuperActionInterface {
19
+ connect(): void;
20
+ disconnect(): void;
21
+ }
22
+
23
+ export class ActionEvent extends Event implements ActionEventInterface {
24
+ actionParams: ActionInterface;
25
+
26
+ constructor(actionParams: ActionInterface, eventInit?: EventInit) {
27
+ super("#action", eventInit);
28
+ this.actionParams = actionParams;
29
+ }
30
+ }
31
+
32
+ export class SuperAction implements SuperActionInterface {
33
+ #connected = false;
34
+
35
+ #boundDispatch: EventListenerOrEventListenerObject;
36
+ #params: SuperActionParamsInterface;
37
+ #target: EventTarget;
38
+
39
+ constructor(params: SuperActionParamsInterface) {
40
+ this.#params = { ...params };
41
+ this.#target = params.target ?? params.host;
42
+ this.#boundDispatch = this.#dispatch.bind(this);
43
+
44
+ if (this.#params.connected) this.connect();
45
+ }
46
+
47
+ connect() {
48
+ if (this.#connected) return;
49
+ this.#connected = true;
50
+
51
+ let { host, eventNames } = this.#params;
52
+ for (let name of eventNames) {
53
+ host.addEventListener(name, this.#boundDispatch);
54
+ }
55
+ }
56
+
57
+ disconnect() {
58
+ let { host, eventNames } = this.#params;
59
+ for (let name of eventNames) {
60
+ host.removeEventListener(name, this.#boundDispatch);
61
+ }
62
+ }
63
+
64
+ #dispatch(sourceEvent: Event) {
65
+ let { type, currentTarget, target } = sourceEvent;
66
+ if (!currentTarget) return;
67
+
68
+ let formData: FormData | undefined;
69
+ if (target instanceof HTMLFormElement) formData = new FormData(target);
70
+
71
+ for (let node of sourceEvent.composedPath()) {
72
+ if (node instanceof Element) {
73
+ if (node.hasAttribute(`${type}:prevent-default`))
74
+ sourceEvent.preventDefault();
75
+
76
+ if (node.hasAttribute(`${type}:stop-immediate-propagation`))
77
+ return;
78
+
79
+ let action = node.getAttribute(`${type}:`);
80
+ if (action) {
81
+ let composed = node.hasAttribute(`${type}:composed`);
82
+ let event = new ActionEvent(
83
+ { action, sourceEvent, formData },
84
+ { bubbles: true, composed },
85
+ );
86
+
87
+ this.#target.dispatchEvent(event);
88
+ }
89
+
90
+ if (node.hasAttribute(`${type}:stop-propagation`)) return;
91
+ }
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./",
5
+ "outDir": "../dist"
6
+ }
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["DOM", "ESNext"],
4
+ "declaration": true,
5
+ "strict": true,
6
+ "module": "esnext",
7
+ "target": "esnext"
8
+ }
9
+ }