@tabler/core 1.6.0 → 1.6.1

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 (115) hide show
  1. package/dist/css/tabler-flags.css +1 -1
  2. package/dist/css/tabler-flags.css.map +1 -1
  3. package/dist/css/tabler-flags.min.css +1 -1
  4. package/dist/css/tabler-flags.min.css.map +1 -1
  5. package/dist/css/tabler-flags.rtl.css +1 -1
  6. package/dist/css/tabler-flags.rtl.css.map +1 -1
  7. package/dist/css/tabler-flags.rtl.min.css +1 -1
  8. package/dist/css/tabler-flags.rtl.min.css.map +1 -1
  9. package/dist/css/tabler-marketing.css +1 -1
  10. package/dist/css/tabler-marketing.css.map +1 -1
  11. package/dist/css/tabler-marketing.min.css +1 -1
  12. package/dist/css/tabler-marketing.min.css.map +1 -1
  13. package/dist/css/tabler-marketing.rtl.css +1 -1
  14. package/dist/css/tabler-marketing.rtl.css.map +1 -1
  15. package/dist/css/tabler-marketing.rtl.min.css +1 -1
  16. package/dist/css/tabler-marketing.rtl.min.css.map +1 -1
  17. package/dist/css/tabler-payments.css +1 -1
  18. package/dist/css/tabler-payments.css.map +1 -1
  19. package/dist/css/tabler-payments.min.css +1 -1
  20. package/dist/css/tabler-payments.min.css.map +1 -1
  21. package/dist/css/tabler-payments.rtl.css +1 -1
  22. package/dist/css/tabler-payments.rtl.css.map +1 -1
  23. package/dist/css/tabler-payments.rtl.min.css +1 -1
  24. package/dist/css/tabler-payments.rtl.min.css.map +1 -1
  25. package/dist/css/tabler-props.css +1 -1
  26. package/dist/css/tabler-props.css.map +1 -1
  27. package/dist/css/tabler-props.min.css +1 -1
  28. package/dist/css/tabler-props.min.css.map +1 -1
  29. package/dist/css/tabler-props.rtl.css +1 -1
  30. package/dist/css/tabler-props.rtl.css.map +1 -1
  31. package/dist/css/tabler-props.rtl.min.css +1 -1
  32. package/dist/css/tabler-props.rtl.min.css.map +1 -1
  33. package/dist/css/tabler-socials.css +1 -1
  34. package/dist/css/tabler-socials.css.map +1 -1
  35. package/dist/css/tabler-socials.min.css +1 -1
  36. package/dist/css/tabler-socials.min.css.map +1 -1
  37. package/dist/css/tabler-socials.rtl.css +1 -1
  38. package/dist/css/tabler-socials.rtl.css.map +1 -1
  39. package/dist/css/tabler-socials.rtl.min.css +1 -1
  40. package/dist/css/tabler-socials.rtl.min.css.map +1 -1
  41. package/dist/css/tabler-themes.css +1 -1
  42. package/dist/css/tabler-themes.css.map +1 -1
  43. package/dist/css/tabler-themes.min.css +1 -1
  44. package/dist/css/tabler-themes.min.css.map +1 -1
  45. package/dist/css/tabler-themes.rtl.css +1 -1
  46. package/dist/css/tabler-themes.rtl.css.map +1 -1
  47. package/dist/css/tabler-themes.rtl.min.css +1 -1
  48. package/dist/css/tabler-themes.rtl.min.css.map +1 -1
  49. package/dist/css/tabler-vendors.css +1 -1
  50. package/dist/css/tabler-vendors.css.map +1 -1
  51. package/dist/css/tabler-vendors.min.css +1 -1
  52. package/dist/css/tabler-vendors.min.css.map +1 -1
  53. package/dist/css/tabler-vendors.rtl.css +1 -1
  54. package/dist/css/tabler-vendors.rtl.css.map +1 -1
  55. package/dist/css/tabler-vendors.rtl.min.css +1 -1
  56. package/dist/css/tabler-vendors.rtl.min.css.map +1 -1
  57. package/dist/css/tabler.css +12 -2
  58. package/dist/css/tabler.css.map +1 -1
  59. package/dist/css/tabler.min.css +2 -2
  60. package/dist/css/tabler.min.css.map +1 -1
  61. package/dist/css/tabler.rtl.css +12 -2
  62. package/dist/css/tabler.rtl.css.map +1 -1
  63. package/dist/css/tabler.rtl.min.css +2 -2
  64. package/dist/css/tabler.rtl.min.css.map +1 -1
  65. package/dist/js/tabler-theme.esm.js +21 -6
  66. package/dist/js/tabler-theme.esm.js.map +1 -1
  67. package/dist/js/tabler-theme.esm.min.js +2 -2
  68. package/dist/js/tabler-theme.esm.min.js.map +1 -1
  69. package/dist/js/tabler-theme.js +21 -6
  70. package/dist/js/tabler-theme.js.map +1 -1
  71. package/dist/js/tabler-theme.min.js +2 -2
  72. package/dist/js/tabler-theme.min.js.map +1 -1
  73. package/dist/js/tabler.esm.js +332 -131
  74. package/dist/js/tabler.esm.js.map +1 -1
  75. package/dist/js/tabler.esm.min.js +2 -2
  76. package/dist/js/tabler.esm.min.js.map +1 -1
  77. package/dist/js/tabler.js +332 -131
  78. package/dist/js/tabler.js.map +1 -1
  79. package/dist/js/tabler.min.js +2 -2
  80. package/dist/js/tabler.min.js.map +1 -1
  81. package/dist/libs/vanilla-calendar-pro/index.js +2 -2
  82. package/dist/types/src/clipboard.d.ts +3 -0
  83. package/dist/types/src/confetti.d.ts +2 -0
  84. package/dist/types/src/countup.d.ts +2 -0
  85. package/dist/types/src/datepicker.d.ts +24 -6
  86. package/dist/types/src/input-mask.d.ts +2 -1
  87. package/dist/types/src/otp-input.d.ts +6 -2
  88. package/dist/types/src/strength.d.ts +1 -0
  89. package/dist/types/src/switch-icon.d.ts +1 -0
  90. package/js/src/autosize.ts +8 -3
  91. package/js/src/bootstrap/base-component.ts +12 -2
  92. package/js/src/bootstrap/dom/manipulator.ts +1 -1
  93. package/js/src/bootstrap/dropdown.ts +3 -1
  94. package/js/src/bootstrap/util/index.ts +14 -1
  95. package/js/src/clipboard.ts +26 -2
  96. package/js/src/confetti.ts +42 -7
  97. package/js/src/countup.ts +30 -12
  98. package/js/src/datepicker.ts +147 -27
  99. package/js/src/dropdown.ts +12 -5
  100. package/js/src/global.d.ts +4 -2
  101. package/js/src/input-mask.ts +7 -7
  102. package/js/src/otp-input.ts +99 -12
  103. package/js/src/popover.ts +13 -7
  104. package/js/src/sidebar.ts +14 -6
  105. package/js/src/sortable.ts +5 -1
  106. package/js/src/sparkline.ts +15 -6
  107. package/js/src/strength.ts +17 -4
  108. package/js/src/switch-icon.ts +15 -0
  109. package/js/src/tab.ts +17 -7
  110. package/js/src/toast.ts +19 -11
  111. package/js/src/tooltip.ts +16 -7
  112. package/js/tabler-theme.ts +28 -4
  113. package/package.json +2 -2
  114. package/scss/layout/_navbar.scss +1 -2
  115. package/scss/ui/_cards.scss +14 -2
@@ -5,12 +5,12 @@
5
5
  * --------------------------------------------------------------------------
6
6
  */
7
7
 
8
- import type { Calendar, DateAny, DateMode, DatesArr, MonthsCount, Options, PositionToInput, Range, WeekDayID } from 'vanilla-calendar-pro'
8
+ import type { Calendar, CalendarExtension, Options, Range } from 'vanilla-calendar-pro'
9
9
  import BaseComponent from './bootstrap/base-component'
10
10
  import EventHandler from './bootstrap/dom/event-handler'
11
11
  import SelectorEngine from './bootstrap/dom/selector-engine'
12
12
  import { initAll } from './bootstrap/util/component-functions'
13
- import { isDisabled } from './bootstrap/util/index'
13
+ import { getElement, isDisabled } from './bootstrap/util/index'
14
14
  import type { ElementSelector } from './bootstrap/types'
15
15
 
16
16
  /**
@@ -40,10 +40,45 @@ const HIDE_DELAY = 100 // ms delay before hiding after selection
40
40
 
41
41
  const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/
42
42
 
43
- // The date, weekday, month-count and placement options are handed straight to
44
- // Vanilla Calendar Pro, so they use its own literal unions rather than the wider
45
- // `string` / `number`. That keeps a bad value a compile error here instead of a
46
- // silent no-op inside the calendar.
43
+ // Vanilla Calendar Pro 3.4 moved several months, the time picker, week
44
+ // numbers, popups and animation into extensions that must be registered when
45
+ // the calendar is created, or its constructor throws. The library is loaded
46
+ // as a whole on `window`, so every extension it exports is registered;
47
+ // 3.3 exports none and gets no `extensions` option.
48
+ const EXTENSION_NAMES = ['annotations', 'months', 'motion', 'time', 'weeks'] as const
49
+
50
+ const writtenSelections = new WeakMap<HTMLElement, { text: string; dates: string[] }>()
51
+
52
+ // The public API is typed with local copies of the Vanilla Calendar Pro types.
53
+ // `vanilla-calendar-pro` is an optional peer dependency, so the published
54
+ // `dist/types` must not import it: a project that never loads the datepicker
55
+ // would otherwise fail to type-check with TS2307. The unions match the
56
+ // plugin's own, so a bad value is still a compile error. Members that need the
57
+ // plugin's full types carry an internal JSDoc tag and are left out of
58
+ // `dist/types` by `stripInternal` (the tag must not appear in plain comments,
59
+ // or TypeScript strips the declaration that follows).
60
+ type DateAny = Date | number | `${number}-${string}-${string}` | 'today'
61
+ type DatesArr = Array<Date | number | string>
62
+ type DateMode = 'single' | 'multiple' | 'multiple-ranged'
63
+ type MonthsCount = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
64
+ type PositionToInput = 'auto' | 'left' | 'center' | 'right' | ['bottom' | 'top', 'left' | 'center' | 'right']
65
+ type WeekDayID = 0 | 1 | 2 | 3 | 4 | 5 | 6
66
+
67
+ /**
68
+ * The part of a Vanilla Calendar Pro instance the `calendar` getter promises.
69
+ * Cast it to `Calendar` from `vanilla-calendar-pro` for the full type.
70
+ */
71
+ interface CalendarInstance {
72
+ // Loosely typed on purpose: the plugin's context is large and version-specific.
73
+ context: Record<string, any>
74
+ init(): unknown
75
+ update(resetOptions?: object): unknown
76
+ set(options: object, resetOptions?: object): unknown
77
+ show(): unknown
78
+ hide(): unknown
79
+ destroy(): unknown
80
+ }
81
+
47
82
  type ComponentConfig = {
48
83
  /** 'light', 'dark' or 'auto' for the popup only; null inherits from the nearest `[data-bs-theme]` */
49
84
  datepickerTheme: string | null
@@ -66,7 +101,7 @@ type ComponentConfig = {
66
101
  selectionMode: DateMode
67
102
  placement: PositionToInput
68
103
  /** pass-through for any Vanilla Calendar Pro option */
69
- vcpOptions: Options
104
+ vcpOptions: object
70
105
  }
71
106
 
72
107
  type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
@@ -92,8 +127,8 @@ const Default: ComponentConfig = {
92
127
 
93
128
  const DefaultType: Record<keyof ComponentConfig, string> = {
94
129
  datepickerTheme: '(null|string)',
95
- dateMin: '(null|string|number|object)',
96
- dateMax: '(null|string|number|object)',
130
+ dateMin: '(null|string|number|object|date)',
131
+ dateMax: '(null|string|number|object|date)',
97
132
  dateFormat: '(null|object|function)',
98
133
  displayElement: '(null|string|element|boolean)',
99
134
  displayMonthsCount: 'number',
@@ -126,6 +161,7 @@ const afterPluginTimers = (callback: () => void): void => {
126
161
  class Datepicker extends BaseComponent {
127
162
  declare _element: HTMLElement & { value: string }
128
163
  declare _config: ComponentConfig
164
+ /** @internal */
129
165
  _calendar: Calendar | null = null
130
166
  _isShown = false
131
167
  // The plugin opens and closes the popup on its own too (its click and focus
@@ -136,6 +172,7 @@ class Datepicker extends BaseComponent {
136
172
  _isSilent = false
137
173
  _skipPluginShow = false
138
174
  _resolveShown: (() => void) | null = null
175
+ _hideTimeout = 0
139
176
  _isInput = false
140
177
  _isInline = false
141
178
  _boundInput: HTMLInputElement | null = null
@@ -143,6 +180,7 @@ class Datepicker extends BaseComponent {
143
180
  _displayElement: HTMLElement | false | null = null
144
181
  _themeObserver: MutationObserver | null = null
145
182
  _onFocusIn: ((event: Event) => void) | null = null
183
+ _onResize = (): void => this._alignToPositionElement()
146
184
  // The plugin builds a popup lazily, so its context has no selection until
147
185
  // the first show; this mirror answers `getSelectedDates()` before that.
148
186
  _selectedDates: string[] = []
@@ -171,7 +209,7 @@ class Datepicker extends BaseComponent {
171
209
  }
172
210
 
173
211
  /** The Vanilla Calendar Pro instance, for options the component does not expose. */
174
- get calendar(): Calendar | null {
212
+ get calendar(): CalendarInstance | null {
175
213
  return this._calendar
176
214
  }
177
215
 
@@ -243,13 +281,35 @@ class Datepicker extends BaseComponent {
243
281
  }
244
282
 
245
283
  dispose(): void {
284
+ window.clearTimeout(this._hideTimeout)
246
285
  this._themeObserver?.disconnect()
286
+ window.removeEventListener('resize', this._onResize)
247
287
 
248
288
  if (this._onFocusIn) {
249
289
  EventHandler.off(document, EVENT_FOCUSIN, this._onFocusIn)
250
290
  }
251
291
 
252
- this._calendar?.destroy()
292
+ if (this._calendar) {
293
+ this._calendar.destroy()
294
+
295
+ // The plugin swaps the element for a clone taken at init, which has the
296
+ // value and listeners from back then. The live element goes back in;
297
+ // an inline calendar keeps the clone (its original children), so only
298
+ // the bound input moves back into it.
299
+ const clone = this._calendar.context.mainElement as HTMLElement | undefined
300
+ if (clone && clone !== this._element && clone.isConnected) {
301
+ if (this._isInput) {
302
+ clone.replaceWith(this._element)
303
+ } else if (this._boundInput) {
304
+ const copy = SelectorEngine.findOne(SELECTOR_BOUND_INPUT, clone)
305
+ if (copy) {
306
+ copy.replaceWith(this._boundInput)
307
+ } else {
308
+ clone.append(this._boundInput)
309
+ }
310
+ }
311
+ }
312
+ }
253
313
 
254
314
  super.dispose()
255
315
  }
@@ -260,8 +320,9 @@ class Datepicker extends BaseComponent {
260
320
  }
261
321
 
262
322
  setSelectedDates(dates: DatesArr): void {
263
- this._selectedDates = dates.map(String)
264
- this._calendar?.set({ selectedDates: dates })
323
+ this._selectedDates = dates.map((date) => this._toIsoDate(date))
324
+ this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
325
+ this._writeSelection(this._selectedDates)
265
326
  }
266
327
 
267
328
  // Private
@@ -306,12 +367,11 @@ class Datepicker extends BaseComponent {
306
367
  }
307
368
 
308
369
  _updateDisplayWithSelectedDates(): void {
309
- const { selectedDates } = this._config
310
- if (!selectedDates || selectedDates.length === 0) {
370
+ if (this._selectedDates.length === 0) {
311
371
  return
312
372
  }
313
373
 
314
- this._writeSelection(selectedDates)
374
+ this._writeSelection(this._selectedDates)
315
375
  }
316
376
 
317
377
  _writeSelection(selectedDates: string[]): void {
@@ -319,6 +379,7 @@ class Datepicker extends BaseComponent {
319
379
 
320
380
  if (this._isInput) {
321
381
  this._element.value = formattedDate
382
+ writtenSelections.set(this._element, { text: formattedDate, dates: [...selectedDates] })
322
383
  }
323
384
 
324
385
  if (this._boundInput) {
@@ -334,7 +395,7 @@ class Datepicker extends BaseComponent {
334
395
  let { positionElement } = this._config
335
396
 
336
397
  if (typeof positionElement === 'string') {
337
- positionElement = SelectorEngine.findOne(positionElement)
398
+ positionElement = getElement(positionElement)
338
399
  }
339
400
 
340
401
  // Use the input's wrapper when it sits in an icon or group wrapper
@@ -349,7 +410,7 @@ class Datepicker extends BaseComponent {
349
410
  const { displayElement } = this._config
350
411
 
351
412
  if (typeof displayElement === 'string') {
352
- return SelectorEngine.findOne(displayElement)
413
+ return getElement(displayElement)
353
414
  }
354
415
 
355
416
  // For buttons/non-inputs (not inline), look for a display child
@@ -480,6 +541,8 @@ class Datepicker extends BaseComponent {
480
541
  this._isShown = true
481
542
  this._syncThemeAttribute(calendar.context.mainElement)
482
543
  this._alignToPositionElement()
544
+ window.removeEventListener('resize', this._onResize)
545
+ window.addEventListener('resize', this._onResize)
483
546
 
484
547
  if (!wasShown) {
485
548
  EventHandler.trigger(this._element, EVENT_SHOWN)
@@ -502,16 +565,39 @@ class Datepicker extends BaseComponent {
502
565
  }
503
566
 
504
567
  this._isShown = false
568
+ window.removeEventListener('resize', this._onResize)
505
569
  EventHandler.trigger(this._element, EVENT_HIDDEN)
506
570
  }
507
571
 
572
+ // The extensions the loaded library exports, after the ones the caller
573
+ // registered in `vcpOptions` themselves.
574
+ /** @internal */
575
+ _calendarExtensions(own: readonly CalendarExtension[] = []): CalendarExtension[] {
576
+ const library = window.VanillaCalendarPro as Record<string, unknown> | undefined
577
+ const extensions = [...own]
578
+
579
+ for (const name of EXTENSION_NAMES) {
580
+ const extension = library?.[name] as CalendarExtension | undefined
581
+ if (extension && typeof extension === 'object' && !extensions.includes(extension)) {
582
+ extensions.push(extension)
583
+ }
584
+ }
585
+
586
+ return extensions
587
+ }
588
+
589
+ /** @internal */
508
590
  _buildCalendarOptions(): Options {
509
591
  // The plugin uses 'system' for auto-detection, Bootstrap and Tabler use 'auto'
510
592
  const theme = this._getEffectiveTheme()
511
593
  const vcpTheme = !theme || theme === 'auto' ? 'system' : theme
512
594
 
595
+ const vcpOptions = this._config.vcpOptions as Options
596
+ const extensions = this._calendarExtensions(vcpOptions.extensions)
597
+
513
598
  const calendarOptions: Options = {
514
- ...this._config.vcpOptions,
599
+ ...vcpOptions,
600
+ ...(extensions.length > 0 ? { extensions } : {}),
515
601
  inputMode: !this._isInline,
516
602
  positionToInput: this._config.placement,
517
603
  firstWeekday: this._config.firstWeekday,
@@ -540,23 +626,24 @@ class Datepicker extends BaseComponent {
540
626
  }
541
627
 
542
628
  if (this._config.dateMin) {
543
- calendarOptions.dateMin = this._config.dateMin
629
+ calendarOptions.dateMin = this._config.dateMin as Options['dateMin']
544
630
  }
545
631
 
546
632
  if (this._config.dateMax) {
547
- calendarOptions.dateMax = this._config.dateMax
633
+ calendarOptions.dateMax = this._config.dateMax as Options['dateMax']
548
634
  }
549
635
 
550
636
  return calendarOptions
551
637
  }
552
638
 
639
+ /** @internal */
553
640
  _handleDateClick(self: Calendar, event: MouseEvent): void {
554
641
  const selectedDates = [...self.context.selectedDates]
555
642
  this._selectedDates = selectedDates
556
643
 
557
- if (selectedDates.length > 0) {
558
- this._writeSelection(selectedDates)
559
- }
644
+ // Written even when empty: clicking the picked day again deselects it, and
645
+ // the field must not keep submitting the old date.
646
+ this._writeSelection(selectedDates)
560
647
 
561
648
  const args: ChangeEventArgs = { dates: selectedDates, event }
562
649
  EventHandler.trigger(this._element, EVENT_CHANGE, args)
@@ -572,7 +659,8 @@ class Datepicker extends BaseComponent {
572
659
  const shouldHide = (this._config.selectionMode === 'single' && selectedDates.length > 0) || (this._config.selectionMode === 'multiple-ranged' && selectedDates.length >= 2)
573
660
 
574
661
  if (shouldHide) {
575
- setTimeout(() => this.hide(), HIDE_DELAY)
662
+ window.clearTimeout(this._hideTimeout)
663
+ this._hideTimeout = window.setTimeout(() => this.hide(), HIDE_DELAY)
576
664
  }
577
665
  }
578
666
 
@@ -620,6 +708,13 @@ class Datepicker extends BaseComponent {
620
708
  return
621
709
  }
622
710
 
711
+ const written = writtenSelections.get(this._element)
712
+ if (written && written.text.trim() === value) {
713
+ this._selectedDates = [...written.dates]
714
+ this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
715
+ return
716
+ }
717
+
623
718
  // `YYYY-MM-DD` is read as a local date: `new Date('2020-06-20')` would be
624
719
  // UTC midnight and land on the previous day west of Greenwich.
625
720
  const date = DATE_PATTERN.test(value) ? this._parseDate(value) : new Date(value)
@@ -627,11 +722,36 @@ class Datepicker extends BaseComponent {
627
722
  return
628
723
  }
629
724
 
725
+ this._selectedDates = [this._toIsoDate(date)]
726
+ // The calendar opens on the month of the value, not on today.
727
+ this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
728
+ }
729
+
730
+ _toIsoDate(value: DateAny | string): string {
731
+ if (typeof value === 'string' && DATE_PATTERN.test(value)) {
732
+ return value
733
+ }
734
+
735
+ const date = value instanceof Date ? value : typeof value === 'string' && DATE_PATTERN.test(value) ? this._parseDate(value) : new Date(value)
736
+ if (Number.isNaN(date.getTime())) {
737
+ return String(value)
738
+ }
739
+
630
740
  const year = date.getFullYear()
631
741
  const month = String(date.getMonth() + 1).padStart(2, '0')
632
742
  const day = String(date.getDate()).padStart(2, '0')
633
- this._selectedDates = [`${year}-${month}-${day}`]
634
- this._calendar?.set({ selectedDates: this._selectedDates })
743
+ return `${year}-${month}-${day}`
744
+ }
745
+
746
+ /** @internal */
747
+ _monthOf(value: string | undefined): Pick<Options, 'selectedMonth' | 'selectedYear'> {
748
+ if (!value || !DATE_PATTERN.test(value)) {
749
+ return {}
750
+ }
751
+
752
+ const date = this._parseDate(value)
753
+ // `getMonth()` is always 0-11, which TypeScript cannot narrow on its own
754
+ return { selectedMonth: date.getMonth() as Range<12>, selectedYear: date.getFullYear() }
635
755
  }
636
756
  }
637
757
 
@@ -1,11 +1,18 @@
1
1
  import { Dropdown } from './bootstrap'
2
+ import Manipulator from './bootstrap/dom/manipulator'
3
+ import { onDOMContentLoaded } from './bootstrap/util/index'
2
4
 
3
5
  // js-docs-start dropdown-init
4
- const dropdownTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"]'))
5
- dropdownTriggerList.map(function (dropdownTriggerEl: HTMLElement) {
6
- const options = {
7
- boundary: dropdownTriggerEl.getAttribute('data-bs-boundary') === 'viewport' ? document.documentElement : ('clippingParents' as const),
6
+ onDOMContentLoaded(() => {
7
+ for (const dropdownTriggerEl of document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"], [data-tblr-toggle="dropdown"]')) {
8
+ try {
9
+ const options = {
10
+ boundary: Manipulator.getDataAttribute(dropdownTriggerEl, 'boundary') === 'viewport' ? document.documentElement : ('clippingParents' as const),
11
+ }
12
+ Dropdown.getOrCreateInstance(dropdownTriggerEl, options)
13
+ } catch (error) {
14
+ console.error(error)
15
+ }
8
16
  }
9
- return new Dropdown(dropdownTriggerEl, options)
10
17
  })
11
18
  // js-docs-end dropdown-init
@@ -3,11 +3,13 @@ import type { JQueryStaticLike } from './bootstrap/types'
3
3
  // Plugins that are loaded separately, not bundled: IMask (https://imask.js.org),
4
4
  // SortableJS (https://sortablejs.github.io/Sortable/) and Vanilla Calendar Pro
5
5
  // (https://vanilla-calendar.pro). The instance types live in the component
6
- // modules, so they are part of the published `dist/types`.
6
+ // modules, so they are part of the published `dist/types`. This file itself is
7
+ // not emitted there (tsc does not copy `.d.ts` sources), so the real
8
+ // `vanilla-calendar-pro` import below is only seen inside this repo.
7
9
  declare global {
8
10
  interface Window {
9
11
  VanillaCalendarPro?: typeof import('vanilla-calendar-pro')
10
- IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => import('./input-mask').IMaskInstance
12
+ IMask?: new (element: HTMLElement, options: { mask: unknown; lazy?: boolean } & Record<string, unknown>) => import('./input-mask').IMaskInstance
11
13
  Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => import('./sortable').SortableInstance
12
14
  jQuery?: JQueryStaticLike
13
15
  }
@@ -19,7 +19,8 @@ interface IMaskInstance {
19
19
  }
20
20
 
21
21
  type ComponentConfig = {
22
- mask: string
22
+ /** an IMask mask: a pattern string, `Number`, `Date`, a RegExp or a mask object */
23
+ mask: unknown
23
24
  lazy: boolean
24
25
  }
25
26
 
@@ -42,7 +43,7 @@ const Default: ComponentConfig = {
42
43
  }
43
44
 
44
45
  const DefaultType: Record<keyof ComponentConfig, string> = {
45
- mask: 'string',
46
+ mask: '(string|function|regexp|object|array)',
46
47
  lazy: 'boolean',
47
48
  }
48
49
 
@@ -65,10 +66,8 @@ class InputMask extends BaseComponent {
65
66
  return
66
67
  }
67
68
 
68
- this._mask = new window.IMask(this._element, {
69
- mask: this._config.mask,
70
- lazy: this._config.lazy,
71
- })
69
+ // Every option from the config reaches IMask, not only the two typed ones.
70
+ this._mask = new window.IMask(this._element, { ...this._config })
72
71
  }
73
72
 
74
73
  // Getters
@@ -110,8 +109,9 @@ class InputMask extends BaseComponent {
110
109
  dataOptions.mask = mask
111
110
  }
112
111
 
112
+ // A bare `data-mask-visible` means visible, like `="true"`.
113
113
  if (element?.hasAttribute(ATTRIBUTE_VISIBLE)) {
114
- dataOptions.lazy = element.getAttribute(ATTRIBUTE_VISIBLE) !== 'true'
114
+ dataOptions.lazy = element.getAttribute(ATTRIBUTE_VISIBLE) === 'false'
115
115
  }
116
116
 
117
117
  return super._mergeConfigObj({ ...dataOptions, ...config }, element)
@@ -48,6 +48,9 @@ const SELECTOR_INPUT = 'input'
48
48
  // Keeps the active-slot highlight in sync with the caret as it moves without typing
49
49
  const SYNC_EVENTS = ['blur', 'keyup', 'select']
50
50
 
51
+ // A touch focuses the input after `pointerup`, so the tap is only settled by `click`
52
+ const POINTER_END_EVENTS = ['click', 'pointercancel']
53
+
51
54
  const MASK_CHARACTER = '•'
52
55
 
53
56
  // Per-type input mode, validation pattern, and a filter that strips disallowed characters
@@ -57,6 +60,9 @@ const TYPES: Record<OtpInputType, { inputmode: string; pattern: string; filter:
57
60
  alpha: { inputmode: 'text', pattern: '[A-Za-z]*', filter: /[^A-Za-z]/g },
58
61
  }
59
62
 
63
+ // Upper bound for the slot count, whatever the config asks for.
64
+ const MAX_LENGTH = 32
65
+
60
66
  const Default: ComponentConfig = {
61
67
  groups: null,
62
68
  length: null,
@@ -87,17 +93,38 @@ class OtpInput extends BaseComponent {
87
93
  _input!: HTMLInputElement
88
94
  _type!: (typeof TYPES)[OtpInputType]
89
95
  _length = 0
96
+ _wasComplete = false
90
97
  _slots: HTMLElement[] = []
91
98
  _slotsContainer: HTMLElement | null = null
92
99
  // Tracks a tap so focus (fired natively by the browser) can respect the
93
100
  // clicked slot instead of jumping to the first empty one
94
101
  _pointerActive = false
95
102
  _pointerIndex = 0
103
+ _composedValue: string | null = null
104
+
105
+ _onInput = (event: Event): void => {
106
+ if ((event as InputEvent).isComposing) {
107
+ return
108
+ }
96
109
 
97
- _onInput = (): void => this._handleInput()
110
+ const composed = this._composedValue
111
+ this._composedValue = null
112
+ if (composed !== null && composed === this._input.value) {
113
+ return
114
+ }
115
+
116
+ this._handleInput()
117
+ }
118
+ _onCompositionEnd = (): void => {
119
+ this._handleInput()
120
+ this._composedValue = this._input.value
121
+ }
98
122
  _onBeforeInput = (event: Event): void => this._handleBeforeInput(event as InputEvent)
99
123
  _onFocus = (): void => this._handleFocus()
100
124
  _onPointerDown = (event: Event): void => this._handlePointerDown(event as PointerEvent)
125
+ _onPointerEnd = (): void => {
126
+ this._pointerActive = false
127
+ }
101
128
  _onSync = (): void => this._render()
102
129
  _onSelectionChange = (): void => {
103
130
  if (document.activeElement === this._input) {
@@ -125,6 +152,7 @@ class OtpInput extends BaseComponent {
125
152
  this._renderSlots()
126
153
  this._addEventListeners()
127
154
  this._render()
155
+ this._wasComplete = this._input.value.length === this._length
128
156
  }
129
157
 
130
158
  // Getters
@@ -158,6 +186,7 @@ class OtpInput extends BaseComponent {
158
186
  }
159
187
 
160
188
  focus(): void {
189
+ this._pointerActive = false
161
190
  this._input.focus()
162
191
  this._selectSlot(this._firstEmptyIndex())
163
192
  this._render()
@@ -173,6 +202,10 @@ class OtpInput extends BaseComponent {
173
202
  this._input.removeEventListener('beforeinput', this._onBeforeInput)
174
203
  this._input.removeEventListener('focus', this._onFocus)
175
204
  this._input.removeEventListener('pointerdown', this._onPointerDown)
205
+ this._input.removeEventListener('compositionend', this._onCompositionEnd)
206
+ for (const type of POINTER_END_EVENTS) {
207
+ this._input.removeEventListener(type, this._onPointerEnd)
208
+ }
176
209
  for (const type of SYNC_EVENTS) {
177
210
  this._input.removeEventListener(type, this._onSync)
178
211
  }
@@ -186,12 +219,22 @@ class OtpInput extends BaseComponent {
186
219
 
187
220
  // Private
188
221
  _resolveLength(): number {
189
- if (this._config.length) {
190
- return this._config.length
222
+ const candidates: unknown[] = [this._config.length, Number.parseInt(this._input.getAttribute('maxlength') ?? '', 10)]
223
+
224
+ const { groups } = this._config
225
+ if (Array.isArray(groups) && groups.length > 0) {
226
+ candidates.push(groups.reduce<number>((sum, group) => sum + Number(group), 0))
227
+ }
228
+
229
+ // The first usable value wins; a negative, fractional, or huge length
230
+ // would render no slots or flood the DOM.
231
+ for (const candidate of candidates) {
232
+ if (typeof candidate === 'number' && Number.isInteger(candidate) && candidate > 0) {
233
+ return Math.min(candidate, MAX_LENGTH)
234
+ }
191
235
  }
192
236
 
193
- const maxLength = Number.parseInt(this._input.getAttribute('maxlength') ?? '', 10)
194
- return Number.isNaN(maxLength) || maxLength < 1 ? 6 : maxLength
237
+ return 6
195
238
  }
196
239
 
197
240
  _setupInput(): void {
@@ -199,7 +242,8 @@ class OtpInput extends BaseComponent {
199
242
 
200
243
  // A single text field backs the whole control so screen readers, password
201
244
  // managers, and SMS autofill treat it like any other input.
202
- if (input.type === 'number' || input.type === 'password') {
245
+ // Only text-like types support the selection API the slots rely on.
246
+ if (input.type !== 'text' && input.type !== 'tel') {
203
247
  input.type = 'text'
204
248
  }
205
249
 
@@ -255,6 +299,11 @@ class OtpInput extends BaseComponent {
255
299
  this._input.addEventListener('beforeinput', this._onBeforeInput)
256
300
  this._input.addEventListener('focus', this._onFocus)
257
301
  this._input.addEventListener('pointerdown', this._onPointerDown)
302
+ this._input.addEventListener('compositionend', this._onCompositionEnd)
303
+ for (const type of POINTER_END_EVENTS) {
304
+ this._input.addEventListener(type, this._onPointerEnd)
305
+ }
306
+
258
307
  document.addEventListener('selectionchange', this._onSelectionChange)
259
308
 
260
309
  for (const type of SYNC_EVENTS) {
@@ -292,9 +341,10 @@ class OtpInput extends BaseComponent {
292
341
  this._afterValueChange()
293
342
  }
294
343
 
295
- // Intercepts single-character typing and backspace so each slot is
344
+ // Intercepts single-character typing, Backspace and Delete so each slot is
296
345
  // overwritten in place rather than inserting and shifting the value.
297
- // Anything else (paste, autofill, IME) falls through to `_handleInput`.
346
+ // Anything else (paste, autofill) falls through to `_handleInput`; IME
347
+ // composition is sanitised once on `compositionend`.
298
348
  _handleBeforeInput(event: InputEvent): void {
299
349
  const { inputType, data } = event
300
350
 
@@ -307,8 +357,9 @@ class OtpInput extends BaseComponent {
307
357
  }
308
358
 
309
359
  const index = Math.min(this._input.selectionStart ?? 0, this._length - 1)
360
+ const end = Math.max(this._input.selectionEnd ?? index, index + 1)
310
361
  const chars = [...this._input.value]
311
- chars[index] = char
362
+ chars.splice(index, end - index, char)
312
363
  this._input.value = chars.join('').slice(0, this._length)
313
364
 
314
365
  this._selectSlot(index + 1)
@@ -319,6 +370,8 @@ class OtpInput extends BaseComponent {
319
370
  if (inputType === 'deleteContentBackward') {
320
371
  event.preventDefault()
321
372
 
373
+ const before = this._input.value
374
+
322
375
  const start = this._input.selectionStart ?? 0
323
376
  const end = this._input.selectionEnd ?? start
324
377
  const chars = [...this._input.value]
@@ -333,7 +386,29 @@ class OtpInput extends BaseComponent {
333
386
  this._selectSlot(start - 1)
334
387
  }
335
388
 
336
- this._afterValueChange()
389
+ this._afterValueChange(before)
390
+ return
391
+ }
392
+
393
+ if (inputType === 'deleteContentForward') {
394
+ event.preventDefault()
395
+
396
+ const before = this._input.value
397
+
398
+ const start = this._input.selectionStart ?? 0
399
+ const end = this._input.selectionEnd ?? start
400
+ const chars = [...this._input.value]
401
+
402
+ if (end > start) {
403
+ chars.splice(start, end - start)
404
+ this._input.value = chars.join('')
405
+ } else if (start < chars.length) {
406
+ chars.splice(start, 1)
407
+ this._input.value = chars.join('')
408
+ }
409
+
410
+ this._selectSlot(start)
411
+ this._afterValueChange(before)
337
412
  }
338
413
  }
339
414
 
@@ -378,8 +453,15 @@ class OtpInput extends BaseComponent {
378
453
  return null
379
454
  }
380
455
 
381
- _afterValueChange(): void {
456
+ // `before` is the value ahead of the edit: a Backspace in the first slot or
457
+ // a Delete past the last one changes nothing and fires no event.
458
+ _afterValueChange(before?: string): void {
382
459
  this._render()
460
+
461
+ if (before !== undefined && before === this._input.value) {
462
+ return
463
+ }
464
+
383
465
  EventHandler.trigger(this._element, EVENT_INPUT, { value: this._input.value })
384
466
  this._checkComplete()
385
467
  }
@@ -413,11 +495,16 @@ class OtpInput extends BaseComponent {
413
495
  }
414
496
  }
415
497
 
498
+ // Fires once when the value becomes full, not on every edit of a full value.
416
499
  _checkComplete(): void {
417
500
  const { value } = this._input
418
- if (value.length === this._length) {
501
+ const complete = value.length === this._length
502
+
503
+ if (complete && !this._wasComplete) {
419
504
  EventHandler.trigger(this._element, EVENT_COMPLETE, { value })
420
505
  }
506
+
507
+ this._wasComplete = complete
421
508
  }
422
509
  }
423
510