@hyperfixi/behaviors 3.3.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +199 -1
- package/README.md +29 -9
- package/dist/all.browser.global.js +45 -100
- package/dist/all.browser.global.js.map +1 -1
- package/dist/autodismiss.browser.global.js +5 -11
- package/dist/autodismiss.browser.global.js.map +1 -1
- package/dist/behaviors/autodismiss.cjs +19 -22
- package/dist/behaviors/autodismiss.cjs.map +1 -1
- package/dist/behaviors/autodismiss.d.cts +4 -4
- package/dist/behaviors/autodismiss.d.ts +4 -4
- package/dist/behaviors/autodismiss.js +3 -3
- package/dist/behaviors/clickoutside.cjs +19 -18
- package/dist/behaviors/clickoutside.cjs.map +1 -1
- package/dist/behaviors/clickoutside.d.cts +4 -4
- package/dist/behaviors/clickoutside.d.ts +4 -4
- package/dist/behaviors/clickoutside.js +3 -3
- package/dist/behaviors/clipboard.cjs +18 -19
- package/dist/behaviors/clipboard.cjs.map +1 -1
- package/dist/behaviors/clipboard.d.cts +4 -4
- package/dist/behaviors/clipboard.d.ts +4 -4
- package/dist/behaviors/clipboard.js +3 -3
- package/dist/behaviors/draggable.cjs +20 -25
- package/dist/behaviors/draggable.cjs.map +1 -1
- package/dist/behaviors/draggable.d.cts +4 -4
- package/dist/behaviors/draggable.d.ts +4 -4
- package/dist/behaviors/draggable.js +3 -3
- package/dist/behaviors/focustrap.cjs +18 -19
- package/dist/behaviors/focustrap.cjs.map +1 -1
- package/dist/behaviors/focustrap.d.cts +4 -4
- package/dist/behaviors/focustrap.d.ts +4 -4
- package/dist/behaviors/focustrap.js +3 -3
- package/dist/behaviors/removable.cjs +18 -21
- package/dist/behaviors/removable.cjs.map +1 -1
- package/dist/behaviors/removable.d.cts +4 -3
- package/dist/behaviors/removable.d.ts +4 -3
- package/dist/behaviors/removable.js +3 -3
- package/dist/behaviors/resizable.cjs +19 -19
- package/dist/behaviors/resizable.cjs.map +1 -1
- package/dist/behaviors/resizable.d.cts +4 -4
- package/dist/behaviors/resizable.d.ts +4 -4
- package/dist/behaviors/resizable.js +3 -3
- package/dist/behaviors/scrollreveal.cjs +19 -22
- package/dist/behaviors/scrollreveal.cjs.map +1 -1
- package/dist/behaviors/scrollreveal.d.cts +4 -4
- package/dist/behaviors/scrollreveal.d.ts +4 -4
- package/dist/behaviors/scrollreveal.js +3 -3
- package/dist/behaviors/sortable.cjs +19 -20
- package/dist/behaviors/sortable.cjs.map +1 -1
- package/dist/behaviors/sortable.d.cts +4 -4
- package/dist/behaviors/sortable.d.ts +4 -4
- package/dist/behaviors/sortable.js +3 -3
- package/dist/behaviors/tabs.cjs +20 -25
- package/dist/behaviors/tabs.cjs.map +1 -1
- package/dist/behaviors/tabs.d.cts +4 -4
- package/dist/behaviors/tabs.d.ts +4 -4
- package/dist/behaviors/tabs.js +3 -3
- package/dist/behaviors/toggleable.cjs +25 -26
- package/dist/behaviors/toggleable.cjs.map +1 -1
- package/dist/behaviors/toggleable.d.cts +4 -4
- package/dist/behaviors/toggleable.d.ts +4 -4
- package/dist/behaviors/toggleable.js +3 -3
- package/dist/chunk-2GIS4DCY.js +30 -0
- package/dist/chunk-2GIS4DCY.js.map +1 -0
- package/dist/chunk-3BCBZR6R.js +30 -0
- package/dist/chunk-3BCBZR6R.js.map +1 -0
- package/dist/chunk-4YUIDZAO.js +30 -0
- package/dist/chunk-4YUIDZAO.js.map +1 -0
- package/dist/{chunk-XYZ7FOXJ.js → chunk-AQCOK5J5.js} +10 -14
- package/dist/chunk-AQCOK5J5.js.map +1 -0
- package/dist/chunk-ATGHQQC7.js +30 -0
- package/dist/chunk-ATGHQQC7.js.map +1 -0
- package/dist/{chunk-HQDYH45I.js → chunk-CWLAXLWR.js} +5 -13
- package/dist/chunk-CWLAXLWR.js.map +1 -0
- package/dist/chunk-DMWN52SV.js +30 -0
- package/dist/chunk-DMWN52SV.js.map +1 -0
- package/dist/{chunk-PDMOOWFW.js → chunk-EIVPVTUI.js} +4 -8
- package/dist/{chunk-PDMOOWFW.js.map → chunk-EIVPVTUI.js.map} +1 -1
- package/dist/chunk-HHTEMPIT.js +30 -0
- package/dist/chunk-HHTEMPIT.js.map +1 -0
- package/dist/{chunk-26IX7QU7.js → chunk-HJESSMKB.js} +4 -6
- package/dist/{chunk-26IX7QU7.js.map → chunk-HJESSMKB.js.map} +1 -1
- package/dist/{chunk-5S7WN3ZQ.js → chunk-IFAH652L.js} +4 -7
- package/dist/chunk-IFAH652L.js.map +1 -0
- package/dist/chunk-IUGJG4RA.js +30 -0
- package/dist/chunk-IUGJG4RA.js.map +1 -0
- package/dist/{chunk-ZR66E3T6.js → chunk-JNLVXPHR.js} +13 -13
- package/dist/chunk-KEZEGPEX.js +30 -0
- package/dist/chunk-KEZEGPEX.js.map +1 -0
- package/dist/{chunk-66U2RSYJ.js → chunk-KP4GNXYS.js} +4 -10
- package/dist/chunk-KP4GNXYS.js.map +1 -0
- package/dist/chunk-KXQMCN4O.js +30 -0
- package/dist/chunk-KXQMCN4O.js.map +1 -0
- package/dist/{chunk-YXS27XM6.js → chunk-KYBOZ7C4.js} +3 -7
- package/dist/chunk-KYBOZ7C4.js.map +1 -0
- package/dist/chunk-LBWM6TTU.js +30 -0
- package/dist/chunk-LBWM6TTU.js.map +1 -0
- package/dist/{chunk-3DVDS7CT.js → chunk-LCDONZBX.js} +5 -5
- package/dist/chunk-LCDONZBX.js.map +1 -0
- package/dist/{chunk-OJ3YWO3W.js → chunk-NE76IBV2.js} +3 -7
- package/dist/chunk-NE76IBV2.js.map +1 -0
- package/dist/{chunk-HMU342XE.js → chunk-PVO6TZCZ.js} +3 -9
- package/dist/{chunk-HMU342XE.js.map → chunk-PVO6TZCZ.js.map} +1 -1
- package/dist/{chunk-URZGNPTQ.js → chunk-RILYRQ72.js} +4 -10
- package/dist/chunk-RILYRQ72.js.map +1 -0
- package/dist/{chunk-FBQ3X3UY.js → chunk-U4QMFVAM.js} +5 -13
- package/dist/chunk-U4QMFVAM.js.map +1 -0
- package/dist/chunk-VWDTOSE3.js +26 -0
- package/dist/chunk-VWDTOSE3.js.map +1 -0
- package/dist/chunk-YRQBU2OW.js +30 -0
- package/dist/chunk-YRQBU2OW.js.map +1 -0
- package/dist/clickoutside.browser.global.js +5 -7
- package/dist/clickoutside.browser.global.js.map +1 -1
- package/dist/clipboard.browser.global.js +4 -8
- package/dist/clipboard.browser.global.js.map +1 -1
- package/dist/core.browser.global.js +13 -25
- package/dist/core.browser.global.js.map +1 -1
- package/dist/draggable.browser.global.js +5 -13
- package/dist/draggable.browser.global.js.map +1 -1
- package/dist/focustrap.browser.global.js +4 -8
- package/dist/focustrap.browser.global.js.map +1 -1
- package/dist/index.cjs +96 -271
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +42 -55
- package/dist/index.js.map +1 -1
- package/dist/loaders.cjs +75 -237
- package/dist/loaders.cjs.map +1 -1
- package/dist/loaders.js +13 -13
- package/dist/registry.cjs +4 -4
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.d.cts +5 -6
- package/dist/registry.d.ts +5 -6
- package/dist/registry.js +1 -1
- package/dist/removable.browser.global.js +3 -9
- package/dist/removable.browser.global.js.map +1 -1
- package/dist/resizable.browser.global.js +4 -7
- package/dist/resizable.browser.global.js.map +1 -1
- package/dist/resolver.browser.global.js +45 -100
- package/dist/resolver.browser.global.js.map +1 -1
- package/dist/schemas/index.cjs +38 -93
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +2 -2
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.js +11 -11
- package/dist/schemas/index.js.map +1 -1
- package/dist/scrollreveal.browser.global.js +5 -11
- package/dist/scrollreveal.browser.global.js.map +1 -1
- package/dist/sortable.browser.global.js +5 -9
- package/dist/sortable.browser.global.js.map +1 -1
- package/dist/tabs.browser.global.js +6 -14
- package/dist/tabs.browser.global.js.map +1 -1
- package/dist/toggleable.browser.global.js +9 -13
- package/dist/toggleable.browser.global.js.map +1 -1
- package/dist/{types-DCCtVQS5.d.cts → types-CbA4L8XJ.d.cts} +16 -23
- package/dist/{types-DCCtVQS5.d.ts → types-CbA4L8XJ.d.ts} +16 -23
- package/package.json +5 -5
- package/src/behavior-resolver.ts +24 -56
- package/src/behaviors/autodismiss.test.ts +12 -20
- package/src/behaviors/autodismiss.ts +8 -23
- package/src/behaviors/clickoutside.test.ts +13 -21
- package/src/behaviors/clickoutside.ts +8 -23
- package/src/behaviors/clipboard.test.ts +12 -34
- package/src/behaviors/clipboard.ts +8 -23
- package/src/behaviors/curated-runtime.test.ts +22 -24
- package/src/behaviors/draggable.ts +10 -24
- package/src/behaviors/experimental-runtime.test.ts +19 -18
- package/src/behaviors/focustrap.test.ts +12 -20
- package/src/behaviors/focustrap.ts +8 -23
- package/src/behaviors/integration.test.ts +19 -54
- package/src/behaviors/optional-runtime.test.ts +17 -22
- package/src/behaviors/removable.ts +6 -20
- package/src/behaviors/resizable.ts +9 -24
- package/src/behaviors/scrollreveal.test.ts +13 -21
- package/src/behaviors/scrollreveal.ts +8 -23
- package/src/behaviors/sortable.ts +7 -22
- package/src/behaviors/tabs.test.ts +12 -34
- package/src/behaviors/tabs.ts +8 -23
- package/src/behaviors/toggleable.ts +10 -26
- package/src/browser-resolver-bundle.ts +20 -30
- package/src/core-bundle.ts +11 -11
- package/src/index.test.ts +2 -2
- package/src/index.ts +30 -47
- package/src/registry.test.ts +9 -34
- package/src/registry.ts +7 -11
- package/src/schemas/autodismiss.schema.ts +3 -9
- package/src/schemas/clickoutside.schema.ts +3 -5
- package/src/schemas/clipboard.schema.ts +2 -6
- package/src/schemas/draggable.schema.ts +4 -12
- package/src/schemas/focustrap.schema.ts +2 -6
- package/src/schemas/index.ts +2 -1
- package/src/schemas/removable.schema.ts +2 -8
- package/src/schemas/resizable.schema.ts +3 -6
- package/src/schemas/scrollreveal.schema.ts +3 -9
- package/src/schemas/sortable.schema.ts +3 -7
- package/src/schemas/tabs.schema.ts +4 -12
- package/src/schemas/toggleable.schema.ts +10 -13
- package/src/schemas/types.ts +37 -30
- package/dist/chunk-2HUF6RMI.js +0 -40
- package/dist/chunk-2HUF6RMI.js.map +0 -1
- package/dist/chunk-3DVDS7CT.js.map +0 -1
- package/dist/chunk-5S7WN3ZQ.js.map +0 -1
- package/dist/chunk-66U2RSYJ.js.map +0 -1
- package/dist/chunk-6TJXDCAQ.js +0 -11
- package/dist/chunk-6TJXDCAQ.js.map +0 -1
- package/dist/chunk-7TPDLB7W.js +0 -40
- package/dist/chunk-7TPDLB7W.js.map +0 -1
- package/dist/chunk-CR3J7VV6.js +0 -40
- package/dist/chunk-CR3J7VV6.js.map +0 -1
- package/dist/chunk-EDBG2F57.js +0 -40
- package/dist/chunk-EDBG2F57.js.map +0 -1
- package/dist/chunk-FBQ3X3UY.js.map +0 -1
- package/dist/chunk-HQDYH45I.js.map +0 -1
- package/dist/chunk-HX663BJ4.js +0 -40
- package/dist/chunk-HX663BJ4.js.map +0 -1
- package/dist/chunk-KROW43CG.js +0 -40
- package/dist/chunk-KROW43CG.js.map +0 -1
- package/dist/chunk-L52AGCMP.js +0 -40
- package/dist/chunk-L52AGCMP.js.map +0 -1
- package/dist/chunk-MTI5PSRH.js +0 -40
- package/dist/chunk-MTI5PSRH.js.map +0 -1
- package/dist/chunk-OJ3YWO3W.js.map +0 -1
- package/dist/chunk-ORBJ7MRW.js +0 -40
- package/dist/chunk-ORBJ7MRW.js.map +0 -1
- package/dist/chunk-SLSCZV4A.js +0 -40
- package/dist/chunk-SLSCZV4A.js.map +0 -1
- package/dist/chunk-URZGNPTQ.js.map +0 -1
- package/dist/chunk-WKBRZGGL.js +0 -40
- package/dist/chunk-WKBRZGGL.js.map +0 -1
- package/dist/chunk-XYZ7FOXJ.js.map +0 -1
- package/dist/chunk-YXS27XM6.js.map +0 -1
- /package/dist/{chunk-ZR66E3T6.js.map → chunk-JNLVXPHR.js.map} +0 -0
|
@@ -2,50 +2,28 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
describe('Clipboard behavior', () => {
|
|
4
4
|
describe('registerClipboard', () => {
|
|
5
|
-
it('
|
|
5
|
+
it('defines the behavior on the host from its hyperscript source', async () => {
|
|
6
6
|
const { registerClipboard, clipboardSource } = await import('./clipboard');
|
|
7
|
-
const
|
|
8
|
-
compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
|
|
9
|
-
execute: vi.fn().mockResolvedValue(undefined),
|
|
10
|
-
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
11
|
-
};
|
|
7
|
+
const host = { evaluate: vi.fn() };
|
|
12
8
|
|
|
13
|
-
await registerClipboard(
|
|
9
|
+
await registerClipboard(host);
|
|
14
10
|
|
|
15
|
-
expect(
|
|
16
|
-
expect(mock.execute).toHaveBeenCalledWith(
|
|
17
|
-
{ type: 'behavior' },
|
|
18
|
-
expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
|
|
19
|
-
);
|
|
11
|
+
expect(host.evaluate).toHaveBeenCalledWith(clipboardSource);
|
|
20
12
|
});
|
|
21
13
|
|
|
22
|
-
it('should throw
|
|
14
|
+
it('should throw when the host rejects the source', async () => {
|
|
23
15
|
const { registerClipboard } = await import('./clipboard');
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
const host = {
|
|
17
|
+
evaluate: vi.fn(() => {
|
|
18
|
+
throw new Error('boom');
|
|
19
|
+
}),
|
|
28
20
|
};
|
|
29
|
-
await expect(registerClipboard(
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should throw when no runtime available', async () => {
|
|
33
|
-
const { registerClipboard } = await import('./clipboard');
|
|
34
|
-
await expect(registerClipboard(undefined)).rejects.toThrowError(/LokaScript not found/);
|
|
21
|
+
await expect(registerClipboard(host)).rejects.toThrowError(/Failed to define Clipboard/);
|
|
35
22
|
});
|
|
36
23
|
|
|
37
|
-
it('should
|
|
24
|
+
it('should throw when no host is available', async () => {
|
|
38
25
|
const { registerClipboard } = await import('./clipboard');
|
|
39
|
-
|
|
40
|
-
compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
|
|
41
|
-
execute: vi.fn().mockResolvedValue(undefined),
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
await registerClipboard(mock as any);
|
|
45
|
-
expect(mock.execute).toHaveBeenCalledWith(
|
|
46
|
-
expect.anything(),
|
|
47
|
-
expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
|
|
48
|
-
);
|
|
26
|
+
await expect(registerClipboard(undefined)).rejects.toThrowError(/No hyperscript host/);
|
|
49
27
|
});
|
|
50
28
|
});
|
|
51
29
|
|
|
@@ -5,39 +5,24 @@
|
|
|
5
5
|
* Uses navigator.clipboard.writeText() with execCommand fallback (in its
|
|
6
6
|
* hyperscript `source`'s js() body).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
8
|
+
* Defined from its hyperscript `source` (the single source of truth shared with
|
|
9
|
+
* the browser bundle and patterns-reference).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { clipboardSchema } from '../schemas/clipboard.schema';
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
13
|
+
import { defineBehavior, resolveRuntime } from '../schemas/types';
|
|
14
|
+
import type { HyperscriptHost } from '../schemas/types';
|
|
15
15
|
|
|
16
16
|
// Re-export schema-derived values
|
|
17
17
|
export const clipboardSource = clipboardSchema.source;
|
|
18
18
|
export const clipboardMetadata = clipboardSchema;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Define the Clipboard behavior on a hyperscript host (`window.hyperfixi` when none is given)
|
|
22
|
+
* from its source, as a `<script type="text/hyperscript">` would.
|
|
23
23
|
*/
|
|
24
|
-
export async function registerClipboard(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (!hf) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const result = hf.compileSync(clipboardSchema.source, { traditional: true });
|
|
34
|
-
|
|
35
|
-
if (!result.ok) {
|
|
36
|
-
throw new Error(`Failed to compile Clipboard behavior: ${JSON.stringify(result.errors)}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
40
|
-
await hf.execute(result.ast, ctx);
|
|
24
|
+
export async function registerClipboard(host?: HyperscriptHost): Promise<void> {
|
|
25
|
+
defineBehavior(clipboardSchema, host);
|
|
41
26
|
}
|
|
42
27
|
|
|
43
28
|
// Auto-register when loaded as a script tag
|
|
@@ -1,45 +1,43 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
/**
|
|
3
|
-
* Curated behaviors — REAL runtime behavior tests
|
|
3
|
+
* Curated behaviors — REAL runtime behavior tests.
|
|
4
4
|
*
|
|
5
|
-
* These exercise each curated behavior's actual hyperscript `source`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* bodies actually do the right thing at runtime.
|
|
5
|
+
* These exercise each curated behavior's actual hyperscript `source` on the real
|
|
6
|
+
* @hyperfixi/engine: define → install on an element → drive the DOM → assert both the
|
|
7
|
+
* effect AND the documented lifecycle events. This is the "parses ≠ works" guard — the
|
|
8
|
+
* mock-based unit tests only prove `register*()` hands the source to the host; these prove
|
|
9
|
+
* the sources do the right thing when they run.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
* js() block). Both are fixed in the schema sources.
|
|
11
|
+
* The engine runs the same source the pages do, and upstream _hyperscript runs it the same
|
|
12
|
+
* way (measured in a browser, 2026-10-03, with at least one argument given: upstream's
|
|
13
|
+
* `install X` without parentheses throws; `install X()` works there).
|
|
16
14
|
*/
|
|
17
15
|
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest';
|
|
18
|
-
import {
|
|
16
|
+
import { api, everything, processNode, register } from '@hyperfixi/engine';
|
|
19
17
|
import { registerToggleable } from './toggleable';
|
|
20
18
|
import { registerRemovable } from './removable';
|
|
21
19
|
import { registerClickOutside } from './clickoutside';
|
|
22
20
|
import { registerClipboard } from './clipboard';
|
|
23
21
|
import { registerAutoDismiss } from './autodismiss';
|
|
24
22
|
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
const hf = hyperscript as any;
|
|
27
|
-
|
|
28
23
|
const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
|
|
29
24
|
|
|
25
|
+
/** Install from an attribute, as a page does: the engine reads `_` and runs the install. */
|
|
30
26
|
async function install(code: string, el: HTMLElement): Promise<void> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await
|
|
27
|
+
el.setAttribute('_', code);
|
|
28
|
+
processNode(el);
|
|
29
|
+
await tick();
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
beforeAll(async () => {
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await
|
|
41
|
-
await
|
|
42
|
-
await
|
|
33
|
+
// The engine is assembled from modules; this bundle is every one. Behaviors are globals
|
|
34
|
+
// (`window.Toggleable`), so define the set once per file.
|
|
35
|
+
register(...everything);
|
|
36
|
+
await registerToggleable(api);
|
|
37
|
+
await registerRemovable(api);
|
|
38
|
+
await registerClickOutside(api);
|
|
39
|
+
await registerClipboard(api);
|
|
40
|
+
await registerAutoDismiss(api);
|
|
43
41
|
});
|
|
44
42
|
|
|
45
43
|
afterEach(() => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Draggable Behavior
|
|
3
3
|
*
|
|
4
|
-
* Makes elements draggable with pointer events.
|
|
5
|
-
* `source` (the single source of truth shared with the
|
|
4
|
+
* Makes elements draggable with pointer events. Defined from its hyperscript
|
|
5
|
+
* `source` (the single source of truth shared with the browser bundle and
|
|
6
6
|
* patterns-reference) — no imperative installer. The source uses
|
|
7
7
|
* `repeat until event pointerup` + `wait for pointermove or pointerup` to run the
|
|
8
|
-
* drag loop, `
|
|
8
|
+
* drag loop, `my offsetLeft` / `my offsetTop` for the start offset, and
|
|
9
|
+
* `add { left: ${…}px }` to move.
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
12
|
* ```html
|
|
@@ -21,34 +22,19 @@
|
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
import { draggableSchema } from '../schemas/draggable.schema';
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
25
|
+
import { defineBehavior, resolveRuntime } from '../schemas/types';
|
|
26
|
+
import type { HyperscriptHost } from '../schemas/types';
|
|
26
27
|
|
|
27
28
|
// Re-export schema-derived values for backwards compatibility
|
|
28
29
|
export const draggableSource = draggableSchema.source;
|
|
29
30
|
export const draggableMetadata = draggableSchema;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
* source
|
|
33
|
+
* Define the Draggable behavior on a hyperscript host (`window.hyperfixi` when none is given)
|
|
34
|
+
* from its source, as a `<script type="text/hyperscript">` would.
|
|
34
35
|
*/
|
|
35
|
-
export async function registerDraggable(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!hf) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const result = hf.compileSync(draggableSchema.source, { traditional: true });
|
|
45
|
-
|
|
46
|
-
if (!result.ok) {
|
|
47
|
-
throw new Error(`Failed to compile Draggable behavior: ${JSON.stringify(result.errors)}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
51
|
-
await hf.execute(result.ast, ctx);
|
|
36
|
+
export async function registerDraggable(host?: HyperscriptHost): Promise<void> {
|
|
37
|
+
defineBehavior(draggableSchema, host);
|
|
52
38
|
}
|
|
53
39
|
|
|
54
40
|
// Auto-register when loaded as a script tag
|
|
@@ -2,39 +2,40 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Experimental behaviors — REAL runtime tests.
|
|
4
4
|
*
|
|
5
|
-
* Draggable / Sortable / Resizable
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* These tests are the "parses ≠ works" guard: register → install → drive pointer
|
|
9
|
-
* events → assert the DOM effect AND the documented lifecycle events.
|
|
5
|
+
* Draggable / Sortable / Resizable are defined from their hyperscript `source` like every
|
|
6
|
+
* other behavior (the "no imperative JS" rule). These tests are the "parses ≠ works" guard:
|
|
7
|
+
* define → install → drive pointer events → assert the DOM effect AND the lifecycle events.
|
|
10
8
|
*
|
|
11
|
-
* happy-dom has no PointerEvent and zero layout
|
|
12
|
-
* carry clientX/clientY) and assert deltas, which are
|
|
9
|
+
* happy-dom has no PointerEvent and zero layout (`my offsetLeft` and `my offsetWidth` read
|
|
10
|
+
* 0), so we drive `MouseEvent`s (which carry clientX/clientY) and assert deltas, which are
|
|
11
|
+
* layout-independent.
|
|
13
12
|
*/
|
|
14
13
|
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
|
|
15
|
-
import {
|
|
14
|
+
import { api, everything, processNode, register } from '@hyperfixi/engine';
|
|
16
15
|
import { registerDraggable } from './draggable';
|
|
17
16
|
import { registerSortable } from './sortable';
|
|
18
17
|
import { registerResizable } from './resizable';
|
|
19
18
|
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
-
const hf = hyperscript as any;
|
|
22
19
|
const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
|
|
23
20
|
|
|
24
21
|
function pointer(type: string, x: number, y: number): MouseEvent {
|
|
25
22
|
return new MouseEvent(type, { clientX: x, clientY: y, bubbles: true, cancelable: true });
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
/** Install from an attribute, as a page does: the engine reads `_` and runs the install. */
|
|
28
26
|
async function install(code: string, el: HTMLElement): Promise<void> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await
|
|
27
|
+
el.setAttribute('_', code);
|
|
28
|
+
processNode(el);
|
|
29
|
+
await tick();
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
beforeAll(async () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// The engine is assembled from modules; this bundle is every one. Behaviors are globals
|
|
34
|
+
// (`window.Toggleable`), so define the set once per file.
|
|
35
|
+
register(...everything);
|
|
36
|
+
await registerDraggable(api);
|
|
37
|
+
await registerSortable(api);
|
|
38
|
+
await registerResizable(api);
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
afterEach(() => {
|
|
@@ -63,7 +64,7 @@ describe('Draggable — runtime (source-compiled)', () => {
|
|
|
63
64
|
document.dispatchEvent(pointer('pointerup', 60, 80));
|
|
64
65
|
await tick(40);
|
|
65
66
|
|
|
66
|
-
//
|
|
67
|
+
// offsetLeft/offsetTop = 0 in happy-dom → xoff = 10, so left = 60 - 10 = 50, top = 80 - 10 = 70
|
|
67
68
|
expect(el.style.left).toBe('50px');
|
|
68
69
|
expect(el.style.top).toBe('70px');
|
|
69
70
|
expect(ev).toContain('start');
|
|
@@ -126,7 +127,7 @@ describe('Resizable — runtime (source-compiled)', () => {
|
|
|
126
127
|
document.dispatchEvent(pointer('pointerup', 60, 40));
|
|
127
128
|
await tick(40);
|
|
128
129
|
|
|
129
|
-
//
|
|
130
|
+
// offsetWidth/offsetHeight = 0 in happy-dom → newWidth = 0 + 60 - 10 = 50, newHeight = 0 + 40 - 10 = 30
|
|
130
131
|
expect(box.style.width).toBe('50px');
|
|
131
132
|
expect(box.style.height).toBe('30px');
|
|
132
133
|
expect(ev).toContain('start');
|
|
@@ -2,36 +2,28 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
describe('FocusTrap behavior', () => {
|
|
4
4
|
describe('registerFocusTrap', () => {
|
|
5
|
-
it('
|
|
5
|
+
it('defines the behavior on the host from its hyperscript source', async () => {
|
|
6
6
|
const { registerFocusTrap, focusTrapSource } = await import('./focustrap');
|
|
7
|
-
const
|
|
8
|
-
compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
|
|
9
|
-
execute: vi.fn().mockResolvedValue(undefined),
|
|
10
|
-
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
11
|
-
};
|
|
7
|
+
const host = { evaluate: vi.fn() };
|
|
12
8
|
|
|
13
|
-
await registerFocusTrap(
|
|
9
|
+
await registerFocusTrap(host);
|
|
14
10
|
|
|
15
|
-
expect(
|
|
16
|
-
expect(mock.execute).toHaveBeenCalledWith(
|
|
17
|
-
{ type: 'behavior' },
|
|
18
|
-
expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
|
|
19
|
-
);
|
|
11
|
+
expect(host.evaluate).toHaveBeenCalledWith(focusTrapSource);
|
|
20
12
|
});
|
|
21
13
|
|
|
22
|
-
it('should throw
|
|
14
|
+
it('should throw when the host rejects the source', async () => {
|
|
23
15
|
const { registerFocusTrap } = await import('./focustrap');
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
const host = {
|
|
17
|
+
evaluate: vi.fn(() => {
|
|
18
|
+
throw new Error('boom');
|
|
19
|
+
}),
|
|
28
20
|
};
|
|
29
|
-
await expect(registerFocusTrap(
|
|
21
|
+
await expect(registerFocusTrap(host)).rejects.toThrowError(/Failed to define FocusTrap/);
|
|
30
22
|
});
|
|
31
23
|
|
|
32
|
-
it('should throw when no
|
|
24
|
+
it('should throw when no host is available', async () => {
|
|
33
25
|
const { registerFocusTrap } = await import('./focustrap');
|
|
34
|
-
await expect(registerFocusTrap(undefined)).rejects.toThrowError(/
|
|
26
|
+
await expect(registerFocusTrap(undefined)).rejects.toThrowError(/No hyperscript host/);
|
|
35
27
|
});
|
|
36
28
|
});
|
|
37
29
|
|
|
@@ -5,41 +5,26 @@
|
|
|
5
5
|
* and restores focus on deactivation. The Tab-trapping / focus-model logic
|
|
6
6
|
* lives in the schema's hyperscript `source` (an `init`-block `js()` body).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
8
|
+
* Defined from its hyperscript `source` — the single source of truth shared with
|
|
9
|
+
* the browser bundle and patterns-reference. This collapses FocusTrap onto
|
|
10
10
|
* the one runtime path (no more imperative-installer fork between CDN and npm);
|
|
11
11
|
* see docs-internal/BEHAVIORS_CONSOLIDATION_PLAN.md §3d.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { focusTrapSchema } from '../schemas/focustrap.schema';
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
15
|
+
import { defineBehavior, resolveRuntime } from '../schemas/types';
|
|
16
|
+
import type { HyperscriptHost } from '../schemas/types';
|
|
17
17
|
|
|
18
18
|
// Re-export schema-derived values
|
|
19
19
|
export const focusTrapSource = focusTrapSchema.source;
|
|
20
20
|
export const focusTrapMetadata = focusTrapSchema;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Define the FocusTrap behavior on a hyperscript host (`window.hyperfixi` when none is given)
|
|
24
|
+
* from its source, as a `<script type="text/hyperscript">` would.
|
|
25
25
|
*/
|
|
26
|
-
export async function registerFocusTrap(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!hf) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const result = hf.compileSync(focusTrapSchema.source, { traditional: true });
|
|
36
|
-
|
|
37
|
-
if (!result.ok) {
|
|
38
|
-
throw new Error(`Failed to compile FocusTrap behavior: ${JSON.stringify(result.errors)}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
42
|
-
await hf.execute(result.ast, ctx);
|
|
26
|
+
export async function registerFocusTrap(host?: HyperscriptHost): Promise<void> {
|
|
27
|
+
defineBehavior(focusTrapSchema, host);
|
|
43
28
|
}
|
|
44
29
|
|
|
45
30
|
// Auto-register when loaded as a script tag
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import type {
|
|
2
|
+
import type { HyperscriptHost } from '../schemas/types';
|
|
3
3
|
import { registerDraggable, draggableSource } from './draggable';
|
|
4
4
|
import { registerToggleable, toggleableSource } from './toggleable';
|
|
5
5
|
import { registerRemovable, removableSource } from './removable';
|
|
@@ -12,21 +12,15 @@ import { registerFocusTrap, focusTrapSource } from './focustrap';
|
|
|
12
12
|
import { registerScrollReveal, scrollRevealSource } from './scrollreveal';
|
|
13
13
|
import { registerTabs, tabsSource } from './tabs';
|
|
14
14
|
|
|
15
|
-
function
|
|
16
|
-
return {
|
|
17
|
-
compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
|
|
18
|
-
execute: vi.fn().mockResolvedValue(undefined),
|
|
19
|
-
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
20
|
-
...overrides,
|
|
21
|
-
};
|
|
15
|
+
function createMockHost(overrides: Partial<HyperscriptHost> = {}): HyperscriptHost {
|
|
16
|
+
return { evaluate: vi.fn(), ...overrides };
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// three (FocusTrap/ScrollReveal/Tabs) carry their web-API logic in an `init`-block
|
|
19
|
+
// EVERY behavior is defined from its hyperscript `source` with the host's `evaluate` — one
|
|
20
|
+
// path, identical browser/npm, no imperative JS installer (the "no imperative JS" rule). The
|
|
21
|
+
// optional three (FocusTrap/ScrollReveal/Tabs) carry their web-API logic in an `init`-block
|
|
28
22
|
// `js()` body, and the experimental three (Draggable/Sortable/Resizable) run their
|
|
29
|
-
// pointer-drag loops via `repeat until event` — all through the single
|
|
23
|
+
// pointer-drag loops via `repeat until event` — all through the single path.
|
|
30
24
|
const compiledBehaviors = [
|
|
31
25
|
{ name: 'Removable', register: registerRemovable, source: removableSource },
|
|
32
26
|
{ name: 'Toggleable', register: registerToggleable, source: toggleableSource },
|
|
@@ -42,57 +36,28 @@ const compiledBehaviors = [
|
|
|
42
36
|
] as const;
|
|
43
37
|
|
|
44
38
|
describe('behavior registration integration', () => {
|
|
45
|
-
describe('source-
|
|
39
|
+
describe('source-defined behaviors', () => {
|
|
46
40
|
for (const { name, register, source } of compiledBehaviors) {
|
|
47
41
|
describe(name, () => {
|
|
48
|
-
it('should
|
|
49
|
-
const
|
|
50
|
-
await register(
|
|
42
|
+
it('should define the source on the host', async () => {
|
|
43
|
+
const host = createMockHost();
|
|
44
|
+
await register(host);
|
|
51
45
|
|
|
52
|
-
expect(
|
|
46
|
+
expect(host.evaluate).toHaveBeenCalledWith(source);
|
|
53
47
|
});
|
|
54
48
|
|
|
55
|
-
it('should
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(mock.createContext).toHaveBeenCalled();
|
|
60
|
-
expect(mock.execute).toHaveBeenCalledWith(
|
|
61
|
-
{ type: 'behavior' },
|
|
62
|
-
{ locals: expect.any(Map), globals: expect.any(Map) }
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should throw on compile failure', async () => {
|
|
67
|
-
const mock = createMockInstance({
|
|
68
|
-
compileSync: vi.fn().mockReturnValue({
|
|
69
|
-
ok: false,
|
|
70
|
-
errors: [{ message: 'syntax error', line: 1 }],
|
|
49
|
+
it('should throw when the host rejects the source', async () => {
|
|
50
|
+
const host = createMockHost({
|
|
51
|
+
evaluate: vi.fn(() => {
|
|
52
|
+
throw new Error('syntax error');
|
|
71
53
|
}),
|
|
72
54
|
});
|
|
73
55
|
|
|
74
|
-
await expect(register(
|
|
75
|
-
new RegExp(`Failed to compile ${name}`)
|
|
76
|
-
);
|
|
56
|
+
await expect(register(host)).rejects.toThrowError(new RegExp(`Failed to define ${name}`));
|
|
77
57
|
});
|
|
78
58
|
|
|
79
|
-
it('should throw when
|
|
80
|
-
await expect(register(undefined)).rejects.toThrowError(/
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should fall back to manual context when createContext is missing', async () => {
|
|
84
|
-
const mock = createMockInstance();
|
|
85
|
-
delete (mock as Partial<LokaScriptInstance>).createContext;
|
|
86
|
-
|
|
87
|
-
await register(mock);
|
|
88
|
-
|
|
89
|
-
expect(mock.execute).toHaveBeenCalledWith(
|
|
90
|
-
{ type: 'behavior' },
|
|
91
|
-
expect.objectContaining({
|
|
92
|
-
locals: expect.any(Map),
|
|
93
|
-
globals: expect.any(Map),
|
|
94
|
-
})
|
|
95
|
-
);
|
|
59
|
+
it('should throw when no host is available', async () => {
|
|
60
|
+
await expect(register(undefined)).rejects.toThrowError(/No hyperscript host/);
|
|
96
61
|
});
|
|
97
62
|
});
|
|
98
63
|
}
|
|
@@ -2,40 +2,35 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Optional behaviors — REAL runtime behavior tests.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* This is the "parses ≠ works" guard (§2/§5): the mock-based unit tests only prove
|
|
13
|
-
* `register*()` calls compileSync; these prove the `init`-block `js()` bodies (the
|
|
14
|
-
* focus model, the IntersectionObserver, the ARIA/keyboard wiring) actually run
|
|
15
|
-
* correctly once compiled — exactly the class of bug the imperative path masked.
|
|
5
|
+
* Like curated-runtime.test.ts, these exercise each behavior's actual hyperscript `source`
|
|
6
|
+
* on the real @hyperfixi/engine — define → install on an element → drive the DOM → assert
|
|
7
|
+
* both the effect AND the documented lifecycle events. The optional three carry their
|
|
8
|
+
* web-API logic (a focus model, an IntersectionObserver, ARIA/keyboard wiring) in an
|
|
9
|
+
* `init`-block `js()` body; these prove those bodies run correctly once the source is
|
|
10
|
+
* defined — exactly the class of bug a parse check cannot see.
|
|
16
11
|
*/
|
|
17
12
|
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest';
|
|
18
|
-
import {
|
|
13
|
+
import { api, everything, processNode, register } from '@hyperfixi/engine';
|
|
19
14
|
import { registerFocusTrap } from './focustrap';
|
|
20
15
|
import { registerScrollReveal } from './scrollreveal';
|
|
21
16
|
import { registerTabs } from './tabs';
|
|
22
17
|
|
|
23
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
-
const hf = hyperscript as any;
|
|
25
|
-
|
|
26
18
|
const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
|
|
27
19
|
|
|
20
|
+
/** Install from an attribute, as a page does: the engine reads `_` and runs the install. */
|
|
28
21
|
async function install(code: string, el: HTMLElement): Promise<void> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await
|
|
22
|
+
el.setAttribute('_', code);
|
|
23
|
+
processNode(el);
|
|
24
|
+
await tick();
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
beforeAll(async () => {
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await
|
|
28
|
+
// The engine is assembled from modules; this bundle is every one. Behaviors are globals
|
|
29
|
+
// (`window.Toggleable`), so define the set once per file.
|
|
30
|
+
register(...everything);
|
|
31
|
+
await registerFocusTrap(api);
|
|
32
|
+
await registerScrollReveal(api);
|
|
33
|
+
await registerTabs(api);
|
|
39
34
|
});
|
|
40
35
|
|
|
41
36
|
afterEach(() => {
|
|
@@ -13,33 +13,19 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { removableSchema } from '../schemas/removable.schema';
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
16
|
+
import { defineBehavior, resolveRuntime } from '../schemas/types';
|
|
17
|
+
import type { HyperscriptHost } from '../schemas/types';
|
|
18
18
|
|
|
19
19
|
// Re-export schema-derived values for backwards compatibility
|
|
20
20
|
export const removableSource = removableSchema.source;
|
|
21
21
|
export const removableMetadata = removableSchema;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Define the Removable behavior on a hyperscript host (`window.hyperfixi` when none is given)
|
|
25
|
+
* from its source, as a `<script type="text/hyperscript">` would.
|
|
25
26
|
*/
|
|
26
|
-
export async function registerRemovable(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!hf) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const result = hf.compileSync(removableSchema.source, { traditional: true });
|
|
36
|
-
|
|
37
|
-
if (!result.ok) {
|
|
38
|
-
throw new Error(`Failed to compile Removable behavior: ${JSON.stringify(result.errors)}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
42
|
-
await hf.execute(result.ast, ctx);
|
|
27
|
+
export async function registerRemovable(host?: HyperscriptHost): Promise<void> {
|
|
28
|
+
defineBehavior(removableSchema, host);
|
|
43
29
|
}
|
|
44
30
|
|
|
45
31
|
// Auto-register when loaded as a script tag
|