vanilla-jui 1.3.8 → 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,437 @@
1
+ import { jsx } from 'vanilla-signal';
2
+
3
+ import { randomId, resolveProps } from '../utilities/core.js';
4
+ import {
5
+ isNode,
6
+ isRenderableContent,
7
+ normalizeContentNodes,
8
+ requireRenderDOM,
9
+ resolveElement,
10
+ } from '../utilities/dom.js';
11
+ import { createEventManager } from '../utilities/events.js';
12
+
13
+ const DROP_PROPS_SCHEMA = {
14
+ name: { default: null, types: ['string', 'null'] },
15
+ mode: { default: 'click', type: 'string', enum: ['hover', 'click'] },
16
+ position: {
17
+ default: 'auto',
18
+ type: 'string',
19
+ enum: [
20
+ 'auto',
21
+ 'top-left',
22
+ 'top-center',
23
+ 'top-right',
24
+ 'bottom-left',
25
+ 'bottom-center',
26
+ 'bottom-right',
27
+ 'left',
28
+ 'right',
29
+ ],
30
+ },
31
+ offset: {
32
+ default: 10,
33
+ type: 'number',
34
+ validate: (value) => value >= 0,
35
+ message: 'expects a positive number or 0.',
36
+ },
37
+ content: {
38
+ default: '',
39
+ validate: isRenderableContent,
40
+ message: 'expects string, Node, array, function or null.',
41
+ },
42
+ className: { default: null, types: ['string', 'null'] },
43
+ id: {
44
+ default: null,
45
+ types: ['string', 'null'],
46
+ normalize: (value) => {
47
+ if (typeof value === 'string')
48
+ return value.trim() ? value.trim() : randomId();
49
+ if (value == null) return randomId();
50
+ return value;
51
+ },
52
+ },
53
+ containerClassName: { default: null, types: ['string', 'null'] },
54
+ delay: { default: 0, types: ['number', 'object'] },
55
+ hoverIntent: { default: true, type: 'boolean' },
56
+ onShown: { default: null, types: ['function', 'null'] },
57
+ onHidden: { default: null, types: ['function', 'null'] },
58
+ };
59
+
60
+ /**
61
+ * @typedef {object} DropProps
62
+ * @property {string|null} [name] 浮层名称,写入 data-drop。
63
+ * @property {"hover"|"click"} [mode="click"] 触发方式。
64
+ * @property {"auto"|"top-left"|"top-center"|"top-right"|"bottom-left"|"bottom-center"|"bottom-right"|"left"|"right"} [position="auto"] 浮层位置。
65
+ * @property {number} [offset=10] 浮层与触发元素的间距。
66
+ * @property {string|Node|Node[]|Function|null} [content=""] 浮层内容。
67
+ * @property {string|null} [className] 浮层额外类名。
68
+ * @property {string|null} [id] 浮层 id,不传时自动生成。
69
+ * @property {string|null} [containerClassName] 内容容器额外类名。
70
+ * @property {number|{show?:number,hide?:number}} [delay=0] 展示/隐藏延迟。
71
+ * @property {boolean} [hoverIntent=true] hover 模式下是否启用意图判断,减少误触发。
72
+ * @property {Function|null} [onShown] 展示后回调。
73
+ * @property {Function|null} [onHidden] 隐藏后回调。
74
+ */
75
+
76
+ /**
77
+ * 通用浮层组件。
78
+ *
79
+ * 可用于菜单、提示、下拉面板等场景,支持点击或 hover 触发,并自动计算视口内位置。
80
+ */
81
+ export class Drop {
82
+ /**
83
+ * 创建浮层实例。
84
+ * @param {Element|Node|string|Array} element 触发元素、选择器或 JSX/h 返回节点。
85
+ * @param {DropProps} [options={}] 浮层配置。
86
+ */
87
+ constructor(element, options = {}) {
88
+ requireRenderDOM('Drop');
89
+
90
+ this.target = resolveElement(element, 'Drop.element');
91
+ this.props = resolveProps(options, DROP_PROPS_SCHEMA, 'Drop');
92
+ this._init(this.props);
93
+ }
94
+
95
+ _init(props) {
96
+ this.isVisible = false;
97
+ this.root = null;
98
+ this.cleanup = {
99
+ events: createEventManager(),
100
+ };
101
+ this._timer = { show: null, hide: null };
102
+ this._hoverIntentData = { x: 0, y: 0, lastMoveTime: 0 };
103
+
104
+ const { delay } = props;
105
+ if (typeof delay === 'number' && delay >= 0) {
106
+ this.delayShow = delay;
107
+ this.delayHide = delay;
108
+ } else if (typeof delay === 'object' && delay !== null) {
109
+ this.delayShow = Number(delay.show) || 0;
110
+ this.delayHide = Number(delay.hide) || 0;
111
+ } else {
112
+ this.delayShow = 0;
113
+ this.delayHide = 0;
114
+ }
115
+
116
+ this._buildDrop(props);
117
+ this._bindEvents(props);
118
+ }
119
+
120
+ /**
121
+ * 创建浮层 DOM(不挂载到 document)。
122
+ * @private
123
+ */
124
+ _buildDrop(props) {
125
+ const { className, content, id, name, containerClassName } = props;
126
+ const _class = className || '';
127
+ const _wrapper =
128
+ isNode(content) && content.nodeType === 1
129
+ ? content
130
+ : jsx('div', {
131
+ className: containerClassName
132
+ ? `drop-container ${containerClassName}`
133
+ : 'drop-container',
134
+ children: normalizeContentNodes(content, this),
135
+ });
136
+
137
+ this.root = jsx('div', {
138
+ className: _class ? `j-drop ${_class}` : 'j-drop',
139
+ id: id,
140
+ 'data-drop': name || randomId(),
141
+ children: _wrapper,
142
+ });
143
+ }
144
+
145
+ /**
146
+ * 按触发模式绑定事件(仅 target 和 document)。
147
+ * @private
148
+ */
149
+ _bindEvents(props) {
150
+ const { mode, hoverIntent } = props;
151
+ this._unbindEvents();
152
+
153
+ if (mode === 'hover') {
154
+ if (hoverIntent) {
155
+ this.cleanup.events.on('target:enter', this.target, 'mouseenter', () =>
156
+ this._startHoverIntent()
157
+ );
158
+ this.cleanup.events.on('target:leave', this.target, 'mouseleave', () =>
159
+ this._cancelHoverIntent()
160
+ );
161
+ } else {
162
+ this.cleanup.events.on('target:enter', this.target, 'mouseenter', () =>
163
+ this.show()
164
+ );
165
+ this.cleanup.events.on('target:leave', this.target, 'mouseleave', () =>
166
+ this.hide()
167
+ );
168
+ }
169
+ } else if (mode === 'click') {
170
+ this.cleanup.events.on('target:click', this.target, 'click', () =>
171
+ this.toggle()
172
+ );
173
+ this.cleanup.events.on('document:click', document, 'click', (event) =>
174
+ this._docClick(event)
175
+ );
176
+ }
177
+ }
178
+
179
+ /**
180
+ * 绑定 root 浮层的 hover 事件(show 时调用)。
181
+ * @private
182
+ */
183
+ _bindRootEvents() {
184
+ if (!this.root || this.props.mode !== 'hover') return;
185
+ this.cleanup.events.on('root:enter', this.root, 'mouseenter', () =>
186
+ this.show()
187
+ );
188
+ this.cleanup.events.on('root:leave', this.root, 'mouseleave', () =>
189
+ this.hide()
190
+ );
191
+ }
192
+
193
+ /**
194
+ * 解绑 root 浮层的 hover 事件(hide 时调用)。
195
+ * @private
196
+ */
197
+ _unbindRootEvents() {
198
+ this.cleanup.events.off('root:enter');
199
+ this.cleanup.events.off('root:leave');
200
+ }
201
+
202
+ /**
203
+ * 解绑当前实例的触发事件。
204
+ * @private
205
+ * @returns {void}
206
+ */
207
+ _unbindEvents() {
208
+ this.cleanup?.events.clear();
209
+ }
210
+
211
+ /**
212
+ * 启动 hoverIntent 检测。
213
+ * @private
214
+ * @returns {void}
215
+ */
216
+ _startHoverIntent() {
217
+ this.cleanup.events.on('document:mousemove', document, 'mousemove', (e) =>
218
+ this._onMouseMove(e)
219
+ );
220
+ clearTimeout(this._timer.show);
221
+ this._timer.show = setTimeout(() => {
222
+ const now = Date.now();
223
+ const dt = now - this._hoverIntentData.lastMoveTime;
224
+ const dx = Math.abs(this._hoverIntentData.x - this._lastX || 0);
225
+ const dy = Math.abs(this._hoverIntentData.y - this._lastY || 0);
226
+ const dist = dx + dy;
227
+ if (dist < 5 || dt > 100) {
228
+ this.show();
229
+ this.cleanup.events.off('document:mousemove');
230
+ } else {
231
+ // 继续检测 hover 意图。
232
+ this._startHoverIntent();
233
+ }
234
+ }, this.delayShow);
235
+ }
236
+
237
+ /**
238
+ * 取消 hoverIntent 检测并隐藏浮层。
239
+ * @private
240
+ * @returns {void}
241
+ */
242
+ _cancelHoverIntent() {
243
+ clearTimeout(this._timer.show);
244
+ this.cleanup.events.off('document:mousemove');
245
+ this.hide();
246
+ }
247
+
248
+ /**
249
+ * 记录 hoverIntent 所需的鼠标移动状态。
250
+ * @private
251
+ * @param {MouseEvent} e 鼠标移动事件。
252
+ * @returns {void}
253
+ */
254
+ _onMouseMove(e) {
255
+ this._lastX = e.clientX;
256
+ this._lastY = e.clientY;
257
+ this._hoverIntentData.lastMoveTime = Date.now();
258
+ this._hoverIntentData.x = e.clientX;
259
+ this._hoverIntentData.y = e.clientY;
260
+ }
261
+
262
+ /**
263
+ * 计算并设置浮层位置。
264
+ * @private
265
+ * @returns {void}
266
+ */
267
+ _setPosition() {
268
+ const rect = this.target.getBoundingClientRect();
269
+ const drop = this.root;
270
+ const { offset, position } = this.props;
271
+
272
+ drop.style.visibility = 'hidden';
273
+ drop.style.display = 'block';
274
+ const dropRect = drop.getBoundingClientRect();
275
+ let top = 0;
276
+ let left = 0;
277
+
278
+ // auto 模式优先尝试上方居中,不够空间时放到下方居中。
279
+ let pos = position;
280
+ if (pos === 'auto') {
281
+ const spaceBelow = window.innerHeight - rect.bottom;
282
+ pos =
283
+ spaceBelow > dropRect.height + offset ? 'top-center' : 'bottom-center';
284
+ }
285
+
286
+ switch (pos) {
287
+ case 'top-left':
288
+ top = rect.top - dropRect.height - offset;
289
+ left = rect.left;
290
+ break;
291
+ case 'top-center':
292
+ top = rect.top - dropRect.height - offset;
293
+ left = rect.left + rect.width / 2 - dropRect.width / 2;
294
+ break;
295
+ case 'top-right':
296
+ top = rect.top - dropRect.height - offset;
297
+ left = rect.right - dropRect.width;
298
+ break;
299
+ case 'bottom-left':
300
+ top = rect.bottom + offset;
301
+ left = rect.left;
302
+ break;
303
+ case 'bottom-center':
304
+ top = rect.bottom + offset;
305
+ left = rect.left + rect.width / 2 - dropRect.width / 2;
306
+ break;
307
+ case 'bottom-right':
308
+ top = rect.bottom + offset;
309
+ left = rect.right - dropRect.width;
310
+ break;
311
+ case 'left':
312
+ top = rect.top + rect.height / 2 - dropRect.height / 2;
313
+ left = rect.left - dropRect.width - offset;
314
+ break;
315
+ case 'right':
316
+ top = rect.top + rect.height / 2 - dropRect.height / 2;
317
+ left = rect.right + offset;
318
+ break;
319
+ default:
320
+ break;
321
+ }
322
+
323
+ // 限制在视口内。
324
+ top = Math.max(8, Math.min(top, window.innerHeight - dropRect.height - 8));
325
+ left = Math.max(8, Math.min(left, window.innerWidth - dropRect.width - 8));
326
+
327
+ drop.style.top = `${top + window.scrollY}px`;
328
+ drop.style.left = `${left + window.scrollX}px`;
329
+ drop.style.visibility = '';
330
+ drop.style.display = '';
331
+ }
332
+
333
+ _docClick(e) {
334
+ if (!this.root.contains(e.target) && !this.target.contains(e.target)) {
335
+ this.hide();
336
+ }
337
+ }
338
+
339
+ /**
340
+ * 应用展示或隐藏状态。
341
+ * @private
342
+ */
343
+ _exec(visible) {
344
+ if (!this.root) return;
345
+
346
+ if (visible) {
347
+ if (!this.root.parentNode) document.body.appendChild(this.root);
348
+ this._bindRootEvents();
349
+ this._setPosition();
350
+ } else {
351
+ this._unbindRootEvents();
352
+ this.root.style.top = '';
353
+ this.root.style.left = '';
354
+ if (this.root.parentNode) this.root.parentNode.removeChild(this.root);
355
+ }
356
+
357
+ this.root.classList.toggle('is-active', visible);
358
+ this.isVisible = visible;
359
+ }
360
+
361
+ // ========== 公开 API ==========
362
+
363
+ /**
364
+ * 展示浮层。
365
+ * @param {boolean} [useDelay=true] 是否应用展示延迟。
366
+ * @returns {void}
367
+ */
368
+ show(useDelay = true) {
369
+ clearTimeout(this._timer.hide);
370
+ if (this.isVisible) return;
371
+
372
+ if (useDelay && this.delayShow > 0) {
373
+ clearTimeout(this._timer.show);
374
+ this._timer.show = setTimeout(() => this._exec(true), this.delayShow);
375
+ } else {
376
+ this._exec(true);
377
+ }
378
+
379
+ if (this.props.onShown) this.props.onShown();
380
+ }
381
+
382
+ /**
383
+ * 隐藏浮层。
384
+ * @param {boolean} [useDelay=true] 是否应用隐藏延迟。
385
+ * @returns {void}
386
+ */
387
+ hide(useDelay = true) {
388
+ clearTimeout(this._timer.show);
389
+ if (!this.isVisible) return;
390
+
391
+ if (useDelay && this.delayHide > 0) {
392
+ clearTimeout(this._timer.hide);
393
+ this._timer.hide = setTimeout(() => this._exec(false), this.delayHide);
394
+ } else {
395
+ this._exec(false);
396
+ }
397
+
398
+ if (this.props.onHidden) this.props.onHidden();
399
+ }
400
+
401
+ /**
402
+ * 切换浮层显示状态。
403
+ * @returns {void}
404
+ */
405
+ toggle() {
406
+ if (this.isVisible) {
407
+ this.hide();
408
+ } else {
409
+ this.show();
410
+ }
411
+ }
412
+
413
+ /**
414
+ * 销毁当前浮层实例并移除 DOM。
415
+ * @returns {void}
416
+ */
417
+ destroy() {
418
+ if (!this.props) return;
419
+
420
+ clearTimeout(this._timer?.show);
421
+ clearTimeout(this._timer?.hide);
422
+
423
+ this._unbindEvents();
424
+ if (this.root?.parentNode) this.root.parentNode.removeChild(this.root);
425
+
426
+ this.props = null;
427
+ this.root = null;
428
+ this.target = null;
429
+ this._timer = { show: null, hide: null };
430
+ this.cleanup?.events.clear();
431
+ this.cleanup = null;
432
+ }
433
+ }
434
+
435
+ export function createDrop(container, input = {}) {
436
+ return new Drop(container, input);
437
+ }