UNPKG

@instawork/design-system

Version:

The design system for Instawork's web apps

174 lines 7.34 kB
import * as $ from 'jquery'; import { Duration } from 'luxon'; import { DateUtil } from '../common'; import { SyncValueAttribute, TemporalComponent } from '../temporal-component'; import TEMPLATE from './time-selector.component.pug'; import './time-selector.component.scss'; const DEFAULT_STEP = 15; const DEFAULT_LABEL_FORMAT = 'h:mm a'; // 1:30 PM const DEFAULT_PLACEHOLDER = ''; /** * Wraps an HTML <select> component to act as a time selector, and automatically generates incremented options between * a min and max time, given an increment in minutes. * * If the max value does not occur naturally from increments of the min value, it will be added as the last option. * * Attributes: * min (required): An ISO 8601, Python "naive", or Python "aware" formatted datetime string representing the first * possible time option * max (required): An ISO 8601, Python "naive", or Python "aware" formatted datetime string representing the last * possible time option * step (optional): A numeric value representing the option value increment in minutes. Defaults to 15. * label-format (optional): A Luxon format string to use for formatting the text of the options. Defaults to "h:mm a" * (1:30 PM) * placeholder (optional): A string label to use as a placeholder for an empty value. Defaults to an empty string. * No empty value placeholder is added if the field is required. * value (optional): An ISO 8601, Python "naive", or Python "aware" formatted datetime string representing the initial * value * * Usage: * ```html * <iw-time-selector required min="2020-11-03T08:00:00.000" max="2020-11-03T016:00:00.000" value="2020-11-03T12:00:00.000" increment="15" label-format="h:mm a" /> * ``` */ export class TimeSelectorComponent extends TemporalComponent { constructor($el) { super($el); this.$value.on('change', () => this.importValue(this.el.value)); this.allowedValues = [...this.createOptionValues()]; } static loadPlugin() { super.loadPlugin(); if (!$.fn.iwTimeSelector) { $.fn.iwTimeSelector = this.jQueryPlugin('TimeSelectorComponent'); } } get min() { return this.parseValue(this.$el.attr('min')); } get max() { return this.parseValue(this.$el.attr('max')); } get step() { const stepInput = this.$el.attr('step'); const step = stepInput ? parseInt(stepInput, 10) : DEFAULT_STEP; if (isNaN(step)) { return Duration.invalid(`Input "${stepInput}" cannot be parsed to an integer value`); } return Duration.fromObject({ minutes: step }); } get labelFormat() { return this.$el.attr('label-format') || DEFAULT_LABEL_FORMAT; } get syncValueAttr() { return SyncValueAttribute.host; } ; validateHostAttributes() { var _a, _b; super.validateHostAttributes(); if (!((_a = this.min) === null || _a === void 0 ? void 0 : _a.isValid)) { throw this.invalidDateTimeAttributeError('min', this.min); } if (!((_b = this.max) === null || _b === void 0 ? void 0 : _b.isValid)) { throw this.invalidDateTimeAttributeError('max', this.max); } if (!this.step.isValid) { throw this.attributeError('increment', this.step.invalidExplanation); } } initDom() { super.initDom(); this.syncOptions(); } syncOptions() { this.$value.empty(); this.$value.html([...this.createOptions()].join('')); this.selectValue(this.luxonValue); } selectValue(value) { const doSelect = typeof value === 'string' || (value === null || value === void 0 ? void 0 : value.isValid) === true || typeof value === 'undefined'; if (doSelect) { const valueIndex = this.getValueIndex(value); if (typeof valueIndex === 'number') { // only update the index if it's a defined number - undefined means an invalid value was somehow selected, and // the display should keep that value and show the invalid treatment this.valueEl.selectedIndex = valueIndex; } } } /** * Returns the index of the option that represents the value. For undefined values, returns -1 if the input is * marked as "required"; otherwise 0 (to select the "empty" option). Returns undefined for values that do not have a * corresponding option (these will be invalid). * @param value * @protected */ getValueIndex(value) { if (!value) { if (this.required) { return -1; } return 0; } const formattedValue = typeof value === 'string' ? value : this.formatValue(value); const $selected = this.$value.find(`[value="${formattedValue}"]`); const index = $selected.index(); return index >= 0 ? index : undefined; // -1 at this point means the value is not included in the options } *createOptions() { if (!this.required) { yield `<option>${this.placeholder || DEFAULT_PLACEHOLDER}</option>`; } for (const value of this.createOptionValues()) { yield `<option value="${this.formatValue(value)}">${value.toFormat(this.labelFormat)}</option>`; } } *createOptionValues() { let current = this.min; yield current; while (current < this.max) { current = current.plus(this.step); yield current; } // add the "max" value if it does not occur naturally as an increment of the "min" value if (!current.equals(this.max)) { yield this.max; } } onValue(value, isChanged, isValid) { super.onValue(value, isChanged, isValid); if (isChanged) { this.selectValue(value); } } updateValidityState(value) { value = super.updateValidityState(value); if (!(value === null || value === void 0 ? void 0 : value.isValid)) { return value; } if (value < this.min) { this.valueEl.setCustomValidity('rangeUnderflow'); return DateUtil.invalidate(value, 'rangeUnderflow', 'The defined value is before the specified min date/time'); } if (value > this.max) { this.valueEl.setCustomValidity('rangeOverflow'); return DateUtil.invalidate(value, 'rangeOverflow', 'The defined value is after the specified min date/time'); } if (typeof this.getValueIndex(value) === 'undefined') { this.valueEl.setCustomValidity('stepMismatch'); return DateUtil.invalidate(value, 'stepMismatch', 'The defined value is not one of the defined options'); } return value; } setAttrProp(name, value) { if (name !== 'min' && name !== 'max') { super.setAttrProp(name, value); } } setValue(value) { this.importValue(value); return this.$value; } } TimeSelectorComponent.COMPONENT_SELECTOR = 'iw-time-selector'; TimeSelectorComponent.TEMPLATE = TEMPLATE; //# sourceMappingURL=time-selector.component.js.map