@w-lfpup/superaction 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/{build.yml → builds.yml} +2 -2
- package/README.md +70 -23
- package/dist/mod.d.ts +11 -5
- package/dist/mod.js +14 -12
- package/examples/counter/mod.js +3 -3
- package/examples/counter/mod.ts +3 -3
- package/examples/sketch/actions.ts +6 -6
- package/examples/sketch/mod.js +10 -16
- package/examples/sketch/mod.ts +11 -13
- package/examples/sketch/worker.js +6 -8
- package/examples/sketch/worker.ts +6 -6
- package/examples/tsconfig.json +1 -0
- package/package.json +3 -3
- package/src/mod.ts +24 -15
- package/action_events.md +0 -76
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: Builds
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
@@ -10,7 +10,7 @@ jobs:
|
|
|
10
10
|
build_and_test:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
14
|
- uses: actions/setup-node@v4
|
|
15
15
|
- name: Install
|
|
16
16
|
run: npm ci
|
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# SuperAction-js
|
|
2
2
|
|
|
3
|
-
A hypertext extension to dispatch meaningful actions from
|
|
3
|
+
A hypertext extension to dispatch meaningful actions from HTML.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/w-lfpup/superaction-js/actions/workflows/builds.yml)
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
Install via npm.
|
|
10
|
+
,
|
|
8
11
|
|
|
9
12
|
```sh
|
|
10
13
|
npm install --save-dev @w-lfpup/superaction
|
|
@@ -18,12 +21,10 @@ npm install --save-dev https://github.com/w-lfpup/superaction-js
|
|
|
18
21
|
|
|
19
22
|
## Setup
|
|
20
23
|
|
|
21
|
-
Create a `SuperAction` instance dispatch action events.
|
|
24
|
+
Create a `SuperAction` instance to dispatch action events.
|
|
22
25
|
|
|
23
26
|
The `SuperAction` instance below listens for `click` events. Event listeners are immediately `connected` to the `document`.
|
|
24
27
|
|
|
25
|
-
This enables the DOM to declaratively send meaningful messages to Javascript-land.
|
|
26
|
-
|
|
27
28
|
```js
|
|
28
29
|
import { SuperAction } from "superaction";
|
|
29
30
|
|
|
@@ -34,6 +35,8 @@ const _superAction = new SuperAction({
|
|
|
34
35
|
});
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
Now the DOM can declarativey dispatch meaningful messages from HTML to Javascript-land.
|
|
39
|
+
|
|
37
40
|
## Declare
|
|
38
41
|
|
|
39
42
|
Add an attribute with the pattern `event:=action`. The `#action` event will dispatch from the `host` element
|
|
@@ -50,7 +53,7 @@ Add an event listener to connect action events from the UI to javascript-land.
|
|
|
50
53
|
|
|
51
54
|
```js
|
|
52
55
|
document.addEventListener("#action", (e) => {
|
|
53
|
-
let {
|
|
56
|
+
let { kind, originElement, originEvent, formData } = e.action;
|
|
54
57
|
|
|
55
58
|
if ("increment" === action) {
|
|
56
59
|
// increment something!
|
|
@@ -60,36 +63,80 @@ document.addEventListener("#action", (e) => {
|
|
|
60
63
|
|
|
61
64
|
Form data is available when action events originate from form elements.
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
## Event stacking
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
`Superaction-js` listens to any DOM event that bubbles. It also dispatches all actions found along the composed path of a DOM event.
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
Turns out that's [all UI Events](https://www.w3.org/TR/uievents/#events-uievents). Which is a lot of events!
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
import type { ActionEventInterface } from "superaction";
|
|
72
|
+
Consider the following example:
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
```html
|
|
75
|
+
<body click:="A">
|
|
76
|
+
<div click:="B">
|
|
77
|
+
<button click:="C">hai :3</button>
|
|
78
|
+
</div>
|
|
79
|
+
</body>
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
When a person clicks the button above, the order of action events is:
|
|
83
|
+
|
|
84
|
+
- Action "C"
|
|
85
|
+
- Action "B"
|
|
86
|
+
- Action "A"
|
|
87
|
+
|
|
88
|
+
## Propagation
|
|
89
|
+
|
|
90
|
+
Action events propagate similar to DOM events. Their declarative API reflects their DOM Event counterpart:
|
|
91
|
+
|
|
92
|
+
- `event:prevent-default`
|
|
93
|
+
- `event:stop-propagation`
|
|
94
|
+
- `event:stop-immediate-propagation`
|
|
95
|
+
|
|
96
|
+
Consider the following example:
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<body
|
|
100
|
+
click:="A"
|
|
101
|
+
click:stop-immediate-propagation>
|
|
102
|
+
<form
|
|
103
|
+
click:="B"
|
|
104
|
+
click:prevent-default>
|
|
105
|
+
<button
|
|
106
|
+
type=submit
|
|
107
|
+
click:="C">
|
|
108
|
+
UwU
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
type=submit
|
|
112
|
+
click:="D"
|
|
113
|
+
click:stop-propagation>
|
|
114
|
+
^_^
|
|
115
|
+
</button>
|
|
116
|
+
</form>
|
|
117
|
+
</body>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
So when a person clicks the buttons above, the order of actions is:
|
|
121
|
+
|
|
122
|
+
Click button C:
|
|
80
123
|
|
|
81
|
-
|
|
124
|
+
- Action "C" dispatched
|
|
125
|
+
- `preventDefault()` is called on the original `HTMLSubmitEvent`
|
|
126
|
+
- Action "B" dispatched
|
|
127
|
+
- Action propagation is stopped similar to `event.stopImmediatePropagation()`
|
|
128
|
+
- Action "A" does _not_ dispatch
|
|
82
129
|
|
|
83
|
-
|
|
84
|
-
- a small [sketchpad](https://w-lfpup.github.io/superaction-js/examples/sketch/) using an offscreen canvas
|
|
130
|
+
Click button D:
|
|
85
131
|
|
|
86
|
-
|
|
132
|
+
- Action "D" dispatched
|
|
133
|
+
- Action event propagation stopped similar to `event.stopPropagation()`
|
|
87
134
|
|
|
88
|
-
|
|
135
|
+
## Why #action ?
|
|
89
136
|
|
|
90
|
-
|
|
137
|
+
The `#action` event name, specifically the `#`, is used to prevent cyclical event disptaches.
|
|
91
138
|
|
|
92
|
-
|
|
139
|
+
We can't _dynamically_ add attribtues to elements that start with `#`. And in this way, some of the infinite loop risk is mitigated.
|
|
93
140
|
|
|
94
141
|
## License
|
|
95
142
|
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface GlobalEventHandlersEventMap {
|
|
3
|
+
["#action"]: ActionEventInterface;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
1
6
|
export interface ActionInterface {
|
|
2
|
-
|
|
7
|
+
kind: string;
|
|
3
8
|
formData?: FormData;
|
|
4
|
-
|
|
9
|
+
originElement: EventTarget;
|
|
10
|
+
originEvent: Event;
|
|
5
11
|
}
|
|
6
12
|
export interface ActionEventInterface extends Event {
|
|
7
|
-
|
|
13
|
+
action: ActionInterface;
|
|
8
14
|
}
|
|
9
15
|
export interface SuperActionParamsInterface {
|
|
10
16
|
connected?: boolean;
|
|
@@ -17,8 +23,8 @@ export interface SuperActionInterface {
|
|
|
17
23
|
disconnect(): void;
|
|
18
24
|
}
|
|
19
25
|
export declare class ActionEvent extends Event implements ActionEventInterface {
|
|
20
|
-
|
|
21
|
-
constructor(
|
|
26
|
+
action: ActionInterface;
|
|
27
|
+
constructor(action: ActionInterface, eventInit?: EventInit);
|
|
22
28
|
}
|
|
23
29
|
export declare class SuperAction implements SuperActionInterface {
|
|
24
30
|
#private;
|
package/dist/mod.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
export class ActionEvent extends Event {
|
|
2
|
-
|
|
3
|
-
constructor(
|
|
2
|
+
action;
|
|
3
|
+
constructor(action, eventInit) {
|
|
4
4
|
super("#action", eventInit);
|
|
5
|
-
this.
|
|
5
|
+
this.action = action;
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
export class SuperAction {
|
|
9
9
|
#connected = false;
|
|
10
|
-
#boundDispatch;
|
|
10
|
+
#boundDispatch = this.#dispatch.bind(this);
|
|
11
11
|
#params;
|
|
12
12
|
#target;
|
|
13
13
|
constructor(params) {
|
|
14
14
|
this.#params = { ...params };
|
|
15
15
|
this.#target = params.target ?? params.host;
|
|
16
|
-
this.#boundDispatch = this.#dispatch.bind(this);
|
|
17
16
|
if (this.#params.connected)
|
|
18
17
|
this.connect();
|
|
19
18
|
}
|
|
@@ -27,28 +26,31 @@ export class SuperAction {
|
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
disconnect() {
|
|
29
|
+
if (!this.#connected)
|
|
30
|
+
return;
|
|
31
|
+
this.#connected = false;
|
|
30
32
|
let { host, eventNames } = this.#params;
|
|
31
33
|
for (let name of eventNames) {
|
|
32
34
|
host.removeEventListener(name, this.#boundDispatch);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
|
-
#dispatch(
|
|
36
|
-
let { type, currentTarget, target } =
|
|
37
|
+
#dispatch(originEvent) {
|
|
38
|
+
let { type, currentTarget, target } = originEvent;
|
|
37
39
|
if (!currentTarget)
|
|
38
40
|
return;
|
|
39
41
|
let formData;
|
|
40
42
|
if (target instanceof HTMLFormElement)
|
|
41
43
|
formData = new FormData(target);
|
|
42
|
-
for (let node of
|
|
44
|
+
for (let node of originEvent.composedPath()) {
|
|
43
45
|
if (node instanceof Element) {
|
|
44
46
|
if (node.hasAttribute(`${type}:prevent-default`))
|
|
45
|
-
|
|
47
|
+
originEvent.preventDefault();
|
|
46
48
|
if (node.hasAttribute(`${type}:stop-immediate-propagation`))
|
|
47
49
|
return;
|
|
48
|
-
let
|
|
49
|
-
if (
|
|
50
|
+
let kind = node.getAttribute(`${type}:`);
|
|
51
|
+
if (kind) {
|
|
50
52
|
let composed = node.hasAttribute(`${type}:composed`);
|
|
51
|
-
let event = new ActionEvent({
|
|
53
|
+
let event = new ActionEvent({ kind, originElement: node, originEvent, formData }, { bubbles: true, composed });
|
|
52
54
|
this.#target.dispatchEvent(event);
|
|
53
55
|
}
|
|
54
56
|
if (node.hasAttribute(`${type}:stop-propagation`))
|
package/examples/counter/mod.js
CHANGED
|
@@ -7,11 +7,11 @@ const _superAction = new SuperAction({
|
|
|
7
7
|
const countEl = document.querySelector("[count]");
|
|
8
8
|
let count = parseFloat(countEl.textContent ?? "");
|
|
9
9
|
addEventListener("#action", function (e) {
|
|
10
|
-
let {
|
|
11
|
-
if ("increment" ===
|
|
10
|
+
let { kind } = e.action;
|
|
11
|
+
if ("increment" === kind) {
|
|
12
12
|
count += 1;
|
|
13
13
|
}
|
|
14
|
-
if ("decrement" ===
|
|
14
|
+
if ("decrement" === kind) {
|
|
15
15
|
count -= 1;
|
|
16
16
|
}
|
|
17
17
|
countEl.textContent = count.toString();
|
package/examples/counter/mod.ts
CHANGED
|
@@ -18,13 +18,13 @@ const countEl = document.querySelector("[count]")!;
|
|
|
18
18
|
let count = parseFloat(countEl.textContent ?? "");
|
|
19
19
|
|
|
20
20
|
addEventListener("#action", function (e) {
|
|
21
|
-
let {
|
|
21
|
+
let { kind } = e.action;
|
|
22
22
|
|
|
23
|
-
if ("increment" ===
|
|
23
|
+
if ("increment" === kind) {
|
|
24
24
|
count += 1;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
if ("decrement" ===
|
|
27
|
+
if ("decrement" === kind) {
|
|
28
28
|
count -= 1;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -13,32 +13,32 @@ interface PenParams {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
interface SetupCanvas {
|
|
16
|
-
|
|
16
|
+
kind: "setup_canvas";
|
|
17
17
|
offscreenCanvas: OffscreenCanvas;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface SetCanvasParams {
|
|
21
|
-
|
|
21
|
+
kind: "set_canvas_params";
|
|
22
22
|
params: CanvasParams;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
interface SetColor {
|
|
26
|
-
|
|
26
|
+
kind: "set_color";
|
|
27
27
|
color: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
interface MovePen {
|
|
31
|
-
|
|
31
|
+
kind: "move_pen";
|
|
32
32
|
params: PenParams;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
interface PressPen {
|
|
36
|
-
|
|
36
|
+
kind: "press_pen";
|
|
37
37
|
params: PenParams;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
interface LiftPen {
|
|
41
|
-
|
|
41
|
+
kind: "lift_pen";
|
|
42
42
|
params: PenParams;
|
|
43
43
|
}
|
|
44
44
|
|
package/examples/sketch/mod.js
CHANGED
|
@@ -4,37 +4,31 @@ const _superAction = new SuperAction({
|
|
|
4
4
|
connected: true,
|
|
5
5
|
eventNames: ["input", "pointerdown", "pointerup", "pointermove"],
|
|
6
6
|
});
|
|
7
|
-
// Setup workers
|
|
8
7
|
const worker = new Worker("worker.js", { type: "module" });
|
|
9
8
|
const canvas = document.querySelector("canvas");
|
|
10
9
|
const offscreenCanvas = canvas.transferControlToOffscreen();
|
|
11
10
|
const resizeObserver = new ResizeObserver(sendCanvasParams);
|
|
12
11
|
resizeObserver.observe(canvas);
|
|
13
|
-
// Add reactions
|
|
14
12
|
addEventListener("#action", function (e) {
|
|
15
|
-
let {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if ("set_color" === action &&
|
|
19
|
-
sourceEvent.target instanceof HTMLInputElement) {
|
|
13
|
+
let { kind, originEvent } = e.action;
|
|
14
|
+
if ("set_color" === kind &&
|
|
15
|
+
originEvent.target instanceof HTMLInputElement) {
|
|
20
16
|
worker.postMessage({
|
|
21
|
-
|
|
22
|
-
color:
|
|
17
|
+
kind,
|
|
18
|
+
color: originEvent.target.value,
|
|
23
19
|
});
|
|
24
20
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let { x, y, movementX, movementY } = sourceEvent;
|
|
21
|
+
if (originEvent instanceof PointerEvent) {
|
|
22
|
+
let { x, y, movementX, movementY } = originEvent;
|
|
28
23
|
worker.postMessage({
|
|
29
|
-
|
|
24
|
+
kind,
|
|
30
25
|
params: { x, y, movementX, movementY },
|
|
31
26
|
});
|
|
32
27
|
}
|
|
33
28
|
});
|
|
34
|
-
// Initialize offscreen canvas
|
|
35
29
|
function setupCanvas() {
|
|
36
30
|
worker.postMessage({
|
|
37
|
-
|
|
31
|
+
kind: "setup_canvas",
|
|
38
32
|
offscreenCanvas,
|
|
39
33
|
}, [offscreenCanvas]);
|
|
40
34
|
}
|
|
@@ -42,7 +36,7 @@ function sendCanvasParams() {
|
|
|
42
36
|
let { top, left } = canvas.getBoundingClientRect();
|
|
43
37
|
let { clientWidth, clientHeight } = canvas;
|
|
44
38
|
worker.postMessage({
|
|
45
|
-
|
|
39
|
+
kind: "set_canvas_params",
|
|
46
40
|
params: { top, left, width: clientWidth, height: clientHeight },
|
|
47
41
|
});
|
|
48
42
|
}
|
package/examples/sketch/mod.ts
CHANGED
|
@@ -21,29 +21,27 @@ const offscreenCanvas = canvas.transferControlToOffscreen();
|
|
|
21
21
|
const resizeObserver = new ResizeObserver(sendCanvasParams);
|
|
22
22
|
resizeObserver.observe(canvas);
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// send actions to the offscreen canvas worker
|
|
25
25
|
addEventListener("#action", function (e: ActionEventInterface) {
|
|
26
|
-
let {
|
|
27
|
-
|
|
28
|
-
// send actions to the offscreen canvas worker
|
|
26
|
+
let { kind, originEvent } = e.action;
|
|
29
27
|
|
|
30
28
|
// set color action needs input value
|
|
31
29
|
if (
|
|
32
|
-
"set_color" ===
|
|
33
|
-
|
|
30
|
+
"set_color" === kind &&
|
|
31
|
+
originEvent.target instanceof HTMLInputElement
|
|
34
32
|
) {
|
|
35
33
|
worker.postMessage({
|
|
36
|
-
|
|
37
|
-
color:
|
|
34
|
+
kind,
|
|
35
|
+
color: originEvent.target.value,
|
|
38
36
|
});
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
// other pointer actions
|
|
42
|
-
if (
|
|
43
|
-
let { x, y, movementX, movementY } =
|
|
40
|
+
if (originEvent instanceof PointerEvent) {
|
|
41
|
+
let { x, y, movementX, movementY } = originEvent;
|
|
44
42
|
|
|
45
43
|
worker.postMessage({
|
|
46
|
-
|
|
44
|
+
kind,
|
|
47
45
|
params: { x, y, movementX, movementY },
|
|
48
46
|
});
|
|
49
47
|
}
|
|
@@ -53,7 +51,7 @@ addEventListener("#action", function (e: ActionEventInterface) {
|
|
|
53
51
|
function setupCanvas() {
|
|
54
52
|
worker.postMessage(
|
|
55
53
|
{
|
|
56
|
-
|
|
54
|
+
kind: "setup_canvas",
|
|
57
55
|
offscreenCanvas,
|
|
58
56
|
},
|
|
59
57
|
[offscreenCanvas],
|
|
@@ -65,7 +63,7 @@ function sendCanvasParams() {
|
|
|
65
63
|
let { clientWidth, clientHeight } = canvas;
|
|
66
64
|
|
|
67
65
|
worker.postMessage({
|
|
68
|
-
|
|
66
|
+
kind: "set_canvas_params",
|
|
69
67
|
params: { top, left, width: clientWidth, height: clientHeight },
|
|
70
68
|
});
|
|
71
69
|
}
|
|
@@ -4,11 +4,11 @@ let pen_to_paper = false;
|
|
|
4
4
|
let canvasParams;
|
|
5
5
|
self.addEventListener("message", function (e) {
|
|
6
6
|
let { data } = e;
|
|
7
|
-
if ("setup_canvas" === data.
|
|
7
|
+
if ("setup_canvas" === data.kind) {
|
|
8
8
|
canvas = data.offscreenCanvas;
|
|
9
9
|
ctx = canvas.getContext("2d");
|
|
10
10
|
}
|
|
11
|
-
if ("set_canvas_params" === data.
|
|
11
|
+
if ("set_canvas_params" === data.kind) {
|
|
12
12
|
canvas.width = data.params.width;
|
|
13
13
|
canvas.height = data.params.height;
|
|
14
14
|
canvasParams = data.params;
|
|
@@ -17,17 +17,16 @@ self.addEventListener("message", function (e) {
|
|
|
17
17
|
ctx.lineCap = "round";
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
if ("set_color" === data.
|
|
20
|
+
if ("set_color" === data.kind) {
|
|
21
21
|
let { color } = data;
|
|
22
22
|
if (ctx) {
|
|
23
23
|
ctx.strokeStyle = color;
|
|
24
24
|
ctx.fillStyle = color;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
if ("press_pen" === data.
|
|
27
|
+
if ("press_pen" === data.kind) {
|
|
28
28
|
pen_to_paper = true;
|
|
29
29
|
if (ctx) {
|
|
30
|
-
// create first point
|
|
31
30
|
ctx.beginPath();
|
|
32
31
|
let { top, left } = canvasParams;
|
|
33
32
|
let { x, y } = data.params;
|
|
@@ -36,11 +35,10 @@ self.addEventListener("message", function (e) {
|
|
|
36
35
|
ctx.arc(dx, dy, ctx.lineWidth * 0.5, 0, 2 * Math.PI, true);
|
|
37
36
|
ctx.fill();
|
|
38
37
|
ctx.closePath();
|
|
39
|
-
// start a "line"
|
|
40
38
|
ctx.beginPath();
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
|
-
if ("move_pen" === data.
|
|
41
|
+
if ("move_pen" === data.kind) {
|
|
44
42
|
if (ctx && pen_to_paper) {
|
|
45
43
|
let { top, left } = canvasParams;
|
|
46
44
|
let { movementY, movementX, x, y } = data.params;
|
|
@@ -51,7 +49,7 @@ self.addEventListener("message", function (e) {
|
|
|
51
49
|
ctx.stroke();
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
|
-
if ("lift_pen" === data.
|
|
52
|
+
if ("lift_pen" === data.kind) {
|
|
55
53
|
pen_to_paper = false;
|
|
56
54
|
ctx?.closePath();
|
|
57
55
|
}
|
|
@@ -8,12 +8,12 @@ let canvasParams: CanvasParams;
|
|
|
8
8
|
self.addEventListener("message", function (e: MessageEvent<Actions>) {
|
|
9
9
|
let { data } = e;
|
|
10
10
|
|
|
11
|
-
if ("setup_canvas" === data.
|
|
11
|
+
if ("setup_canvas" === data.kind) {
|
|
12
12
|
canvas = data.offscreenCanvas;
|
|
13
13
|
ctx = canvas.getContext("2d");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
if ("set_canvas_params" === data.
|
|
16
|
+
if ("set_canvas_params" === data.kind) {
|
|
17
17
|
canvas.width = data.params.width;
|
|
18
18
|
canvas.height = data.params.height;
|
|
19
19
|
canvasParams = data.params;
|
|
@@ -23,7 +23,7 @@ self.addEventListener("message", function (e: MessageEvent<Actions>) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
if ("set_color" === data.
|
|
26
|
+
if ("set_color" === data.kind) {
|
|
27
27
|
let { color } = data;
|
|
28
28
|
if (ctx) {
|
|
29
29
|
ctx.strokeStyle = color;
|
|
@@ -31,7 +31,7 @@ self.addEventListener("message", function (e: MessageEvent<Actions>) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
if ("press_pen" === data.
|
|
34
|
+
if ("press_pen" === data.kind) {
|
|
35
35
|
pen_to_paper = true;
|
|
36
36
|
if (ctx) {
|
|
37
37
|
// create first point
|
|
@@ -51,7 +51,7 @@ self.addEventListener("message", function (e: MessageEvent<Actions>) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
if ("move_pen" === data.
|
|
54
|
+
if ("move_pen" === data.kind) {
|
|
55
55
|
if (ctx && pen_to_paper) {
|
|
56
56
|
let { top, left } = canvasParams;
|
|
57
57
|
let { movementY, movementX, x, y } = data.params;
|
|
@@ -65,7 +65,7 @@ self.addEventListener("message", function (e: MessageEvent<Actions>) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
if ("lift_pen" === data.
|
|
68
|
+
if ("lift_pen" === data.kind) {
|
|
69
69
|
pen_to_paper = false;
|
|
70
70
|
ctx?.closePath();
|
|
71
71
|
}
|
package/examples/tsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"name": "@w-lfpup/superaction",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"main": "dist/mod.js",
|
|
5
|
-
"description": "A hypertext extension to dispatch meaningful actions from
|
|
5
|
+
"description": "A hypertext extension to dispatch meaningful actions from UI events",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.2.0",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"prepare": "npm run build
|
|
9
|
+
"prepare": "npm run build",
|
|
10
10
|
"build": "npx tsc --project ./src",
|
|
11
11
|
"build:examples": "npx tsc --project ./examples",
|
|
12
12
|
"format": "npx prettier ./ --write"
|
package/src/mod.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface GlobalEventHandlersEventMap {
|
|
3
|
+
["#action"]: ActionEventInterface;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export interface ActionInterface {
|
|
2
|
-
|
|
8
|
+
kind: string;
|
|
3
9
|
formData?: FormData;
|
|
4
|
-
|
|
10
|
+
originElement: EventTarget;
|
|
11
|
+
originEvent: Event;
|
|
5
12
|
}
|
|
6
13
|
|
|
7
14
|
export interface ActionEventInterface extends Event {
|
|
8
|
-
|
|
15
|
+
action: ActionInterface;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export interface SuperActionParamsInterface {
|
|
@@ -21,25 +28,24 @@ export interface SuperActionInterface {
|
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export class ActionEvent extends Event implements ActionEventInterface {
|
|
24
|
-
|
|
31
|
+
action: ActionInterface;
|
|
25
32
|
|
|
26
|
-
constructor(
|
|
33
|
+
constructor(action: ActionInterface, eventInit?: EventInit) {
|
|
27
34
|
super("#action", eventInit);
|
|
28
|
-
this.
|
|
35
|
+
this.action = action;
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
export class SuperAction implements SuperActionInterface {
|
|
33
40
|
#connected = false;
|
|
41
|
+
#boundDispatch = this.#dispatch.bind(this);
|
|
34
42
|
|
|
35
|
-
#boundDispatch: EventListenerOrEventListenerObject;
|
|
36
43
|
#params: SuperActionParamsInterface;
|
|
37
44
|
#target: EventTarget;
|
|
38
45
|
|
|
39
46
|
constructor(params: SuperActionParamsInterface) {
|
|
40
47
|
this.#params = { ...params };
|
|
41
48
|
this.#target = params.target ?? params.host;
|
|
42
|
-
this.#boundDispatch = this.#dispatch.bind(this);
|
|
43
49
|
|
|
44
50
|
if (this.#params.connected) this.connect();
|
|
45
51
|
}
|
|
@@ -55,32 +61,35 @@ export class SuperAction implements SuperActionInterface {
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
disconnect() {
|
|
64
|
+
if (!this.#connected) return;
|
|
65
|
+
this.#connected = false;
|
|
66
|
+
|
|
58
67
|
let { host, eventNames } = this.#params;
|
|
59
68
|
for (let name of eventNames) {
|
|
60
69
|
host.removeEventListener(name, this.#boundDispatch);
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
#dispatch(
|
|
65
|
-
let { type, currentTarget, target } =
|
|
73
|
+
#dispatch(originEvent: Event) {
|
|
74
|
+
let { type, currentTarget, target } = originEvent;
|
|
66
75
|
if (!currentTarget) return;
|
|
67
76
|
|
|
68
77
|
let formData: FormData | undefined;
|
|
69
78
|
if (target instanceof HTMLFormElement) formData = new FormData(target);
|
|
70
79
|
|
|
71
|
-
for (let node of
|
|
80
|
+
for (let node of originEvent.composedPath()) {
|
|
72
81
|
if (node instanceof Element) {
|
|
73
82
|
if (node.hasAttribute(`${type}:prevent-default`))
|
|
74
|
-
|
|
83
|
+
originEvent.preventDefault();
|
|
75
84
|
|
|
76
85
|
if (node.hasAttribute(`${type}:stop-immediate-propagation`))
|
|
77
86
|
return;
|
|
78
87
|
|
|
79
|
-
let
|
|
80
|
-
if (
|
|
88
|
+
let kind = node.getAttribute(`${type}:`);
|
|
89
|
+
if (kind) {
|
|
81
90
|
let composed = node.hasAttribute(`${type}:composed`);
|
|
82
91
|
let event = new ActionEvent(
|
|
83
|
-
{
|
|
92
|
+
{ kind, originElement: node, originEvent, formData },
|
|
84
93
|
{ bubbles: true, composed },
|
|
85
94
|
);
|
|
86
95
|
|
package/action_events.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
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.
|