Source: index.js

/**
 * 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