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,746 @@
1
+ import {
2
+ bindAttr,
3
+ bindClass,
4
+ createDeepStore,
5
+ createRoot,
6
+ flushSync,
7
+ insert,
8
+ jsx,
9
+ } from 'vanilla-signal';
10
+
11
+ import Component from '../core/Component.js';
12
+ import { randomId, resolveProps, validateParam } from '../utilities/core.js';
13
+ import {
14
+ isRenderableContent,
15
+ createLoading,
16
+ normalizeContentNodes,
17
+ q,
18
+ requireContainer,
19
+ requireRenderDOM,
20
+ } from '../utilities/dom.js';
21
+
22
+ const TABS_PROPS_SCHEMA = {
23
+ id: {
24
+ default: null,
25
+ types: ['string', 'null'],
26
+ normalize: (value) => {
27
+ if (typeof value === 'string')
28
+ return value.trim() ? value.trim() : randomId();
29
+ if (value == null) return randomId();
30
+ return value;
31
+ },
32
+ },
33
+ direction: {
34
+ default: 'top',
35
+ type: 'string',
36
+ enum: ['top', 'bottom', 'left', 'right'],
37
+ },
38
+ active: { default: 0, types: ['number', 'string'] },
39
+ disabled: { default: [], types: ['number', 'string', 'array'] },
40
+ onChange: { default: null, types: ['function', 'null'] },
41
+ tabs: { default: [], type: 'array' },
42
+ onAdd: { default: null, types: ['function', 'null'] },
43
+ onRemove: { default: null, types: ['function', 'null'] },
44
+ };
45
+
46
+ const TAB_CONFIG_RULE = {
47
+ type: 'object',
48
+ validate: (value) =>
49
+ !!value &&
50
+ isRenderableContent(value.title) &&
51
+ isRenderableContent(value.panel),
52
+ message:
53
+ 'expects an object with renderable title and panel: string, Node, array, function or null.',
54
+ };
55
+
56
+ function cloneTabItems(tabs) {
57
+ return Array.isArray(tabs) ? tabs.map((t) => ({ ...t })) : [];
58
+ }
59
+
60
+ function normalizeTtl(ttl) {
61
+ return typeof ttl === 'number' && ttl > 0 ? ttl : 0;
62
+ }
63
+
64
+ /**
65
+ * 标签页组件,继承 Component。
66
+ *
67
+ * DOM 创建一次,通过 createEffect 细粒度更新 class/ARIA。
68
+ */
69
+ export class Tabs extends Component {
70
+ /**
71
+ * @param {Element|Node|string|Array} container 挂载容器(元素、选择器或 JSX/h 返回节点)。
72
+ * @param {object} [input={}] 标签页配置。
73
+ */
74
+ constructor(container, input = {}) {
75
+ requireRenderDOM('Tabs');
76
+
77
+ const el = requireContainer(container, 'Tabs');
78
+
79
+ const props = resolveProps(input, TABS_PROPS_SCHEMA, 'Tabs');
80
+ super(props);
81
+
82
+ this.dom.container = el;
83
+
84
+ this.state = createDeepStore({
85
+ current: {
86
+ index: -1,
87
+ name: null,
88
+ },
89
+ disabled: this._parseDisabled(props.disabled),
90
+ isVertical: props.direction === 'left' || props.direction === 'right',
91
+ draggable: false,
92
+ loading: false,
93
+ });
94
+
95
+ this.runtime.cache = { panels: new Map() };
96
+ this.runtime.panelLoadId = 0;
97
+
98
+ try {
99
+ this.onInit(props);
100
+ } catch (error) {
101
+ this.destroy();
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ onInit(props) {
107
+ this.root = this.buildRoot(props);
108
+ this.rebuildItems();
109
+ void this._activate(props.active, false);
110
+ this.bindEvents();
111
+
112
+ this._initDrag();
113
+ }
114
+
115
+ buildRoot(props) {
116
+ const { id, direction } = props;
117
+ const nav = jsx('nav', { className: 'tab-list' });
118
+ const wrap = jsx('div', { className: 'tab-wrap', children: nav });
119
+ const panelWrapper = jsx('div', { className: 'tab-panel' });
120
+
121
+ return jsx('div', {
122
+ className: `j-tabs is-${direction}`,
123
+ id,
124
+ children: [wrap, panelWrapper],
125
+ });
126
+ }
127
+
128
+ /**
129
+ * 清空 root 内容并重建 tab-list 和 tab-panel。
130
+ * @private
131
+ */
132
+ rebuildItems() {
133
+ const tabList = q('.tab-list', this.root);
134
+ const panelWrapper = q('.tab-panel', this.root);
135
+ if (!tabList || !panelWrapper) return;
136
+
137
+ tabList.textContent = '';
138
+ panelWrapper.textContent = '';
139
+
140
+ this.dom.tabs = [];
141
+ this.dom.panels = [];
142
+ this.dom.panelBodies = [];
143
+
144
+ const tabFragment = document.createDocumentFragment();
145
+ const panelFragment = document.createDocumentFragment();
146
+
147
+ this.props.tabs.forEach((item, index) => {
148
+ const name = item.name || randomId();
149
+
150
+ const tab = jsx('div', {
151
+ className: 'tab-item',
152
+ 'data-tab': name,
153
+ children: jsx('span', {
154
+ children: normalizeContentNodes(item.title, { tabs: this, item }),
155
+ }),
156
+ });
157
+
158
+ this.dom.tabs.push(tab);
159
+ const body = jsx('div');
160
+ this.dom.panels.push(
161
+ jsx('div', {
162
+ className: 'panel-item',
163
+ role: 'tabpanel',
164
+ children: body,
165
+ })
166
+ );
167
+ this.dom.panelBodies.push(body);
168
+ if (typeof item.panel !== 'function') {
169
+ body.append(
170
+ ...normalizeContentNodes(item.panel, {
171
+ tabs: this,
172
+ item,
173
+ index,
174
+ name,
175
+ })
176
+ );
177
+ }
178
+ tabFragment.append(tab);
179
+ panelFragment.append(this.dom.panels[this.dom.panels.length - 1]);
180
+ });
181
+
182
+ tabList.append(tabFragment);
183
+ panelWrapper.append(panelFragment);
184
+
185
+ this.cleanup.bindings?.();
186
+ this.cleanup.bindings = createRoot((dispose) => {
187
+ this.dom.tabs.forEach((tab, index) => {
188
+ const name = tab.dataset.tab;
189
+ bindClass(tab, 'is-active', () => this.state.current.index === index);
190
+ bindClass(tab, 'is-disabled', () =>
191
+ this.state.disabled.names.includes(name)
192
+ );
193
+ bindAttr(
194
+ tab,
195
+ 'aria-selected',
196
+ () => this.state.current.index === index
197
+ );
198
+ bindAttr(tab, 'aria-disabled', () =>
199
+ this.state.disabled.names.includes(name)
200
+ );
201
+ });
202
+ this.dom.panels.forEach((panel, index) => {
203
+ bindClass(panel, 'is-active', () => this.state.current.index === index);
204
+ bindAttr(
205
+ panel,
206
+ 'aria-hidden',
207
+ () => this.state.current.index !== index
208
+ );
209
+ });
210
+ return dispose;
211
+ });
212
+ }
213
+
214
+ _parseDisabled(disabled) {
215
+ if (disabled == null) {
216
+ return {
217
+ names: [],
218
+ indexes: [],
219
+ };
220
+ }
221
+ const toName = (val) => {
222
+ if (typeof val === 'number') return this.props.tabs[val]?.name || null;
223
+ if (typeof val === 'string') return val;
224
+ return null;
225
+ };
226
+ const names = Array.isArray(disabled)
227
+ ? disabled.map(toName).filter(Boolean)
228
+ : (() => {
229
+ const name = toName(disabled);
230
+ return name ? [name] : [];
231
+ })();
232
+
233
+ return this._createDisabledState(names);
234
+ }
235
+
236
+ _createDisabledState(names) {
237
+ const uniqNames = Array.from(new Set(names));
238
+ return {
239
+ names: uniqNames,
240
+ indexes: uniqNames
241
+ .map((name) => this.props.tabs.findIndex((tab) => tab.name === name))
242
+ .filter((index) => index >= 0),
243
+ };
244
+ }
245
+
246
+ _syncCurrent(index) {
247
+ this.state.current = {
248
+ index,
249
+ name:
250
+ index >= 0 && index < this.dom.tabs.length
251
+ ? this.dom.tabs[index]?.dataset.tab || null
252
+ : null,
253
+ };
254
+ }
255
+
256
+ _getPanelKey(item, index) {
257
+ return item.name || this.dom.tabs[index]?.dataset.tab || String(index);
258
+ }
259
+
260
+ _getCachedPanel(item, index) {
261
+ if (!item?.cache) return null;
262
+
263
+ const entry = this.runtime.cache.panels.get(this._getPanelKey(item, index));
264
+ if (!entry) return null;
265
+
266
+ const ttl = normalizeTtl(item.ttl);
267
+ if (ttl && Date.now() - entry.updatedAt > ttl) {
268
+ this.runtime.cache.panels.delete(this._getPanelKey(item, index));
269
+ return null;
270
+ }
271
+
272
+ return entry;
273
+ }
274
+
275
+ _setCachedPanel(item, index, content) {
276
+ if (!item?.cache) return;
277
+
278
+ this.runtime.cache.panels.set(this._getPanelKey(item, index), {
279
+ content,
280
+ updatedAt: Date.now(),
281
+ });
282
+ }
283
+
284
+ _renderPanelContent(index, content) {
285
+ const body = this.dom.panelBodies[index];
286
+ const item = this.props.tabs[index];
287
+ if (!body || !item) return;
288
+
289
+ body.textContent = '';
290
+ body.append(
291
+ ...normalizeContentNodes(content, {
292
+ tabs: this,
293
+ item,
294
+ index,
295
+ name: this.dom.tabs[index]?.dataset.tab || item.name || index,
296
+ })
297
+ );
298
+ }
299
+
300
+ async _loadPanel(index) {
301
+ const item = this.props.tabs[index];
302
+ const body = this.dom.panelBodies[index];
303
+ if (!item || !body || typeof item.panel !== 'function') {
304
+ this.runtime.panelLoadId += 1;
305
+ flushSync(() => {
306
+ this.state.loading = false;
307
+ });
308
+ return;
309
+ }
310
+
311
+ const cached = this._getCachedPanel(item, index);
312
+ if (cached) {
313
+ this.runtime.panelLoadId += 1;
314
+ flushSync(() => {
315
+ this.state.loading = false;
316
+ });
317
+ this._renderPanelContent(index, cached.content);
318
+ return;
319
+ }
320
+
321
+ const loadId = ++this.runtime.panelLoadId;
322
+ flushSync(() => {
323
+ this.state.loading = true;
324
+ });
325
+ body.textContent = '';
326
+ body.style.minHeight = '80px';
327
+ body.appendChild(createLoading());
328
+
329
+ try {
330
+ const content = await Promise.resolve(
331
+ item.panel({
332
+ tabs: this,
333
+ item,
334
+ index,
335
+ name: this.dom.tabs[index]?.dataset.tab || item.name || index,
336
+ })
337
+ );
338
+
339
+ if (this.runtime.destroyed || loadId !== this.runtime.panelLoadId) return;
340
+
341
+ this._setCachedPanel(item, index, content);
342
+ this._renderPanelContent(index, content);
343
+ } finally {
344
+ if (!this.runtime.destroyed && loadId === this.runtime.panelLoadId) {
345
+ flushSync(() => {
346
+ this.state.loading = false;
347
+ body.style.minHeight = '';
348
+ });
349
+ }
350
+ }
351
+ }
352
+
353
+ get activeIndex() {
354
+ return this.state.current.index;
355
+ }
356
+
357
+ get disabledNames() {
358
+ return this.state.disabled.names;
359
+ }
360
+
361
+ bindEvents() {
362
+ this.unbindEvents();
363
+
364
+ const tabList = q('.tab-list', this.root);
365
+ if (!tabList) return;
366
+
367
+ this.cleanup.events.on('tabClick', tabList, 'click', (e) => {
368
+ const tab = e.target.closest('.tab-item');
369
+ if (!tab) return;
370
+ const name = tab.dataset.tab;
371
+ if (name && !this.state.disabled.names.includes(name)) {
372
+ void this.activate(name);
373
+ }
374
+ });
375
+ }
376
+
377
+ unbindEvents() {
378
+ this.cleanup.events.clear();
379
+ }
380
+
381
+ assertActive(method) {
382
+ if (this.runtime.destroyed) {
383
+ throw new Error(`Tabs.${method}: instance has been destroyed.`);
384
+ }
385
+ }
386
+
387
+ _getIndex(val) {
388
+ if (typeof val === 'number') return val;
389
+ if (typeof val === 'string') {
390
+ return this.dom.tabs.findIndex((tab) => tab.dataset.tab === val);
391
+ }
392
+ return -1;
393
+ }
394
+
395
+ async _activate(val, fireEvent = true) {
396
+ const index = this._getIndex(val);
397
+ if (
398
+ index < 0 ||
399
+ index >= this.dom.tabs.length ||
400
+ this.state.disabled.names.includes(this.dom.tabs[index]?.dataset.tab) ||
401
+ this.state.current.index === index
402
+ ) {
403
+ return;
404
+ }
405
+
406
+ flushSync(() => {
407
+ this._syncCurrent(index);
408
+ });
409
+
410
+ await this._loadPanel(index);
411
+
412
+ if (fireEvent && this.props.onChange) {
413
+ const tabEl = this.dom.tabs[index];
414
+ const panelEl = this.dom.panels[index];
415
+ await Promise.resolve(
416
+ this.props.onChange(index, tabEl?.dataset.tab || index, tabEl, panelEl)
417
+ );
418
+ }
419
+ }
420
+
421
+ /**
422
+ * 激活指定标签。
423
+ * @param {number|string} val 标签索引或 `data-tab` 名称。
424
+ */
425
+ async activate(val) {
426
+ this.assertActive('activate');
427
+ await this._activate(val, true);
428
+ }
429
+
430
+ /**
431
+ * 将组件挂载到构造器指定的容器中。
432
+ */
433
+ build() {
434
+ this.assertActive('render');
435
+ insert(this.dom.container, () => this.root);
436
+ }
437
+
438
+ /**
439
+ * 动态新增标签。
440
+ * @param {object} tabConfig 标签配置。
441
+ */
442
+ async add(tabConfig) {
443
+ this.assertActive('add');
444
+ validateParam('tabConfig', tabConfig, TAB_CONFIG_RULE, 'Tabs.add');
445
+
446
+ tabConfig.name = tabConfig.name || randomId();
447
+ this.props.tabs = [...cloneTabItems(this.props.tabs), tabConfig];
448
+ this.runtime.cache.panels.clear();
449
+
450
+ this.rebuildItems();
451
+ this.syncActiveNames(this.resolveActiveNames(this.props.active));
452
+ this.bindEvents();
453
+
454
+ this._refreshDrag();
455
+
456
+ const { onAdd } = this.props;
457
+ if (onAdd) {
458
+ const index = this.props.tabs.length - 1;
459
+ await Promise.resolve(
460
+ onAdd(index, tabConfig, this.dom.tabs[index], this.dom.panels[index])
461
+ );
462
+ }
463
+ }
464
+
465
+ /**
466
+ * 根据索引或名称删除标签。
467
+ * @param {number|string} val 标签索引或 `data-tab` 名称。
468
+ */
469
+ async delete(val) {
470
+ this.assertActive('delete');
471
+ if (this.props.tabs.length <= 1) return;
472
+
473
+ const index = this._getIndex(val);
474
+ if (index < 0 || index >= this.props.tabs.length) return;
475
+
476
+ const removedName = this.props.tabs[index].name;
477
+ const { onRemove } = this.props;
478
+
479
+ this.props.tabs = this.props.tabs.filter((_, i) => i !== index);
480
+ this.runtime.cache.panels.delete(removedName);
481
+
482
+ if (this.state.current.index >= this.props.tabs.length) {
483
+ flushSync(() => {
484
+ this._syncCurrent(this.props.tabs.length - 1);
485
+ });
486
+ } else if (this.state.current.index > index) {
487
+ flushSync(() => {
488
+ this._syncCurrent(this.state.current.index - 1);
489
+ });
490
+ }
491
+
492
+ this.rebuildItems();
493
+ this.bindEvents();
494
+
495
+ await this._loadPanel(this.state.current.index);
496
+
497
+ this._refreshDrag();
498
+
499
+ if (onRemove) {
500
+ await Promise.resolve(onRemove(index, removedName));
501
+ }
502
+ }
503
+
504
+ /**
505
+ * 根据索引或名称禁用标签。
506
+ * @param {number|string} val 标签索引或 `data-tab` 名称。
507
+ */
508
+ disable(val) {
509
+ this.assertActive('disable');
510
+ const name =
511
+ typeof val === 'number' ? this.dom.tabs[val]?.dataset.tab : val;
512
+ if (name && !this.state.disabled.names.includes(name)) {
513
+ flushSync(() => {
514
+ this.state.disabled = this._createDisabledState([
515
+ ...this.state.disabled.names,
516
+ name,
517
+ ]);
518
+ });
519
+ }
520
+ }
521
+
522
+ /**
523
+ * 根据索引或名称启用标签。
524
+ * @param {number|string} val 标签索引或 `data-tab` 名称。
525
+ */
526
+ enable(val) {
527
+ this.assertActive('enable');
528
+ const name =
529
+ typeof val === 'number' ? this.dom.tabs[val]?.dataset.tab : val;
530
+ if (name) {
531
+ flushSync(() => {
532
+ this.state.disabled = this._createDisabledState(
533
+ this.state.disabled.names.filter((n) => n !== name)
534
+ );
535
+ });
536
+ }
537
+ }
538
+
539
+ resolveActiveNames(active) {
540
+ if (active == null) return -1;
541
+ if (typeof active === 'number') return active;
542
+ if (typeof active === 'string') {
543
+ return this.dom.tabs.findIndex((tab) => tab.dataset.tab === active);
544
+ }
545
+ return 0;
546
+ }
547
+
548
+ syncActiveNames(index) {
549
+ flushSync(() => {
550
+ this._syncCurrent(index);
551
+ });
552
+ }
553
+
554
+ /**
555
+ * 使用新配置重新初始化状态。
556
+ * @param {object} [patch={}] 需要覆盖的配置。
557
+ */
558
+ async reInit(patch = {}) {
559
+ this.assertActive('reInit');
560
+ Object.assign(this.props, resolveProps(patch, TABS_PROPS_SCHEMA, 'Tabs'));
561
+
562
+ flushSync(() => {
563
+ this.state.disabled = this._parseDisabled(this.props.disabled);
564
+ });
565
+
566
+ this.rebuildItems();
567
+ this.syncActiveNames(this.resolveActiveNames(this.props.active));
568
+ this.bindEvents();
569
+
570
+ await this._loadPanel(this.state.current.index);
571
+
572
+ this._refreshDrag();
573
+ }
574
+
575
+ // ========== 拖拽相关 ==========
576
+
577
+ get _dragContainer() {
578
+ return this.root ? q('.tab-wrap', this.root) : null;
579
+ }
580
+
581
+ get _dragInner() {
582
+ return this.root ? q('.tab-list', this.root) : null;
583
+ }
584
+
585
+ _initDrag() {
586
+ const { direction } = this.props;
587
+ if (!this._dragContainer || !this._dragInner) return;
588
+
589
+ const isVertical = direction === 'left' || direction === 'right';
590
+ const draggable = isVertical
591
+ ? this._dragInner.scrollHeight > this._dragContainer.clientHeight + 5
592
+ : this._dragInner.scrollWidth > this._dragContainer.clientWidth + 5;
593
+
594
+ flushSync(() => {
595
+ this.state.isVertical = isVertical;
596
+ this.state.draggable = draggable;
597
+ });
598
+
599
+ if (!draggable) {
600
+ this._removeDragEvents();
601
+ return;
602
+ }
603
+
604
+ this._bindDragEvents();
605
+
606
+ this.cleanup.events.on('resize', window, 'resize', () => {
607
+ cancelAnimationFrame(this._resizeRaf);
608
+ this._resizeRaf = requestAnimationFrame(() => {
609
+ this._refreshDrag();
610
+ });
611
+ });
612
+ }
613
+
614
+ _bindDragEvents() {
615
+ this._removeDragEvents();
616
+
617
+ const container = this._dragContainer;
618
+ const inner = this._dragInner;
619
+ const isVertical = this.state.isVertical;
620
+
621
+ let posStart = 0;
622
+ let scrollStart = 0;
623
+ let lastPos = 0;
624
+ let frameRequested = false;
625
+
626
+ const getPos = (e) =>
627
+ isVertical
628
+ ? e.touches
629
+ ? e.touches[0].pageY
630
+ : e.pageY
631
+ : e.touches
632
+ ? e.touches[0].pageX
633
+ : e.pageX;
634
+
635
+ const onDragStart = (e) => {
636
+ this.isDragging = true;
637
+ inner.classList.add('dragging');
638
+ posStart = getPos(e);
639
+ lastPos = posStart;
640
+ scrollStart = isVertical ? container.scrollTop : container.scrollLeft;
641
+ this._velocity = 0;
642
+ cancelAnimationFrame(this.raf);
643
+ };
644
+
645
+ const onDragMove = (e) => {
646
+ if (!this.isDragging) return;
647
+ e.preventDefault();
648
+ const current = getPos(e);
649
+ const dist = posStart - current;
650
+ this._velocity = lastPos - current;
651
+ lastPos = current;
652
+
653
+ if (!frameRequested) {
654
+ frameRequested = true;
655
+ requestAnimationFrame(() => {
656
+ frameRequested = false;
657
+ if (isVertical) {
658
+ container.scrollTop = scrollStart + dist;
659
+ } else {
660
+ container.scrollLeft = scrollStart + dist;
661
+ }
662
+ });
663
+ }
664
+ };
665
+
666
+ const onDragEnd = () => {
667
+ if (!this.isDragging) return;
668
+ this.isDragging = false;
669
+ inner.classList.remove('dragging');
670
+ this._startInertiaScroll();
671
+ };
672
+
673
+ this.cleanup.events.on('drag:mousedown', inner, 'mousedown', onDragStart);
674
+ this.cleanup.events.on(
675
+ 'drag:touchstart',
676
+ inner,
677
+ 'touchstart',
678
+ onDragStart,
679
+ { passive: true }
680
+ );
681
+ this.cleanup.events.on('drag:mousemove', window, 'mousemove', onDragMove, {
682
+ passive: false,
683
+ });
684
+ this.cleanup.events.on('drag:touchmove', window, 'touchmove', onDragMove, {
685
+ passive: false,
686
+ });
687
+ this.cleanup.events.on('drag:mouseup', window, 'mouseup', onDragEnd);
688
+ this.cleanup.events.on('drag:touchend', window, 'touchend', onDragEnd);
689
+ }
690
+
691
+ _startInertiaScroll() {
692
+ const container = this._dragContainer;
693
+ let v = this._velocity;
694
+ const isVertical = this.state.isVertical;
695
+ let last = performance.now();
696
+
697
+ const step = (now) => {
698
+ const dt = now - last;
699
+ last = now;
700
+ v *= 0.92;
701
+ if (Math.abs(v) < 0.3) return;
702
+ if (isVertical) {
703
+ container.scrollTop += v * dt * 0.05;
704
+ } else {
705
+ container.scrollLeft += v * dt * 0.05;
706
+ }
707
+ this.raf = requestAnimationFrame(step);
708
+ };
709
+
710
+ this.raf = requestAnimationFrame(step);
711
+ }
712
+
713
+ _removeDragEvents() {
714
+ this.cleanup.events.off('drag:mousedown');
715
+ this.cleanup.events.off('drag:touchstart');
716
+ this.cleanup.events.off('drag:mousemove');
717
+ this.cleanup.events.off('drag:touchmove');
718
+ this.cleanup.events.off('drag:mouseup');
719
+ this.cleanup.events.off('drag:touchend');
720
+ }
721
+
722
+ _refreshDrag() {
723
+ this._initDrag();
724
+ }
725
+
726
+ // ========== 销毁 ==========
727
+
728
+ onDestroy() {
729
+ this.unbindEvents();
730
+ this._removeDragEvents();
731
+ this.cleanup.bindings?.();
732
+ this.cleanup.bindings = null;
733
+ // this.runtime.cache.panels?.clear();
734
+ cancelAnimationFrame(this.raf);
735
+ cancelAnimationFrame(this._resizeRaf);
736
+ this.cleanup.events.off('resize');
737
+
738
+ if (this.root?.parentNode) {
739
+ this.root.parentNode.removeChild(this.root);
740
+ }
741
+ }
742
+ }
743
+
744
+ export function createTabs(container, input = {}) {
745
+ return new Tabs(container, input);
746
+ }