web-ui-pack
Version:
Web package with UI elements
556 lines (555 loc) • 20.5 kB
JavaScript
import animate, { isAnimEnabled } from "../helpers/animate";
import isOverlap from "../helpers/isOverlap";
import { parseMsTime } from "../helpers/styleHelpers";
import { onEvent } from "../indexHelpers";
import { WUPcssIcon, WUPcssScrollSmall } from "../styles";
import WUPSelectControl from "./select";
const tagName = "wup-selectmany";
export default class WUPSelectManyControl extends WUPSelectControl {
#ctr = this.constructor;
static get $styleRoot() {
return `:root {
--ctrl-select-item-text: inherit;
--ctrl-select-item-bg: rgba(0,0,0,0.04);
--ctrl-select-item-del-display: none;
--ctrl-select-item-del: var(--ctrl-icon);
--ctrl-select-item-del-img: var(--wup-icon-cross);
--ctrl-select-item-del-size: 0.8em;
--ctrl-select-gap: 0.5em;
}
[wupdark] {
--ctrl-select-item-bg: #fff2;
--ctrl-select-item-del: var(--ctrl-icon);
}`;
}
static get $style() {
return `${super.$style}
:host label {
position: relative;
}
${WUPcssScrollSmall(":host label>span")}
:host label > span {
position: initial;
overflow: auto;
gap: var(--ctrl-select-gap);
flex-wrap: wrap;
flex-direction: row;
margin: var(--ctrl-padding);
padding: 0;
margin-left: 0;
margin-right: 0;
max-height: 5em;
}
:host strong {
top: 1.6em;
margin: var(--ctrl-padding);
margin-top: 0;
margin-bottom: 0;
}
:host[filled] strong {
transform: var(--ctrl-label-active-pos);
}
:host [item],
:host input {
padding: var(--ctrl-select-gap);
}
:host input {
flex: 1 1 auto;
width: 0;
min-width: 1em;
padding-left: 0; padding-right: 0;
}
:host[filled] input:placeholder-shown,
:host[filled] input:not(:focus) {
min-width: 0;
padding-left: calc(var(--ctrl-select-gap));
margin-right: 0;
margin-left: calc(-1 * var(--ctrl-select-gap));
}
:host [item] {
--ctrl-icon: var(--ctrl-select-item-del);
--ctrl-icon-size: var(--ctrl-select-item-del-size);
--ctrl-icon-img: var(--ctrl-select-item-del-img);
color: var(--ctrl-select-item-text);
background-color: var(--ctrl-select-item-bg);
border-radius: var(--ctrl-border-radius);
cursor: pointer;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
flex: 0 0 auto;
}
:host [item]:after {
${WUPcssIcon}
display: var(--ctrl-select-item-del-display);
content: "";
padding: 0;
margin-left: 0.5em;
}
:host [item][focused],
:host [item][drag],
:host [item][drop] {
color: var(--ctrl-focus-label);
box-shadow: inset 0 0 3px 0 var(--ctrl-focus);
}
:host [item][removed],
:host [item][drag][remove] {
--ctrl-icon: var(--ctrl-err);
text-decoration: line-through;
color: var(--ctrl-err);
background-color: var(--ctrl-err-bg);
}
:host[readonly] [item] {
pointer-events: none;
touch-action: none;
}
:host button[clear] {
display: inline-block;
opacity: 0;
}
(hover: hover) and (pointer: fine) {
:host [item]:hover {
--ctrl-icon: var(--ctrl-err);
text-decoration: line-through;
color: var(--ctrl-err);
background-color: var(--ctrl-err-bg);
}
}
not all and (pointer: fine) {
:host [item] {
-webkit-user-select: none;
user-select: none;
}${""}
}
not all and (prefers-reduced-motion) {
:host [item][removed] {
transition: all var(--anim-t) ease-in-out;
transition-property: margin, padding, width, opacity;
padding-left: 0; padding-right: 0;
margin-left: 0; margin-right: 0;
width: 0;
opacity: 0;
}
}
:host [item][drag] {
z-index: 9999;
position: fixed;
left:0; top:0;
cursor: grabbing;
text-decoration: none;
--ctrl-icon: var(--ctrl-select-item-del);
color: var(--ctrl-select-item-text);
background-color: var(--ctrl-select-item-bg);
}
:host [item][drop] {
opacity: 0.7;
}`;
}
static $isEmpty(v) {
return !v || v.length === 0;
}
static $filterMenuItem(menuItemText, menuItemValue, inputValue, inputRawValue) {
if (this._opts.hideSelected && this.$value?.includes(menuItemValue)) {
return false;
}
return super.$filterMenuItem.call(this, menuItemText, menuItemValue, inputValue, inputRawValue);
}
static $defaults = {
...WUPSelectControl.$defaults,
multiple: true,
sortable: false,
hideSelected: false,
};
$refItems;
renderControl() {
super.renderControl();
this.$refLabel.prepend(this.$refTitle);
}
canHandleUndo() {
return false;
}
canParseInput(_text) {
return false;
}
parseInput(text) {
this._opts.multiple = false;
const vi = super.parseInput(text);
this._opts.multiple = true;
if (vi === undefined || this.$value?.some((v) => this.#ctr.$isEqual(v, vi, this))) {
return this.$value;
}
return this.$value ? [...this.$value, vi] : [vi];
}
gotChanges(propsChanged) {
this._opts.multiple = true;
this.removeAttribute("w-multiple");
super.gotChanges(propsChanged);
this._opts.sortable ??= false;
if (this._opts.sortable) {
!this._disposeDragdrop && this.applyDragdrop();
}
else {
this._disposeDragdrop?.call(this);
this._disposeDragdrop = undefined;
}
}
_wasSortAfterClick;
_disposeDragdrop;
applyDragdrop() {
this._disposeDragdrop = onEvent(this, "pointerdown", (e) => {
this._wasSortAfterClick = false;
if (this.$isReadOnly || this.$isDisabled) {
return;
}
const t = e.target;
let eli = (this.$refItems && this.$refItems.findIndex((item) => t === item || this.includes.call(item, t)));
if (eli === -1 || eli === undefined) {
return;
}
const el = this.$refItems[eli];
let dr;
let isWaitTouch = false;
let r0 = onEvent(document, "touchstart", () => {
isWaitTouch = true;
r0 = onEvent(document, "touchmove", (ev) => {
if (ev.cancelable) {
ev.preventDefault();
isWaitTouch = false;
}
}, { passive: false, capture: true });
}, { capture: true });
let isInside = true;
let isThrottle = false;
const r1 = onEvent(document, "pointermove", (ev) => {
if (isWaitTouch) {
return;
}
if (!dr) {
this._wasSortAfterClick = true;
dr = el.cloneNode(true);
dr.setAttribute("drag", "");
dr.style.width = `${el.offsetWidth}px`;
dr.style.height = `${el.offsetHeight}px`;
el.parentElement.prepend(dr);
el.setAttribute("drop", "");
this.setAttribute("hovered", "");
}
const x = ev.clientX - el.offsetWidth / 2;
const y = ev.clientY - el.offsetHeight / 2;
dr.style.transform = `translate(${x}px, ${y}px)`;
isInside = isOverlap(this.getBoundingClientRect(), dr.getBoundingClientRect());
this.setAttr.call(dr, "remove", !isInside, true);
if (!isInside) {
return;
}
if (isThrottle) {
return;
}
let nearest = eli;
let nearestEnd = eli;
let dist = Number.MAX_SAFE_INTEGER;
const rects = this.$refItems.map((item) => item.getBoundingClientRect());
let lineY = 0;
rects.some((r, i) => {
const nextLineY = r.y + r.height / 2;
if (Math.abs(nextLineY - lineY) > 3) {
lineY = nextLineY;
const c = Math.abs(ev.clientY - lineY);
if (c < dist) {
dist = c;
nearest = i;
nearestEnd = i;
}
else {
return true;
}
}
else {
nearestEnd += 1;
}
return false;
});
dist = Number.MAX_SAFE_INTEGER;
const nearestStart = nearest;
for (let i = nearest; i <= nearestEnd; ++i) {
const r = rects[i];
const dx = ev.clientX - (r.x + r.width / 2);
const dy = ev.clientY - (r.y + r.height / 2);
const c = Math.sqrt(dx * dx + dy * dy);
if (c < dist) {
dist = c;
nearest = i;
}
}
if (eli !== nearest) {
const trg = this.$refItems[nearest];
const r = rects[nearest];
const isYChangeByEdges = nearestStart === nearest || nearestEnd === nearest;
const isLeftOrTop = isYChangeByEdges
? eli > nearest
: Math.abs(r.x - ev.clientX) < Math.abs(r.x + r.width - ev.clientX);
let nextEli = eli;
if (nearest < eli) {
nextEli = isLeftOrTop ? nearest : nearest + 1;
}
else {
nextEli = nearest;
}
if (nextEli !== eli) {
if (isLeftOrTop) {
trg.parentElement.insertBefore(el, trg);
}
else {
trg.parentElement.insertBefore(el, trg.nextElementSibling);
}
this.$refItems.splice(nextEli, 0, this.$refItems.splice(eli, 1)[0]);
eli = nextEli;
isThrottle = true;
setTimeout(() => (isThrottle = false), 100);
}
}
});
const cancel = () => {
if (dr) {
setTimeout(() => (this._wasSortAfterClick = false), 1);
this.removeAttribute("hovered");
if (!isInside) {
el.removeAttribute("drop");
dr.remove();
this.removeValue(eli);
}
else {
const animTime = parseMsTime(window.getComputedStyle(el).getPropertyValue("--anim-t"));
const from = dr.getBoundingClientRect();
const to = el.getBoundingClientRect();
const diff = { x: to.x - from.x, y: to.y - from.y };
animate(0, 1, animTime, (v) => {
dr.style.transform = `translate(${from.x + diff.x * v}px, ${from.y + diff.y * v}px)`;
}).finally(() => {
el.removeAttribute("drop");
dr.remove();
});
this.setValue(this.$refItems.map((a) => a._wupValue), 3);
}
}
r0();
r1();
r2();
r3();
};
const r2 = onEvent(document, "pointerup", cancel, { capture: true });
const r3 = onEvent(document, "pointercancel", cancel, { capture: true });
});
}
canOpenMenu(openCase, e) {
return !this._wasSortAfterClick && super.canOpenMenu(openCase, e);
}
renderMenu(popup, menuId) {
const r = super.renderMenu(popup, menuId);
this.filterMenuItems();
return r;
}
renderItems(v, all) {
const refs = this.$refItems ?? [];
v.forEach((vi, i) => {
let r = refs[i];
if (!r) {
r = this.$refInput.parentNode.insertBefore(document.createElement("span"), this.$refInput);
r.setAttribute("item", "");
r.setAttribute("aria-hidden", true);
refs.push(r);
}
if (r._wupValue !== vi) {
r.textContent = this.valueToText(vi, all);
r._wupValue = vi;
}
});
const toRemove = refs.length - v.length;
toRemove > 0 && refs.splice(v.length, toRemove).forEach((el) => !el.hasAttribute("removed") && el.remove());
this.$refPopup && this.filterMenuItems();
this.$refItems = refs;
this.ariaSpeakValue();
}
ariaSpeakValue() {
this.$isFocused &&
this.$refItems?.length &&
this.$ariaSpeak(this.$refItems.map((el) => el.textContent).join(","), 0);
}
resetInputValue() {
this.$refInput.value = this.valueToInput(this.$value, true);
}
valueToInput(v, isReset) {
!isReset && setTimeout(() => this.renderItems(v ?? [], this.getItems()));
return this.$isFocused || !v?.length ? "" : " ";
}
selectValue(v, canCloseMenu = true) {
super.selectValue(v, canCloseMenu);
this._opts.hideSelected && this.focusMenuItem(null);
}
_focusIndex;
focusItemByIndex(i) {
const el = i == null ? null : this.$refItems[i];
this.focusMenuItem(el);
if (el) {
el.setAttribute("role", "option");
el.removeAttribute("aria-hidden");
el.removeAttribute("aria-selected");
}
this._focusIndex = i ?? undefined;
}
focusMenuItem(next) {
if (this._focusIndex != null) {
const prev = this.$refItems[this._focusIndex];
if (prev) {
prev.setAttribute("aria-hidden", true);
prev.removeAttribute("role");
}
this._focusIndex = undefined;
}
super.focusMenuItem(next);
}
selectMenuItemByValue(v) {
!this._opts.hideSelected && super.selectMenuItemByValue(v);
}
selectMenuItem(next) {
!this._opts.hideSelected && super.selectMenuItem(next);
}
clearFilterMenuItems() {
!this._opts.hideSelected && super.clearFilterMenuItems();
}
removeValue(index) {
const item = this.$refItems.splice(index, 1)[0];
this._focusIndex === index && this.focusItemByIndex(null);
let ms = 0;
const isAnim = isAnimEnabled();
if (isAnim) {
item.style.width = `${item.offsetWidth}px`;
item.setAttribute("removed", "");
setTimeout(() => (item.style.width = ""));
ms = parseMsTime(window.getComputedStyle(item).getPropertyValue("--anim-t"));
}
setTimeout(() => item.remove(), ms);
const v = [...this.$value];
v.splice(index, 1);
this.setValue(v.length ? v : undefined, 3);
}
setValue(v, reason, skipInput = false) {
const isChanged = super.setValue(v, reason, skipInput);
isChanged !== false && this.setAttr("filled", !this.$isEmpty, true);
return isChanged;
}
gotFocus(ev) {
const r = super.gotFocus(ev);
this.ariaSpeakValue();
this.$refInput.value = "";
const isTouchScreen = !window.matchMedia("(hover: hover) and (pointer: fine)").matches;
let preventClickAfterFocus = isTouchScreen;
isTouchScreen && setTimeout(() => (preventClickAfterFocus = false));
const dsps = onEvent(this.$refInput.parentElement, "click", (e) => {
if (e.button || this.$isDisabled || this.$isReadOnly || preventClickAfterFocus) {
return;
}
const t = e.target;
const eli = this.$refItems?.findIndex((li) => li === t || this.includes.call(li, t));
if (eli != null && eli > -1) {
e.preventDefault();
this.removeValue(eli);
}
}, { passive: false });
r.push(dsps);
const dsps2 = onEvent(this.$refInput, "blur", () => {
this.$refInput.value = " ";
onEvent(this.$refInput, "focus", () => (this.$refInput.value = ""), { once: true });
});
r.push(dsps2);
return r;
}
gotFocusLost() {
super.gotFocusLost();
this.focusItemByIndex(null);
}
gotKeyDown(e) {
super.gotKeyDown(e);
if (!(this.$refInput.selectionEnd === 0 && this.$refItems?.length)) {
return;
}
let handled = true;
if (e.shiftKey) {
if (!this._opts.sortable || this._focusIndex == null) {
return;
}
const prev = this._focusIndex;
const trg = this.$refItems[prev];
let isR = false;
const lastInd = this.$refItems.length - 1;
switch (e.key) {
case "ArrowLeft":
this._focusIndex = this._focusIndex > 0 ? this._focusIndex - 1 : lastInd;
break;
case "ArrowRight":
this._focusIndex = this._focusIndex < lastInd ? this._focusIndex + 1 : 0;
isR = true;
break;
default:
handled = false;
break;
}
if (handled) {
e.preventDefault();
trg.parentElement.insertBefore(trg, this._focusIndex === lastInd
? this.$refInput
: this.$refItems[isR && this._focusIndex !== 0 ? this._focusIndex + 1 : this._focusIndex]);
this.$refItems.splice(this._focusIndex, 0, this.$refItems.splice(prev, 1)[0]);
this.setValue(this.$refItems.map((a) => a._wupValue), 3);
}
return;
}
let next = this._focusIndex ?? null;
const len = this.$refItems.length;
switch (e.key) {
case "Enter":
if (next != null) {
this._focusIndex = undefined;
next = Math.max(0, next - 1);
}
else {
handled = false;
}
break;
case "Backspace":
if (next != null) {
this.removeValue(next);
next = !this.$refItems.length ? null : Math.max(0, next - 1);
break;
}
case "ArrowLeft":
next = Math.max(0, (next ?? this.$refItems.length) - 1);
break;
case "Delete":
if (next != null) {
this.removeValue(next);
next = !this.$refItems.length ? null : Math.min(next, this.$refItems.length - 1);
break;
}
case "ArrowRight":
if (next != null) {
next = Math.min(this.$refItems.length - 1, next + 1);
if (next === this._focusIndex) {
next = null;
}
}
else {
handled = false;
}
break;
default:
handled = false;
break;
}
if (handled && (this._focusIndex !== next || len !== this.$refItems.length)) {
e.preventDefault();
this.focusItemByIndex(next);
}
}
}
customElements.define(tagName, WUPSelectManyControl);