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,536 @@
1
+ import {
2
+ bindAttr,
3
+ createDeepStore,
4
+ createEffect,
5
+ createRoot,
6
+ flushSync,
7
+ jsx,
8
+ } from 'vanilla-signal';
9
+
10
+ import Component from '../core/Component.js';
11
+ import { resolveProps, validateParam } from '../utilities/core.js';
12
+ import { requireContainer, requireRenderDOM } from '../utilities/dom.js';
13
+ import { icon } from './icons.js';
14
+
15
+ const PAGE_RULE = {
16
+ type: 'object',
17
+ required: true,
18
+ validate: (value) =>
19
+ Number.isInteger(value.size) &&
20
+ value.size > 0 &&
21
+ Number.isInteger(value.current) &&
22
+ value.current > 0,
23
+ message: 'expects { size, current } with positive integers.',
24
+ };
25
+
26
+ const COUNT_RULE = {
27
+ type: 'object',
28
+ required: true,
29
+ validate: (value) =>
30
+ Number.isInteger(value.sibling) &&
31
+ value.sibling >= 0 &&
32
+ Number.isInteger(value.boundary) &&
33
+ value.boundary >= 0,
34
+ message:
35
+ 'expects { sibling, boundary } with integers greater than or equal 0.',
36
+ };
37
+
38
+ const PAGINATION_PROPS_SCHEMA = {
39
+ total: {
40
+ default: 0,
41
+ type: 'number',
42
+ validate: (value) => Number.isFinite(value) && value >= 0,
43
+ message: 'expects a non-negative finite number.',
44
+ },
45
+ page: {
46
+ default: () => ({ size: 10, current: 1 }),
47
+ factory: true,
48
+ ...PAGE_RULE,
49
+ },
50
+ count: {
51
+ default: () => ({ sibling: 1, boundary: 1 }),
52
+ factory: true,
53
+ ...COUNT_RULE,
54
+ },
55
+ lock: { default: true, type: 'boolean' },
56
+ onChange: { default: null, types: ['function', 'null'] },
57
+ };
58
+
59
+ function clamp(value, min, max) {
60
+ return Math.min(Math.max(value, min), max);
61
+ }
62
+
63
+ function normalizeProps(input = {}) {
64
+ const props = resolveProps(
65
+ input,
66
+ PAGINATION_PROPS_SCHEMA,
67
+ 'Pagination.props'
68
+ );
69
+ return {
70
+ ...props,
71
+ page: { ...props.page },
72
+ count: { ...props.count },
73
+ };
74
+ }
75
+
76
+ function createMoreItem(key) {
77
+ return { type: 'more', key };
78
+ }
79
+
80
+ function createPageItem(page) {
81
+ return { type: 'page', key: `page-${page}`, page };
82
+ }
83
+
84
+ function pageRange(start, end) {
85
+ const pages = [];
86
+ for (let page = start; page <= end; page++) pages.push(page);
87
+ return pages;
88
+ }
89
+
90
+ function resolvePageItems(current, pageCount, count) {
91
+ if (pageCount <= 0) return [];
92
+
93
+ const boundary = Math.max(0, count.boundary);
94
+ const sibling = Math.max(0, count.sibling);
95
+ const visibleCount = boundary * 2 + sibling * 2 + 3;
96
+
97
+ if (pageCount <= visibleCount) {
98
+ return pageRange(1, pageCount).map(createPageItem);
99
+ }
100
+
101
+ const pages = new Set();
102
+
103
+ for (let page = 1; page <= Math.min(boundary, pageCount); page++) {
104
+ pages.add(page);
105
+ }
106
+
107
+ for (
108
+ let page = Math.max(1, current - sibling);
109
+ page <= Math.min(pageCount, current + sibling);
110
+ page++
111
+ ) {
112
+ pages.add(page);
113
+ }
114
+
115
+ for (
116
+ let page = Math.max(1, pageCount - boundary + 1);
117
+ page <= pageCount;
118
+ page++
119
+ ) {
120
+ pages.add(page);
121
+ }
122
+
123
+ const sorted = Array.from(pages).sort((a, b) => a - b);
124
+ const items = [];
125
+
126
+ for (const page of sorted) {
127
+ const previous = items.at(-1);
128
+ if (previous?.type === 'page' && page - previous.page > 1) {
129
+ if (page - previous.page === 2) {
130
+ items.push(createPageItem(previous.page + 1));
131
+ } else {
132
+ items.push(createMoreItem(`more-${previous.page}-${page}`));
133
+ }
134
+ }
135
+ items.push(createPageItem(page));
136
+ }
137
+
138
+ return items;
139
+ }
140
+
141
+ /**
142
+ * 分页组件。
143
+ *
144
+ * 构造器只验证和保存配置;调用 build() 后才挂载 DOM 和绑定交互。
145
+ */
146
+ export class Pagination extends Component {
147
+ /**
148
+ * @param {Element|Node|string|Array} container 挂载容器。
149
+ * @param {object} [input={}] 分页配置。
150
+ */
151
+ constructor(container, input = {}) {
152
+ requireRenderDOM('Pagination');
153
+
154
+ const el = requireContainer(container, 'Pagination.container');
155
+ const props = normalizeProps(input);
156
+ super(props);
157
+
158
+ const pageCount = this._getPageCount(props.total, props.page.size);
159
+ const current = clamp(props.page.current, 1, pageCount);
160
+
161
+ this.dom.container = el;
162
+ this.dom.list = null;
163
+ this.dom.prev = null;
164
+ this.dom.next = null;
165
+ this.dom.pageNodes = [];
166
+ this.dom.items = [];
167
+
168
+ this.runtime.built = false;
169
+ this.runtime.itemsKey = '';
170
+ this.runtime.changeId = 0;
171
+
172
+ this.state = createDeepStore({
173
+ total: props.total,
174
+ page: {
175
+ size: props.page.size,
176
+ current,
177
+ },
178
+ count: {
179
+ sibling: props.count.sibling,
180
+ boundary: props.count.boundary,
181
+ },
182
+ pageCount,
183
+ locked: false,
184
+ });
185
+
186
+ this.props.page.current = current;
187
+ }
188
+
189
+ /**
190
+ * 构建分页 DOM 并绑定事件。
191
+ * @returns {Pagination} 当前实例。
192
+ */
193
+ build() {
194
+ this._assertActive('build');
195
+ if (this.runtime.built) return this;
196
+
197
+ this.init(this.props);
198
+ this.root = jsx('div', {
199
+ className: 'j-pagination',
200
+ role: 'navigation',
201
+ 'aria-label': 'Pagination',
202
+ children: jsx('ul', {
203
+ className: 'pagination',
204
+ 'aria-live': 'polite',
205
+ }),
206
+ });
207
+
208
+ this.dom.list = this.root.querySelector('.pagination');
209
+ this.dom.prev = this._buildControlItem('prev');
210
+ this.dom.next = this._buildControlItem('next');
211
+ this.dom.list.append(this.dom.prev, this.dom.next);
212
+
213
+ this.dom.container.textContent = '';
214
+ this.dom.container.appendChild(this.root);
215
+ this.runtime.built = true;
216
+
217
+ this._bindControlState();
218
+ this._bindPageItems();
219
+ this._bindEvents();
220
+ return this;
221
+ }
222
+
223
+ /**
224
+ * 跳转到指定页码。
225
+ * @param {number} page 新页码。
226
+ * @returns {Pagination} 当前实例。
227
+ */
228
+ go(page) {
229
+ this._assertActive('go');
230
+ validateParam(
231
+ 'page',
232
+ page,
233
+ {
234
+ type: 'number',
235
+ validate: Number.isInteger,
236
+ message: 'expects an integer.',
237
+ },
238
+ 'Pagination.go'
239
+ );
240
+
241
+ if (this._isLocked()) return this;
242
+
243
+ const nextPage = clamp(page, 1, this.state.pageCount);
244
+ if (nextPage === this.state.page.current) return this;
245
+
246
+ const shouldLock =
247
+ this.props.lock && typeof this.props.onChange === 'function';
248
+ const changeId = shouldLock
249
+ ? ++this.runtime.changeId
250
+ : this.runtime.changeId;
251
+
252
+ flushSync(() => {
253
+ this.state.page.current = nextPage;
254
+ if (shouldLock) this.state.locked = true;
255
+ });
256
+ this.props.page.current = nextPage;
257
+
258
+ if (typeof this.props.onChange === 'function') {
259
+ let result;
260
+ try {
261
+ result = this.props.onChange(nextPage, this);
262
+ } catch (error) {
263
+ if (shouldLock && changeId === this.runtime.changeId) {
264
+ flushSync(() => {
265
+ this.state.locked = false;
266
+ });
267
+ }
268
+ throw error;
269
+ }
270
+
271
+ if (shouldLock && result && typeof result.then === 'function') {
272
+ const unlock = () => {
273
+ if (!this.runtime.destroyed && changeId === this.runtime.changeId) {
274
+ flushSync(() => {
275
+ this.state.locked = false;
276
+ });
277
+ }
278
+ };
279
+ void Promise.resolve(result).then(unlock, unlock);
280
+ } else if (shouldLock && changeId === this.runtime.changeId) {
281
+ flushSync(() => {
282
+ this.state.locked = false;
283
+ });
284
+ }
285
+ }
286
+
287
+ return this;
288
+ }
289
+
290
+ /**
291
+ * 更新分页配置。
292
+ * @param {object} [newProps={}] 新配置,会与当前 props 合并。
293
+ * @returns {Pagination} 当前实例。
294
+ */
295
+ update(newProps = {}) {
296
+ this._assertActive('update');
297
+ validateParam(
298
+ 'newProps',
299
+ newProps,
300
+ { type: 'object' },
301
+ 'Pagination.update'
302
+ );
303
+
304
+ const props = normalizeProps({
305
+ total: newProps.total ?? this.props.total,
306
+ lock: Object.hasOwn(newProps, 'lock') ? newProps.lock : this.props.lock,
307
+ onChange: Object.hasOwn(newProps, 'onChange')
308
+ ? newProps.onChange
309
+ : this.props.onChange,
310
+ page: {
311
+ ...this.props.page,
312
+ ...newProps.page,
313
+ },
314
+ count: {
315
+ ...this.props.count,
316
+ ...newProps.count,
317
+ },
318
+ });
319
+ const pageCount = this._getPageCount(props.total, props.page.size);
320
+ props.page.current = clamp(props.page.current, 1, pageCount);
321
+
322
+ this.props = props;
323
+ flushSync(() => {
324
+ this.state.total = props.total;
325
+ this.state.page.size = props.page.size;
326
+ this.state.page.current = props.page.current;
327
+ this.state.count.sibling = props.count.sibling;
328
+ this.state.count.boundary = props.count.boundary;
329
+ this.state.pageCount = pageCount;
330
+ if (!props.lock) this.state.locked = false;
331
+ });
332
+
333
+ return this;
334
+ }
335
+
336
+ _getPageCount(total, size) {
337
+ return Math.max(1, Math.ceil(total / size));
338
+ }
339
+
340
+ _getPageItems() {
341
+ return resolvePageItems(
342
+ this.state.page.current,
343
+ this.state.pageCount,
344
+ this.state.count
345
+ );
346
+ }
347
+
348
+ _getItemsKey() {
349
+ return [
350
+ this.state.page.current,
351
+ this._isLocked() ? 'locked' : 'unlocked',
352
+ ...this._getPageItems().map((item) => item.key),
353
+ ].join('|');
354
+ }
355
+
356
+ _isLocked() {
357
+ return this.props.lock && this.state.locked;
358
+ }
359
+
360
+ _isPrevDisabled() {
361
+ return this._isLocked() || this.state.page.current <= 1;
362
+ }
363
+
364
+ _isNextDisabled() {
365
+ return this._isLocked() || this.state.page.current >= this.state.pageCount;
366
+ }
367
+
368
+ _buildControlItem(type) {
369
+ const item = jsx('li', {
370
+ className: 'item',
371
+ children: jsx('button', {
372
+ className: 'j-button is-icon is-ghost',
373
+ type: 'button',
374
+ 'data-page-action': type,
375
+ children: icon(type === 'prev' ? 'arrow-left' : 'arrow-right'),
376
+ }),
377
+ });
378
+
379
+ return item;
380
+ }
381
+
382
+ _buildPageItem(item) {
383
+ const disabled = this._isLocked();
384
+
385
+ if (item.type === 'more') {
386
+ return jsx('li', {
387
+ className: 'item more',
388
+ 'aria-hidden': 'true',
389
+ children: jsx('button', {
390
+ className: 'j-button is-icon is-ghost',
391
+ type: 'button',
392
+ disabled,
393
+ children: icon('more'),
394
+ }),
395
+ });
396
+ }
397
+
398
+ return jsx('li', {
399
+ className: 'item',
400
+ children:
401
+ item.page === this.state.page.current
402
+ ? jsx('span', {
403
+ className: 'j-button is-icon is-active',
404
+ 'data-current-page': String(item.page),
405
+ 'aria-current': 'page',
406
+ 'aria-label': `Page ${item.page}, current page`,
407
+ children: disabled
408
+ ? jsx('i', {
409
+ className: 'animate-spin',
410
+ children: icon('loader'),
411
+ })
412
+ : String(item.page),
413
+ })
414
+ : jsx('button', {
415
+ className: 'j-button is-icon is-ghost',
416
+ type: 'button',
417
+ 'data-page': String(item.page),
418
+ 'aria-label': `Go to page ${item.page}`,
419
+ 'aria-disabled': disabled ? 'true' : null,
420
+ disabled,
421
+ tabindex: disabled ? '-1' : null,
422
+ children: String(item.page),
423
+ }),
424
+ });
425
+ }
426
+
427
+ _bindControlState() {
428
+ this.cleanup.controls?.();
429
+ this.cleanup.controls = createRoot((dispose) => {
430
+ const prev = this.dom.prev?.querySelector('[data-page-action]');
431
+ const next = this.dom.next?.querySelector('[data-page-action]');
432
+
433
+ if (prev) {
434
+ // bindClass(prev, 'is-disabled', () => this._isPrevDisabled());
435
+ bindAttr(prev, 'disabled', () => this._isPrevDisabled());
436
+ bindAttr(prev, 'aria-disabled', () =>
437
+ this._isPrevDisabled() ? 'true' : 'false'
438
+ );
439
+ bindAttr(prev, 'tabindex', () =>
440
+ this._isPrevDisabled() ? '-1' : null
441
+ );
442
+ bindAttr(prev, 'aria-label', () => 'Go to previous page');
443
+ }
444
+
445
+ if (next) {
446
+ // bindClass(next, 'is-disabled', () => this._isNextDisabled());
447
+ bindAttr(next, 'disabled', () => this._isNextDisabled());
448
+ bindAttr(next, 'aria-disabled', () =>
449
+ this._isNextDisabled() ? 'true' : 'false'
450
+ );
451
+ bindAttr(next, 'tabindex', () =>
452
+ this._isNextDisabled() ? '-1' : null
453
+ );
454
+ bindAttr(next, 'aria-label', () => 'Go to next page');
455
+ }
456
+
457
+ return dispose;
458
+ });
459
+ }
460
+
461
+ _bindPageItems() {
462
+ this.cleanup.itemsEffect?.dispose();
463
+ this.cleanup.itemsEffect = createEffect(() => {
464
+ const nextKey = this._getItemsKey();
465
+ if (nextKey === this.runtime.itemsKey) return;
466
+
467
+ this.runtime.itemsKey = nextKey;
468
+ this._renderPageItems();
469
+ });
470
+ }
471
+
472
+ _renderPageItems() {
473
+ this.dom.items = [];
474
+
475
+ const items = this._getPageItems();
476
+ const nodes = items.map((item) => this._buildPageItem(item));
477
+
478
+ for (const node of this.dom.pageNodes) node.remove();
479
+ this.dom.pageNodes = nodes;
480
+ for (const node of nodes) this.dom.list.insertBefore(node, this.dom.next);
481
+ }
482
+
483
+ _bindEvents() {
484
+ this.cleanup.events.on('click', this.root, 'click', (event) => {
485
+ const target = event.target.closest('[data-page], [data-page-action]');
486
+ if (!target || !this.root.contains(target)) return;
487
+
488
+ event.preventDefault();
489
+
490
+ const action = target.dataset.pageAction;
491
+ if (action === 'prev' && !this._isPrevDisabled()) {
492
+ this.go(this.state.page.current - 1);
493
+ return;
494
+ }
495
+
496
+ if (action === 'next' && !this._isNextDisabled()) {
497
+ this.go(this.state.page.current + 1);
498
+ return;
499
+ }
500
+
501
+ if (target.dataset.page) {
502
+ this.go(Number(target.dataset.page));
503
+ }
504
+ });
505
+ }
506
+
507
+ _assertActive(method) {
508
+ if (this.runtime.destroyed) {
509
+ throw new Error(`Pagination.${method}: instance has been destroyed.`);
510
+ }
511
+ }
512
+
513
+ onDestroy() {
514
+ this.cleanup.events.clear();
515
+ this.cleanup.controls?.();
516
+ this.cleanup.itemsEffect?.dispose();
517
+ this.cleanup.controls = null;
518
+ this.cleanup.itemsEffect = null;
519
+
520
+ if (this.dom.container) this.dom.container.textContent = '';
521
+
522
+ this.runtime.built = false;
523
+ this.runtime.itemsKey = '';
524
+ this.runtime.changeId = 0;
525
+ this.dom.container = null;
526
+ this.dom.list = null;
527
+ this.dom.prev = null;
528
+ this.dom.next = null;
529
+ this.dom.pageNodes = [];
530
+ this.dom.items = [];
531
+ }
532
+ }
533
+
534
+ export function createPagination(container, props = {}) {
535
+ return new Pagination(container, props);
536
+ }