@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.
- package/dist/css/tabler-flags.css +1 -1
- package/dist/css/tabler-flags.css.map +1 -1
- package/dist/css/tabler-flags.min.css +1 -1
- package/dist/css/tabler-flags.min.css.map +1 -1
- package/dist/css/tabler-flags.rtl.css +1 -1
- package/dist/css/tabler-flags.rtl.css.map +1 -1
- package/dist/css/tabler-flags.rtl.min.css +1 -1
- package/dist/css/tabler-flags.rtl.min.css.map +1 -1
- package/dist/css/tabler-marketing.css +1 -1
- package/dist/css/tabler-marketing.css.map +1 -1
- package/dist/css/tabler-marketing.min.css +1 -1
- package/dist/css/tabler-marketing.min.css.map +1 -1
- package/dist/css/tabler-marketing.rtl.css +1 -1
- package/dist/css/tabler-marketing.rtl.css.map +1 -1
- package/dist/css/tabler-marketing.rtl.min.css +1 -1
- package/dist/css/tabler-marketing.rtl.min.css.map +1 -1
- package/dist/css/tabler-payments.css +1 -1
- package/dist/css/tabler-payments.css.map +1 -1
- package/dist/css/tabler-payments.min.css +1 -1
- package/dist/css/tabler-payments.min.css.map +1 -1
- package/dist/css/tabler-payments.rtl.css +1 -1
- package/dist/css/tabler-payments.rtl.css.map +1 -1
- package/dist/css/tabler-payments.rtl.min.css +1 -1
- package/dist/css/tabler-payments.rtl.min.css.map +1 -1
- package/dist/css/tabler-props.css +1 -1
- package/dist/css/tabler-props.css.map +1 -1
- package/dist/css/tabler-props.min.css +1 -1
- package/dist/css/tabler-props.min.css.map +1 -1
- package/dist/css/tabler-props.rtl.css +1 -1
- package/dist/css/tabler-props.rtl.css.map +1 -1
- package/dist/css/tabler-props.rtl.min.css +1 -1
- package/dist/css/tabler-props.rtl.min.css.map +1 -1
- package/dist/css/tabler-socials.css +1 -1
- package/dist/css/tabler-socials.css.map +1 -1
- package/dist/css/tabler-socials.min.css +1 -1
- package/dist/css/tabler-socials.min.css.map +1 -1
- package/dist/css/tabler-socials.rtl.css +1 -1
- package/dist/css/tabler-socials.rtl.css.map +1 -1
- package/dist/css/tabler-socials.rtl.min.css +1 -1
- package/dist/css/tabler-socials.rtl.min.css.map +1 -1
- package/dist/css/tabler-themes.css +1 -1
- package/dist/css/tabler-themes.css.map +1 -1
- package/dist/css/tabler-themes.min.css +1 -1
- package/dist/css/tabler-themes.min.css.map +1 -1
- package/dist/css/tabler-themes.rtl.css +1 -1
- package/dist/css/tabler-themes.rtl.css.map +1 -1
- package/dist/css/tabler-themes.rtl.min.css +1 -1
- package/dist/css/tabler-themes.rtl.min.css.map +1 -1
- package/dist/css/tabler-vendors.css +1 -1
- package/dist/css/tabler-vendors.css.map +1 -1
- package/dist/css/tabler-vendors.min.css +1 -1
- package/dist/css/tabler-vendors.min.css.map +1 -1
- package/dist/css/tabler-vendors.rtl.css +1 -1
- package/dist/css/tabler-vendors.rtl.css.map +1 -1
- package/dist/css/tabler-vendors.rtl.min.css +1 -1
- package/dist/css/tabler-vendors.rtl.min.css.map +1 -1
- package/dist/css/tabler.css +12 -2
- package/dist/css/tabler.css.map +1 -1
- package/dist/css/tabler.min.css +2 -2
- package/dist/css/tabler.min.css.map +1 -1
- package/dist/css/tabler.rtl.css +12 -2
- package/dist/css/tabler.rtl.css.map +1 -1
- package/dist/css/tabler.rtl.min.css +2 -2
- package/dist/css/tabler.rtl.min.css.map +1 -1
- package/dist/js/tabler-theme.esm.js +21 -6
- package/dist/js/tabler-theme.esm.js.map +1 -1
- package/dist/js/tabler-theme.esm.min.js +2 -2
- package/dist/js/tabler-theme.esm.min.js.map +1 -1
- package/dist/js/tabler-theme.js +21 -6
- package/dist/js/tabler-theme.js.map +1 -1
- package/dist/js/tabler-theme.min.js +2 -2
- package/dist/js/tabler-theme.min.js.map +1 -1
- package/dist/js/tabler.esm.js +332 -131
- package/dist/js/tabler.esm.js.map +1 -1
- package/dist/js/tabler.esm.min.js +2 -2
- package/dist/js/tabler.esm.min.js.map +1 -1
- package/dist/js/tabler.js +332 -131
- package/dist/js/tabler.js.map +1 -1
- package/dist/js/tabler.min.js +2 -2
- package/dist/js/tabler.min.js.map +1 -1
- package/dist/libs/vanilla-calendar-pro/index.js +2 -2
- package/dist/types/src/clipboard.d.ts +3 -0
- package/dist/types/src/confetti.d.ts +2 -0
- package/dist/types/src/countup.d.ts +2 -0
- package/dist/types/src/datepicker.d.ts +24 -6
- package/dist/types/src/input-mask.d.ts +2 -1
- package/dist/types/src/otp-input.d.ts +6 -2
- package/dist/types/src/strength.d.ts +1 -0
- package/dist/types/src/switch-icon.d.ts +1 -0
- package/js/src/autosize.ts +8 -3
- package/js/src/bootstrap/base-component.ts +12 -2
- package/js/src/bootstrap/dom/manipulator.ts +1 -1
- package/js/src/bootstrap/dropdown.ts +3 -1
- package/js/src/bootstrap/util/index.ts +14 -1
- package/js/src/clipboard.ts +26 -2
- package/js/src/confetti.ts +42 -7
- package/js/src/countup.ts +30 -12
- package/js/src/datepicker.ts +147 -27
- package/js/src/dropdown.ts +12 -5
- package/js/src/global.d.ts +4 -2
- package/js/src/input-mask.ts +7 -7
- package/js/src/otp-input.ts +99 -12
- package/js/src/popover.ts +13 -7
- package/js/src/sidebar.ts +14 -6
- package/js/src/sortable.ts +5 -1
- package/js/src/sparkline.ts +15 -6
- package/js/src/strength.ts +17 -4
- package/js/src/switch-icon.ts +15 -0
- package/js/src/tab.ts +17 -7
- package/js/src/toast.ts +19 -11
- package/js/src/tooltip.ts +16 -7
- package/js/tabler-theme.ts +28 -4
- package/package.json +2 -2
- package/scss/layout/_navbar.scss +1 -2
- package/scss/ui/_cards.scss +14 -2
package/js/src/datepicker.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* --------------------------------------------------------------------------
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Calendar,
|
|
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
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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:
|
|
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():
|
|
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
|
|
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(
|
|
264
|
-
this._calendar?.set({ selectedDates:
|
|
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
|
-
|
|
310
|
-
if (!selectedDates || selectedDates.length === 0) {
|
|
370
|
+
if (this._selectedDates.length === 0) {
|
|
311
371
|
return
|
|
312
372
|
}
|
|
313
373
|
|
|
314
|
-
this._writeSelection(
|
|
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 =
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
|
package/js/src/dropdown.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
package/js/src/global.d.ts
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/js/src/input-mask.ts
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
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)
|
|
114
|
+
dataOptions.lazy = element.getAttribute(ATTRIBUTE_VISIBLE) === 'false'
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
return super._mergeConfigObj({ ...dataOptions, ...config }, element)
|
package/js/src/otp-input.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|