mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Rewrite switch-icon, countup, input-mask and sortable as components (#3046)
This commit is contained in:
1 parent
a13ebc3621
commit
bd5c010595
14 files changed
+1007
-95
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": minor
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added `SwitchIcon`, `CountUp`, `InputMask` and `Sortable` components to `tabler.js`, with countUp.js bundled.
|
||||||
+131
-25
@@ -1,33 +1,139 @@
|
|||||||
// js-docs-start countup-init
|
/**
|
||||||
const countupElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-countup]')
|
* --------------------------------------------------------------------------
|
||||||
|
* Tabler countup.ts
|
||||||
|
* Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE)
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
if (countupElements.length) {
|
import { CountUp as CountUpPlugin, type CountUpOptions } from 'countup.js'
|
||||||
countupElements.forEach(function (element: HTMLElement) {
|
|
||||||
let options: Record<string, unknown> = {}
|
import BaseComponent from './bootstrap/base-component'
|
||||||
try {
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')!) : {}
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
options = Object.assign(
|
|
||||||
{
|
// Mapped copy of the plugin's interface, so it satisfies the `Record` base config
|
||||||
enableScrollSpy: true,
|
type ComponentConfig = { [K in keyof CountUpOptions]: CountUpOptions[K] } & {
|
||||||
},
|
autoAnimate: boolean
|
||||||
dataOptions,
|
}
|
||||||
)
|
|
||||||
} catch (error) {
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
// ignore invalid JSON
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NAME = 'countup'
|
||||||
|
const DATA_ATTRIBUTE = `data-${NAME}`
|
||||||
|
|
||||||
|
const SELECTOR_DATA_COUNTUP = `[${DATA_ATTRIBUTE}]`
|
||||||
|
|
||||||
|
const Default: ComponentConfig = {
|
||||||
|
autoAnimate: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultType: Record<'autoAnimate', string> = {
|
||||||
|
autoAnimate: 'boolean',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class definition
|
||||||
|
*
|
||||||
|
* Animates the number written inside the element with countUp.js
|
||||||
|
* (https://github.com/inorganik/countUp.js), bundled with `tabler.js`. Options
|
||||||
|
* come from the `data-countup` attribute as JSON, or from the config object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CountUp extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
|
_plugin: CountUpPlugin | null = null
|
||||||
|
|
||||||
|
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||||
|
super(element, config)
|
||||||
|
|
||||||
|
if (!this._element) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip thousands separators, currency symbols and other non-numeric characters
|
// Strip thousands separators, currency symbols and other non-numeric
|
||||||
// so formatted targets like "1,234", "1 234" or "$99.5" parse correctly.
|
// characters so formatted targets like "1,234", "1 234" or "$99.5" parse.
|
||||||
const value = parseFloat((element.textContent ?? '').replace(/[^0-9.-]/g, ''))
|
const value = Number.parseFloat((this._element.textContent ?? '').replace(/[^0-9.-]/g, ''))
|
||||||
|
|
||||||
if (!Number.isNaN(value) && window.countUp && window.countUp.CountUp) {
|
if (Number.isNaN(value)) {
|
||||||
const countUp = new window.countUp.CountUp(element, value, options)
|
return
|
||||||
// When scrollSpy is enabled CountUp starts the animation itself once the
|
}
|
||||||
// element scrolls into view, so only start manually when it's disabled.
|
|
||||||
if (!countUp.error && !options.enableScrollSpy) {
|
this._plugin = new CountUpPlugin(this._element, value, this._config)
|
||||||
countUp.start()
|
|
||||||
|
// With `autoAnimate` the plugin starts itself once the element scrolls into
|
||||||
|
// view, so only start by hand when it is off.
|
||||||
|
if (!this._plugin.error && !this._config.autoAnimate && !this._config.enableScrollSpy) {
|
||||||
|
this._plugin.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
static get Default(): ComponentConfig {
|
||||||
|
return Default
|
||||||
|
}
|
||||||
|
|
||||||
|
static get DefaultType(): Record<'autoAnimate', string> {
|
||||||
|
return DefaultType
|
||||||
|
}
|
||||||
|
|
||||||
|
static get NAME(): string {
|
||||||
|
return NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public
|
||||||
|
start(): void {
|
||||||
|
this._plugin?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this._plugin?.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
update(value: number | string): void {
|
||||||
|
this._plugin?.update(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseResume(): void {
|
||||||
|
this._plugin?.pauseResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this._plugin?.onDestroy()
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
_mergeConfigObj(config?: Record<string, unknown>, element?: HTMLElement): Record<string, unknown> {
|
||||||
|
// `data-countup='{"duration":4}'` carries the options as JSON; a bare
|
||||||
|
// `data-countup` or invalid JSON means defaults.
|
||||||
|
let dataOptions: Record<string, unknown> = {}
|
||||||
|
const raw = element?.getAttribute(DATA_ATTRIBUTE)
|
||||||
|
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
dataOptions = JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
// ignore invalid JSON
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
return super._mergeConfigObj({ ...dataOptions, ...config }, element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data API implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
// js-docs-start countup-init
|
||||||
|
for (const element of SelectorEngine.find(SELECTOR_DATA_COUNTUP)) {
|
||||||
|
CountUp.getOrCreateInstance(element)
|
||||||
}
|
}
|
||||||
// js-docs-end countup-init
|
// js-docs-end countup-init
|
||||||
|
|
||||||
|
export default CountUp
|
||||||
Vendored
+20
-13
@@ -12,17 +12,24 @@ declare module 'autosize' {
|
|||||||
export default autosize
|
export default autosize
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
// IMask (https://imask.js.org) is loaded separately, not bundled
|
||||||
countUp?: {
|
interface IMaskInstance {
|
||||||
CountUp: new (
|
value: string
|
||||||
target: HTMLElement,
|
unmaskedValue: string
|
||||||
endVal: number,
|
updateValue(): void
|
||||||
options?: Record<string, unknown>,
|
updateOptions(options: Record<string, unknown>): void
|
||||||
) => {
|
destroy(): void
|
||||||
error: boolean
|
}
|
||||||
start: () => void
|
|
||||||
}
|
// SortableJS (https://sortablejs.github.io/Sortable/) is loaded separately, not bundled
|
||||||
}
|
interface SortableInstance {
|
||||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => unknown
|
option(name: string, value?: unknown): unknown
|
||||||
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => unknown
|
toArray(): string[]
|
||||||
|
sort(order: string[], useAnimation?: boolean): void
|
||||||
|
destroy(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => IMaskInstance
|
||||||
|
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => SortableInstance
|
||||||
}
|
}
|
||||||
+120
-8
@@ -1,10 +1,122 @@
|
|||||||
// js-docs-start input-mask-init
|
/**
|
||||||
const maskElementList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-mask]'))
|
* --------------------------------------------------------------------------
|
||||||
maskElementList.map(function (maskEl: HTMLElement) {
|
* Tabler input-mask.ts
|
||||||
window.IMask &&
|
* Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE)
|
||||||
new window.IMask(maskEl, {
|
* --------------------------------------------------------------------------
|
||||||
mask: maskEl.dataset.mask as string,
|
*/
|
||||||
lazy: maskEl.dataset.maskVisible !== 'true',
|
|
||||||
|
import BaseComponent from './bootstrap/base-component'
|
||||||
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
|
type ComponentConfig = {
|
||||||
|
mask: string
|
||||||
|
lazy: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NAME = 'input-mask'
|
||||||
|
|
||||||
|
const ATTRIBUTE_MASK = 'data-mask'
|
||||||
|
const ATTRIBUTE_VISIBLE = 'data-mask-visible'
|
||||||
|
|
||||||
|
const SELECTOR_DATA_MASK = `[${ATTRIBUTE_MASK}]`
|
||||||
|
|
||||||
|
const Default: ComponentConfig = {
|
||||||
|
mask: '',
|
||||||
|
lazy: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
|
mask: 'string',
|
||||||
|
lazy: 'boolean',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class definition
|
||||||
|
*
|
||||||
|
* Wraps the IMask plugin (https://imask.js.org), loaded separately as
|
||||||
|
* `window.IMask`. Without the plugin the component is inert.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InputMask extends BaseComponent {
|
||||||
|
declare _element: HTMLInputElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
|
_mask: IMaskInstance | null = null
|
||||||
|
|
||||||
|
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||||
|
super(element, config)
|
||||||
|
|
||||||
|
if (!this._element || !this._config.mask || !window.IMask) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._mask = new window.IMask(this._element, {
|
||||||
|
mask: this._config.mask,
|
||||||
|
lazy: this._config.lazy,
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
static get Default(): ComponentConfig {
|
||||||
|
return Default
|
||||||
|
}
|
||||||
|
|
||||||
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
|
return DefaultType
|
||||||
|
}
|
||||||
|
|
||||||
|
static get NAME(): string {
|
||||||
|
return NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The IMask instance, for options the component does not expose. */
|
||||||
|
get mask(): IMaskInstance | null {
|
||||||
|
return this._mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public
|
||||||
|
update(): void {
|
||||||
|
this._mask?.updateValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this._mask?.destroy()
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
_mergeConfigObj(config?: Record<string, unknown>, element?: HTMLElement): Record<string, unknown> {
|
||||||
|
// `data-mask` holds the pattern; `data-mask-visible="true"` shows it
|
||||||
|
// before typing, which is IMask's `lazy: false`.
|
||||||
|
const dataOptions: Record<string, unknown> = {}
|
||||||
|
const mask = element?.getAttribute(ATTRIBUTE_MASK)
|
||||||
|
|
||||||
|
if (mask) {
|
||||||
|
dataOptions.mask = mask
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element?.hasAttribute(ATTRIBUTE_VISIBLE)) {
|
||||||
|
dataOptions.lazy = element.getAttribute(ATTRIBUTE_VISIBLE) !== 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
return super._mergeConfigObj({ ...dataOptions, ...config }, element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data API implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
// js-docs-start input-mask-init
|
||||||
|
for (const element of SelectorEngine.find(SELECTOR_DATA_MASK)) {
|
||||||
|
InputMask.getOrCreateInstance(element)
|
||||||
|
}
|
||||||
// js-docs-end input-mask-init
|
// js-docs-end input-mask-init
|
||||||
|
|
||||||
|
export default InputMask
|
||||||
+106
-15
@@ -1,22 +1,113 @@
|
|||||||
// Initializes Sortable on elements marked with [data-sortable]
|
/**
|
||||||
// Allows options via JSON in data attribute: data-sortable='{"animation":150}'
|
* --------------------------------------------------------------------------
|
||||||
|
* Tabler sortable.ts
|
||||||
|
* Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE)
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
const sortableElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-sortable]')
|
import BaseComponent from './bootstrap/base-component'
|
||||||
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
if (sortableElements.length) {
|
type ComponentConfig = Record<string, unknown>
|
||||||
sortableElements.forEach(function (element: HTMLElement) {
|
|
||||||
let options: Record<string, unknown> = {}
|
|
||||||
|
|
||||||
try {
|
type ComponentConfigInput = Record<string, unknown>
|
||||||
const rawOptions = element.getAttribute('data-sortable')
|
|
||||||
options = rawOptions ? JSON.parse(rawOptions) : {}
|
/**
|
||||||
} catch (e) {
|
* Constants
|
||||||
// ignore invalid JSON
|
*/
|
||||||
|
|
||||||
|
const NAME = 'sortable'
|
||||||
|
const DATA_ATTRIBUTE = `data-${NAME}`
|
||||||
|
|
||||||
|
const SELECTOR_DATA_SORTABLE = `[${DATA_ATTRIBUTE}]`
|
||||||
|
|
||||||
|
const Default: ComponentConfig = {}
|
||||||
|
|
||||||
|
const DefaultType: Record<string, string> = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class definition
|
||||||
|
*
|
||||||
|
* Wraps SortableJS (https://sortablejs.github.io/Sortable/), loaded separately
|
||||||
|
* as `window.Sortable`. Without the plugin the component is inert. Options come
|
||||||
|
* from the `data-sortable` attribute as JSON, or from the config object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Sortable extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
|
_sortable: SortableInstance | null = null
|
||||||
|
|
||||||
|
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||||
|
super(element, config)
|
||||||
|
|
||||||
|
if (!this._element || !window.Sortable) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.Sortable) {
|
this._sortable = new window.Sortable(this._element, this._config)
|
||||||
// eslint-disable-next-line no-new
|
}
|
||||||
new window.Sortable(element, options)
|
|
||||||
|
// Getters
|
||||||
|
static get Default(): ComponentConfig {
|
||||||
|
return Default
|
||||||
|
}
|
||||||
|
|
||||||
|
static get DefaultType(): Record<string, string> {
|
||||||
|
return DefaultType
|
||||||
|
}
|
||||||
|
|
||||||
|
static get NAME(): string {
|
||||||
|
return NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The SortableJS instance, for options the component does not expose. */
|
||||||
|
get sortable(): SortableInstance | null {
|
||||||
|
return this._sortable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public
|
||||||
|
toArray(): string[] {
|
||||||
|
return this._sortable?.toArray() ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(order: string[], useAnimation?: boolean): void {
|
||||||
|
this._sortable?.sort(order, useAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this._sortable?.destroy()
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
_mergeConfigObj(config?: Record<string, unknown>, element?: HTMLElement): Record<string, unknown> {
|
||||||
|
// `data-sortable='{"animation":150}'` carries the options as JSON; a bare
|
||||||
|
// `data-sortable` or invalid JSON means defaults.
|
||||||
|
let dataOptions: Record<string, unknown> = {}
|
||||||
|
const raw = element?.getAttribute(DATA_ATTRIBUTE)
|
||||||
|
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
dataOptions = JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
// ignore invalid JSON
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
return super._mergeConfigObj({ ...dataOptions, ...config }, element)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data API implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
// js-docs-start sortable-init
|
||||||
|
for (const element of SelectorEngine.find(SELECTOR_DATA_SORTABLE)) {
|
||||||
|
Sortable.getOrCreateInstance(element)
|
||||||
|
}
|
||||||
|
// js-docs-end sortable-init
|
||||||
|
|
||||||
|
export default Sortable
|
||||||
+100
-9
@@ -1,11 +1,102 @@
|
|||||||
// js-docs-start switch-icon-init
|
/**
|
||||||
const switchesTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="switch-icon"]'))
|
* --------------------------------------------------------------------------
|
||||||
switchesTriggerList.map(function (switchTriggerEl: HTMLElement) {
|
* Tabler switch-icon.ts
|
||||||
switchTriggerEl.addEventListener('click', (e: MouseEvent) => {
|
* Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE)
|
||||||
e.stopPropagation()
|
* --------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
const active = switchTriggerEl.classList.toggle('active')
|
import BaseComponent from './bootstrap/base-component'
|
||||||
switchTriggerEl.setAttribute('aria-pressed', active ? 'true' : 'false')
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
})
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
})
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
|
type ComponentConfig = Record<string, never>
|
||||||
|
|
||||||
|
type ComponentConfigInput = Record<string, unknown>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NAME = 'switch-icon'
|
||||||
|
const DATA_KEY = `bs.${NAME}`
|
||||||
|
const EVENT_KEY = `.${DATA_KEY}`
|
||||||
|
|
||||||
|
const EVENT_CLICK = `click${EVENT_KEY}`
|
||||||
|
const EVENT_CHANGE = `change${EVENT_KEY}`
|
||||||
|
|
||||||
|
const CLASS_NAME_ACTIVE = 'active'
|
||||||
|
|
||||||
|
const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${NAME}"], [data-tblr-toggle="${NAME}"]`
|
||||||
|
|
||||||
|
const Default: ComponentConfig = {}
|
||||||
|
|
||||||
|
const DefaultType: Record<keyof ComponentConfig, string> = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class definition
|
||||||
|
*
|
||||||
|
* A toggle button that swaps between two icons. The state is the `active`
|
||||||
|
* class, mirrored in `aria-pressed`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SwitchIcon extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
|
|
||||||
|
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||||
|
super(element, config)
|
||||||
|
|
||||||
|
if (!this._element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._element.hasAttribute('aria-pressed')) {
|
||||||
|
this._element.setAttribute('aria-pressed', String(this.isActive))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The button usually sits inside a clickable row or card; the click must
|
||||||
|
// not reach it.
|
||||||
|
EventHandler.on(this._element, EVENT_CLICK, (event: Event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
this.toggle()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
static get Default(): ComponentConfig {
|
||||||
|
return Default
|
||||||
|
}
|
||||||
|
|
||||||
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
|
return DefaultType
|
||||||
|
}
|
||||||
|
|
||||||
|
static get NAME(): string {
|
||||||
|
return NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
get isActive(): boolean {
|
||||||
|
return this._element.classList.contains(CLASS_NAME_ACTIVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public
|
||||||
|
toggle(force?: boolean): void {
|
||||||
|
const active = this._element.classList.toggle(CLASS_NAME_ACTIVE, force)
|
||||||
|
this._element.setAttribute('aria-pressed', String(active))
|
||||||
|
|
||||||
|
EventHandler.trigger(this._element, EVENT_CHANGE, { active })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data API implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
// js-docs-start switch-icon-init
|
||||||
|
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
||||||
|
SwitchIcon.getOrCreateInstance(element)
|
||||||
|
}
|
||||||
// js-docs-end switch-icon-init
|
// js-docs-end switch-icon-init
|
||||||
|
|
||||||
|
export default SwitchIcon
|
||||||
@@ -15,3 +15,7 @@ export * from './src/bootstrap'
|
|||||||
|
|
||||||
// Tabler's own components
|
// Tabler's own components
|
||||||
export { default as Autosize } from './src/autosize'
|
export { default as Autosize } from './src/autosize'
|
||||||
|
export { default as CountUp } from './src/countup'
|
||||||
|
export { default as InputMask } from './src/input-mask'
|
||||||
|
export { default as Sortable } from './src/sortable'
|
||||||
|
export { default as SwitchIcon } from './src/switch-icon'
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { CountUp as CountUpPlugin } from 'countup.js'
|
||||||
|
import { clearFixture, getFixture } from '../helpers/fixture'
|
||||||
|
import CountUp from '../../src/countup'
|
||||||
|
|
||||||
|
vi.mock('countup.js', () => ({
|
||||||
|
CountUp: vi.fn(function (this: Record<string, unknown>) {
|
||||||
|
this.error = ''
|
||||||
|
this.start = vi.fn()
|
||||||
|
this.reset = vi.fn()
|
||||||
|
this.update = vi.fn()
|
||||||
|
this.pauseResume = vi.fn()
|
||||||
|
this.onDestroy = vi.fn()
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const plugin = vi.mocked(CountUpPlugin)
|
||||||
|
|
||||||
|
const lastInstance = (): Record<string, ReturnType<typeof vi.fn>> => plugin.mock.instances[plugin.mock.instances.length - 1] as unknown as Record<string, ReturnType<typeof vi.fn>>
|
||||||
|
|
||||||
|
describe('CountUp', () => {
|
||||||
|
let fixtureEl: HTMLElement
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
fixtureEl = getFixture()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
fixtureEl.innerHTML = '<h1 data-countup>30000</h1>'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearFixture()
|
||||||
|
})
|
||||||
|
|
||||||
|
const element = (): HTMLElement => fixtureEl.querySelector('h1')!
|
||||||
|
|
||||||
|
describe('NAME', () => {
|
||||||
|
it('should return plugin name', () => {
|
||||||
|
expect(CountUp.NAME).toBe('countup')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create the plugin with the parsed number and autoAnimate on', () => {
|
||||||
|
new CountUp(element())
|
||||||
|
|
||||||
|
expect(plugin).toHaveBeenCalledTimes(1)
|
||||||
|
expect(plugin.mock.calls[0][0]).toBe(element())
|
||||||
|
expect(plugin.mock.calls[0][1]).toBe(30000)
|
||||||
|
expect(plugin.mock.calls[0][2]).toMatchObject({ autoAnimate: true })
|
||||||
|
expect(lastInstance().start).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should read options from the data-countup JSON', () => {
|
||||||
|
fixtureEl.innerHTML = '<h1 data-countup=\'{"duration":4,"suffix":"%"}\'>300</h1>'
|
||||||
|
|
||||||
|
const instance = new CountUp(element())
|
||||||
|
|
||||||
|
expect(plugin.mock.calls[0][2]).toMatchObject({ duration: 4, suffix: '%', autoAnimate: true })
|
||||||
|
expect(instance._config.duration).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should let the config object win over the attribute', () => {
|
||||||
|
fixtureEl.innerHTML = '<h1 data-countup=\'{"duration":4}\'>300</h1>'
|
||||||
|
|
||||||
|
new CountUp(element(), { duration: 6 })
|
||||||
|
|
||||||
|
expect(plugin.mock.calls[0][2]).toMatchObject({ duration: 6 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore invalid JSON', () => {
|
||||||
|
fixtureEl.innerHTML = '<h1 data-countup="{oops">300</h1>'
|
||||||
|
|
||||||
|
expect(() => new CountUp(element())).not.toThrow()
|
||||||
|
expect(plugin).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse formatted numbers', () => {
|
||||||
|
fixtureEl.innerHTML = '<h1 data-countup>$1,234.5</h1>'
|
||||||
|
|
||||||
|
new CountUp(element())
|
||||||
|
|
||||||
|
expect(plugin.mock.calls[0][1]).toBe(1234.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create the plugin when the text is not a number', () => {
|
||||||
|
fixtureEl.innerHTML = '<h1 data-countup>soon</h1>'
|
||||||
|
|
||||||
|
new CountUp(element())
|
||||||
|
|
||||||
|
expect(plugin).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should start by hand when autoAnimate is off', () => {
|
||||||
|
new CountUp(element(), { autoAnimate: false })
|
||||||
|
|
||||||
|
expect(lastInstance().start).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('public API', () => {
|
||||||
|
it('should forward start, reset, update and pauseResume to the plugin', () => {
|
||||||
|
const instance = new CountUp(element())
|
||||||
|
|
||||||
|
instance.start()
|
||||||
|
instance.reset()
|
||||||
|
instance.update(42)
|
||||||
|
instance.pauseResume()
|
||||||
|
|
||||||
|
expect(lastInstance().start).toHaveBeenCalledTimes(1)
|
||||||
|
expect(lastInstance().reset).toHaveBeenCalledTimes(1)
|
||||||
|
expect(lastInstance().update).toHaveBeenCalledWith(42)
|
||||||
|
expect(lastInstance().pauseResume).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should destroy the plugin and remove the instance on dispose', () => {
|
||||||
|
const instance = new CountUp(element())
|
||||||
|
const el = element()
|
||||||
|
const destroy = lastInstance().onDestroy
|
||||||
|
|
||||||
|
instance.dispose()
|
||||||
|
|
||||||
|
expect(destroy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(CountUp.getInstance(el)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getOrCreateInstance', () => {
|
||||||
|
it('should reuse the instance', () => {
|
||||||
|
const first = CountUp.getOrCreateInstance(element())
|
||||||
|
const second = CountUp.getOrCreateInstance(element())
|
||||||
|
|
||||||
|
expect(second).toBe(first)
|
||||||
|
expect(plugin).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { clearFixture, getFixture } from '../helpers/fixture'
|
||||||
|
import InputMask from '../../src/input-mask'
|
||||||
|
|
||||||
|
describe('InputMask', () => {
|
||||||
|
let fixtureEl: HTMLElement
|
||||||
|
let plugin: ReturnType<typeof vi.fn>
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
fixtureEl = getFixture()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
plugin = vi.fn(function (this: Record<string, unknown>) {
|
||||||
|
this.updateValue = vi.fn()
|
||||||
|
this.destroy = vi.fn()
|
||||||
|
})
|
||||||
|
window.IMask = plugin as unknown as Window['IMask']
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-mask="00/00/0000" data-mask-visible="true">'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearFixture()
|
||||||
|
delete window.IMask
|
||||||
|
})
|
||||||
|
|
||||||
|
const input = (): HTMLInputElement => fixtureEl.querySelector('input')!
|
||||||
|
const lastInstance = (): Record<string, ReturnType<typeof vi.fn>> => plugin.mock.instances[plugin.mock.instances.length - 1] as Record<string, ReturnType<typeof vi.fn>>
|
||||||
|
|
||||||
|
describe('NAME', () => {
|
||||||
|
it('should return plugin name', () => {
|
||||||
|
expect(InputMask.NAME).toBe('input-mask')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create the mask from data-mask and data-mask-visible', () => {
|
||||||
|
const instance = new InputMask(input())
|
||||||
|
|
||||||
|
expect(plugin).toHaveBeenCalledWith(input(), { mask: '00/00/0000', lazy: false })
|
||||||
|
expect(instance.mask).toBe(plugin.mock.instances[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should default to a lazy mask', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-mask="(00) 0000-0000">'
|
||||||
|
|
||||||
|
new InputMask(input())
|
||||||
|
|
||||||
|
expect(plugin).toHaveBeenCalledWith(input(), { mask: '(00) 0000-0000', lazy: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should let the config object win over the attributes', () => {
|
||||||
|
new InputMask(input(), { mask: '0000', lazy: true })
|
||||||
|
|
||||||
|
expect(plugin).toHaveBeenCalledWith(input(), { mask: '0000', lazy: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stay inert without a mask', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-mask>'
|
||||||
|
|
||||||
|
const instance = new InputMask(input())
|
||||||
|
|
||||||
|
expect(plugin).not.toHaveBeenCalled()
|
||||||
|
expect(instance.mask).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stay inert when the plugin is not loaded', () => {
|
||||||
|
delete window.IMask
|
||||||
|
|
||||||
|
expect(() => new InputMask(input())).not.toThrow()
|
||||||
|
expect(InputMask.getInstance(input())).toBeInstanceOf(InputMask)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('public API', () => {
|
||||||
|
it('should forward update to the mask', () => {
|
||||||
|
const instance = new InputMask(input())
|
||||||
|
instance.update()
|
||||||
|
|
||||||
|
expect(lastInstance().updateValue).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should destroy the mask and remove the instance on dispose', () => {
|
||||||
|
const instance = new InputMask(input())
|
||||||
|
const element = input()
|
||||||
|
const destroy = lastInstance().destroy
|
||||||
|
|
||||||
|
instance.dispose()
|
||||||
|
|
||||||
|
expect(destroy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(InputMask.getInstance(element)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getOrCreateInstance', () => {
|
||||||
|
it('should reuse the instance', () => {
|
||||||
|
const first = InputMask.getOrCreateInstance(input())
|
||||||
|
const second = InputMask.getOrCreateInstance(input())
|
||||||
|
|
||||||
|
expect(second).toBe(first)
|
||||||
|
expect(plugin).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { clearFixture, getFixture } from '../helpers/fixture'
|
||||||
|
import Sortable from '../../src/sortable'
|
||||||
|
|
||||||
|
describe('Sortable', () => {
|
||||||
|
let fixtureEl: HTMLElement
|
||||||
|
let plugin: ReturnType<typeof vi.fn>
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
fixtureEl = getFixture()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
plugin = vi.fn(function (this: Record<string, unknown>) {
|
||||||
|
this.toArray = vi.fn(() => ['a', 'b'])
|
||||||
|
this.sort = vi.fn()
|
||||||
|
this.option = vi.fn()
|
||||||
|
this.destroy = vi.fn()
|
||||||
|
})
|
||||||
|
window.Sortable = plugin as unknown as Window['Sortable']
|
||||||
|
fixtureEl.innerHTML = '<ul data-sortable=\'{"animation":150}\'><li>a</li><li>b</li></ul>'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearFixture()
|
||||||
|
delete window.Sortable
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = (): HTMLElement => fixtureEl.querySelector('ul')!
|
||||||
|
const lastInstance = (): Record<string, ReturnType<typeof vi.fn>> => plugin.mock.instances[plugin.mock.instances.length - 1] as Record<string, ReturnType<typeof vi.fn>>
|
||||||
|
|
||||||
|
describe('NAME', () => {
|
||||||
|
it('should return plugin name', () => {
|
||||||
|
expect(Sortable.NAME).toBe('sortable')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create the plugin with options from data-sortable', () => {
|
||||||
|
const instance = new Sortable(list())
|
||||||
|
|
||||||
|
expect(plugin).toHaveBeenCalledTimes(1)
|
||||||
|
expect(plugin.mock.calls[0][0]).toBe(list())
|
||||||
|
expect(plugin.mock.calls[0][1]).toMatchObject({ animation: 150 })
|
||||||
|
expect(instance.sortable).toBe(plugin.mock.instances[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should let the config object win over the attribute', () => {
|
||||||
|
new Sortable(list(), { animation: 0, group: 'shared' })
|
||||||
|
|
||||||
|
expect(plugin.mock.calls[0][1]).toMatchObject({ animation: 0, group: 'shared' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore invalid JSON', () => {
|
||||||
|
fixtureEl.innerHTML = '<ul data-sortable="{oops"><li>a</li></ul>'
|
||||||
|
|
||||||
|
expect(() => new Sortable(list())).not.toThrow()
|
||||||
|
expect(plugin).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stay inert when the plugin is not loaded', () => {
|
||||||
|
delete window.Sortable
|
||||||
|
|
||||||
|
expect(() => new Sortable(list())).not.toThrow()
|
||||||
|
expect(Sortable.getInstance(list())).toBeInstanceOf(Sortable)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('public API', () => {
|
||||||
|
it('should forward toArray and sort to the plugin', () => {
|
||||||
|
const instance = new Sortable(list())
|
||||||
|
|
||||||
|
expect(instance.toArray()).toEqual(['a', 'b'])
|
||||||
|
|
||||||
|
instance.sort(['b', 'a'], true)
|
||||||
|
expect(lastInstance().sort).toHaveBeenCalledWith(['b', 'a'], true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should destroy the plugin and remove the instance on dispose', () => {
|
||||||
|
const instance = new Sortable(list())
|
||||||
|
const element = list()
|
||||||
|
const destroy = lastInstance().destroy
|
||||||
|
|
||||||
|
instance.dispose()
|
||||||
|
|
||||||
|
expect(destroy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(Sortable.getInstance(element)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getOrCreateInstance', () => {
|
||||||
|
it('should reuse the instance', () => {
|
||||||
|
const first = Sortable.getOrCreateInstance(list())
|
||||||
|
const second = Sortable.getOrCreateInstance(list())
|
||||||
|
|
||||||
|
expect(second).toBe(first)
|
||||||
|
expect(plugin).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { clearFixture, getFixture } from '../helpers/fixture'
|
||||||
|
import SwitchIcon from '../../src/switch-icon'
|
||||||
|
|
||||||
|
describe('SwitchIcon', () => {
|
||||||
|
let fixtureEl: HTMLElement
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
fixtureEl = getFixture()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixtureEl.innerHTML = '<div id="parent"><button type="button" class="switch-icon" data-bs-toggle="switch-icon" aria-pressed="false"></button></div>'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearFixture()
|
||||||
|
})
|
||||||
|
|
||||||
|
const button = (): HTMLButtonElement => fixtureEl.querySelector('button')!
|
||||||
|
|
||||||
|
describe('NAME', () => {
|
||||||
|
it('should return plugin name', () => {
|
||||||
|
expect(SwitchIcon.NAME).toBe('switch-icon')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should set aria-pressed from the active class when missing', () => {
|
||||||
|
fixtureEl.innerHTML = '<button type="button" class="switch-icon active" data-bs-toggle="switch-icon"></button>'
|
||||||
|
|
||||||
|
new SwitchIcon(button())
|
||||||
|
|
||||||
|
expect(button().getAttribute('aria-pressed')).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep an existing aria-pressed value', () => {
|
||||||
|
new SwitchIcon(button())
|
||||||
|
|
||||||
|
expect(button().getAttribute('aria-pressed')).toBe('false')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('toggle', () => {
|
||||||
|
it('should toggle the active class and aria-pressed', () => {
|
||||||
|
const instance = new SwitchIcon(button())
|
||||||
|
|
||||||
|
instance.toggle()
|
||||||
|
expect(button().classList.contains('active')).toBe(true)
|
||||||
|
expect(button().getAttribute('aria-pressed')).toBe('true')
|
||||||
|
expect(instance.isActive).toBe(true)
|
||||||
|
|
||||||
|
instance.toggle()
|
||||||
|
expect(button().classList.contains('active')).toBe(false)
|
||||||
|
expect(button().getAttribute('aria-pressed')).toBe('false')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should force the state with an argument', () => {
|
||||||
|
const instance = new SwitchIcon(button())
|
||||||
|
|
||||||
|
instance.toggle(true)
|
||||||
|
instance.toggle(true)
|
||||||
|
|
||||||
|
expect(instance.isActive).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should trigger change.bs.switch-icon with the new state', () => {
|
||||||
|
const instance = new SwitchIcon(button())
|
||||||
|
const spy = vi.fn()
|
||||||
|
button().addEventListener('change.bs.switch-icon', spy)
|
||||||
|
|
||||||
|
instance.toggle()
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
expect((spy.mock.calls[0][0] as Event & { active: boolean }).active).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('click', () => {
|
||||||
|
it('should toggle on click and stop propagation', () => {
|
||||||
|
new SwitchIcon(button())
|
||||||
|
const parentSpy = vi.fn()
|
||||||
|
fixtureEl.querySelector('#parent')!.addEventListener('click', parentSpy)
|
||||||
|
|
||||||
|
button().click()
|
||||||
|
|
||||||
|
expect(button().classList.contains('active')).toBe(true)
|
||||||
|
expect(parentSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stop toggling after dispose', () => {
|
||||||
|
const instance = new SwitchIcon(button())
|
||||||
|
instance.dispose()
|
||||||
|
|
||||||
|
button().click()
|
||||||
|
|
||||||
|
expect(button().classList.contains('active')).toBe(false)
|
||||||
|
expect(SwitchIcon.getInstance(button())).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -101,13 +101,37 @@ You can also add a fancy animation to add variety to your button. See demo below
|
|||||||
|
|
||||||
## JavaScript
|
## JavaScript
|
||||||
|
|
||||||
Tabler toggles the `active` class on every element with `data-bs-toggle="switch-icon"` on click. This is the code that runs:
|
Tabler wraps the button in a `SwitchIcon` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-bs-toggle="switch-icon"`, and a click toggles the `active` class and `aria-pressed`. This is the code that runs:
|
||||||
|
|
||||||
<CodeDocs name="switch-icon-init" file="core/js/src/switch-icon.ts" />
|
<CodeDocs name="switch-icon-init" file="core/js/src/switch-icon.ts" />
|
||||||
|
|
||||||
|
Create the component yourself for a button added later, or change the state from code:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const favorite = tabler.SwitchIcon.getOrCreateInstance(document.getElementById('favorite'));
|
||||||
|
favorite.toggle(); // flip
|
||||||
|
favorite.toggle(true); // force on
|
||||||
|
```
|
||||||
|
|
||||||
|
Every change fires `change.bs.switch-icon` on the button, with the new state in `event.active`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
document.getElementById('favorite').addEventListener('change.bs.switch-icon', (event) => {
|
||||||
|
console.log(event.active ? 'added to favorites' : 'removed from favorites');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `toggle(force)` | Flips the state, or sets it when `force` is `true` or `false`. Updates `aria-pressed` and fires `change.bs.switch-icon`. |
|
||||||
|
| `isActive` | Getter. `true` when the button has the `active` class. |
|
||||||
|
| `dispose()` | Removes the click handler and the component from the element. |
|
||||||
|
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
|
||||||
|
| `getOrCreateInstance(element)` | Static. Returns the component for the element and creates it when needed. |
|
||||||
|
|
||||||
## Accessibility
|
## Accessibility
|
||||||
|
|
||||||
- A switch icon is a toggle button. Give it `type="button"`, an `aria-label` saying what it toggles, and `aria-pressed` for the state - Tabler keeps `aria-pressed` in sync from the first click, but the initial value has to be in your markup.
|
- A switch icon is a toggle button. Give it `type="button"`, an `aria-label` saying what it toggles, and `aria-pressed` for the state. Tabler keeps `aria-pressed` in sync, and fills it in from the `active` class when the markup leaves it out.
|
||||||
- The two icons are decoration. The state lives in `aria-pressed`, not in which icon is visible.
|
- The two icons are decoration. The state lives in `aria-pressed`, not in which icon is visible.
|
||||||
- Colour alone should not carry the state. A filled red heart and an outlined gray one differ in shape too, which is what makes them readable.
|
- Colour alone should not carry the state. A filled red heart and an outlined gray one differ in shape too, which is what makes them readable.
|
||||||
- The animations run on every toggle. Disable them under `prefers-reduced-motion` if you add your own.
|
- The animations run on every toggle. Disable them under `prefers-reduced-motion` if you add your own.
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Countup
|
title: Countup
|
||||||
summary: A countup animates a number from zero to its final value when it scrolls into view.
|
summary: A countup animates a number from zero to its final value when it scrolls into view.
|
||||||
docs-libs: [countup]
|
|
||||||
description: Animate numbers with the countup component, for statistics on dashboards and landing pages.
|
description: Animate numbers with the countup component, for statistics on dashboards and landing pages.
|
||||||
related: [/ui/plugins/chart]
|
related: [/ui/plugins/chart]
|
||||||
---
|
---
|
||||||
import Example from '@components/Example.astro';
|
import Example from '@components/Example.astro';
|
||||||
import CodeDocs from '@components/CodeDocs.astro';
|
import CodeDocs from '@components/CodeDocs.astro';
|
||||||
import TabsPackage from '@components/TabsPackage.astro';
|
|
||||||
import { Code } from 'astro:components';
|
|
||||||
import { site } from '@shared/lib/site.ts';
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -23,23 +19,7 @@ Write the final number as the text of the element and add `data-countup`. Tabler
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install countup.js with npm:
|
The [countUp.js](https://inorganik.github.io/countUp.js/) library is bundled with `tabler.js`, so there is nothing extra to install or load. For options beyond the ones below, see its website.
|
||||||
|
|
||||||
<TabsPackage name="countup.js" />
|
|
||||||
|
|
||||||
Or include it from a CDN:
|
|
||||||
|
|
||||||
<Code lang="html" code={`<script src="${site.cdnUrl}/dist/libs/countup.js/dist/countUp.umd.js"></script>`} />
|
|
||||||
|
|
||||||
Tabler reads the library from the `countUp` global, so load the UMD build above before `tabler.js`. If you install with npm and bundle it yourself, assign it first:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import * as countUp from 'countup.js';
|
|
||||||
|
|
||||||
window.countUp = countUp;
|
|
||||||
```
|
|
||||||
|
|
||||||
For options beyond the ones below, see the [countUp.js website](https://inorganik.github.io/countUp.js/).
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -129,10 +109,34 @@ Grouping is on by default, so thousands get a separator. Set `"useGrouping": fal
|
|||||||
|
|
||||||
## JavaScript
|
## JavaScript
|
||||||
|
|
||||||
Tabler automatically initializes all elements with `data-countup` on page load. This is the code that runs:
|
Tabler wraps the library in a `CountUp` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-countup`. This is the code that runs:
|
||||||
|
|
||||||
<CodeDocs name="countup-init" file="core/js/src/countup.ts" />
|
<CodeDocs name="countup-init" file="core/js/src/countup.ts" />
|
||||||
|
|
||||||
|
Create the component yourself for an element added later. Options go in the `data-countup` attribute, in the config object, or both; the object wins.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const countup = new tabler.CountUp(document.getElementById('total'), { duration: 4 });
|
||||||
|
```
|
||||||
|
|
||||||
|
By default the animation starts when the element scrolls into view. Set `autoAnimate` to `false` to start it right away, or to control it yourself:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const countup = tabler.CountUp.getOrCreateInstance(element, { autoAnimate: false });
|
||||||
|
countup.reset();
|
||||||
|
countup.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `start()` | Starts the animation. |
|
||||||
|
| `reset()` | Sets the number back to the starting value, so the animation can run again. |
|
||||||
|
| `update(value)` | Animates to a new final value. |
|
||||||
|
| `pauseResume()` | Pauses a running animation, or resumes a paused one. |
|
||||||
|
| `dispose()` | Stops the animation and removes the component from the element. |
|
||||||
|
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
|
||||||
|
| `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. |
|
||||||
|
|
||||||
## Accessibility
|
## Accessibility
|
||||||
|
|
||||||
- Write the final number in the HTML, as the examples do. That is what a search engine indexes, what shows without JavaScript, and what a screen reader reads if it reaches the element before the animation starts.
|
- Write the final number in the HTML, as the examples do. That is what a search engine indexes, what shows without JavaScript, and what a screen reader reads if it reaches the element before the animation starts.
|
||||||
|
|||||||
@@ -60,10 +60,33 @@ If you need more examples of input masks, you can find them in the [IMask docume
|
|||||||
|
|
||||||
## JavaScript
|
## JavaScript
|
||||||
|
|
||||||
Tabler automatically initializes all elements with `data-mask` on page load. This is the code that runs:
|
Tabler wraps the plugin in an `InputMask` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-mask`. This is the code that runs:
|
||||||
|
|
||||||
<CodeDocs name="input-mask-init" file="core/js/src/input-mask.ts" />
|
<CodeDocs name="input-mask-init" file="core/js/src/input-mask.ts" />
|
||||||
|
|
||||||
|
Create the component yourself for a field added later. The pattern comes from `data-mask`, or from the config object, which wins:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const phone = new tabler.InputMask(document.getElementById('phone'), { mask: '(00) 0000-0000' });
|
||||||
|
```
|
||||||
|
|
||||||
|
`data-mask-visible="true"` shows the pattern before the user types. In the config object that is `lazy: false`.
|
||||||
|
|
||||||
|
The `mask` getter returns the IMask instance, so everything from the [IMask API](https://imask.js.org/guide.html#api) is available:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const phone = tabler.InputMask.getOrCreateInstance(document.getElementById('phone'));
|
||||||
|
console.log(phone.mask.unmaskedValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `update()` | Re-applies the mask after the value was changed from code. |
|
||||||
|
| `mask` | Getter. The IMask instance, or `null` when the library is not loaded. |
|
||||||
|
| `dispose()` | Removes the mask and the component from the element. |
|
||||||
|
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
|
||||||
|
| `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. |
|
||||||
|
|
||||||
## Accessibility
|
## Accessibility
|
||||||
|
|
||||||
- A mask changes what the field accepts as the user types, which is easy to lose track of without sight. Say the expected format in a hint tied to the field with `aria-describedby`.
|
- A mask changes what the field accepts as the user types, which is easy to lose track of without sight. Say the expected format in a hint tied to the field with `aria-describedby`.
|
||||||
|
|||||||
Reference in new issue
Block a user