vanilla-jui 1.3.9 → 1.3.10

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.
Files changed (77) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.umd.js +1 -1
  3. package/dist/style.css +1 -1
  4. package/package.json +3 -2
  5. package/src/components/accordion.js +373 -0
  6. package/src/components/components.js +0 -0
  7. package/src/components/drop.js +437 -0
  8. package/src/components/flow.js +1133 -0
  9. package/src/components/form.js +585 -0
  10. package/src/components/icons.js +83 -0
  11. package/src/components/menu.js +422 -0
  12. package/src/components/modal.js +1174 -0
  13. package/src/components/offcanvas.js +332 -0
  14. package/src/components/pagination.js +536 -0
  15. package/src/components/parabola.js +295 -0
  16. package/src/components/sticky.js +271 -0
  17. package/src/components/swiper.js +1123 -0
  18. package/src/components/tabs.js +746 -0
  19. package/src/components/theme.js +353 -0
  20. package/src/components/toast.js +329 -0
  21. package/src/components/toc.js +226 -0
  22. package/src/components/tooltip.js +86 -0
  23. package/src/components/validator.js +586 -0
  24. package/src/core/Component.js +248 -0
  25. package/src/css/accordion.css +72 -0
  26. package/src/css/animation.css +177 -0
  27. package/src/css/avatar.css +62 -0
  28. package/src/css/badge.css +66 -0
  29. package/src/css/breadcrumb.css +106 -0
  30. package/src/css/button.css +296 -0
  31. package/src/css/card.css +151 -0
  32. package/src/css/color.css +1176 -0
  33. package/src/css/color_p3.css +1184 -0
  34. package/src/css/components.css +26 -0
  35. package/src/css/content.css +716 -0
  36. package/src/css/divider.css +9 -0
  37. package/src/css/drop.css +28 -0
  38. package/src/css/flow.css +86 -0
  39. package/src/css/font.css +23 -0
  40. package/src/css/form.css +919 -0
  41. package/src/css/gradient.css +211 -0
  42. package/src/css/icon.css +63 -0
  43. package/src/css/index.css +12 -0
  44. package/src/css/layout.css +782 -0
  45. package/src/css/loading.css +38 -0
  46. package/src/css/menu.css +564 -0
  47. package/src/css/modal.css +111 -0
  48. package/src/css/offcanvas.css +104 -0
  49. package/src/css/pagination.css +25 -0
  50. package/src/css/popup.css +69 -0
  51. package/src/css/skeleton.css +76 -0
  52. package/src/css/status.css +56 -0
  53. package/src/css/swiper.css +160 -0
  54. package/src/css/table.css +52 -0
  55. package/src/css/tabs.css +95 -0
  56. package/src/css/tag.css +98 -0
  57. package/src/css/theme-palette.css +59 -0
  58. package/src/css/tip.css +84 -0
  59. package/src/css/toast.css +154 -0
  60. package/src/css/toast.min.css +1 -0
  61. package/src/css/toc.css +38 -0
  62. package/src/css/tokens.css +1324 -0
  63. package/src/css/toolbar.css +115 -0
  64. package/src/css/tooltip.css +19 -0
  65. package/src/icons/index.js +30 -0
  66. package/src/index.js +45 -0
  67. package/src/locales/index.js +45 -0
  68. package/src/locales/index.min.js +1 -0
  69. package/src/utilities/browser.js +111 -0
  70. package/src/utilities/cache.js +159 -0
  71. package/src/utilities/core.js +502 -0
  72. package/src/utilities/debug.js +103 -0
  73. package/src/utilities/dom.js +313 -0
  74. package/src/utilities/events.js +69 -0
  75. package/src/utilities/http.js +30 -0
  76. package/src/utilities/legacy-dom.js +179 -0
  77. package/src/utilities/storage.js +46 -0
@@ -0,0 +1,422 @@
1
+ import { render, jsx } from 'vanilla-signal';
2
+
3
+ import { randomId, resolveProps, validateParam } from '../utilities/core.js';
4
+ import { q, requireRenderDOM, resolveElement } from '../utilities/dom.js';
5
+ import { createEventManager } from '../utilities/events.js';
6
+ import { icon } from './icons.js';
7
+
8
+ const MENU_OPTIONS_SCHEMA = {
9
+ type: { default: 'mobile', type: 'string' },
10
+ id: {
11
+ default: '',
12
+ type: 'string',
13
+ normalize: (value) => (value.trim() === '' ? randomId() : value.trim()),
14
+ },
15
+ items: { default: [], type: 'array' },
16
+ backText: { default: 'Back', type: 'string' },
17
+ };
18
+
19
+ const MENU_ITEMS_RULE = { type: 'array' };
20
+
21
+ /**
22
+ * @typedef {object} MenuItem
23
+ * @property {string|number} [id] 菜单项 id,会用于生成 DOM id。
24
+ * @property {string} title 菜单项标题。
25
+ * @property {string} [url="#"] 菜单链接。
26
+ * @property {string} [target] 链接 target。
27
+ * @property {string[]} [classes] 菜单项额外类名。
28
+ * @property {MenuItem[]} [children] 子菜单列表。
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} MenuOptions
33
+ * @property {string} [type="mobile"] 菜单类型,对应 `j-${type}-menu` 类名,可选值为 `mobile` 或 `bottom`。
34
+ * @property {string} [id] 菜单 ul 节点 id,不传时自动生成。
35
+ * @property {MenuItem[]} [items=[]] 菜单数据。
36
+ * @property {string} [backText="Back"] 移动端子菜单返回按钮文案。
37
+ */
38
+
39
+ /**
40
+ * 菜单组件。
41
+ *
42
+ * 支持绑定已有菜单 DOM,也支持通过配置动态创建移动菜单或底部菜单。
43
+ */
44
+ export class Menu {
45
+ /**
46
+ * 创建菜单实例。
47
+ * @param {MenuOptions} [options={}] 菜单配置。
48
+ * @param {Element|Node|string|Array|false} [element=false] 已有菜单节点、选择器或 JSX/h 返回节点;默认 `false` 按 items 动态创建。
49
+ */
50
+ constructor(options = {}, element = false) {
51
+ this.options = resolveProps(options, MENU_OPTIONS_SCHEMA, 'Menu.options');
52
+ this._element = element;
53
+ this.dom = {
54
+ root: null,
55
+ };
56
+ this.cleanup = {
57
+ events: createEventManager(),
58
+ };
59
+ this._bound = false;
60
+ this._destroyed = false;
61
+ }
62
+
63
+ /**
64
+ * 校验菜单数据。
65
+ * @private
66
+ * @param {MenuItem[]} items 菜单数据。
67
+ * @returns {void}
68
+ */
69
+ _verifyItems(items) {
70
+ validateParam('items', items, MENU_ITEMS_RULE, 'Menu');
71
+ }
72
+
73
+ /**
74
+ * 构建菜单。
75
+ *
76
+ * element 为 false 时动态创建 DOM;否则绑定已有节点。
77
+ * @returns {Menu}
78
+ */
79
+ build() {
80
+ if (this._bound) return this;
81
+
82
+ requireRenderDOM('Menu');
83
+
84
+ if (this._element === false) {
85
+ this.dom.root = this._buildRoot();
86
+ this._bound = true;
87
+ } else {
88
+ this.dom.root = resolveElement(this._element, 'Menu.element');
89
+ this._bound = true;
90
+ }
91
+
92
+ this._bindEvents();
93
+
94
+ return this;
95
+ }
96
+
97
+ /**
98
+ * 根据 items 创建菜单根节点。
99
+ * @private
100
+ * @returns {HTMLElement}
101
+ */
102
+ _buildRoot() {
103
+ const { items, id, type } = this.options;
104
+
105
+ return jsx('nav', {
106
+ className: `j-${type}-menu`,
107
+ children: jsx('ul', {
108
+ className: 'menu',
109
+ id: id,
110
+ children: items.map((item) => this._buildItem(item)),
111
+ }),
112
+ });
113
+ }
114
+
115
+ /**
116
+ * 递归创建菜单项。
117
+ * @private
118
+ * @param {MenuItem} item 菜单项配置。
119
+ * @returns {HTMLElement}
120
+ */
121
+ _buildItem(item) {
122
+ const hasChildren = item.children && item.children.length > 0;
123
+ const classes = ['menu-item'];
124
+
125
+ if (hasChildren) {
126
+ classes.push('menu-item-has-children');
127
+ }
128
+
129
+ if (item.classes && Array.isArray(item.classes)) {
130
+ classes.push(...item.classes);
131
+ }
132
+
133
+ const children = [
134
+ jsx('a', {
135
+ className: 'menu-link',
136
+ href: item.url || '',
137
+ ...(item.target && { target: item.target }),
138
+ children: item.title,
139
+ }),
140
+ ];
141
+
142
+ if (hasChildren) {
143
+ children.push(
144
+ jsx('ul', {
145
+ className: 'sub-menu',
146
+ children: item.children.map((child) => this._buildItem(child)),
147
+ })
148
+ );
149
+ }
150
+
151
+ return jsx('li', {
152
+ className: classes.join(' '),
153
+ id: `menu-item-${item.id || randomId()}`,
154
+ children,
155
+ });
156
+ }
157
+
158
+ /**
159
+ * 根据菜单类型绑定交互事件。
160
+ * @private
161
+ * @returns {void}
162
+ */
163
+ _bindEvents() {
164
+ if (!this.dom.root) return;
165
+
166
+ if (this.options.type === 'mobile') {
167
+ this.cleanup.events.on('mobile', this.dom.root, 'click', (e) => {
168
+ const target = e.target;
169
+
170
+ if (target.closest('.menu-item.back')) {
171
+ e.preventDefault();
172
+ this._handleBack(target);
173
+ return;
174
+ }
175
+
176
+ const menuItem = target.closest('.menu-item.menu-item-has-children');
177
+
178
+ if (menuItem) {
179
+ const directLink = q(':scope > a', menuItem);
180
+ if (
181
+ directLink &&
182
+ (target === directLink || directLink.contains(target))
183
+ ) {
184
+ e.preventDefault();
185
+ this._handleMenuClick(menuItem);
186
+ }
187
+ }
188
+ });
189
+ } else if (this.options.type === 'bottom') {
190
+ this.cleanup.events.on('bottom', document, 'click', (e) => {
191
+ const target = e.target;
192
+
193
+ // 检查是否点击的是子菜单中的链接
194
+ const isSubmenuLink = target.closest('.sub-menu a');
195
+ if (isSubmenuLink) {
196
+ // 如果是子菜单链接,不阻止默认行为,允许跳转
197
+ return;
198
+ }
199
+
200
+ // 只允许第一层 menu-item-has-children 被点击
201
+ const firstLevelMenuItem = target.closest(
202
+ '.menu > .menu-item.menu-item-has-children'
203
+ );
204
+
205
+ if (firstLevelMenuItem) {
206
+ e.preventDefault();
207
+ this._toggleActive(firstLevelMenuItem);
208
+ } else {
209
+ // 点击其他地方移除所有 is-active
210
+ q('.menu-item.is-active', this.dom.root)?.classList.remove(
211
+ 'is-active'
212
+ );
213
+ }
214
+ });
215
+ }
216
+ }
217
+
218
+ /**
219
+ * 解绑当前菜单实例绑定的事件。
220
+ * @private
221
+ * @returns {void}
222
+ */
223
+ _unbindEvents() {
224
+ this.cleanup.events.clear();
225
+ }
226
+
227
+ /**
228
+ * 清理当前构建出的 DOM 与事件,可选择保留实例引用用于重建。
229
+ * @private
230
+ * @param {object} [options={}] 清理选项。
231
+ * @param {boolean} [options.keepElement=false] 是否保留初始 element 引用。
232
+ * @returns {void}
233
+ */
234
+ _teardown({ keepElement = false } = {}) {
235
+ this._unbindEvents();
236
+ this.cleanup.items?.();
237
+ this.cleanup.items = null;
238
+
239
+ if (this._element === false && this.dom.root?.parentElement) {
240
+ this.dom.root.remove();
241
+ }
242
+
243
+ if (!keepElement) {
244
+ this._element = null;
245
+ }
246
+
247
+ this.dom.root = null;
248
+ this._bound = false;
249
+ }
250
+
251
+ /**
252
+ * 处理移动端有子菜单项的进入操作。
253
+ * @private
254
+ * @param {HTMLElement} menuItem 菜单项节点。
255
+ * @returns {void}
256
+ */
257
+ _handleMenuClick(menuItem) {
258
+ menuItem.classList.add('is-active');
259
+
260
+ const subMenu = q(':scope > .sub-menu', menuItem);
261
+ if (!subMenu) return;
262
+
263
+ const existingBack = q(':scope > .menu-item.back', subMenu);
264
+ if (existingBack) return;
265
+
266
+ const backButton = jsx('li', {
267
+ className: 'menu-item back',
268
+ children: jsx('a', {
269
+ className: 'menu-link',
270
+ children: [
271
+ jsx('icon', {
272
+ className: 'el-icon el-prefix',
273
+ children: icon('arrow-left'),
274
+ }),
275
+ jsx('span', {
276
+ className: 'menu-text',
277
+ children: this.options.backText,
278
+ }),
279
+ ],
280
+ }),
281
+ });
282
+
283
+ subMenu.insertBefore(backButton, subMenu.firstChild);
284
+ }
285
+
286
+ /**
287
+ * 处理移动端子菜单返回操作。
288
+ * @private
289
+ * @param {Element} target 点击目标。
290
+ * @returns {void}
291
+ */
292
+ _handleBack(target) {
293
+ const backItem = target.closest('.menu-item.back');
294
+ if (!backItem) return;
295
+
296
+ const subMenu = backItem.parentElement;
297
+ const parentMenuItem = subMenu.parentElement;
298
+
299
+ if (
300
+ parentMenuItem &&
301
+ parentMenuItem.classList.contains('menu-item-has-children')
302
+ ) {
303
+ parentMenuItem.classList.remove('is-active');
304
+ }
305
+
306
+ backItem.remove();
307
+ }
308
+
309
+ /**
310
+ * 切换底部菜单激活状态。
311
+ * @private
312
+ * @param {HTMLElement} menuItem 菜单项节点。
313
+ * @returns {void}
314
+ */
315
+ _toggleActive(menuItem) {
316
+ const isActive = menuItem.classList.contains('is-active');
317
+
318
+ // 移除其他所有 is-active
319
+ q('.menu-item.is-active', this.dom.root)?.classList.remove('is-active');
320
+
321
+ // 切换当前项的 is-active
322
+ if (!isActive) {
323
+ menuItem.classList.add('is-active');
324
+ }
325
+ }
326
+
327
+ /**
328
+ * 替换菜单数据;动态创建的菜单会在已构建时重建 DOM。
329
+ * @param {MenuItem[]} items 新菜单数据。
330
+ * @returns {Menu}
331
+ */
332
+ setItems(items) {
333
+ this._verifyItems(items);
334
+
335
+ this.options.items = items;
336
+
337
+ if (this._bound) {
338
+ if (this._element === false) {
339
+ const element = this._element;
340
+ this._teardown({ keepElement: true });
341
+ this._element = element;
342
+ this.build();
343
+ } else {
344
+ const rootIsList =
345
+ this.dom.root.matches?.('.menu') ||
346
+ this.dom.root.matches?.('ul') ||
347
+ this.dom.root.matches?.('ol');
348
+ const list = rootIsList
349
+ ? this.dom.root
350
+ : q(':scope > .menu', this.dom.root) || q('.menu', this.dom.root);
351
+
352
+ if (!list) {
353
+ throw new Error('Menu: .menu element not found for setItems().');
354
+ }
355
+
356
+ this._unbindEvents();
357
+ this.cleanup.items?.();
358
+ this.cleanup.items = render(
359
+ () => items.map((item) => this._buildItem(item)),
360
+ list
361
+ );
362
+ this._bindEvents();
363
+ }
364
+ }
365
+
366
+ return this;
367
+ }
368
+
369
+ /**
370
+ * 根据 id 移除菜单项。
371
+ * @param {string|number} id 菜单项 id。
372
+ * @returns {Menu}
373
+ */
374
+ removeItem(id) {
375
+ if (!this.dom.root) return this;
376
+
377
+ const item = q(`[id^="menu-item-${id}"]`, this.dom.root);
378
+
379
+ if (item) {
380
+ item.remove();
381
+ }
382
+
383
+ if (this.options.items) {
384
+ const removeFromArray = (arr) => {
385
+ for (let i = arr.length - 1; i >= 0; i--) {
386
+ if (arr[i].id === id) {
387
+ arr.splice(i, 1);
388
+ } else if (arr[i].children) {
389
+ removeFromArray(arr[i].children);
390
+ }
391
+ }
392
+ };
393
+ removeFromArray(this.options.items);
394
+ }
395
+
396
+ return this;
397
+ }
398
+
399
+ /**
400
+ * 销毁当前菜单实例并解绑事件。
401
+ * @returns {void}
402
+ */
403
+ destroy() {
404
+ if (this._destroyed) return;
405
+ this._destroyed = true;
406
+
407
+ if (this._bound) {
408
+ this._teardown();
409
+ } else {
410
+ this._element = null;
411
+ this.dom.root = null;
412
+ this.cleanup.events.clear();
413
+ }
414
+
415
+ this.cleanup = null;
416
+ this.options = null;
417
+ }
418
+ }
419
+
420
+ export function createMenu(element, options = {}) {
421
+ return new Menu(element, options);
422
+ }