@instawork/design-system
Version:
The design system for Instawork's web apps
174 lines • 7.34 kB
JavaScript
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