UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

1,177 lines (1,032 loc) 31.3 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { instanceSymbol } from "../../../constants.mjs"; import { assembleMethodSymbol, registerCustomElement, } from "../../../dom/customelement.mjs"; import { FilterDateRangeStyleSheet } from "../stylesheet/filter-date-range.mjs"; import { FilterControlsDefaultsStyleSheet } from "../stylesheet/filter-controls-defaults.mjs"; import { AbstractBase } from "./abstract-base.mjs"; import { positionPopper } from "../../form/util/floating-ui.mjs"; import { ATTRIBUTE_ROLE } from "../../../dom/constants.mjs"; import { getDocument } from "../../../dom/util.mjs"; import { STYLE_DISPLAY_MODE_BLOCK } from "../constants.mjs"; import { DeadMansSwitch } from "../../../util/deadmansswitch.mjs"; import { addAttributeToken, removeAttributeToken, } from "../../../dom/attributes.mjs"; import { findTargetElementFromEvent } from "../../../dom/events.mjs"; import { getLocaleOfDocument } from "../../../dom/locale.mjs"; export { DateRange }; /** * @private * @type {symbol} */ const timerCallbackSymbol = Symbol("timerCallback"); // /** // * @private // * @type {symbol} // */ // const internalStateSymbol = Symbol("internalState"); /** * @private * @type {symbol} */ const radioInputsElementSymbol = Symbol("radioInputsElement"); /** * @private * @type {symbol} */ const inputInputsElementSymbol = Symbol("inputInputsElement"); /** * @private * @type {symbol} */ const selectInputsElementSymbol = Symbol("selectInputsElement"); /** * local symbol * @private * @type {symbol} */ const resizeObserverSymbol = Symbol("resizeObserver"); /** * @private * @type {symbol} */ const controlElementSymbol = Symbol("controlElement"); /** * local symbol * @private * @type {symbol} */ const closeEventHandler = Symbol("closeEventHandler"); /** * @private * @type {symbol} */ const inputElementSymbol = Symbol("inputElement"); /** * @private * @type {symbol} */ const formContainerElementSymbol = Symbol("formContainerElement"); /** * local symbol * @private * @type {symbol} */ const popperElementSymbol = Symbol("popperElement"); /** * local symbol * @private * @type {symbol} */ const arrowElementSymbol = Symbol("arrowElement"); /** * The date-range filter control is used to filter a data set by a date range. * * <img src="./images/data-range.png"> * * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library * * You can create this control either by specifying the HTML tag <monster-filter-date-range />` directly in the HTML or using * Javascript via the `document.createElement('monster-filter-date-range');` method. * * ```html * <monster-filter-date-range></monster-filter-date-range> * ``` * * Or you can create this CustomControl directly in Javascript: * * ```js * import '@schukai/component-datatable/source/filter/date-range.mjs'; * document.createElement('monster-filter-date-range'); * ``` * * @startuml data-range.png * skinparam monochrome true * skinparam shadowing false * HTMLElement <|-- CustomElement * CustomElement <|-- AbstractBase * AbstractBase <|-- DateRange * @enduml * * @copyright Volker Schukai * @summary A date range filter control */ class DateRange extends AbstractBase { // constructor() { // super(); // this[internalStateSymbol] = {} // } /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for( "@schukai/monster/components/filter/date-range@@instance", ); } /** * * @return {FilterButton} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); } /** * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) * * @param {*} value */ set value(value) { this[inputElementSymbol].value = value; } /** * @return {*} */ get value() { return this[inputElementSymbol].value; } /** * To set the options via the HTML tag, the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @return {Object} * * @property {Object} templates * @property {string} templates.main * @property {Object} labels * @property {Object} features * @property {boolean} features.moreThan * @property {boolean} features.within * @property {boolean} features.today */ get defaults() { const d = Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, labels: getTranslations(), features: { moreThan: true, within: true, today: true, }, popper: { placement: "bottom", middleware: ["flip", "offset:1"], }, }); return initOptionsFromArguments.call(this, d); } /** * * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [FilterControlsDefaultsStyleSheet, FilterDateRangeStyleSheet]; } /** * * @return {string} */ static getTag() { return "monster-filter-date-range"; } /** * @return {void} */ connectedCallback() { super.connectedCallback(); const document = getDocument(); for (const [, type] of Object.entries(["click", "touch"])) { // close on outside ui-events document.addEventListener(type, this[closeEventHandler]); } updatePopper.call(this); attachResizeObserver.call(this); } /** * @return {void} */ disconnectedCallback() { super.disconnectedCallback(); // close on outside ui-events for (const [, type] of Object.entries(["click", "touch"])) { document.removeEventListener(type, this[closeEventHandler]); } disconnectResizeObserver.call(this); } /** * * @return {Monster.Datatable.Filter.Range} */ showDialog() { show.call(this); return this; } /** * * @return {Monster.Datatable.Filter.Range} */ hideDialog() { hide.call(this); return this; } /** * * @return {Monster.Datatable.Filter.Range} */ toggleDialog() { if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { this.hideDialog(); } else { this.showDialog(); } return this; } } function getTranslations() { const locale = getLocaleOfDocument(); switch (locale.language) { case "de": return { singleValue: "Wert", fromValue: "Von", toValue: "Bis", rangeFrom: "Von", rangeTo: "Bis", todayValue: "Heute", withinValue: "Innerhalb", months: "Monate", days: "Tage", years: "Jahre", weeks: "Wochen", moreThanValue: "Mehr als", }; case "fr": return { singleValue: "Valeur", fromValue: "De", toValue: "À", rangeFrom: "De", rangeTo: "À", todayValue: "Aujourd'hui", withinValue: "Dans", months: "Mois", days: "Jours", years: "Années", weeks: "Semaines", moreThanValue: "Plus de", }; case "sp": return { singleValue: "Valor", fromValue: "Desde", toValue: "Hasta", rangeFrom: "Desde", rangeTo: "Hasta", todayValue: "Hoy", withinValue: "Dentro de", months: "Meses", days: "Días", years: "Años", weeks: "Semanas", moreThanValue: "Más de", }; case "it": return { singleValue: "Valore", fromValue: "Da", toValue: "A", rangeFrom: "Da", rangeTo: "A", todayValue: "Oggi", withinValue: "Entro", months: "Mesi", days: "Giorni", years: "Anni", weeks: "Settimane", moreThanValue: "Più di", }; case "pl": return { singleValue: "Wartość", fromValue: "Od", toValue: "Do", rangeFrom: "Od", rangeTo: "Do", todayValue: "Dziś", withinValue: "W ciągu", months: "Miesiące", days: "Dni", years: "Lata", weeks: "Tygodnie", moreThanValue: "Więcej niż", }; case "no": return { singleValue: "Verdi", fromValue: "Fra", toValue: "Til", rangeFrom: "Fra", rangeTo: "Til", todayValue: "I dag", withinValue: "Innen", months: "Måneder", days: "Dager", years: "År", weeks: "Uker", moreThanValue: "Mer enn", }; case "dk": return { singleValue: "Værdi", fromValue: "Fra", toValue: "Til", rangeFrom: "Fra", rangeTo: "Til", todayValue: "I dag", withinValue: "Indenfor", months: "Måneder", days: "Dage", years: "År", weeks: "Uger", moreThanValue: "Mere end", }; case "sw": return { singleValue: "Värde", fromValue: "Från", toValue: "Till", rangeFrom: "Från", rangeTo: "Till", todayValue: "Idag", withinValue: "Inom", months: "Månader", days: "Dagar", years: "År", weeks: "Veckor", moreThanValue: "Mer än", }; default: case "en": return { singleValue: "Value", fromValue: "From", toValue: "To", rangeFrom: "From", rangeTo: "To", todayValue: "Today", withinValue: "Within", months: "Months", days: "Days", years: "Years", weeks: "Weeks", moreThanValue: "More than", }; } } /** * * @param {HTMLElement} group * @param {string} type */ function updateMainFilterValueFromPopperChange(group, type) { function calculateTargetDate(diff) { if (isNaN(diff)) { diff = 0; } const currentDate = new Date(); currentDate.setDate(currentDate.getDate() + diff); return currentDate.toISOString().slice(0, 10); } if (type === "single") { this[inputElementSymbol].value = group.querySelector("input[type=date]").value; } else if (type === "from") { this[inputElementSymbol].value = group.querySelector("input[type=date]").value + "-"; } else if (type === "to") { this[inputElementSymbol].value = "-" + group.querySelector("input[type=date]").value; } else if (type === "from-to") { // this option contain two input fields, we check which one was changed and read the other one // the other field is in the same form group, for simplification we get both controls from the group here const group = this[formContainerElementSymbol].querySelectorAll( "input[type=radio][value=" + type + "] ~ input[type=date]", ); let from = group[0].value; if (from === "" || from === null || from < 0) { from = 0; } let to = group[1].value; if (to === "" || to === null || to < 0) { to = 0; } let range; if (from === 0 && to === 0) { range = ""; } else if (from === 0) { range = "-" + to; } else if (to === 0) { range = from + "-"; } else if (from === to) { range = from; } else { range = from + "-" + to; } this[inputElementSymbol].value = range; } else if (type === "today") { this[inputElementSymbol].value = new Date().toISOString().slice(0, 10); } else if (type === "within" || type === "more-than") { const diffValue = parseInt(group.querySelector("input[type=number]").value); const duration = group.querySelector("select").value; let diffInDays; switch (duration) { case "days": diffInDays = diffValue; break; case "weeks": diffInDays = diffValue * 7; break; case "months": diffInDays = diffValue * 30; break; case "years": diffInDays = diffValue * 365; break; default: diffInDays = 0; } if (diffInDays === 0) { this[inputElementSymbol].value = ""; return; } const suffix = type !== "within" ? "" : "-"; const prefix = type !== "within" ? "-" : ""; this[inputElementSymbol].value = suffix + calculateTargetDate(type === "within" ? diffInDays : -diffInDays) + prefix; } } /** * @private * @return {initEventHandler} */ function initEventHandler() { this[closeEventHandler] = (event) => { const path = event.composedPath(); for (const [, element] of Object.entries(path)) { if (element === this) { return; } } hide.call(this); }; this[inputElementSymbol].addEventListener("click", () => { this.toggleDialog(); }); this[formContainerElementSymbol].addEventListener("click", (event) => { const element = findTargetElementFromEvent( event, "data-monster-role", "range-type", ); if (!element) { return; } const type = element.getAttribute("data-monster-range-type"); const radio = this[formContainerElementSymbol].querySelector( "input[type=radio][value=" + type + "]", ); if (!radio) { return; } radio.checked = true; if (type === "today") { this[inputElementSymbol].value = new Date().toISOString().slice(0, 10); return; } // enable input from this group and disable the other const group = this[formContainerElementSymbol].querySelectorAll( "input[type=radio][value=" + type + "] ~ input, input[type=radio][value=" + type + "] ~ select", ); for (const [, element] of Object.entries(group)) { element.disabled = false; } const otherGroup = this[formContainerElementSymbol].querySelectorAll( "input[type=radio]:not([value=" + type + "]) ~ input, input[type=radio]:not([value=" + type + "]) ~ select", ); for (const [, element] of Object.entries(otherGroup)) { element.disabled = true; } }); // change the input value if the user change the value of the radio button this[inputElementSymbol].addEventListener("change", (event) => { queueMicrotask(() => { updatePopperInputsFromMainValue.call(this); }); }); // if the user change the value of on of the input fields, we should update the value of the input field this[formContainerElementSymbol].addEventListener("change", (event) => { const element = findTargetElementFromEvent( event, "data-monster-watch", "true", ); if (!element) { return; } const group = findTargetElementFromEvent( event, "data-monster-role", "range-type", ); if (!group) { return; } const type = group.getAttribute("data-monster-range-type"); updateMainFilterValueFromPopperChange.call(this, group, type); }); this.addEventListener("keydown", (event) => { // if key code esc than hide dialog if (event.key === "Escape") { hide.call(this); queueMicrotask(() => { this[inputElementSymbol].focus(); }); return; } const input = findTargetElementFromEvent( event, "data-monster-role", "input", ); if (!input) { return; } // key code down should activate first radio with name="singleValue" if (event.key === "ArrowDown") { show.call(this); } }); return this; } /** * @private */ function attachResizeObserver() { // against flickering this[resizeObserverSymbol] = new ResizeObserver((entries) => { if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { try { this[timerCallbackSymbol].touch(); return; } catch (e) { delete this[timerCallbackSymbol]; } } this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { updatePopper.call(this); }); }); requestAnimationFrame(() => { let parent = this.parentNode; while (!(parent instanceof HTMLElement) && parent !== null) { parent = parent.parentNode; } if (parent instanceof HTMLElement) { this[resizeObserverSymbol].observe(parent); } }); } function disconnectResizeObserver() { if (this[resizeObserverSymbol] instanceof ResizeObserver) { this[resizeObserverSymbol].disconnect(); } } /** * @private */ function hide() { this[popperElementSymbol].style.display = "none"; removeAttributeToken(this[controlElementSymbol], "class", "open"); } /** * @private * @this PopperButton */ function show() { if (this.getOption("disabled", false) === true) { return; } if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) { return; } this[popperElementSymbol].style.visibility = "hidden"; this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK; addAttributeToken(this[controlElementSymbol], "class", "open"); // if value is "" than we should clear the form if (this[inputElementSymbol].value === "") { clearAndDisableFormGroups.call(this); } queueMicrotask(() => { updatePopperInputsFromMainValue.call(this); updatePopper.call(this); }); } /** * @private */ function clearAndDisableFormGroups() { this[radioInputsElementSymbol].forEach((radio) => { radio.checked = false; }); this[inputInputsElementSymbol].forEach((input) => { input.value = ""; input.disabled = true; }); this[selectInputsElementSymbol].forEach((select) => { select.value = ""; select.disabled = true; }); } /** * @private * @return {string} */ function guessRangeTypeFromCurrentValue() { const value = this[inputElementSymbol].value.trim(); if (value === "") { return "empty"; } const singleDateRegex = /^\d{4}-\d{2}-\d{2}$/; const fromDateRegex = /^\d{4}-\d{2}-\d{2}-$/; const toDateRegex = /^-\d{4}-\d{2}-\d{2}$/; const fromToDateRegex = /^\d{4}-\d{2}-\d{2}-\d{4}-\d{2}-\d{2}$/; if (singleDateRegex.test(value)) { return "single"; } if (fromToDateRegex.test(value)) { return "from-to"; } if (fromDateRegex.test(value)) { return "from"; } if (toDateRegex.test(value)) { return "to"; } return "invalid"; } function getRangeTypeFromForm() { const group = this[formContainerElementSymbol].querySelector( "input[type=radio][name=rangeType]:checked", ); if (!group) { return; } return group.value; } /** * function, that calculates starting from a date a range of days, months, weeks or years * * @example * * // if today is 2020-10-12 * calculateDateRangeFromToday("2020-10-12", "days") // 0 * calculateDateRangeFromToday("2020-10-13", "days") // 1 * calculateDateRangeFromToday("2020-12-14", "days") // 63 * calculateDateRangeFromToday("2020-10-12", "months") // 0 * calculateDateRangeFromToday("2020-11-12", "months") // 1 * calculateDateRangeFromToday("2021-10-12", "months") // 11 * * @param {string} date * @param {string} unit (day, month, year, week) * @return {number} range */ function calculateDateRangeFromToday(date, unit) { // calculate the date of today -1 const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const dateAsObj = new Date(date); const diff = dateAsObj.getTime() - yesterday.getTime(); // return the rages in days, months, years or weeks switch (unit) { case "days": return Math.floor(diff / (1000 * 60 * 60 * 24)); case "months": return Math.floor(diff / (1000 * 60 * 60 * 24 * 30)); case "years": return Math.floor(diff / (1000 * 60 * 60 * 24 * 365)); case "weeks": return Math.floor(diff / (1000 * 60 * 60 * 24 * 7)); } return 0; } /** * @private */ function updatePopperInputsFromMainValue() { const featuresMoreThan = this.getOption("features.moreThan", false); const featuresWithin = this.getOption("features.within", false); let guessedValueType = guessRangeTypeFromCurrentValue.call(this); if ( !guessedValueType || guessedValueType === "empty" || guessedValueType === "invalid" ) { return; } const rangeType = getRangeTypeFromForm.call(this); if (guessedValueType === rangeType) { return; } let lastRangeUnit = "days"; // check if type from form is within and current type is to // if so, then do not update the form if ( guessedValueType === "to" && rangeType === "within" && featuresWithin === true ) { guessedValueType = "within"; lastRangeUnit = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=within] ~ select[name=withinValueUnit]`, ).value; } // check if type from form is more than and current type is to // if so, then do not update the form if ( guessedValueType === "from" && rangeType === "more-than" && featuresMoreThan === true ) { return; } clearAndDisableFormGroups.call(this); let radio = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=${guessedValueType}]`, ); let input; let select; switch (guessedValueType) { case "within": radio.checked = true; input = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=within] ~ input[name=withinValue]`, ); select = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=within] ~ select[name=withinValueUnit]`, ); input.disabled = false; select.disabled = false; const d1 = this[inputElementSymbol].value.substring(1); const range1 = calculateDateRangeFromToday(d1, lastRangeUnit); select.value = lastRangeUnit; input.value = range1; break; case "more-than": radio.checked = true; input = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=more-than] ~ input[name=moreThanValue]`, ); select = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=more-than] ~ select[name=moreThanValueUnit]`, ); input.disabled = false; select.disabled = false; const d2 = this[inputElementSymbol].value.substring(0, 10); const range2 = calculateDateRangeFromToday(d2, lastRangeUnit); select.value = lastRangeUnit; input.value = range2; break; case "single": // check if the date is today const today = new Date(); const date = new Date(this[inputElementSymbol].value); const featuresToday = this.getOption("features.today", false); if ( featuresToday && today.getFullYear() === date.getFullYear() && today.getMonth() === date.getMonth() && today.getDate() === date.getDate() ) { radio = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=today]`, ); input = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=today] ~ input[name=todayValue]`, ); } else { input = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=single] ~ input[name=singleValue]`, ); input.disabled = false; } radio.checked = true; input.value = this[inputElementSymbol].value; break; case "from": radio.checked = true; const fromInput = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=from] ~ input[name=fromValue]`, ); // the input value has an - suffix fromInput.value = this[inputElementSymbol].value.substring( 0, this[inputElementSymbol].value.length - 1, ); fromInput.disabled = false; break; case "to": radio.checked = true; const toInput = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=to] ~ input[name=toValue]`, ); // the input value has an - prefix toInput.value = this[inputElementSymbol].value.substring(1); toInput.disabled = false; break; case "from-to": radio.checked = true; const fromToInput = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=from-to] ~ input[name=fromValue]`, ); const toToInput = this[formContainerElementSymbol].querySelector( `input[name=rangeType][value=from-to] ~ input[name=toValue]`, ); const fromToRegexWithNamedGroups = /(?<from>\d{4}-\d{2}-\d{2})-(?<to>\d{4}-\d{2}-\d{2})/; const groups = fromToRegexWithNamedGroups.exec( this[inputElementSymbol].value, ); fromToInput.value = groups.groups.from; toToInput.value = groups.groups.to; fromToInput.disabled = false; toToInput.disabled = false; break; } } /** * @private * @return {Monster.Components.Datatable.Filter.Range} */ function initControlReferences() { if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } this[controlElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=control]`, ); this[inputElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=input]`, ); this[popperElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=popper]`, ); this[arrowElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=arrow]`, ); this[formContainerElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=form]`, ); this[radioInputsElementSymbol] = this[ formContainerElementSymbol ].querySelectorAll(`input[name=rangeType]`); this[inputInputsElementSymbol] = this[ formContainerElementSymbol ].querySelectorAll(`input[name=rangeType] ~ input`); this[selectInputsElementSymbol] = this[ formContainerElementSymbol ].querySelectorAll(`input[name=rangeType] ~ select`); return this; } /** * @private * @param {object} options * @return {object} */ function initOptionsFromArguments(options) { return options; } /** * @private */ function updatePopper() { if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) { return; } if (this.getOption("disabled", false) === true) { return; } positionPopper.call( this, this[controlElementSymbol], this[popperElementSymbol], this.getOption("popper", {}), ); } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control"> <input data-monster-attributes="disabled path:disabled | if:true, class path:classes.input" data-monster-role="input" part="button" data-monster-replace="path:labels.button"> <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1"> <div data-monster-role="arrow"></div> <div part="filter" class="flex" data-monster-replace="path:filter"> <div class="form-container" data-monster-role="form"> <div data-monster-role="range-type" data-monster-range-type="today" data-monster-attributes="class path:features.moreThan | ?:form-group:hidden"> <input type="radio" name="rangeType" value="today" tabindex="0"> <label for="today"><span data-monster-replace="path:labels.todayValue"></span></label> <input type="hidden" name="todayValue" disabled tabindex="0"> </div> <div data-monster-role="range-type" data-monster-range-type="within" data-monster-attributes="class path:features.within | ?:form-group:hidden"> <input type="radio" name="rangeType" value="within" tabindex="0"> <label for="within"><span data-monster-replace="path:labels.withinValue"></span></label> <input data-monster-watch="true" type="number" name="withinValue" disabled tabindex="0"> <select data-monster-watch="true" name="withinValueUnit" disabled tabindex="0"> <option selected value="days" data-monster-replace="path:labels.days"></option> <option value="weeks" data-monster-replace="path:labels.weeks"></option> <option value="months" data-monster-replace="path:labels.months"></option> <option value="years" data-monster-replace="path:labels.years"></option> </select> </div> <div data-monster-role="range-type" data-monster-range-type="more-than" data-monster-attributes="class path:features.moreThan | ?:form-group:hidden"> <input type="radio" name="rangeType" value="more-than" tabindex="0"> <label for="more-than"><span data-monster-replace="path:labels.moreThanValue"></span></label> <input data-monster-watch="true" type="number" name="moreThanValue" disabled tabindex="0"> <select data-monster-watch="true" name="moreThanValueUnit" disabled tabindex="0"> <option selected value="days" data-monster-replace="path:labels.days"></option> <option value="weeks" data-monster-replace="path:labels.weeks"></option> <option value="months" data-monster-replace="path:labels.months"></option> <option value="years" data-monster-replace="path:labels.years"></option> </select> </div> <div class="form-group" data-monster-role="range-type" data-monster-range-type="single"> <input type="radio" name="rangeType" value="single" tabindex="0"> <label for="single"><span data-monster-replace="path:labels.singleValue"></span></label> <input type="date" data-monster-watch="true" name="singleValue" disabled tabindex="0"> </div> <div class="form-group" data-monster-role="range-type" data-monster-range-type="from"> <input type="radio" name="rangeType" value="from" tabindex="0"> <label for="from"><span data-monster-replace="path:labels.fromValue"></span></label> <input type="date" data-monster-watch="true" name="fromValue" disabled tabindex="0"> </div> <div class="form-group" data-monster-role="range-type" data-monster-range-type="to"> <input type="radio" name="rangeType" value="to" tabindex="0"> <label for="to"><span data-monster-replace="path:labels.toValue"></span></label> <input type="date" data-monster-watch="true" name="toValue" disabled tabindex="0"> </div> <div class="form-group" data-monster-role="range-type" data-monster-range-type="from-to"> <input type="radio" name="rangeType" value="from-to" tabindex="0"> <label for="rangeFrom"><span data-monster-replace="path:labels.fromValue"></span></label> <input type="date" data-monster-watch="true" name="fromValue" disabled tabindex="0"> <label for="rangeTo"><span data-monster-replace="path:labels.toValue"></span></label> <input type="date" data-monster-watch="true" name="toValue" disabled tabindex="0"> </div> </div> </div> </div> `; } registerCustomElement(DateRange);