vanilla-jui 1.4.5 → 1.4.7
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/README.md +6 -0
- package/dist/index.d.ts +1273 -1896
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +21 -2
- package/src/components/accordion.ts +297 -216
- package/src/components/flow.ts +222 -258
- package/src/components/form.ts +44 -102
- package/src/components/menu.ts +268 -356
- package/src/components/modal.ts +181 -641
- package/src/components/offcanvas.ts +225 -106
- package/src/components/pagination.ts +296 -326
- package/src/components/sticky.ts +55 -54
- package/src/components/swiper.ts +237 -247
- package/src/components/tabs.ts +278 -271
- package/src/components/toc.ts +42 -46
- package/src/core/Component.ts +69 -47
- package/src/css/accordion.css +2 -2
- package/src/css/animation.css +1 -5
- package/src/css/avatar.css +1 -1
- package/src/css/badge.css +1 -1
- package/src/css/breadcrumb.css +9 -10
- package/src/css/button.css +1 -1
- package/src/css/card.css +2 -2
- package/src/css/color.css +974 -1002
- package/src/css/color_p3.css +974 -1004
- package/src/css/components.css +1 -1
- package/src/css/content.css +72 -63
- package/src/css/drop.css +1 -1
- package/src/css/editor.css +14 -14
- package/src/css/flow.css +4 -14
- package/src/css/font.css +5 -4
- package/src/css/form.css +76 -52
- package/src/css/gradient.css +299 -222
- package/src/css/icon.css +57 -57
- package/src/css/index.css +1 -1
- package/src/css/layout.css +13 -10
- package/src/css/loading.css +60 -12
- package/src/css/menu.css +262 -188
- package/src/css/modal.css +1 -1
- package/src/css/offcanvas.css +9 -13
- package/src/css/pagination.css +5 -1
- package/src/css/popup.css +1 -1
- package/src/css/skeleton.css +3 -2
- package/src/css/status.css +2 -3
- package/src/css/swiper.css +1 -1
- package/src/css/table.css +1 -1
- package/src/css/tabs.css +12 -11
- package/src/css/tag.css +1 -1
- package/src/css/theme-palette.css +19 -5
- package/src/css/themes/default/accordion.css +61 -61
- package/src/css/themes/default/avatar.css +54 -54
- package/src/css/themes/default/badge.css +62 -62
- package/src/css/themes/default/breadcrumb.css +23 -23
- package/src/css/themes/default/button.css +203 -203
- package/src/css/themes/default/content.css +53 -53
- package/src/css/themes/default/drop.css +10 -10
- package/src/css/themes/default/editor.css +9 -9
- package/src/css/themes/default/index.css +1 -1
- package/src/css/themes/default/loading.css +26 -20
- package/src/css/themes/default/menu.css +75 -11
- package/src/css/themes/default/modal.css +32 -36
- package/src/css/themes/default/pagination.css +7 -7
- package/src/css/themes/default/popup.css +6 -6
- package/src/css/themes/default/swiper.css +32 -32
- package/src/css/themes/default/table.css +16 -16
- package/src/css/themes/default/tabs.css +30 -30
- package/src/css/themes/default/tag.css +71 -71
- package/src/css/themes/default/theme-palette.css +11 -11
- package/src/css/themes/default/tip.css +42 -42
- package/src/css/themes/default/toast.css +60 -60
- package/src/css/themes/default/toolbar.css +23 -29
- package/src/css/themes/default/tooltip.css +43 -13
- package/src/css/themes/index.css +1 -1
- package/src/css/tip.css +2 -2
- package/src/css/toast.css +3 -3
- package/src/css/tokens.css +12 -17
- package/src/css/toolbar.css +9 -10
- package/src/css/tooltip.css +3 -2
- package/src/index.ts +22 -19
- package/src/{components → primitives}/drop.ts +58 -49
- package/src/{components → primitives}/icons.ts +1 -1
- package/src/primitives/loading.ts +23 -0
- package/src/primitives/parabola.ts +388 -0
- package/src/primitives/popup.ts +61 -0
- package/src/{components → primitives}/theme.ts +89 -57
- package/src/{components → primitives}/toast.ts +18 -25
- package/src/{components → primitives}/tooltip.ts +100 -25
- package/src/utilities/browser.ts +23 -49
- package/src/utilities/class-name.ts +5 -0
- package/src/utilities/dom.ts +14 -32
- package/src/utilities/function.ts +4 -0
- package/src/utilities/http.ts +7 -7
- package/src/utilities/id.ts +57 -0
- package/src/utilities/object.ts +5 -0
- package/src/utilities/timer.ts +38 -0
- package/src/utilities/types.ts +420 -0
- package/src/{components → validation}/validator.ts +64 -53
- package/src/components/parabola.ts +0 -335
- package/src/utilities/cache.ts +0 -57
- package/src/utilities/core.ts +0 -614
- package/src/utilities/storage.ts +0 -46
package/src/components/menu.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createDeepStore,
|
|
3
|
+
createEffect,
|
|
4
|
+
createRoot,
|
|
5
|
+
jsx,
|
|
6
|
+
untrack,
|
|
7
|
+
} from 'vanilla-signal';
|
|
8
|
+
import { t } from 'vanilla-signal-i18n';
|
|
9
|
+
|
|
10
|
+
import Component, {
|
|
11
|
+
type ComponentDOM,
|
|
12
|
+
type ComponentRuntime,
|
|
13
|
+
} from '../core/Component.ts';
|
|
14
|
+
import locales from '../locales/index.ts';
|
|
15
|
+
import { icon } from '../primitives/icons.ts';
|
|
16
|
+
import { joinClasses } from '../utilities/class-name.ts';
|
|
17
|
+
import { all } from '../utilities/dom.ts';
|
|
18
|
+
import { randomId } from '../utilities/id.ts';
|
|
3
19
|
import {
|
|
4
20
|
type ResolveSchema,
|
|
5
|
-
randomId,
|
|
6
21
|
resolveProps,
|
|
7
22
|
validateParam,
|
|
8
|
-
} from '../utilities/
|
|
9
|
-
import { all, type DOMReference, q, resolveElement } from '../utilities/dom.ts';
|
|
10
|
-
import { createEventManager, type IEventManager } from '../utilities/events.ts';
|
|
11
|
-
import { icon } from './icons.ts';
|
|
23
|
+
} from '../utilities/types.ts';
|
|
12
24
|
|
|
13
|
-
export type MenuType = string;
|
|
25
|
+
export type MenuType = string | undefined;
|
|
14
26
|
export type MenuItemId = string | number;
|
|
15
27
|
|
|
16
28
|
export interface MenuClassNames {
|
|
@@ -20,10 +32,9 @@ export interface MenuClassNames {
|
|
|
20
32
|
hasChildren: string;
|
|
21
33
|
link: string;
|
|
22
34
|
subMenu: string;
|
|
23
|
-
|
|
35
|
+
backItem: string;
|
|
24
36
|
active: string;
|
|
25
|
-
|
|
26
|
-
iconPrefix: string;
|
|
37
|
+
backIcon: string;
|
|
27
38
|
text: string;
|
|
28
39
|
}
|
|
29
40
|
|
|
@@ -38,57 +49,88 @@ export interface MenuItem extends Record<string, unknown> {
|
|
|
38
49
|
children?: MenuItem[];
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
export interface
|
|
52
|
+
export interface MenuProps extends Record<string, unknown> {
|
|
42
53
|
type?: MenuType;
|
|
43
|
-
id?: string;
|
|
44
|
-
|
|
54
|
+
id?: string | null;
|
|
55
|
+
data?: MenuItem[];
|
|
45
56
|
backText?: string;
|
|
46
57
|
className?: MenuClassNameConfig;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
interface
|
|
50
|
-
type
|
|
60
|
+
interface ResolvedMenuProps extends Record<string, unknown> {
|
|
61
|
+
type?: MenuType;
|
|
51
62
|
id: string;
|
|
52
|
-
|
|
63
|
+
data: MenuItem[];
|
|
53
64
|
backText: string;
|
|
54
65
|
className: MenuClassNames;
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
interface
|
|
68
|
+
interface MenuState extends Record<string, unknown> {
|
|
69
|
+
data: MenuItem[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface MenuDOM extends ComponentDOM {
|
|
58
73
|
root: HTMLElement | null;
|
|
74
|
+
list: HTMLElement | null;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
interface
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
interface MenuRuntime extends ComponentRuntime {
|
|
78
|
+
built: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface MenuCleanupExtras {
|
|
82
|
+
state?: (() => void) | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface MenuSnapshot {
|
|
86
|
+
data: unknown;
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
const DEFAULT_CLASS_NAMES: MenuClassNames = {
|
|
67
|
-
root: '',
|
|
90
|
+
root: 'j-menu',
|
|
68
91
|
list: 'menu',
|
|
69
92
|
item: 'menu-item',
|
|
70
93
|
hasChildren: 'menu-item-has-children',
|
|
71
94
|
link: 'menu-link',
|
|
72
95
|
subMenu: 'sub-menu',
|
|
73
|
-
back: 'back',
|
|
74
96
|
active: 'is-active',
|
|
75
|
-
icon: 'el-icon',
|
|
76
|
-
iconPrefix: 'el-prefix',
|
|
77
97
|
text: 'menu-text',
|
|
98
|
+
backItem: 'menu-item back',
|
|
99
|
+
backIcon: 'el-icon el-prefix',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const MENU_ITEM_RULE = {
|
|
103
|
+
type: 'object',
|
|
104
|
+
validate: isMenuItem,
|
|
105
|
+
message: 'expects { title, id?, url?, target?, classes?, children? }.',
|
|
78
106
|
};
|
|
79
107
|
|
|
80
|
-
const
|
|
81
|
-
type:
|
|
108
|
+
const MENU_DATA_RULE = {
|
|
109
|
+
type: 'array',
|
|
110
|
+
items: MENU_ITEM_RULE,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const MENU_PROPS_SCHEMA = {
|
|
114
|
+
type: { default: undefined, types: ['string', 'undefined'] },
|
|
82
115
|
id: {
|
|
83
|
-
default:
|
|
84
|
-
|
|
116
|
+
default: null,
|
|
117
|
+
types: ['string', 'null'],
|
|
85
118
|
normalize: (value: unknown) => {
|
|
86
|
-
if (typeof value
|
|
87
|
-
|
|
119
|
+
if (typeof value === 'string') {
|
|
120
|
+
const id = value.trim();
|
|
121
|
+
return id || randomId();
|
|
122
|
+
}
|
|
123
|
+
if (value == null) return randomId();
|
|
124
|
+
return value;
|
|
88
125
|
},
|
|
89
126
|
},
|
|
90
|
-
|
|
91
|
-
|
|
127
|
+
data: {
|
|
128
|
+
default: [],
|
|
129
|
+
type: 'array',
|
|
130
|
+
normalize: cloneMenuData,
|
|
131
|
+
items: MENU_ITEM_RULE,
|
|
132
|
+
},
|
|
133
|
+
backText: { default: t('b', locales), type: 'string' },
|
|
92
134
|
className: {
|
|
93
135
|
default: DEFAULT_CLASS_NAMES,
|
|
94
136
|
type: 'object',
|
|
@@ -97,41 +139,40 @@ const MENU_OPTIONS_SCHEMA = {
|
|
|
97
139
|
...(value && typeof value === 'object' ? value : {}),
|
|
98
140
|
}),
|
|
99
141
|
},
|
|
100
|
-
} satisfies ResolveSchema<
|
|
142
|
+
} satisfies ResolveSchema<MenuProps>;
|
|
101
143
|
|
|
102
|
-
const
|
|
144
|
+
const MENU_STATE_SCHEMA = {
|
|
145
|
+
data: MENU_DATA_RULE,
|
|
146
|
+
};
|
|
103
147
|
|
|
104
|
-
function
|
|
148
|
+
function normalizeProps(input: MenuProps = {}): ResolvedMenuProps {
|
|
105
149
|
const props = resolveProps(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
'Menu.
|
|
109
|
-
) as
|
|
150
|
+
input,
|
|
151
|
+
MENU_PROPS_SCHEMA,
|
|
152
|
+
'Menu.props'
|
|
153
|
+
) as ResolvedMenuProps;
|
|
154
|
+
|
|
110
155
|
return {
|
|
111
156
|
...props,
|
|
112
|
-
|
|
157
|
+
data: cloneMenuData(props.data),
|
|
113
158
|
className: { ...props.className },
|
|
114
159
|
};
|
|
115
160
|
}
|
|
116
161
|
|
|
117
|
-
function classList(...tokens: Array<string | null | undefined>): string {
|
|
118
|
-
return tokens.filter(Boolean).join(' ');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
162
|
function normalizeItemClasses(classes: MenuItem['classes']): string[] {
|
|
122
163
|
if (classes == null) return [];
|
|
123
|
-
if (typeof classes === 'string')
|
|
164
|
+
if (typeof classes === 'string') {
|
|
124
165
|
return classes.trim().split(/\s+/).filter(Boolean);
|
|
125
|
-
|
|
166
|
+
}
|
|
167
|
+
return classes.slice();
|
|
126
168
|
}
|
|
127
169
|
|
|
128
170
|
function itemId(id: MenuItemId | undefined): string {
|
|
129
171
|
return `menu-item-${id ?? randomId()}`;
|
|
130
172
|
}
|
|
131
173
|
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
return tag === 'ul' || tag === 'ol';
|
|
174
|
+
function itemUrl(url: MenuItem['url']): string {
|
|
175
|
+
return typeof url === 'string' ? url.trim() : '';
|
|
135
176
|
}
|
|
136
177
|
|
|
137
178
|
function isMenuItem(value: unknown): value is MenuItem {
|
|
@@ -156,10 +197,19 @@ function isMenuItem(value: unknown): value is MenuItem {
|
|
|
156
197
|
);
|
|
157
198
|
}
|
|
158
199
|
|
|
159
|
-
function
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
200
|
+
function cloneMenuData(data: unknown): MenuItem[] {
|
|
201
|
+
if (!Array.isArray(data)) return [];
|
|
202
|
+
return data.map((item) => {
|
|
203
|
+
validateParam('item', item, MENU_ITEM_RULE, 'Menu.data');
|
|
204
|
+
const nextItem = { ...(item as MenuItem) };
|
|
205
|
+
if (Array.isArray(nextItem.classes)) {
|
|
206
|
+
nextItem.classes = nextItem.classes.slice();
|
|
207
|
+
}
|
|
208
|
+
if (Array.isArray(nextItem.children)) {
|
|
209
|
+
nextItem.children = cloneMenuData(nextItem.children);
|
|
210
|
+
}
|
|
211
|
+
return nextItem;
|
|
212
|
+
});
|
|
163
213
|
}
|
|
164
214
|
|
|
165
215
|
function findDirectMenuLink(menuItem: HTMLElement): HTMLElement | null {
|
|
@@ -186,153 +236,113 @@ function findDirectBackItem(subMenu: HTMLElement): HTMLElement | null {
|
|
|
186
236
|
);
|
|
187
237
|
}
|
|
188
238
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
*/
|
|
194
|
-
export class Menu {
|
|
195
|
-
options: ResolvedMenuOptions | null;
|
|
196
|
-
dom: MenuDOM;
|
|
197
|
-
cleanup: MenuCleanup | null;
|
|
198
|
-
private _element: DOMReference;
|
|
199
|
-
private _bound: boolean;
|
|
200
|
-
private _destroyed: boolean;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 创建菜单实例。
|
|
204
|
-
* @param {MenuOptions} [options={}] 菜单配置。
|
|
205
|
-
* @param {Element|Node|string|Array|false} [element=false] 已有菜单节点、选择器或 JSX/h 返回节点;默认 `false` 按 items 动态创建。
|
|
206
|
-
*/
|
|
207
|
-
constructor(options: MenuOptions = {}, element: DOMReference = false) {
|
|
208
|
-
this.options = normalizeOptions(options);
|
|
209
|
-
this._element = element;
|
|
210
|
-
this.dom = {
|
|
211
|
-
root: null,
|
|
212
|
-
};
|
|
213
|
-
this.cleanup = {
|
|
214
|
-
events: createEventManager(),
|
|
215
|
-
};
|
|
216
|
-
this._bound = false;
|
|
217
|
-
this._destroyed = false;
|
|
218
|
-
}
|
|
239
|
+
class MenuComponent extends Component<ResolvedMenuProps, MenuState, MenuDOM> {
|
|
240
|
+
declare runtime: MenuRuntime;
|
|
241
|
+
declare state: MenuState;
|
|
242
|
+
declare cleanup: Component['cleanup'] & MenuCleanupExtras;
|
|
219
243
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
244
|
+
constructor(input: MenuProps = {}) {
|
|
245
|
+
const props = normalizeProps(input);
|
|
246
|
+
super(props);
|
|
223
247
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
validateParam('items', items, MENU_ITEMS_RULE, 'Menu');
|
|
232
|
-
items.forEach((item, index) => {
|
|
233
|
-
validateParam(
|
|
234
|
-
String(index),
|
|
235
|
-
item,
|
|
236
|
-
{
|
|
237
|
-
validate: isMenuItem,
|
|
238
|
-
message:
|
|
239
|
-
'expects { title, id?, url?, target?, classes?, children? }.',
|
|
240
|
-
},
|
|
241
|
-
'Menu.items'
|
|
242
|
-
);
|
|
243
|
-
});
|
|
248
|
+
this.dom.list = null;
|
|
249
|
+
this.cleanup.state = null;
|
|
250
|
+
this.runtime.built = false;
|
|
251
|
+
|
|
252
|
+
this.state = createDeepStore({
|
|
253
|
+
data: cloneMenuData(props.data),
|
|
254
|
+
}) as MenuState;
|
|
244
255
|
}
|
|
245
256
|
|
|
246
|
-
/**
|
|
247
|
-
* 构建菜单。
|
|
248
|
-
*
|
|
249
|
-
* element 为 false 时动态创建 DOM;否则绑定已有节点。
|
|
250
|
-
* @returns {Menu}
|
|
251
|
-
*/
|
|
252
257
|
build(): this {
|
|
253
|
-
if (this.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (this._element === false) {
|
|
257
|
-
this._verifyItems(this.options.items);
|
|
258
|
-
this.dom.root = this._buildRoot();
|
|
259
|
-
this._bound = true;
|
|
260
|
-
} else {
|
|
261
|
-
const root = resolveElement(this._element);
|
|
262
|
-
if (!root) throw new Error('Menu.element: container not found.');
|
|
263
|
-
this.dom.root = root as HTMLElement;
|
|
264
|
-
this._bound = true;
|
|
258
|
+
if (this.runtime.destroyed) {
|
|
259
|
+
throw new Error('Menu.build: instance has been destroyed.');
|
|
265
260
|
}
|
|
261
|
+
if (this.runtime.built) return this;
|
|
266
262
|
|
|
267
|
-
this.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* 根据 items 创建菜单根节点。
|
|
274
|
-
* @private
|
|
275
|
-
* @returns {HTMLElement}
|
|
276
|
-
*/
|
|
277
|
-
_buildRoot(): HTMLElement {
|
|
278
|
-
if (!this.options) throw new Error('Menu._buildRoot: instance destroyed.');
|
|
279
|
-
const { items, id, type, className } = this.options;
|
|
280
|
-
const rootClass = className.root || `j-${type}-menu`;
|
|
281
|
-
|
|
282
|
-
return jsx('nav', {
|
|
283
|
-
className: rootClass,
|
|
263
|
+
this.validateData(this.state.data);
|
|
264
|
+
this.dom.root = jsx('nav', {
|
|
265
|
+
className: this.props.className.root,
|
|
284
266
|
'data-menu': 'root',
|
|
285
|
-
|
|
267
|
+
...(this.props.type !== undefined && {
|
|
268
|
+
'data-menu-type': this.props.type,
|
|
269
|
+
}),
|
|
286
270
|
children: jsx('ul', {
|
|
287
|
-
className: className.list,
|
|
288
|
-
id: id,
|
|
271
|
+
className: this.props.className.list,
|
|
272
|
+
id: this.props.id,
|
|
289
273
|
'data-menu-list': 'root',
|
|
290
|
-
children: items.map((item) => this._buildItem(item)),
|
|
291
274
|
}),
|
|
292
275
|
}) as HTMLElement;
|
|
276
|
+
this.dom.list = this.dom.root.querySelector('[data-menu-list="root"]');
|
|
277
|
+
this.runtime.built = true;
|
|
278
|
+
this.bindState();
|
|
279
|
+
this.bindEvents();
|
|
280
|
+
this.emit('init', this.props);
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private bindState(): void {
|
|
285
|
+
if (this.cleanup.state) return;
|
|
286
|
+
|
|
287
|
+
this.cleanup.state = createRoot((dispose) => {
|
|
288
|
+
createEffect(() => {
|
|
289
|
+
const snapshot = {
|
|
290
|
+
data: this.state.data,
|
|
291
|
+
};
|
|
292
|
+
untrack(() => this.renderSnapshot(snapshot));
|
|
293
|
+
});
|
|
294
|
+
return dispose;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private renderSnapshot(snapshot: MenuSnapshot): void {
|
|
299
|
+
if (!this.runtime.built || !this.dom.root || !this.dom.list) return;
|
|
300
|
+
this.validateData(snapshot.data);
|
|
301
|
+
const data = cloneMenuData(snapshot.data);
|
|
302
|
+
|
|
303
|
+
this.dom.list.textContent = '';
|
|
304
|
+
this.dom.list.append(...data.map((item) => this.buildItem(item)));
|
|
293
305
|
}
|
|
294
306
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
* @private
|
|
298
|
-
* @param {MenuItem} item 菜单项配置。
|
|
299
|
-
* @returns {HTMLElement}
|
|
300
|
-
*/
|
|
301
|
-
_buildItem(item: MenuItem): HTMLElement {
|
|
302
|
-
if (!this.options) throw new Error('Menu._buildItem: instance destroyed.');
|
|
303
|
-
const { className } = this.options;
|
|
307
|
+
private buildItem(item: MenuItem): HTMLElement {
|
|
308
|
+
const { className } = this.props;
|
|
304
309
|
const childrenItems = item.children || [];
|
|
305
310
|
const hasChildren = childrenItems.length > 0;
|
|
306
311
|
const classes = [className.item];
|
|
307
312
|
|
|
308
|
-
if (hasChildren)
|
|
309
|
-
classes.push(className.hasChildren);
|
|
310
|
-
}
|
|
311
|
-
|
|
313
|
+
if (hasChildren) classes.push(className.hasChildren);
|
|
312
314
|
classes.push(...normalizeItemClasses(item.classes));
|
|
313
315
|
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
316
|
+
const url = itemUrl(item.url);
|
|
317
|
+
const shouldRenderSpan = !hasChildren && !url;
|
|
318
|
+
const title = shouldRenderSpan
|
|
319
|
+
? (jsx('span', {
|
|
320
|
+
className: className.link,
|
|
321
|
+
'data-menu-link': '',
|
|
322
|
+
children: item.title,
|
|
323
|
+
}) as HTMLElement)
|
|
324
|
+
: (jsx('a', {
|
|
325
|
+
className: className.link,
|
|
326
|
+
href: url,
|
|
327
|
+
...(item.target && { target: item.target }),
|
|
328
|
+
'data-menu-link': '',
|
|
329
|
+
children: item.title,
|
|
330
|
+
}) as HTMLElement);
|
|
331
|
+
|
|
332
|
+
const children: Node[] = [title];
|
|
323
333
|
|
|
324
334
|
if (hasChildren) {
|
|
325
335
|
children.push(
|
|
326
336
|
jsx('ul', {
|
|
327
337
|
className: className.subMenu,
|
|
328
338
|
'data-menu-list': 'sub',
|
|
329
|
-
children: childrenItems.map((child) => this.
|
|
339
|
+
children: childrenItems.map((child) => this.buildItem(child)),
|
|
330
340
|
}) as HTMLElement
|
|
331
341
|
);
|
|
332
342
|
}
|
|
333
343
|
|
|
334
344
|
return jsx('li', {
|
|
335
|
-
className:
|
|
345
|
+
className: joinClasses(...classes),
|
|
336
346
|
id: itemId(item.id),
|
|
337
347
|
'data-menu-item': item.id == null ? '' : String(item.id),
|
|
338
348
|
...(hasChildren && { 'data-menu-has-children': '' }),
|
|
@@ -340,109 +350,72 @@ export class Menu {
|
|
|
340
350
|
}) as HTMLElement;
|
|
341
351
|
}
|
|
342
352
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
*/
|
|
348
|
-
_bindEvents(): void {
|
|
349
|
-
if (!this.dom.root || !this.options || !this.cleanup) return;
|
|
353
|
+
private bindEvents(): void {
|
|
354
|
+
this.unbindEvents();
|
|
355
|
+
const root = this.dom.root;
|
|
356
|
+
if (!root) return;
|
|
350
357
|
|
|
351
|
-
if (this.
|
|
352
|
-
this.cleanup.events.on('mobile',
|
|
358
|
+
if (this.props.type === 'mobile') {
|
|
359
|
+
this.cleanup.events.on('mobile', root, 'click', (event) => {
|
|
353
360
|
const target = event.target;
|
|
354
361
|
if (!(target instanceof Element)) return;
|
|
355
362
|
|
|
356
363
|
const backItem = target.closest<HTMLElement>('[data-menu-back]');
|
|
357
|
-
if (backItem &&
|
|
364
|
+
if (backItem && root.contains(backItem)) {
|
|
358
365
|
event.preventDefault();
|
|
359
|
-
this.
|
|
366
|
+
this.handleBack(backItem);
|
|
360
367
|
return;
|
|
361
368
|
}
|
|
362
369
|
|
|
363
370
|
const menuItem = target.closest<HTMLElement>(
|
|
364
371
|
'[data-menu-has-children]'
|
|
365
372
|
);
|
|
373
|
+
if (!menuItem || !root.contains(menuItem)) return;
|
|
366
374
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
375
|
+
const subMenu = findDirectSubMenu(menuItem);
|
|
376
|
+
if (subMenu?.contains(target)) return;
|
|
377
|
+
|
|
378
|
+
const directLink = findDirectMenuLink(menuItem);
|
|
379
|
+
if (
|
|
380
|
+
target === menuItem ||
|
|
381
|
+
(directLink && (target === directLink || directLink.contains(target)))
|
|
382
|
+
) {
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
this.handleMenuClick(menuItem);
|
|
376
385
|
}
|
|
377
386
|
});
|
|
378
|
-
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (this.props.type === 'bottom') {
|
|
379
390
|
this.cleanup.events.on('bottom', document, 'click', (event) => {
|
|
380
391
|
const target = event.target;
|
|
381
|
-
if (!(target instanceof Element) || !
|
|
392
|
+
if (!(target instanceof Element) || !root.isConnected) return;
|
|
382
393
|
|
|
383
394
|
const submenuLink = target.closest(
|
|
384
395
|
'[data-menu-list="sub"] [data-menu-link]'
|
|
385
396
|
);
|
|
386
|
-
if (submenuLink &&
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
397
|
+
if (submenuLink && root.contains(submenuLink)) return;
|
|
389
398
|
|
|
390
399
|
const firstLevelMenuItem = target.closest<HTMLElement>(
|
|
391
400
|
'[data-menu-list="root"] > [data-menu-has-children]'
|
|
392
401
|
);
|
|
393
402
|
|
|
394
|
-
if (firstLevelMenuItem &&
|
|
403
|
+
if (firstLevelMenuItem && root.contains(firstLevelMenuItem)) {
|
|
395
404
|
event.preventDefault();
|
|
396
|
-
this.
|
|
405
|
+
this.toggleActive(firstLevelMenuItem);
|
|
397
406
|
} else {
|
|
398
|
-
this.
|
|
407
|
+
this.clearActive();
|
|
399
408
|
}
|
|
400
409
|
});
|
|
401
410
|
}
|
|
402
411
|
}
|
|
403
412
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
* @private
|
|
407
|
-
* @returns {void}
|
|
408
|
-
*/
|
|
409
|
-
_unbindEvents(): void {
|
|
410
|
-
this.cleanup?.events.clear();
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* 清理当前构建出的 DOM 与事件,可选择保留实例引用用于重建。
|
|
415
|
-
* @private
|
|
416
|
-
* @param {object} [options={}] 清理选项。
|
|
417
|
-
* @param {boolean} [options.keepElement=false] 是否保留初始 element 引用。
|
|
418
|
-
* @returns {void}
|
|
419
|
-
*/
|
|
420
|
-
_teardown({ keepElement = false }: { keepElement?: boolean } = {}): void {
|
|
421
|
-
this._unbindEvents();
|
|
422
|
-
this.cleanup?.items?.();
|
|
423
|
-
if (this.cleanup) this.cleanup.items = null;
|
|
424
|
-
|
|
425
|
-
if (this._element === false && this.dom.root?.parentElement) {
|
|
426
|
-
this.dom.root.remove();
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (!keepElement) {
|
|
430
|
-
this._element = null;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
this.dom.root = null;
|
|
434
|
-
this._bound = false;
|
|
413
|
+
private unbindEvents(): void {
|
|
414
|
+
this.cleanup.events.clear();
|
|
435
415
|
}
|
|
436
416
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
* @private
|
|
440
|
-
* @param {HTMLElement} menuItem 菜单项节点。
|
|
441
|
-
* @returns {void}
|
|
442
|
-
*/
|
|
443
|
-
_handleMenuClick(menuItem: HTMLElement): void {
|
|
444
|
-
if (!this.options) return;
|
|
445
|
-
menuItem.classList.add(this.options.className.active);
|
|
417
|
+
private handleMenuClick(menuItem: HTMLElement): void {
|
|
418
|
+
menuItem.classList.add(this.props.className.active);
|
|
446
419
|
|
|
447
420
|
const subMenu = findDirectSubMenu(menuItem);
|
|
448
421
|
if (!subMenu) return;
|
|
@@ -450,22 +423,22 @@ export class Menu {
|
|
|
450
423
|
const existingBack = findDirectBackItem(subMenu);
|
|
451
424
|
if (existingBack) return;
|
|
452
425
|
|
|
453
|
-
const { className } = this.
|
|
426
|
+
const { className } = this.props;
|
|
454
427
|
const backButton = jsx('li', {
|
|
455
|
-
className:
|
|
428
|
+
className: className.backItem,
|
|
456
429
|
'data-menu-back': '',
|
|
457
430
|
children: jsx('a', {
|
|
458
431
|
className: className.link,
|
|
459
432
|
href: '',
|
|
460
433
|
'data-menu-link': '',
|
|
461
434
|
children: [
|
|
462
|
-
jsx('
|
|
463
|
-
className:
|
|
435
|
+
jsx('span', {
|
|
436
|
+
className: className.backIcon,
|
|
464
437
|
children: icon('arrow-left'),
|
|
465
438
|
}),
|
|
466
439
|
jsx('span', {
|
|
467
440
|
className: className.text,
|
|
468
|
-
children: this.
|
|
441
|
+
children: this.props.backText,
|
|
469
442
|
}),
|
|
470
443
|
],
|
|
471
444
|
}),
|
|
@@ -474,15 +447,7 @@ export class Menu {
|
|
|
474
447
|
subMenu.insertBefore(backButton, subMenu.firstChild);
|
|
475
448
|
}
|
|
476
449
|
|
|
477
|
-
|
|
478
|
-
* 处理移动端子菜单返回操作。
|
|
479
|
-
* @private
|
|
480
|
-
* @param {Element} target 点击目标。
|
|
481
|
-
* @returns {void}
|
|
482
|
-
*/
|
|
483
|
-
_handleBack(target: Element): void {
|
|
484
|
-
if (!this.options) return;
|
|
485
|
-
|
|
450
|
+
private handleBack(target: Element): void {
|
|
486
451
|
const backItem = target.closest<HTMLElement>('[data-menu-back]');
|
|
487
452
|
if (!backItem) return;
|
|
488
453
|
|
|
@@ -490,132 +455,79 @@ export class Menu {
|
|
|
490
455
|
const parentMenuItem = subMenu?.parentElement;
|
|
491
456
|
|
|
492
457
|
if (parentMenuItem?.hasAttribute('data-menu-has-children')) {
|
|
493
|
-
parentMenuItem.classList.remove(this.
|
|
458
|
+
parentMenuItem.classList.remove(this.props.className.active);
|
|
494
459
|
}
|
|
495
460
|
|
|
496
461
|
backItem.remove();
|
|
497
462
|
}
|
|
498
463
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
* @returns {void}
|
|
504
|
-
*/
|
|
505
|
-
_toggleActive(menuItem: HTMLElement): void {
|
|
506
|
-
if (!this.options) return;
|
|
507
|
-
const isActive = menuItem.classList.contains(this.options.className.active);
|
|
508
|
-
|
|
509
|
-
this._clearActive();
|
|
510
|
-
|
|
511
|
-
if (!isActive) {
|
|
512
|
-
menuItem.classList.add(this.options.className.active);
|
|
513
|
-
}
|
|
464
|
+
private toggleActive(menuItem: HTMLElement): void {
|
|
465
|
+
const isActive = menuItem.classList.contains(this.props.className.active);
|
|
466
|
+
this.clearActive();
|
|
467
|
+
if (!isActive) menuItem.classList.add(this.props.className.active);
|
|
514
468
|
}
|
|
515
469
|
|
|
516
|
-
|
|
517
|
-
if (!this.dom.root
|
|
470
|
+
private clearActive(): void {
|
|
471
|
+
if (!this.dom.root) return;
|
|
518
472
|
all<HTMLElement>('[data-menu-item]', this.dom.root).forEach((item) => {
|
|
519
|
-
item.classList.remove(this.
|
|
473
|
+
item.classList.remove(this.props.className.active);
|
|
520
474
|
});
|
|
521
475
|
}
|
|
522
476
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
* @returns {Menu}
|
|
527
|
-
*/
|
|
528
|
-
setItems(items: MenuItem[]): this {
|
|
529
|
-
if (!this.options) throw new Error('Menu.setItems: instance destroyed.');
|
|
530
|
-
this._verifyItems(items);
|
|
531
|
-
|
|
532
|
-
this.options.items = items;
|
|
533
|
-
|
|
534
|
-
if (this._bound) {
|
|
535
|
-
if (this._element === false) {
|
|
536
|
-
const element = this._element;
|
|
537
|
-
this._teardown({ keepElement: true });
|
|
538
|
-
this._element = element;
|
|
539
|
-
this.build();
|
|
540
|
-
} else if (this.dom.root) {
|
|
541
|
-
const list = findMenuList(this.dom.root);
|
|
542
|
-
|
|
543
|
-
if (!list) {
|
|
544
|
-
throw new Error('Menu: [data-menu-list] element not found.');
|
|
545
|
-
}
|
|
477
|
+
private validateData(data: unknown): void {
|
|
478
|
+
validateParam('data', data, MENU_DATA_RULE, 'Menu');
|
|
479
|
+
}
|
|
546
480
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.cleanup.items = render(
|
|
551
|
-
() => items.map((item) => this._buildItem(item)),
|
|
552
|
-
list
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
this._bindEvents();
|
|
556
|
-
}
|
|
481
|
+
private assertStatePatchKey(key: string): void {
|
|
482
|
+
if (!Object.hasOwn(MENU_STATE_SCHEMA, key)) {
|
|
483
|
+
throw new Error(`Menu.setState: "${key}" is not a supported state key.`);
|
|
557
484
|
}
|
|
558
|
-
|
|
559
|
-
return this;
|
|
560
485
|
}
|
|
561
486
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
487
|
+
protected override normalizeStatePatch(
|
|
488
|
+
patch: Partial<MenuState>
|
|
489
|
+
): Partial<MenuState> {
|
|
490
|
+
const nextPatch = { ...patch };
|
|
491
|
+
if (Object.hasOwn(nextPatch, 'data')) {
|
|
492
|
+
nextPatch.data = cloneMenuData(nextPatch.data);
|
|
493
|
+
}
|
|
494
|
+
return nextPatch;
|
|
495
|
+
}
|
|
569
496
|
|
|
570
|
-
|
|
571
|
-
|
|
497
|
+
protected override validateStatePatch(patch: Partial<MenuState>): void {
|
|
498
|
+
validateParam(
|
|
499
|
+
'state',
|
|
500
|
+
patch,
|
|
501
|
+
{
|
|
502
|
+
type: 'plainObject',
|
|
503
|
+
},
|
|
504
|
+
'Menu.setState'
|
|
572
505
|
);
|
|
573
506
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
} else {
|
|
584
|
-
const children = arr[i].children;
|
|
585
|
-
if (children) removeFromArray(children);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
removeFromArray(this.options.items);
|
|
507
|
+
for (const key of Object.keys(patch)) {
|
|
508
|
+
this.assertStatePatchKey(key);
|
|
509
|
+
const stateKey = key as keyof typeof MENU_STATE_SCHEMA;
|
|
510
|
+
validateParam(
|
|
511
|
+
key,
|
|
512
|
+
patch[key as keyof MenuState],
|
|
513
|
+
MENU_STATE_SCHEMA[stateKey],
|
|
514
|
+
'Menu.setState'
|
|
515
|
+
);
|
|
590
516
|
}
|
|
591
|
-
|
|
592
|
-
return this;
|
|
593
517
|
}
|
|
594
518
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
this.
|
|
602
|
-
|
|
603
|
-
if (this._bound) {
|
|
604
|
-
this._teardown();
|
|
605
|
-
} else {
|
|
606
|
-
this._element = null;
|
|
607
|
-
this.dom.root = null;
|
|
608
|
-
this.cleanup?.events.clear();
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
this.cleanup = null;
|
|
612
|
-
this.options = null;
|
|
519
|
+
protected onDestroy(): void {
|
|
520
|
+
this.unbindEvents();
|
|
521
|
+
this.cleanup.state?.();
|
|
522
|
+
this.cleanup.state = null;
|
|
523
|
+
this.dom.root?.remove();
|
|
524
|
+
this.dom.list = null;
|
|
525
|
+
this.runtime.built = false;
|
|
613
526
|
}
|
|
614
527
|
}
|
|
615
528
|
|
|
616
|
-
export
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return new Menu(options, element);
|
|
529
|
+
export type Menu = MenuComponent;
|
|
530
|
+
|
|
531
|
+
export function createMenu(input: MenuProps = {}): Menu {
|
|
532
|
+
return new MenuComponent(input);
|
|
621
533
|
}
|