native-document 1.0.182 → 1.0.183
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/package.json +1 -1
- package/src/components/menu/types/Menu.d.ts +43 -25
- package/src/components/menu/types/MenuGroup.d.ts +48 -24
- package/src/components/menu/types/MenuItem.d.ts +55 -23
- package/src/components/menu/types/MenuLink.d.ts +18 -8
- package/src/core/elements/anchor/anchor.js +1 -1
- package/src/core/elements/control/for-each-array-cache.js +2 -0
- package/src/core/wrappers/AttributesWrapper.js +13 -4
- package/src/core/wrappers/ElementCreator.js +1 -1
- package/src/core/wrappers/NdPrototype.js +41 -8
- package/src/core/wrappers/template-cloner/NodeCloner.js +16 -7
- package/src/core/wrappers/template-cloner/TemplateCloner.js +144 -5
- package/src/ui/components/form/fields/SelectFieldRender.js +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "native-document",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.183",
|
|
4
4
|
"description": "A reactive JavaScript framework that preserves native DOM simplicity without sacrificing modern features",
|
|
5
5
|
"author": "AfroCodeur <https://github.com/afrocodeur>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import type { ValidChild } from '../../../../types/elements';
|
|
2
2
|
import type { ObservableItem } from '../../../../types/observable';
|
|
3
3
|
import type { BaseComponent } from '../../BaseComponent';
|
|
4
|
-
import type { MenuItemInterface } from './MenuItem';
|
|
4
|
+
import type { MenuItemInterface, MenuItemOptions } from './MenuItem';
|
|
5
5
|
import type { MenuGroupInterface } from './MenuGroup';
|
|
6
|
-
import type { MenuLinkInterface } from './MenuLink';
|
|
6
|
+
import type { MenuLinkInterface, MenuLinkOptions } from './MenuLink';
|
|
7
7
|
import type { MenuDividerInterface } from './MenuDivider';
|
|
8
8
|
import type { GlobalAttributes } from '../../../../types/globals';
|
|
9
9
|
|
|
10
10
|
export type MenuDescription = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
items: ObservableItem<Array<MenuItemInterface | MenuGroupInterface | MenuDividerInterface>>;
|
|
12
|
+
render: ((desc: MenuDescription, instance: MenuInterface) => ValidChild) | null;
|
|
13
|
+
orientation: 'horizontal' | 'vertical' | 'inline';
|
|
14
|
+
closeOnSelect: boolean;
|
|
15
|
+
keyboardLoop: boolean;
|
|
16
|
+
activeItem: ObservableItem<MenuItemInterface | null>;
|
|
17
|
+
/** Observable tracking which item currently has an open submenu. */
|
|
18
|
+
menuActive: ObservableItem<MenuItemInterface | null>;
|
|
19
|
+
/** True once the user has clicked to activate hover-open behaviour. */
|
|
20
|
+
isMenuActivated: ObservableItem<boolean>;
|
|
21
|
+
/** Null until ResizeObserver fires; then true when below compactThreshold. */
|
|
22
|
+
compact: ObservableItem<boolean | null>;
|
|
23
|
+
active: ((item: MenuItemInterface) => boolean) | null;
|
|
24
|
+
compactThreshold: number;
|
|
25
|
+
clickFirst: boolean;
|
|
26
|
+
props: GlobalAttributes;
|
|
27
|
+
};
|
|
22
28
|
|
|
23
29
|
export interface MenuInterface extends BaseComponent {
|
|
24
30
|
dataResolver(resolver: (data: unknown) => unknown): this;
|
|
@@ -33,28 +39,40 @@ export interface MenuInterface extends BaseComponent {
|
|
|
33
39
|
active(callback: (item: MenuItemInterface) => boolean): this;
|
|
34
40
|
onItemClick(handler: (item: MenuItemInterface, event: MouseEvent) => void): this;
|
|
35
41
|
onItemSelect(handler: (item: MenuItemInterface) => void): this;
|
|
36
|
-
group(label: ValidChild, icon: ValidChild, builder: (group: MenuGroupInterface) => void, props?:
|
|
42
|
+
group(label: ValidChild, icon: ValidChild, builder: (group: MenuGroupInterface) => void, props?: GlobalAttributes): this;
|
|
37
43
|
getItem(key: string): MenuItemInterface | undefined;
|
|
38
44
|
compactThreshold(width?: number): this;
|
|
39
45
|
clickFirst(mode?: boolean): this;
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
|
|
47
|
+
item(label: ValidChild): this;
|
|
48
|
+
item(label: ValidChild, configBuilder: (item: MenuItemInterface) => void): this;
|
|
49
|
+
item(label: ValidChild, options: MenuItemOptions): this;
|
|
50
|
+
item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void): this;
|
|
51
|
+
item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void, props: GlobalAttributes): this;
|
|
52
|
+
|
|
53
|
+
link(label: ValidChild): this;
|
|
54
|
+
link(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
55
|
+
link(label: ValidChild, options: MenuLinkOptions): this;
|
|
56
|
+
link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
57
|
+
link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
|
|
58
|
+
|
|
59
|
+
linkTo(label: ValidChild): this;
|
|
60
|
+
linkTo(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
61
|
+
linkTo(label: ValidChild, options: MenuLinkOptions): this;
|
|
62
|
+
linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
63
|
+
linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
|
|
64
|
+
|
|
42
65
|
separator(): this;
|
|
43
66
|
divider(): this;
|
|
44
67
|
add(item: MenuItemInterface | MenuGroupInterface | MenuLinkInterface | MenuDividerInterface): this;
|
|
45
68
|
getDepth(): number;
|
|
46
|
-
getRoot():
|
|
69
|
+
getRoot(): MenuInterface;
|
|
70
|
+
setParent(parent: MenuInterface | MenuGroupInterface): this;
|
|
47
71
|
onClicked(): this;
|
|
48
72
|
onHovered(): this;
|
|
49
|
-
render(template: (description: MenuDescription, instance: MenuInterface) => ValidChild): this;
|
|
50
73
|
}
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
export declare function Menu(props?: Record<string, unknown>): MenuInterface;
|
|
75
|
+
export declare function Menu(props?: GlobalAttributes): MenuInterface;
|
|
54
76
|
export declare namespace Menu {
|
|
55
|
-
|
|
56
|
-
|
|
57
77
|
function use(template: (description: MenuDescription, instance: MenuInterface) => ValidChild): void;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
78
|
+
}
|
|
@@ -1,44 +1,68 @@
|
|
|
1
1
|
import type { ValidChild } from '../../../../types/elements';
|
|
2
2
|
import type { ObservableItem } from '../../../../types/observable';
|
|
3
3
|
import type { BaseComponent } from '../../BaseComponent';
|
|
4
|
-
import type { MenuItemInterface } from './MenuItem';
|
|
4
|
+
import type { MenuItemInterface, MenuItemOptions } from './MenuItem';
|
|
5
|
+
import type { MenuLinkInterface, MenuLinkOptions } from './MenuLink';
|
|
5
6
|
import type { MenuDividerInterface } from './MenuDivider';
|
|
6
7
|
import type { GlobalAttributes } from '../../../../types/globals';
|
|
7
8
|
|
|
8
9
|
export type MenuGroupDescription = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
icon: ValidChild | null;
|
|
11
|
+
label: ValidChild;
|
|
12
|
+
data: unknown | null;
|
|
13
|
+
items: ObservableItem<Array<MenuItemInterface | MenuDividerInterface>>;
|
|
14
|
+
render: ((desc: MenuGroupDescription, instance: MenuGroupInterface) => ValidChild) | null;
|
|
15
|
+
collapsable: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Null until .collapsable() is called — then an ObservableItem<boolean>.
|
|
18
|
+
* The render uses .toggle() and .transform() on this value; it is never
|
|
19
|
+
* a plain boolean at runtime.
|
|
20
|
+
*/
|
|
21
|
+
collapsed: ObservableItem<boolean> | null;
|
|
22
|
+
visibility: ObservableItem<boolean>;
|
|
23
|
+
collapsableOpenedIcon: ValidChild | null;
|
|
24
|
+
collapsableClosedIcon: ValidChild | null;
|
|
25
|
+
props: GlobalAttributes;
|
|
26
|
+
};
|
|
21
27
|
|
|
22
28
|
export interface MenuGroupInterface extends BaseComponent {
|
|
23
29
|
data(data: unknown): this;
|
|
24
30
|
icon(icon: ValidChild): this;
|
|
25
31
|
collapsable(mode?: boolean, openedIcon?: ValidChild, closedIcon?: ValidChild): this;
|
|
26
32
|
collapsed(mode?: boolean): this;
|
|
27
|
-
divider(): this;
|
|
28
33
|
visibility(mode: boolean | ObservableItem<boolean>): this;
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
item(label: ValidChild): this;
|
|
36
|
+
item(label: ValidChild, configBuilder: (item: MenuItemInterface) => void): this;
|
|
37
|
+
item(label: ValidChild, options: MenuItemOptions): this;
|
|
38
|
+
item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void): this;
|
|
39
|
+
item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void, props: GlobalAttributes): this;
|
|
40
|
+
|
|
41
|
+
link(label: ValidChild): this;
|
|
42
|
+
link(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
43
|
+
link(label: ValidChild, options: MenuLinkOptions): this;
|
|
44
|
+
link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
45
|
+
link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
|
|
46
|
+
|
|
47
|
+
linkTo(label: ValidChild): this;
|
|
48
|
+
linkTo(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
49
|
+
linkTo(label: ValidChild, options: MenuLinkOptions): this;
|
|
50
|
+
linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
|
|
51
|
+
linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
|
|
52
|
+
|
|
53
|
+
group(label: ValidChild, builder: (group: MenuGroupInterface) => void): this;
|
|
31
54
|
separator(): this;
|
|
32
|
-
|
|
55
|
+
divider(): this;
|
|
56
|
+
add(item: MenuItemInterface | MenuGroupInterface | MenuLinkInterface | MenuDividerInterface): this;
|
|
57
|
+
getDepth(): number;
|
|
58
|
+
getRoot(): unknown;
|
|
59
|
+
setParent(parent: unknown): this;
|
|
60
|
+
onClicked(): this;
|
|
61
|
+
onHovered(): this;
|
|
33
62
|
render(template: (description: MenuGroupDescription, instance: MenuGroupInterface) => ValidChild): this;
|
|
34
63
|
}
|
|
35
64
|
|
|
36
|
-
|
|
37
|
-
export declare function MenuGroup(label: ValidChild, props?: Record<string, unknown>): MenuGroupInterface;
|
|
65
|
+
export declare function MenuGroup(label: ValidChild, props?: GlobalAttributes): MenuGroupInterface;
|
|
38
66
|
export declare namespace MenuGroup {
|
|
39
|
-
|
|
40
|
-
|
|
41
67
|
function use(template: (description: MenuGroupDescription, instance: MenuGroupInterface) => ValidChild): void;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
68
|
+
}
|
|
@@ -3,21 +3,30 @@ import type { ObservableItem } from '../../../../types/observable';
|
|
|
3
3
|
import type { BaseComponent } from '../../BaseComponent';
|
|
4
4
|
import type { GlobalAttributes } from '../../../../types/globals';
|
|
5
5
|
|
|
6
|
+
export type MenuItemOptions = {
|
|
7
|
+
icon?: ValidChild;
|
|
8
|
+
action?: string | Record<string, unknown> | ((data?: unknown) => void);
|
|
9
|
+
shortcut?: ValidChild;
|
|
10
|
+
disabled?: boolean | ObservableItem<boolean>;
|
|
11
|
+
};
|
|
12
|
+
|
|
6
13
|
export type MenuItemDescription = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
key: string | null;
|
|
15
|
+
action: string | Record<string, unknown> | ((data?: unknown) => void) | null;
|
|
16
|
+
label: ValidChild | null;
|
|
17
|
+
icon: ValidChild | null;
|
|
18
|
+
shortcut: ValidChild | null;
|
|
19
|
+
disabled: ObservableItem<boolean> | boolean | null;
|
|
20
|
+
selected: ObservableItem<boolean> | boolean | null;
|
|
21
|
+
value: unknown;
|
|
22
|
+
data: unknown | null;
|
|
23
|
+
render: ((desc: MenuItemDescription, instance: MenuItemInterface) => ValidChild) | null;
|
|
24
|
+
trailing: ValidChild | null;
|
|
25
|
+
visibility: ObservableItem<boolean> | null;
|
|
26
|
+
dataResolver: (() => unknown) | null;
|
|
27
|
+
interaction: 'click' | 'hover' | null;
|
|
28
|
+
props: GlobalAttributes;
|
|
29
|
+
};
|
|
21
30
|
|
|
22
31
|
export interface MenuItemInterface extends BaseComponent {
|
|
23
32
|
label(label: ValidChild): this;
|
|
@@ -27,20 +36,43 @@ export interface MenuItemInterface extends BaseComponent {
|
|
|
27
36
|
disabled(disabled?: boolean | ObservableItem<boolean>): this;
|
|
28
37
|
selected(selected?: boolean | ObservableItem<boolean>): this;
|
|
29
38
|
value(value: unknown): this;
|
|
30
|
-
action(action: string | Record<string, unknown>): this;
|
|
39
|
+
action(action: string | Record<string, unknown> | ((data?: unknown) => void)): this;
|
|
31
40
|
data(data: unknown): this;
|
|
32
|
-
divider(): this;
|
|
33
41
|
key(key: string): this;
|
|
34
42
|
visibility(mode: boolean | ObservableItem<boolean>): this;
|
|
35
|
-
render(template: (description: MenuItemDescription, instance: MenuItemInterface) => ValidChild): this;
|
|
36
|
-
}
|
|
37
43
|
|
|
44
|
+
item(label: ValidChild): this;
|
|
45
|
+
item(label: ValidChild, configBuilder: (item: MenuItemInterface) => void): this;
|
|
46
|
+
item(label: ValidChild, options: MenuItemOptions): this;
|
|
47
|
+
item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void): this;
|
|
48
|
+
item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void, props: GlobalAttributes): this;
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
link(label: ValidChild): this;
|
|
51
|
+
link(label: ValidChild, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
|
|
52
|
+
link(label: ValidChild, options: import('./MenuLink').MenuLinkOptions): this;
|
|
53
|
+
link(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
|
|
54
|
+
link(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void, props: GlobalAttributes): this;
|
|
44
55
|
|
|
56
|
+
linkTo(label: ValidChild): this;
|
|
57
|
+
linkTo(label: ValidChild, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
|
|
58
|
+
linkTo(label: ValidChild, options: import('./MenuLink').MenuLinkOptions): this;
|
|
59
|
+
linkTo(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
|
|
60
|
+
linkTo(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void, props: GlobalAttributes): this;
|
|
45
61
|
|
|
62
|
+
group(label: ValidChild, builder: (group: unknown) => void): this;
|
|
63
|
+
separator(): this;
|
|
64
|
+
divider(): this;
|
|
65
|
+
add(item: unknown): this;
|
|
66
|
+
getDepth(): number;
|
|
67
|
+
getRoot(): unknown;
|
|
68
|
+
setParent(parent: unknown): this;
|
|
69
|
+
onClicked(): this;
|
|
70
|
+
onHovered(): this;
|
|
71
|
+
emit(eventName: string, ...args: unknown[]): void;
|
|
72
|
+
render(template: (description: MenuItemDescription, instance: MenuItemInterface) => ValidChild): this;
|
|
46
73
|
}
|
|
74
|
+
|
|
75
|
+
export declare function MenuItem(props?: GlobalAttributes): MenuItemInterface;
|
|
76
|
+
export declare namespace MenuItem {
|
|
77
|
+
function use(template: (description: MenuItemDescription, instance: MenuItemInterface) => ValidChild): void;
|
|
78
|
+
}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import type { ValidChild } from '../../../../types/elements';
|
|
2
|
+
import type { ObservableItem } from '../../../../types/observable';
|
|
2
3
|
import type { MenuItemInterface, MenuItemDescription } from './MenuItem';
|
|
4
|
+
import type { GlobalAttributes } from '../../../../types/globals';
|
|
5
|
+
|
|
6
|
+
export type MenuLinkOptions = {
|
|
7
|
+
icon?: ValidChild;
|
|
8
|
+
/** URL string or named route name. Used as-is for link(), wrapped in { isRoute: true } for linkTo(). */
|
|
9
|
+
href?: string;
|
|
10
|
+
target?: '_blank' | '_self' | '_parent' | '_top' | string;
|
|
11
|
+
shortcut?: ValidChild;
|
|
12
|
+
disabled?: boolean | ObservableItem<boolean>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type MenuLinkDescription = MenuItemDescription & {
|
|
16
|
+
target: '_blank' | '_self' | '_parent' | '_top' | string | null;
|
|
17
|
+
};
|
|
3
18
|
|
|
4
19
|
export interface MenuLinkInterface extends MenuItemInterface {
|
|
5
20
|
target(target: '_blank' | '_self' | '_parent' | '_top' | string): this;
|
|
6
21
|
}
|
|
7
22
|
|
|
8
|
-
|
|
9
|
-
export declare function MenuLink(props?: Record<string, unknown>): MenuLinkInterface;
|
|
23
|
+
export declare function MenuLink(props?: GlobalAttributes): MenuLinkInterface;
|
|
10
24
|
export declare namespace MenuLink {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
function use(template: (description: MenuItemDescription, instance: MenuLinkInterface) => ValidChild): void;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
25
|
+
function use(template: (description: MenuLinkDescription, instance: MenuLinkInterface) => ValidChild): void;
|
|
26
|
+
}
|
|
@@ -35,7 +35,7 @@ export default function Anchor(name, isUniqueChild = false) {
|
|
|
35
35
|
? () => true: (parent) => (parent.firstChild === anchorStart && parent.lastChild === anchorEnd);
|
|
36
36
|
|
|
37
37
|
const insertBefore = (parent, child, target) => {
|
|
38
|
-
const childElement =
|
|
38
|
+
const childElement = ElementCreator.getChild(child);
|
|
39
39
|
insertBeforeRaw(parent, childElement, target);
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -10,6 +10,8 @@ export default function ForEachArrayCache(isIndexesRequired) {
|
|
|
10
10
|
return Array.from(this.$nodes.keys());
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
this.entries = this.$nodes.entries.bind(this.$nodes);
|
|
14
|
+
|
|
13
15
|
if(isIndexesRequired) {
|
|
14
16
|
this.delete = function(item) {
|
|
15
17
|
this.$nodes.get(item)?.nd.destroy();
|
|
@@ -35,7 +35,7 @@ export const bindClassAttribute = (element, data) => {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (value.$hydrate) {
|
|
38
|
-
value.$hydrate(element, className);
|
|
38
|
+
value.$hydrate(element, className, 'class');
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -85,6 +85,10 @@ export const bindStyleAttribute = (element, data) => {
|
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
if(value.$hydrate) {
|
|
89
|
+
value.$hydrate(element, styleName, 'style');
|
|
90
|
+
}
|
|
91
|
+
|
|
88
92
|
element.style[styleName] = value;
|
|
89
93
|
}
|
|
90
94
|
};
|
|
@@ -167,6 +171,10 @@ const AttributesWrapper = (element, attributes = {}) => {
|
|
|
167
171
|
element.setAttribute(attributeName, value);
|
|
168
172
|
continue;
|
|
169
173
|
}
|
|
174
|
+
if(value.$hydrate) {
|
|
175
|
+
value.$hydrate(element, attributeName, 'attribute');
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
170
178
|
// const attributeName = originalAttributeName.toLowerCase();
|
|
171
179
|
if(value.__$Observable) {
|
|
172
180
|
if(BOOLEAN_ATTRIBUTES.has(attributeName)) {
|
|
@@ -190,9 +198,10 @@ const AttributesWrapper = (element, attributes = {}) => {
|
|
|
190
198
|
bindBooleanAttribute(element, attributeName, value);
|
|
191
199
|
continue;
|
|
192
200
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
|
|
202
|
+
// if(value.__$isTemplateBinding) {
|
|
203
|
+
// // value.$hydrate(element, attributeName, 'attribute');
|
|
204
|
+
// }
|
|
196
205
|
|
|
197
206
|
element.setAttribute(attributeName, value);
|
|
198
207
|
}
|
|
@@ -28,6 +28,10 @@ EVENTS.forEach(eventSourceName => {
|
|
|
28
28
|
const inlineHandler = 'on'+eventName;
|
|
29
29
|
const fnName = 'on'+eventSourceName;
|
|
30
30
|
NDElement.prototype[fnName] = function(callback = null, options = null) {
|
|
31
|
+
if(callback.__$isTemplateBinding) {
|
|
32
|
+
this.attach(fnName, callback);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
31
35
|
if(!this.$element[inlineHandler] && !options) {
|
|
32
36
|
this.$element[inlineHandler] = callback;
|
|
33
37
|
return this;
|
|
@@ -37,37 +41,58 @@ EVENTS.forEach(eventSourceName => {
|
|
|
37
41
|
};
|
|
38
42
|
if(!HTMLElement.prototype[fnName]) {
|
|
39
43
|
HTMLElement.prototype[fnName] = function(callback, options) {
|
|
44
|
+
if(callback.__$isTemplateBinding) {
|
|
45
|
+
this.nd.attach(fnName, callback);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
40
48
|
if(!this[inlineHandler] && !options) {
|
|
41
49
|
this[inlineHandler] = callback;
|
|
42
50
|
return this;
|
|
43
51
|
}
|
|
44
52
|
this.addEventListener(eventName, callback, options);
|
|
45
53
|
return this;
|
|
46
|
-
}
|
|
54
|
+
};
|
|
47
55
|
}
|
|
48
56
|
});
|
|
49
57
|
|
|
50
58
|
EVENTS_WITH_STOP.forEach(eventSourceName => {
|
|
51
59
|
const eventName = eventSourceName.toLowerCase();
|
|
52
60
|
const stopFnName = 'onStop'+eventSourceName;
|
|
53
|
-
const preventStopFnName = 'onPreventStop'+eventSourceName;
|
|
54
61
|
|
|
55
62
|
NDElement.prototype[stopFnName] = function(callback = null, options = null) {
|
|
63
|
+
if(callback.__$isTemplateBinding) {
|
|
64
|
+
this.attach(stopFnName, callback);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
56
67
|
_stop(this.$element, eventName, callback, options);
|
|
57
68
|
return this;
|
|
58
69
|
};
|
|
59
|
-
|
|
60
|
-
_preventStop(this.$element, eventName, callback, options);
|
|
61
|
-
return this;
|
|
62
|
-
};
|
|
63
|
-
if(HTMLElement.prototype[stopFnName]) {
|
|
70
|
+
if(!HTMLElement.prototype[stopFnName]) {
|
|
64
71
|
HTMLElement.prototype[stopFnName] = function(callback = null, options = null) {
|
|
72
|
+
if(callback.__$isTemplateBinding) {
|
|
73
|
+
this.nd.attach(stopFnName, callback);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
65
76
|
_stop(this, eventName, callback, options);
|
|
66
77
|
return this;
|
|
67
78
|
};
|
|
68
79
|
}
|
|
80
|
+
// ------
|
|
81
|
+
const preventStopFnName = 'onPreventStop'+eventSourceName;
|
|
82
|
+
NDElement.prototype[preventStopFnName] = function(callback = null, options = null) {
|
|
83
|
+
if(callback.__$isTemplateBinding) {
|
|
84
|
+
this.attach(preventStopFnName, callback);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
_preventStop(this.$element, eventName, callback, options);
|
|
88
|
+
return this;
|
|
89
|
+
};
|
|
69
90
|
if(!HTMLElement.prototype[preventStopFnName]) {
|
|
70
91
|
HTMLElement.prototype[preventStopFnName] = function(callback = null, options = null) {
|
|
92
|
+
if(callback.__$isTemplateBinding) {
|
|
93
|
+
this.nd.attach(preventStopFnName, callback);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
71
96
|
_preventStop(this, eventName, callback, options);
|
|
72
97
|
return this;
|
|
73
98
|
};
|
|
@@ -78,11 +103,19 @@ EVENTS_WITH_PREVENT.forEach(eventSourceName => {
|
|
|
78
103
|
const eventName = eventSourceName.toLowerCase();
|
|
79
104
|
const preventFnName = 'onPrevent'+eventSourceName;
|
|
80
105
|
NDElement.prototype[preventFnName] = function(callback = null, options = null) {
|
|
106
|
+
if(callback.__$isTemplateBinding) {
|
|
107
|
+
this.attach(preventFnName, callback);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
81
110
|
_prevent(this.$element, eventName, callback, options);
|
|
82
111
|
return this;
|
|
83
112
|
};
|
|
84
|
-
if(HTMLElement.prototype[preventFnName]) {
|
|
113
|
+
if(!HTMLElement.prototype[preventFnName]) {
|
|
85
114
|
HTMLElement.prototype[preventFnName] = function(callback = null, options = null) {
|
|
115
|
+
if(callback.__$isTemplateBinding) {
|
|
116
|
+
this.nd.attach(preventFnName, callback);
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
86
119
|
_prevent(this, eventName, callback, options);
|
|
87
120
|
return this;
|
|
88
121
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {ElementCreator} from '../ElementCreator';
|
|
2
2
|
import {createTextNode} from '../HtmlElementWrapper';
|
|
3
3
|
import {NDElement} from '../NDElement';
|
|
4
|
+
import {call} from '@babel/traverse/lib/path/context';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Stores deferred attribute, class, style, and event bindings for a cloneable element.
|
|
@@ -49,11 +50,17 @@ NodeCloner.prototype.__$isNodeCloner = true;
|
|
|
49
50
|
|
|
50
51
|
const buildProperties = (cache, properties, data) => {
|
|
51
52
|
for(const key in properties) {
|
|
52
|
-
|
|
53
|
+
const value = properties[key];
|
|
54
|
+
cache[key] = getPropertyValue(value, data);
|
|
53
55
|
}
|
|
54
56
|
return cache;
|
|
55
57
|
};
|
|
56
58
|
|
|
59
|
+
const getPropertyValue = (callbackOrProperty, data) => {
|
|
60
|
+
const value = (typeof callbackOrProperty ==='string') ? data[0][callbackOrProperty] : callbackOrProperty;
|
|
61
|
+
return (typeof value === 'function') ? value.apply(this, data) : value;
|
|
62
|
+
};
|
|
63
|
+
|
|
57
64
|
/**
|
|
58
65
|
* Pre-compiles all registered bindings into a sequence of optimised steps.
|
|
59
66
|
* Called once before the first clone operation. Subsequent calls are no-ops.
|
|
@@ -71,13 +78,14 @@ NodeCloner.prototype.resolve = function() {
|
|
|
71
78
|
const methodName = methods[0];
|
|
72
79
|
const callback = this.$ndMethods[methodName];
|
|
73
80
|
steps.push((clonedNode, data) => {
|
|
74
|
-
clonedNode.nd[methodName](callback.bind(clonedNode, ...data));
|
|
81
|
+
clonedNode.nd[methodName](callback.length === 0 ? callback : callback.bind(clonedNode, ...data));
|
|
75
82
|
});
|
|
76
83
|
} else {
|
|
77
84
|
steps.push((clonedNode, data) => {
|
|
78
85
|
const nd = clonedNode.nd;
|
|
79
86
|
for(const methodName in this.$ndMethods) {
|
|
80
|
-
|
|
87
|
+
const callback = this.$ndMethods[methodName];
|
|
88
|
+
nd[methodName](callback.length === 0 ? callback : this.$ndMethods[methodName].bind(clonedNode, ...data));
|
|
81
89
|
}
|
|
82
90
|
});
|
|
83
91
|
}
|
|
@@ -90,7 +98,7 @@ NodeCloner.prototype.resolve = function() {
|
|
|
90
98
|
const key = keys[0];
|
|
91
99
|
const callback = this.$classes[key];
|
|
92
100
|
steps.push((clonedNode, data) => {
|
|
93
|
-
cache[key] = callback
|
|
101
|
+
cache[key] = getPropertyValue(callback, data);
|
|
94
102
|
ElementCreator.processClassAttribute(clonedNode, cache);
|
|
95
103
|
});
|
|
96
104
|
} else {
|
|
@@ -107,7 +115,7 @@ NodeCloner.prototype.resolve = function() {
|
|
|
107
115
|
const key = keys[0];
|
|
108
116
|
const callback = this.$styles[key];
|
|
109
117
|
steps.push((clonedNode, data) => {
|
|
110
|
-
cache[key] = callback
|
|
118
|
+
cache[key] = getPropertyValue(callback, data);
|
|
111
119
|
ElementCreator.processStyleAttribute(clonedNode, cache);
|
|
112
120
|
});
|
|
113
121
|
} else {
|
|
@@ -124,7 +132,7 @@ NodeCloner.prototype.resolve = function() {
|
|
|
124
132
|
const key = keys[0];
|
|
125
133
|
const callback = this.$attrs[key];
|
|
126
134
|
steps.push((clonedNode, data) => {
|
|
127
|
-
cache[key] = callback
|
|
135
|
+
cache[key] = getPropertyValue(callback, data);
|
|
128
136
|
ElementCreator.processAttributes(clonedNode, cache);
|
|
129
137
|
});
|
|
130
138
|
} else {
|
|
@@ -207,7 +215,8 @@ NodeCloner.prototype.attr = function(attrName, value) {
|
|
|
207
215
|
this.$styles[value.property] = value.value;
|
|
208
216
|
return this;
|
|
209
217
|
}
|
|
218
|
+
|
|
210
219
|
this.$attrs = this.$attrs || {};
|
|
211
|
-
this.$attrs[
|
|
220
|
+
this.$attrs[value.property] = value.value;
|
|
212
221
|
return this;
|
|
213
222
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import TemplateBinding from '../TemplateBinding';
|
|
2
2
|
import { $hydrateFn} from './utils';
|
|
3
3
|
import NodeCloner from './NodeCloner';
|
|
4
|
+
import {ElementCreator} from '../ElementCreator';
|
|
5
|
+
import NativeDocumentError from '../../errors/NativeDocumentError';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Creates a high-performance template cloner for repeated rendering of the same structure.
|
|
@@ -22,7 +24,10 @@ import NodeCloner from './NodeCloner';
|
|
|
22
24
|
export function TemplateCloner($fn) {
|
|
23
25
|
let $node = null;
|
|
24
26
|
|
|
27
|
+
this.$scopeDataBuilder = null;
|
|
28
|
+
|
|
25
29
|
const assignClonerToNode = ($node) => {
|
|
30
|
+
$node = ElementCreator.getChild($node);
|
|
26
31
|
const childNodes = $node.childNodes;
|
|
27
32
|
let containDynamicNode = $node.nodeCloner?.shouldBeHydrate();
|
|
28
33
|
const childNodesLength = childNodes.length;
|
|
@@ -73,20 +78,45 @@ export function TemplateCloner($fn) {
|
|
|
73
78
|
*/
|
|
74
79
|
this.clone = (data) => {
|
|
75
80
|
const binder = createTemplateCloner(this);
|
|
76
|
-
|
|
81
|
+
const helpers = {
|
|
82
|
+
useCallback: binder.attach.bind(binder),
|
|
83
|
+
useCallbacks: (callbacks) => {
|
|
84
|
+
for(const key in callbacks) {
|
|
85
|
+
callbacks[key] = binder.attach(callbacks[key]);
|
|
86
|
+
}
|
|
87
|
+
return callbacks;
|
|
88
|
+
},
|
|
89
|
+
useData: (callback) => {
|
|
90
|
+
this.$scopeDataBuilder = callback;
|
|
91
|
+
},
|
|
92
|
+
use: binder.freeProps.bind(binder),
|
|
93
|
+
};
|
|
94
|
+
$node = ElementCreator.getChild($fn(binder, helpers));
|
|
77
95
|
if(!$node.nodeCloner) {
|
|
78
96
|
$node.nodeCloner = new NodeCloner($node);
|
|
79
97
|
}
|
|
80
98
|
assignClonerToNode($node);
|
|
99
|
+
if(this.$scopeDataBuilder) {
|
|
100
|
+
this.clone = (data) => {
|
|
101
|
+
const scopeData = this.scopeData();
|
|
102
|
+
return $node.dynamicCloneNode([...data, scopeData]);
|
|
103
|
+
};
|
|
104
|
+
return $node.dynamicCloneNode([...data, this.scopeData()]);
|
|
105
|
+
}
|
|
106
|
+
|
|
81
107
|
this.clone = $node.dynamicCloneNode;
|
|
82
108
|
return $node.dynamicCloneNode(data);
|
|
83
109
|
};
|
|
84
110
|
|
|
111
|
+
this.scopeData = () => {
|
|
112
|
+
return this.$scopeDataBuilder?.() || {};
|
|
113
|
+
};
|
|
114
|
+
|
|
85
115
|
|
|
86
116
|
const createBinding = (hydrateFunction, targetType) => {
|
|
87
117
|
return new TemplateBinding((element, property) => {
|
|
88
118
|
$hydrateFn(hydrateFunction, targetType, element, property);
|
|
89
|
-
});
|
|
119
|
+
}, this);
|
|
90
120
|
};
|
|
91
121
|
|
|
92
122
|
/**
|
|
@@ -113,6 +143,18 @@ export function TemplateCloner($fn) {
|
|
|
113
143
|
return this.value(propertyName);
|
|
114
144
|
};
|
|
115
145
|
|
|
146
|
+
this.data = (property) => {
|
|
147
|
+
return this.freeProps((...Args) => {
|
|
148
|
+
const data = Args.at(-1);
|
|
149
|
+
if(process.env.NODE_ENV === 'development') {
|
|
150
|
+
if(!data[property]) {
|
|
151
|
+
throw new NativeDocumentError(property + ' is not defined in useData');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return data[property];
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
116
158
|
/**
|
|
117
159
|
* Creates a text/value binding — the result is set as text content or input value.
|
|
118
160
|
* Alias: .text()
|
|
@@ -142,6 +184,16 @@ export function TemplateCloner($fn) {
|
|
|
142
184
|
return createBinding(fn, 'attributes');
|
|
143
185
|
};
|
|
144
186
|
|
|
187
|
+
this.freeProps = (callbackOrProperty) => {
|
|
188
|
+
return new TemplateBinding((element, property, type) => {
|
|
189
|
+
let targetType = type;
|
|
190
|
+
if(type === 'attribute') {
|
|
191
|
+
targetType = 'attributes';
|
|
192
|
+
}
|
|
193
|
+
$hydrateFn(callbackOrProperty, targetType, element, property);
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
145
197
|
/**
|
|
146
198
|
* Creates an event binding — fn(data) returns the event handler to attach.
|
|
147
199
|
*
|
|
@@ -155,6 +207,13 @@ export function TemplateCloner($fn) {
|
|
|
155
207
|
this.callback = this.attach;
|
|
156
208
|
}
|
|
157
209
|
|
|
210
|
+
const createDataProxy = ($binder) => {
|
|
211
|
+
return new Proxy($binder, {
|
|
212
|
+
get(target, key) {
|
|
213
|
+
return target.data(key);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
};
|
|
158
217
|
|
|
159
218
|
const createTemplateCloner = ($binder) => {
|
|
160
219
|
return new Proxy($binder, {
|
|
@@ -162,18 +221,98 @@ const createTemplateCloner = ($binder) => {
|
|
|
162
221
|
if(prop in target) {
|
|
163
222
|
return target[prop];
|
|
164
223
|
}
|
|
165
|
-
if (typeof prop === 'symbol')
|
|
166
|
-
|
|
224
|
+
if (typeof prop === 'symbol') {
|
|
225
|
+
return target[prop];
|
|
226
|
+
}
|
|
227
|
+
if(prop === '$data') {
|
|
228
|
+
return createDataProxy($binder);
|
|
229
|
+
}
|
|
230
|
+
return target.freeProps(prop);
|
|
167
231
|
},
|
|
168
232
|
});
|
|
169
233
|
};
|
|
170
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Creates a high-performance template factory that compiles once and clones efficiently.
|
|
237
|
+
* The template function is called only on the first render to build and optimise the DOM
|
|
238
|
+
* structure. Subsequent calls clone the compiled result and hydrate it with new data.
|
|
239
|
+
*
|
|
240
|
+
* Execution happens in three distinct phases:
|
|
241
|
+
*
|
|
242
|
+
* **Phase 1 — Compilation** (runs once, on first call)
|
|
243
|
+
* The template function receives `$scope` — a binding proxy. Accessing `$scope.name`,
|
|
244
|
+
* `$scope.color` etc. declares bindings on the template node without reading values yet.
|
|
245
|
+
* `$scope.$data` provides direct bindings to local state properties declared via `useData`.
|
|
246
|
+
* The DOM structure is built and optimised during this phase.
|
|
247
|
+
*
|
|
248
|
+
* **Phase 2 — Hydration** (runs once per clone)
|
|
249
|
+
* `useData(() => ({ ... }))` creates isolated local state for each cloned instance.
|
|
250
|
+
* The factory is called once per item — every clone gets its own independent state object.
|
|
251
|
+
*
|
|
252
|
+
* **Phase 3 — Runtime** (runs on each interaction or reactive update)
|
|
253
|
+
* `useCallback`, `useCallbacks` and `use` callbacks receive the same arguments as the
|
|
254
|
+
* template function, plus the local state object as the last argument:
|
|
255
|
+
* - `item` — the actual item data at the time of execution
|
|
256
|
+
* - `index` — position in the list (when used with ForEachArray)
|
|
257
|
+
* - `data` — the local state for this specific clone (from useData)
|
|
258
|
+
*
|
|
259
|
+
* @template T
|
|
260
|
+
* @param {(
|
|
261
|
+
* $scope: T & { $data: Record<string, any> },
|
|
262
|
+
* helpers: {
|
|
263
|
+
* useData: (factory: () => Record<string, any>) => void,
|
|
264
|
+
* useCallback: (fn: (item: T, index: number, data: Record<string, any>) => EventListener) => TemplateBinding,
|
|
265
|
+
* useCallbacks: (callbacks: Record<string, (item: T, index: number, data: Record<string, any>) => EventListener>) => Record<string, TemplateBinding>,
|
|
266
|
+
* use: (fn: (item: T, index: number, data: Record<string, any>) => any) => TemplateBinding,
|
|
267
|
+
* }
|
|
268
|
+
* ) => HTMLElement} fn - Template builder function called once during compilation
|
|
269
|
+
* @returns {(item: T, index?: number) => HTMLElement} Factory function — pass directly to ForEachArray or call manually
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* const UserRow = useCache(($scope, { useData, useCallback, useCallbacks, use }) => {
|
|
273
|
+
*
|
|
274
|
+
* // Phase 1 — declare bindings (runs once)
|
|
275
|
+
* const color = $scope.color;
|
|
276
|
+
* // Bind directly from local state via $scope.$data
|
|
277
|
+
* const selectedClass = $scope.$data.selected;
|
|
278
|
+
*
|
|
279
|
+
* // Phase 2 — local state per clone (runs once per item)
|
|
280
|
+
* useData(() => ({
|
|
281
|
+
* selected: $(false),
|
|
282
|
+
* }));
|
|
283
|
+
*
|
|
284
|
+
* // Phase 3 — runtime callbacks (run on interaction)
|
|
285
|
+
*
|
|
286
|
+
* // Single callback
|
|
287
|
+
* const toggle = useCallback((item, index, data) => {
|
|
288
|
+
* data.selected.toggle();
|
|
289
|
+
* });
|
|
290
|
+
*
|
|
291
|
+
* // Multiple callbacks at once
|
|
292
|
+
* const { select, deselect } = useCallbacks({
|
|
293
|
+
* select: (item, index, data) => data.selected.set(true),
|
|
294
|
+
* deselect: (item, index, data) => data.selected.set(false),
|
|
295
|
+
* });
|
|
296
|
+
*
|
|
297
|
+
* const isSelected = use((item, index, data) => {
|
|
298
|
+
* return data.selected.val() ? 'is-selected' : '';
|
|
299
|
+
* });
|
|
300
|
+
*
|
|
301
|
+
* return Div({ class: isSelected, style: { color } }, Strong($scope.name))
|
|
302
|
+
* .onClick(toggle);
|
|
303
|
+
* });
|
|
304
|
+
*
|
|
305
|
+
* // Pass directly to ForEachArray
|
|
306
|
+
* ForEachArray($users, UserRow)
|
|
307
|
+
*
|
|
308
|
+
* // Or call manually
|
|
309
|
+
* UserRow(item, index)
|
|
310
|
+
*/
|
|
171
311
|
export function useCache(fn) {
|
|
172
312
|
let $cache = null;
|
|
173
313
|
|
|
174
314
|
let wrapper = (args) => {
|
|
175
315
|
$cache = new TemplateCloner(fn);
|
|
176
|
-
|
|
177
316
|
const node = $cache.clone(args);
|
|
178
317
|
wrapper = $cache.clone;
|
|
179
318
|
return node;
|
|
@@ -120,7 +120,12 @@ const buildSelectedLabel = ($desc) => {
|
|
|
120
120
|
return $desc.selectedLabelRender($desc);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
const dependencies = [$desc.value];
|
|
124
|
+
if(!Array.isArray($desc.options)) {
|
|
125
|
+
dependencies.push($desc.options);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return $.computed((val) => {
|
|
124
129
|
if(!val || (Array.isArray(val) && val.length === 0)) {
|
|
125
130
|
return $desc.placeholder || 'Select...';
|
|
126
131
|
}
|
|
@@ -156,7 +161,7 @@ const buildSelectedLabel = ($desc) => {
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
return labels.join(', ');
|
|
159
|
-
});
|
|
164
|
+
}, dependencies);
|
|
160
165
|
};
|
|
161
166
|
|
|
162
167
|
const buildNativeSelect = ($desc) => {
|