enterprise/search: ability to use more precise search queries (#7698)
* api: use DjangoQL for searches Signed-off-by: Jens Langhammer <jens@goauthentik.io> * expand search input and use textarea for multiline Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start implementing autocomplete Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only use ql for events Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make QL search opt in Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make pretend json relation work Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix schema Signed-off-by: Jens Langhammer <jens@goauthentik.io> * test Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make autocomplete l1 work Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use forked js lib with types, separate QL Signed-off-by: Jens Langhammer <jens@goauthentik.io> * first attempt at making it fit our UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make dark theme somewhat work, fix search Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make more parts work Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make auto complete box be under cursor Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: ripplefcl <github@ripple.contact> * remove django autocomplete for now Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-add event filtering Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix search when no ql is enabled Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make meta+enter submit, fix colour Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make dark theme Signed-off-by: Jens Langhammer <jens@goauthentik.io> * formatting Signed-off-by: Jens Langhammer <jens@goauthentik.io> * enterprise Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Update authentik/enterprise/search/apps.py Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Signed-off-by: Jens L. <jens@beryju.org> * add json element autocomplete Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Co-authored-by: ripplefcl <github@ripple.contact> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix query Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix search reset Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix dark theme Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: ripplefcl <github@ripple.contact> Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
17
web/package-lock.json
generated
17
web/package-lock.json
generated
@ -28,6 +28,7 @@
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@lit/task": "^1.0.2",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@mrmarble/djangoql-completion": "^0.8.3",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
@ -2660,6 +2661,16 @@
|
||||
"langium": "3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mrmarble/djangoql-completion": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@mrmarble/djangoql-completion/-/djangoql-completion-0.8.3.tgz",
|
||||
"integrity": "sha512-wYctvF0gQs48wL9jLQ+H2g2B0yJj7CrUSNi4ec5gcSuICIRqD/QSt6G+3zDdeW1LlI/4uj/FByJvg8k4TAAnVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lex": "1.7.9",
|
||||
"lodash": "4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/nice": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz",
|
||||
@ -18978,6 +18989,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lex": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/lex/-/lex-1.7.9.tgz",
|
||||
"integrity": "sha512-vzaalVBmFLnMaedq0QAsBAaXsWahzRpvnIBdBjj7y+7EKTS6lnziU2y/PsU2c6rV5qYj2B5IDw0uNJ9peXD0vw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
|
||||
@ -99,6 +99,7 @@
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@lit/task": "^1.0.2",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@mrmarble/djangoql-completion": "^0.8.3",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
|
||||
@ -20,6 +20,7 @@ import { Event, EventsApi } from "@goauthentik/api";
|
||||
@customElement("ak-event-list")
|
||||
export class EventListPage extends TablePage<Event> {
|
||||
expandable = true;
|
||||
supportsQL = true;
|
||||
|
||||
pageTitle(): string {
|
||||
return msg("Event Log");
|
||||
|
||||
@ -85,6 +85,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
supportsQL = true;
|
||||
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
|
||||
306
web/src/components/ak-search-ql/index.ts
Normal file
306
web/src/components/ak-search-ql/index.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/buttons/Dropdown";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import DjangoQL, { Introspections } from "@mrmarble/djangoql-completion";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFSearchInput from "@patternfly/patternfly/components/SearchInput/search-input.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
export class QL extends DjangoQL {
|
||||
createCompletionElement() {
|
||||
this.completionEnabled = !!this.options.completionEnabled;
|
||||
return;
|
||||
}
|
||||
logError(message: string): void {
|
||||
console.warn(`authentik/ql: ${message}`);
|
||||
}
|
||||
textareaResize() {}
|
||||
}
|
||||
|
||||
@customElement("ak-search-ql")
|
||||
export class QLSearch extends AKElement {
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
@query("[name=search]")
|
||||
searchElement?: HTMLTextAreaElement;
|
||||
|
||||
@state()
|
||||
menuOpen = false;
|
||||
|
||||
@property()
|
||||
onSearch?: (value: string) => void;
|
||||
|
||||
@state()
|
||||
selected?: number;
|
||||
|
||||
@state()
|
||||
cursorX: number = 0;
|
||||
|
||||
@state()
|
||||
cursorY: number = 0;
|
||||
|
||||
ql?: QL;
|
||||
canvas?: CanvasRenderingContext2D;
|
||||
|
||||
set apiResponse(value: PaginatedResponse<unknown> | undefined) {
|
||||
if (!value || !value.autocomplete || !this.ql) {
|
||||
return;
|
||||
}
|
||||
this.ql.loadIntrospections(value.autocomplete as unknown as Introspections);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFFormControl,
|
||||
PFSearchInput,
|
||||
css`
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
.ql.pf-c-form-control {
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
height: 2.25em;
|
||||
}
|
||||
.selected {
|
||||
background-color: var(--pf-c-search-input__menu-item--hover--BackgroundColor);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu {
|
||||
--pf-c-search-input__menu--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu-item {
|
||||
--pf-c-search-input__menu-item--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu-item:hover {
|
||||
--pf-c-search-input__menu-item--BackgroundColor: var(
|
||||
--ak-dark-background-lighter
|
||||
);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu-list-item.selected {
|
||||
--pf-c-search-input__menu-item--hover--BackgroundColor: var(
|
||||
--ak-dark-background-light
|
||||
);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__text::before {
|
||||
border: 0;
|
||||
}
|
||||
.pf-c-search-input__menu {
|
||||
position: fixed;
|
||||
min-width: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (!this.searchElement) {
|
||||
return;
|
||||
}
|
||||
this.ql = new QL({
|
||||
completionEnabled: true,
|
||||
introspections: {
|
||||
current_model: "",
|
||||
models: {},
|
||||
},
|
||||
selector: this.searchElement,
|
||||
autoResize: false,
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
console.error("authentik/ql: failed to get canvas context");
|
||||
return;
|
||||
}
|
||||
context.font = window.getComputedStyle(this.searchElement).font;
|
||||
this.canvas = context;
|
||||
}
|
||||
|
||||
refreshCompletions() {
|
||||
this.value = this.searchElement?.value;
|
||||
if (!this.ql) {
|
||||
return;
|
||||
}
|
||||
this.ql.generateSuggestions();
|
||||
if (this.ql.suggestions.length < 1 || this.ql.loading) {
|
||||
this.menuOpen = false;
|
||||
return;
|
||||
}
|
||||
this.menuOpen = true;
|
||||
this.updateDropdownPosition();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
updateDropdownPosition() {
|
||||
if (!this.searchElement) {
|
||||
return;
|
||||
}
|
||||
const bcr = this.getBoundingClientRect();
|
||||
// We need the width of a letter to measure x; we use a monospaced font but still
|
||||
// check the length for `m` as its the widest ASCII char
|
||||
const metrics = this.canvas?.measureText("m");
|
||||
const letterWidth = Math.ceil(metrics?.width || 0) + 1;
|
||||
|
||||
// Mostly static variables for padding, font line-height and how many
|
||||
const lineHeight = parseInt(window.getComputedStyle(this.searchElement).lineHeight, 10);
|
||||
const paddingTop = parseInt(window.getComputedStyle(this.searchElement).paddingTop, 10);
|
||||
const paddingLeft = parseInt(window.getComputedStyle(this.searchElement).paddingLeft, 10);
|
||||
const paddingRight = parseInt(window.getComputedStyle(this.searchElement).paddingRight, 10);
|
||||
const actualInnerWidth = bcr.width - paddingLeft - paddingRight;
|
||||
|
||||
let relX = 0;
|
||||
let relY = 1;
|
||||
let letterIndex = 0;
|
||||
|
||||
this.searchElement.value.split(" ").some((word, idx) => {
|
||||
letterIndex += word.length;
|
||||
const newRelX = relX + word.length * letterWidth;
|
||||
if (newRelX > actualInnerWidth) {
|
||||
relY += 1;
|
||||
if (letterIndex > this.searchElement!.selectionStart) {
|
||||
relX =
|
||||
letterWidth * word.length -
|
||||
(letterIndex - this.searchElement!.selectionStart) * letterWidth;
|
||||
return true;
|
||||
}
|
||||
relX = word.length * letterWidth;
|
||||
} else {
|
||||
relX = newRelX + 1;
|
||||
}
|
||||
});
|
||||
|
||||
this.cursorX = bcr.x + paddingLeft + relX;
|
||||
this.cursorY = bcr.y + paddingTop + relY * lineHeight;
|
||||
}
|
||||
|
||||
onKeyDown(ev: KeyboardEvent) {
|
||||
this.updateDropdownPosition();
|
||||
if (ev.key === "Enter" && ev.metaKey && this.onSearch && this.searchElement) {
|
||||
this.onSearch(this.searchElement?.value);
|
||||
return;
|
||||
}
|
||||
if (!this.menuOpen) return;
|
||||
switch (ev.key) {
|
||||
case "ArrowUp":
|
||||
if (this.ql?.suggestions.length) {
|
||||
if (this.selected === undefined) {
|
||||
this.selected = this.ql?.suggestions.length - 1;
|
||||
} else if (this.selected === 0) {
|
||||
this.selected = undefined;
|
||||
} else {
|
||||
this.selected -= 1;
|
||||
}
|
||||
this.refreshCompletions();
|
||||
ev.preventDefault();
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (this.ql?.suggestions.length) {
|
||||
if (this.selected === undefined) {
|
||||
this.selected = 0;
|
||||
} else if (this.selected < this.ql?.suggestions.length - 1) {
|
||||
this.selected += 1;
|
||||
} else {
|
||||
this.selected = undefined;
|
||||
}
|
||||
this.refreshCompletions();
|
||||
ev.preventDefault();
|
||||
}
|
||||
break;
|
||||
case "Tab":
|
||||
if (this.selected) {
|
||||
this.ql?.selectCompletion(this.selected);
|
||||
ev.preventDefault();
|
||||
}
|
||||
break;
|
||||
case "Enter":
|
||||
// Technically this is a textarea, due to automatic multi-line feature,
|
||||
// but other than that it should look and behave like a normal input.
|
||||
// So expected behavior when pressing Enter is to submit the form,
|
||||
// not to add a new line.
|
||||
if (this.selected !== undefined) {
|
||||
this.ql?.selectCompletion(this.selected);
|
||||
}
|
||||
ev.preventDefault();
|
||||
break;
|
||||
case "Escape":
|
||||
this.menuOpen = false;
|
||||
break;
|
||||
case "Shift": // Shift
|
||||
case "Control": // Ctrl
|
||||
case "Alt": // Alt
|
||||
case "Meta": // Windows Key or Cmd on Mac
|
||||
// Control keys shouldn't trigger completion popup
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
if (!this.menuOpen || !this.ql) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
class="pf-c-search-input__menu"
|
||||
style="left: ${this.cursorX}px; top: ${this.cursorY}px;"
|
||||
>
|
||||
<ul class="pf-c-search-input__menu-list">
|
||||
${this.ql.suggestions.map((suggestion, idx) => {
|
||||
return html`<li
|
||||
class="pf-c-search-input__menu-list-item ${this.selected === idx
|
||||
? "selected"
|
||||
: ""}"
|
||||
>
|
||||
<button
|
||||
class="pf-c-search-input__menu-item"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.ql?.selectCompletion(idx);
|
||||
this.refreshCompletions();
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-search-input__menu-item-text"
|
||||
>${suggestion.text}</span
|
||||
>
|
||||
</button>
|
||||
</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-search-input">
|
||||
<div class="pf-c-search-input__bar">
|
||||
<span class="pf-c-search-input__text">
|
||||
<textarea
|
||||
class="pf-c-form-control ql"
|
||||
name="search"
|
||||
placeholder=${msg("Search...")}
|
||||
spellcheck="false"
|
||||
@input=${(ev: InputEvent) => this.refreshCompletions()}
|
||||
@keydown=${this.onKeyDown}
|
||||
>
|
||||
${ifDefined(this.value)}</textarea
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
${this.renderMenu()}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-search-ql": QLSearch;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import {
|
||||
APIError,
|
||||
@ -31,13 +32,20 @@ import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";
|
||||
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { Pagination } from "@goauthentik/api";
|
||||
import { LicenseSummaryStatusEnum, Pagination } from "@goauthentik/api";
|
||||
|
||||
export interface TableLike {
|
||||
order?: string;
|
||||
fetch: () => void;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
pagination: Pagination;
|
||||
autocomplete?: { [key: string]: string };
|
||||
|
||||
results: Array<T>;
|
||||
}
|
||||
|
||||
export class TableColumn {
|
||||
title: string;
|
||||
orderBy?: string;
|
||||
@ -94,19 +102,16 @@ export class TableColumn {
|
||||
}
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
pagination: Pagination;
|
||||
|
||||
results: Array<T>;
|
||||
}
|
||||
|
||||
export abstract class Table<T> extends AKElement implements TableLike {
|
||||
export abstract class Table<T> extends WithLicenseSummary(AKElement) implements TableLike {
|
||||
abstract apiEndpoint(): Promise<PaginatedResponse<T>>;
|
||||
abstract columns(): TableColumn[];
|
||||
abstract row(item: T): SlottedTemplateResult[];
|
||||
|
||||
private isLoading = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
supportsQL: boolean = false;
|
||||
|
||||
searchEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
@ -181,6 +186,12 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
PFDropdown,
|
||||
PFPagination,
|
||||
css`
|
||||
.pf-c-toolbar__group.pf-m-search-filter.ql {
|
||||
flex-grow: 1;
|
||||
}
|
||||
ak-table-search.ql {
|
||||
width: 100% !important;
|
||||
}
|
||||
.pf-c-table thead .pf-c-table__check {
|
||||
min-width: 3rem;
|
||||
}
|
||||
@ -474,14 +485,17 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
});
|
||||
this.fetch();
|
||||
};
|
||||
|
||||
const isQL =
|
||||
this.supportsQL && this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed;
|
||||
return !this.searchEnabled()
|
||||
? html``
|
||||
: html`<div class="pf-c-toolbar__group pf-m-search-filter">
|
||||
: html`<div class="pf-c-toolbar__group pf-m-search-filter ${isQL ? "ql" : ""}">
|
||||
<ak-table-search
|
||||
class="pf-c-toolbar__item pf-m-search-filter"
|
||||
?supportsQL=${this.supportsQL}
|
||||
class="pf-c-toolbar__item pf-m-search-filter ${isQL ? "ql" : ""}"
|
||||
value=${ifDefined(this.search)}
|
||||
.onSearch=${runSearch}
|
||||
.apiResponse=${this.data}
|
||||
>
|
||||
</ak-table-search>
|
||||
</div>`;
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import "@goauthentik/components/ak-search-ql";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
@ -11,11 +14,19 @@ import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-gro
|
||||
import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-table-search")
|
||||
export class TableSearch extends AKElement {
|
||||
export class TableSearch extends WithLicenseSummary(AKElement) {
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
supportsQL: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
apiResponse?: PaginatedResponse<unknown>;
|
||||
|
||||
@property()
|
||||
onSearch?: (value: string) => void;
|
||||
|
||||
@ -30,39 +41,63 @@ export class TableSearch extends AKElement {
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
ak-search-ql {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
renderInput(): TemplateResult {
|
||||
if (
|
||||
this.supportsQL &&
|
||||
this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed
|
||||
) {
|
||||
return html`<ak-search-ql
|
||||
.apiResponse=${this.apiResponse}
|
||||
.value=${this.value}
|
||||
.onSearch=${(value: string) => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch(value);
|
||||
}}
|
||||
name="search"
|
||||
></ak-search-ql>`;
|
||||
}
|
||||
return html`<input
|
||||
class="pf-c-form-control"
|
||||
name="search"
|
||||
type="search"
|
||||
placeholder=${msg("Search...")}
|
||||
value="${ifDefined(this.value)}"
|
||||
@search=${(ev: Event) => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch((ev.target as HTMLInputElement).value);
|
||||
}}
|
||||
/>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<form
|
||||
class="pf-c-input-group"
|
||||
method="GET"
|
||||
method="get"
|
||||
@submit=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.onSearch) return;
|
||||
const el = this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
|
||||
const el = this.shadowRoot?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
|
||||
"[name=search]",
|
||||
);
|
||||
if (!el) return;
|
||||
if (el.value === "") return;
|
||||
this.onSearch(el?.value);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
name="search"
|
||||
type="search"
|
||||
placeholder=${msg("Search...")}
|
||||
value="${ifDefined(this.value)}"
|
||||
@search=${(ev: Event) => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch((ev.target as HTMLInputElement).value);
|
||||
}}
|
||||
/>
|
||||
${this.renderInput()}
|
||||
<button
|
||||
class="pf-c-button pf-m-control"
|
||||
type="reset"
|
||||
@click=${() => {
|
||||
if (!this.onSearch) return;
|
||||
this.value = "";
|
||||
this.onSearch("");
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user