adwaveui
Version:
Interactive Web Components inspired by the Gtk Adwaita theme.
486 lines (484 loc) • 14.4 kB
JavaScript
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
};