UNPKG

web-ui-pack

Version:
556 lines (555 loc) 20.5 kB
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; } @media (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); } } @media not all and (pointer: fine) { :host [item] { -webkit-user-select: none; user-select: none; }${""} } @media 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);