965 lines
33 KiB
JavaScript
965 lines
33 KiB
JavaScript
/**
|
|
* Time picker for Bootstrap 4
|
|
*
|
|
* https://github.com/lesilent/timepicker-bs4
|
|
*/
|
|
(function () {
|
|
//-------------------------------------
|
|
'use strict';
|
|
|
|
/**
|
|
* Array of dayjs format substrings,
|
|
*
|
|
* 0 = used for regex to determine whether format contains unit
|
|
* 1 = format used for buttons and inputs
|
|
* 2 = the unit/view name
|
|
*
|
|
* @type {array}
|
|
*/
|
|
let FORMATS = [
|
|
['h', 'h', 'hour'],
|
|
['m', 'mm', 'minute'],
|
|
['s', 'ss', 'second'],
|
|
['a', 'A', 'meridiem'],
|
|
];
|
|
|
|
/**
|
|
* Unit lengths
|
|
*
|
|
* @type {object}
|
|
*/
|
|
const UNIT_LENGTHS = { hour: 24, minute: 60, second: 60, meridiem: 2 };
|
|
|
|
/**
|
|
*
|
|
* @type {string}
|
|
*/
|
|
const ACTIVE_CLASS = 'active btn-info';
|
|
|
|
/**
|
|
*
|
|
* @type {string}
|
|
*/
|
|
const INACTIVE_CLASS = 'btn-outline-dark border-white';
|
|
|
|
/**
|
|
* Flag for whether plugin has been initialized
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
let initialized = false;
|
|
|
|
/**
|
|
* Parse a time string and return a dayjs object
|
|
*
|
|
* @param {string} str
|
|
* @param {object} options
|
|
* @return {object|boolean} either a dayjs object or false on error
|
|
*/
|
|
function parseTime(str, options)
|
|
{
|
|
let input_time = false, matches;
|
|
if (typeof str == 'string')
|
|
{
|
|
str = str.replace(/^\s+|\s+$/g, '');
|
|
if ((matches = str.match(/^([0-2]?\d)(?:s*:\s*([0-5]\d))?(?:\s*:\s*([0-5]\d))?(?:\s*([AP])\.?(?:M\.?)?)?$/i))
|
|
&& parseInt(matches[1]) > (matches[4] ? 0 : -1) && parseInt(matches[1]) < (matches[4] ? 13 : 24)
|
|
&& (matches[2] === undefined || (parseInt(matches[2]) > -1 && parseInt(matches[2]) < 60))
|
|
&& (matches[3] === undefined || (parseInt(matches[3]) > -1 && parseInt(matches[3]) < 60)))
|
|
{
|
|
let hour = parseInt(matches[1]);
|
|
if (matches[4])
|
|
{
|
|
hour = hour % 12 + ((matches[4].toUpperCase() == 'P') ? 12 : 0);
|
|
}
|
|
input_time = dayjs().hour(hour).minute(matches[2] == undefined ? 0 : parseInt(matches[2])).second((matches[3] === undefined) ? 0 : parseInt(matches[3]));
|
|
}
|
|
else
|
|
{
|
|
input_time = (options && options.format)
|
|
? dayjs(str, options.format)
|
|
: dayjs(str);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
input_time = dayjs(str);
|
|
}
|
|
return (input_time && input_time.isValid()) ? input_time : false;
|
|
}
|
|
|
|
/**
|
|
* Return allowed unit text object based on min time, max time, and step
|
|
*
|
|
* @param {object} options
|
|
* @return {object}
|
|
*/
|
|
function getUnitText(options)
|
|
{
|
|
const minTime = options.minTime || dayjs().startOf('day');
|
|
const maxTime = options.maxTime || dayjs().endOf('day');
|
|
const step = options.step || 60;
|
|
let valid = { offset: {}, hour: {}, minute: {}, second: {}, meridiem: {}, length: 0 };
|
|
let iTime = minTime.clone();
|
|
const unixOffset = minTime.startOf('day').unix();
|
|
while (iTime.isBefore(maxTime) || iTime.isSame(maxTime, 'second'))
|
|
{
|
|
valid.offset[iTime.unix() - unixOffset] = true;
|
|
valid.hour[iTime.hour()] = true;
|
|
valid.minute[iTime.minute()] = true;
|
|
valid.second[iTime.second()] = true;
|
|
valid.meridiem[(iTime.hour() > 11) ? 1 : 0] = true;
|
|
valid.length++;
|
|
iTime = iTime.add(step, 'second');
|
|
}
|
|
|
|
// Convert valid units to arrays
|
|
let unitText = { hour: [], minute: [], second:[], meridiem: [], length: valid.length };
|
|
for (let i = 0; i < 24; i++)
|
|
{
|
|
unitText.hour.push((i in valid.hour)
|
|
? ((i == 0 || i == 12) ? 12 : i % 12)
|
|
: null);
|
|
}
|
|
['minute', 'second'].forEach(function (field) {
|
|
for (let i = 0; i < 60; i++)
|
|
{
|
|
unitText[field].push((i in valid[field]) ? ((i < 10) ? '0' + i : i) : null);
|
|
}
|
|
});
|
|
unitText.meridiem.push((0 in valid.meridiem) ? 'AM' : null);
|
|
unitText.meridiem.push((1 in valid.meridiem) ? 'PM' : null);
|
|
/*
|
|
// Craate position arrays
|
|
for (let i = 1; i < 13; i++)
|
|
{
|
|
position.hour[i] = ((i % 12) in valid.hour || (i % 12 + 12) in valid.hour);
|
|
position.minute[i] = (i * 5 % 60) in valid.minute;
|
|
position.second[i] = (i * 5 % 60) in valid.second;
|
|
position.meridiem[i] = (i % 6 > 0) ? (((i < 6) ? 1 : 0) in valid.meridiem) : false;
|
|
}
|
|
*/
|
|
return unitText;
|
|
}
|
|
|
|
/**
|
|
* Return whether format string contains a token string
|
|
*
|
|
* This removes escaped characters from format string prior searching for token
|
|
*
|
|
* @param {string} format
|
|
* @param {string} searchElement
|
|
* @return {boolean}
|
|
*/
|
|
function hasFormat(format, searchElement)
|
|
{
|
|
return (format.replace(/\[[^\]]*\]/g).indexOf(searchElement) >= 0);
|
|
}
|
|
|
|
/**
|
|
* Update the view
|
|
*
|
|
* @param {object} $input the input object
|
|
*/
|
|
function updateView($input)
|
|
{
|
|
const prevView = $input.data('prevview');
|
|
const view = $input.data('view') || 'hour';
|
|
const viewTime = $input.data('viewtime');
|
|
const options = $input.data('options');
|
|
const clock_24 = hasFormat(options.format, 'H');
|
|
const step = options.step || 60;
|
|
let submit_disabled = false;
|
|
if (60 % step > 0)
|
|
{
|
|
const minTime = options.minTime || dayjs().startOf('day');
|
|
let viewOffset = viewTime.diff(viewTime.startOf('day'), 'second');
|
|
if (!hasFormat(options.format, 's'))
|
|
{
|
|
viewOffset -= (viewOffset % 60);
|
|
}
|
|
submit_disabled = ((viewOffset - minTime.diff(minTime.startOf('day'), 'second')) % step > 0);
|
|
}
|
|
const input_id = $input.attr('id');
|
|
const $content = jQuery('#' + input_id + '-picker-content').attr('data-view', view);
|
|
$content.find('.submit-btn').prop('disabled', submit_disabled
|
|
|| (options.minTime && viewTime.isBefore(options.minTime, 'second'))
|
|
|| (options.maxTime && viewTime.isAfter(options.maxTime, 'second')));
|
|
let number, position, format;
|
|
switch (view)
|
|
{
|
|
case 'hour':
|
|
number = viewTime.get(view);
|
|
position = (number % 12 > 0) ? (number % 12) : 12;
|
|
format = clock_24 ? 'H' : 'h';
|
|
if (hasFormat(options.format, format + format))
|
|
{
|
|
format += format;
|
|
}
|
|
break;
|
|
case 'minute':
|
|
case 'second':
|
|
number = viewTime.get(view);
|
|
if (number % 5 == 0)
|
|
{
|
|
position = (number > 0) ? (number / 5) : 12;
|
|
}
|
|
format = view.charAt(0).repeat(2);
|
|
break;
|
|
case 'meridiem':
|
|
position = (viewTime.hour() > 11) ? 3 : 9;
|
|
format = 'A';
|
|
break;
|
|
default:
|
|
console.warn('Invalid view ' + view);
|
|
return false;
|
|
}
|
|
const text = viewTime.format(format);
|
|
$content.find('.timepicker-btns button').removeClass('font-weight-bold').filter('[data-unit="' + view + '"]').addClass('font-weight-bold').text(text);
|
|
$content.find('.clock-input-table .chevron-btn').data('unit', view);
|
|
|
|
FORMATS[0][1] = clock_24 ? 'H' : 'h';
|
|
FORMATS.forEach(function (formats) {
|
|
const text = viewTime.format(formats[1]);
|
|
$content.find('.' + formats[2] + '-btn').text(text);
|
|
$content.find('.' + formats[2] + '-input').val(text);
|
|
});
|
|
|
|
if (view != prevView)
|
|
{
|
|
$content.find('.clock-input-table button').each(function () {
|
|
const $this = jQuery(this);
|
|
let pos = parseInt($this.attr('class').match(/\bpos\-(\d+)/)[1]);
|
|
let disabled = true;
|
|
if (pos > 0)
|
|
{
|
|
let positions;
|
|
switch (view)
|
|
{
|
|
case 'hour':
|
|
positions = [pos, pos % 12, pos % 12 + 12];
|
|
break;
|
|
case 'minute':
|
|
case 'second':
|
|
positions = [pos % 12 * 5];
|
|
break;
|
|
case 'meridiem':
|
|
positions = (pos % 6 > 0) ? [(pos < 6) ? 1 : 0] : [];
|
|
break;
|
|
}
|
|
positions.forEach(function (pos) {
|
|
disabled &&= (options.unitText[view][pos] === null);
|
|
});
|
|
$this.prop('disabled', disabled).toggleClass('text-light', disabled).toggleClass(INACTIVE_CLASS, pos != position).toggleClass(ACTIVE_CLASS, pos == position);
|
|
}
|
|
else
|
|
{
|
|
$this.text(text);
|
|
}
|
|
});
|
|
}
|
|
$content.data('prevview', view);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update the clock picker in the popover
|
|
*
|
|
* @param {object} $input the input object
|
|
*/
|
|
function updatePicker($input)
|
|
{
|
|
const input_id = $input.attr('id');
|
|
const options = $input.data('options');
|
|
const now = dayjs();
|
|
const minTime = options.minTime || dayjs().startOf('day');
|
|
const maxTime = options.maxTime || dayjs().endOf('day');
|
|
const step = options.step || 60;
|
|
let validSteps = { hour: {}, minute: {}, second: {}, meridiem: {} };
|
|
let viewTime = $input.data('viewtime');
|
|
let iTime = minTime.clone();
|
|
while (iTime.isBefore(maxTime) || iTime.isSame(maxTime, 'second'))
|
|
{
|
|
validSteps.hour[iTime.hour()] = true;
|
|
validSteps.minute[iTime.minute()] = true;
|
|
validSteps.second[iTime.second()] = true;
|
|
validSteps.meridiem[(iTime.hour() > 11) ? 1 : 0] = true;
|
|
if (!iTime.isBefore(minTime) && !iTime.isAfter(maxTime))
|
|
{
|
|
if (!viewTime && now.isBefore(iTime))
|
|
{
|
|
viewTime = iTime;
|
|
}
|
|
}
|
|
iTime = iTime.add(step, 'second');
|
|
}
|
|
if (!viewTime)
|
|
{
|
|
viewTime = now.endOf(hasFormat(options.format, 's') ? 'second' : 'minute');
|
|
}
|
|
$input.data('viewtime', viewTime);
|
|
|
|
// Build html
|
|
const has_second = (hasFormat(options.format, 's') && step % 60 > 0);
|
|
const clock_24 = hasFormat(options.format, 'H');
|
|
const clock_enabled = ((step % 300) == 0) && false;
|
|
const viewHour = viewTime.hour();
|
|
let html = '<div class="clock-input' + (clock_enabled ? '' : ' d-none') + '">'
|
|
+ '<div class="d-flex justify-content-center align-items-center timepicker-btns">'
|
|
+ '<div class="btn-group">'
|
|
+ '<button type="button" class="btn px-2 font-weight-bold hour-btn" data-unit="hour">' + viewTime.format('hh') + '</button>'
|
|
+ '<a class="btn px-0 disabled" href="javascript:void(0)" role="button" aria-disabled="true">:</a>'
|
|
+ '<button type="button" class="btn px-2 minute-btn" data-unit="minute"' + + ((step % 3600 > 0) ? '' : ' disabled="disabled"') + '>' + viewTime.format('mm') + '</button>'
|
|
+ (has_second ? '<a class="btn px-0 disabled" href="javascript:void(0)" role="button" aria-disabled="true">:</a><button type="button" id="' + input_id + '-picker-second-btn" class="btn px-2 second-btn" data-unit="second">' + viewTime.format('ss') + '</button>' : '')
|
|
+ (clock_24 ? '<button type="button" class="btn px-2 meridiem-btn" data-unit="meridiem">' + viewTime.format('A') + '</button>' : '')
|
|
+ '</div></div>'
|
|
+ ('<table class="clock-input-table table table-sm table-borderless timepicker-table mx-auto w-auto mb-1">'
|
|
+ '<thead class="thead-light"><tr><th class="text-center py-1" colspan="5"><span class="hour">Hour</span><span class="minute">Minute</span><span class="second">Second</span><span class="meridiem">Meridiem</span></th></tr></thead>'
|
|
+ '<tbody>'
|
|
+ '<tr>'
|
|
+ '<td></td>'
|
|
+ '<td class="text-right">{{11}}</td>'
|
|
+ '<td class="text-center">{{12}}</td>'
|
|
+ '<td class="text-left">{{1}}</td>'
|
|
+ '<td></td>'
|
|
+ '</tr><tr>'
|
|
+ '<td class="text-right">{{10}}</td>'
|
|
+ '<td class="text-center align-bottom" colspan="3"><a id="' + input_id + '-picker-add-link" class="btn btn-link px-1 mx-0 chevron-btn" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>'
|
|
+ '<td class="text-left">{{2}}</td>'
|
|
+ '</tr><tr>'
|
|
+ '<td class="text-right">{{9}}</td>'
|
|
+ '<td class="text-center" colspan="3">{{0}}</td>'
|
|
+ '<td class="text-left">{{3}}</td>'
|
|
+ '</tr><tr>'
|
|
+ '<td class="text-right">{{8}}</td>'
|
|
+ '<td class="text-center" colspan="3"><a id="' + input_id + '-picker-sub-link" class="btn btn-link px-1 mx-0 chevron-btn" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>'
|
|
+ '<td class="text-left">{{4}}</td>'
|
|
+ '</tr><tr>'
|
|
+ '<td></td>'
|
|
+ '<td class="text-right">{{7}}</td>'
|
|
+ '<td class="text-center">{{6}}</td>'
|
|
+ '<td class="text-left">{{5}}</td>'
|
|
+ '<td></td>'
|
|
+ '</tr></tbody></table>').replace(/{{(\w+)}}/g, function (match, position) {
|
|
if (position == 0)
|
|
{
|
|
return '<button type="button" id="'+ input_id + '-picker-center-btn" class="btn btn-outline-dark border-white font-weight-bold pos-0" data-hour="' + viewHour + '"'
|
|
+ (viewHour in validSteps.hour ? '' : ' disabled="disabled"')
|
|
+ '>' + ((viewHour % 12 > 0) ? viewHour % 12 : 12) + '</button>';
|
|
}
|
|
|
|
const pos_hour = position % 12;
|
|
const pos_minute = pos_hour * 5;
|
|
|
|
const meridiem_class = (position % 3 > 0) ? ' text-light' : '';
|
|
const meridiem_text = (position % 6 > 0) ? ((position < 6) ? 'PM' : 'AM') : ' ';
|
|
return '<button type="button" class="btn px-1 '
|
|
+ ((viewHour % 12 == position % 12) ? 'active btn-info' : 'btn-outline-dark border-white')
|
|
+ ' pos-' + position + '" '
|
|
+ (pos_hour in validSteps.hour ? '' : ' disabled="disabled"')
|
|
+ '><span class="hour">' + position + '</span><span class="minute second">' + (pos_minute > 9 ? '' : '0') + pos_minute + '</span><span class="meridiem' + meridiem_class + '">' + meridiem_text
|
|
+ '</span></button>';
|
|
})
|
|
+ '</div><div class="keyboard-input' + (clock_enabled ? ' d-none' : '') + '"><table class="mx-auto text-center"><tr>'
|
|
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="hour" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>'
|
|
+ '<td></td>'
|
|
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="minute" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>'
|
|
+ (has_second ? '<td></td><td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="second" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>' : '')
|
|
+ (clock_24 ? '' : '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="meridiem" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>')
|
|
+ '</tr><tr>'
|
|
+ '<td><input type="text" class="form-control text-center border-light hour-input" minlength="1" maxlength="2" inputmode="numeric" /></td><td>:</td><td><input type="text" class="form-control text-center border-light minute-input" minlength="1" maxlength="2" inputmode="numeric" /></td>'
|
|
+ (has_second ? '<td>:</td><td><input type="text" class="form-control text-center border-light second-input" minlength="1" maxlength="2" inputmode="numeric" /></td>' : '')
|
|
+ (clock_24 ? '' : '<td><button type="button" class="btn meridiem-btn border-light"></button></td>')
|
|
+ '</tr><tr>'
|
|
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="hour" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>'
|
|
+ '<td></td>'
|
|
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="minute" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>'
|
|
+ (has_second ? '<td></td><td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="second" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>' : '')
|
|
+ (clock_24 ? '' : '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="meridiem" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>')
|
|
+ '</tr></table></div>'
|
|
+ '<div class="d-flex justify-content-between">'
|
|
+ '<div class="invisible"><button type="button" class="btn btn-link input-toggle-btn' + (clock_enabled ? ' d-none' : '') + '" data-input="clock"><i class="far fa-clock fa-fw"></i></button><button type="button" class="btn btn-link input-toggle-btn' + (clock_enabled ? '' : ' d-none') + '" data-input="keyboard"><i class="far fa-keyboard fa-fw"></i></button></div>'
|
|
+ '<div><button type="button" class="btn btn-secondary mx-1 cancel-btn" data-dismiss="popover" title="Cancel"><i class="fas fa-times fa-fw"></i></button><button type="button" class="btn btn-primary mx-1 submit-btn" title="OK"><i class="fas fa-check fa-fw"></i></button></div>'
|
|
+ '</div>';
|
|
|
|
const $content = jQuery('#' + input_id + '-picker-content');
|
|
const $table = $content.html(html).find('.clock-input-table');
|
|
const $center_btn = jQuery('#' + input_id + '-picker-center-btn');
|
|
$content.parents('.timepicker-popover').attr('data-scheme', (options.scheme == 'auto') ? ((window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : '') : options.scheme);
|
|
$content.find('.timepicker-btns button').on('click', function () {
|
|
const unit = jQuery(this).blur().data('unit');
|
|
$content.find('.clock-input-table .chevron-btn').data('unit', unit);
|
|
updateView($input.data('view', unit));
|
|
});
|
|
const $hour_input = $content.find('.hour-input').on('change', function () {
|
|
let hour = this.value.replace(/\D+/, '');
|
|
if (hour.length > 0)
|
|
{
|
|
hour = parseInt(hour);
|
|
if (hour > -1 && hour < 24 && hour in options.unitText.hour)
|
|
{
|
|
$input.data('viewtime', $input.data('viewtime').hour(hour));
|
|
}
|
|
}
|
|
updateView($input);
|
|
});
|
|
$hour_input.add($content.find('.minute-input, .second-input').on('change', function () {
|
|
let number = this.value.replace(/\D+/, '');
|
|
if (number.length > 0)
|
|
{
|
|
number = parseInt(number);
|
|
const unit = jQuery(this).attr('class').match(/(minute|second)\-input/)[1];
|
|
if (number > -1 && number < 60 && number in options.unitText[unit])
|
|
{
|
|
$input.data('viewtime', $input.data('viewtime').set(unit, number));
|
|
}
|
|
}
|
|
updateView($input);
|
|
})).on('keyup', function (event) {
|
|
if (event.key == 'Enter')
|
|
{
|
|
$content.find('.submit-btn').triggerHandler('click');
|
|
}
|
|
});
|
|
|
|
$content.find('.meridiem-btn').on('click', function () {
|
|
$input.data('viewtime', $input.data('viewtime').hour(($input.data('viewtime').hour() + 12) % 24));
|
|
updateView($input);
|
|
}).on('keydown', function (event) {
|
|
const key = event.key.toUpperCase();
|
|
const hour = $input.data('viewtime').hour();
|
|
let offset = 0;
|
|
if (key == 'A' && hour > 12)
|
|
{
|
|
offset = -12;
|
|
}
|
|
else if (key == 'P' && hour < 11)
|
|
{
|
|
offset = 12;
|
|
}
|
|
if (offset != 0)
|
|
{
|
|
$input.data('viewtime', $input.data('viewtime').hour(hour + offset));
|
|
updateView($input);
|
|
}
|
|
});
|
|
$content.find('.input-toggle-btn').attr('tabindex', -1).on('click', function () {
|
|
const input = jQuery(this).data('input');
|
|
$content.find('.clock-input').toggleClass('d-none', input != 'clock');
|
|
$content.find('.keyboard-input').toggleClass('d-none', input != 'keyboard');
|
|
$content.find('.input-toggle-btn').each(function () {
|
|
const $this = jQuery(this);
|
|
$this.toggleClass('d-none', $this.data('input') == input);
|
|
});
|
|
$input.popover('update');
|
|
$hour_input.select();
|
|
});
|
|
$content.find('.cancel-btn').on('click', function () {
|
|
$input.popover('hide');
|
|
});
|
|
$content.find('.submit-btn').on('click', function () {
|
|
$input.val($input.data('viewtime').format($input.data('options').format)).popover('hide').data('view', null).trigger('change');
|
|
});
|
|
$table.find('button').on('click', function () {
|
|
const $this = jQuery(this);
|
|
let viewTime = $input.data('viewtime');
|
|
let position = $this.attr('class').match(/\bpos\-(\d+)/)[1];
|
|
const view = $input.data('view') || 'hour';
|
|
switch (view)
|
|
{
|
|
case 'hour':
|
|
if (position > 0)
|
|
{
|
|
viewTime = viewTime.hour(position % 12 + ((viewTime.hour() > 11) ? 12 : 0));
|
|
}
|
|
break;
|
|
case 'minute':
|
|
if (position > 0)
|
|
{
|
|
viewTime = viewTime.minute(position % 12 * 5);
|
|
}
|
|
break;
|
|
case 'second':
|
|
if (position > 0)
|
|
{
|
|
viewTime = viewTime.second(position % 12 * 5);
|
|
}
|
|
break;
|
|
case 'meridiem':
|
|
if (position > 0)
|
|
{
|
|
viewTime = viewTime.hour(viewTime.hour() % 12 + ((position < 6) ? 12 : 0));
|
|
}
|
|
break;
|
|
}
|
|
$input.data('viewtime', viewTime);
|
|
let picked = true;
|
|
FORMATS.forEach(function (formats) {
|
|
const regex = new RegExp(formats[0], 'i');
|
|
if (picked && regex.test(options.format))
|
|
{
|
|
position = null;
|
|
let number;
|
|
const nextUnit = formats[2];
|
|
switch (nextUnit)
|
|
{
|
|
case 'hour':
|
|
number = viewTime.get(nextUnit) % 12;
|
|
position = (number > 0) ? number : 12;
|
|
break;
|
|
case 'minute':
|
|
case 'second':
|
|
number = viewTime.get(nextUnit);
|
|
if (number % 5 == 0)
|
|
{
|
|
position = (number > 0) ? (number / 5) : 12;
|
|
}
|
|
break;
|
|
case 'meridiem':
|
|
position = (viewTime.hour() > 11) ? 3 : 9;
|
|
break;
|
|
}
|
|
$input.data('view', nextUnit);
|
|
picked = false;
|
|
}
|
|
});
|
|
|
|
if (picked)
|
|
{
|
|
$input.val(viewTime.format(options.format)).popover('hide').data('view', null).trigger('change');
|
|
}
|
|
else
|
|
{
|
|
updateView($input);
|
|
}
|
|
});
|
|
$content.find('.chevron-btn').attr('tabindex', -1).on('click', function () {
|
|
const $this = jQuery(this).blur();
|
|
const options = $input.data('options');
|
|
const unit = $this.data('unit') || 'hour';
|
|
const step = $this.data('step');
|
|
const viewTime = $input.data('viewtime');
|
|
const number = (unit == 'meridiem')
|
|
? ((viewTime.hour() > 11) ? 1 : 0)
|
|
: viewTime.get(unit);
|
|
if (!(number in options.unitText[unit]))
|
|
{
|
|
// Set to number
|
|
}
|
|
if (options.unitText[unit].length < 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const unitLength = UNIT_LENGTHS[unit];
|
|
let idx = number;
|
|
switch (unit)
|
|
{
|
|
case 'hour':
|
|
case 'minute':
|
|
case 'second':
|
|
do
|
|
{
|
|
idx = (idx + step + unitLength) % unitLength;
|
|
if (options.unitText[unit][idx] !== null)
|
|
{
|
|
$center_btn.text(options.unitText[unit][idx]);
|
|
$input.data('viewtime', viewTime.set(unit, idx));
|
|
break;
|
|
}
|
|
}
|
|
while (idx != number);
|
|
break;
|
|
case 'meridiem':
|
|
idx = (idx + step + unitLength) % unitLength;
|
|
if (options.unitText[unit][idx] !== null)
|
|
{
|
|
$center_btn.text(options.unitText[unit][idx]);
|
|
$input.data('viewtime', viewTime.hour(viewTime.hour() % 12 + ((idx > 0) ? 12 : 0)));
|
|
}
|
|
break;
|
|
}
|
|
updateView($input);
|
|
});
|
|
updateView($input);
|
|
}
|
|
|
|
/**
|
|
* Add method for initializing plugin
|
|
*/
|
|
jQuery.fn.timepicker = function (options) {
|
|
// Get boostrap version
|
|
const bs_version = parseInt(((typeof bootstrap == 'object') ? bootstrap.Dropdown.VERSION : jQuery.fn.dropdown.Constructor.VERSION || '0').replace(/\..+$/, ''));
|
|
if (bs_version < 4)
|
|
{
|
|
console.error('Invalid bootstrap version ' + bs_version + ' detected');
|
|
}
|
|
|
|
// Handle functions
|
|
if (typeof options == 'string')
|
|
{
|
|
if (this.length < 1)
|
|
{
|
|
return undefined;
|
|
}
|
|
let input_options = this.data('options') || {};
|
|
const single_arg = (arguments.length == 1);
|
|
switch (options)
|
|
{
|
|
case 'format':
|
|
if (single_arg)
|
|
{
|
|
return input_options.format;
|
|
}
|
|
else if (arguments[1] && typeof arguments[1] == 'string')
|
|
{
|
|
input_options.format = arguments[1];
|
|
this.data('options', input_options);
|
|
}
|
|
else
|
|
{
|
|
console.warn('Invalid format');
|
|
}
|
|
break;
|
|
case 'defaultTime':
|
|
case 'minTime':
|
|
case 'maxTime':
|
|
if (single_arg)
|
|
{
|
|
return input_options[options];
|
|
}
|
|
else if (arguments[1])
|
|
{
|
|
let newTime = parseTime(arguments[1]);
|
|
if (newTime && newTime.isValid())
|
|
{
|
|
if (options == 'defaultTime')
|
|
{
|
|
if (input_options.minTime && newTime.isBefore(input_options.minTime))
|
|
{
|
|
newTime = false;
|
|
console.warn('defaultTime is before minTime');
|
|
}
|
|
else if (input_options.maxTime && newTime.isAfter(input_options.maxTime))
|
|
{
|
|
newTime = false;
|
|
console.warn('defaultTime is after maxTime');
|
|
}
|
|
}
|
|
if (newTime)
|
|
{
|
|
input_options[options] = newTime;
|
|
input_options.unitText = getUnitText(input_options);
|
|
this.data('options', input_options);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
console.warn('Invalid ' + options);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
input_options[options] = null;
|
|
input_options.unitText = getUnitText(input_options);
|
|
this.data('options', input_options);
|
|
}
|
|
break;
|
|
case 'step':
|
|
if (single_arg)
|
|
{
|
|
return input_options[options];
|
|
}
|
|
else if (arguments[1])
|
|
{
|
|
const step = parseInt(arguments[1]);
|
|
if (step > 0 && step < 86400
|
|
&& step % (hasFormat(input_options.format, 's') ? 1 : 60) == 0)
|
|
{
|
|
input_options.step = step;
|
|
input_options.unitText = getUnitText(input_options);
|
|
this.data('options', input_options);
|
|
}
|
|
else
|
|
{
|
|
console.warn('Invalid ' + options);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
input_options.step = 60;
|
|
input_options.unitText = getUnitText(input_options);
|
|
this.data('options', input_options);
|
|
}
|
|
break;
|
|
case 'scheme':
|
|
if (single_arg)
|
|
{
|
|
return input_options.scheme;
|
|
}
|
|
else if (arguments[1] === null || typeof arguments[1] == 'string')
|
|
{
|
|
input_options.scheme = arguments[1];
|
|
this.data('options', input_options);
|
|
}
|
|
else
|
|
{
|
|
console.warn('Invalid scheme');
|
|
}
|
|
break;
|
|
case 'time':
|
|
if (single_arg)
|
|
{
|
|
return parseTime(this.val(), input_options) || null;
|
|
}
|
|
else
|
|
{
|
|
const newTime = (arguments[1]) ? parseTime(arguments[1], input_options) : null;
|
|
this.val((newTime && newTime.isValid()) ? newTime.format(input_options.format) : '');
|
|
}
|
|
break;
|
|
case 'viewTime':
|
|
if (single_arg)
|
|
{
|
|
return this.data('viewtime');
|
|
}
|
|
else
|
|
{
|
|
const newTime = (arguments[1]) ? parseTime(arguments[1], input_options) : null;
|
|
this.data('viewtime', newTime);
|
|
}
|
|
break;
|
|
case 'view':
|
|
if (single_arg)
|
|
{
|
|
return this.data('view');
|
|
}
|
|
else
|
|
{
|
|
const view = arguments[1];
|
|
updateView(jQuery(this).data('view', view));
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
// Initialize code if it hasn't already
|
|
if (!initialized)
|
|
{
|
|
initialized = true;
|
|
let table_class = '.timepicker-table ';
|
|
jQuery(document.head).append('<style id="timepicker-style">'
|
|
+ '.timepicker-popover { font-size: inherit; }'
|
|
+ '.timepicker-popover .btn-link:hover { background-color: #e2e6ea; }'
|
|
+ '.timepicker-popover .meridiem-btn { min-width: 3.2rem; }'
|
|
+ '.timepicker-popover .meridiem-btn:hover { box-shadow: 0 0 0 .2rem rgba(0,123,255,.25); }'
|
|
+ '.timepicker-popover input { width: 3rem; }'
|
|
+ '.timepicker-btns .btn:hover { background-color: #e2e6ea; color: #000; }'
|
|
+ '.timepicker-content span.hour, .timepicker-content span.minute, .timepicker-content span.second, .timepicker-content span.meridiem { display: none; }'
|
|
+ '.timepicker-content[data-view="hour"] span.hour,'
|
|
+ '.timepicker-content[data-view="minute"] span.minute,'
|
|
+ '.timepicker-content[data-view="second"] span.second,'
|
|
+ '.timepicker-content[data-view="meridiem"] span.meridiem { display: inline; }'
|
|
+ table_class + 'td button:focus { box-shadow: none !important; }'
|
|
+ table_class + 'td button:not(:disabled):hover { background-color: #6c757d !important; border-color: #6c757d !important; color: #fff; }'
|
|
+ table_class + 'td button:disabled { cursor: not-allowed; }'
|
|
+ table_class + 'td button.today { background-color: #fcf8e3; }'
|
|
+ table_class + 'td, .timepicker-table th { padding: 0; }'
|
|
+ table_class + 'button.btn { min-width: 2.3rem; }'
|
|
+ table_class + 'button:focus { box-shadow: none !important; }'
|
|
+ table_class + 'button:not(:disabled):hover { background-color: #6c757d !important; border-color: #6c757d !important; color: #fff; }'
|
|
+ table_class + 'td button.pos-1 { margin: .6rem 0 0 .4rem; }'
|
|
+ table_class + 'td button.pos-2 { margin: 0 .7rem .3rem 0; }'
|
|
+ table_class + 'td button.pos-3 { margin: 0 0 0 .6rem; }'
|
|
+ table_class + 'td button.pos-4 { margin: .3rem .7rem 0 0; }'
|
|
+ table_class + 'td button.pos-5 { margin: 0 0 .6rem .4rem; }'
|
|
+ table_class + 'td button.pos-6 { margin: .8rem 0 0 0; }'
|
|
+ table_class + 'td button.pos-7 { margin: 0 .4rem .6rem 0; }'
|
|
+ table_class + 'td button.pos-8 { margin: .3rem 0 0 .7rem; }'
|
|
+ table_class + 'td button.pos-9 { margin: 0 .6rem 0 0; }'
|
|
+ table_class + 'td button.pos-10 { margin: 0 0 .3rem .7rem; }'
|
|
+ table_class + 'td button.pos-11 { margin: .6rem .4rem 0 0; }'
|
|
+ table_class + 'td button.pos-12 { margin: 0 0 .6rem 0; }'
|
|
+ '.timepicker-popover[data-scheme="dark"] { background-color: #000000; border-color: #ffffff; color: #dee2e6; }'
|
|
+ '.timepicker-popover[data-scheme="dark"] .popover-header { background-color: #343a40; color: #ffffff; }'
|
|
+ '.timepicker-popover[data-scheme="dark"] .popover-header .close { filter: invert(1) grayscale(1) brightness(2); }'
|
|
+ '.timepicker-popover[data-scheme="dark"] .border-light { background-color: #000000; border-color: #6c757d !important; color: #ffffff; }'
|
|
+ '.timepicker-popover[data-scheme="dark"] input.border-light:focus { background-color: inherit; border-color: #86b7fe !important; color: #ffffff; }'
|
|
+ '.timepicker-popover[data-scheme="dark"] .meridiem-btn { color: #ffffff; }'
|
|
+ '.timepicker-popover[data-scheme="dark"] .meridiem-btn:hover { border-color: #86b7fe !important; }'
|
|
+ '</style>');
|
|
|
|
// Make popovers close when clicked outside of them
|
|
jQuery(document.body).on('mouseup', function (e) {
|
|
if (jQuery(e.target).parents('.popover').length == 0)
|
|
{
|
|
jQuery('.timepicker').popover('hide');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Process options
|
|
if (typeof options == 'undefined')
|
|
{
|
|
options = {};
|
|
}
|
|
const common_options = jQuery.extend({}, jQuery.fn.timepicker.defaults, options);
|
|
['minTime', 'maxTime'].forEach(function (option) {
|
|
if (common_options[option])
|
|
{
|
|
common_options[option] = parseTime(common_options[option]);
|
|
}
|
|
});
|
|
|
|
// Initialize the inputs
|
|
return this.each(function () {
|
|
const $input = jQuery(this);
|
|
|
|
// Get input id
|
|
let input_id = this.id;
|
|
let $toggles = $input.siblings().find('[data-toggle="timepicker"]:not([data-target])');
|
|
if (this.id)
|
|
{
|
|
$toggles = $toggles.add('[data-toggle="timepicker"][data-target="#' + this.id + '"]');
|
|
}
|
|
else
|
|
{
|
|
input_id = 'input-' + Math.floor(Math.random() * 1000000 + 1);
|
|
this.id = input_id;
|
|
}
|
|
|
|
// Process options
|
|
let input_options = jQuery.extend(true, {}, common_options);
|
|
let format = $input.data('format') || common_options.format;
|
|
if (format)
|
|
{
|
|
input_options.format = format;
|
|
}
|
|
let minTime = $input.attr('min') || $input.data('mintime') || common_options.minTime;
|
|
if (minTime && (minTime = parseTime(minTime)) && minTime.isValid())
|
|
{
|
|
input_options.minTime = minTime;
|
|
}
|
|
let maxTime = $input.attr('max') || $input.data('maxtime') || common_options.maxTime;
|
|
if (maxTime && (maxTime = parseTime(maxTime)) && maxTime.isValid())
|
|
{
|
|
input_options.maxTime = maxTime;
|
|
}
|
|
let defaultTime = $input.data('default') || common_options.defaultTime;
|
|
if (defaultTime && (defaultTime = parseTime(defaultTime)) && defaultTime.isValid()
|
|
&& !(input_options.minTime && defaultTime.isBefore(input_options.minTime))
|
|
&& !(input_options.maxTime && defaultTime.isAfter(input_options.maxTime)))
|
|
{
|
|
input_options.defaultTime = defaultTime;
|
|
}
|
|
const step = $input.attr('step') || $input.data('step') || common_options.step;
|
|
if (step > 0 && step < 86400 && 60 % step > 0)
|
|
{
|
|
input_options.step = parseInt(step);
|
|
}
|
|
const scheme = $input.data('scheme') || common_options.scheme;
|
|
if (scheme)
|
|
{
|
|
input_options.scheme = scheme;
|
|
}
|
|
input_options.unitText = getUnitText(input_options);
|
|
$input.data('options', input_options);
|
|
if ($input.data('timepicker'))
|
|
{
|
|
// If timepicker is already initialized, then return
|
|
return this;
|
|
}
|
|
$input.data('timepicker', true).addClass('timepicker');
|
|
|
|
// Set inputmode
|
|
if (this.type == 'text' && !this.inputMode)
|
|
{
|
|
this.inputMode = 'tel';
|
|
}
|
|
|
|
const $label = jQuery('label[for="' + input_id + '"]');
|
|
const placement = (window.screen.width > 575) ? 'bottom' : 'top';
|
|
$input.on('change', function () {
|
|
this.value = this.value.replace(/^\s+|\s+$/g, '');
|
|
const options = $input.data('options');
|
|
const newTime = parseTime(this.value, options);
|
|
this.value = (newTime !== false) ? newTime.format(options.format) : '';
|
|
}).on('shown.bs.popover', function () {
|
|
if (window.screen.width > 575)
|
|
{
|
|
jQuery('#' + input_id + '-picker-content').find('.hour-input').select();
|
|
}
|
|
}).on('inserted.bs.popover', function () {
|
|
jQuery('.popover').find('[data-dismiss="popover"]').on('click', function () {
|
|
$input.popover('hide');
|
|
});
|
|
updatePicker($input, input_options);
|
|
}).on('hide.bs.popover', function () {
|
|
$input.data('view', null);
|
|
}).popover({
|
|
html: true,
|
|
placement: placement,
|
|
sanitize: false,
|
|
title: '<button class="close mt-n1" data-dismiss="popover">×</button>' + (($label.length > 0) ? $label.html() : 'Time'),
|
|
template: '<div id="' + input_id + '-picker-popover" class="popover timepicker-popover bs-popover-"' + placement + '" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div id="' + input_id + '-popover-body" class="popover-body border-bottom"></div><div class="popover-footer bg-light text-right px-3 py-2 rounded-lg" hidden="hidden"><button type="button" class="btn btn-secondary btn-sm" title="Close the picker" data-dismiss="popover"><i class="fas fa-times"></i> Close</button></div></div>',
|
|
trigger: (($toggles.length > 0) ? 'manual' : 'click'),
|
|
popperConfig: {
|
|
/*
|
|
modifiers: {
|
|
hide: {
|
|
enabled: false
|
|
},
|
|
preventOverflow: {
|
|
enabled: false,
|
|
// boundariesElement: 'window',
|
|
escapeWithReference: true
|
|
}
|
|
},
|
|
// positionFixed: true
|
|
*/
|
|
},
|
|
content: function () {
|
|
const options = $input.data('options');
|
|
const viewTime = parseTime($input.val() || options.defaultTime || '', options);
|
|
$input.data('viewtime', viewTime);
|
|
return '<div id="' + input_id + '-picker-content" class="timepicker-content" data-view="hour"></div>';
|
|
}
|
|
});
|
|
|
|
$toggles.on('click', function () {
|
|
$input.popover('toggle');
|
|
this.blur();
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Default options
|
|
*
|
|
* @type {object}
|
|
* @todo add support for additional options
|
|
*/
|
|
jQuery.fn.timepicker.defaults = {
|
|
defaultTime: null,
|
|
format: 'hh:mm A',
|
|
maxTime: null,
|
|
minTime: null,
|
|
step: 60,
|
|
scheme: 'light'
|
|
};
|
|
|
|
/*
|
|
* Initialize timepickers
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
jQuery('[data-toggle="timepicker"][data-target]').each(function () {
|
|
jQuery(jQuery(this).data('target')).timepicker();
|
|
});
|
|
});
|
|
|
|
//-------------------------------------
|
|
}());
|