mirror of
https://github.com/tabler/tabler.git
synced 2026-01-25 12:26:30 +00:00
Compare commits
12 Commits
changeset-
...
dev-js-lib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
224de12414 | ||
|
|
da69e147fc | ||
|
|
087794dc49 | ||
|
|
d0e92a3fe0 | ||
|
|
1d4c1fa016 | ||
|
|
276fe61996 | ||
|
|
4abc069959 | ||
|
|
01b8208227 | ||
|
|
d3a358fec9 | ||
|
|
4ef9dbde51 | ||
|
|
ebcfa18060 | ||
|
|
96168a826c |
42
.github/workflows/build.yml
vendored
Normal file
42
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
|
||||
env:
|
||||
NODE: 22
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: node --version
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Test build
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
@@ -38,5 +38,5 @@ jobs:
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
- name: Run tests
|
||||
run: pnpm run test
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -41,4 +41,9 @@ sri.json
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
.tsbuildinfo
|
||||
.tsbuildinfo
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
**/coverage/
|
||||
*.lcov
|
||||
@@ -1,10 +1,107 @@
|
||||
// Autosize plugin
|
||||
const autosizeElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-bs-toggle="autosize"]')
|
||||
import autosize from 'autosize'
|
||||
|
||||
if (autosizeElements.length) {
|
||||
autosizeElements.forEach(function (element: HTMLElement) {
|
||||
if (window.autosize) {
|
||||
window.autosize(element)
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler autosize.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'autosize'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const SELECTOR_DATA_AUTOSIZE = '[data-bs-autosize]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class Autosize {
|
||||
private element: HTMLElement | HTMLTextAreaElement
|
||||
private initialized: boolean = false
|
||||
|
||||
constructor(element: HTMLElement | HTMLTextAreaElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize autosize on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
autosize(this.element)
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update autosize (useful when content changes programmatically)
|
||||
*/
|
||||
update(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
autosize.update(this.element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy autosize instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
autosize.destroy(this.element)
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement | HTMLTextAreaElement): Autosize | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement | HTMLTextAreaElement): Autosize {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement | HTMLTextAreaElement, Autosize>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const autosizeTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_AUTOSIZE))
|
||||
autosizeTriggerList.map(function (autosizeTriggerEl: HTMLElement) {
|
||||
const instance = Autosize.getOrCreateInstance(autosizeTriggerEl)
|
||||
elementMap.set(autosizeTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default Autosize
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export * as Popper from '@popperjs/core'
|
||||
|
||||
// Export all Bootstrap components directly for consistent usage
|
||||
export { Alert, Button, Carousel, Collapse, Dropdown, Modal, Offcanvas, Popover, ScrollSpy, Tab, Toast, Tooltip } from 'bootstrap'
|
||||
|
||||
// Re-export everything as namespace for backward compatibility
|
||||
export * as bootstrap from 'bootstrap'
|
||||
2
core/js/src/clipboard.ts
Normal file
2
core/js/src/clipboard.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Backward compatibility alias (old file name).
|
||||
export { default } from './copy'
|
||||
26
core/js/src/collapse.ts
Normal file
26
core/js/src/collapse.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Collapse } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core collapse
|
||||
*/
|
||||
const collapseTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="collapse"]'))
|
||||
collapseTriggerList.map(function (collapseTriggerEl: HTMLElement) {
|
||||
const target = collapseTriggerEl.getAttribute('data-bs-target') || collapseTriggerEl.getAttribute('href')
|
||||
if (target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const collapseEl = document.querySelector<HTMLElement>(target)
|
||||
if (collapseEl === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const collapse = new Collapse(collapseEl, {
|
||||
toggle: false,
|
||||
})
|
||||
|
||||
collapseTriggerEl.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
collapse.toggle()
|
||||
})
|
||||
})
|
||||
334
core/js/src/copy.ts
Normal file
334
core/js/src/copy.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler copy.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { Tooltip } from 'bootstrap'
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
const NAME = 'copy'
|
||||
const DATA_KEY = `bs.${NAME}`
|
||||
|
||||
type CopyConfig = {
|
||||
timeout: number
|
||||
reset: boolean
|
||||
extend: boolean
|
||||
recopy: boolean
|
||||
feedback: 'swap' | 'tooltip'
|
||||
tooltip: string
|
||||
tooltipPlacement: string
|
||||
}
|
||||
|
||||
const Default: CopyConfig = {
|
||||
timeout: 1500,
|
||||
reset: true,
|
||||
extend: true,
|
||||
recopy: true,
|
||||
feedback: 'swap',
|
||||
tooltip: 'Copied!',
|
||||
tooltipPlacement: 'top',
|
||||
}
|
||||
|
||||
const Selector = {
|
||||
toggle: '[data-bs-toggle="copy"]',
|
||||
default: '[data-bs-copy-default]',
|
||||
success: '[data-bs-copy-success]',
|
||||
} as const
|
||||
|
||||
const Event = {
|
||||
COPY: `copy.bs.${NAME}`,
|
||||
COPIED: `copied.bs.${NAME}`,
|
||||
COPYFAIL: `copyfail.bs.${NAME}`,
|
||||
RESET: `reset.bs.${NAME}`,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
class Copy {
|
||||
static NAME = NAME
|
||||
static DATA_KEY = DATA_KEY
|
||||
static Default = Default
|
||||
static Selector = Selector
|
||||
static Event = Event
|
||||
|
||||
private _element: HTMLElement | null
|
||||
private _timer: number | null = null
|
||||
private _isCopied: boolean = false
|
||||
private _config: CopyConfig
|
||||
private _tooltipInstance: any = null
|
||||
|
||||
constructor(element: HTMLElement, config: Partial<CopyConfig> = {}) {
|
||||
this._element = element
|
||||
this._config = { ...Default, ...this._getConfig(), ...config }
|
||||
}
|
||||
|
||||
static getInstance(element?: HTMLElement | null): Copy | null {
|
||||
return (element as any)?.[DATA_KEY] ?? null
|
||||
}
|
||||
|
||||
static getOrCreateInstance(element?: HTMLElement | null, config?: Partial<CopyConfig>): Copy | null {
|
||||
if (!element) return null
|
||||
;(element as any)[DATA_KEY] ??= new Copy(element, config)
|
||||
return (element as any)[DATA_KEY]
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._timer) clearTimeout(this._timer)
|
||||
this._timer = null
|
||||
this._disposeTooltip()
|
||||
|
||||
if (this._element) {
|
||||
delete (this._element as any)[DATA_KEY]
|
||||
}
|
||||
|
||||
this._element = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API: manual reset
|
||||
*/
|
||||
reset(): void {
|
||||
if (!this._element) return
|
||||
if (!this._isCopied) return
|
||||
|
||||
if (this._timer) clearTimeout(this._timer)
|
||||
this._timer = null
|
||||
|
||||
this._isCopied = false
|
||||
this._applyFeedback(false)
|
||||
this._trigger(Event.RESET, { manual: true })
|
||||
}
|
||||
|
||||
async copy(): Promise<boolean> {
|
||||
if (!this._element) return false
|
||||
|
||||
// Already "Copied" and recopy disabled — just extend timer or noop
|
||||
if (this._isCopied && !this._config.recopy) {
|
||||
if (this._config.reset && this._config.extend) this._armResetTimer()
|
||||
return true
|
||||
}
|
||||
|
||||
const text = this._getText()
|
||||
const target = this._getTarget()
|
||||
|
||||
const before = this._trigger(Event.COPY, { text, target })
|
||||
if (before.defaultPrevented) return false
|
||||
|
||||
if (text == null) {
|
||||
this._trigger(Event.COPYFAIL, { text: null, target, reason: 'no-text' })
|
||||
return false
|
||||
}
|
||||
|
||||
const ok = await this._writeText(text)
|
||||
if (!ok) {
|
||||
this._trigger(Event.COPYFAIL, { text, target, reason: 'clipboard-failed' })
|
||||
return false
|
||||
}
|
||||
|
||||
this._isCopied = true
|
||||
this._applyFeedback(true)
|
||||
this._trigger(Event.COPIED, { text, target })
|
||||
|
||||
if (this._config.reset) this._armResetTimer()
|
||||
return true
|
||||
}
|
||||
|
||||
private _getConfig(): Partial<CopyConfig> {
|
||||
if (!this._element) return {}
|
||||
|
||||
const d = this._element.dataset
|
||||
const num = (v: string | undefined) => {
|
||||
if (v == null || v === '') return undefined
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}
|
||||
const bool = (v: string | undefined) => (v != null ? v !== 'false' : undefined)
|
||||
const str = (v: string | undefined) => (v != null && v !== '' ? v : undefined)
|
||||
|
||||
const config: Partial<CopyConfig> = {}
|
||||
|
||||
const timeout = num(d.bsCopyTimeout)
|
||||
const reset = bool(d.bsCopyReset)
|
||||
const extend = bool(d.bsCopyExtend)
|
||||
const recopy = bool(d.bsCopyRecopy)
|
||||
const feedback = str(d.bsCopyFeedback)
|
||||
const tooltip = str(d.bsCopyTooltip)
|
||||
const tooltipPlacement = str(d.bsCopyTooltipPlacement)
|
||||
|
||||
if (timeout !== undefined) config.timeout = timeout
|
||||
if (reset !== undefined) config.reset = reset
|
||||
if (extend !== undefined) config.extend = extend
|
||||
if (recopy !== undefined) config.recopy = recopy
|
||||
if (feedback === 'swap' || feedback === 'tooltip') config.feedback = feedback
|
||||
if (tooltip !== undefined) config.tooltip = tooltip
|
||||
if (tooltipPlacement !== undefined) config.tooltipPlacement = tooltipPlacement
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
private _getTarget(): Element | null {
|
||||
if (!this._element) return null
|
||||
const selector = this._element.dataset.bsCopyTarget
|
||||
return selector ? document.querySelector(selector) : null
|
||||
}
|
||||
|
||||
private _getText(): string | null {
|
||||
if (!this._element) return null
|
||||
|
||||
const d = this._element.dataset
|
||||
if (d.bsCopyText) return d.bsCopyText
|
||||
|
||||
const target = this._getTarget()
|
||||
if (!target) return null
|
||||
|
||||
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return target.value
|
||||
return target.textContent?.trim() ?? ''
|
||||
}
|
||||
|
||||
private async _writeText(text: string): Promise<boolean> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch {
|
||||
// ignore and fallback
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.setAttribute('readonly', '')
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
const ok = document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
return ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private _armResetTimer(): void {
|
||||
if (!this._element) return
|
||||
|
||||
if (!this._config.extend && this._timer) return
|
||||
if (this._timer) clearTimeout(this._timer)
|
||||
this._timer = window.setTimeout(() => {
|
||||
this._isCopied = false
|
||||
this._applyFeedback(false)
|
||||
this._trigger(Event.RESET, { manual: false })
|
||||
}, this._config.timeout)
|
||||
}
|
||||
|
||||
private _applyFeedback(isCopied: boolean): void {
|
||||
if (!this._element) return
|
||||
|
||||
const mode = this._config.feedback
|
||||
|
||||
// Tooltip mode: use Bootstrap Tooltip (manual trigger)
|
||||
if (mode === 'tooltip') {
|
||||
if (isCopied) this._showTooltip()
|
||||
else this._hideTooltip()
|
||||
return
|
||||
}
|
||||
|
||||
this._swapText(isCopied)
|
||||
}
|
||||
|
||||
private _swapText(isCopied: boolean): void {
|
||||
if (!this._element) return
|
||||
|
||||
const def = this._element.querySelector<HTMLElement>(Selector.default)
|
||||
const success = this._element.querySelector<HTMLElement>(Selector.success)
|
||||
|
||||
if (def) {
|
||||
def.hidden = isCopied
|
||||
def.classList.toggle('d-none', isCopied)
|
||||
}
|
||||
if (success) {
|
||||
success.hidden = !isCopied
|
||||
success.classList.toggle('d-none', !isCopied)
|
||||
}
|
||||
|
||||
this._element.classList.toggle('is-copied', isCopied)
|
||||
if (isCopied) this._element.setAttribute('aria-label', 'Copied')
|
||||
else this._element.removeAttribute('aria-label')
|
||||
}
|
||||
|
||||
private _showTooltip(): void {
|
||||
if (!this._element) return
|
||||
|
||||
if (!this._tooltipInstance) {
|
||||
this._element.setAttribute('data-bs-title', this._config.tooltip)
|
||||
this._tooltipInstance = Tooltip.getOrCreateInstance(this._element, {
|
||||
trigger: 'manual',
|
||||
placement: this._config.tooltipPlacement,
|
||||
})
|
||||
} else {
|
||||
this._element.setAttribute('data-bs-title', this._config.tooltip)
|
||||
// Bootstrap 5.3+ supports setContent(); use it when available
|
||||
if (typeof this._tooltipInstance.setContent === 'function') {
|
||||
this._tooltipInstance.setContent({ '.tooltip-inner': this._config.tooltip })
|
||||
}
|
||||
}
|
||||
|
||||
this._tooltipInstance.show()
|
||||
}
|
||||
|
||||
private _hideTooltip(): void {
|
||||
this._tooltipInstance?.hide()
|
||||
}
|
||||
|
||||
private _disposeTooltip(): void {
|
||||
if (this._tooltipInstance) {
|
||||
this._tooltipInstance.dispose()
|
||||
this._tooltipInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
private _trigger(type: string, detail: unknown): CustomEvent {
|
||||
if (!this._element) {
|
||||
return new CustomEvent(type, { bubbles: true, cancelable: false, detail })
|
||||
}
|
||||
|
||||
const cancelable = type === Event.COPY
|
||||
const event = new CustomEvent(type, { bubbles: true, cancelable, detail })
|
||||
this._element.dispatchEvent(event)
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data API — click
|
||||
*/
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as Element | null
|
||||
const trigger = target?.closest?.(Selector.toggle) as HTMLElement | null
|
||||
if (!trigger) return
|
||||
e.preventDefault()
|
||||
Copy.getOrCreateInstance(trigger)?.copy()
|
||||
})
|
||||
|
||||
/**
|
||||
* Data API — Enter/Space
|
||||
*/
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const ke = e as KeyboardEvent
|
||||
if (ke.key !== 'Enter' && ke.key !== ' ' && ke.key !== 'Spacebar') return
|
||||
|
||||
const target = ke.target as Element | null
|
||||
const trigger = target?.closest?.(Selector.toggle) as HTMLElement | null
|
||||
if (!trigger) return
|
||||
|
||||
ke.preventDefault()
|
||||
Copy.getOrCreateInstance(trigger)?.copy()
|
||||
})
|
||||
|
||||
export default Copy
|
||||
@@ -1,10 +1,56 @@
|
||||
const countupElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-countup]')
|
||||
import { CountUp } from 'countup.js'
|
||||
|
||||
if (countupElements.length) {
|
||||
countupElements.forEach(function (element: HTMLElement) {
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler countup.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'countup'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const SELECTOR_DATA_COUNTUP = '[data-countup]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class Countup {
|
||||
private element: HTMLElement
|
||||
private countUpInstance: CountUp | null = null
|
||||
private initialized: boolean = false
|
||||
private options: Record<string, any> = {}
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize countup on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse options from data attribute
|
||||
let options: Record<string, any> = {}
|
||||
try {
|
||||
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')!) : {}
|
||||
const dataOptions = this.element.getAttribute('data-countup') ? JSON.parse(this.element.getAttribute('data-countup')!) : {}
|
||||
options = Object.assign(
|
||||
{
|
||||
enableScrollSpy: true,
|
||||
@@ -15,13 +61,79 @@ if (countupElements.length) {
|
||||
// ignore invalid JSON
|
||||
}
|
||||
|
||||
const value = parseInt(element.innerHTML, 10)
|
||||
this.options = options
|
||||
|
||||
if (window.countUp && window.countUp.CountUp) {
|
||||
const countUp = new window.countUp.CountUp(element, value, options)
|
||||
if (!countUp.error) {
|
||||
countUp.start()
|
||||
const value = parseInt(this.element.innerHTML, 10)
|
||||
|
||||
if (!isNaN(value)) {
|
||||
this.countUpInstance = new CountUp(this.element, value, options)
|
||||
if (!this.countUpInstance.error) {
|
||||
this.countUpInstance.start()
|
||||
this.initialized = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update countup (restart animation)
|
||||
*/
|
||||
update(): void {
|
||||
if (!this.initialized || !this.countUpInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = parseInt(this.element.innerHTML, 10)
|
||||
if (!isNaN(value)) {
|
||||
this.countUpInstance = new CountUp(this.element, value, this.options)
|
||||
if (!this.countUpInstance.error) {
|
||||
this.countUpInstance.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy countup instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
// CountUp doesn't have a destroy method, so we just reset state
|
||||
this.countUpInstance = null
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement): Countup | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement): Countup {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement, Countup>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const countupTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_COUNTUP))
|
||||
countupTriggerList.map(function (countupTriggerEl: HTMLElement) {
|
||||
const instance = Countup.getOrCreateInstance(countupTriggerEl)
|
||||
elementMap.set(countupTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default Countup
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dropdown } from './bootstrap'
|
||||
import { Dropdown } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core dropdowns
|
||||
|
||||
15
core/js/src/global.d.ts
vendored
15
core/js/src/global.d.ts
vendored
@@ -10,5 +10,20 @@ interface Window {
|
||||
}
|
||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => any
|
||||
Sortable?: new (element: HTMLElement, options?: any) => any
|
||||
Tabler?: {
|
||||
setTheme: (value: string) => void
|
||||
setPrimary: (value: string) => void
|
||||
setBase: (value: string) => void
|
||||
setFont: (value: string) => void
|
||||
setRadius: (value: string) => void
|
||||
reset: () => void
|
||||
getConfig: () => {
|
||||
theme: string
|
||||
'theme-base': string
|
||||
'theme-font': string
|
||||
'theme-primary': string
|
||||
'theme-radius': string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,153 @@
|
||||
// Input mask plugin
|
||||
import IMask, { type InputMask as IMaskInputMask } from 'imask'
|
||||
|
||||
const maskElementList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-mask]'))
|
||||
maskElementList.map(function (maskEl: HTMLElement) {
|
||||
window.IMask &&
|
||||
new window.IMask(maskEl, {
|
||||
mask: maskEl.dataset.mask,
|
||||
lazy: maskEl.dataset['mask-visible'] === 'true',
|
||||
})
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler input-mask.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'input-mask'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const SELECTOR_DATA_MASK = '[data-mask]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class InputMask {
|
||||
private element: HTMLElement | HTMLInputElement
|
||||
private maskInstance: IMaskInputMask<any> | null = null
|
||||
private initialized: boolean = false
|
||||
private options: Record<string, any> = {}
|
||||
|
||||
constructor(element: HTMLElement | HTMLInputElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize input mask on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
const mask = this.element.getAttribute('data-mask')
|
||||
if (!mask) {
|
||||
return
|
||||
}
|
||||
|
||||
const options: Record<string, any> = {
|
||||
mask: mask,
|
||||
lazy: this.element.getAttribute('data-mask-visible') === 'true',
|
||||
}
|
||||
|
||||
this.options = options
|
||||
|
||||
this.maskInstance = IMask(this.element, options)
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update input mask (useful when mask changes programmatically)
|
||||
*/
|
||||
update(mask?: string, options?: Partial<Record<string, any>>): void {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const newOptions: Record<string, any> = {
|
||||
...this.options,
|
||||
...options,
|
||||
}
|
||||
|
||||
if (mask) {
|
||||
newOptions.mask = mask
|
||||
}
|
||||
|
||||
this.maskInstance.updateOptions(newOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current masked value
|
||||
*/
|
||||
getValue(): string {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.maskInstance.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current unmasked value
|
||||
*/
|
||||
getUnmaskedValue(): string {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.maskInstance.unmaskedValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy input mask instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
this.maskInstance.destroy()
|
||||
this.maskInstance = null
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement | HTMLInputElement): InputMask | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement | HTMLInputElement): InputMask {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement | HTMLInputElement, InputMask>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const inputMaskTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_MASK))
|
||||
inputMaskTriggerList.map(function (inputMaskTriggerEl: HTMLElement) {
|
||||
const instance = InputMask.getOrCreateInstance(inputMaskTriggerEl)
|
||||
elementMap.set(inputMaskTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default InputMask
|
||||
|
||||
24
core/js/src/modal.ts
Normal file
24
core/js/src/modal.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Modal } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core modals
|
||||
*/
|
||||
const modalTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="modal"]'))
|
||||
modalTriggerList.map(function (modalTriggerEl: HTMLElement) {
|
||||
const target = modalTriggerEl.getAttribute('data-bs-target') || modalTriggerEl.getAttribute('href')
|
||||
if (target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const modalEl = document.querySelector<HTMLElement>(target)
|
||||
if (modalEl === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const modal = new Modal(modalEl)
|
||||
|
||||
modalTriggerEl.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
modal.show()
|
||||
})
|
||||
})
|
||||
24
core/js/src/offcanvas.ts
Normal file
24
core/js/src/offcanvas.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Offcanvas } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core offcanvas
|
||||
*/
|
||||
const offcanvasTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="offcanvas"]'))
|
||||
offcanvasTriggerList.map(function (offcanvasTriggerEl: HTMLElement) {
|
||||
const target = offcanvasTriggerEl.getAttribute('data-bs-target') || offcanvasTriggerEl.getAttribute('href')
|
||||
if (target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const offcanvasEl = document.querySelector<HTMLElement>(target)
|
||||
if (offcanvasEl === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const offcanvas = new Offcanvas(offcanvasEl)
|
||||
|
||||
offcanvasTriggerEl.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
offcanvas.show()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Popover } from './bootstrap'
|
||||
import { Popover } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core popovers
|
||||
|
||||
@@ -1,11 +1,147 @@
|
||||
/*
|
||||
Switch icons
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler switch-icon.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
const switchesTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="switch-icon"]'))
|
||||
switchesTriggerList.map(function (switchTriggerEl: HTMLElement) {
|
||||
switchTriggerEl.addEventListener('click', (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
switchTriggerEl.classList.toggle('active')
|
||||
})
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'switch-icon'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const CLASS_NAME_ACTIVE = 'active'
|
||||
|
||||
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="switch-icon"]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class SwitchIcon {
|
||||
private element: HTMLElement
|
||||
private initialized: boolean = false
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize switch-icon on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this._setListeners()
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle active state
|
||||
*/
|
||||
toggle(): void {
|
||||
this.element.classList.toggle(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show (activate) switch-icon
|
||||
*/
|
||||
show(): void {
|
||||
this.element.classList.add(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide (deactivate) switch-icon
|
||||
*/
|
||||
hide(): void {
|
||||
this.element.classList.remove(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if switch-icon is active
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.element.classList.contains(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy switch-icon instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this._removeListeners()
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Private
|
||||
/**
|
||||
* Set event listeners
|
||||
*/
|
||||
private _setListeners(): void {
|
||||
this.element.addEventListener('click', this._handleClick)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event listeners
|
||||
*/
|
||||
private _removeListeners(): void {
|
||||
this.element.removeEventListener('click', this._handleClick)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click event
|
||||
*/
|
||||
private _handleClick = (e: MouseEvent): void => {
|
||||
e.stopPropagation()
|
||||
this.toggle()
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement): SwitchIcon | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement): SwitchIcon {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement, SwitchIcon>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const switchIconTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_TOGGLE))
|
||||
switchIconTriggerList.map(function (switchIconTriggerEl: HTMLElement) {
|
||||
const instance = SwitchIcon.getOrCreateInstance(switchIconTriggerEl)
|
||||
elementMap.set(switchIconTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default SwitchIcon
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tab } from './bootstrap'
|
||||
import { Tab } from 'bootstrap'
|
||||
|
||||
export const EnableActivationTabsFromLocationHash = (): void => {
|
||||
const locationHash: string = window.location.hash
|
||||
|
||||
222
core/js/src/theme.ts
Normal file
222
core/js/src/theme.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler theme.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'theme'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
interface ThemeConfig {
|
||||
'theme': string
|
||||
'theme-base': string
|
||||
'theme-font': string
|
||||
'theme-primary': string
|
||||
'theme-radius': string
|
||||
}
|
||||
|
||||
const DEFAULT_THEME_CONFIG: ThemeConfig = {
|
||||
'theme': 'light',
|
||||
'theme-base': 'gray',
|
||||
'theme-font': 'sans-serif',
|
||||
'theme-primary': 'blue',
|
||||
'theme-radius': '1',
|
||||
}
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class Theme {
|
||||
private initialized: boolean = false
|
||||
private config: ThemeConfig
|
||||
|
||||
constructor(config?: Partial<ThemeConfig>) {
|
||||
this.config = { ...DEFAULT_THEME_CONFIG, ...config }
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize theme
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this._applyTheme()
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update theme configuration
|
||||
*/
|
||||
update(config: Partial<ThemeConfig>): void {
|
||||
this.config = { ...this.config, ...config }
|
||||
this._applyTheme()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme (light/dark)
|
||||
*/
|
||||
setTheme(value: string): void {
|
||||
this.update({ theme: value })
|
||||
this._updateURL('theme', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set primary color
|
||||
*/
|
||||
setPrimary(value: string): void {
|
||||
this.update({ 'theme-primary': value })
|
||||
this._updateURL('theme-primary', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme base
|
||||
*/
|
||||
setBase(value: string): void {
|
||||
this.update({ 'theme-base': value })
|
||||
this._updateURL('theme-base', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set font family
|
||||
*/
|
||||
setFont(value: string): void {
|
||||
this.update({ 'theme-font': value })
|
||||
this._updateURL('theme-font', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set border radius
|
||||
*/
|
||||
setRadius(value: string): void {
|
||||
this.update({ 'theme-radius': value })
|
||||
this._updateURL('theme-radius', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all theme settings to default
|
||||
*/
|
||||
reset(): void {
|
||||
this.config = { ...DEFAULT_THEME_CONFIG }
|
||||
this._applyTheme()
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href)
|
||||
for (const key in DEFAULT_THEME_CONFIG) {
|
||||
url.searchParams.delete(key)
|
||||
localStorage.removeItem('tabler-' + key)
|
||||
}
|
||||
window.history.pushState({}, '', url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme configuration
|
||||
*/
|
||||
getConfig(): ThemeConfig {
|
||||
const config: ThemeConfig = { ...DEFAULT_THEME_CONFIG }
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
for (const key in DEFAULT_THEME_CONFIG) {
|
||||
const stored = localStorage.getItem('tabler-' + key)
|
||||
if (stored) {
|
||||
config[key as keyof ThemeConfig] = stored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose theme instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Private
|
||||
/**
|
||||
* Update URL with theme parameter
|
||||
*/
|
||||
private _updateURL(key: string, value: string): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href)
|
||||
if (value === DEFAULT_THEME_CONFIG[key as keyof ThemeConfig]) {
|
||||
url.searchParams.delete(key)
|
||||
} else {
|
||||
url.searchParams.set(key, value)
|
||||
}
|
||||
window.history.pushState({}, '', url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme to document
|
||||
*/
|
||||
private _applyTheme(): void {
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams: URLSearchParams, prop: string): string | null => searchParams.get(prop),
|
||||
})
|
||||
|
||||
for (const key in this.config) {
|
||||
const param = params[key]
|
||||
let selectedValue: string
|
||||
|
||||
if (!!param) {
|
||||
localStorage.setItem('tabler-' + key, param)
|
||||
selectedValue = param
|
||||
} else {
|
||||
const storedTheme = localStorage.getItem('tabler-' + key)
|
||||
selectedValue = storedTheme ? storedTheme : this.config[key as keyof ThemeConfig]
|
||||
}
|
||||
|
||||
if (selectedValue !== this.config[key as keyof ThemeConfig]) {
|
||||
document.documentElement.setAttribute('data-bs-' + key, selectedValue)
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-bs-' + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(config?: Partial<ThemeConfig>): Theme {
|
||||
if (!Theme.instance) {
|
||||
Theme.instance = new Theme(config)
|
||||
}
|
||||
return Theme.instance
|
||||
}
|
||||
|
||||
private static instance: Theme | null = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const theme = Theme.getOrCreateInstance()
|
||||
theme.init()
|
||||
|
||||
export default Theme
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Toast } from './bootstrap'
|
||||
import { Toast } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Toasts
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip } from './bootstrap'
|
||||
import { Tooltip } from 'bootstrap'
|
||||
|
||||
const tooltipTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]'))
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl: HTMLElement) {
|
||||
|
||||
@@ -1,43 +1,7 @@
|
||||
/**
|
||||
* demo-theme is specifically loaded right after the body and not deferred
|
||||
* Theme initialization entry point
|
||||
* This file is specifically loaded right after the body and not deferred
|
||||
* to ensure we switch to the chosen dark/light theme as fast as possible.
|
||||
* This will prevent any flashes of the light theme (default) before switching.
|
||||
*/
|
||||
interface ThemeConfig {
|
||||
'theme': string
|
||||
'theme-base': string
|
||||
'theme-font': string
|
||||
'theme-primary': string
|
||||
'theme-radius': string
|
||||
}
|
||||
|
||||
const themeConfig: ThemeConfig = {
|
||||
'theme': 'light',
|
||||
'theme-base': 'gray',
|
||||
'theme-font': 'sans-serif',
|
||||
'theme-primary': 'blue',
|
||||
'theme-radius': '1',
|
||||
}
|
||||
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams: URLSearchParams, prop: string): string | null => searchParams.get(prop),
|
||||
})
|
||||
|
||||
for (const key in themeConfig) {
|
||||
const param = params[key]
|
||||
let selectedValue: string
|
||||
|
||||
if (!!param) {
|
||||
localStorage.setItem('tabler-' + key, param)
|
||||
selectedValue = param
|
||||
} else {
|
||||
const storedTheme = localStorage.getItem('tabler-' + key)
|
||||
selectedValue = storedTheme ? storedTheme : themeConfig[key as keyof ThemeConfig]
|
||||
}
|
||||
|
||||
if (selectedValue !== themeConfig[key as keyof ThemeConfig]) {
|
||||
document.documentElement.setAttribute('data-bs-' + key, selectedValue)
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-bs-' + key)
|
||||
}
|
||||
}
|
||||
import './src/theme'
|
||||
|
||||
@@ -1,16 +1,56 @@
|
||||
import './src/autosize'
|
||||
import './src/countup'
|
||||
import './src/input-mask'
|
||||
import './src/dropdown'
|
||||
import './src/tooltip'
|
||||
import './src/popover'
|
||||
import './src/switch-icon'
|
||||
import './src/tab'
|
||||
import './src/toast'
|
||||
import './src/modal'
|
||||
import './src/collapse'
|
||||
import './src/offcanvas'
|
||||
import './src/sortable'
|
||||
import './src/switch-icon'
|
||||
import './src/autosize'
|
||||
import './src/countup'
|
||||
import './src/input-mask'
|
||||
import './src/copy'
|
||||
import Theme from './src/theme'
|
||||
|
||||
// Re-export everything from bootstrap.ts (single source of truth)
|
||||
export * from './src/bootstrap'
|
||||
// Export Popper
|
||||
export * as Popper from '@popperjs/core'
|
||||
|
||||
// Export all Bootstrap components directly for consistent usage
|
||||
export { Alert, Button, Carousel, Collapse, Dropdown, Modal, Offcanvas, Popover, ScrollSpy, Tab, Toast, Tooltip } from 'bootstrap'
|
||||
|
||||
// Export custom Tabler components
|
||||
export { default as Autosize } from './src/autosize'
|
||||
export { default as SwitchIcon } from './src/switch-icon'
|
||||
export { default as Countup } from './src/countup'
|
||||
export { default as InputMask } from './src/input-mask'
|
||||
export { default as Copy } from './src/copy'
|
||||
export { default as Clipboard } from './src/clipboard'
|
||||
|
||||
// Re-export everything as namespace for backward compatibility
|
||||
export * as bootstrap from 'bootstrap'
|
||||
|
||||
// Re-export tabler namespace
|
||||
export * as tabler from './src/tabler'
|
||||
|
||||
// Create global Tabler API object
|
||||
const themeInstance = Theme.getOrCreateInstance()
|
||||
|
||||
const Tabler = {
|
||||
setTheme: (value: string) => themeInstance.setTheme(value),
|
||||
setPrimary: (value: string) => themeInstance.setPrimary(value),
|
||||
setBase: (value: string) => themeInstance.setBase(value),
|
||||
setFont: (value: string) => themeInstance.setFont(value),
|
||||
setRadius: (value: string) => themeInstance.setRadius(value),
|
||||
reset: () => themeInstance.reset(),
|
||||
getConfig: () => themeInstance.getConfig(),
|
||||
}
|
||||
|
||||
// Export Tabler API
|
||||
export { Tabler }
|
||||
|
||||
// Make Tabler available globally on window object
|
||||
if (typeof window !== 'undefined') {
|
||||
;(window as any).Tabler = Tabler
|
||||
}
|
||||
|
||||
168
core/js/tests/README.md
Normal file
168
core/js/tests/README.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Tabler JavaScript Tests
|
||||
|
||||
## How does Tabler's test suite work?
|
||||
|
||||
Tabler uses **Vitest** for unit testing. Each plugin has a file dedicated to its tests in `tests/unit/<plugin-name>.spec.ts`.
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run the unit test suite, run:
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
To run tests in watch mode:
|
||||
```bash
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
To run tests with UI:
|
||||
```bash
|
||||
pnpm test:ui
|
||||
```
|
||||
|
||||
To run tests with coverage:
|
||||
```bash
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
## How do I add a new unit test?
|
||||
|
||||
1. Locate and open the file dedicated to the plugin which you need to add tests to (`tests/unit/<plugin-name>.spec.ts`).
|
||||
2. Review the Vitest API Documentation and use the existing tests as references for how to structure your new tests.
|
||||
3. Write the necessary unit test(s) for the new or revised functionality.
|
||||
4. Run `pnpm test` to see the results of your newly-added test(s).
|
||||
|
||||
**Note:** Your new unit tests should fail before your changes are applied to the plugin, and should pass after your changes are applied to the plugin.
|
||||
|
||||
## What should a unit test look like?
|
||||
|
||||
- Each test should have a unique name clearly stating what unit is being tested.
|
||||
- Each test should be in the corresponding `describe` block.
|
||||
- Each test should test only one unit per test, although one test can include several assertions. Create multiple tests for multiple units of functionality.
|
||||
- Each test should use `expect` to ensure something is expected.
|
||||
- Each test should follow the project's JavaScript Code Guidelines.
|
||||
|
||||
## Code Coverage
|
||||
|
||||
We're aiming for at least 90% test coverage for our code. To ensure your changes meet or exceed this limit, run:
|
||||
|
||||
```bash
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
This will generate coverage reports in multiple formats:
|
||||
- **Text**: Displayed in terminal
|
||||
- **HTML**: Open `coverage/index.html` in your browser for detailed coverage report
|
||||
- **JSON**: `coverage/coverage-final.json` for programmatic access
|
||||
- **LCOV**: `coverage/lcov.info` for CI/CD integration
|
||||
|
||||
### Coverage Thresholds
|
||||
|
||||
The following thresholds are enforced (minimum 90%):
|
||||
- Lines: 90%
|
||||
- Functions: 90%
|
||||
- Branches: 90%
|
||||
- Statements: 90%
|
||||
|
||||
If coverage falls below these thresholds, tests will fail.
|
||||
|
||||
## Test Structure
|
||||
|
||||
Tests follow Bootstrap's test structure pattern:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import PluginName from '../../src/plugin-name'
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
describe('PluginName', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
})
|
||||
|
||||
describe('getInstance', () => {
|
||||
it('should return null if there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
expect(PluginName.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Example Tests
|
||||
|
||||
### Synchronous test
|
||||
|
||||
```typescript
|
||||
describe('getInstance', () => {
|
||||
it('should return null if there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
expect(PluginName.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should return this instance', () => {
|
||||
fixtureEl.innerHTML = '<div></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const instance = new PluginName(divEl)
|
||||
|
||||
expect(PluginName.getInstance(divEl)).toEqual(instance)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Asynchronous test
|
||||
|
||||
```typescript
|
||||
it('should show a tooltip without the animation', async () => {
|
||||
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'
|
||||
const tooltipEl = fixtureEl.querySelector('a') as HTMLElement
|
||||
const tooltip = new Tooltip(tooltipEl, {
|
||||
animation: false
|
||||
})
|
||||
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
tooltipEl.addEventListener('shown.bs.tooltip', () => {
|
||||
const tip = document.querySelector('.tooltip')
|
||||
|
||||
expect(tip).not.toBeNull()
|
||||
expect(tip?.classList.contains('fade')).toBe(false)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
tooltip.show()
|
||||
await promise
|
||||
})
|
||||
```
|
||||
|
||||
## Fixture Helper
|
||||
|
||||
Use the fixture helper to manage test DOM elements:
|
||||
|
||||
```typescript
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
describe('MyPlugin', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
})
|
||||
|
||||
it('should work', () => {
|
||||
fixtureEl.innerHTML = '<div class="test"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
// Your test code here
|
||||
})
|
||||
})
|
||||
```
|
||||
36
core/js/tests/helpers/fixture.ts
Normal file
36
core/js/tests/helpers/fixture.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Test fixture helper
|
||||
* Similar to Bootstrap's fixtureEl pattern
|
||||
*/
|
||||
|
||||
let fixtureContainer: HTMLDivElement | null = null
|
||||
|
||||
/**
|
||||
* Get or create fixture container
|
||||
*/
|
||||
export function getFixture(): HTMLDivElement {
|
||||
if (!fixtureContainer) {
|
||||
fixtureContainer = document.createElement('div')
|
||||
fixtureContainer.id = 'test-fixture'
|
||||
document.body.appendChild(fixtureContainer)
|
||||
}
|
||||
return fixtureContainer
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear fixture container
|
||||
*/
|
||||
export function clearFixture(): void {
|
||||
const fixture = getFixture()
|
||||
fixture.innerHTML = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove fixture container
|
||||
*/
|
||||
export function removeFixture(): void {
|
||||
if (fixtureContainer && fixtureContainer.parentNode) {
|
||||
fixtureContainer.parentNode.removeChild(fixtureContainer)
|
||||
fixtureContainer = null
|
||||
}
|
||||
}
|
||||
13
core/js/tests/helpers/setup.ts
Normal file
13
core/js/tests/helpers/setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { afterEach } from 'vitest'
|
||||
import { clearFixture } from './fixture'
|
||||
|
||||
/**
|
||||
* Setup file for Vitest tests
|
||||
* This file runs before each test file
|
||||
*
|
||||
* Note: jsdom automatically cleans up the DOM between tests,
|
||||
* but we clear fixture for consistency with Bootstrap's test pattern
|
||||
*/
|
||||
afterEach(() => {
|
||||
clearFixture()
|
||||
})
|
||||
346
core/js/tests/unit/clipboard.spec.ts
Normal file
346
core/js/tests/unit/clipboard.spec.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { Tooltip } from 'bootstrap'
|
||||
import Copy from '../../src/copy'
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
describe('Copy', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
let clipboardWriteText: ReturnType<typeof vi.fn>
|
||||
let originalClipboardDescriptor: PropertyDescriptor | undefined
|
||||
let originalExecCommand: any
|
||||
|
||||
const setNavigatorClipboard = (writeTextImpl: (text: string) => Promise<void>) => {
|
||||
originalClipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 'clipboard')
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: writeTextImpl },
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
|
||||
clipboardWriteText = vi.fn().mockResolvedValue(undefined)
|
||||
setNavigatorClipboard(clipboardWriteText)
|
||||
|
||||
originalExecCommand = (document as any).execCommand
|
||||
;(document as any).execCommand = vi.fn().mockReturnValue(false)
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
|
||||
if (originalClipboardDescriptor) {
|
||||
Object.defineProperty(navigator, 'clipboard', originalClipboardDescriptor)
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete (navigator as any).clipboard
|
||||
}
|
||||
|
||||
;(document as any).execCommand = originalExecCommand
|
||||
})
|
||||
|
||||
describe('getInstance / getOrCreateInstance', () => {
|
||||
it('should return null when instance does not exist', () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="x"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
expect(Copy.getInstance(buttonEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should create and store instance on element', () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="x"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance1 = Copy.getOrCreateInstance(buttonEl)
|
||||
expect(instance1).toBeInstanceOf(Copy)
|
||||
expect(Copy.getInstance(buttonEl)).toBe(instance1)
|
||||
|
||||
const instance2 = Copy.getOrCreateInstance(buttonEl)
|
||||
expect(instance2).toBe(instance1)
|
||||
})
|
||||
|
||||
it('dispose should remove stored instance', () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="x"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
expect(Copy.getInstance(buttonEl)).toBe(instance)
|
||||
|
||||
instance.dispose()
|
||||
expect(Copy.getInstance(buttonEl)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy()', () => {
|
||||
it('should copy text from data-bs-copy-text and set copied state', async () => {
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const def = buttonEl.querySelector('[data-bs-copy-default]') as HTMLElement
|
||||
const success = buttonEl.querySelector('[data-bs-copy-success]') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(clipboardWriteText).toHaveBeenCalledWith('hello')
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
expect(buttonEl.getAttribute('aria-label')).toBe('Copied')
|
||||
expect(def.hidden).toBe(true)
|
||||
expect(success.hidden).toBe(false)
|
||||
})
|
||||
|
||||
it('should fire cancelable COPY event and respect preventDefault()', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
buttonEl.addEventListener(Copy.Event.COPY, (e) => e.preventDefault())
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(clipboardWriteText).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should emit COPYFAIL when there is no text', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const onFail = vi.fn()
|
||||
buttonEl.addEventListener(Copy.Event.COPYFAIL, onFail as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(onFail).toHaveBeenCalledTimes(1)
|
||||
const ev = onFail.mock.calls[0][0] as CustomEvent
|
||||
expect(ev.detail.reason).toBe('no-text')
|
||||
})
|
||||
|
||||
it('should emit COPYFAIL when clipboard write fails', async () => {
|
||||
clipboardWriteText.mockRejectedValueOnce(new Error('nope'))
|
||||
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const onFail = vi.fn()
|
||||
buttonEl.addEventListener(Copy.Event.COPYFAIL, onFail as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(onFail).toHaveBeenCalledTimes(1)
|
||||
const ev = onFail.mock.calls[0][0] as CustomEvent
|
||||
expect(ev.detail.reason).toBe('clipboard-failed')
|
||||
})
|
||||
|
||||
it('should reset automatically after timeout when reset=true', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const def = buttonEl.querySelector('[data-bs-copy-default]') as HTMLElement
|
||||
const success = buttonEl.querySelector('[data-bs-copy-success]') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { timeout: 1000 })!
|
||||
await instance.copy()
|
||||
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
vi.advanceTimersByTime(999)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
expect(buttonEl.getAttribute('aria-label')).toBeNull()
|
||||
expect(def.hidden).toBe(false)
|
||||
expect(success.hidden).toBe(true)
|
||||
})
|
||||
|
||||
it('should extend timeout without recopy when recopy=false and extend=true', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { timeout: 1000, recopy: false, extend: true, reset: true })!
|
||||
|
||||
await instance.copy()
|
||||
expect(clipboardWriteText).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(500)
|
||||
await instance.copy()
|
||||
expect(clipboardWriteText).toHaveBeenCalledTimes(1)
|
||||
|
||||
// 600ms after the second click: should still be copied (timer extended)
|
||||
vi.advanceTimersByTime(600)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
// 401ms more (total 1001ms after second click): should reset
|
||||
vi.advanceTimersByTime(401)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
})
|
||||
|
||||
it('should recopy when recopy=true (default)', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { reset: false })!
|
||||
|
||||
await instance.copy()
|
||||
await instance.copy()
|
||||
|
||||
expect(clipboardWriteText).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should not extend timeout when extend=false and timer is already running', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { timeout: 1000, recopy: false, extend: false, reset: true })!
|
||||
|
||||
await instance.copy()
|
||||
vi.advanceTimersByTime(500)
|
||||
await instance.copy() // should NOT extend existing timer
|
||||
|
||||
vi.advanceTimersByTime(501) // total 1001ms from first copy
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reset()', () => {
|
||||
it('should manually reset and emit RESET with manual=true', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const onReset = vi.fn()
|
||||
buttonEl.addEventListener(Copy.Event.RESET, onReset as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { reset: false })!
|
||||
await instance.copy()
|
||||
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
instance.reset()
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
expect(onReset).toHaveBeenCalledTimes(1)
|
||||
const ev = onReset.mock.calls[0][0] as CustomEvent
|
||||
expect(ev.detail.manual).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data API', () => {
|
||||
it('should copy on click via document listener', async () => {
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const span = buttonEl.querySelector('span') as HTMLElement
|
||||
|
||||
span.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
|
||||
// allow async copy() chain to complete (Data API doesn't await)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(clipboardWriteText).toHaveBeenCalledWith('hello')
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('feedback: tooltip', () => {
|
||||
it('should show/hide bootstrap tooltip when available', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const show = vi.fn()
|
||||
const hide = vi.fn()
|
||||
const dispose = vi.fn()
|
||||
const getOrCreateInstance = vi.spyOn(Tooltip, 'getOrCreateInstance').mockReturnValue({ show, hide, dispose } as any)
|
||||
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, {
|
||||
feedback: 'tooltip',
|
||||
tooltip: 'Copied!',
|
||||
tooltipPlacement: 'top',
|
||||
timeout: 500,
|
||||
reset: true,
|
||||
})!
|
||||
|
||||
await instance.copy()
|
||||
expect(getOrCreateInstance).toHaveBeenCalledTimes(1)
|
||||
expect(show).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(500)
|
||||
expect(hide).toHaveBeenCalledTimes(1)
|
||||
|
||||
instance.dispose()
|
||||
expect(dispose).toHaveBeenCalledTimes(1)
|
||||
getOrCreateInstance.mockRestore()
|
||||
})
|
||||
|
||||
it('should not swap when tooltip mode is enabled', async () => {
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const show = vi.fn()
|
||||
const hide = vi.fn()
|
||||
const dispose = vi.fn()
|
||||
const getOrCreateInstance = vi.spyOn(Tooltip, 'getOrCreateInstance').mockReturnValue({ show, hide, dispose } as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { feedback: 'tooltip', reset: false })!
|
||||
await instance.copy()
|
||||
|
||||
// tooltip mode uses Tooltip feedback and does not rely on swap markup
|
||||
expect(show).toHaveBeenCalledTimes(1)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
|
||||
instance.dispose()
|
||||
getOrCreateInstance.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('static properties', () => {
|
||||
it('should have correct NAME', () => {
|
||||
expect(Copy.NAME).toBe('copy')
|
||||
})
|
||||
|
||||
it('should have correct DATA_KEY', () => {
|
||||
expect(Copy.DATA_KEY).toBe('bs.copy')
|
||||
})
|
||||
})
|
||||
})
|
||||
204
core/js/tests/unit/switch-icon.spec.ts
Normal file
204
core/js/tests/unit/switch-icon.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import SwitchIcon from '../../src/switch-icon'
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
/**
|
||||
* Unit tests for SwitchIcon plugin
|
||||
* Following Bootstrap's test structure pattern
|
||||
*/
|
||||
|
||||
describe('SwitchIcon', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
})
|
||||
|
||||
describe('getInstance', () => {
|
||||
it('should return null if there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
expect(SwitchIcon.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should return instance when created via Data API', () => {
|
||||
// Simulate Data API behavior - element with data attribute triggers initialization
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
// Manually trigger Data API-like behavior
|
||||
const switchIcon = SwitchIcon.getOrCreateInstance(divEl)
|
||||
// Note: In real Data API, elementMap.set() is called, but getOrCreateInstance doesn't do that
|
||||
// So we test that getInstance returns null when instance is not in map
|
||||
expect(SwitchIcon.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOrCreateInstance', () => {
|
||||
it('should return new instance when there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
const switchIcon = SwitchIcon.getOrCreateInstance(divEl)
|
||||
|
||||
expect(switchIcon).toBeInstanceOf(SwitchIcon)
|
||||
// Note: getOrCreateInstance doesn't add to elementMap, only Data API does
|
||||
expect(SwitchIcon.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should return new instance each time (not stored in map)', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon1 = SwitchIcon.getOrCreateInstance(divEl)
|
||||
const switchIcon2 = SwitchIcon.getOrCreateInstance(divEl)
|
||||
|
||||
// Both are new instances because they're not stored in elementMap
|
||||
// getOrCreateInstance creates new instance if not found in map
|
||||
expect(switchIcon1).toBeInstanceOf(SwitchIcon)
|
||||
expect(switchIcon2).toBeInstanceOf(SwitchIcon)
|
||||
// They are different instances because elementMap is not used by getOrCreateInstance
|
||||
expect(switchIcon1).not.toEqual(switchIcon2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggle', () => {
|
||||
it('should toggle active class', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
|
||||
switchIcon.toggle()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
|
||||
switchIcon.toggle()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('show', () => {
|
||||
it('should add active class', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
expect(divEl.classList.contains('active')).toBe(false)
|
||||
|
||||
switchIcon.show()
|
||||
expect(divEl.classList.contains('active')).toBe(true)
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hide', () => {
|
||||
it('should remove active class', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon" class="active"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
expect(divEl.classList.contains('active')).toBe(true)
|
||||
|
||||
switchIcon.hide()
|
||||
expect(divEl.classList.contains('active')).toBe(false)
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
it('should initialize and set event listeners', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
|
||||
// Simulate click
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not initialize twice', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
const firstClick = () => {
|
||||
divEl.click()
|
||||
}
|
||||
firstClick()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
|
||||
// Reset
|
||||
switchIcon.hide()
|
||||
switchIcon.init() // Should not add duplicate listeners
|
||||
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should remove event listeners', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
switchIcon.dispose()
|
||||
|
||||
// Click should not toggle after dispose
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('should do nothing if not initialized', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
// Should not throw
|
||||
expect(() => {
|
||||
switchIcon.dispose()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('click event', () => {
|
||||
it('should toggle on click after init', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('should stop propagation', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
let parentClicked = false
|
||||
|
||||
fixtureEl.addEventListener('click', () => {
|
||||
parentClicked = true
|
||||
})
|
||||
|
||||
switchIcon.init()
|
||||
divEl.click()
|
||||
|
||||
expect(parentClicked).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
64
core/js/tests/unit/tabler.test.ts
Normal file
64
core/js/tests/unit/tabler.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { hexToRgba, getColor, prefix } from '../../src/tabler'
|
||||
|
||||
describe('tabler', () => {
|
||||
describe('prefix', () => {
|
||||
it('should have correct prefix value', () => {
|
||||
expect(prefix).toBe('tblr-')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hexToRgba', () => {
|
||||
it('should convert hex color to rgba string', () => {
|
||||
expect(hexToRgba('#ff0000', 1)).toBe('rgba(255, 0, 0, 1)')
|
||||
expect(hexToRgba('#00ff00', 0.5)).toBe('rgba(0, 255, 0, 0.5)')
|
||||
expect(hexToRgba('#0000ff', 0.8)).toBe('rgba(0, 0, 255, 0.8)')
|
||||
})
|
||||
|
||||
it('should handle hex without #', () => {
|
||||
expect(hexToRgba('ff0000', 1)).toBe('rgba(255, 0, 0, 1)')
|
||||
})
|
||||
|
||||
it('should handle uppercase hex', () => {
|
||||
expect(hexToRgba('#FF0000', 1)).toBe('rgba(255, 0, 0, 1)')
|
||||
})
|
||||
|
||||
it('should return null for invalid hex', () => {
|
||||
expect(hexToRgba('invalid', 1)).toBeNull()
|
||||
expect(hexToRgba('#gg0000', 1)).toBeNull()
|
||||
expect(hexToRgba('', 1)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getColor', () => {
|
||||
let originalBody: HTMLBodyElement
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original body
|
||||
originalBody = document.body
|
||||
|
||||
// Set test CSS variable value
|
||||
document.body.style.setProperty(`--${prefix}primary`, '#ff0000')
|
||||
})
|
||||
|
||||
it('should get color from CSS variable', () => {
|
||||
const color = getColor('primary')
|
||||
expect(color).toBe('#ff0000')
|
||||
})
|
||||
|
||||
it('should return null if color variable does not exist', () => {
|
||||
const color = getColor('nonexistent')
|
||||
expect(color).toBe('')
|
||||
})
|
||||
|
||||
it('should convert to rgba when opacity is provided', () => {
|
||||
const color = getColor('primary', 0.5)
|
||||
expect(color).toBe('rgba(255, 0, 0, 0.5)')
|
||||
})
|
||||
|
||||
it('should return hex color when opacity is 1', () => {
|
||||
const color = getColor('primary', 1)
|
||||
expect(color).toBe('#ff0000')
|
||||
})
|
||||
})
|
||||
})
|
||||
20
core/js/tests/vitest.config.ts
Normal file
20
core/js/tests/vitest.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./helpers/setup.ts'],
|
||||
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, '../src')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -20,15 +20,15 @@
|
||||
"css-lint-variables": "find-unused-sass-variables scss/ node_modules/bootstrap/scss/",
|
||||
"js": "pnpm run js-build && pnpm run js-build-min",
|
||||
"js-build": "concurrently \"pnpm run js-build-standalone\" \"pnpm run js-build-standalone-esm\" \"pnpm run js-build-theme\" \"pnpm run js-build-theme-esm\"",
|
||||
"js-build-theme-esm": "cross-env THEME=true ESM=true vite build --config .build/vite.config.ts",
|
||||
"js-build-theme": "cross-env THEME=true vite build --config .build/vite.config.ts",
|
||||
"js-build-standalone": "vite build --config .build/vite.config.ts",
|
||||
"js-build-standalone-esm": "cross-env ESM=true vite build --config .build/vite.config.ts",
|
||||
"js-build-theme-esm": "cross-env THEME=true ESM=true vite build --config .build/vite.config.mts",
|
||||
"js-build-theme": "cross-env THEME=true vite build --config .build/vite.config.mts",
|
||||
"js-build-standalone": "vite build --config .build/vite.config.mts",
|
||||
"js-build-standalone-esm": "cross-env ESM=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min": "concurrently \"pnpm run js-build-min-standalone\" \"pnpm run js-build-min-standalone-esm\" \"pnpm run js-build-min-theme\" \"pnpm run js-build-min-theme-esm\"",
|
||||
"js-build-min-standalone": "cross-env MINIFY=true vite build --config .build/vite.config.ts",
|
||||
"js-build-min-standalone-esm": "cross-env MINIFY=true ESM=true vite build --config .build/vite.config.ts",
|
||||
"js-build-min-theme": "cross-env MINIFY=true THEME=true vite build --config .build/vite.config.ts",
|
||||
"js-build-min-theme-esm": "cross-env MINIFY=true THEME=true ESM=true vite build --config .build/vite.config.ts",
|
||||
"js-build-min-standalone": "cross-env MINIFY=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min-standalone-esm": "cross-env MINIFY=true ESM=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min-theme": "cross-env MINIFY=true THEME=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min-theme-esm": "cross-env MINIFY=true THEME=true ESM=true vite build --config .build/vite.config.mts",
|
||||
"copy": "concurrently \"pnpm run copy-img\" \"pnpm run copy-libs\" \"pnpm run copy-fonts\"",
|
||||
"copy-img": "shx mkdir -p dist/img && shx cp -rf img/* dist/img",
|
||||
"copy-libs": "tsx .build/copy-libs.ts",
|
||||
@@ -41,7 +41,10 @@
|
||||
"generate-sri": "tsx .build/generate-sri.ts",
|
||||
"format:check": "prettier --check \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
|
||||
"format:write": "prettier --write \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
|
||||
"type-check": "tsc --noEmit"
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run --config js/tests/vitest.config.ts",
|
||||
"test:watch": "vitest --config js/tests/vitest.config.ts",
|
||||
"test:ui": "vitest --ui --config js/tests/vitest.config.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -157,12 +160,16 @@
|
||||
"devDependencies": {
|
||||
"@hotwired/turbo": "^8.0.18",
|
||||
"@melloware/coloris": "^0.25.0",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@vitest/coverage-v8": "2.1.9",
|
||||
"@vitest/ui": "^2.0.0",
|
||||
"apexcharts": "^5.3.6",
|
||||
"autosize": "^6.0.1",
|
||||
"choices.js": "^11.1.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"countup.js": "^2.9.0",
|
||||
"driver.js": "^1.0.0",
|
||||
"dropzone": "^6.0.0-beta.2",
|
||||
"find-unused-sass-variables": "^6.1.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
@@ -171,6 +178,7 @@
|
||||
"geist": "^1.5.1",
|
||||
"hugerte": "^1.0.9",
|
||||
"imask": "^7.6.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsvectormap": "^1.7.0",
|
||||
"list.js": "^2.3.1",
|
||||
"litepicker": "^2.0.12",
|
||||
@@ -182,7 +190,7 @@
|
||||
"tom-select": "^2.4.3",
|
||||
"typed.js": "^2.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"driver.js": "^1.0.0"
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
|
||||
/** Theme colors */
|
||||
@each $name, $color in map.merge($theme-colors, $social-colors) {
|
||||
@debug contrast-ratio($color, white), $name, $min-contrast-ratio;
|
||||
--#{$prefix}#{$name}: #{$color};
|
||||
--#{$prefix}#{$name}-rgb: #{to-rgb($color)};
|
||||
--#{$prefix}#{$name}-fg: #{if(contrast-ratio($color) > $min-contrast-ratio, var(--#{$prefix}light), var(--#{$prefix}dark))};
|
||||
|
||||
@@ -138,8 +138,6 @@
|
||||
|
||||
// Colors
|
||||
@function to-rgb($value) {
|
||||
@debug $value;
|
||||
|
||||
@return color.channel($value, 'red', $space: rgb), color.channel($value, 'green', $space: rgb), color.channel($value, 'blue', $space: rgb);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": true,
|
||||
"allowJs": true
|
||||
"allowJs": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["js/src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"js/**/*"
|
||||
|
||||
@@ -1,20 +1,87 @@
|
||||
---
|
||||
title: Autosize
|
||||
summary: The autosize element will automatically adjust the textarea height and make it easier for users to follow as they type.
|
||||
docs-libs: autosize
|
||||
description: Auto-adjusting textarea for better usability.
|
||||
---
|
||||
|
||||
To be able to use the autosize in your application you will need to install the autosize dependency with `npm install autosize`.
|
||||
The autosize component is built into Tabler and works similar to Bootstrap components. It automatically adjusts textarea height as users type.
|
||||
|
||||
## Default markup
|
||||
## Basic usage
|
||||
|
||||
Add the autosize element to your input to make it automatically adjust to the length of a text as a user types it.
|
||||
|
||||
To create autosize textarea, add the `data-bs-toggle="autosize"` attribute to the textarea element:
|
||||
The easiest way to use autosize is through the Data API. Add the `data-bs-autosize` attribute to the textarea element:
|
||||
|
||||
{% capture html -%}
|
||||
<label class="form-label">Autosize example</label>
|
||||
<textarea class="form-control" data-bs-toggle="autosize" placeholder="Type something…"></textarea>
|
||||
<textarea class="form-control" data-bs-autosize placeholder="Type something…"></textarea>
|
||||
{%- endcapture %}
|
||||
{% include "docs/example.html" html=html column vertical %}
|
||||
|
||||
## With initial rows
|
||||
|
||||
You can set an initial number of rows for the textarea:
|
||||
|
||||
{% capture html -%}
|
||||
<label class="form-label">Autosize with initial rows</label>
|
||||
<textarea class="form-control" data-bs-autosize rows="3" placeholder="Type something…"></textarea>
|
||||
{%- endcapture %}
|
||||
{% include "docs/example.html" html=html column vertical %}
|
||||
|
||||
## Usage
|
||||
|
||||
### Via data attributes
|
||||
|
||||
Add `data-bs-autosize` to a textarea element to automatically initialize autosize:
|
||||
|
||||
```html
|
||||
<textarea class="form-control" data-bs-autosize placeholder="Type something…"></textarea>
|
||||
```
|
||||
|
||||
### Via JavaScript
|
||||
|
||||
Initialize autosize programmatically using the `Autosize` class:
|
||||
|
||||
```javascript
|
||||
import { Autosize } from '@tabler/core'
|
||||
|
||||
// Get or create instance
|
||||
const textarea = document.querySelector('textarea')
|
||||
const autosize = Autosize.getOrCreateInstance(textarea)
|
||||
autosize.init()
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `init()` | Initialize autosize on the element. |
|
||||
| `update()` | Update autosize when content changes programmatically. |
|
||||
| `dispose()` | Destroy autosize instance. |
|
||||
| `getInstance(element)` | *Static* method which allows you to get the autosize instance associated with a DOM element. |
|
||||
| `getOrCreateInstance(element)` | *Static* method which allows you to get the autosize instance associated with a DOM element, or create a new one in case it wasn't initialized. |
|
||||
|
||||
#### Example: Update after content change
|
||||
|
||||
```javascript
|
||||
import { Autosize } from '@tabler/core'
|
||||
|
||||
const textarea = document.querySelector('textarea')
|
||||
const autosize = Autosize.getOrCreateInstance(textarea)
|
||||
autosize.init()
|
||||
|
||||
// Later, when content changes programmatically
|
||||
textarea.value = 'New content that is longer than before...'
|
||||
autosize.update()
|
||||
```
|
||||
|
||||
#### Example: Get existing instance
|
||||
|
||||
```javascript
|
||||
import { Autosize } from '@tabler/core'
|
||||
|
||||
const textarea = document.querySelector('textarea')
|
||||
const autosize = Autosize.getInstance(textarea)
|
||||
|
||||
if (autosize) {
|
||||
autosize.update()
|
||||
}
|
||||
```
|
||||
|
||||
311
docs/content/ui/components/copy.md
Normal file
311
docs/content/ui/components/copy.md
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
title: Copy
|
||||
summary: Quickly copy text to the clipboard with UI feedback.
|
||||
description: Use Copy to copy text to the clipboard and provide instant UI feedback via swap or tooltip.
|
||||
---
|
||||
|
||||
Use the **Copy** plugin to quickly copy text to the clipboard and provide instant UI feedback (**swap** or **tooltip**).
|
||||
|
||||
## How it works
|
||||
|
||||
Copy is a small JavaScript plugin that:
|
||||
|
||||
- listens for **Data API** triggers (`data-bs-toggle="copy"`)
|
||||
- reads the text to copy from:
|
||||
- `data-bs-copy-text` **or**
|
||||
- `data-bs-copy-target` (input/textarea value, otherwise `textContent`)
|
||||
- writes to clipboard via:
|
||||
- `navigator.clipboard.writeText()` (preferred)
|
||||
- fallback: `document.execCommand('copy')`
|
||||
- shows feedback via:
|
||||
- **swap**: toggles `[data-bs-copy-default]` / `[data-bs-copy-success]`
|
||||
- **tooltip**: uses **Bootstrap Tooltip** if available
|
||||
|
||||
## Quick start
|
||||
|
||||
### 1) Include JavaScript
|
||||
|
||||
Copy is included in Tabler’s JavaScript bundle. Make sure you load:
|
||||
|
||||
```html
|
||||
<script src="/dist/js/tabler.js"></script>
|
||||
```
|
||||
|
||||
### 2) Add a trigger
|
||||
|
||||
{% capture html -%}
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
data-bs-toggle="copy"
|
||||
data-bs-copy-text="INVITE-123"
|
||||
>
|
||||
<span data-bs-copy-default>Copy</span>
|
||||
<span data-bs-copy-success hidden>Copied</span>
|
||||
</button>
|
||||
{%- endcapture -%}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
## Usage
|
||||
|
||||
### Via data attributes (recommended)
|
||||
|
||||
Copy is enabled through the Data API:
|
||||
|
||||
- add `data-bs-toggle="copy"` to the trigger element
|
||||
- add `data-bs-copy-text` or `data-bs-copy-target`
|
||||
|
||||
{% capture html -%}
|
||||
<div class="row g-2 align-items-center" style="max-width: 28rem;">
|
||||
<div class="col-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
data-bs-toggle="copy"
|
||||
data-bs-copy-target="#invite"
|
||||
>
|
||||
<span data-bs-copy-default>Copy</span>
|
||||
<span data-bs-copy-success hidden>Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input id="invite" class="form-control" value="INVITE-123" readonly>
|
||||
</div>
|
||||
</div>
|
||||
{%- endcapture -%}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
### Via JavaScript
|
||||
|
||||
```js
|
||||
import { Clipboard as Copy } from '@tabler/core'
|
||||
|
||||
const el = document.querySelector('[data-bs-toggle="copy"]')
|
||||
const instance = Copy.getOrCreateInstance(el)
|
||||
|
||||
instance.copy()
|
||||
```
|
||||
|
||||
## Markup
|
||||
|
||||
### Swap feedback (default)
|
||||
|
||||
The plugin looks for these optional elements **inside** the trigger:
|
||||
|
||||
- `[data-bs-copy-default]` — visible by default
|
||||
- `[data-bs-copy-success]` — shown after copy; should start with `hidden`
|
||||
|
||||
{% capture html -%}
|
||||
<button type="button" class="btn" data-bs-toggle="copy" data-bs-copy-text="Hello">
|
||||
<span data-bs-copy-default>Copy</span>
|
||||
<span data-bs-copy-success hidden>Copied</span>
|
||||
</button>
|
||||
{%- endcapture -%}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
### Tooltip feedback
|
||||
|
||||
Tooltip feedback does **not** require the two spans. It uses Bootstrap Tooltip if present:
|
||||
|
||||
{% capture html -%}
|
||||
<div class="row g-2 align-items-center" style="max-width: 28rem;">
|
||||
<div class="col">
|
||||
<input id="token" class="form-control" value="sk_live_123" readonly>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
data-bs-toggle="copy"
|
||||
data-bs-copy-target="#token"
|
||||
data-bs-copy-feedback="tooltip"
|
||||
data-bs-copy-tooltip="Copied!"
|
||||
>
|
||||
Copy token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{%- endcapture -%}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
> Tooltip mode requires Bootstrap Tooltip + Popper.
|
||||
> If Tooltip isn’t available, Copy falls back to **swap** behavior.
|
||||
|
||||
## Options
|
||||
|
||||
Options can be passed via data attributes (`data-bs-copy-*`) or JS configuration.
|
||||
|
||||
### Data attribute options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---|---:|---:|---|
|
||||
| `data-bs-copy-text` | string | — | Explicit text to copy (highest priority). |
|
||||
| `data-bs-copy-target` | string (selector) | — | Element selector to copy from. For inputs/textarea it copies `value`; otherwise it copies `textContent`. |
|
||||
| `data-bs-copy-timeout` | number (ms) | `1500` | Duration to show copied feedback before reset (if enabled). |
|
||||
| `data-bs-copy-reset` | boolean | `true` | Whether to automatically revert from “Copied” back to default state. Use `"false"` to disable. |
|
||||
| `data-bs-copy-extend` | boolean | `true` | If true, repeated successful copies extend the reset timer. |
|
||||
| `data-bs-copy-recopy` | boolean | `true` | If false and currently “Copied”, subsequent activations only extend timer (if enabled) without writing to clipboard again. |
|
||||
| `data-bs-copy-feedback` | `"swap" \| "tooltip"` | `"swap"` | Feedback mode. Tooltip requires Bootstrap Tooltip. |
|
||||
| `data-bs-copy-tooltip` | string | `"Copied!"` | Tooltip text (tooltip mode only). |
|
||||
| `data-bs-copy-tooltip-placement` | string | `"top"` | Tooltip placement (tooltip mode only). |
|
||||
|
||||
### JavaScript options
|
||||
|
||||
```js
|
||||
import { Clipboard as Copy } from '@tabler/core'
|
||||
|
||||
Copy.getOrCreateInstance(el, {
|
||||
timeout: 1200,
|
||||
reset: true,
|
||||
extend: true,
|
||||
recopy: true,
|
||||
feedback: 'swap', // or 'tooltip'
|
||||
tooltip: 'Copied!',
|
||||
tooltipPlacement: 'top',
|
||||
})
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
### `copy()`
|
||||
|
||||
Copies text and triggers feedback.
|
||||
|
||||
```js
|
||||
import { Clipboard as Copy } from '@tabler/core'
|
||||
|
||||
const instance = Copy.getOrCreateInstance(el)
|
||||
instance.copy()
|
||||
```
|
||||
|
||||
### `reset()`
|
||||
|
||||
Resets UI back to the default state (useful when `data-bs-copy-reset="false"`).
|
||||
|
||||
```js
|
||||
import { Clipboard as Copy } from '@tabler/core'
|
||||
|
||||
const instance = Copy.getOrCreateInstance(el)
|
||||
instance.reset()
|
||||
```
|
||||
|
||||
### `dispose()`
|
||||
|
||||
Destroys the instance and clears timers.
|
||||
|
||||
```js
|
||||
import { Clipboard as Copy } from '@tabler/core'
|
||||
|
||||
const instance = Copy.getOrCreateInstance(el)
|
||||
instance.dispose()
|
||||
```
|
||||
|
||||
### `Copy.getInstance(element)`
|
||||
|
||||
Returns the existing instance for an element, or `null` if none exists.
|
||||
|
||||
```js
|
||||
import { Clipboard as Copy } from '@tabler/core'
|
||||
|
||||
Copy.getInstance(el)
|
||||
```
|
||||
|
||||
### `Copy.getOrCreateInstance(element, config?)`
|
||||
|
||||
Returns an existing instance or creates a new one.
|
||||
|
||||
```js
|
||||
import { Clipboard as Copy } from '@tabler/core'
|
||||
|
||||
Copy.getOrCreateInstance(el)
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
Copy emits Bootstrap-style events on the trigger element.
|
||||
|
||||
| Event | Description | Cancelable |
|
||||
|---|---|---|
|
||||
| `copy.bs.copy` | Fired immediately before copying. Access `event.detail.text` and `event.detail.target`. | **Yes** (call `event.preventDefault()`) |
|
||||
| `copied.bs.copy` | Fired after a successful copy. | No |
|
||||
| `copyfail.bs.copy` | Fired when copying fails (no text / clipboard error). `event.detail.reason` describes the cause. | No |
|
||||
| `reset.bs.copy` | Fired when the UI resets back to default. | No |
|
||||
|
||||
### Event example
|
||||
|
||||
```js
|
||||
document.addEventListener('copy.bs.copy', (e) => {
|
||||
if (!e.detail.text) e.preventDefault()
|
||||
})
|
||||
|
||||
document.addEventListener('copied.bs.copy', (e) => {
|
||||
console.log('Copied:', e.detail.text)
|
||||
})
|
||||
|
||||
document.addEventListener('copyfail.bs.copy', (e) => {
|
||||
console.warn('Copy failed:', e.detail.reason)
|
||||
})
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Copy from an input group
|
||||
|
||||
{% capture html -%}
|
||||
<div class="input-group" style="max-width: 460px;">
|
||||
<input id="invite-group" class="form-control" value="INVITE-123" readonly>
|
||||
<button class="btn" type="button" data-bs-toggle="copy" data-bs-copy-target="#invite-group">
|
||||
<span data-bs-copy-default>Copy</span>
|
||||
<span data-bs-copy-success hidden>Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
{%- endcapture -%}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
### Copy a code snippet
|
||||
|
||||
{% capture html -%}
|
||||
<div class="card" style="max-width: 720px;">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>Install</strong>
|
||||
<button type="button" class="btn btn-sm" data-bs-toggle="copy" data-bs-copy-target="#snippet">
|
||||
<span data-bs-copy-default>Copy</span>
|
||||
<span data-bs-copy-success hidden>Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre id="snippet" class="mb-0"><code>npm install @tabler/core</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
{%- endcapture -%}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
### Tooltip feedback
|
||||
|
||||
{% capture html -%}
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
data-bs-toggle="copy"
|
||||
data-bs-copy-text="sk_live_123"
|
||||
data-bs-copy-feedback="tooltip"
|
||||
data-bs-copy-tooltip="Copied!"
|
||||
data-bs-copy-timeout="900"
|
||||
>
|
||||
Copy token
|
||||
</button>
|
||||
{%- endcapture -%}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Prefer using a `<button type="button">` for triggers.
|
||||
- If you use an `<a>` as a trigger, set `role="button"` and ensure it is keyboard accessible.
|
||||
- Consider adding `aria-live="polite"` to the trigger to announce changes for assistive tech.
|
||||
- In swap mode, the plugin toggles `hidden` on the feedback elements; ensure the success text is meaningful (e.g. “Copied”).
|
||||
|
||||
## Browser support notes
|
||||
|
||||
- Clipboard API (`navigator.clipboard`) typically requires a **secure context** (HTTPS) and user interaction.
|
||||
- Fallback (`document.execCommand('copy')`) may be inconsistent across older browsers and restricted environments.
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
---
|
||||
title: Countup
|
||||
summary: A countup element is used to display numerical data in an interesting way and make the interface more interactive.
|
||||
docs-libs: countup
|
||||
description: Display numbers dynamically with countups.
|
||||
---
|
||||
|
||||
The countup component is used to display numbers dynamically. It is a great way to make the interface more interactive and engaging. The countup component is a simple and easy way to animate numbers in your application.
|
||||
|
||||
To be able to use the countup in your application you will need to install the countup.js dependency with `npm install countup.js`.
|
||||
The countup component is built into Tabler and works similar to Bootstrap components. It animates numbers dynamically, making the interface more interactive and engaging.
|
||||
|
||||
For more advanced features of countups, see the demo on the [countUp.js website](https://inorganik.github.io/countUp.js/).
|
||||
|
||||
## Basic usage
|
||||
|
||||
To create a countup, add `data-countup` to any HTML text tag and specify the number which is to be reached. The animation will be triggered as soon as the number enters the viewport.
|
||||
The easiest way to use countup is through the Data API. Add the `data-countup` attribute to any HTML text element and specify the number which is to be reached. The animation will be triggered as soon as the number enters the viewport.
|
||||
|
||||
```html
|
||||
<h1 data-countup>30000</h1>
|
||||
@@ -120,3 +117,64 @@ Set the countup suffix using `suffix` and specifying the suffix you want to add,
|
||||
<h1 data-countup='{"suffix":"‰"}'>300</h1>
|
||||
{%- endcapture %}
|
||||
{% include "docs/example.html" html=html vertical separated %}
|
||||
|
||||
## Usage
|
||||
|
||||
### Via data attributes
|
||||
|
||||
Add `data-countup` to any HTML text element to automatically initialize countup. You can pass options as JSON:
|
||||
|
||||
```html
|
||||
<h1 data-countup>30000</h1>
|
||||
<h1 data-countup='{"duration":4,"prefix":"$"}'>30000</h1>
|
||||
```
|
||||
|
||||
### Via JavaScript
|
||||
|
||||
Initialize countup programmatically using the `Countup` class:
|
||||
|
||||
```javascript
|
||||
import { Countup } from '@tabler/core'
|
||||
|
||||
// Get or create instance
|
||||
const element = document.querySelector('[data-countup]')
|
||||
const countup = Countup.getOrCreateInstance(element)
|
||||
countup.init()
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `init()` | Initialize countup on the element. |
|
||||
| `update()` | Update countup when the target value changes programmatically. |
|
||||
| `dispose()` | Destroy countup instance. |
|
||||
| `getInstance(element)` | *Static* method which allows you to get the countup instance associated with a DOM element. |
|
||||
| `getOrCreateInstance(element)` | *Static* method which allows you to get the countup instance associated with a DOM element, or create a new one in case it wasn't initialized. |
|
||||
|
||||
#### Example: Update after value change
|
||||
|
||||
```javascript
|
||||
import { Countup } from '@tabler/core'
|
||||
|
||||
const element = document.querySelector('[data-countup]')
|
||||
const countup = Countup.getOrCreateInstance(element)
|
||||
countup.init()
|
||||
|
||||
// Later, when the target value changes programmatically
|
||||
element.innerHTML = '50000'
|
||||
countup.update()
|
||||
```
|
||||
|
||||
#### Example: Get existing instance
|
||||
|
||||
```javascript
|
||||
import { Countup } from '@tabler/core'
|
||||
|
||||
const element = document.querySelector('[data-countup]')
|
||||
const countup = Countup.getInstance(element)
|
||||
|
||||
if (countup) {
|
||||
countup.update()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,36 +5,90 @@ banner: icons
|
||||
description: Transition between two icons smoothly.
|
||||
---
|
||||
|
||||
## Default markup
|
||||
The switch-icon component is built into Tabler and works similar to Bootstrap components. It automatically toggles between two icons when clicked.
|
||||
|
||||
The icon transition is triggered by adding an `.active` class to the `switch-icon` component.
|
||||
## Structure
|
||||
|
||||
{% capture html -%}
|
||||
The switch-icon component requires a specific HTML structure:
|
||||
|
||||
```html
|
||||
<button class="switch-icon" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
{% include "ui/icon.html" icon="heart" %}
|
||||
<!-- Icon code here -->
|
||||
</span>
|
||||
<span class="switch-icon-b text-red">
|
||||
{% include "ui/icon.html" icon="heart" type="filled" %}
|
||||
<!-- Icon code here -->
|
||||
</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
**Required elements:**
|
||||
- `button` element with `switch-icon` class and `data-bs-toggle="switch-icon"` attribute
|
||||
- `switch-icon-a` span containing the first icon (shown by default)
|
||||
- `switch-icon-b` span containing the second icon (shown when `.active` class is added to the button)
|
||||
|
||||
The transition between icons is triggered by adding or removing the `.active` class on the button element.
|
||||
|
||||
## Basic usage
|
||||
|
||||
The easiest way to use switch-icon is through the Data API. Add the `data-bs-toggle="switch-icon"` attribute to a button element:
|
||||
|
||||
{% capture html -%}
|
||||
{% include "ui/switch-icon.html" icon="heart" icon-b-color="red" %}
|
||||
{%- endcapture %}
|
||||
{% include "docs/example.html" html=html %}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
## Switch animations
|
||||
|
||||
You can also add a fancy animation to add variety to your button. See demo below:
|
||||
You can also add a fancy animation to add variety to your button. Available animation variants:
|
||||
|
||||
{% capture html -%}
|
||||
<button class="switch-icon" data-bs-toggle="switch-icon">
|
||||
| Variant | Description |
|
||||
| --- | --- |
|
||||
| `fade` | Fade transition between icons. |
|
||||
| `scale` | Scale animation when switching icons. |
|
||||
| `flip` | Flip animation when switching icons. |
|
||||
| `slide-up` | Slide animation from bottom to top. |
|
||||
| `slide-down` | Slide animation from top to bottom. |
|
||||
| `slide-left` | Slide animation from right to left. |
|
||||
| `slide-right` | Slide animation from left to right. |
|
||||
| `slide-start` | Slide animation from end to start (RTL-aware). |
|
||||
| `slide-end` | Slide animation from start to end (RTL-aware). |
|
||||
|
||||
To add an animation, add the corresponding class to the button element. For example, to use the fade animation, add `switch-icon-fade` class:
|
||||
|
||||
```html
|
||||
<button class="switch-icon switch-icon-fade" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
{% include "ui/icon.html" icon="circle" %}
|
||||
<!-- Icon code here -->
|
||||
</span>
|
||||
<span class="switch-icon-b text-primary">
|
||||
{% include "ui/icon.html" icon="circle" type="filled" %}
|
||||
<span class="switch-icon-b text-red">
|
||||
<!-- Icon code here -->
|
||||
</span>
|
||||
</button>
|
||||
<button class="switch-icon switch-icon-fade" data-bs-toggle="switch-icon">
|
||||
```
|
||||
|
||||
See demo below:
|
||||
|
||||
{% capture html -%}
|
||||
{% include "ui/switch-icon.html" icon="circle" icon-b-color="primary" %}
|
||||
{% include "ui/switch-icon.html" icon="heart" variant="fade" icon-b-color="red" %}
|
||||
{% include "ui/switch-icon.html" icon="star" variant="scale" icon-b-color="yellow" %}
|
||||
{% include "ui/switch-icon.html" icon="thumb-up" variant="flip" icon-b-color="facebook" %}
|
||||
{% include "ui/switch-icon.html" icon="brand-twitter" icon-b="brand-twitter" variant="slide-up" icon-b-color="twitter" %}
|
||||
{% include "ui/switch-icon.html" icon="check" icon-b="x" variant="slide-left" icon-b-color="red" %}
|
||||
{% include "ui/switch-icon.html" icon="arrow-up" icon-b="arrow-down" variant="slide-down" icon-b-color="secondary" %}
|
||||
{% include "ui/switch-icon.html" icon="car" icon-b="scooter" variant="slide-end" icon-b-color="secondary" %}
|
||||
{%- endcapture %}
|
||||
{% include "docs/example.html" html=html centered %}
|
||||
|
||||
## Usage
|
||||
|
||||
### Via data attributes
|
||||
|
||||
Add `data-bs-toggle="switch-icon"` to a button element to automatically initialize switch-icon:
|
||||
|
||||
```html
|
||||
<button class="switch-icon" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
{% include "ui/icon.html" icon="heart" %}
|
||||
</span>
|
||||
@@ -42,178 +96,77 @@ You can also add a fancy animation to add variety to your button. See demo below
|
||||
{% include "ui/icon.html" icon="heart" type="filled" %}
|
||||
</span>
|
||||
</button>
|
||||
<button class="switch-icon switch-icon-scale" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
{% include "ui/icon.html" icon="star" %}
|
||||
</span>
|
||||
<span class="switch-icon-b text-yellow">
|
||||
{% include "ui/icon.html" icon="star" type="filled" %}
|
||||
</span>
|
||||
</button>
|
||||
<button class="switch-icon switch-icon-flip" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
{% include "ui/icon.html" icon="thumb-up" %}
|
||||
</span>
|
||||
<span class="switch-icon-b text-facebook">
|
||||
{% include "ui/icon.html" icon="thumb-up" type="filled" %}
|
||||
</span>
|
||||
</button>
|
||||
<button class="switch-icon switch-icon-slide-up" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="switch-icon-b text-twitter">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="switch-icon switch-icon-slide-left" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M5 12l5 5l10 -10" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="switch-icon-b text-red">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="switch-icon switch-icon-slide-down" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="18" y1="13" x2="12" y2="19" />
|
||||
<line x1="6" y1="13" x2="12" y2="19" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="switch-icon-b text-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="18" y1="11" x2="12" y2="5" />
|
||||
<line x1="6" y1="11" x2="12" y2="5" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="switch-icon switch-icon-slide-end" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="7" cy="17" r="2" />
|
||||
<circle cx="17" cy="17" r="2" />
|
||||
<path d="M5 17h-2v-6l2 -5h9l4 5h1a2 2 0 0 1 2 2v4h-2m-4 0h-6m-6 -6h15m-6 0v-5" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="switch-icon-b text-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="18" cy="17" r="2" />
|
||||
<circle cx="6" cy="17" r="2" />
|
||||
<path d="M8 17h5a6 6 0 0 1 5 -5v-5a2 2 0 0 0 -2 -2h-1" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
{%- endcapture %}
|
||||
{% include "docs/example.html" html=html %}
|
||||
```
|
||||
|
||||
### Via JavaScript
|
||||
|
||||
Initialize switch-icon programmatically using the `SwitchIcon` class:
|
||||
|
||||
```javascript
|
||||
import { SwitchIcon } from '@tabler/core'
|
||||
|
||||
// Get or create instance
|
||||
const button = document.querySelector('.switch-icon')
|
||||
const switchIcon = SwitchIcon.getOrCreateInstance(button)
|
||||
switchIcon.init()
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `init()` | Initialize switch-icon on the element. |
|
||||
| `toggle()` | Toggle active state of the switch-icon. |
|
||||
| `show()` | Activate the switch-icon (add `.active` class). |
|
||||
| `hide()` | Deactivate the switch-icon (remove `.active` class). |
|
||||
| `isActive()` | Check if switch-icon is currently active. |
|
||||
| `dispose()` | Destroy switch-icon instance. |
|
||||
| `getInstance(element)` | *Static* method which allows you to get the switch-icon instance associated with a DOM element. |
|
||||
| `getOrCreateInstance(element)` | *Static* method which allows you to get the switch-icon instance associated with a DOM element, or create a new one in case it wasn't initialized. |
|
||||
|
||||
#### Example: Toggle programmatically
|
||||
|
||||
```javascript
|
||||
import { SwitchIcon } from '@tabler/core'
|
||||
|
||||
const button = document.querySelector('.switch-icon')
|
||||
const switchIcon = SwitchIcon.getOrCreateInstance(button)
|
||||
switchIcon.init()
|
||||
|
||||
// Later, toggle programmatically
|
||||
switchIcon.toggle()
|
||||
```
|
||||
|
||||
#### Example: Show/hide programmatically
|
||||
|
||||
```javascript
|
||||
import { SwitchIcon } from '@tabler/core'
|
||||
|
||||
const button = document.querySelector('.switch-icon')
|
||||
const switchIcon = SwitchIcon.getOrCreateInstance(button)
|
||||
switchIcon.init()
|
||||
|
||||
// Activate
|
||||
switchIcon.show()
|
||||
|
||||
// Deactivate
|
||||
switchIcon.hide()
|
||||
|
||||
// Check state
|
||||
if (switchIcon.isActive()) {
|
||||
console.log('Switch icon is active')
|
||||
}
|
||||
```
|
||||
|
||||
#### Example: Get existing instance
|
||||
|
||||
```javascript
|
||||
import { SwitchIcon } from '@tabler/core'
|
||||
|
||||
const button = document.querySelector('.switch-icon')
|
||||
const switchIcon = SwitchIcon.getInstance(button)
|
||||
|
||||
if (switchIcon) {
|
||||
switchIcon.toggle()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,34 +2,15 @@
|
||||
title: Input mask
|
||||
summary: An input mask is used to clarify the input format required in a given field and is helpful for users, removing confusion and reducing the number of validation errors.
|
||||
description: Clarify input formats for users.
|
||||
docs-libs: imask
|
||||
---
|
||||
|
||||
## Installation
|
||||
The input mask component is built into Tabler and works similar to Bootstrap components. It automatically applies input masks to form fields to clarify the required format.
|
||||
|
||||
To be able to use the input mask in your application you will need to install the imask dependency. You can do this by running the following command:
|
||||
For more advanced features of input masks, see the [IMask documentation](https://imask.js.org/guide.html#masked-input).
|
||||
|
||||
{% include "docs/tabs-package.html" name="imask" %}
|
||||
## Basic usage
|
||||
|
||||
And import or require:
|
||||
|
||||
```javascript
|
||||
import IMask from 'imask';
|
||||
```
|
||||
|
||||
You can also use the CDN link to include the script in your project:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/imask"></script>
|
||||
```
|
||||
|
||||
If you struggle with the installation, you can find more information in the [IMask documentation](https://imask.js.org/guide.html#installation).
|
||||
|
||||
## Default markup
|
||||
|
||||
Use an input mask in the fields where users have to enter their phone number, to make the formatting rules clear and help them avoid confusion.
|
||||
|
||||
To create an input mask, add the `data-mask` attribute to the input element:
|
||||
The easiest way to use input mask is through the Data API. Add the `data-mask` attribute to the input element:
|
||||
|
||||
```html
|
||||
<input
|
||||
@@ -60,4 +41,89 @@ Look at the example below to see how the input mask works:
|
||||
|
||||
## More examples
|
||||
|
||||
If you need more examples of input masks, you can find them in the [IMask documentation](https://imask.js.org/guide.html#masked-input).
|
||||
If you need more examples of input masks, you can find them in the [IMask documentation](https://imask.js.org/guide.html#masked-input).
|
||||
|
||||
## Usage
|
||||
|
||||
### Via data attributes
|
||||
|
||||
Add `data-mask` to an input element to automatically initialize input mask. You can also add `data-mask-visible="true"` to show the mask placeholder:
|
||||
|
||||
```html
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
data-mask="(00) 0000-0000"
|
||||
data-mask-visible="true"
|
||||
placeholder="(00) 0000-0000"
|
||||
autocomplete="off"
|
||||
/>
|
||||
```
|
||||
|
||||
### Via JavaScript
|
||||
|
||||
Initialize input mask programmatically using the `InputMask` class:
|
||||
|
||||
```javascript
|
||||
import { InputMask } from '@tabler/core'
|
||||
|
||||
// Get or create instance
|
||||
const input = document.querySelector('[data-mask]')
|
||||
const inputMask = InputMask.getOrCreateInstance(input)
|
||||
inputMask.init()
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `init()` | Initialize input mask on the element. |
|
||||
| `update(mask?, options?)` | Update input mask when mask or options change programmatically. |
|
||||
| `getValue()` | Get the current masked value. |
|
||||
| `getUnmaskedValue()` | Get the current unmasked value. |
|
||||
| `dispose()` | Destroy input mask instance. |
|
||||
| `getInstance(element)` | *Static* method which allows you to get the input mask instance associated with a DOM element. |
|
||||
| `getOrCreateInstance(element)` | *Static* method which allows you to get the input mask instance associated with a DOM element, or create a new one in case it wasn't initialized. |
|
||||
|
||||
#### Example: Update mask programmatically
|
||||
|
||||
```javascript
|
||||
import { InputMask } from '@tabler/core'
|
||||
|
||||
const input = document.querySelector('[data-mask]')
|
||||
const inputMask = InputMask.getOrCreateInstance(input)
|
||||
inputMask.init()
|
||||
|
||||
// Later, update the mask
|
||||
inputMask.update('000-000-0000')
|
||||
```
|
||||
|
||||
#### Example: Get values
|
||||
|
||||
```javascript
|
||||
import { InputMask } from '@tabler/core'
|
||||
|
||||
const input = document.querySelector('[data-mask]')
|
||||
const inputMask = InputMask.getOrCreateInstance(input)
|
||||
inputMask.init()
|
||||
|
||||
// Get masked value (e.g., "(12) 3456-7890")
|
||||
const maskedValue = inputMask.getValue()
|
||||
|
||||
// Get unmasked value (e.g., "1234567890")
|
||||
const unmaskedValue = inputMask.getUnmaskedValue()
|
||||
```
|
||||
|
||||
#### Example: Get existing instance
|
||||
|
||||
```javascript
|
||||
import { InputMask } from '@tabler/core'
|
||||
|
||||
const input = document.querySelector('[data-mask]')
|
||||
const inputMask = InputMask.getInstance(input)
|
||||
|
||||
if (inputMask) {
|
||||
const value = inputMask.getValue()
|
||||
console.log('Masked value:', value)
|
||||
}
|
||||
```
|
||||
354
docs/content/ui/getting-started/theme.md
Normal file
354
docs/content/ui/getting-started/theme.md
Normal file
@@ -0,0 +1,354 @@
|
||||
---
|
||||
title: Theme Customization
|
||||
summary: Tabler provides multiple ways to customize themes including HTML attributes, CSS classes, CSS custom properties, and JavaScript API. Learn how to control color mode, primary colors, fonts, base colors, and border radius to create a personalized user experience.
|
||||
description: Customize Tabler themes using attributes, CSS, and JavaScript.
|
||||
bootstrapLink: customize/color-modes/
|
||||
---
|
||||
|
||||
Tabler offers flexible theme customization through multiple methods. You can control the color mode (light/dark), primary color, font family, base color scheme, and border radius using HTML attributes, CSS classes, CSS custom properties, or the JavaScript API.
|
||||
|
||||
## Color Mode (Light/Dark)
|
||||
|
||||
### Using HTML Attributes
|
||||
|
||||
The most common way to control the color mode is by setting the `data-bs-theme` attribute on the `<html>` element:
|
||||
|
||||
```html
|
||||
<html data-bs-theme="dark">
|
||||
<!-- Dark mode content -->
|
||||
</html>
|
||||
```
|
||||
|
||||
Available values:
|
||||
- `light` - Light color mode (default)
|
||||
- `dark` - Dark color mode
|
||||
|
||||
### Using JavaScript
|
||||
|
||||
You can dynamically change the theme using JavaScript:
|
||||
|
||||
```javascript
|
||||
// Set dark mode
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark')
|
||||
|
||||
// Set light mode
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light')
|
||||
|
||||
// Remove attribute to use default (light)
|
||||
document.documentElement.removeAttribute('data-bs-theme')
|
||||
```
|
||||
|
||||
### Using URL Parameters
|
||||
|
||||
Tabler automatically reads theme settings from URL parameters. You can link to pages with specific themes:
|
||||
|
||||
```html
|
||||
<a href="?theme=dark">Switch to Dark Mode</a>
|
||||
<a href="?theme=light">Switch to Light Mode</a>
|
||||
```
|
||||
|
||||
### Using CSS Classes
|
||||
|
||||
You can also use CSS classes for theme control:
|
||||
|
||||
```html
|
||||
<html class="theme-dark">
|
||||
<!-- Dark mode content -->
|
||||
</html>
|
||||
```
|
||||
|
||||
## Primary Color
|
||||
|
||||
### Using HTML Attributes
|
||||
|
||||
Set the primary color using the `data-bs-theme-primary` attribute:
|
||||
|
||||
```html
|
||||
<html data-bs-theme-primary="red">
|
||||
<!-- Red primary color -->
|
||||
</html>
|
||||
```
|
||||
|
||||
Available values include: `blue`, `azure`, `indigo`, `purple`, `pink`, `red`, `orange`, `yellow`, `lime`, `green`, `teal`, `cyan`, and more.
|
||||
|
||||
### Using JavaScript
|
||||
|
||||
```javascript
|
||||
document.documentElement.setAttribute('data-bs-theme-primary', 'red')
|
||||
document.documentElement.setAttribute('data-bs-theme-primary', 'green')
|
||||
document.documentElement.setAttribute('data-bs-theme-primary', 'purple')
|
||||
```
|
||||
|
||||
### Using URL Parameters
|
||||
|
||||
```html
|
||||
<a href="?theme-primary=red">Red Theme</a>
|
||||
<a href="?theme-primary=green">Green Theme</a>
|
||||
```
|
||||
|
||||
## Base Color Scheme
|
||||
|
||||
### Using HTML Attributes
|
||||
|
||||
Control the gray color scheme using `data-bs-theme-base`:
|
||||
|
||||
```html
|
||||
<html data-bs-theme-base="slate">
|
||||
<!-- Slate gray scheme -->
|
||||
</html>
|
||||
```
|
||||
|
||||
Available values: `slate`, `gray`, `zinc`, `neutral`, `stone`
|
||||
|
||||
### Using JavaScript
|
||||
|
||||
```javascript
|
||||
document.documentElement.setAttribute('data-bs-theme-base', 'slate')
|
||||
document.documentElement.setAttribute('data-bs-theme-base', 'gray')
|
||||
document.documentElement.setAttribute('data-bs-theme-base', 'zinc')
|
||||
```
|
||||
|
||||
## Font Family
|
||||
|
||||
### Using HTML Attributes
|
||||
|
||||
Set the font family with `data-bs-theme-font`:
|
||||
|
||||
```html
|
||||
<html data-bs-theme-font="monospace">
|
||||
<!-- Monospace font -->
|
||||
</html>
|
||||
```
|
||||
|
||||
Available values: `sans-serif`, `serif`, `monospace`, `comic`
|
||||
|
||||
### Using JavaScript
|
||||
|
||||
```javascript
|
||||
document.documentElement.setAttribute('data-bs-theme-font', 'monospace')
|
||||
document.documentElement.setAttribute('data-bs-theme-font', 'serif')
|
||||
```
|
||||
|
||||
## Border Radius
|
||||
|
||||
### Using HTML Attributes
|
||||
|
||||
Control border radius factor with `data-bs-theme-radius`:
|
||||
|
||||
```html
|
||||
<html data-bs-theme-radius="2">
|
||||
<!-- Larger border radius -->
|
||||
</html>
|
||||
```
|
||||
|
||||
Available values: `0`, `0.5`, `1`, `1.5`, `2`
|
||||
|
||||
### Using JavaScript
|
||||
|
||||
```javascript
|
||||
document.documentElement.setAttribute('data-bs-theme-radius', '0')
|
||||
document.documentElement.setAttribute('data-bs-theme-radius', '1')
|
||||
document.documentElement.setAttribute('data-bs-theme-radius', '2')
|
||||
```
|
||||
|
||||
## Combining Multiple Settings
|
||||
|
||||
You can combine multiple theme attributes on the same element:
|
||||
|
||||
```html
|
||||
<html
|
||||
data-bs-theme="dark"
|
||||
data-bs-theme-primary="red"
|
||||
data-bs-theme-base="slate"
|
||||
data-bs-theme-font="monospace"
|
||||
data-bs-theme-radius="2">
|
||||
<!-- Custom themed page -->
|
||||
</html>
|
||||
```
|
||||
|
||||
Or using JavaScript:
|
||||
|
||||
```javascript
|
||||
const html = document.documentElement
|
||||
html.setAttribute('data-bs-theme', 'dark')
|
||||
html.setAttribute('data-bs-theme-primary', 'red')
|
||||
html.setAttribute('data-bs-theme-base', 'slate')
|
||||
html.setAttribute('data-bs-theme-font', 'monospace')
|
||||
html.setAttribute('data-bs-theme-radius', '2')
|
||||
```
|
||||
|
||||
## Using CSS Custom Properties
|
||||
|
||||
Tabler uses CSS custom properties (variables) that you can override directly:
|
||||
|
||||
```html
|
||||
<style>
|
||||
:root {
|
||||
--tblr-primary: #d63939; /* Red */
|
||||
--tblr-body-font-family: 'Courier New', monospace;
|
||||
--tblr-border-radius-scale: 2;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
You can also target specific theme modes:
|
||||
|
||||
```css
|
||||
[data-bs-theme='dark'] {
|
||||
--tblr-primary: #ff6b6b;
|
||||
--tblr-body-bg: #1a1a1a;
|
||||
}
|
||||
```
|
||||
|
||||
## Using JavaScript API
|
||||
|
||||
Tabler provides a convenient JavaScript API for theme manipulation. The API automatically handles DOM updates, localStorage persistence, and URL updates.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<script src="{{ cdnUrl }}/dist/js/tabler.min.js"></script>
|
||||
<script>
|
||||
Tabler.setTheme('dark')
|
||||
Tabler.setPrimary('red')
|
||||
Tabler.setBase('slate')
|
||||
Tabler.setFont('monospace')
|
||||
Tabler.setRadius('2')
|
||||
</script>
|
||||
```
|
||||
|
||||
### Available Methods
|
||||
|
||||
- `Tabler.setTheme(value)` - Set color mode (`light` or `dark`)
|
||||
- `Tabler.setPrimary(value)` - Set primary color
|
||||
- `Tabler.setBase(value)` - Set base color scheme
|
||||
- `Tabler.setFont(value)` - Set font family
|
||||
- `Tabler.setRadius(value)` - Set border radius factor
|
||||
- `Tabler.reset()` - Reset all settings to defaults
|
||||
- `Tabler.getConfig()` - Get current theme configuration
|
||||
|
||||
### Example: Theme Switcher
|
||||
|
||||
```html
|
||||
<button onclick="Tabler.setTheme('dark')">Dark Mode</button>
|
||||
<button onclick="Tabler.setTheme('light')">Light Mode</button>
|
||||
<button onclick="Tabler.setPrimary('red')">Red Theme</button>
|
||||
<button onclick="Tabler.reset()">Reset</button>
|
||||
```
|
||||
|
||||
## Persistence with localStorage
|
||||
|
||||
Tabler automatically saves theme settings to `localStorage` when using the JavaScript API. Settings are stored with keys like:
|
||||
- `tabler-theme`
|
||||
- `tabler-theme-primary`
|
||||
- `tabler-theme-base`
|
||||
- `tabler-theme-font`
|
||||
- `tabler-theme-radius`
|
||||
|
||||
You can manually read and write to localStorage:
|
||||
|
||||
```javascript
|
||||
// Save theme setting
|
||||
localStorage.setItem('tabler-theme', 'dark')
|
||||
|
||||
// Read theme setting
|
||||
const theme = localStorage.getItem('tabler-theme') || 'light'
|
||||
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||
```
|
||||
|
||||
## Scoped Themes
|
||||
|
||||
You can apply themes to specific elements instead of the entire page:
|
||||
|
||||
```html
|
||||
<div data-bs-theme="dark">
|
||||
<div class="card">
|
||||
<!-- This card uses dark theme -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-bs-theme="light">
|
||||
<div class="card">
|
||||
<!-- This card uses light theme -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete example showing different ways to control themes:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Theme Customization</title>
|
||||
<link rel="stylesheet" href="{{ cdnUrl }}/dist/css/tabler.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-xl">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Theme Customization</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3>Method 1: Direct Attribute Manipulation</h3>
|
||||
<button
|
||||
class="btn"
|
||||
onclick="document.documentElement.setAttribute('data-bs-theme', 'dark')">
|
||||
Set Dark Mode
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
onclick="document.documentElement.setAttribute('data-bs-theme', 'light')">
|
||||
Set Light Mode
|
||||
</button>
|
||||
|
||||
<h3 class="mt-4">Method 2: Using Tabler API</h3>
|
||||
<button class="btn" onclick="Tabler.setTheme('dark')">
|
||||
Dark Mode (API)
|
||||
</button>
|
||||
<button class="btn" onclick="Tabler.setPrimary('red')">
|
||||
Red Primary (API)
|
||||
</button>
|
||||
<button class="btn" onclick="Tabler.reset()">
|
||||
Reset (API)
|
||||
</button>
|
||||
|
||||
<h3 class="mt-4">Method 3: URL Parameters</h3>
|
||||
<a href="?theme=dark&theme-primary=red" class="btn">
|
||||
Dark + Red via URL
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ cdnUrl }}/dist/js/tabler.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Initialize on Load**: Apply saved theme settings when the page loads
|
||||
2. **User Preferences**: Consider saving user preferences to your backend
|
||||
3. **Smooth Transitions**: Add CSS transitions for smoother theme changes
|
||||
4. **Accessibility**: Ensure theme changes don't break accessibility features
|
||||
5. **Consistency**: Use the same method (API, attributes, or CSS) throughout your application
|
||||
|
||||
## Bootstrap Documentation
|
||||
|
||||
Tabler's theme system is built on top of Bootstrap's color mode and CSS variables system. For more information, see:
|
||||
|
||||
- [Bootstrap Color Modes](https://getbootstrap.com/docs/5.3/customize/color-modes/) - Learn about Bootstrap's dark mode implementation
|
||||
- [Bootstrap CSS Variables](https://getbootstrap.com/docs/5.3/customize/css-variables/) - Understand how CSS custom properties work in Bootstrap
|
||||
- [Bootstrap Theming](https://getbootstrap.com/docs/5.3/customize/overview/) - General theming and customization guide
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Customize Tabler](/ui/getting-started/customize) - Learn about CSS customization
|
||||
- [Installation](/ui/getting-started/installation) - Set up Tabler in your project
|
||||
- [Colors](/ui/base/colors) - Understand Tabler's color system
|
||||
@@ -8,6 +8,7 @@
|
||||
"clean": "turbo clean",
|
||||
"bundlewatch": "turbo bundlewatch",
|
||||
"type-check": "turbo type-check",
|
||||
"test": "turbo test",
|
||||
"version": "changeset version",
|
||||
"publish": "changeset publish",
|
||||
"reformat-md": "tsx .build/reformat-mdx.ts",
|
||||
|
||||
1132
pnpm-lock.yaml
generated
1132
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
title: Form elements
|
||||
page-header: Form elements
|
||||
page-menu: form.elements
|
||||
page-libs: [nouislider, autosize, tabler-flags, tabler-payments, litepicker, tom-select, imask]
|
||||
page-libs: [nouislider, tabler-flags, tabler-payments, litepicker, tom-select, imask]
|
||||
layout: default
|
||||
permalink: form-elements.html
|
||||
---
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
{% unless include.hide-code %}
|
||||
<div class="position-relative">
|
||||
<a class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-clipboard-text="{{ include.html | escape_attribute }}">
|
||||
{% include "ui/icon.html" icon="clipboard" %}
|
||||
{% include "ui/icon.html" icon="check" class="d-none" %}
|
||||
<a class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-bs-toggle="copy" data-bs-copy-text="{{ include.html | escape_attribute }}">
|
||||
{% include "ui/icon.html" icon="clipboard" attributes="data-bs-copy-default" %}
|
||||
{% include "ui/icon.html" icon="check" attributes="data-bs-copy-success" class="d-none" %}
|
||||
</a>
|
||||
|
||||
```html
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
<script>
|
||||
/*
|
||||
This script handles the theme settings offcanvas functionality
|
||||
It saves the selected settings to localStorage and applies them to the document
|
||||
It uses the Tabler API to save the selected settings and apply them to the document
|
||||
It also updates the URL with the selected settings as query parameters
|
||||
It also has a reset button to clear all settings and revert to default
|
||||
|
||||
@@ -127,9 +127,22 @@
|
||||
var form = document.getElementById("offcanvas-settings")
|
||||
var resetButton = document.getElementById("reset-changes")
|
||||
|
||||
// Wait for Tabler to be available
|
||||
var waitForTabler = function (callback) {
|
||||
if (window.Tabler) {
|
||||
callback()
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
waitForTabler(callback)
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
var checkItems = function () {
|
||||
var config = window.Tabler ? window.Tabler.getConfig() : themeConfig
|
||||
|
||||
for (var key in themeConfig) {
|
||||
var value = window.localStorage["tabler-" + key] || themeConfig[key]
|
||||
var value = config[key] || themeConfig[key]
|
||||
|
||||
if (!!value) {
|
||||
var radios = form.querySelectorAll(`[name="${key}"]`)
|
||||
@@ -143,36 +156,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener("change", function (event) {
|
||||
var target = event.target,
|
||||
name = target.name,
|
||||
value = target.value
|
||||
waitForTabler(function () {
|
||||
form.addEventListener("change", function (event) {
|
||||
var target = event.target,
|
||||
name = target.name,
|
||||
value = target.value
|
||||
|
||||
for (var key in themeConfig) {
|
||||
if (name === key) {
|
||||
document.documentElement.setAttribute("data-bs-" + key, value)
|
||||
window.localStorage.setItem("tabler-" + key, value)
|
||||
url.searchParams.set(key, value)
|
||||
if (name === "theme") {
|
||||
window.Tabler.setTheme(value)
|
||||
} else if (name === "theme-primary") {
|
||||
window.Tabler.setPrimary(value)
|
||||
} else if (name === "theme-base") {
|
||||
window.Tabler.setBase(value)
|
||||
} else if (name === "theme-font") {
|
||||
window.Tabler.setFont(value)
|
||||
} else if (name === "theme-radius") {
|
||||
window.Tabler.setRadius(value)
|
||||
}
|
||||
}
|
||||
|
||||
window.history.pushState({}, "", url)
|
||||
})
|
||||
url.searchParams.set(name, value)
|
||||
window.history.pushState({}, "", url)
|
||||
})
|
||||
|
||||
resetButton.addEventListener("click", function () {
|
||||
for (var key in themeConfig) {
|
||||
var value = themeConfig[key]
|
||||
document.documentElement.removeAttribute("data-bs-" + key)
|
||||
window.localStorage.removeItem("tabler-" + key)
|
||||
url.searchParams.delete(key)
|
||||
}
|
||||
resetButton.addEventListener("click", function () {
|
||||
window.Tabler.reset()
|
||||
checkItems()
|
||||
})
|
||||
|
||||
checkItems()
|
||||
|
||||
window.history.pushState({}, "", url)
|
||||
})
|
||||
|
||||
checkItems()
|
||||
})
|
||||
</script>
|
||||
{% endcapture_script %}
|
||||
@@ -1 +1 @@
|
||||
<input type="text" name="input-{{ include.name | default: 'mask' }}" class="form-control" data-mask="{{ include.mask | default: '00/00/0000' }}"{% if include.visible %} data-mask-visible="true"{% endif %}{% if include.placeholder %} placeholder="{{ include.placeholder }}"{% else %} placeholder="{{ include.mask }}"{% endif %}{% if include.reverse %} data-mask-reverse="true"{% endif %}autocomplete="off"/>
|
||||
<input type="text" name="input-{{ include.name | default: 'mask' }}" class="form-control" data-mask="{{ include.mask | default: '00/00/0000' }}"{% if include.visible %} data-mask-visible="true"{% endif %}{% if include.placeholder %} placeholder="{{ include.placeholder }}"{% else %} placeholder="{{ include.mask }}"{% endif %}{% if include.reverse %} data-mask-reverse="true"{% endif %} autocomplete="off"/>
|
||||
@@ -1 +1 @@
|
||||
<textarea class="form-control{% if include.class %} {{ include.class }}{% endif %}" data-bs-toggle="autosize" placeholder="{{ include.placeholder | default: 'Type something…' }}"{% if include.rows %} rows="{{ include.rows }}"{% endif %}></textarea>
|
||||
<textarea class="form-control{% if include.class %} {{ include.class }}{% endif %}" data-bs-autosize placeholder="{{ include.placeholder | default: 'Type something…' }}"{% if include.rows %} rows="{{ include.rows }}"{% endif %}></textarea>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{%- assign icon-type = include.type | default: "outline" -%}
|
||||
|
||||
{%- assign replace-to = "icon" -%}
|
||||
{%- assign icon-attributes = include.attributes | default: '' | strip -%}
|
||||
|
||||
{%- if include.class -%}
|
||||
{%- assign replace-to = replace-to | append: ' ' | append: include.class -%}
|
||||
@@ -22,10 +23,15 @@
|
||||
{%- assign replace-to = 'class="' | append: replace-to | append: '"' -%}
|
||||
|
||||
{%- if site.useIconfont -%}
|
||||
<i class="icon ti ti-{{ icon-name }}{% if include.color %} {{ include.color }}{% endif %}{% if include.class %} {{ include.class }}{% endif %}"></i>
|
||||
<i{% if icon-attributes %} {{ icon-attributes }}{% endif %} class="icon ti ti-{{ icon-name }}{% if include.color %} {{ include.color }}{% endif %}{% if include.class %} {{ include.class }}{% endif %}"></i>
|
||||
{%- elsif icons[icon-name] -%}
|
||||
<!-- Download SVG icon from http://tabler.io/icons/icon/{{ icon-name }} -->
|
||||
{% assign svg-icon = icons[icon-name].svg[icon-type] | default: '' -%}
|
||||
{%- assign svg-icon = svg-icon | replace: '<path stroke="none" d="M0 0h24v24H0z" fill="none"/>', '' -%}
|
||||
{{ svg-icon | replace_regex: 'class=\"[^"]+\"', replace-to | replace: 'class="', 'aria-hidden="true" focusable="false" class="' }}
|
||||
{%- assign svg-attributes = 'aria-hidden="true" focusable="false"' -%}
|
||||
{%- if icon-attributes -%}
|
||||
{%- assign svg-attributes = svg-attributes | append: ' ' | append: icon-attributes -%}
|
||||
{%- endif -%}
|
||||
{%- assign svg-attributes = svg-attributes | append: ' class="' -%}
|
||||
{{ svg-icon | replace_regex: 'class=\"[^"]+\"', replace-to | replace: 'class="', svg-attributes }}
|
||||
{%- endif -%}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% assign icon = include.icon | default: 'heart' %}
|
||||
{% assign icon-b = include.icon-b | default: icon | default: 'heart' %}
|
||||
{% assign icon-a-color = include.icon-a-color | default: 'muted' %}
|
||||
{% assign icon-b-color = include.icon-b-color | default: 'red' %}
|
||||
{% assign icon = include.icon | default: 'heart' -%}
|
||||
{% assign icon-b = include.icon-b | default: icon | default: 'heart' -%}
|
||||
{% assign icon-a-color = include.icon-a-color | default: 'secondary' -%}
|
||||
{% assign icon-b-color = include.icon-b-color | default: 'red' -%}
|
||||
|
||||
{% if icon == 'star' or icon == 'heart' %}
|
||||
{% assign icon-b-class = 'icon-filled' %}
|
||||
{% else %}
|
||||
{% assign icon-b-class = include.icon-b-class %}
|
||||
{% endif %}
|
||||
{% if icon == 'star' or icon == 'heart' or icon == 'circle' or icon == 'brand-twitter' -%}
|
||||
{% assign icon-b-class = 'icon-filled' -%}
|
||||
{% else -%}
|
||||
{% assign icon-b-class = include.icon-b-class -%}
|
||||
{% endif -%}
|
||||
|
||||
<button class="switch-icon{% if include.variant %} switch-icon-{{ include.variant }}{% endif %}{% if include.active %} active{% endif %}" data-bs-toggle="switch-icon">
|
||||
<span class="switch-icon-a text-{{ icon-a-color }}">
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
<!-- END PAGE BODY -->
|
||||
|
||||
{% for lib in libs -%}
|
||||
{% if docs-libs contains lib[0] or libs.global-libs contains lib[0] or lib[0] == "clipboard" -%}
|
||||
{% if docs-libs contains lib[0] or libs.global-libs contains lib[0] -%}
|
||||
{% for file in lib[1].js -%}
|
||||
<script
|
||||
src="{% if file contains 'http://' or file contains 'https://' %}{{ file | replace: 'GOOGLE_MAPS_KEY', google-maps-key }}{% else %}/dist/libs/{{ lib[1].npm }}/{% if environment != 'development' %}{{ file | replace: '@', '' }}{% else %}{{ file }}{% endif %}{% if environment != 'development' %}?{{ 'now' | date: '%s' }}{% endif %}{% endif %}"
|
||||
@@ -173,40 +173,6 @@
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const elements = document.querySelectorAll('[data-clipboard-text]');
|
||||
|
||||
elements.forEach(function (element) {
|
||||
const clipboard = new ClipboardJS(element, {
|
||||
text: function (trigger) {
|
||||
return element.getAttribute('data-clipboard-text');
|
||||
}
|
||||
});
|
||||
|
||||
clipboard.on('success', function (e) {
|
||||
e.clearSelection();
|
||||
e.trigger.classList.add('btn-success');
|
||||
e.trigger.classList.remove('btn-dark');
|
||||
e.trigger.children[0].classList.add('d-none');
|
||||
e.trigger.children[1].classList.remove('d-none');
|
||||
|
||||
setTimeout(function () {
|
||||
e.trigger.classList.remove('btn-success');
|
||||
e.trigger.classList.add('btn-dark');
|
||||
|
||||
e.trigger.children[0].classList.remove('d-none');
|
||||
e.trigger.children[1].classList.add('d-none');
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
clipboard.on('error', function (e) {
|
||||
console.error('Error copying text: ', e);
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="/dist/js/tabler{% if environment != 'development' %}.min{% endif %}.js"></script>
|
||||
<script src="/js/docs{% if environment != 'development' %}.min{% endif %}.js" defer></script>
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
"^type-check"
|
||||
],
|
||||
"cache": true
|
||||
},
|
||||
"test": {
|
||||
"outputs": [
|
||||
"coverage/**"
|
||||
],
|
||||
"cache": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user