/**
* This is jsdoc 'auto' documentation see https://jsdoc.app/
* npm install -g jsdoc
* jsdoc index.js -d ./docs/
*
*
* @fileoverview INPUT-DURATION
* @file INPUT-DURATION
* @author Stephan Luis
* @see <a href="https://www.npmjs.com/package/input-duration">NPM input-duration</a>
* @see <a href="https://github.com/StephanLuis/input-duration">GitHub input-duration</a>
* @copyright MIT License, Copyright (c) 2022
* @version 2.2.8 (release)
*/
/**
* Class for the Input-Duration webcomponent
*
* Boolean attributes `show-days`, `no-hours`, `no-minutes`, `no-seconds`, and
* `no-microseconds` control field visibility. String attributes
* `hours-divider`, `minutes-divider`, `seconds-divider`, and
* `microseconds-divider` customize the separators; `days-divider` customizes
* the day label. Divider values are rendered as text.
* The days field grows to fit larger counts and has extra spacing before its
* divider to emphasize the less-common unit.
* User edits update the public value and form state when the field changes,
* after its intermediate input has been normalized.
* The element is form-associated in supporting browsers; use `name`,
* `required`, and `disabled` attributes for form submission and validation.
*
* @example
* <input-duration no-microseconds hours-divider="h " microseconds-divider="ms"></input-duration>
*
* @class InputDuration
* @extends HTMLElement
*/
class InputDuration extends HTMLElement {
static formAssociated = true;
/**
* HTML attributes that update the component when changed.
*
* @static
* @returns {string[]} Observed value, required, visibility, and divider attributes.
*/
static get observedAttributes() {
return [
'value',
'required',
'show-days',
'no-hours',
'no-minutes',
'no-seconds',
'no-microseconds',
'days-divider',
'hours-divider',
'minutes-divider',
'seconds-divider',
'microseconds-divider'
];
}
constructor() {
super();
this.valueIsSet = false;
this.formInternals = typeof this.attachInternals === 'function'
? this.attachInternals()
: null;
/**
* Shadow root containing this component's controls.
* @type {ShadowRoot}
*/
this.myElement = this.attachShadow({ mode: 'open' });
this.boot();
} // end constructor
/**
* Updates the rendered control when an observed attribute changes.
*
* @param {string} name - Changed attribute name.
* @param {?string} oldValue - Previous attribute value.
* @param {?string} newValue - New attribute value, or null when removed.
* @returns {void}
*/
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
if (name !== 'value') {
if (name === 'required') {
this.updateFormValue();
} else if (name === 'show-days') {
if (newValue !== null && Number(this.myElement.querySelector('.sD').value) === 0) {
const hours = Number(this.myElement.querySelector('.sH').value);
this.myElement.querySelector('.sD').value = String(Math.floor(hours / 24));
this.myElement.querySelector('.sH').value = String(hours % 24).padStart(2, '0');
}
this.updateVisibleFields();
this.updateDividers();
this.committedValue = this.readInputValue();
this.updateFormValue();
} else {
this.updateVisibleFields();
this.updateDividers();
}
return;
}
if (newValue === null) {
this.resetInputs();
} else if (this.isValidValue(newValue)) {
this.setInputValues(newValue);
} else {
this.warnInvalidValue(newValue);
}
}
/**
* Gets the last committed duration value.
* @returns {?string} Duration in `HH:mm:ss.SSS` or `D:HH:mm:ss.SSS` format.
*/
get value() {
return this.committedValue ?? this.readInputValue();
}
/**
* Reads and serializes the current input fields, including uncommitted edits.
*
* @returns {?string} Duration in the current display format.
* @private
*/
readInputValue() {
const hoursInput = this.myElement.querySelector('input[name="startHours"]');
if (hoursInput === null) {
return null;
}
const days = Number(this.myElement.querySelector('.sD').value) || 0;
const hours = hoursInput.value;
const minutes = this.myElement.querySelector('input[name="startMinutes"]').value;
const seconds = this.myElement.querySelector('input[name="startSeconds"]').value;
const milliseconds = this.myElement.querySelector('input[name="startMilliSecs"]').value;
if (this.hasAttribute('show-days') || days > 0) {
return `${days}:${hours}:${minutes}:${seconds}.${milliseconds}`;
}
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
/**
* Gets a normalized, unit-labeled duration.
*
* @returns {?string} Duration such as `2d 03h 04m 05.006s`.
*/
get dayFriendlyValue() {
if (this.value === null) {
return null;
}
const parts = this.value.split(/[:.]/);
const hasDays = parts.length === 5;
const daysInput = hasDays ? Number(parts[0]) : 0;
const hoursInput = Number(parts[hasDays ? 1 : 0]) || 0;
const totalHours = daysInput * 24 + hoursInput;
const days = Math.floor(totalHours / 24);
const hours = totalHours % 24;
const minutes = Number(parts[hasDays ? 2 : 1]) || 0;
const seconds = Number(parts[hasDays ? 3 : 2]) || 0;
const milliseconds = Number(parts[hasDays ? 4 : 3]) || 0;
return `${days}d ${String(hours).padStart(2, '0')}h ${String(minutes).padStart(2, '0')}m ${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(3, '0')}s`;
}
/**
* Current form validity state.
*
* @returns {?ValidityState} The browser-computed validity state.
*/
get validity() {
return this.formInternals ? this.formInternals.validity : null;
}
/**
* Validation message for the current form state.
*
* @returns {string} The browser-computed validation message.
*/
get validationMessage() {
return this.formInternals ? this.formInternals.validationMessage : '';
}
/**
* Whether this control participates in constraint validation.
*
* @returns {boolean} Whether the control will be validated.
*/
get willValidate() {
return this.formInternals ? this.formInternals.willValidate : false;
}
/**
* Runs constraint validation and fires an invalid event when needed.
*
* @returns {boolean} Whether the control is valid.
*/
checkValidity() {
return this.formInternals ? this.formInternals.checkValidity() : true;
}
/**
* Runs constraint validation and asks the browser to report any error.
*
* @returns {boolean} Whether the control is valid.
*/
reportValidity() {
return this.formInternals ? this.formInternals.reportValidity() : true;
}
/**
* Whether the control is disabled.
*
* @returns {boolean} The value of the `disabled` attribute.
*/
get disabled() {
return this.hasAttribute('disabled');
}
/**
* Sets whether the control is disabled.
*
* @param {boolean} value - Whether to disable the control.
*/
set disabled(value) {
this.toggleAttribute('disabled', Boolean(value));
}
/**
* Sets the duration using `HH:mm:ss.SSS` or `D:HH:mm:ss.SSS` format.
*
* @param {string} val - Duration string, such as `05:05:05.005`.
* @returns {void}
*/
set value(val) {
// for setting the value in js (like document.querySelector("#bobPrepop").value = "05:05:05.005")
if (!val) {
this.resetInputs();
this.removeAttribute('value');
return;
}
if (typeof val !== 'string' || !this.isValidValue(val)) {
this.warnInvalidValue(val);
return;
}
this.setInputValues(val);
if (this.getAttribute('value') !== val) {
this.setAttribute('value', val);
}
}
/**
* Checks whether a value follows the supported duration format and ranges.
*
* @param {*} val - Candidate value.
* @returns {boolean} Whether the value follows a supported duration format.
*/
isValidValue(val) {
if (typeof val !== 'string') {
return false;
}
const parts = val.split(/[:.]/);
if (parts.length === 4) {
return /^[0-9]{2}$/.test(parts[0]) &&
/^[0-5][0-9]$/.test(parts[1]) &&
/^[0-5][0-9]$/.test(parts[2]) &&
/^[0-9]{3}$/.test(parts[3]);
}
return parts.length === 5 &&
/^[0-9]+$/.test(parts[0]) &&
Number.isSafeInteger(Number(parts[0])) &&
Number(parts[0]) <= Math.floor(Number.MAX_SAFE_INTEGER / 24) &&
/^(?:[01][0-9]|2[0-3])$/.test(parts[1]) &&
/^[0-5][0-9]$/.test(parts[2]) &&
/^[0-5][0-9]$/.test(parts[3]) &&
/^[0-9]{3}$/.test(parts[4]);
}
/**
* Copies a validated duration string into the shadow inputs.
*
* @param {string} val - Valid duration in `HH:mm:ss.SSS` format.
* @returns {void}
*/
setInputValues(val) {
this.valueIsSet = true;
const parts = val.split(/[:.]/);
const hasDays = parts.length === 5;
const days = hasDays ? Number(parts[0]) : 0;
let hours = Number(parts[hasDays ? 1 : 0]);
if (!hasDays && this.hasAttribute('show-days')) {
this.myElement.querySelector('.sD').value = String(Math.floor(hours / 24));
hours %= 24;
} else {
this.myElement.querySelector('.sD').value = String(days);
}
const daysInput = this.myElement.querySelector('.sD');
daysInput.oldValue = daysInput.value;
daysInput.style.width = `${Math.max(2, daysInput.value.length)}ch`;
const values = [String(hours).padStart(2, '0'), parts[hasDays ? 2 : 1], parts[hasDays ? 3 : 2], parts[hasDays ? 4 : 3]];
const inputNames = ['startHours', 'startMinutes', 'startSeconds', 'startMilliSecs'];
inputNames.forEach((name, index) => {
const input = this.myElement.querySelector(`input[name="${name}"]`);
input.value = values[index];
input.oldValue = input.value;
});
this.updateVisibleFields();
this.updateDividers();
this.committedValue = this.readInputValue();
this.updateFormValue();
}
/**
* Resets all duration fields to zero.
*
* @returns {void}
*/
resetInputs(valueIsSet = false) {
const defaults = ['0', '00', '00', '00', '000'];
const inputs = this.myElement.querySelectorAll('.timeCase input');
this.valueIsSet = valueIsSet;
inputs.forEach((input, index) => {
input.value = defaults[index];
input.oldValue = input.value;
});
this.updateVisibleFields();
this.updateDividers();
this.committedValue = this.readInputValue();
this.updateFormValue();
}
/**
* Keeps the form-associated value synchronized with the displayed duration.
*
* @returns {void}
*/
updateFormValue() {
if (this.formInternals) {
const value = this.value;
const isValid = this.isValidValue(value);
const valueMissing = this.hasAttribute('required') && !this.valueIsSet;
const validity = {};
if (valueMissing) {
validity.valueMissing = true;
} else if (!isValid) {
validity.badInput = true;
}
if (Object.keys(validity).length) {
const message = valueMissing
? 'Please enter a duration.'
: 'Enter a duration in HH:mm:ss.SSS or D:HH:mm:ss.SSS format.';
const anchor = [...this.myElement.querySelectorAll('.timeCase input')]
.find(input => input.style.display !== 'none' && !input.disabled);
if (anchor) {
this.formInternals.setValidity(validity, message, anchor);
} else {
this.formInternals.setValidity(validity, message);
}
} else {
this.formInternals.setValidity({});
}
const state = JSON.stringify({
value,
valueIsSet: Boolean(this.valueIsSet)
});
this.formInternals.setFormValue(isValid && !valueMissing ? value : null, state);
}
}
/**
* Commits edits after a field change, once its value has been normalized.
*
* @returns {void}
*/
addFormValueSync() {
const control = this.myElement.querySelector('.timeCase');
const syncFormValue = () => {
this.committedValue = this.readInputValue();
this.valueIsSet = true;
this.updateFormValue();
const EventConstructor = this.ownerDocument.defaultView.Event;
this.dispatchEvent(new EventConstructor('change', { bubbles: true }));
};
control.addEventListener('change', syncFormValue);
}
/**
* Disables or enables the internal fields when the form disables this control.
*
* @param {boolean} disabled - Whether the control is disabled.
* @returns {void}
*/
formDisabledCallback(disabled) {
this.myElement.querySelectorAll('.timeCase input').forEach(input => {
input.disabled = disabled;
});
}
/**
* Restores the initial value when its form is reset.
*
* @returns {void}
*/
formResetCallback() {
const defaultValue = this.getAttribute('value');
if (defaultValue !== null && this.isValidValue(defaultValue)) {
this.setInputValues(defaultValue);
} else {
this.resetInputs();
}
}
/**
* Restores the duration and required state saved by the browser.
*
* @param {string} state - Serialized duration state.
* @param {string} mode - Browser restoration mode.
* @returns {void}
*/
formStateRestoreCallback(state, mode) {
let restoredState;
try {
restoredState = JSON.parse(state);
} catch {
if (this.isValidValue(state)) {
this.setInputValues(state);
this.updateFormValue();
return;
}
this.warnInvalidValue(state);
this.resetInputs();
return;
}
if (
!restoredState ||
typeof restoredState !== 'object' ||
!this.isValidValue(restoredState.value) ||
typeof restoredState.valueIsSet !== 'boolean'
) {
this.warnInvalidValue(state);
this.resetInputs();
return;
}
this.setInputValues(restoredState.value);
this.valueIsSet = restoredState.valueIsSet;
this.updateFormValue();
}
/**
* Reports a value that does not match the supported duration format.
*
* @param {*} val - Rejected value.
* @returns {void}
*/
warnInvalidValue(val) {
console.warn(`The specified value ${val} does not conform to a supported duration format: "HH:mm:ss.SSS" (HH 00-99) or "D:HH:mm:ss.SSS" (HH 00-23); minutes and seconds must be 00-59 and milliseconds 000-999.`);
}
/**
* Called from constructor, sets up the html and event listeners
* @method
*/
boot() {
this.addHTML();
// // set up event listeners
this.add0s();
this.addNumberLooping();
this.addArrowKeyInput();
this.addNumericInput();
this.addClicksToActivate();
this.nonNumericBugInHTML();
this.removeEprop();
this.addFormValueSync();
this.updateFormValue();
}
/**
* Handles arrow-key adjustment and field navigation while keeping the
* active field selected.
* @method
* @see boot()
*/
addArrowKeyInput() {
var mE = this.myElement;
const keepAdjustedFieldSelected = input => {
setTimeout(function () {
input.suppressAutoAdvance = false;
if (mE.activeElement) {
mE.activeElement.select();
}
}, 5);
};
this.myElement.querySelector("div.timeCase")
.addEventListener('keydown', function (event) {
var et = event.target;
const visibleInputs = [...mE.querySelectorAll('.timeCase input')]
.filter(input => input.style.display !== 'none');
const activeIndex = visibleInputs.indexOf(mE.activeElement);
switch (true) {
case event.key == "ArrowLeft":
if (activeIndex > 0) {
const nextInput = visibleInputs[activeIndex - 1];
nextInput.focus();
setTimeout(() => nextInput.select(), 5);
}
break;
case event.key == "ArrowRight":
if (activeIndex !== -1 && activeIndex < visibleInputs.length - 1) {
const nextInput = visibleInputs[activeIndex + 1];
nextInput.focus();
setTimeout(() => nextInput.select(), 5);
}
break;
// these keep the input selected
case event.key == "ArrowUp":
et.suppressAutoAdvance = true;
keepAdjustedFieldSelected(et);
break;
case event.key == "ArrowDown":
et.suppressAutoAdvance = true;
keepAdjustedFieldSelected(et);
break;
// this is good for testing, to determine whether a key press is detected
case /^([0-9]?)$/.test(event.key):
break;
}
});
}
/**
* updates inputs-duration marked with shadow html and css
* @method
* @see boot()
*/
addHTML() {
var spanIn = document.createElement("div");
spanIn.classList.add('timeCase');
spanIn.setAttribute('part', 'control');
var template = document.createElement('template');
template.innerHTML = `
<input type="number" class="sD ts_digit" part="field field-days" name="startDays" data-tp="1" min="0" step="1" value="0" style="display: none">
<span class="bds-d" part="divider divider-days-hours" style="display: none">d </span>
<input type="number" class="sH D2 ts_digit" part="field field-hours" name="startHours" data-tp="2" min="-1" max="100" value="00" >
<span class="bds-h" part="divider divider-hours-minutes">:</span>
<input type="number" class="sM D2 ts_digit" part="field field-minutes" name="startMinutes" data-tp="3" min="-1" max="60" value="00">
<span class="bds-m" part="divider divider-minutes-seconds">:</span>
<input type="number" class="sS D2 ts_digit" part="field field-seconds" name="startSeconds" data-tp="4" min="-1" max="60" value="00">
<span class="bds-s" part="divider divider-seconds-milliseconds">.</span>
<input type="number" class="sMS D3 ts_digit" part="field field-milliseconds" name="startMilliSecs" data-tp="5" min="-10" max="1010" step="10" value="000">
<span class="bds-ms" part="divider divider-milliseconds-suffix" style="display: none"></span>
<div id="svgContainer" part="icon">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true"
focusable="false" data-prefix="fal" data-icon="stopwatch" class="svg-inline--fa fa-stopwatch fa-w-14"
role="img" viewBox="0 0 448 512">
<path xmlns="http://www.w3.org/2000/svg" fill="currentColor" d="M393.9 184l22.6-22.6c4.7-4.7 4.7-12.3 0-17l-17-17c-4.7-4.7-12.3-4.7-17 0l-20.7 20.7c-31.1-27.5-70.4-45.9-113.8-50.8V48h28c6.6 0 12-5.4 12-12V12c0-6.6-5.4-12-12-12H172c-6.6 0-12 5.4-12 12v24c0 6.6 5.4 12 12 12h28v49.4C96.4 109.3 16 197.2 16 304c0 114.9 93.1 208 208 208s208-93.1 208-208c0-44.7-14.1-86.1-38.1-120zM224 464c-88.4 0-160-71.6-160-160s71.6-160 160-160 160 71.6 160 160-71.6 160-160 160zm12-112h-24c-6.6 0-12-5.4-12-12V204c0-6.6 5.4-12 12-12h24c6.6 0 12 5.4 12 12v136c0 6.6-5.4 12-12 12z"/>
</svg>
</div>
<style>
:host {
display: inline-block;
color: inherit;
font: inherit;
vertical-align: middle;
--input-duration-gap: 0.08em;
--input-duration-padding: 0.4em 0.55em;
--input-duration-border-color: #9aa6b2;
--input-duration-border-radius: 0.4em;
--input-duration-background: transparent;
--input-duration-color: inherit;
--input-duration-focus-color: #2196f3;
--input-duration-focus-outline: 3px solid #2196f3;
--input-duration-focus-outline-offset: 2px;
--input-duration-invalid-color: #b42318;
--input-duration-divider-color: currentColor;
--input-duration-divider-opacity: 0.7;
--input-duration-disabled-color: GrayText;
--input-duration-icon-color: inherit;
}
.timeCase,
.timeCase[part~="control"] {
display: inline-flex;
align-items: center;
gap: var(--input-duration-gap);
max-width: 100%;
padding: var(--input-duration-padding);
border: 1px solid var(--input-duration-border-color);
border-radius: var(--input-duration-border-radius);
background: var(--input-duration-background);
color: var(--input-duration-color);
font-variant-numeric: tabular-nums;
line-height: 1.4;
white-space: nowrap;
}
.timeCase:focus-within {
border-color: var(--input-duration-focus-color);
outline: var(--input-duration-focus-outline);
outline-offset: var(--input-duration-focus-outline-offset);
}
:host(:invalid) .timeCase {
border-color: var(--input-duration-invalid-color);
}
.timeCase input {
box-sizing: content-box;
width: 2ch;
min-width: 2ch;
margin: 0;
padding: 0;
border: 0;
outline: 0;
background: transparent;
color: inherit;
font: inherit;
font-variant-numeric: tabular-nums;
text-align: center;
caret-color: transparent;
appearance: textfield;
}
.timeCase input.D3 {
width: 3ch;
min-width: 3ch;
}
.timeCase input.sD {
width: 2ch;
min-width: 2ch;
}
.timeCase input::-webkit-outer-spin-button,
.timeCase input::-webkit-inner-spin-button {
margin: 0;
appearance: none;
}
.timeCase span {
color: var(--input-duration-divider-color);
opacity: var(--input-duration-divider-opacity);
}
.timeCase .bds-d {
margin-left: 0.04em;
}
#svgContainer {
display: inline-flex;
width: 1em;
height: 1em;
flex: 0 0 auto;
align-items: center;
justify-content: center;
margin-left: 0.2em;
color: var(--input-duration-icon-color);
}
#svgContainer svg {
display: block;
width: 100%;
height: 100%;
}
.timeCase input:disabled {
background: transparent;
color: var(--input-duration-disabled-color);
cursor: not-allowed;
opacity: 0.65;
}
</style> `
spanIn.appendChild(template.content);
this.myElement.appendChild(spanIn);
this.committedValue = this.readInputValue();
}
/**
* eventlistener that adds number looping for up and down keys triggered by
* the change event of the input
* @method
* @see boot()
*/
addNumberLooping() {
// Numeric Value looping (eventually this can be 'parameterised' refactored)
// ex. 99 uparrow to 0 hours, 59 uparrow to 0 minutes and seconds, 990 uparrow to 010, downarrow 000 milliseconds
const sD = this.myElement.querySelector("input.sD");
const sH = this.myElement.querySelector("input.sH");
sH.addEventListener('change', () => {
if (this.hasAttribute('show-days') || Number(sD.value) > 0) {
if (Number(sH.value) === 24) {
sH.value = '00';
const nextDay = Number(sD.value) + 1;
if (Number.isSafeInteger(nextDay)) {
sD.value = String(nextDay);
}
} else if (Number(sH.value) === -1) {
if (Number(sD.value) > 0) {
sD.value = String(Number(sD.value) - 1);
sH.value = '23';
} else {
sH.value = '00';
}
}
} else {
if (sH.value == 100) {
sH.value = '00';
}
if (sH.value == -1) {
sH.value = 99;
}
}
sD.oldValue = sD.value;
sH.oldValue = sH.value;
sD.style.width = `${Math.max(2, sD.value.length)}ch`;
this.updateDividers();
});
const sM = this.myElement.querySelector("input.sM");
sM.addEventListener('change', function (event) {
if (sM.value == 60) {
sM.value = '00';
}
if (sM.value == -1) {
sM.value = 59;
}
});
const sS = this.myElement.querySelector("input.sS");
sS.addEventListener('change', function (event) {
if (sS.value == 60) {
sS.value = '00';
}
if (sS.value == -1) {
sS.value = 59;
}
});
const sMS = this.myElement.querySelector("input.sMS");
sMS.addEventListener('change', function (event) {
if (sMS.value == 1000) {
sMS.value = "010";
}
if (sMS.value == -10) {
sMS.value = 990;
}
if (sMS.value == 0) {
}
});
}
/**
* Filters numeric input and advances to the next unit. If the next unit is
* hidden, focus leaves the control once the current field is complete.
* @method
* @see boot()
*/
addNumericInput() {
const mE = this.myElement;
const owner = this;
// https://stackoverflow.com/questions/469357/html-text-input-allow-only-numeric-input
// Digit input 0-9 (typed in by user)
function setInputFilter(inputElement, inputFilter) {
inputElement.oldValue = inputElement.value;
inputElement.addEventListener('input', function () {
if (inputFilter(this.value, !this.suppressAutoAdvance)) {
this.oldValue = this.value;
} else {
this.value = this.oldValue;
}
});
}
const moveToNextUnitOrBlur = (input, nextSelector) => {
const nextInput = mE.querySelector(nextSelector);
if (nextInput.style.display === 'none') {
input.blur();
return;
}
nextInput.focus();
nextInput.select();
};
// three filters, hours or minutes and seconds (are the same) but milliseconds is different
var sH = this.myElement.querySelector('input.sH');
setInputFilter(sH, function (value, shouldAdvance) {
const hourPattern = owner.hasAttribute('show-days') ||
Number(owner.myElement.querySelector('.sD').value) > 0
? /^(?:[0-9]?|[01][0-9]?|2[0-3]?)$/
: /^(?:[0-9]?|[0-9][0-9]?)$/;
const newV = hourPattern.test(value);
if (shouldAdvance && newV && value.length === 2) {
moveToNextUnitOrBlur(sH, 'input.sM');
}
return newV;
});
const sD = this.myElement.querySelector('input.sD');
setInputFilter(sD, function (value) {
const valid = /^\d*$/.test(value) && (value === '' || Number.isSafeInteger(Number(value)));
if (valid) {
sD.style.width = `${Math.max(2, value.length)}ch`;
}
return valid;
});
sD.addEventListener('change', function () {
if (this.value === '') {
this.value = '0';
}
this.value = String(Number(this.value));
this.oldValue = this.value;
this.style.width = `${Math.max(2, this.value.length)}ch`;
owner.updateDividers();
});
var sM = this.myElement.querySelector('input.sM');
setInputFilter(sM, function (value, shouldAdvance) {
var newV = /^([0-9]?|[0-5][0-9]?)$/.test(value);
const nextUnitHidden = mE.querySelector('input.sS').style.display === 'none';
if (shouldAdvance && newV &&
(nextUnitHidden ? value.length === 2 : value > 5 || value.length === 2)) {
moveToNextUnitOrBlur(sM, 'input.sS');
}
return newV;
});
var sS = this.myElement.querySelector('input.sS');
setInputFilter(sS, function (value, shouldAdvance) {
var newV = /^([0-9]?|[0-5][0-9]?)$/.test(value);
const nextUnitHidden = mE.querySelector('input.sMS').style.display === 'none';
if (shouldAdvance && newV &&
(nextUnitHidden ? value.length === 2 : value > 5 || value.length === 2)) {
moveToNextUnitOrBlur(sS, 'input.sMS');
}
return newV;
});
var sMS = this.myElement.querySelector('input.sMS');
setInputFilter(sMS, function (value) {
var newV = /^([0-9]?|[0-9][0-9]?|[0-9][0-9][0-9]?)$/.test(value);
if (newV && value.length === 3) {
mE.querySelector('input.sMS').blur();
}
return newV;
});
}
/**
* eventlistener that adds 0s to preface numbers less than 10 triggered by
* the change event of the input
* @method
* @see boot()
*/
add0s() {
// input for hours, minutes, seconds
this.myElement.querySelectorAll('input:not(.sD)').forEach(i => i.addEventListener('change', function () {
if (!isNaN(this.value) && this.value.length === 1) {
this.value = '0' + this.value;
}
}));
// milliseconds
this.myElement.querySelectorAll("input.sMS").forEach(i => i.addEventListener('change', function () {
if (!isNaN(this.value) && this.value.length === 2) {
this.value = '0' + this.value;
}
}));
}
/**
* eventlistener that adds click to activate input of values by the keyboard triggered by
* the click event of the input
* @method
* @see boot()
*/
addClicksToActivate() {
this.myElement.querySelectorAll('input').forEach(i => i.addEventListener("click", function () { this.select(); }))
}
/**
* eventlistener that adds key filtering to stop letters (or other non-numerics) from being input triggered by
* the keypress event of the input
* @method
* @see boot()
*/
nonNumericBugInHTML() {
this.myElement.querySelectorAll(".timeCase input").forEach(item =>
item.addEventListener('keypress', function (e) {
e = e || window.event;
var charCode = (typeof e.which == "undefined") ? e.keyCode : e.which;
var charStr = String.fromCharCode(charCode);
if (!charStr.match(/^[0-9]+$/))
e.preventDefault();
}));
}
/**
* Stop event propigation from the timeCase up through the DOM so when keys
* are being used they don't affect other controls (like volume on plyr)
* @method
*/
removeEprop() {
var closure = this;
closure.shadowRoot.querySelector('.timeCase')
.addEventListener('keydown', function (event) { event.stopPropagation();});
// document.querySelectorAll('input-duration').forEach(x => x.shadowRoot.querySelector('.timeCase')
// .addEventListener('keydown', function (event) { event.stopPropagation();}));
}
// formatting and behaviour options
/**
* Disables the entire control and excludes it from form submission.
* @method
*/
disable() {
this.setAttribute('disabled', '');
this.formDisabledCallback(true);
}
/**
* Enables the duration fields.
*
* @returns {void}
*/
enable() {
this.removeAttribute('disabled');
this.formDisabledCallback(false);
}
/**
* Hides the hours field and its adjacent divider.
* Sets the `no-hours` attribute.
*
* @returns {void}
*/
hideHours() {
this.setAttribute('no-hours', '');
}
/**
* Shows the hours field and its adjacent divider.
* Removes the `no-hours` attribute.
*
* @returns {void}
*/
showHours() {
this.removeAttribute('no-hours');
}
/**
* Backward-compatible alias for {@link InputDuration#hideHours}.
*
* @returns {void}
*/
noHours() {
this.hideHours();
}
/**
* Hides the minutes field and its adjacent divider.
* Sets the `no-minutes` attribute.
*
* @returns {void}
*/
hideMinutes() {
this.setAttribute('no-minutes', '');
}
/**
* Shows the minutes field and its adjacent divider.
* Removes the `no-minutes` attribute.
*
* @returns {void}
*/
showMinutes() {
this.removeAttribute('no-minutes');
}
/**
* Backward-compatible alias for {@link InputDuration#hideMinutes}.
*
* @returns {void}
*/
noMinutes() {
this.hideMinutes();
}
/**
* Hides the seconds field and its adjacent divider.
* Sets the `no-seconds` attribute.
*
* @returns {void}
*/
hideSeconds() {
this.setAttribute('no-seconds', '');
}
/**
* Shows the seconds field and its adjacent divider.
* Removes the `no-seconds` attribute.
*
* @returns {void}
*/
showSeconds() {
this.removeAttribute('no-seconds');
}
/**
* Backward-compatible alias for {@link InputDuration#hideSeconds}.
*
* @returns {void}
*/
noSeconds() {
this.hideSeconds();
}
/**
* Hides the milliseconds field and its adjacent divider or suffix.
* Sets the `no-microseconds` attribute.
*
* @returns {void}
*/
hideMicroSeconds() {
this.setAttribute('no-microseconds', '');
}
/**
* Shows the milliseconds field and its adjacent divider or suffix.
* Removes the `no-microseconds` attribute.
*
* @returns {void}
*/
showMicroSeconds() {
this.removeAttribute('no-microseconds');
}
/**
* Backward-compatible alias for {@link InputDuration#hideMicroSeconds}.
*
* @returns {void}
*/
noMicroSeconds() {
this.hideMicroSeconds();
}
/**
* Replaces the hours/minutes separator.
* Also available through the `hours-divider` attribute.
*
* @param {string} text - Divider text to display.
* @returns {void}
*/
setHoursDivider(text) {
this.setAttribute('hours-divider', text);
}
/**
* Replaces the day/hour separator.
* Also available through the `days-divider` attribute.
*
* @param {string} text - Divider text to display.
* @returns {void}
*/
setDaysDivider(text) {
this.setAttribute('days-divider', text);
}
/**
* Shows the days field and its adjacent divider.
* Sets the `show-days` attribute.
*
* @returns {void}
*/
showDays() {
this.setAttribute('show-days', '');
}
/**
* Hides the days field and its adjacent divider.
* Removes the `show-days` attribute.
*
* @returns {void}
*/
hideDays() {
this.removeAttribute('show-days');
}
/**
* Replaces the minutes/seconds separator.
* Also available through the `minutes-divider` attribute.
*
* @param {string} text - Divider text to display.
* @returns {void}
*/
setMinutesDivider(text) {
this.setAttribute('minutes-divider', text);
}
/**
* Replaces the seconds/milliseconds separator.
* Also available through the `seconds-divider` attribute.
*
* @param {string} text - Divider text to display.
* @returns {void}
*/
setSecondsDivider(text) {
this.setAttribute('seconds-divider', text);
}
/**
* Sets a suffix to display after milliseconds.
* Also available through the `microseconds-divider` attribute.
*
* @param {string} text - Suffix text to display.
* @returns {void}
*/
setMicroSecondsDivider(text) {
this.setAttribute('microseconds-divider', text);
}
/**
* Synchronizes field and divider visibility with the `no-*` attributes.
* The days field grows for longer values; its minimum matches other fields.
*
* @returns {void}
*/
updateVisibleFields() {
const hiddenFields = {
'no-hours': '.sH',
'no-minutes': '.sM',
'no-seconds': '.sS',
'no-microseconds': '.sMS'
};
for (const [attribute, selector] of Object.entries(hiddenFields)) {
const field = this.myElement.querySelector(selector);
field.style.display = this.hasAttribute(attribute) ? 'none' : '';
}
const showDays = this.hasAttribute('show-days');
const dayField = this.myElement.querySelector('.sD');
dayField.style.display = showDays ? '' : 'none';
dayField.style.width = `${Math.max(2, dayField.value.length)}ch`;
const dayMode = showDays || Number(dayField.value) > 0;
this.myElement.querySelector('.sH').max = dayMode ? '24' : '100';
}
/**
* Synchronizes divider text and shows one separator between each pair of
* visible units, including when intervening units are hidden.
*
* @returns {void}
*/
updateDividers() {
const dayFormat = this.hasAttribute('show-days') ||
Number(this.myElement.querySelector('.sD').value) > 0;
const dividers = {
'days-divider': ['.bds-d', 'd '],
'hours-divider': ['.bds-h', dayFormat ? 'h ' : ':'],
'minutes-divider': ['.bds-m', dayFormat ? 'm ' : ':'],
'seconds-divider': ['.bds-s', '.'],
'microseconds-divider': ['.bds-ms', dayFormat ? 's' : '']
};
for (const [attribute, [selector, defaultText]] of Object.entries(dividers)) {
const divider = this.myElement.querySelector(selector);
const text = this.getAttribute(attribute) ?? defaultText;
divider.textContent = text;
}
const units = [
{ divider: '.bds-d', visible: this.hasAttribute('show-days') },
{ divider: '.bds-h', visible: !this.hasAttribute('no-hours') },
{ divider: '.bds-m', visible: !this.hasAttribute('no-minutes') },
{ divider: '.bds-s', visible: !this.hasAttribute('no-seconds') },
{ visible: !this.hasAttribute('no-microseconds') }
];
units.forEach((unit, index) => {
if (!unit.divider) {
return;
}
const hasVisibleFollowingUnit = units
.slice(index + 1)
.some(followingUnit => followingUnit.visible);
const divider = this.myElement.querySelector(unit.divider);
divider.style.display = unit.visible && hasVisibleFollowingUnit ? '' : 'none';
});
const microsecondsSuffix = this.myElement.querySelector('.bds-ms');
const suffixText = this.getAttribute('microseconds-divider') ??
(dayFormat ? 's' : '');
microsecondsSuffix.style.display =
!this.hasAttribute('no-microseconds') && suffixText ? '' : 'none';
}
}
// end class
customElements.define('input-duration', InputDuration);
export default InputDuration;
// an option is to make all above an IIFE