746 lines
18 KiB
JavaScript
746 lines
18 KiB
JavaScript
import { DEFAULT_SETTINGS, DEFAULT_LANG } from "./defaults";
|
|
import { ONE_DAY } from "./constants";
|
|
|
|
const EVENT_DEFAULTS = {
|
|
bubbles: true,
|
|
cancelable: false,
|
|
detail: null
|
|
};
|
|
|
|
class Timepicker {
|
|
constructor(targetEl, options = {}) {
|
|
this._handleFormatValue = this._handleFormatValue.bind(this);
|
|
this._handleKeyUp = this._handleKeyUp.bind(this);
|
|
|
|
this.targetEl = targetEl;
|
|
|
|
const attrOptions = Timepicker.extractAttrOptions(
|
|
targetEl,
|
|
Object.keys(DEFAULT_SETTINGS)
|
|
);
|
|
|
|
this.settings = this.parseSettings({
|
|
...DEFAULT_SETTINGS,
|
|
...options,
|
|
...attrOptions
|
|
});
|
|
}
|
|
|
|
static extractAttrOptions(element, keys) {
|
|
const output = {};
|
|
for (const key of keys) {
|
|
if (key in element.dataset) {
|
|
output[key] = element.dataset[key];
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
static isVisible(elem) {
|
|
var el = elem[0];
|
|
return el.offsetWidth > 0 && el.offsetHeight > 0;
|
|
}
|
|
|
|
static hideAll() {
|
|
for (const el of document.getElementsByClassName('ui-timepicker-input')) {
|
|
const tp = el.timepickerObj;
|
|
if (tp) {
|
|
tp.hideMe();
|
|
}
|
|
}
|
|
}
|
|
|
|
hideMe() {
|
|
if (this.settings.useSelect) {
|
|
this.targetEl.blur();
|
|
return;
|
|
}
|
|
|
|
if (!this.list || !Timepicker.isVisible(this.list)) {
|
|
return;
|
|
}
|
|
|
|
if (this.settings.selectOnBlur) {
|
|
this._selectValue();
|
|
}
|
|
|
|
this.list.hide();
|
|
|
|
const hideTimepickerEvent = new CustomEvent('hideTimepicker', EVENT_DEFAULTS);
|
|
this.targetEl.dispatchEvent(hideTimepickerEvent);
|
|
}
|
|
|
|
_findRow(value) {
|
|
if (!value && value !== 0) {
|
|
return false;
|
|
}
|
|
|
|
var out = false;
|
|
var value = this.settings.roundingFunction(value, this.settings);
|
|
if (!this.list) {
|
|
return false;
|
|
}
|
|
|
|
this.list.find("li").each(function(i, obj) {
|
|
const parsed = parseInt(obj.dataset.time);
|
|
|
|
if (isNaN(parsed)) {
|
|
return;
|
|
}
|
|
|
|
if (parsed == value) {
|
|
out = obj;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
_hideKeyboard() {
|
|
return (
|
|
(window.navigator.msMaxTouchPoints || "ontouchstart" in document) &&
|
|
this.settings.disableTouchKeyboard
|
|
);
|
|
}
|
|
|
|
_setTimeValue(value, source) {
|
|
if (this.targetEl.nodeName === "INPUT") {
|
|
if (value !== null || this.targetEl.value != "") {
|
|
this.targetEl.value = value;
|
|
}
|
|
|
|
var tp = this;
|
|
var settings = tp.settings;
|
|
|
|
if (settings.useSelect && source != "select" && tp.list) {
|
|
tp.list.val(tp._roundAndFormatTime(tp.anytime2int(value)));
|
|
}
|
|
}
|
|
|
|
const selectTimeEvent = new CustomEvent('selectTime', EVENT_DEFAULTS);
|
|
|
|
if (this.selectedValue != value) {
|
|
this.selectedValue = value;
|
|
|
|
const changeTimeEvent = new CustomEvent('changeTime', EVENT_DEFAULTS);
|
|
const changeEvent = new CustomEvent('change',
|
|
Object.assign(EVENT_DEFAULTS, { detail: 'timepicker'}));
|
|
|
|
if (source == "select") {
|
|
this.targetEl.dispatchEvent(selectTimeEvent);
|
|
this.targetEl.dispatchEvent(changeTimeEvent);
|
|
this.targetEl.dispatchEvent(changeEvent);
|
|
|
|
} else if (["error", "initial"].indexOf(source) == -1) {
|
|
this.targetEl.dispatchEvent(changeTimeEvent);
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
if (["error", "initial"].indexOf(source) == -1) {
|
|
this.targetEl.dispatchEvent(selectTimeEvent);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_getTimeValue() {
|
|
if (this.targetEl.nodeName === "INPUT") {
|
|
return this.targetEl.value;
|
|
} else {
|
|
// use the element's data attributes to store values
|
|
return this.selectedValue;
|
|
}
|
|
}
|
|
|
|
_selectValue() {
|
|
var tp = this;
|
|
var settings = tp.settings;
|
|
var list = tp.list;
|
|
|
|
var cursor = list.find(".ui-timepicker-selected");
|
|
|
|
if (cursor.hasClass("ui-timepicker-disabled")) {
|
|
return false;
|
|
}
|
|
|
|
if (!cursor.length) {
|
|
return true;
|
|
}
|
|
|
|
var timeValue = cursor.get(0).dataset.time;
|
|
|
|
// selected value found
|
|
if (timeValue) {
|
|
const parsedTimeValue = parseInt(timeValue);
|
|
if (!isNaN(parsedTimeValue)) {
|
|
timeValue = parsedTimeValue;
|
|
}
|
|
|
|
}
|
|
|
|
if (timeValue !== null) {
|
|
if (typeof timeValue != "string") {
|
|
timeValue = tp._int2time(timeValue);
|
|
}
|
|
|
|
tp._setTimeValue(timeValue, "select");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
anytime2int(input) {
|
|
if (typeof input === 'number') {
|
|
return input;
|
|
} else if (typeof input === 'string') {
|
|
return this.time2int(input);
|
|
} else if (typeof input === 'object' && input instanceof Date) {
|
|
return (
|
|
input.getHours() * 3600 +
|
|
input.getMinutes() * 60 +
|
|
input.getSeconds()
|
|
);
|
|
} else if (typeof input == 'function') {
|
|
return input();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
time2int(timeString) {
|
|
if (timeString === "" || timeString === null || timeString === undefined) {
|
|
return null;
|
|
}
|
|
|
|
if (timeString === 'now') {
|
|
return this.anytime2int(new Date());
|
|
}
|
|
|
|
if (typeof timeString != "string") {
|
|
return timeString;
|
|
}
|
|
|
|
timeString = timeString.toLowerCase().replace(/[\s\.]/g, "");
|
|
|
|
// if the last character is an "a" or "p", add the "m"
|
|
if (timeString.slice(-1) == "a" || timeString.slice(-1) == "p") {
|
|
timeString += "m";
|
|
}
|
|
|
|
let pattern = /^(([^0-9]*))?([0-9]?[0-9])(([0-5][0-9]))?(([0-5][0-9]))?(([^0-9]*))$/;
|
|
|
|
const hasDelimetersMatch = timeString.match(/\W/);
|
|
if (hasDelimetersMatch) {
|
|
pattern = /^(([^0-9]*))?([0-9]?[0-9])(\W+([0-5][0-9]?))?(\W+([0-5][0-9]))?(([^0-9]*))$/;
|
|
}
|
|
|
|
var time = timeString.match(pattern);
|
|
if (!time) {
|
|
return null;
|
|
}
|
|
|
|
var hour = parseInt(time[3] * 1, 10);
|
|
var ampm = time[2] || time[9];
|
|
var hours = hour;
|
|
var minutes = time[5] * 1 || 0;
|
|
var seconds = time[7] * 1 || 0;
|
|
|
|
if (!ampm && time[3].length == 2 && time[3][0] == "0") {
|
|
// preceding '0' implies AM
|
|
ampm = "am";
|
|
}
|
|
|
|
if (hour <= 12 && ampm) {
|
|
ampm = ampm.trim();
|
|
var isPm = ampm == this.settings.lang.pm || ampm == this.settings.lang.PM;
|
|
|
|
if (hour == 12) {
|
|
hours = isPm ? 12 : 0;
|
|
} else {
|
|
hours = hour + (isPm ? 12 : 0);
|
|
}
|
|
} else {
|
|
var t = hour * 3600 + minutes * 60 + seconds;
|
|
if (t >= ONE_DAY + (this.settings.show2400 ? 1 : 0)) {
|
|
if (this.settings.wrapHours === false) {
|
|
return null;
|
|
}
|
|
|
|
hours = hour % 24;
|
|
}
|
|
}
|
|
|
|
var timeInt = hours * 3600 + minutes * 60 + seconds;
|
|
|
|
// if no am/pm provided, intelligently guess based on the scrollDefault
|
|
if (
|
|
hour < 12 &&
|
|
!ampm &&
|
|
this.settings._twelveHourTime &&
|
|
this.settings.scrollDefault()
|
|
) {
|
|
var delta = timeInt - this.settings.scrollDefault();
|
|
if (delta < 0 && delta >= ONE_DAY / -2) {
|
|
timeInt = (timeInt + ONE_DAY / 2) % ONE_DAY;
|
|
}
|
|
}
|
|
|
|
return timeInt;
|
|
}
|
|
|
|
intStringDateOrFunc2func(input) {
|
|
if (input === null || input === undefined) {
|
|
return () => null;
|
|
} else if (typeof input === 'function') {
|
|
return () => this.anytime2int(input());
|
|
} else {
|
|
return () => this.anytime2int(input);
|
|
}
|
|
}
|
|
|
|
parseSettings(settings) {
|
|
settings.lang = { ...DEFAULT_LANG, ...settings.lang };
|
|
|
|
// lang is used by other functions the rest of this depends on
|
|
// todo: unwind circular dependency on lang
|
|
this.settings = settings;
|
|
|
|
if (settings.listWidth) {
|
|
settings.listWidth = this.anytime2int(settings.listWidth);
|
|
}
|
|
|
|
settings.minTime = this.intStringDateOrFunc2func(settings.minTime);
|
|
settings.maxTime = this.intStringDateOrFunc2func(settings.maxTime);
|
|
settings.durationTime = this.intStringDateOrFunc2func(settings.durationTime);
|
|
|
|
if (settings.scrollDefault) {
|
|
settings.scrollDefault = this.intStringDateOrFunc2func(settings.scrollDefault);
|
|
} else {
|
|
settings.scrollDefault = settings.minTime;
|
|
}
|
|
|
|
if (
|
|
typeof settings.timeFormat === "string" &&
|
|
settings.timeFormat.match(/[gh]/)
|
|
) {
|
|
settings._twelveHourTime = true;
|
|
}
|
|
|
|
if (
|
|
settings.showOnFocus === false &&
|
|
settings.showOn.indexOf("focus") != -1
|
|
) {
|
|
settings.showOn.splice(settings.showOn.indexOf("focus"), 1);
|
|
}
|
|
|
|
if (typeof settings.step != 'function') {
|
|
const curryStep = settings.step;
|
|
settings.step = function() {
|
|
return curryStep;
|
|
};
|
|
}
|
|
|
|
if (!settings.disableTimeRanges) {
|
|
settings.disableTimeRanges = [];
|
|
}
|
|
|
|
if (settings.disableTimeRanges.length > 0) {
|
|
// convert string times to integers
|
|
for (var i in settings.disableTimeRanges) {
|
|
settings.disableTimeRanges[i] = [
|
|
this.anytime2int(settings.disableTimeRanges[i][0]),
|
|
this.anytime2int(settings.disableTimeRanges[i][1])
|
|
];
|
|
}
|
|
|
|
// sort by starting time
|
|
settings.disableTimeRanges = settings.disableTimeRanges.sort(function(
|
|
a,
|
|
b
|
|
) {
|
|
return a[0] - b[0];
|
|
});
|
|
|
|
// merge any overlapping ranges
|
|
for (var i = settings.disableTimeRanges.length - 1; i > 0; i--) {
|
|
if (
|
|
settings.disableTimeRanges[i][0] <=
|
|
settings.disableTimeRanges[i - 1][1]
|
|
) {
|
|
settings.disableTimeRanges[i - 1] = [
|
|
Math.min(
|
|
settings.disableTimeRanges[i][0],
|
|
settings.disableTimeRanges[i - 1][0]
|
|
),
|
|
Math.max(
|
|
settings.disableTimeRanges[i][1],
|
|
settings.disableTimeRanges[i - 1][1]
|
|
)
|
|
];
|
|
settings.disableTimeRanges.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
/*
|
|
* Filter freeform input
|
|
*/
|
|
_disableTextInputHandler(e) {
|
|
switch (e.keyCode) {
|
|
case 13: // return
|
|
case 9: //tab
|
|
return;
|
|
|
|
default:
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
_int2duration(seconds, step) {
|
|
seconds = Math.abs(seconds);
|
|
var minutes = Math.round(seconds / 60),
|
|
duration = [],
|
|
hours,
|
|
mins;
|
|
|
|
if (minutes < 60) {
|
|
// Only show (x mins) under 1 hour
|
|
duration = [minutes, this.settings.lang.mins];
|
|
} else {
|
|
hours = Math.floor(minutes / 60);
|
|
mins = minutes % 60;
|
|
|
|
// Show decimal notation (eg: 1.5 hrs) for 30 minute steps
|
|
if (step == 30 && mins == 30) {
|
|
hours += this.settings.lang.decimal + 5;
|
|
}
|
|
|
|
duration.push(hours);
|
|
duration.push(
|
|
hours == 1 ? this.settings.lang.hr : this.settings.lang.hrs
|
|
);
|
|
|
|
// Show remainder minutes notation (eg: 1 hr 15 mins) for non-30 minute steps
|
|
// and only if there are remainder minutes to show
|
|
if (step != 30 && mins) {
|
|
duration.push(mins);
|
|
duration.push(this.settings.lang.mins);
|
|
}
|
|
}
|
|
|
|
return duration.join(" ");
|
|
}
|
|
|
|
_roundAndFormatTime(seconds) {
|
|
// console.log('_roundAndFormatTime')
|
|
seconds = this.settings.roundingFunction(seconds, this.settings);
|
|
if (seconds !== null) {
|
|
return this._int2time(seconds);
|
|
}
|
|
}
|
|
|
|
_int2time(timeInt) {
|
|
if (typeof timeInt != "number") {
|
|
return null;
|
|
}
|
|
|
|
var seconds = parseInt(timeInt % 60),
|
|
minutes = parseInt((timeInt / 60) % 60),
|
|
hours = parseInt((timeInt / (60 * 60)) % 24);
|
|
|
|
var time = new Date(1970, 0, 2, hours, minutes, seconds, 0);
|
|
|
|
if (isNaN(time.getTime())) {
|
|
return null;
|
|
}
|
|
|
|
if (typeof this.settings.timeFormat === "function") {
|
|
return this.settings.timeFormat(time);
|
|
}
|
|
|
|
var output = "";
|
|
var hour, code;
|
|
for (var i = 0; i < this.settings.timeFormat.length; i++) {
|
|
code = this.settings.timeFormat.charAt(i);
|
|
switch (code) {
|
|
case "a":
|
|
output +=
|
|
time.getHours() > 11
|
|
? this.settings.lang.pm
|
|
: this.settings.lang.am;
|
|
break;
|
|
|
|
case "A":
|
|
output +=
|
|
time.getHours() > 11
|
|
? this.settings.lang.PM
|
|
: this.settings.lang.AM;
|
|
break;
|
|
|
|
case "g":
|
|
hour = time.getHours() % 12;
|
|
output += hour === 0 ? "12" : hour;
|
|
break;
|
|
|
|
case "G":
|
|
hour = time.getHours();
|
|
if (timeInt === ONE_DAY) hour = this.settings.show2400 ? 24 : 0;
|
|
output += hour;
|
|
break;
|
|
|
|
case "h":
|
|
hour = time.getHours() % 12;
|
|
|
|
if (hour !== 0 && hour < 10) {
|
|
hour = "0" + hour;
|
|
}
|
|
|
|
output += hour === 0 ? "12" : hour;
|
|
break;
|
|
|
|
case "H":
|
|
hour = time.getHours();
|
|
if (timeInt === ONE_DAY) hour = this.settings.show2400 ? 24 : 0;
|
|
output += hour > 9 ? hour : "0" + hour;
|
|
break;
|
|
|
|
case "i":
|
|
var minutes = time.getMinutes();
|
|
output += minutes > 9 ? minutes : "0" + minutes;
|
|
break;
|
|
|
|
case "s":
|
|
seconds = time.getSeconds();
|
|
output += seconds > 9 ? seconds : "0" + seconds;
|
|
break;
|
|
|
|
case "\\":
|
|
// escape character; add the next character and skip ahead
|
|
i++;
|
|
output += this.settings.timeFormat.charAt(i);
|
|
break;
|
|
|
|
default:
|
|
output += code;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
_setSelected() {
|
|
const list = this.list;
|
|
|
|
list.find("li").removeClass("ui-timepicker-selected");
|
|
|
|
const timeValue = this.anytime2int(this._getTimeValue());
|
|
|
|
if (timeValue === null) {
|
|
return;
|
|
}
|
|
|
|
const selected = this._findRow(timeValue);
|
|
if (selected) {
|
|
|
|
const selectedRect = selected.getBoundingClientRect();
|
|
const listRect = list.get(0).getBoundingClientRect();
|
|
const topDelta = selectedRect.top - listRect.top;
|
|
|
|
if (topDelta + selectedRect.height > listRect.height || topDelta < 0) {
|
|
const newScroll = list.scrollTop()
|
|
+ (selectedRect.top - listRect.top)
|
|
- selectedRect.height;
|
|
|
|
list.scrollTop(newScroll);
|
|
}
|
|
|
|
const parsed = parseInt(selected.dataset.time);
|
|
|
|
if (this.settings.forceRoundTime || parsed === timeValue) {
|
|
selected.classList.add('ui-timepicker-selected');
|
|
}
|
|
}
|
|
}
|
|
|
|
_isFocused(el) {
|
|
return (el === document.activeElement);
|
|
}
|
|
|
|
_handleFormatValue(e) {
|
|
if (e && e.detail == "timepicker") {
|
|
return;
|
|
}
|
|
|
|
this._formatValue(e);
|
|
}
|
|
|
|
_formatValue(e, origin) {
|
|
if (this.targetEl.value === "") {
|
|
this._setTimeValue(null, origin);
|
|
return;
|
|
}
|
|
|
|
// IE fires change event before blur
|
|
if (this._isFocused(this.targetEl) && (!e || e.type != "change")) {
|
|
return;
|
|
}
|
|
|
|
var settings = this.settings;
|
|
var seconds = this.anytime2int(this.targetEl.value);
|
|
|
|
if (seconds === null) {
|
|
const timeFormatErrorEvent = new CustomEvent('timeFormatError', EVENT_DEFAULTS);
|
|
this.targetEl.dispatchEvent(timeFormatErrorEvent);
|
|
return;
|
|
}
|
|
|
|
var rangeError = false;
|
|
// check that the time in within bounds
|
|
if (
|
|
settings.minTime !== null &&
|
|
settings.maxTime !== null &&
|
|
(seconds < settings.minTime() || seconds > settings.maxTime())
|
|
) {
|
|
rangeError = true;
|
|
}
|
|
|
|
// check that time isn't within disabled time ranges
|
|
for (const range of settings.disableTimeRanges) {
|
|
if (seconds >= range[0] && seconds < range[1]) {
|
|
rangeError = true;
|
|
break;
|
|
}
|
|
};
|
|
|
|
if (settings.forceRoundTime) {
|
|
var roundSeconds = settings.roundingFunction(seconds, settings);
|
|
if (roundSeconds != seconds) {
|
|
seconds = roundSeconds;
|
|
origin = null;
|
|
}
|
|
}
|
|
|
|
var prettyTime = this._int2time(seconds);
|
|
|
|
if (rangeError) {
|
|
this._setTimeValue(prettyTime);
|
|
const timeRangeErrorEvent = new CustomEvent('timeRangeError', EVENT_DEFAULTS);
|
|
this.targetEl.dispatchEvent(timeRangeErrorEvent);
|
|
} else {
|
|
this._setTimeValue(prettyTime, origin);
|
|
}
|
|
}
|
|
|
|
_generateNoneElement(optionValue, useSelect) {
|
|
var label, className, value;
|
|
|
|
if (typeof optionValue == "object") {
|
|
label = optionValue.label;
|
|
className = optionValue.className;
|
|
value = optionValue.value;
|
|
} else if (typeof optionValue == "string") {
|
|
label = optionValue;
|
|
value = "";
|
|
} else {
|
|
$.error("Invalid noneOption value");
|
|
}
|
|
|
|
let el;
|
|
if (useSelect) {
|
|
el = document.createElement("option");
|
|
el.value = value;
|
|
} else {
|
|
el = document.createElement("li");
|
|
el.dataset.time = String(value);
|
|
}
|
|
|
|
el.innerText = label;
|
|
el.classList.add(className);
|
|
return el;
|
|
}
|
|
|
|
|
|
/*
|
|
* Time typeahead
|
|
*/
|
|
_handleKeyUp(e) {
|
|
if (!this.list || !Timepicker.isVisible(this.list) || this.settings.disableTextInput) {
|
|
return true;
|
|
}
|
|
|
|
if (e.type === "paste" || e.type === "cut") {
|
|
const handler = () => {
|
|
if (this.settings.typeaheadHighlight) {
|
|
this._setSelected();
|
|
} else {
|
|
this.list.hide();
|
|
}
|
|
}
|
|
|
|
setTimeout(handler, 0);
|
|
return;
|
|
}
|
|
|
|
switch (e.keyCode) {
|
|
case 96: // numpad numerals
|
|
case 97:
|
|
case 98:
|
|
case 99:
|
|
case 100:
|
|
case 101:
|
|
case 102:
|
|
case 103:
|
|
case 104:
|
|
case 105:
|
|
case 48: // numerals
|
|
case 49:
|
|
case 50:
|
|
case 51:
|
|
case 52:
|
|
case 53:
|
|
case 54:
|
|
case 55:
|
|
case 56:
|
|
case 57:
|
|
case 65: // a
|
|
case 77: // m
|
|
case 80: // p
|
|
case 186: // colon
|
|
case 8: // backspace
|
|
case 46: // delete
|
|
if (this.settings.typeaheadHighlight) {
|
|
this._setSelected();
|
|
} else {
|
|
this.list.hide();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// IE9-11 polyfill for CustomEvent
|
|
(function () {
|
|
|
|
if ( typeof window.CustomEvent === "function" ) return false;
|
|
|
|
function CustomEvent ( event, params ) {
|
|
if (!params) {
|
|
params = {};
|
|
}
|
|
|
|
params = Object.assign(EVENT_DEFAULTS, params);
|
|
var evt = document.createEvent( 'CustomEvent' );
|
|
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
|
|
return evt;
|
|
}
|
|
|
|
window.CustomEvent = CustomEvent;
|
|
})();
|
|
|
|
export default Timepicker;
|