UNPKG

web-ui-pack

Version:
551 lines (550 loc) 20 kB
import onEvent from "../helpers/onEvent"; import WUPScrolled from "../helpers/scrolled"; import localeInfo from "../objects/localeInfo"; import WUPTimeObject from "../objects/timeObject"; import { WUPcssIcon } from "../styles"; import WUPBaseComboControl from "./baseCombo"; const tagName = "wup-time"; export default class WUPTimeControl extends WUPBaseComboControl { #ctr = this.constructor; static $ariaOk = __wupln("Ok", "aria"); static $ariaCancel = __wupln("Cancel", "aria"); static $ariaHours = __wupln("Hours", "aria"); static $ariaMinutes = __wupln("Minutes", "aria"); static $ariaHours12 = __wupln("AM PM", "aria"); static get $styleRoot() { return `:root { --ctrl-time-current: #000; --ctrl-time-current-bg: #d9f7fd; --ctrl-time-off-text: var(--ctrl-err); --ctrl-time-off-bg: none; } [wupdark] { --ctrl-time-current: #25a1b6; --ctrl-time-current-bg: #fff1; --ctrl-time-off-text: var(--ctrl-err); --ctrl-time-off-bg: none; }`; } static get $style() { const focusStyle = ` content: " "; position: absolute; display: block; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 2em; height: 2em; border-radius: 50%; box-shadow: 0 0 3px 1px var(--ctrl-focus); `; return `${super.$style} :host { --ctrl-icon-img: var(--wup-icon-time-lg); --ctrl-icon-img: var(--wup-icon-time); } :host > [menu] { overflow: hidden; } :host > [menu] > div:first-child { position: relative; } :host > [menu] ul { margin: 0; padding: 0; list-style-type: none; cursor: pointer; overflow: auto; text-align: center; display: inline-block; vertical-align: middle; } :host > [menu] li, :host > [menu] mark, :host > [menu] ul:after { padding: 1em; line-height: 1em; } :host > [menu] mark { z-index: -1; position: absolute; display: block; top:50%; left:0; right:0; transform: translateY(-50%); margin: 0; padding-left: 2.8em; font: inherit; background: var(--ctrl-time-current-bg); } :host > [menu] ul:after { content: "99"; height: 0; margin:0; padding-top:0; padding-bottom:0; visibility: hidden; overflow: hidden; user-select: none; pointer-events: none; display: block; } :host > [menu] ul:nth-child(3):after { content: "AM"; } :host > [menu] li[aria-selected=true], :host > [menu] mark, :host > [menu] ul:after { font-weight: bold; color: var(--ctrl-time-current); } :host > [menu] li[focused] { position: relative; } :host > [menu] li[focused]:after { ${focusStyle} } :host > [menu] li[aria-hidden] { pointer-events: none; touch-action: none; opacity: 0; } :host > [menu] li[disabled] { border-radius: 999px; color: var(--ctrl-time-off-text); --ctrl-focus: var(--ctrl-time-off-text); background-color: var(--ctrl-time-off-bg); } :host [group] { display: flex; border-top: 1px solid var(--base-sep); } :host [group] > button { cursor: pointer; flex: 1 1 50%; display: inline-flex; align-content: center; justify-content: center; height: 2.4em; border: none; border-radius: 0; padding: 0; margin: 0; background: var(--popup-bg); } :host [group] > button:first-child { --ctrl-icon-img: var(--wup-icon-check); --ctrl-icon: var(--ctrl-err-valid); border-bottom-left-radius: var(--border-radius); border-right: 1px solid var(--base-sep); } :host [group] > button:last-child { --ctrl-icon-img: var(--wup-icon-cross); --ctrl-icon: var(--ctrl-err); border-bottom-right-radius: var(--border-radius); } :host [group] > button:after { ${WUPcssIcon} content: ""; padding:0; } @media (hover: hover) and (pointer: fine) { :host > [menu] li:hover { position: relative; } :host > [menu] li:hover:after { ${focusStyle} } :host > [menu] button:hover { box-shadow: inset 0 0 0 99999px rgb(0,0,0,0.05); } } :host > [menu] button[disabled] { box-shadow: inset 0 0 0 99999px rgb(0,0,0,0.05); cursor: not-allowed; --ctrl-icon: inherit; }`; } static get mappedAttributes() { const m = super.mappedAttributes; m.min.type = 4; m.max.type = 4; return m; } static $defaults = { ...WUPBaseComboControl.$defaults, validationRules: { ...WUPBaseComboControl.$defaults.validationRules, min: (v, setV, c) => (v === undefined || v < setV) && __wupln(`Min value is ${setV.format(c._opts.format)}`, "validation"), max: (v, setV, c) => (v === undefined || v > setV) && __wupln(`Max value is ${setV.format(c._opts.format)}`, "validation"), exclude: (v, fn, c) => (v === undefined || fn.test(v, c)) && __wupln("This value is disabled", "validation"), }, step: 1, format: "", min: null, max: null, exclude: null, menuButtonsOff: false, }; constructor() { super(); this._opts.format = this.#ctr.$defaults.format || localeInfo.time; } parse(text) { if (!text) { return undefined; } return new WUPTimeObject(text); } parseInput(text) { return new WUPTimeObject(text); } canParseInput(_text) { return !this.refMask || this.refMask.isCompleted; } valueToStorage(v) { return v.toString("-"); } gotChanges(propsChanged) { this._opts.format = this._opts.format?.replace(/\D{0,1}(ss|SS)/, "") || "hh:mm A"; this._opts.mask ||= this._opts.format .replace(/hh|HH/, "00") .replace(/[hH]/, "#0") .replace(/mm|MM/, "00") .replace(/[mM]/, "#0") .replace(/a/, "//[ap]//m") .replace(/A/, "//[AP]//M"); this._opts.maskholder ||= this._opts.format .replace(/([mMhH]){1,2}/g, "$1$1") .replace(/a/, "*m") .replace(/A/, "*M"); this._opts.step ||= this.#ctr.$defaults.step; super.gotChanges(propsChanged); } $refMenuLists; $refButtonOk; $refButtonCancel; get validations() { const vls = super.validations; if (this._opts.min) vls.min = this._opts.min; if (this._opts.max) vls.max = this._opts.max; if (this._opts.exclude) vls.exclude = this._opts.exclude; return vls; } disableItems() { let { min, max, exclude } = this._opts; if ((!min && !max && !exclude) || !this.$refMenuLists) { return; } min ??= 0; max ??= WUPTimeObject.$maxValue; for (let n = 0; n < 2; ++n) { const lst = this.$refMenuLists[n]; const was = lst._value; for (let i = 0; i < lst.children.length; ++i) { const el = lst.children.item(i); lst._value = el._value; const v = this.getMenuValue(); const isDisabled = v < min || v > max || exclude?.test(v, this); this.setAttr.call(el, "disabled", isDisabled, true); } lst._value = was; } const lst = this.$refMenuLists[2]; if (lst) { for (let i = 0; i < lst.children.length; ++i) { const el = lst.children.item(i); const isPM = el._value === 2; const isDisabled = (isPM && max < new WUPTimeObject(12, 0)) || (!isPM && min > new WUPTimeObject(11, 0)); this.setAttr.call(el, "disabled", isDisabled, true); } } const v = this.getMenuValue(); const isDisabled = v < min || v > max || exclude?.test(v, this); this.$refButtonOk && this.setAttr.call(this.$refButtonOk, "disabled", isDisabled, true); } #isMenuInitPhase; #valueBeforeMenu; renderMenu(popup, menuId, rows = 5) { popup.$options.minWidthByTarget = false; this.#isMenuInitPhase = true; const append = (ul, v, twoDigs, savedV) => { const li = ul.appendChild(document.createElement("li")); li.textContent = twoDigs && +v < 10 ? `0${v}` : v.toString(); li._value = savedV ?? v; return li; }; const selectNext = (prev, next) => { this.#lastInputChanged = false; const el = next.items[0]; if (el) { this._selectedMenuItem = prev.items[0]; this.selectMenuItem(el); this._selectedMenuItem = undefined; this._focusedMenuItem && this.focusMenuItem(next.items[0]); } !this.#isMenuInitPhase && this.$isOpened && this.disableItems(); }; const drows = Math.round(rows / 2) - 1; const onClickSkip = this._opts.menuButtonsOff ? () => this.setValue(this.getMenuValue(), 4) : undefined; const parent = popup.appendChild(document.createElement("div")); parent.setAttribute("id", menuId); parent.setAttribute("role", "application"); this.$refMenuLists = []; const getCurHours = () => { const hh = this.$value ? this.$value.hours : new Date().getHours(); return h12 ? hh % 12 : hh; }; const lh = parent.appendChild(document.createElement("ul")); lh.setAttribute("aria-label", this.#ctr.$ariaHours); this.$refMenuLists.push(lh); const h12 = /[aA]$/.test(this._opts.format); const hh2 = /[hH]+/g.exec(this._opts.format)[0].length === 2; lh._scrolled = new WUPScrolled(lh, { swipeDebounceMs: 100, scrollByClick: true, onClickSkip, pages: { current: getCurHours(), total: h12 ? 12 : 24, before: drows, after: drows, cycled: true }, onRender: (_dir, v, prev, next) => { v = h12 && v === 0 ? 12 : v; const items = [append(lh, v, hh2)]; lh._value = next.index; next.items = rows === 1 ? items : next.items; selectNext(prev, next); return items; }, }); lh.goToNext = (v) => lh._scrolled.goTo(v); lh.reInit = () => { lh._scrolled.options.pages.current = getCurHours(); lh._scrolled.init(); }; const getCurMinutes = () => Math.round((this.$value ? this.$value.minutes : new Date().getMinutes()) / step); const lm = parent.appendChild(document.createElement("ul")); lm.setAttribute("aria-label", this.#ctr.$ariaMinutes); this.$refMenuLists.push(lm); const mm2 = /[mM]+/g.exec(this._opts.format)[0].length === 2; const { step } = this._opts; lm._scrolled = new WUPScrolled(lm, { swipeDebounceMs: 100, scrollByClick: true, onClickSkip, pages: { current: getCurMinutes(), total: Math.round(60 / step), before: drows, after: drows, cycled: true, }, onRender: (_dir, v, prev, next) => { v = Math.round(v * step); const items = [append(lm, v, mm2)]; lm._value = Math.round(next.index * step); next.items = rows === 1 ? items : next.items; selectNext(prev, next); return items; }, }); lm.goToNext = (v) => lm._scrolled.goTo(v); lm.reInit = () => { lm._scrolled.options.pages.current = getCurMinutes(); lm._scrolled.init(); }; if (h12) { const lower = this._opts.format.endsWith("a"); const lh12 = parent.appendChild(document.createElement("ul")); lh12.setAttribute("aria-label", this.#ctr.$ariaHours12); this.$refMenuLists.push(lh12); lh12._scrolled = new WUPScrolled(lh12, { swipeDebounceMs: 100, scrollByClick: true, onClickSkip, pages: { current: this.$value?.isPM ? 2 : 1, total: 4, before: Math.min(drows, 1), after: Math.min(drows, 1) }, onRender: (_dir, v, prev, next) => { if (next.index === 0 || next.index === 3) { return null; } lh12._value = next.index; const txt = (lower ? ["pm", "am", "pm", "am"] : ["PM", "AM", "PM", "AM"])[v]; const item = append(lh12, txt, false, v); if (v === 0 || v === 3) { item.setAttribute("aria-hidden", true); } const items = [item]; next.items = rows === 1 ? items : next.items; selectNext(prev, next); return items; }, }); lh12.goToNext = (isNext) => { const v = lh12._value; const to = (isNext && v === 2 && 1) || (!isNext && v === 1 && 2) || (isNext ? v + 1 : v - 1); lh12._scrolled.goTo(to); }; } const sep = document.createElement("mark"); sep.setAttribute("aria-hidden", true); sep.textContent = /[hH]([^hH])/.exec(this._opts.format)[1]; parent.appendChild(sep); !this._opts.menuButtonsOff && this.renderMenuButtons(popup); this.#isMenuInitPhase = false; return parent; } renderMenuButtons(popup) { const btns = popup.appendChild(document.createElement("div")); btns.setAttribute("group", ""); const btnOk = btns.appendChild(document.createElement("button")); btnOk.setAttribute("type", "button"); btnOk.setAttribute("aria-label", this.#ctr.$ariaOk); btnOk.setAttribute("tabindex", -1); this.$refButtonOk = btnOk; const btnCancel = btns.appendChild(document.createElement("button")); btnCancel.setAttribute("type", "button"); btnCancel.setAttribute("aria-label", this.#ctr.$ariaCancel); btnCancel.setAttribute("tabindex", -1); this.$refButtonCancel = btnCancel; onEvent(btns, "click", (e) => { e.preventDefault(); const t = e.target; let isOk = null; if (btnOk === t || this.includes.call(btnOk, t)) { isOk = true; } else if (btnCancel === t || this.includes.call(btnCancel, t)) { isOk = false; } isOk !== null && this.gotBtnsClick(e, isOk); }, { passive: false }); } async goOpenMenu(openCase, e) { const r = await super.goOpenMenu(openCase, e); this.#valueBeforeMenu = this.$value; this.#lastInputChanged = false; const v = this.$value; if (this.$refMenuLists && v) { this.#isMenuInitPhase = true; const menuV = this.getMenuValue(); menuV.hours !== v.hours && this.$refMenuLists[0].reInit(); menuV.minutes !== v.minutes && this.$refMenuLists[1].reInit(); this.$refMenuLists[2]?._scrolled.goTo(v.isPM ? 2 : 1, false); this.#isMenuInitPhase = false; } this.disableItems(); return r; } removePopup() { super.removePopup(); delete this.$refMenuLists; delete this.$refButtonOk; delete this.$refButtonCancel; } getMenuValue() { const hh = this.$refMenuLists[0]._value; const mm = this.$refMenuLists[1]._value; const h12 = this.$refMenuLists[2]?._value; return new WUPTimeObject(h12 != null && hh === 0 ? 12 : hh, mm, h12 ? h12 === 2 : undefined); } gotBtnsClick(e, isOk) { if (isOk) { this.selectValue(this.getMenuValue()); } else { setTimeout(() => this.goCloseMenu(4, e)); } } trySetValue() { this._opts.menuButtonsOff && !this.#isMenuInitPhase && this.setValue(this.getMenuValue(), 4); } selectMenuItem(next) { super.selectMenuItem(next); next !== null && this.trySetValue(); } goCloseMenu(closeCase, e) { closeCase === 4 && this._opts.menuButtonsOff && this.setValue(this.#valueBeforeMenu, 2); return super.goCloseMenu(closeCase, e); } valueToInput(v) { if (v === undefined) { return ""; } return v.format(this._opts.format); } #lastInputChanged = false; gotInput(e) { this.#lastInputChanged = true; super.gotInput(e); } focusMenuItem(next) { if (next) { !this.$value && this.trySetValue(); const v = this.getMenuValue(); const str = this.valueToInput(v); next.setAttribute("aria-label", str); } this._focusedMenuItem?.removeAttribute("aria-label"); super.focusMenuItem(next); } focusMenuItemByKeydown(e) { let isHandled = true; let isNext = false; const lst = this.$refMenuLists; let ind = (this._focusedMenuItem && lst.findIndex((ref) => ref.contains(this._focusedMenuItem))) || 0; const initFocus = (i) => (this._focusedMenuItem ? i : 0); switch (e.key) { case "ArrowLeft": ind = Math.max(0, initFocus(ind - 1)); this.focusMenuItem(lst[ind].querySelector("[aria-selected=true]")); break; case "ArrowRight": ind = Math.min(lst.length - 1, initFocus(ind + 1)); this.focusMenuItem(lst[ind].querySelector("[aria-selected=true]")); break; case "ArrowDown": isNext = true; case "ArrowUp": ind = initFocus(ind); if (!this._focusedMenuItem) { this.focusMenuItem(lst[ind].querySelector("[aria-selected=true]")); } else { this._focusedMenuItem = lst[ind].querySelector("[aria-selected=true]"); lst[ind].goToNext(isNext); } break; default: isHandled = false; break; } if (isHandled) { e.preventDefault(); this.#lastInputChanged = false; } } gotKeyDown(e) { const wasOpen = this.$isOpened; const isExtraKey = e.altKey || e.shiftKey || e.ctrlKey; if (!isExtraKey && wasOpen && e.key === "Enter") { e.preventDefault(); if (this.#lastInputChanged || !this.$refButtonOk) { this.goCloseMenu(5); this._inputError && this.resetInputValue(); } else if (!this.$refButtonOk.disabled) { setTimeout(() => this.$refButtonOk.dispatchEvent(new MouseEvent("click", { cancelable: true, bubbles: true }))); } return; } super.gotKeyDown(e); } gotFocusLost() { !this._inputError && this.setInputValue(this.$value, 4); super.gotFocusLost(); } } customElements.define(tagName, WUPTimeControl);