web-ui-pack
Version:
Web package with UI elements
551 lines (550 loc) • 20 kB
JavaScript
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;
}
(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);