enketo-core
Version:
Extensible Enketo form engine
1,355 lines (1,203 loc) • 46.7 kB
JavaScript
import $ from 'jquery';
import event from '../../js/event';
import { time } from '../../js/format';
/*!
* Timepicker
*
* Forked from https://github.com/jdewit/bootstrap-timepicker:
*
* Copyright 2013 Joris de Wit and timepicker contributors
*
* Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors
* Contributors https://github.com/enketo/timepicker-basic/graphs/contributors
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
(($, window, document) => {
// TIMEPICKER PUBLIC CLASS DEFINITION
const Timepicker = function (element, options) {
this.languages = navigator.languages;
this.widget = '';
this.$element = $(element);
this.defaultTime = options.defaultTime;
this.disableFocus = options.disableFocus;
this.disableMousewheel = options.disableMousewheel;
this.isOpen = options.isOpen;
this.minuteStep = options.minuteStep;
this.orientation = options.orientation;
this.secondStep = options.secondStep;
this.snapToStep = options.snapToStep;
this.showInputs = options.showInputs;
this.showSeconds = options.showSeconds;
this.template = options.template;
this.appendWidgetTo = options.appendWidgetTo;
this.showWidgetOnAddonClick = options.showWidgetOnAddonClick;
this.icons = options.icons;
this.maxHours = options.maxHours;
this.explicitMode = options.explicitMode; // If true 123 = 1:23, 12345 = 1:23:45, else invalid.
this.handleDocumentClick = (e) => {
const self = e.data.scope;
// This condition was inspired by datepicker.
// The element the timepicker is invoked on is the input but it has a sibling for addon/button.
if (
!(
self.$element.parent().find(e.target).length ||
self.$widget.is(e.target) ||
self.$widget.find(e.target).length
)
) {
self.hideWidget();
}
};
this._init();
};
Timepicker.prototype = {
get showMeridian() {
return time.hour12;
},
get meridianNotation() {
return {
am: time.amNotation,
pm: time.pmNotation,
};
},
constructor: Timepicker,
_init() {
const self = this;
if (
this.showWidgetOnAddonClick &&
this.$element.parent().hasClass('input-group') &&
this.$element.parent().hasClass('timepicker')
) {
this.$element
.parent('.input-group.timepicker')
.find('.input-group-addon')
.on({
'click.timepicker': $.proxy(this.showWidget, this),
});
this.$element.on({
'focus.timepicker': $.proxy(this.highlightUnit, this),
'click.timepicker': $.proxy(this.highlightUnit, this),
'keydown.timepicker': $.proxy(this.elementKeydown, this),
'blur.timepicker': $.proxy(this.blurElement, this),
'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy(
this.mousewheel,
this
),
});
} else if (this.template) {
this.$element.on({
'focus.timepicker': $.proxy(this.showWidget, this),
'click.timepicker': $.proxy(this.showWidget, this),
'blur.timepicker': $.proxy(this.blurElement, this),
'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy(
this.mousewheel,
this
),
});
} else {
this.$element.on({
'focus.timepicker': $.proxy(this.highlightUnit, this),
'click.timepicker': $.proxy(this.highlightUnit, this),
'keydown.timepicker': $.proxy(this.elementKeydown, this),
'blur.timepicker': $.proxy(this.blurElement, this),
'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy(
this.mousewheel,
this
),
});
}
if (this.template !== false) {
this.$widget = $(this.getTemplate()).on(
'click',
$.proxy(this.widgetClick, this)
);
this.meridianColumns =
this.$widget[0].querySelectorAll('.meridian-column');
} else {
this.$widget = false;
}
if (this.showInputs && this.$widget !== false) {
this.$widget.find('input').each(function () {
$(this).on({
'click.timepicker': function () {
$(this).select();
},
'keydown.timepicker': $.proxy(self.widgetKeydown, self),
'keyup.timepicker': $.proxy(self.widgetKeyup, self),
});
});
}
this.setDefaultTime(this.defaultTime);
},
blurElement() {
this.highlightedUnit = null;
this.updateFromElementVal();
},
clear() {
this.hour = '';
this.minute = '';
this.second = '';
this.meridian = '';
this.$element.val('');
},
decrementHour() {
// If value is empty, make sure that first shown value is current hour.
if (this.hour === '') {
this.hour = this.getCurrentHour();
this.incrementHour();
}
if (this.showMeridian) {
if (this.hour === 1) {
this.hour = 12;
} else if (this.hour === 12) {
this.hour--;
return this.toggleMeridian();
} else if (this.hour === 0) {
this.hour = 11;
return this.toggleMeridian();
} else {
this.hour--;
}
} else if (this.hour <= 0) {
this.hour = this.maxHours - 1;
} else {
this.hour--;
}
},
decrementMinute(step) {
let newVal;
// If value is empty, make sure that first shown value is current minutes.
if (this.minute === '') {
this.minute = this.getCurrentMinute();
this.incrementMinute();
}
if (step) {
newVal = this.minute - step;
} else {
newVal = this.minute - this.minuteStep;
}
if (newVal < 0) {
this.decrementHour();
this.minute = newVal + 60;
} else {
this.minute = newVal;
}
},
decrementSecond() {
const newVal = this.second - this.secondStep;
if (newVal < 0) {
this.decrementMinute(true);
this.second = newVal + 60;
} else {
this.second = newVal;
}
},
elementKeydown(e) {
switch (e.which) {
case 9: // tab
if (e.shiftKey) {
if (this.highlightedUnit === 'hour') {
this.hideWidget();
break;
}
this.highlightPrevUnit();
} else if (
(this.showMeridian &&
this.highlightedUnit === 'meridian') ||
(this.showSeconds &&
this.highlightedUnit === 'second') ||
(!this.showMeridian &&
!this.showSeconds &&
this.highlightedUnit === 'minute')
) {
this.hideWidget();
break;
} else {
this.highlightNextUnit();
}
e.preventDefault();
this.updateFromElementVal();
break;
case 27: // escape
this.updateFromElementVal();
break;
case 37: // left arrow
e.preventDefault();
this.highlightPrevUnit();
this.updateFromElementVal();
break;
case 38: // up arrow
e.preventDefault();
switch (this.highlightedUnit) {
case 'hour':
this.incrementHour();
this.highlightHour();
break;
case 'minute':
this.incrementMinute();
this.highlightMinute();
break;
case 'second':
this.incrementSecond();
this.highlightSecond();
break;
case 'meridian':
this.toggleMeridian();
this.highlightMeridian();
break;
}
this.update();
break;
case 39: // right arrow
e.preventDefault();
this.highlightNextUnit();
this.updateFromElementVal();
break;
case 40: // down arrow
e.preventDefault();
switch (this.highlightedUnit) {
case 'hour':
this.decrementHour();
this.highlightHour();
break;
case 'minute':
this.decrementMinute();
this.highlightMinute();
break;
case 'second':
this.decrementSecond();
this.highlightSecond();
break;
case 'meridian':
this.toggleMeridian();
this.highlightMeridian();
break;
}
this.update();
break;
}
},
getCursorPosition() {
const input = this.$element.get(0);
if ('selectionStart' in input) {
// Standard-compliant browsers
return input.selectionStart;
}
if (document.selection) {
// IE fix
input.focus();
const sel = document.selection.createRange();
const selLen = document.selection.createRange().text.length;
sel.moveStart('character', -input.value.length);
return sel.text.length - selLen;
}
},
getMeridianLength() {
const el = document.createElement('span');
el.textContent = this.meridianNotation.am;
el.style.position = 'absolute';
document.querySelector('body').appendChild(el);
const amLength = el.scrollWidth;
el.textContent = this.meridianNotation.pm;
const pmLength = el.scrollWidth;
el.remove();
return amLength > pmLength ? amLength : pmLength;
},
getTemplate() {
let template;
let hourTemplate;
let minuteTemplate;
let secondTemplate;
let meridianTemplate;
let templateContent;
if (this.showInputs) {
const width =
this.getMeridianLength() > 26
? `style="width: ${this.getMeridianLength() + 24}px"`
: '';
hourTemplate = `<input type="text" class="timepicker-hour" ${width}/>`;
minuteTemplate = `<input type="text" class="timepicker-minute" ${width}/>`;
secondTemplate = `<input type="text" class="timepicker-second" ${width}/>`;
meridianTemplate = `<input type="text" class="timepicker-meridian"${width}/>`;
} else {
hourTemplate = '<span class="timepicker-hour"></span>';
minuteTemplate = '<span class="timepicker-minute"></span>';
secondTemplate = '<span class="timepicker-second"></span>';
meridianTemplate = '<span class="timepicker-meridian"></span>';
}
templateContent = `<table><tr><td><a href="#" data-action="incrementHour"><span class="${
this.icons.up
}"></span></a></td><td class="separator"> </td><td><a href="#" data-action="incrementMinute"><span class="${
this.icons.up
}"></span></a></td>${
this.showSeconds
? `<td class="separator"> </td><td><a href="#" data-action="incrementSecond"><span class="${this.icons.up}"></span></a></td>`
: ''
}<td class="separator meridian-column"> </td><td class="meridian-column"><a href="#" data-action="toggleMeridian"><span class="${
this.icons.up
}"></span></a></td></tr><tr><td>${hourTemplate}</td> <td class="separator">:</td><td>${minuteTemplate}</td> ${
this.showSeconds
? `<td class="separator">:</td><td>${secondTemplate}</td>`
: ''
}<td class="separator meridian-column"> </td><td class="meridian-column">${meridianTemplate}</td></tr><tr><td><a href="#" data-action="decrementHour"><span class="${
this.icons.down
}"></span></a></td><td class="separator"></td><td><a href="#" data-action="decrementMinute"><span class="${
this.icons.down
}"></span></a></td>${
this.showSeconds
? `<td class="separator"> </td><td><a href="#" data-action="decrementSecond"><span class="${this.icons.down}"></span></a></td>`
: ''
}<td class="separator meridian-column"> </td><td class="meridian-column"><a href="#" data-action="toggleMeridian"><span class="${
this.icons.down
}"></span></a></td></tr></table>`;
switch (this.template) {
case 'dropdown':
template = `<div class="timepicker-widget dropdown-menu">${templateContent}</div>`;
break;
}
return template;
},
getTime() {
if (this.hour === '') {
return '';
}
// return this.hour + ':' + ( this.minute.toString().length === 1 ? '0' + this.minute : this.minute ) + ( this.showSeconds ? ':' + ( this.second.toString().length === 1 ? '0' + this.second : this.second ) : '' ) + ( this.showMeridian ? ' ' + this.meridian : '' );
return `${
this.hour.toString().length === 1 ? `0${this.hour}` : this.hour
}:${
this.minute.toString().length === 1
? `0${this.minute}`
: this.minute
}${
this.showSeconds
? `:${
this.second.toString().length === 1
? `0${this.second}`
: this.second
}`
: ''
}${this.showMeridian ? ` ${this.meridian}` : ''}`;
},
hideWidget() {
if (this.isOpen === false) {
return;
}
this.$widget.removeClass('open');
$(document).off(
'mousedown.timepicker, touchend.timepicker',
this.handleDocumentClick
);
this.isOpen = false;
// show/hide approach taken by datepicker
this.$widget.detach();
},
highlightUnit() {
this.position = this.getCursorPosition();
if (this.position >= 0 && this.position <= 2) {
this.highlightHour();
} else if (this.position >= 3 && this.position <= 5) {
this.highlightMinute();
} else if (this.position >= 6 && this.position <= 8) {
if (this.showSeconds) {
this.highlightSecond();
} else {
this.highlightMeridian();
}
} else if (this.position >= 9 && this.position <= 11) {
this.highlightMeridian();
}
},
highlightNextUnit() {
switch (this.highlightedUnit) {
case 'hour':
this.highlightMinute();
break;
case 'minute':
if (this.showSeconds) {
this.highlightSecond();
} else if (this.showMeridian) {
this.highlightMeridian();
} else {
this.highlightHour();
}
break;
case 'second':
if (this.showMeridian) {
this.highlightMeridian();
} else {
this.highlightHour();
}
break;
case 'meridian':
this.highlightHour();
break;
}
},
highlightPrevUnit() {
switch (this.highlightedUnit) {
case 'hour':
if (this.showMeridian) {
this.highlightMeridian();
} else if (this.showSeconds) {
this.highlightSecond();
} else {
this.highlightMinute();
}
break;
case 'minute':
this.highlightHour();
break;
case 'second':
this.highlightMinute();
break;
case 'meridian':
if (this.showSeconds) {
this.highlightSecond();
} else {
this.highlightMinute();
}
break;
}
},
highlightHour() {
const $element = this.$element.get(0);
const self = this;
this.highlightedUnit = 'hour';
if ($element.setSelectionRange) {
setTimeout(() => {
if (self.hour < 10) {
$element.setSelectionRange(0, 1);
} else {
$element.setSelectionRange(0, 2);
}
}, 0);
}
},
highlightMinute() {
const $element = this.$element.get(0);
const self = this;
this.highlightedUnit = 'minute';
if ($element.setSelectionRange) {
setTimeout(() => {
if (self.hour < 10) {
$element.setSelectionRange(2, 4);
} else {
$element.setSelectionRange(3, 5);
}
}, 0);
}
},
highlightSecond() {
const $element = this.$element.get(0);
const self = this;
this.highlightedUnit = 'second';
if ($element.setSelectionRange) {
setTimeout(() => {
if (self.hour < 10) {
$element.setSelectionRange(5, 7);
} else {
$element.setSelectionRange(6, 8);
}
}, 0);
}
},
highlightMeridian() {
const $element = this.$element.get(0);
const self = this;
this.highlightedUnit = 'meridian';
if ($element.setSelectionRange) {
if (this.showSeconds) {
setTimeout(() => {
if (self.hour < 10) {
$element.setSelectionRange(8, 10);
} else {
$element.setSelectionRange(9, 11);
}
}, 0);
} else {
setTimeout(() => {
if (self.hour < 10) {
$element.setSelectionRange(5, 7);
} else {
$element.setSelectionRange(6, 8);
}
}, 0);
}
}
},
getCurrentHour() {
const h24 = new Date().getHours();
return this.showMeridian ? h24 % 12 : h24;
},
getCurrentMinute() {
return new Date().getMinutes();
},
incrementHour() {
// If value is empty, make sure that first shown value is current hour.
if (this.hour === '') {
this.hour = this.getCurrentHour();
this.decrementHour();
}
// if this.hour is empty string
if (this.showMeridian) {
if (this.hour === 11) {
this.hour++;
return this.toggleMeridian();
}
if (this.hour === 12) {
this.hour = 0;
}
}
if (this.hour === this.maxHours - 1) {
this.hour = 0;
return;
}
this.hour++;
},
incrementMinute(step) {
let newVal;
// If value is empty, make sure that first shown value is current minutes.
if (this.minute === '') {
this.minute = this.getCurrentMinute();
this.decrementMinute();
}
if (step) {
newVal = this.minute + step;
} else {
newVal =
this.minute +
this.minuteStep -
(this.minute % this.minuteStep);
}
if (newVal > 59) {
this.incrementHour();
this.minute = newVal - 60;
} else {
this.minute = newVal;
}
},
incrementSecond() {
const newVal =
this.second + this.secondStep - (this.second % this.secondStep);
if (newVal > 59) {
this.incrementMinute(true);
this.second = newVal - 60;
} else {
this.second = newVal;
}
},
mousewheel(e) {
if (this.disableMousewheel) {
return;
}
e.preventDefault();
e.stopPropagation();
const delta = e.originalEvent.wheelDelta || -e.originalEvent.detail;
let scrollTo = null;
if (e.type === 'mousewheel') {
scrollTo = e.originalEvent.wheelDelta * -1;
} else if (e.type === 'DOMMouseScroll') {
scrollTo = 40 * e.originalEvent.detail;
}
if (scrollTo) {
e.preventDefault();
$(this).scrollTop(scrollTo + $(this).scrollTop());
}
switch (this.highlightedUnit) {
case 'minute':
if (delta > 0) {
this.incrementMinute();
} else {
this.decrementMinute();
}
this.highlightMinute();
break;
case 'second':
if (delta > 0) {
this.incrementSecond();
} else {
this.decrementSecond();
}
this.highlightSecond();
break;
case 'meridian':
this.toggleMeridian();
this.highlightMeridian();
break;
default:
if (delta > 0) {
this.incrementHour();
} else {
this.decrementHour();
}
this.highlightHour();
break;
}
return false;
},
/**
* Given a segment value like 43, will round and snap the segment
* to the nearest "step", like 45 if step is 15. Segment will
* "overflow" to 0 if it's larger than 59 or would otherwise
* round up to 60.
*
* @param {number} segment - The segment value
* @param {number} step - The step
*/
changeToNearestStep(segment, step) {
if (segment % step === 0) {
return segment;
}
if (Math.round((segment % step) / step)) {
return (segment + (step - (segment % step))) % 60;
}
return segment - (segment % step);
},
// This method was adapted from datepicker.
place() {
if (this.isInline) {
return;
}
const widgetWidth = this.$widget.outerWidth();
const widgetHeight = this.$widget.outerHeight();
const visualPadding = 10;
const windowWidth = $(window).width();
const windowHeight = $(window).height();
const scrollTop = $(window).scrollTop();
const zIndex =
parseInt(
this.$element
.parents()
.filter(function () {
return $(this).css('z-index') !== 'auto';
})
.first()
.css('z-index'),
10
) + 10;
const offset = this.component
? this.component.parent().offset()
: this.$element.offset();
const height = this.component
? this.component.outerHeight(true)
: this.$element.outerHeight(false);
const width = this.component
? this.component.outerWidth(true)
: this.$element.outerWidth(false);
let { left } = offset;
let { top } = offset;
this.$widget.removeClass(
'timepicker-orient-top timepicker-orient-bottom timepicker-orient-right timepicker-orient-left'
);
if (this.orientation.x !== 'auto') {
this.$widget.addClass(
`timepicker-orient-${this.orientation.x}`
);
if (this.orientation.x === 'right') {
left -= widgetWidth - width;
}
} else {
// auto x orientation is best-placement: if it crosses a window edge, fudge it sideways
// Default to left
this.$widget.addClass('timepicker-orient-left');
if (offset.left < 0) {
left -= offset.left - visualPadding;
} else if (offset.left + widgetWidth > windowWidth) {
left = windowWidth - widgetWidth - visualPadding;
}
}
// auto y orientation is best-situation: top or bottom, no fudging, decision based on which shows more of the widget
let yorient = this.orientation.y;
let topOverflow;
let bottomOverflow;
if (yorient === 'auto') {
topOverflow = -scrollTop + offset.top - widgetHeight;
bottomOverflow =
scrollTop +
windowHeight -
(offset.top + height + widgetHeight);
if (Math.max(topOverflow, bottomOverflow) === bottomOverflow) {
yorient = 'top';
} else {
yorient = 'bottom';
}
}
this.$widget.addClass(`timepicker-orient-${yorient}`);
if (yorient === 'top') {
top += height;
} else {
top -=
widgetHeight +
parseInt(this.$widget.css('padding-top'), 10);
}
this.$widget.css({
top,
left,
zIndex,
});
},
remove() {
$('document').off('.timepicker');
if (this.$widget) {
this.$widget.remove();
}
delete this.$element.data().timepicker;
},
setDefaultTime(defaultTime) {
if (!this.$element.val()) {
if (defaultTime === 'current') {
const dTime = new Date();
let hours = dTime.getHours();
let minutes = dTime.getMinutes();
let seconds = dTime.getSeconds();
let meridian = this.meridianNotation.am;
if (seconds !== 0) {
seconds =
Math.ceil(dTime.getSeconds() / this.secondStep) *
this.secondStep;
if (seconds === 60) {
minutes += 1;
seconds = 0;
}
}
if (minutes !== 0) {
minutes =
Math.ceil(dTime.getMinutes() / this.minuteStep) *
this.minuteStep;
if (minutes === 60) {
hours += 1;
minutes = 0;
}
}
if (this.showMeridian) {
if (hours === 0) {
hours = 12;
} else if (hours >= 12) {
if (hours > 12) {
hours -= 12;
}
meridian = this.meridianNotation.pm;
} else {
meridian = this.meridianNotation.am;
}
}
this.hour = hours;
this.minute = minutes;
this.second = seconds;
this.meridian = meridian;
this.update();
} else if (defaultTime === false) {
this.hour = 0;
this.minute = 0;
this.second = 0;
this.meridian = this.meridianNotation.am;
} else {
this.setTime(defaultTime);
}
} else {
this.updateFromElementVal();
}
},
setTime(time, ignoreWidget) {
if (!time) {
this.clear();
return;
}
let timeMode;
let timeArray;
let hour;
let minute;
let second;
let meridian;
if (typeof time === 'object' && time.getMonth) {
// this is a date object
hour = time.getHours();
minute = time.getMinutes();
second = time.getSeconds();
if (this.showMeridian) {
meridian = this.meridianNotation.am;
if (hour > 12) {
meridian = this.meridianNotation.pm;
hour %= 12;
}
if (hour === 12) {
meridian = this.meridianNotation.pm;
}
}
} else {
const am = this.showMeridian ? this.meridianNotation.am : 'am';
const pm = this.showMeridian ? this.meridianNotation.pm : 'pm';
timeMode =
(new RegExp(am, 'i').test(time) ? 1 : 0) +
(new RegExp(pm, 'i').test(time) ? 2 : 0); // 0 = none, 1 = AM, 2 = PM, 3 = BOTH.
if (timeMode > 2) {
// If both are present, fail.
this.clear();
return;
}
timeArray = time.replace(/[^0-9:]/g, '').split(':');
hour = timeArray[0]
? timeArray[0].toString()
: timeArray.toString();
if (
this.explicitMode &&
hour.length > 2 &&
hour.length % 2 !== 0
) {
this.clear();
return;
}
minute = timeArray[1] ? timeArray[1].toString() : '';
second = timeArray[2] ? timeArray[2].toString() : '';
// adaptive time parsing
if (hour.length > 4) {
second = hour.slice(-2);
hour = hour.slice(0, -2);
}
if (hour.length > 2) {
minute = hour.slice(-2);
hour = hour.slice(0, -2);
}
if (minute.length > 2) {
second = minute.slice(-2);
minute = minute.slice(0, -2);
}
hour = parseInt(hour, 10);
minute = parseInt(minute, 10);
second = parseInt(second, 10);
if (isNaN(hour)) {
hour = 0;
}
if (isNaN(minute)) {
minute = 0;
}
if (isNaN(second)) {
second = 0;
}
// Adjust the time based upon unit boundary.
// NOTE: Negatives will never occur due to time.replace() above.
if (second > 59) {
second = 59;
}
if (minute > 59) {
minute = 59;
}
if (hour >= this.maxHours) {
// No day/date handling.
hour = this.maxHours - 1;
}
if (this.showMeridian) {
if (hour >= 12) {
// Force PM.
if (!timeMode) {
timeMode = 2;
}
hour -= 12;
}
if (!timeMode) {
timeMode = 1;
}
if (hour === 0) {
hour = 12; // AM or PM, reset to 12. 0 AM = 12 AM. 0 PM = 12 PM, etc.
}
meridian =
timeMode === 1
? this.meridianNotation.am
: this.meridianNotation.pm;
} else if (hour < 12 && timeMode === 2) {
hour += 12;
} else if (hour >= this.maxHours) {
hour = this.maxHours - 1;
} else if (hour < 0 || (hour === 12 && timeMode === 1)) {
hour = 0;
}
}
this.hour = hour;
if (this.snapToStep) {
this.minute = this.changeToNearestStep(minute, this.minuteStep);
this.second = this.changeToNearestStep(second, this.secondStep);
} else {
this.minute = minute;
this.second = second;
}
this.meridian = meridian;
this.update(ignoreWidget);
},
showWidget() {
if (this.isOpen) {
return;
}
if (this.$element.is(':disabled')) {
return;
}
// make sure the widget is in sync with input
this.setTime(this.$element.val());
this.updateWidget();
// show/hide approach taken by datepicker
this.$widget.appendTo(this.appendWidgetTo);
$(document).on(
'mousedown.timepicker, touchend.timepicker',
{
scope: this,
},
this.handleDocumentClick
);
this.place();
if (this.disableFocus) {
this.$element.blur();
}
if (this.hour === '') {
if (this.defaultTime) {
this.setDefaultTime(this.defaultTime);
}
}
if (this.isOpen === false) {
this.$widget.addClass('open');
}
this.isOpen = true;
},
toggleMeridian() {
this.meridian =
this.meridian === this.meridianNotation.am
? this.meridianNotation.pm
: this.meridianNotation.am;
},
update(ignoreWidget) {
this.updateElement();
if (!ignoreWidget) {
this.updateWidget();
}
},
updateElement() {
this.$element.val(this.getTime());
this.$element[0].dispatchEvent(event.Change());
},
updateFromElementVal() {
this.setTime(this.$element.val());
},
updateWidget() {
if (this.$widget === false) {
return;
}
const { hour } = this;
const minute =
this.minute.toString().length === 1
? `0${this.minute}`
: this.minute;
const second =
this.second.toString().length === 1
? `0${this.second}`
: this.second;
if (this.showInputs) {
this.$widget.find('input.timepicker-hour').val(hour);
this.$widget.find('input.timepicker-minute').val(minute);
if (this.showSeconds) {
this.$widget.find('input.timepicker-second').val(second);
}
if (this.showMeridian) {
this.$widget
.find('input.timepicker-meridian')
.val(this.meridian);
}
} else {
this.$widget.find('span.timepicker-hour').text(hour);
this.$widget.find('span.timepicker-minute').text(minute);
if (this.showSeconds) {
this.$widget.find('span.timepicker-second').text(second);
}
if (this.showMeridian) {
this.$widget
.find('span.timepicker-meridian')
.text(this.meridian);
}
}
const { showMeridian } = this;
const meridianDisplay = showMeridian ? 'table-cell' : 'none';
this.meridianColumns.forEach((column) => {
column.style.display = meridianDisplay;
});
},
updateLocalization() {
if (this.languages !== navigator.languages) {
this.languages = navigator.languages;
this.updateFromElementVal();
this.updateWidget();
}
},
updateFromWidgetInputs() {
if (this.$widget === false) {
return;
}
const t = `${this.$widget
.find('input.timepicker-hour')
.val()}:${this.$widget.find('input.timepicker-minute').val()}${
this.showSeconds
? `:${this.$widget.find('input.timepicker-second').val()}`
: ''
}${
this.showMeridian
? this.$widget.find('input.timepicker-meridian').val()
: ''
}`;
this.setTime(t, true);
},
widgetClick(e) {
e.stopPropagation();
e.preventDefault();
const $input = $(e.target);
const action = $input.closest('a').data('action');
if (action) {
this[action]();
}
this.update();
if ($input.is('input')) {
$input.get(0).setSelectionRange(0, 2);
}
},
widgetKeydown(e) {
const $input = $(e.target);
const name = $input.attr('class').replace('timepicker-', '');
switch (e.which) {
case 9: // tab
if (e.shiftKey) {
if (name === 'hour') {
return this.hideWidget();
}
} else if (
(this.showMeridian && name === 'meridian') ||
(this.showSeconds && name === 'second') ||
(!this.showMeridian &&
!this.showSeconds &&
name === 'minute')
) {
return this.hideWidget();
}
break;
case 27: // escape
this.hideWidget();
break;
case 38: // up arrow
e.preventDefault();
switch (name) {
case 'hour':
this.incrementHour();
break;
case 'minute':
this.incrementMinute();
break;
case 'second':
this.incrementSecond();
break;
case 'meridian':
this.toggleMeridian();
break;
}
this.setTime(this.getTime());
$input.get(0).setSelectionRange(0, 2);
break;
case 40: // down arrow
e.preventDefault();
switch (name) {
case 'hour':
this.decrementHour();
break;
case 'minute':
this.decrementMinute();
break;
case 'second':
this.decrementSecond();
break;
case 'meridian':
this.toggleMeridian();
break;
}
this.setTime(this.getTime());
$input.get(0).setSelectionRange(0, 2);
break;
}
},
widgetKeyup(e) {
if (
e.which === 65 ||
e.which === 77 ||
e.which === 80 ||
e.which === 46 ||
e.which === 8 ||
(e.which >= 48 && e.which <= 57) ||
(e.which >= 96 && e.which <= 105)
) {
this.updateFromWidgetInputs();
}
},
};
// TIMEPICKER PLUGIN DEFINITION
$.fn.timepicker = function (option, ...rest) {
return this.each(function () {
const $this = $(this);
let data = $this.data('timepicker');
const options = typeof option === 'object' && option;
if (!data) {
$this.data(
'timepicker',
(data = new Timepicker(
this,
$.extend(
{},
$.fn.timepicker.defaults,
options,
$(this).data()
)
))
);
}
if (typeof option === 'string') {
data[option](...rest);
}
});
};
$.fn.timepicker.defaults = {
defaultTime: 'current',
disableFocus: false,
disableMousewheel: false,
isOpen: false,
minuteStep: 15,
orientation: {
x: 'auto',
y: 'auto',
},
secondStep: 15,
snapToStep: false,
showSeconds: false,
showInputs: true,
showMeridian: true,
meridianNotation: {
am: 'AM',
pm: 'PM',
},
template: 'dropdown',
appendWidgetTo: 'body',
showWidgetOnAddonClick: true,
icons: {
up: 'glyphicon glyphicon-chevron-up',
down: 'glyphicon glyphicon-chevron-down',
},
maxHours: 24,
explicitMode: false,
};
$.fn.timepicker.Constructor = Timepicker;
$(document).on(
'focus.timepicker.data-api click.timepicker.data-api',
'[data-provide="timepicker"]',
function (e) {
const $this = $(this);
if ($this.data('timepicker')) {
return;
}
e.preventDefault();
// component click requires us to explicitly show it
$this.timepicker();
}
);
})($, window, document);