UNPKG

admin-lte

Version:

Responsive open source admin dashboard and control panel.

300 lines (239 loc) 8.64 kB
/** * -------------------------------------------- * @file AdminLTE card-widget.ts * @description Card widget for AdminLTE. * @license MIT * -------------------------------------------- */ import { BaseComponent, dispatchCustomEvent } from './base-component' import { slideUp, slideDown } from './util/index' /** * Constants * ==================================================== */ const NAME = 'card-widget' const EVENT_KEY = `.lte.${NAME}` // Cancelable "before" events, dispatched on the card when an action starts. const EVENT_COLLAPSE = `collapse${EVENT_KEY}` const EVENT_EXPAND = `expand${EVENT_KEY}` const EVENT_REMOVE = `remove${EVENT_KEY}` // "After" events, dispatched on the card when the action has completed. const EVENT_COLLAPSED = `collapsed${EVENT_KEY}` const EVENT_EXPANDED = `expanded${EVENT_KEY}` const EVENT_REMOVED = `removed${EVENT_KEY}` const EVENT_MAXIMIZED = `maximized${EVENT_KEY}` const EVENT_MINIMIZED = `minimized${EVENT_KEY}` const CLASS_NAME_CARD = 'card' const CLASS_NAME_COLLAPSED = 'collapsed-card' const CLASS_NAME_COLLAPSING = 'collapsing-card' const CLASS_NAME_EXPANDING = 'expanding-card' const CLASS_NAME_WAS_COLLAPSED = 'was-collapsed' const CLASS_NAME_MAXIMIZED = 'maximized-card' const SELECTOR_DATA_REMOVE = '[data-lte-toggle="card-remove"]' const SELECTOR_DATA_COLLAPSE = '[data-lte-toggle="card-collapse"]' const SELECTOR_DATA_MAXIMIZE = '[data-lte-toggle="card-maximize"]' const SELECTOR_CARD = `.${CLASS_NAME_CARD}` const SELECTOR_CARD_BODY = '.card-body' const SELECTOR_CARD_FOOTER = '.card-footer' type Config = { animationSpeed: number; collapseTrigger: string; removeTrigger: string; maximizeTrigger: string; } const Default: Config = { animationSpeed: 500, collapseTrigger: SELECTOR_DATA_COLLAPSE, removeTrigger: SELECTOR_DATA_REMOVE, maximizeTrigger: SELECTOR_DATA_MAXIMIZE } class CardWidget extends BaseComponent { static get NAME(): string { return NAME } static getInstance(element: Element | null | undefined): CardWidget | null { return this._getInstance(element) as CardWidget | null } static getOrCreateInstance(element: HTMLElement, config: Partial<Config> = {}): CardWidget { return this.getInstance(element) ?? new this(element, config) } _parent: HTMLElement | undefined _config: Config constructor(element: HTMLElement, config: Partial<Config> = {}) { super(element) this._parent = element.closest(SELECTOR_CARD) as HTMLElement | undefined if (element.classList.contains(CLASS_NAME_CARD)) { this._parent = element } this._config = { ...Default, ...config } } collapse() { if (!this._parent) { return } if (dispatchCustomEvent(this._parent, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) { return } this._parent.classList.add(CLASS_NAME_COLLAPSING) this._parent.classList.remove(CLASS_NAME_EXPANDING) // Only target direct children to avoid affecting nested cards const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`) elm.forEach(el => { if (el instanceof HTMLElement) { slideUp(el, this._config.animationSpeed) } }) setTimeout(() => { // Guard against a stale timer: if expand() ran while this collapse // was still animating, the card must not end up marked collapsed. if (this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) { this._parent.classList.add(CLASS_NAME_COLLAPSED) this._parent.classList.remove(CLASS_NAME_COLLAPSING) dispatchCustomEvent(this._parent, EVENT_COLLAPSED) } }, this._config.animationSpeed) } expand() { if (!this._parent) { return } if (dispatchCustomEvent(this._parent, EVENT_EXPAND, { cancelable: true }).defaultPrevented) { return } this._parent.classList.add(CLASS_NAME_EXPANDING) this._parent.classList.remove(CLASS_NAME_COLLAPSING, CLASS_NAME_COLLAPSED) // Only target direct children to avoid affecting nested cards const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`) elm.forEach(el => { if (el instanceof HTMLElement) { slideDown(el, this._config.animationSpeed) } }) setTimeout(() => { // Same stale-timer guard as collapse(), in the other direction. if (this._parent?.classList.contains(CLASS_NAME_EXPANDING)) { this._parent.classList.remove(CLASS_NAME_EXPANDING) dispatchCustomEvent(this._parent, EVENT_EXPANDED) } }, this._config.animationSpeed) } remove() { if (!this._parent) { return } if (dispatchCustomEvent(this._parent, EVENT_REMOVE, { cancelable: true }).defaultPrevented) { return } const parent = this._parent slideUp(parent, this._config.animationSpeed) // Actually remove the card once the animation finishes — hiding it // would keep its form fields submitting and its ids in the document. // `removed` is dispatched just before detachment so it can still bubble. setTimeout(() => { dispatchCustomEvent(parent, EVENT_REMOVED) parent.remove() this.dispose() }, this._config.animationSpeed) } toggle() { // A card mid-collapse counts as collapsed so a second click during the // animation reverses it instead of being swallowed. if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED) || this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) { this.expand() return } this.collapse() } maximize() { if (this._parent) { this._parent.style.height = `${this._parent.offsetHeight}px` this._parent.style.width = `${this._parent.offsetWidth}px` this._parent.style.transition = 'all .15s' setTimeout(() => { const htmlTag = document.querySelector('html') if (htmlTag) { htmlTag.classList.add(CLASS_NAME_MAXIMIZED) } if (this._parent) { this._parent.classList.add(CLASS_NAME_MAXIMIZED) if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) { this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED) } dispatchCustomEvent(this._parent, EVENT_MAXIMIZED) } }, 150) } } minimize() { if (this._parent) { this._parent.style.height = 'auto' this._parent.style.width = 'auto' this._parent.style.transition = 'all .15s' setTimeout(() => { const htmlTag = document.querySelector('html') if (htmlTag) { htmlTag.classList.remove(CLASS_NAME_MAXIMIZED) } if (this._parent) { this._parent.classList.remove(CLASS_NAME_MAXIMIZED) if (this._parent?.classList.contains(CLASS_NAME_WAS_COLLAPSED)) { this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED) } dispatchCustomEvent(this._parent, EVENT_MINIMIZED) // Drop the inline sizing/transition once the restore transition has // finished so the card doesn't carry stale styles forever. setTimeout(() => { this._parent?.style.removeProperty('height') this._parent?.style.removeProperty('width') this._parent?.style.removeProperty('transition') }, 150) } }, 10) } } toggleMaximize() { if (this._parent?.classList.contains(CLASS_NAME_MAXIMIZED)) { this.minimize() return } this.maximize() } } /** * * Data Api implementation * ==================================================== * A single delegated listener on `document` handles all card toggles — so * cards inserted later (AJAX, Turbo Frames) work without re-initialisation, * and the listener itself survives Turbo's <body> swaps. */ document.addEventListener('click', event => { const target = event.target if (!(target instanceof Element)) { return } const collapseTrigger = target.closest(SELECTOR_DATA_COLLAPSE) const removeTrigger = target.closest(SELECTOR_DATA_REMOVE) const maximizeTrigger = target.closest(SELECTOR_DATA_MAXIMIZE) const trigger = collapseTrigger ?? removeTrigger ?? maximizeTrigger if (!trigger) { return } event.preventDefault() const card = trigger.closest(SELECTOR_CARD) as HTMLElement | null if (!card) { return } const widget = CardWidget.getOrCreateInstance(card) if (collapseTrigger) { widget.toggle() } else if (removeTrigger) { widget.remove() } else { widget.toggleMaximize() } }) export default CardWidget