@hyperfixi/behaviors 2.0.0 → 2.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.
- package/dist/all.browser.global.js +35 -6
- package/dist/all.browser.global.js.map +1 -1
- package/dist/autodismiss.browser.global.js +3 -0
- package/dist/autodismiss.browser.global.js.map +1 -0
- package/dist/behaviors/autodismiss.cjs +177 -0
- package/dist/behaviors/autodismiss.cjs.map +1 -0
- package/dist/behaviors/autodismiss.d.cts +15 -0
- package/dist/behaviors/autodismiss.d.ts +15 -0
- package/dist/behaviors/autodismiss.js +15 -0
- package/dist/behaviors/autodismiss.js.map +1 -0
- package/dist/behaviors/clickoutside.cjs +123 -0
- package/dist/behaviors/clickoutside.cjs.map +1 -0
- package/dist/behaviors/clickoutside.d.cts +15 -0
- package/dist/behaviors/clickoutside.d.ts +15 -0
- package/dist/behaviors/clickoutside.js +15 -0
- package/dist/behaviors/clickoutside.js.map +1 -0
- package/dist/behaviors/clipboard.cjs +193 -0
- package/dist/behaviors/clipboard.cjs.map +1 -0
- package/dist/behaviors/clipboard.d.cts +15 -0
- package/dist/behaviors/clipboard.d.ts +15 -0
- package/dist/behaviors/clipboard.js +15 -0
- package/dist/behaviors/clipboard.js.map +1 -0
- package/dist/behaviors/draggable.cjs +49 -7
- package/dist/behaviors/draggable.cjs.map +1 -1
- package/dist/behaviors/draggable.d.cts +2 -1
- package/dist/behaviors/draggable.d.ts +2 -1
- package/dist/behaviors/draggable.js +2 -1
- package/dist/behaviors/focustrap.cjs +191 -0
- package/dist/behaviors/focustrap.cjs.map +1 -0
- package/dist/behaviors/focustrap.d.cts +15 -0
- package/dist/behaviors/focustrap.d.ts +15 -0
- package/dist/behaviors/focustrap.js +15 -0
- package/dist/behaviors/focustrap.js.map +1 -0
- package/dist/behaviors/removable.cjs +9 -2
- package/dist/behaviors/removable.cjs.map +1 -1
- package/dist/behaviors/removable.d.cts +1 -1
- package/dist/behaviors/removable.d.ts +1 -1
- package/dist/behaviors/removable.js +2 -1
- package/dist/behaviors/resizable.cjs +54 -7
- package/dist/behaviors/resizable.cjs.map +1 -1
- package/dist/behaviors/resizable.d.cts +1 -1
- package/dist/behaviors/resizable.d.ts +1 -1
- package/dist/behaviors/resizable.js +2 -1
- package/dist/behaviors/scrollreveal.cjs +149 -0
- package/dist/behaviors/scrollreveal.cjs.map +1 -0
- package/dist/behaviors/scrollreveal.d.cts +15 -0
- package/dist/behaviors/scrollreveal.d.ts +15 -0
- package/dist/behaviors/scrollreveal.js +15 -0
- package/dist/behaviors/scrollreveal.js.map +1 -0
- package/dist/behaviors/sortable.cjs +49 -7
- package/dist/behaviors/sortable.cjs.map +1 -1
- package/dist/behaviors/sortable.d.cts +1 -1
- package/dist/behaviors/sortable.d.ts +1 -1
- package/dist/behaviors/sortable.js +2 -1
- package/dist/behaviors/tabs.cjs +247 -0
- package/dist/behaviors/tabs.cjs.map +1 -0
- package/dist/behaviors/tabs.d.cts +15 -0
- package/dist/behaviors/tabs.d.ts +15 -0
- package/dist/behaviors/tabs.js +15 -0
- package/dist/behaviors/tabs.js.map +1 -0
- package/dist/behaviors/toggleable.cjs +36 -7
- package/dist/behaviors/toggleable.cjs.map +1 -1
- package/dist/behaviors/toggleable.d.cts +2 -1
- package/dist/behaviors/toggleable.d.ts +2 -1
- package/dist/behaviors/toggleable.js +2 -1
- package/dist/chunk-244WSZOL.js +67 -0
- package/dist/chunk-244WSZOL.js.map +1 -0
- package/dist/chunk-2NSKQMNM.js +99 -0
- package/dist/chunk-2NSKQMNM.js.map +1 -0
- package/dist/chunk-64DIQGQI.js +37 -0
- package/dist/chunk-64DIQGQI.js.map +1 -0
- package/dist/chunk-6TJXDCAQ.js +11 -0
- package/dist/chunk-6TJXDCAQ.js.map +1 -0
- package/dist/chunk-7JDV2THA.js +62 -0
- package/dist/chunk-7JDV2THA.js.map +1 -0
- package/dist/chunk-AFGA54SA.js +116 -0
- package/dist/chunk-AFGA54SA.js.map +1 -0
- package/dist/chunk-DYHBKDKB.js +62 -0
- package/dist/chunk-DYHBKDKB.js.map +1 -0
- package/dist/chunk-GPTHAUWN.js +58 -0
- package/dist/chunk-GPTHAUWN.js.map +1 -0
- package/dist/chunk-GVJPA2EK.js +68 -0
- package/dist/chunk-GVJPA2EK.js.map +1 -0
- package/dist/chunk-H2MCHR5A.js +89 -0
- package/dist/chunk-H2MCHR5A.js.map +1 -0
- package/dist/{chunk-2NQHIHD5.js → chunk-H5Y5ARYG.js} +6 -3
- package/dist/chunk-H5Y5ARYG.js.map +1 -0
- package/dist/chunk-JKBWIJFI.js +155 -0
- package/dist/chunk-JKBWIJFI.js.map +1 -0
- package/dist/chunk-QKDYW4VM.js +70 -0
- package/dist/chunk-QKDYW4VM.js.map +1 -0
- package/dist/chunk-RB7OB6OD.js +51 -0
- package/dist/chunk-RB7OB6OD.js.map +1 -0
- package/dist/chunk-RPERQTFU.js +75 -0
- package/dist/chunk-RPERQTFU.js.map +1 -0
- package/dist/chunk-S4QDV2MH.js +75 -0
- package/dist/chunk-S4QDV2MH.js.map +1 -0
- package/dist/chunk-WTUVPVSD.js +80 -0
- package/dist/chunk-WTUVPVSD.js.map +1 -0
- package/dist/chunk-YHQRFUWV.js +62 -0
- package/dist/chunk-YHQRFUWV.js.map +1 -0
- package/dist/chunk-ZQQZTMCJ.js +64 -0
- package/dist/chunk-ZQQZTMCJ.js.map +1 -0
- package/dist/clickoutside.browser.global.js +12 -0
- package/dist/clickoutside.browser.global.js.map +1 -0
- package/dist/clipboard.browser.global.js +3 -0
- package/dist/clipboard.browser.global.js.map +1 -0
- package/dist/core.browser.global.js +3 -3
- package/dist/core.browser.global.js.map +1 -1
- package/dist/draggable.browser.global.js +2 -2
- package/dist/draggable.browser.global.js.map +1 -1
- package/dist/focustrap.browser.global.js +9 -0
- package/dist/focustrap.browser.global.js.map +1 -0
- package/dist/index.cjs +1270 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +155 -48
- package/dist/index.js.map +1 -1
- package/dist/loaders.cjs +1151 -30
- package/dist/loaders.cjs.map +1 -1
- package/dist/loaders.js +9 -3
- package/dist/registry.d.cts +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/removable.browser.global.js +2 -2
- package/dist/removable.browser.global.js.map +1 -1
- package/dist/resizable.browser.global.js +2 -2
- package/dist/resizable.browser.global.js.map +1 -1
- package/dist/schemas/index.cjs +343 -1
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +45 -3
- package/dist/schemas/index.d.ts +45 -3
- package/dist/schemas/index.js +34 -4
- package/dist/schemas/index.js.map +1 -1
- package/dist/scrollreveal.browser.global.js +11 -0
- package/dist/scrollreveal.browser.global.js.map +1 -0
- package/dist/sortable.browser.global.js +2 -2
- package/dist/sortable.browser.global.js.map +1 -1
- package/dist/tabs.browser.global.js +3 -0
- package/dist/tabs.browser.global.js.map +1 -0
- package/dist/toggleable.browser.global.js +2 -2
- package/dist/toggleable.browser.global.js.map +1 -1
- package/dist/{types-CCc_2wAb.d.cts → types-DCCtVQS5.d.cts} +1 -1
- package/dist/{types-CCc_2wAb.d.ts → types-DCCtVQS5.d.ts} +1 -1
- package/package.json +45 -2
- package/src/behaviors/autodismiss.test.ts +61 -0
- package/src/behaviors/autodismiss.ts +107 -0
- package/src/behaviors/clickoutside.test.ts +52 -0
- package/src/behaviors/clickoutside.ts +77 -0
- package/src/behaviors/clipboard.test.ts +67 -0
- package/src/behaviors/clipboard.ts +130 -0
- package/src/behaviors/draggable.ts +62 -15
- package/src/behaviors/focustrap.test.ts +59 -0
- package/src/behaviors/focustrap.ts +158 -0
- package/src/behaviors/index.ts +37 -0
- package/src/behaviors/integration.test.ts +120 -45
- package/src/behaviors/removable.ts +4 -5
- package/src/behaviors/resizable.ts +63 -14
- package/src/behaviors/scrollreveal.test.ts +59 -0
- package/src/behaviors/scrollreveal.ts +78 -0
- package/src/behaviors/sortable.ts +61 -14
- package/src/behaviors/tabs.test.ts +81 -0
- package/src/behaviors/tabs.ts +196 -0
- package/src/behaviors/toggleable.ts +45 -13
- package/src/generated/metadata.ts +38 -7
- package/src/generated/types.ts +5 -5
- package/src/index.test.ts +143 -5
- package/src/index.ts +65 -13
- package/src/loaders.ts +18 -0
- package/src/registry.test.ts +26 -5
- package/src/schemas/autodismiss.schema.ts +66 -0
- package/src/schemas/clickoutside.schema.ts +39 -0
- package/src/schemas/clipboard.schema.ts +72 -0
- package/src/schemas/focustrap.schema.ts +53 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/scrollreveal.schema.ts +60 -0
- package/src/schemas/tabs.schema.ts +66 -0
- package/src/schemas/types.ts +13 -1
- package/dist/chunk-2NQHIHD5.js.map +0 -1
- package/dist/chunk-3Q652JQB.js +0 -37
- package/dist/chunk-3Q652JQB.js.map +0 -1
- package/dist/chunk-HMRPQWQU.js +0 -37
- package/dist/chunk-HMRPQWQU.js.map +0 -1
- package/dist/chunk-MEP6B7XW.js +0 -37
- package/dist/chunk-MEP6B7XW.js.map +0 -1
- package/dist/chunk-URBOGGW6.js +0 -32
- package/dist/chunk-URBOGGW6.js.map +0 -1
- package/dist/chunk-WU37DFAL.js +0 -37
- package/dist/chunk-WU37DFAL.js.map +0 -1
|
@@ -5,6 +5,12 @@ import { registerToggleable, toggleableSource } from './toggleable';
|
|
|
5
5
|
import { registerRemovable, removableSource } from './removable';
|
|
6
6
|
import { registerSortable, sortableSource } from './sortable';
|
|
7
7
|
import { registerResizable, resizableSource } from './resizable';
|
|
8
|
+
import { registerClipboard, clipboardSource } from './clipboard';
|
|
9
|
+
import { registerAutoDismiss, autoDismissSource } from './autodismiss';
|
|
10
|
+
import { registerClickOutside, clickOutsideSource } from './clickoutside';
|
|
11
|
+
import { registerFocusTrap, focusTrapSource } from './focustrap';
|
|
12
|
+
import { registerScrollReveal, scrollRevealSource } from './scrollreveal';
|
|
13
|
+
import { registerTabs, tabsSource } from './tabs';
|
|
8
14
|
|
|
9
15
|
function createMockInstance(overrides: Partial<LokaScriptInstance> = {}): LokaScriptInstance {
|
|
10
16
|
return {
|
|
@@ -15,66 +21,135 @@ function createMockInstance(overrides: Partial<LokaScriptInstance> = {}): LokaSc
|
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
// Source-compiled behaviors (use compileSync + execute)
|
|
25
|
+
const compiledBehaviors = [
|
|
26
|
+
{ name: 'Removable', register: registerRemovable, source: removableSource },
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
// Imperative behaviors (use synthetic node with imperativeInstaller)
|
|
30
|
+
const imperativeBehaviors = [
|
|
19
31
|
{ name: 'Draggable', register: registerDraggable, source: draggableSource },
|
|
20
32
|
{ name: 'Toggleable', register: registerToggleable, source: toggleableSource },
|
|
21
|
-
{ name: 'Removable', register: registerRemovable, source: removableSource },
|
|
22
33
|
{ name: 'Sortable', register: registerSortable, source: sortableSource },
|
|
23
34
|
{ name: 'Resizable', register: registerResizable, source: resizableSource },
|
|
35
|
+
{ name: 'Clipboard', register: registerClipboard, source: clipboardSource },
|
|
36
|
+
{ name: 'AutoDismiss', register: registerAutoDismiss, source: autoDismissSource },
|
|
37
|
+
{ name: 'ClickOutside', register: registerClickOutside, source: clickOutsideSource },
|
|
38
|
+
{ name: 'FocusTrap', register: registerFocusTrap, source: focusTrapSource },
|
|
39
|
+
{ name: 'ScrollReveal', register: registerScrollReveal, source: scrollRevealSource },
|
|
40
|
+
{ name: 'Tabs', register: registerTabs, source: tabsSource },
|
|
24
41
|
] as const;
|
|
25
42
|
|
|
26
43
|
describe('behavior registration integration', () => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
describe('source-compiled behaviors', () => {
|
|
45
|
+
for (const { name, register, source } of compiledBehaviors) {
|
|
46
|
+
describe(name, () => {
|
|
47
|
+
it('should compile with traditional parser', async () => {
|
|
48
|
+
const mock = createMockInstance();
|
|
49
|
+
await register(mock);
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
expect(mock.compileSync).toHaveBeenCalledWith(source, { traditional: true });
|
|
52
|
+
});
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
it('should create context and execute', async () => {
|
|
55
|
+
const mock = createMockInstance();
|
|
56
|
+
await register(mock);
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
expect(mock.createContext).toHaveBeenCalled();
|
|
59
|
+
expect(mock.execute).toHaveBeenCalledWith(
|
|
60
|
+
{ type: 'behavior' },
|
|
61
|
+
{ locals: expect.any(Map), globals: expect.any(Map) }
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should throw on compile failure', async () => {
|
|
66
|
+
const mock = createMockInstance({
|
|
67
|
+
compileSync: vi.fn().mockReturnValue({
|
|
68
|
+
ok: false,
|
|
69
|
+
errors: [{ message: 'syntax error', line: 1 }],
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
46
72
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ok: false,
|
|
51
|
-
errors: [{ message: 'syntax error', line: 1 }],
|
|
52
|
-
}),
|
|
73
|
+
await expect(register(mock)).rejects.toThrowError(
|
|
74
|
+
new RegExp(`Failed to compile ${name}`)
|
|
75
|
+
);
|
|
53
76
|
});
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
it('should throw when lokascript is not available', async () => {
|
|
79
|
+
await expect(register(undefined)).rejects.toThrowError(/LokaScript not found/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should fall back to manual context when createContext is missing', async () => {
|
|
83
|
+
const mock = createMockInstance();
|
|
84
|
+
delete (mock as Partial<LokaScriptInstance>).createContext;
|
|
85
|
+
|
|
86
|
+
await register(mock);
|
|
57
87
|
|
|
58
|
-
|
|
59
|
-
|
|
88
|
+
expect(mock.execute).toHaveBeenCalledWith(
|
|
89
|
+
{ type: 'behavior' },
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
locals: expect.any(Map),
|
|
92
|
+
globals: expect.any(Map),
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
});
|
|
60
96
|
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('imperative behaviors', () => {
|
|
101
|
+
for (const { name, register } of imperativeBehaviors) {
|
|
102
|
+
describe(name, () => {
|
|
103
|
+
it('should register via synthetic node with imperativeInstaller', async () => {
|
|
104
|
+
const mock = createMockInstance();
|
|
105
|
+
await register(mock);
|
|
106
|
+
|
|
107
|
+
// Imperative behaviors do NOT call compileSync
|
|
108
|
+
expect(mock.compileSync).not.toHaveBeenCalled();
|
|
109
|
+
|
|
110
|
+
// They call execute with a synthetic node containing imperativeInstaller
|
|
111
|
+
expect(mock.execute).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
type: 'behavior',
|
|
114
|
+
name,
|
|
115
|
+
imperativeInstaller: expect.any(Function),
|
|
116
|
+
}),
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
locals: expect.any(Map),
|
|
119
|
+
globals: expect.any(Map),
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should create context', async () => {
|
|
125
|
+
const mock = createMockInstance();
|
|
126
|
+
await register(mock);
|
|
127
|
+
expect(mock.createContext).toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should throw when lokascript is not available', async () => {
|
|
131
|
+
await expect(register(undefined)).rejects.toThrowError(/LokaScript not found/);
|
|
132
|
+
});
|
|
61
133
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
134
|
+
it('should fall back to manual context when createContext is missing', async () => {
|
|
135
|
+
const mock = createMockInstance();
|
|
136
|
+
delete (mock as Partial<LokaScriptInstance>).createContext;
|
|
137
|
+
|
|
138
|
+
await register(mock);
|
|
139
|
+
|
|
140
|
+
expect(mock.execute).toHaveBeenCalledWith(
|
|
141
|
+
expect.objectContaining({
|
|
142
|
+
type: 'behavior',
|
|
143
|
+
name,
|
|
144
|
+
imperativeInstaller: expect.any(Function),
|
|
145
|
+
}),
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
locals: expect.any(Map),
|
|
148
|
+
globals: expect.any(Map),
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
});
|
|
77
152
|
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
80
155
|
});
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { removableSchema } from '../schemas/removable.schema';
|
|
16
|
-
import type { LokaScriptInstance
|
|
16
|
+
import type { LokaScriptInstance } from '../schemas/types';
|
|
17
|
+
import { resolveRuntime } from '../schemas/types';
|
|
17
18
|
|
|
18
19
|
// Re-export schema-derived values for backwards compatibility
|
|
19
20
|
export const removableSource = removableSchema.source;
|
|
@@ -23,9 +24,7 @@ export const removableMetadata = removableSchema;
|
|
|
23
24
|
* Register the Removable behavior with LokaScript.
|
|
24
25
|
*/
|
|
25
26
|
export async function registerRemovable(hyperfixi?: LokaScriptInstance): Promise<void> {
|
|
26
|
-
const hf =
|
|
27
|
-
hyperfixi ||
|
|
28
|
-
(typeof window !== 'undefined' ? (window as unknown as LokaScriptWindow).lokascript : null);
|
|
27
|
+
const hf = hyperfixi || resolveRuntime();
|
|
29
28
|
|
|
30
29
|
if (!hf) {
|
|
31
30
|
throw new Error(
|
|
@@ -44,7 +43,7 @@ export async function registerRemovable(hyperfixi?: LokaScriptInstance): Promise
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
// Auto-register when loaded as a script tag
|
|
47
|
-
if (
|
|
46
|
+
if (resolveRuntime()) {
|
|
48
47
|
registerRemovable().catch(console.error);
|
|
49
48
|
}
|
|
50
49
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resizable Behavior
|
|
2
|
+
* Resizable Behavior — Imperative Implementation
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Makes elements resizable by dragging edges or corners.
|
|
5
5
|
* Supports minimum/maximum dimensions and resize handles.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
@@ -13,19 +13,67 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { resizableSchema } from '../schemas/resizable.schema';
|
|
16
|
-
import type { LokaScriptInstance
|
|
16
|
+
import type { LokaScriptInstance } from '../schemas/types';
|
|
17
|
+
import { resolveRuntime } from '../schemas/types';
|
|
17
18
|
|
|
18
19
|
// Re-export schema-derived values for backwards compatibility
|
|
19
20
|
export const resizableSource = resizableSchema.source;
|
|
20
21
|
export const resizableMetadata = resizableSchema;
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Imperative installer for Resizable behavior.
|
|
25
|
+
*/
|
|
26
|
+
function installResizable(element: HTMLElement, params: Record<string, any>): void {
|
|
27
|
+
// Resolve handle element
|
|
28
|
+
let handleEl: HTMLElement = element;
|
|
29
|
+
if (params.handle && params.handle !== 'me') {
|
|
30
|
+
if (typeof params.handle === 'string') {
|
|
31
|
+
const found = element.querySelector(params.handle);
|
|
32
|
+
if (found instanceof HTMLElement) handleEl = found;
|
|
33
|
+
} else if (params.handle instanceof HTMLElement) {
|
|
34
|
+
handleEl = params.handle;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const minW = params.minWidth ?? 50;
|
|
39
|
+
const minH = params.minHeight ?? 50;
|
|
40
|
+
const maxW = params.maxWidth ?? 9999;
|
|
41
|
+
const maxH = params.maxHeight ?? 9999;
|
|
42
|
+
|
|
43
|
+
handleEl.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
element.dispatchEvent(new CustomEvent('resizable:start', { bubbles: true }));
|
|
46
|
+
|
|
47
|
+
const startWidth = element.offsetWidth;
|
|
48
|
+
const startHeight = element.offsetHeight;
|
|
49
|
+
const startX = e.clientX;
|
|
50
|
+
const startY = e.clientY;
|
|
51
|
+
|
|
52
|
+
function onMove(ev: PointerEvent) {
|
|
53
|
+
const newWidth = Math.max(minW, Math.min(maxW, startWidth + (ev.clientX - startX)));
|
|
54
|
+
const newHeight = Math.max(minH, Math.min(maxH, startHeight + (ev.clientY - startY)));
|
|
55
|
+
|
|
56
|
+
element.style.width = `${newWidth}px`;
|
|
57
|
+
element.style.height = `${newHeight}px`;
|
|
58
|
+
element.dispatchEvent(new CustomEvent('resizable:resize', { bubbles: true }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onUp() {
|
|
62
|
+
document.removeEventListener('pointermove', onMove);
|
|
63
|
+
document.removeEventListener('pointerup', onUp);
|
|
64
|
+
element.dispatchEvent(new CustomEvent('resizable:end', { bubbles: true }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
document.addEventListener('pointermove', onMove);
|
|
68
|
+
document.addEventListener('pointerup', onUp);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
22
72
|
/**
|
|
23
73
|
* Register the Resizable behavior with LokaScript.
|
|
24
74
|
*/
|
|
25
75
|
export async function registerResizable(hyperfixi?: LokaScriptInstance): Promise<void> {
|
|
26
|
-
const hf =
|
|
27
|
-
hyperfixi ||
|
|
28
|
-
(typeof window !== 'undefined' ? (window as unknown as LokaScriptWindow).lokascript : null);
|
|
76
|
+
const hf = hyperfixi || resolveRuntime();
|
|
29
77
|
|
|
30
78
|
if (!hf) {
|
|
31
79
|
throw new Error(
|
|
@@ -33,18 +81,19 @@ export async function registerResizable(hyperfixi?: LokaScriptInstance): Promise
|
|
|
33
81
|
);
|
|
34
82
|
}
|
|
35
83
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
84
|
+
const syntheticNode = {
|
|
85
|
+
type: 'behavior',
|
|
86
|
+
name: 'Resizable',
|
|
87
|
+
parameters: ['handle', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
|
|
88
|
+
eventHandlers: [],
|
|
89
|
+
imperativeInstaller: installResizable,
|
|
90
|
+
};
|
|
42
91
|
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
43
|
-
await hf.execute(
|
|
92
|
+
await hf.execute(syntheticNode, ctx);
|
|
44
93
|
}
|
|
45
94
|
|
|
46
95
|
// Auto-register when loaded as a script tag
|
|
47
|
-
if (
|
|
96
|
+
if (resolveRuntime()) {
|
|
48
97
|
registerResizable().catch(console.error);
|
|
49
98
|
}
|
|
50
99
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('ScrollReveal behavior', () => {
|
|
4
|
+
describe('registerScrollReveal', () => {
|
|
5
|
+
it('should register with synthetic node via execute', async () => {
|
|
6
|
+
const { registerScrollReveal } = await import('./scrollreveal');
|
|
7
|
+
const mock = {
|
|
8
|
+
compileSync: vi.fn(),
|
|
9
|
+
execute: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
await registerScrollReveal(mock);
|
|
14
|
+
|
|
15
|
+
expect(mock.compileSync).not.toHaveBeenCalled();
|
|
16
|
+
expect(mock.execute).toHaveBeenCalledWith(
|
|
17
|
+
expect.objectContaining({
|
|
18
|
+
type: 'behavior',
|
|
19
|
+
name: 'ScrollReveal',
|
|
20
|
+
imperativeInstaller: expect.any(Function),
|
|
21
|
+
}),
|
|
22
|
+
expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should throw when no runtime available', async () => {
|
|
27
|
+
const { registerScrollReveal } = await import('./scrollreveal');
|
|
28
|
+
await expect(registerScrollReveal(undefined)).rejects.toThrowError(/LokaScript not found/);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('schema', () => {
|
|
33
|
+
it('should export valid metadata', async () => {
|
|
34
|
+
const { scrollRevealMetadata } = await import('./scrollreveal');
|
|
35
|
+
expect(scrollRevealMetadata.name).toBe('ScrollReveal');
|
|
36
|
+
expect(scrollRevealMetadata.category).toBe('layout');
|
|
37
|
+
expect(scrollRevealMetadata.tier).toBe('common');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should have source containing behavior declaration', async () => {
|
|
41
|
+
const { scrollRevealSource } = await import('./scrollreveal');
|
|
42
|
+
expect(scrollRevealSource).toContain('behavior ScrollReveal');
|
|
43
|
+
expect(scrollRevealSource).toContain('IntersectionObserver');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should document enter/exit events', async () => {
|
|
47
|
+
const { scrollRevealMetadata } = await import('./scrollreveal');
|
|
48
|
+
const eventNames = scrollRevealMetadata.events.map(e => e.name);
|
|
49
|
+
expect(eventNames).toContain('scrollreveal:enter');
|
|
50
|
+
expect(eventNames).toContain('scrollreveal:exit');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should document IntersectionObserver requirement', async () => {
|
|
54
|
+
const { scrollRevealMetadata } = await import('./scrollreveal');
|
|
55
|
+
const reqs = scrollRevealMetadata.requirements!.join(' ');
|
|
56
|
+
expect(reqs).toContain('IntersectionObserver');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollReveal Behavior — Imperative Implementation
|
|
3
|
+
*
|
|
4
|
+
* Adds a class or fires events when the element enters or exits the viewport.
|
|
5
|
+
* Uses the IntersectionObserver web standard API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { scrollRevealSchema } from '../schemas/scrollreveal.schema';
|
|
9
|
+
import type { LokaScriptInstance } from '../schemas/types';
|
|
10
|
+
import { resolveRuntime } from '../schemas/types';
|
|
11
|
+
|
|
12
|
+
// Re-export schema-derived values
|
|
13
|
+
export const scrollRevealSource = scrollRevealSchema.source;
|
|
14
|
+
export const scrollRevealMetadata = scrollRevealSchema;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Imperative installer for ScrollReveal behavior.
|
|
18
|
+
*/
|
|
19
|
+
function installScrollReveal(element: HTMLElement, params: Record<string, any>): void {
|
|
20
|
+
const cls = typeof params.cls === 'string' ? params.cls : 'revealed';
|
|
21
|
+
const threshold = typeof params.threshold === 'number' ? params.threshold : 0.1;
|
|
22
|
+
const once = params.once !== false;
|
|
23
|
+
const rootMargin = typeof params.rootMargin === 'string' ? params.rootMargin : '0px';
|
|
24
|
+
|
|
25
|
+
const observer = new IntersectionObserver(
|
|
26
|
+
entries => {
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (entry.isIntersecting) {
|
|
29
|
+
element.classList.add(cls);
|
|
30
|
+
element.dispatchEvent(new CustomEvent('scrollreveal:enter', { bubbles: true }));
|
|
31
|
+
if (once) {
|
|
32
|
+
observer.disconnect();
|
|
33
|
+
}
|
|
34
|
+
} else if (!once) {
|
|
35
|
+
element.classList.remove(cls);
|
|
36
|
+
element.dispatchEvent(new CustomEvent('scrollreveal:exit', { bubbles: true }));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{ threshold, rootMargin }
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
observer.observe(element);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register the ScrollReveal behavior with LokaScript.
|
|
48
|
+
*/
|
|
49
|
+
export async function registerScrollReveal(hyperfixi?: LokaScriptInstance): Promise<void> {
|
|
50
|
+
const hf = hyperfixi || resolveRuntime();
|
|
51
|
+
|
|
52
|
+
if (!hf) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const syntheticNode = {
|
|
59
|
+
type: 'behavior',
|
|
60
|
+
name: 'ScrollReveal',
|
|
61
|
+
parameters: ['cls', 'threshold', 'once', 'rootMargin'],
|
|
62
|
+
eventHandlers: [],
|
|
63
|
+
imperativeInstaller: installScrollReveal,
|
|
64
|
+
};
|
|
65
|
+
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
66
|
+
await hf.execute(syntheticNode, ctx);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Auto-register when loaded as a script tag
|
|
70
|
+
if (resolveRuntime()) {
|
|
71
|
+
registerScrollReveal().catch(console.error);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default {
|
|
75
|
+
source: scrollRevealSchema.source,
|
|
76
|
+
metadata: scrollRevealSchema,
|
|
77
|
+
register: registerScrollReveal,
|
|
78
|
+
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sortable Behavior
|
|
2
|
+
* Sortable Behavior — Imperative Implementation
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Drag-and-drop reordering of list items.
|
|
5
5
|
* Apply to a container element; children become sortable.
|
|
6
6
|
*
|
|
7
|
+
* Note: This behavior fires lifecycle events but does NOT automatically
|
|
8
|
+
* reorder DOM elements. Users must handle actual reordering in their
|
|
9
|
+
* `sortable:move` event handlers.
|
|
10
|
+
*
|
|
7
11
|
* @example
|
|
8
12
|
* ```html
|
|
9
13
|
* <ul _="install Sortable">
|
|
@@ -14,19 +18,61 @@
|
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { sortableSchema } from '../schemas/sortable.schema';
|
|
17
|
-
import type { LokaScriptInstance
|
|
21
|
+
import type { LokaScriptInstance } from '../schemas/types';
|
|
22
|
+
import { resolveRuntime } from '../schemas/types';
|
|
18
23
|
|
|
19
24
|
// Re-export schema-derived values for backwards compatibility
|
|
20
25
|
export const sortableSource = sortableSchema.source;
|
|
21
26
|
export const sortableMetadata = sortableSchema;
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Imperative installer for Sortable behavior.
|
|
30
|
+
*/
|
|
31
|
+
function installSortable(element: HTMLElement, params: Record<string, any>): void {
|
|
32
|
+
const dragClass = params.dragClass || 'sorting';
|
|
33
|
+
const handleSelector = params.handle || null;
|
|
34
|
+
|
|
35
|
+
element.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
36
|
+
const target = e.target as HTMLElement;
|
|
37
|
+
const item = target.closest('li');
|
|
38
|
+
if (!item || !element.contains(item)) return;
|
|
39
|
+
|
|
40
|
+
// If a handle selector is specified, only start drag from the handle
|
|
41
|
+
if (handleSelector) {
|
|
42
|
+
const handleEl = target.closest(handleSelector);
|
|
43
|
+
if (!handleEl) return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
item.classList.add(dragClass);
|
|
48
|
+
element.dispatchEvent(new CustomEvent('sortable:start', { bubbles: true, detail: { item } }));
|
|
49
|
+
|
|
50
|
+
function onMove(ev: PointerEvent) {
|
|
51
|
+
element.dispatchEvent(
|
|
52
|
+
new CustomEvent('sortable:move', {
|
|
53
|
+
bubbles: true,
|
|
54
|
+
detail: { clientY: ev.clientY, item },
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function onUp() {
|
|
60
|
+
document.removeEventListener('pointermove', onMove);
|
|
61
|
+
document.removeEventListener('pointerup', onUp);
|
|
62
|
+
item!.classList.remove(dragClass);
|
|
63
|
+
element.dispatchEvent(new CustomEvent('sortable:end', { bubbles: true, detail: { item } }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
document.addEventListener('pointermove', onMove);
|
|
67
|
+
document.addEventListener('pointerup', onUp);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
23
71
|
/**
|
|
24
72
|
* Register the Sortable behavior with LokaScript.
|
|
25
73
|
*/
|
|
26
74
|
export async function registerSortable(hyperfixi?: LokaScriptInstance): Promise<void> {
|
|
27
|
-
const hf =
|
|
28
|
-
hyperfixi ||
|
|
29
|
-
(typeof window !== 'undefined' ? (window as unknown as LokaScriptWindow).lokascript : null);
|
|
75
|
+
const hf = hyperfixi || resolveRuntime();
|
|
30
76
|
|
|
31
77
|
if (!hf) {
|
|
32
78
|
throw new Error(
|
|
@@ -34,18 +80,19 @@ export async function registerSortable(hyperfixi?: LokaScriptInstance): Promise<
|
|
|
34
80
|
);
|
|
35
81
|
}
|
|
36
82
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
83
|
+
const syntheticNode = {
|
|
84
|
+
type: 'behavior',
|
|
85
|
+
name: 'Sortable',
|
|
86
|
+
parameters: ['handle', 'dragClass'],
|
|
87
|
+
eventHandlers: [],
|
|
88
|
+
imperativeInstaller: installSortable,
|
|
89
|
+
};
|
|
43
90
|
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
44
|
-
await hf.execute(
|
|
91
|
+
await hf.execute(syntheticNode, ctx);
|
|
45
92
|
}
|
|
46
93
|
|
|
47
94
|
// Auto-register when loaded as a script tag
|
|
48
|
-
if (
|
|
95
|
+
if (resolveRuntime()) {
|
|
49
96
|
registerSortable().catch(console.error);
|
|
50
97
|
}
|
|
51
98
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('Tabs behavior', () => {
|
|
4
|
+
describe('registerTabs', () => {
|
|
5
|
+
it('should register with synthetic node via execute', async () => {
|
|
6
|
+
const { registerTabs } = await import('./tabs');
|
|
7
|
+
const mock = {
|
|
8
|
+
compileSync: vi.fn(),
|
|
9
|
+
execute: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
await registerTabs(mock);
|
|
14
|
+
|
|
15
|
+
expect(mock.compileSync).not.toHaveBeenCalled();
|
|
16
|
+
expect(mock.execute).toHaveBeenCalledWith(
|
|
17
|
+
expect.objectContaining({
|
|
18
|
+
type: 'behavior',
|
|
19
|
+
name: 'Tabs',
|
|
20
|
+
imperativeInstaller: expect.any(Function),
|
|
21
|
+
}),
|
|
22
|
+
expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should throw when no runtime available', async () => {
|
|
27
|
+
const { registerTabs } = await import('./tabs');
|
|
28
|
+
await expect(registerTabs(undefined)).rejects.toThrowError(/LokaScript not found/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should fall back to manual context when createContext is missing', async () => {
|
|
32
|
+
const { registerTabs } = await import('./tabs');
|
|
33
|
+
const mock = {
|
|
34
|
+
compileSync: vi.fn(),
|
|
35
|
+
execute: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await registerTabs(mock as any);
|
|
39
|
+
expect(mock.execute).toHaveBeenCalledWith(
|
|
40
|
+
expect.anything(),
|
|
41
|
+
expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('schema', () => {
|
|
47
|
+
it('should export valid metadata', async () => {
|
|
48
|
+
const { tabsMetadata } = await import('./tabs');
|
|
49
|
+
expect(tabsMetadata.name).toBe('Tabs');
|
|
50
|
+
expect(tabsMetadata.category).toBe('ui');
|
|
51
|
+
expect(tabsMetadata.tier).toBe('core');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should have source containing behavior declaration', async () => {
|
|
55
|
+
const { tabsSource } = await import('./tabs');
|
|
56
|
+
expect(tabsSource).toContain('behavior Tabs');
|
|
57
|
+
expect(tabsSource).toContain('orientation');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should document tabs:change and tabs:changed events', async () => {
|
|
61
|
+
const { tabsMetadata } = await import('./tabs');
|
|
62
|
+
const eventNames = tabsMetadata.events.map(e => e.name);
|
|
63
|
+
expect(eventNames).toContain('tabs:change');
|
|
64
|
+
expect(eventNames).toContain('tabs:changed');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should document ARIA requirements', async () => {
|
|
68
|
+
const { tabsMetadata } = await import('./tabs');
|
|
69
|
+
expect(tabsMetadata.requirements).toBeDefined();
|
|
70
|
+
const reqs = tabsMetadata.requirements!.join(' ');
|
|
71
|
+
expect(reqs).toContain('aria-selected');
|
|
72
|
+
expect(reqs).toContain('tabindex');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should have orientation enum parameter', async () => {
|
|
76
|
+
const { tabsMetadata } = await import('./tabs');
|
|
77
|
+
const orientationParam = tabsMetadata.parameters.find(p => p.name === 'orientation');
|
|
78
|
+
expect(orientationParam?.enum).toEqual(['horizontal', 'vertical']);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|