UNPKG

adwaveui

Version:

Interactive Web Components inspired by the Gtk Adwaita theme.

486 lines (484 loc) 14.4 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/components/input/input.tsx import "../../base-elements.mjs"; import { sig } from "@ncpa0cpl/vanilla-jsx/signals"; import { Input, Suggestions } from "adwavecss"; import { customElement } from "wc_toolkit"; import { Enum } from "../../utils/enum-attribute.mjs"; import { CustomKeyboardEvent, CustomMouseEvent } from "../../utils/events.mjs"; import { fuzzyCmp } from "../../utils/fuzzy-search.mjs"; import { getUid } from "../../utils/get-uid.mjs"; import { preventDefault, stopEvent } from "../../utils/prevent-default.mjs"; import { jsx } from "@ncpa0cpl/vanilla-jsx/jsx-runtime"; var AdwInputChangeEvent = class extends Event { static { __name(this, "AdwInputChangeEvent"); } constructor(eventType, type, value) { super("change", { bubbles: true }); this.value = value; this.t = type; } }; function search(options, query) { const results = []; for (let i = 0; i < options.length; i++) { const option = options[i]; if (option.toLowerCase().startsWith(query)) { results.push(option); } } return results; } __name(search, "search"); function fuzzySearch(options, query) { const results = []; for (let i = 0; i < options.length; i++) { const option = options[i]; if (fuzzyCmp(query, option)) { results.push(option); } } return results; } __name(fuzzySearch, "fuzzySearch"); var { CustomElement } = customElement("adw-input").attributes({ value: "string", defaultValue: "string", disabled: "boolean", name: "string", form: "string", type: "string", placeholder: "string", minLength: "number", maxLength: "number", errorLabel: "string", alertLabel: "string", /** * List of suggestions to show below the input field. Only "matching" * suggestions are shown, if any, unless `suggestionsShowAll` is * set to `true`. */ suggestions: "string[]", /** * When enabled, always show all suggestions, regardless of the input value. */ suggestionsShowAll: "boolean", /** * In which direction the suggestion box should open. * - `up` - The box will open above the input. * - `down` - The box will open below the input. * - `detect` - The box will try to detect if it has * enough space to open `down`, if not it will open `up`. * * Default: `down` */ suggestionsOrientation: Enum(["up", "down", "detect"]), /** * When enabled, perform a fuzzy search on the suggestions to determine which * ones to show. By default only a exact substring match is considered as "matching". */ fuzzy: "boolean" }).events({ "change": AdwInputChangeEvent, "optionclick": CustomMouseEvent, "keydown": CustomKeyboardEvent, "input": InputEvent, "cut": ClipboardEvent, "copy": ClipboardEvent, "paste": ClipboardEvent, "focus": FocusEvent, "blur": FocusEvent }).context( ({ suggestions, suggestionsShowAll, suggestionsOrientation, value, fuzzy }) => { const options = sig.derive( suggestions.signal, suggestionsShowAll.signal, suggestionsOrientation.signal, fuzzy.signal, value.signal, (suggestions2, showAll, orientation, fuzzy2, value2 = "") => { if (!suggestions2 || suggestions2.length === 0) return []; let result; if (showAll) { result = suggestions2.slice(); } else { if (fuzzy2) { result = fuzzySearch(suggestions2, value2); } else { result = search(suggestions2, value2); } } if (orientation === "up") { return result.reverse(); } else { return result; } } ); return { /** Whether suggestions combo box is opened */ open: sig(false), selectedOption: sig(-1), options, uid: getUid(), isInFocus: false, hasChanged: false, lastScrollIntoView: 0 }; } ).methods((wc) => { const { attribute, context } = wc; return { focus() { wc.thisElement.querySelector("input")?.focus(); }, showSuggestions() { context.open.dispatch(true); context.selectedOption.dispatch(-1); this.scrollActiveToView(true); }, hideSuggestions() { context.selectedOption.dispatch(-1); context.open.dispatch(false); }, isSuggestionListOpen() { return context.open.get(); }, scrollActiveToView(forceInstant = false) { setTimeout(() => { const suggestionElem = wc.thisElement.querySelector( `.${Suggestions.suggestions}` ); if (suggestionElem == null) { return; } const activeOption = suggestionElem?.querySelector( `.${Suggestions.active}` ); if (activeOption == null) { return; } const now = Date.now(); if (forceInstant || now - context.lastScrollIntoView <= 100) { activeOption.scrollIntoView({ behavior: "instant", block: "nearest" }); } else { activeOption.scrollIntoView({ behavior: "smooth", block: "nearest" }); } context.lastScrollIntoView = now; }); }, _highlightNextOption(offset = 1) { context.selectedOption.dispatch( (current) => Math.max(0, current - offset) ); }, _highlightPreviousOption(offset = 1) { context.selectedOption.dispatch( (current) => Math.min( context.options.get().length - 1, current + offset ) ); }, _handleOptionClick(event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget; const idx = target.dataset.opt != null ? Number(target.dataset.opt) : void 0; const option = idx != null && context.options.get()[idx] || ""; wc.emitEvent( new CustomMouseEvent("optionclick", { option }, event) ).onCommit(() => { if (idx != null) { attribute.value.set(option); this.hideSuggestions(); } }); }, _handleInputChange(event) { const inputElem = event.target; attribute.value.set(inputElem.value); }, _handleKeyDown(event) { switch (event.key) { case "ArrowUp": event.stopPropagation(); event.preventDefault(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { if (context.open.get()) { this._highlightNextOption(); } }); break; case "ArrowDown": event.stopPropagation(); event.preventDefault(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { if (context.open.get()) { this._highlightPreviousOption(); } }); break; case "PageUp": event.stopPropagation(); event.preventDefault(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { if (context.open.get()) { this._highlightNextOption(8); } }); break; case "PageDown": event.stopPropagation(); event.preventDefault(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { if (context.open.get()) { this._highlightPreviousOption(8); } }); break; case "Home": event.stopPropagation(); event.preventDefault(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { if (context.open.get()) { this._highlightNextOption( context.options.get().length - 1 ); } }); break; case "End": event.stopPropagation(); event.preventDefault(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { if (context.open.get()) { this._highlightPreviousOption( context.options.get().length - 1 ); } }); break; case "Enter": event.stopPropagation(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { if (context.open.get() && context.selectedOption.get() >= 0) { event.preventDefault(); const opt = context.options.get()[context.selectedOption.get()]; if (opt) { attribute.value.set(opt); this.hideSuggestions(); context.hasChanged = false; wc.emitEvent( "change", "select", attribute.value.get() ?? "" ); } } else if (context.hasChanged) { context.hasChanged = false; wc.emitEvent( "change", "submit", attribute.value.get() ?? "" ); } }).onCancel(() => { event.preventDefault(); }); break; case "Backspace": event.stopPropagation(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { this.showSuggestions(); }).onCancel(() => { event.preventDefault(); }); break; case "Escape": event.stopPropagation(); wc.emitEvent(new CustomKeyboardEvent("keydown", {}, event)).onCommit(() => { this.hideSuggestions(); }).onCancel(() => { event.preventDefault(); }); break; } }, _handleFocus(ev) { context.isInFocus = true; if (context.options.get().length) { this.showSuggestions(); } }, _handleBlur(ev) { context.isInFocus = false; this.hideSuggestions(); if (context.hasChanged) { context.hasChanged = false; wc.emitEvent("change", "submit", attribute.value.get() ?? ""); } } }; }).connected((wc) => { const { context, method, attribute: { disabled, errorLabel, form, maxLength, minLength, name, placeholder, suggestions, suggestionsOrientation, type, value, defaultValue } } = wc; if (value.get() == null && defaultValue.get() != null) { value.set(defaultValue.get()); } const forcedPosition = sig(); wc.thisElement.classList.add(Input.wrapper); const isHidden = sig.derive( suggestions.signal, context.options, context.open, (allSuggestions, options, open) => { return !open || options.length === 0 || !allSuggestions || allSuggestions.length === 0; } ); const isUp = sig.derive( suggestionsOrientation.signal, forcedPosition, (o, fo) => { if (o === "detect") { o = fo; } return o === "up"; } ); wc.onChange([value], () => { if (context.isInFocus) { context.hasChanged = true; } }); wc.onChange([value, suggestions], () => { if (context.selectedOption.get() === -1) { return; } context.selectedOption.dispatch( suggestionsOrientation.get() === "up" ? context.options.get().length - 1 : 0 ); }); wc.onChange([context.open], () => { if (context.open.get() && suggestionsOrientation.get() === "detect") { const rect = inputElem.getBoundingClientRect(); const distanceToBottom = window.innerHeight - rect.bottom; const fontSize = getComputedStyle(suggestionBox).fontSize; const emSize = Number(fontSize.replace("px", "")); const maxTargetHeight = Math.min( // 16em 16 * emSize, // 80vh 0.8 * window.innerHeight ); const targetHeight = Math.min( maxTargetHeight, context.options.get().length * (1.75 * emSize) ); if (distanceToBottom < targetHeight) { forcedPosition.dispatch("up"); } else { forcedPosition.dispatch("down"); } } }); wc.cleanup( context.selectedOption.observe(() => { method.scrollActiveToView(); }).detach ); const inputElem = /* @__PURE__ */ jsx( "input", { class: { [Input.input]: true, [Input.disabled]: disabled.signal }, oninput: method._handleInputChange, onkeydown: method._handleKeyDown, onfocus: method._handleFocus, onblur: method._handleBlur, onchange: stopEvent, type: type.signal, value: value.signal, disabled: disabled.signal, name: name.signal, "attribute:form": form.signal, placeholder: placeholder.signal, "attribute:minlength": minLength.signal, "attribute:maxlength": maxLength.signal, "aria-placeholder": placeholder.signal, "aria-label": placeholder.signal, "aria-invalid": errorLabel.signal.derive((err) => !!err), "aria-haspopup": "listbox", "aria-expanded": context.open, "aria-controls": context.uid } ); const suggestionBox = /* @__PURE__ */ jsx( "div", { id: context.uid, class: { [Suggestions.suggestions]: true, "suggestions-options": true, "_adw_hidden": isHidden, "orientation-down": sig.not(isUp), "orientation-up": isUp, "top": isUp }, role: "listbox", children: context.options.derive((options) => { return options.map((optLabel, idx) => { const isActive = sig.eq(context.selectedOption, idx); return /* @__PURE__ */ jsx( "div", { "data-opt": idx, class: { [Suggestions.option]: true, [Suggestions.active]: isActive }, onclick: method._handleOptionClick, onmousedown: preventDefault, role: "option", children: /* @__PURE__ */ jsx("span", { class: "text", children: optLabel }) } ); }); }) } ); wc.attach(inputElem); wc.attach(suggestionBox); }).register(); var AdwInput = CustomElement; export { AdwInput };