UNPKG

@coreui/coreui-pro

Version:

The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team

949 lines (787 loc) 24.8 kB
/** * -------------------------------------------------------------------------- * CoreUI PRO time-picker.js * License (https://coreui.io/pro/license/) * -------------------------------------------------------------------------- */ import * as Popper from '@popperjs/core' import BaseComponent from './base-component.js' import EventHandler from './dom/event-handler.js' import Manipulator from './dom/manipulator.js' import SelectorEngine from './dom/selector-engine.js' import { defineJQueryPlugin, getElement, isRTL } from './util/index.js' import { convert12hTo24h, convert24hTo12h, getAmPm, getLocalizedTimePartials, isAmPm, isValidTime } from './util/time.js' /** * Constants */ const NAME = 'time-picker' const DATA_KEY = 'coreui.time-picker' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' const ENTER_KEY = 'Enter' const ESCAPE_KEY = 'Escape' const SPACE_KEY = 'Space' const TAB_KEY = 'Tab' const RIGHT_MOUSE_BUTTON = 2 const EVENT_CLICK = `click${EVENT_KEY}` const EVENT_HIDE = `hide${EVENT_KEY}` const EVENT_HIDDEN = `hidden${EVENT_KEY}` const EVENT_INPUT = 'input' const EVENT_KEYDOWN = `keydown${EVENT_KEY}` const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` const EVENT_SUBMIT = 'submit' const EVENT_TIME_CHANGE = `timeChange${EVENT_KEY}` const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}` const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}` const CLASS_NAME_BODY = 'time-picker-body' const CLASS_NAME_CLEANER = 'time-picker-cleaner' const CLASS_NAME_DISABLED = 'disabled' const CLASS_NAME_DROPDOWN = 'time-picker-dropdown' const CLASS_NAME_FOOTER = 'time-picker-footer' const CLASS_NAME_INDICATOR = 'time-picker-indicator' const CLASS_NAME_INLINE_ICON = 'time-picker-inline-icon' const CLASS_NAME_INLINE_SELECT = 'time-picker-inline-select' const CLASS_NAME_INPUT = 'time-picker-input' const CLASS_NAME_INPUT_GROUP = 'time-picker-input-group' const CLASS_NAME_IS_INVALID = 'is-invalid' const CLASS_NAME_IS_VALID = 'is-valid' const CLASS_NAME_ROLL = 'time-picker-roll' const CLASS_NAME_ROLL_COL = 'time-picker-roll-col' const CLASS_NAME_ROLL_CELL = 'time-picker-roll-cell' const CLASS_NAME_SELECTED = 'selected' const CLASS_NAME_SHOW = 'show' const CLASS_NAME_TIME_PICKER = 'time-picker' const CLASS_NAME_WAS_VALIDATED = 'was-validated' const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="time-picker"]:not(.disabled):not(:disabled)' const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}` const SELECTOR_WAS_VALIDATED = 'form.was-validated' const Default = { cancelButton: 'Cancel', cancelButtonClasses: ['btn', 'btn-sm', 'btn-ghost-primary'], cleaner: true, confirmButton: 'OK', confirmButtonClasses: ['btn', 'btn-sm', 'btn-primary'], container: false, disabled: false, footer: true, hours: null, indicator: true, inputReadOnly: false, invalid: false, locale: 'default', minutes: true, name: null, placeholder: 'Select time', required: true, seconds: true, size: null, time: null, type: 'dropdown', valid: false, variant: 'roll' } const DefaultType = { cancelButton: '(boolean|string)', cancelButtonClasses: '(array|string)', cleaner: 'boolean', confirmButton: '(boolean|string)', confirmButtonClasses: '(array|string)', container: '(string|element|boolean)', disabled: 'boolean', footer: 'boolean', hours: '(array|function|null)', indicator: 'boolean', inputReadOnly: 'boolean', invalid: 'boolean', locale: 'string', minutes: '(array|boolean|function)', name: '(string|null)', placeholder: 'string', required: 'boolean', seconds: '(array|boolean|function)', size: '(string|null)', time: '(date|string|null)', type: 'string', valid: 'boolean', variant: 'string' } /** * Class definition */ class TimePicker extends BaseComponent { constructor(element, config) { super(element) this._config = this._getConfig(config) this._date = this._convertStringToDate(this._config.time) this._initialDate = null this._ampm = this._date ? getAmPm(new Date(this._date), this._config.locale) : 'am' this._popper = null this._indicatorElement = null this._input = null this._menu = null this._timePickerBody = null this._localizedTimePartials = getLocalizedTimePartials( this._config.locale, this.ampm, this._config.hours, this._config.minutes, this._config.seconds ) this._createTimePicker() this._createTimePickerSelection() this._addEventListeners() this._setUpSelects() } // Getters static get Default() { return Default } static get DefaultType() { return DefaultType } static get NAME() { return NAME } // Public toggle() { return this._isShown() ? this.hide() : this.show() } show() { if (this._config.disabled || this._isShown()) { return } this._initialDate = new Date(this._date) EventHandler.trigger(this._element, EVENT_SHOW) this._element.classList.add(CLASS_NAME_SHOW) this._element.setAttribute('aria-expanded', true) if (this._config.container) { this._menu.classList.add(CLASS_NAME_SHOW) } EventHandler.trigger(this._element, EVENT_SHOWN) this._createPopper() } hide() { EventHandler.trigger(this._element, EVENT_HIDE) if (this._popper) { this._popper.destroy() } this._element.classList.remove(CLASS_NAME_SHOW) this._element.setAttribute('aria-expanded', 'false') if (this._config.container) { this._menu.classList.remove(CLASS_NAME_SHOW) } EventHandler.trigger(this._element, EVENT_HIDDEN) } dispose() { if (this._popper) { this._popper.destroy() } super.dispose() } cancel() { this._date = this._initialDate this._setInputValue(this._initialDate || '') this._timePickerBody.innerHTML = '' this.hide() this._createTimePickerSelection() this._emitChangeEvent(this._date) } clear() { this._date = null this._setInputValue('') this._timePickerBody.innerHTML = '' this._createTimePickerSelection() this._emitChangeEvent(this._date) } reset() { this._date = this._convertStringToDate(this._config.time) this._setInputValue(this._config.time) this._timePickerBody.innerHTML = '' this._createTimePickerSelection() this._emitChangeEvent(this._date) } update(config) { this._config = this._getConfig(config) this._date = this._convertStringToDate(this._config.time) this._ampm = this._date ? getAmPm(new Date(this._date), this._config.locale) : 'am' this._timePickerBody.innerHTML = '' this._createTimePickerSelection() this._setUpSelects() } // Private _addEventListeners() { EventHandler.on(this._indicatorElement, EVENT_CLICK, () => { if (!this._config.disabled) { this.toggle() } }) EventHandler.on(this._indicatorElement, EVENT_KEYDOWN, event => { if (!this._config.disabled && event.key === ENTER_KEY) { this.toggle() } }) EventHandler.on(this._togglerElement, EVENT_CLICK, event => { if (!this._config.disabled && event.target !== this._indicatorElement) { this.show() if (this._config.variant === 'roll') { this._setUpRolls(true) } if (this._config.variant === 'select') { this._setUpSelects() } } }) EventHandler.on(this._element, EVENT_KEYDOWN, event => { if (event.key === ESCAPE_KEY) { this.hide() } }) EventHandler.on(this._element, 'timeChange.coreui.time-picker', () => { if (this._config.variant === 'roll') { this._setUpRolls() } if (this._config.variant === 'select') { this._setUpSelects() } }) EventHandler.on(this._element, 'onCancelClick.coreui.picker', () => { this.cancel() }) EventHandler.on(this._input, EVENT_INPUT, event => { if (isValidTime(event.target.value)) { this._date = this._convertStringToDate(event.target.value) EventHandler.trigger(this._element, EVENT_TIME_CHANGE, { timeString: this._date ? this._date.toTimeString() : null, localeTimeString: this._date ? this._date.toLocaleTimeString() : null, date: this._date }) } }) if (this._config.type === 'dropdown') { EventHandler.on(this._input.form, EVENT_SUBMIT, () => { if (this._input.form.classList.contains(CLASS_NAME_WAS_VALIDATED)) { if (Number.isNaN(Date.parse(`1970-01-01 ${this._input.value}`))) { return this._element.classList.add(CLASS_NAME_IS_INVALID) } if (this._date instanceof Date) { return this._element.classList.add(CLASS_NAME_IS_VALID) } this._element.classList.add(CLASS_NAME_IS_INVALID) } }) } } _createTimePicker() { this._element.classList.add(CLASS_NAME_TIME_PICKER) Manipulator.setDataAttribute( this._element, 'toggle', CLASS_NAME_TIME_PICKER ) if (this._config.size) { this._element.classList.add(`time-picker-${this._config.size}`) } this._element.classList.toggle(CLASS_NAME_IS_VALID, this._config.valid) if (this._config.disabled) { this._element.classList.add(CLASS_NAME_DISABLED) } this._element.classList.toggle(CLASS_NAME_IS_INVALID, this._config.invalid) if (this._config.type === 'dropdown') { this._element.append(this._createTimePickerInputGroup()) const dropdownEl = document.createElement('div') dropdownEl.classList.add(CLASS_NAME_DROPDOWN) dropdownEl.append(this._createTimePickerBody()) if (this._config.footer || this._config.timepicker) { dropdownEl.append(this._createTimePickerFooter()) } const { container } = this._config if (container) { container.append(dropdownEl) } else { this._element.append(dropdownEl) } this._menu = dropdownEl } if (this._config.type === 'inline') { this._element.append(this._createTimePickerBody()) } } _createTimePickerInputGroup() { const inputGroupEl = document.createElement('div') inputGroupEl.classList.add(CLASS_NAME_INPUT_GROUP) const inputEl = document.createElement('input') inputEl.classList.add(CLASS_NAME_INPUT) inputEl.autocomplete = 'off' inputEl.disabled = this._config.disabled inputEl.placeholder = this._config.placeholder inputEl.readOnly = this._config.inputReadOnly inputEl.required = this._config.required inputEl.type = 'text' this._setInputValue(this._date || '', inputEl) if (this._config.name || this._element.id) { inputEl.name = this._config.name || `time-picker-${this._element.id}` } const events = ['change', 'keyup', 'paste'] for (const event of events) { inputEl.addEventListener(event, ({ target }) => { if (target.closest(SELECTOR_WAS_VALIDATED)) { if (Number.isNaN(Date.parse(`1970-01-01 ${target.value}`))) { this._element.classList.add(CLASS_NAME_IS_INVALID) this._element.classList.remove(CLASS_NAME_IS_VALID) return } if (this._date instanceof Date) { this._element.classList.add(CLASS_NAME_IS_VALID) this._element.classList.remove(CLASS_NAME_IS_INVALID) return } this._element.classList.add(CLASS_NAME_IS_INVALID) this._element.classList.remove(CLASS_NAME_IS_VALID) } }) } inputGroupEl.append(inputEl) if (this._config.indicator) { const inputGroupIndicatorEl = document.createElement('div') inputGroupIndicatorEl.classList.add(CLASS_NAME_INDICATOR) if (!this._config.disabled) { inputGroupIndicatorEl.tabIndex = 0 } inputGroupEl.append(inputGroupIndicatorEl) this._indicatorElement = inputGroupIndicatorEl } if (this._config.cleaner) { const inputGroupCleanerEl = document.createElement('div') inputGroupCleanerEl.classList.add(CLASS_NAME_CLEANER) inputGroupCleanerEl.addEventListener('click', event => { event.stopPropagation() this.clear() }) inputGroupEl.append(inputGroupCleanerEl) } this._input = inputEl this._togglerElement = inputGroupEl return inputGroupEl } _createTimePickerSelection() { if (this._config.variant === 'roll') { this._createTimePickerRoll() } if (this._config.variant === 'select') { this._createTimePickerInlineSelects() } } _createTimePickerBody() { const timePickerBodyEl = document.createElement('div') timePickerBodyEl.classList.add(CLASS_NAME_BODY) if (this._config.variant === 'roll') { timePickerBodyEl.classList.add(CLASS_NAME_ROLL) } this._timePickerBody = timePickerBodyEl return timePickerBodyEl } _createTimePickerInlineSelect(className, options) { const selectEl = document.createElement('select') selectEl.classList.add(CLASS_NAME_INLINE_SELECT, className) selectEl.disabled = this._config.disabled selectEl.addEventListener('change', event => this._handleTimeChange(className, event.target.value) ) for (const option of options) { const optionEl = document.createElement('option') optionEl.value = option.value optionEl.innerHTML = option.label selectEl.append(optionEl) } return selectEl } _createTimePickerInlineSelects() { const timeSeparatorEl = document.createElement('div') timeSeparatorEl.innerHTML = ':' this._timePickerBody.innerHTML = `<span class="${CLASS_NAME_INLINE_ICON}"></span>` this._timePickerBody.append( this._createTimePickerInlineSelect( 'hours', this._localizedTimePartials.listOfHours ) ) if (this._config.minutes) { this._timePickerBody.append( timeSeparatorEl.cloneNode(true), this._createTimePickerInlineSelect( 'minutes', this._localizedTimePartials.listOfMinutes ) ) } if (this._config.seconds) { this._timePickerBody.append( timeSeparatorEl, this._createTimePickerInlineSelect( 'seconds', this._localizedTimePartials.listOfSeconds ) ) } if (this._localizedTimePartials.hour12) { this._timePickerBody.append( this._createTimePickerInlineSelect( 'toggle', [ { value: 'am', label: 'AM' }, { value: 'pm', label: 'PM' } ], '_selectAmPm', this._ampm ) ) } } _createTimePickerRoll() { this._timePickerBody.append( this._createTimePickerRollCol( this._localizedTimePartials.listOfHours, 'hours' ) ) if (this._config.minutes) { this._timePickerBody.append( this._createTimePickerRollCol( this._localizedTimePartials.listOfMinutes, 'minutes' ) ) } if (this._config.seconds) { this._timePickerBody.append( this._createTimePickerRollCol( this._localizedTimePartials.listOfSeconds, 'seconds' ) ) } if (this._localizedTimePartials.hour12) { this._timePickerBody.append( this._createTimePickerRollCol( [ { value: 'am', label: 'AM' }, { value: 'pm', label: 'PM' } ], 'toggle', this._ampm ) ) } } _createTimePickerRollCol(options, part) { const timePickerRollColEl = document.createElement('div') timePickerRollColEl.classList.add(CLASS_NAME_ROLL_COL) for (const option of options) { const timePickerRollCellEl = document.createElement('div') timePickerRollCellEl.classList.add(CLASS_NAME_ROLL_CELL) timePickerRollCellEl.setAttribute('role', 'button') timePickerRollCellEl.tabIndex = 0 timePickerRollCellEl.innerHTML = option.label timePickerRollCellEl.addEventListener('click', () => { this._handleTimeChange(part, option.value) }) timePickerRollCellEl.addEventListener('keydown', event => { if (event.code === SPACE_KEY || event.key === ENTER_KEY) { event.preventDefault() this._handleTimeChange(part, option.value) } }) Manipulator.setDataAttribute(timePickerRollCellEl, part, option.value) timePickerRollColEl.append(timePickerRollCellEl) } return timePickerRollColEl } _createTimePickerFooter() { const footerEl = document.createElement('div') footerEl.classList.add(CLASS_NAME_FOOTER) if (this._config.cancelButton) { const cancelButtonEl = document.createElement('button') cancelButtonEl.classList.add( ...this._getButtonClasses(this._config.cancelButtonClasses) ) cancelButtonEl.type = 'button' cancelButtonEl.innerHTML = this._config.cancelButton cancelButtonEl.addEventListener('click', () => { this.cancel() }) footerEl.append(cancelButtonEl) } if (this._config.confirmButton) { const confirmButtonEl = document.createElement('button') confirmButtonEl.classList.add( ...this._getButtonClasses(this._config.confirmButtonClasses) ) confirmButtonEl.type = 'button' confirmButtonEl.innerHTML = this._config.confirmButton confirmButtonEl.addEventListener('click', () => { this.hide() }) footerEl.append(confirmButtonEl) } return footerEl } _emitChangeEvent(date) { this._input.dispatchEvent(new Event('change')) EventHandler.trigger(this._element, EVENT_TIME_CHANGE, { timeString: date === null ? null : date.toTimeString(), localeTimeString: date === null ? null : date.toLocaleTimeString(), date }) } _setUpRolls(initial = false) { for (const part of Array.from(['hours', 'minutes', 'seconds', 'toggle'])) { for (const element of SelectorEngine.find( `[data-coreui-${part}]`, this._element )) { if ( this._getPartOfTime(part) === Manipulator.getDataAttribute(element, part) ) { element.classList.add(CLASS_NAME_SELECTED) this._scrollTo(element.parentElement, element, initial) for (const sibling of element.parentElement.children) { // eslint-disable-next-line max-depth if (sibling !== element) { sibling.classList.remove(CLASS_NAME_SELECTED) } } } } } } _setInputValue(date, input = this._input) { input.value = date instanceof Date ? date.toLocaleTimeString(this._config.locale, { hour12: this._localizedTimePartials.hour12, hour: 'numeric', ...(this._config.minutes && { minute: 'numeric' }), ...(this._config.seconds && { second: 'numeric' }) }) : date } _setUpSelects() { for (const part of Array.from(['hours', 'minutes', 'seconds', 'toggle'])) { for (const element of SelectorEngine.find( `select.${part}`, this._element )) { if (this._getPartOfTime(part)) { element.value = this._getPartOfTime(part) } } } } _updateTimePicker() { this._element.innerHTML = '' this._createTimePicker() } _convertStringToDate(date) { return date ? (date instanceof Date ? date : new Date(`1970-01-01 ${date}`)) : null } _createPopper() { if (typeof Popper === 'undefined') { throw new TypeError( 'CoreUI\'s time picker require Popper (https://popper.js.org)' ) } const popperConfig = { modifiers: [ { name: 'preventOverflow', options: { boundary: 'clippingParents' } }, { name: 'offset', options: { offset: [0, 2] } } ], placement: isRTL() ? 'bottom-end' : 'bottom-start' } this._popper = Popper.createPopper( this._togglerElement, this._menu, popperConfig ) } _getButtonClasses(classes) { if (typeof classes === 'string') { return classes.split(' ') } return classes } _getPartOfTime(part) { if (this._date === null) { return null } if (part === 'hours') { return isAmPm(this._config.locale) ? convert24hTo12h(this._date.getHours()) : this._date.getHours() } if (part === 'minutes') { return this._date.getMinutes() } if (part === 'seconds') { return this._date.getSeconds() } if (part === 'toggle') { return getAmPm(new Date(this._date), this._config.locale) } } _handleTimeChange = (set, value) => { const _date = this._date || new Date('1970-01-01') if (set === 'toggle') { if (value === 'am') { this._ampm = 'am' _date.setHours(_date.getHours() - 12) } if (value === 'pm') { this._ampm = 'pm' _date.setHours(_date.getHours() + 12) } } if (set === 'hours') { if (isAmPm(this._config.locale)) { _date.setHours(convert12hTo24h(this._ampm, Number.parseInt(value, 10))) } else { _date.setHours(Number.parseInt(value, 10)) } } if (set === 'minutes') { _date.setMinutes(Number.parseInt(value, 10)) } if (set === 'seconds') { _date.setSeconds(Number.parseInt(value, 10)) } this._date = new Date(_date) if (this._input) { this._setInputValue(_date) this._input.dispatchEvent(new Event('change')) } EventHandler.trigger(this._element, EVENT_TIME_CHANGE, { timeString: _date.toTimeString(), localeTimeString: _date.toLocaleTimeString(), date: _date }) } _isShown() { return this._element.classList.contains(CLASS_NAME_SHOW) } _scrollTo(parent, children, initial = false) { parent.scrollTo({ top: children.offsetTop, behavior: initial ? 'instant' : 'smooth' }) } _configAfterMerge(config) { if (config.container === 'dropdown' || config.container === 'inline') { config.type = config.container } if (config.container === true) { config.container = document.body } if ( typeof config.container === 'object' || (typeof config.container === 'string' && config.container === 'dropdown' && config.container === 'inline') ) { config.container = getElement(config.container) } return config } // Static static timePickerInterface(element, config) { const data = TimePicker.getOrCreateInstance(element, config) if (typeof config === 'string') { if (typeof data[config] === 'undefined') { throw new TypeError(`No method named "${config}"`) } data[config]() } } static jQueryInterface(config) { return this.each(function () { const data = TimePicker.getOrCreateInstance(this, config) if (typeof config !== 'string') { return } if ( data[config] === undefined || config.startsWith('_') || config === 'constructor' ) { throw new TypeError(`No method named "${config}"`) } data[config](this) }) } static clearMenus(event) { if ( event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY) ) { return } const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN) for (const toggle of openToggles) { const context = TimePicker.getInstance(toggle) if (!context) { continue } const composedPath = event.composedPath() if (composedPath.includes(context._element)) { continue } const relatedTarget = { relatedTarget: context._element } if (event.type === 'click') { relatedTarget.clickEvent = event } context.hide() } } } /** * Data API implementation */ EventHandler.on(window, EVENT_LOAD_DATA_API, () => { const timePickers = SelectorEngine.find(SELECTOR_DATA_TOGGLE) for (let i = 0, len = timePickers.length; i < len; i++) { TimePicker.timePickerInterface(timePickers[i]) } }) EventHandler.on(document, EVENT_CLICK_DATA_API, TimePicker.clearMenus) EventHandler.on(document, EVENT_KEYUP_DATA_API, TimePicker.clearMenus) /** * jQuery */ defineJQueryPlugin(TimePicker) export default TimePicker