browsertrix/frontend/src/components/queue-exclusion-form.ts

239 lines
6.6 KiB
TypeScript

import { state, property } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize";
import debounce from "lodash/fp/debounce";
import LiteElement, { html } from "../utils/LiteElement";
import { regexEscape } from "../utils/string";
export type Exclusion = {
type: "text" | "regex";
value: string;
};
export type ExclusionChangeEvent = CustomEvent<{
value: string;
valid: boolean;
}>;
export type ExclusionAddEvent = CustomEvent<{
regex: string;
onSuccess: () => void;
}>;
const MIN_LENGTH = 2;
/**
* Crawl queue exclusion form
*
* Usage example:
* ```ts
* <btrix-queue-exclusion-form
* @on-change=${this.handleExclusionChange}
* @on-add=${this.handleExclusionAdd}
* ></btrix-queue-exclusion-form>
* ```
*
* @event on-change ExclusionChangeEvent
* @event on-add ExclusionAddEvent
*/
@localized()
export class QueueExclusionForm extends LiteElement {
@property({ type: Boolean })
isSubmitting = false;
@property({ type: String })
fieldErrorMessage = "";
@state()
private selectValue: Exclusion["type"] = "text";
@state()
private inputValue = "";
@state()
private isRegexInvalid = false;
async willUpdate(changedProperties: Map<string, any>) {
if (
changedProperties.get("selectValue") ||
(changedProperties.has("inputValue") &&
changedProperties.get("inputValue") !== undefined)
) {
this.fieldErrorMessage = "";
this.checkInputValidity();
this.dispatchChangeEvent();
}
}
disconnectedCallback(): void {
this.onInput.cancel();
super.disconnectedCallback();
}
render() {
return html`
<fieldset>
<div class="flex">
<div class="px-1 flex-0 w-40">
<sl-select
placeholder=${msg("Select Type")}
size="small"
value=${this.selectValue}
@sl-hide=${this.stopProp}
@sl-after-hide=${this.stopProp}
@sl-change=${(e: any) => {
this.selectValue = e.target.value;
}}
>
<sl-option value="text">${msg("Matches Text")}</sl-option>
<sl-option value="regex">${msg("Regex")}</sl-option>
</sl-select>
</div>
<div class="pl-1 flex-1 flex">
<div class="flex-1 mr-1 mb-2 md:mb-0">
<sl-input
class=${this.fieldErrorMessage ? "invalid" : ""}
size="small"
autocomplete="off"
minlength=${MIN_LENGTH}
placeholder=${this.selectValue === "text"
? "/skip-this-page"
: "example.com/skip.*"}
.value=${this.inputValue}
?disabled=${this.isSubmitting}
@keydown=${this.onKeyDown}
@sl-input=${this.onInput}
>
${this.fieldErrorMessage
? html`
<div slot="help-text">
${this.isRegexInvalid
? html`
<p class="text-danger">
${msg(
html`Regular Expression syntax error:
<code>${this.fieldErrorMessage}</code>`
)}
</p>
<p>
${msg(
html`Please enter a valid constructor string
pattern. See
<a
class="underline hover:no-underline"
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp"
target="_blank"
rel="noopener noreferrer nofollow"
><code>RegExp</code> docs</a
>.`
)}
</p>
`
: html`<p class="text-danger">
${this.fieldErrorMessage}
</p>`}
</div>
`
: ""}
</sl-input>
</div>
<div class="flex-0 w-10 pt-1 text-center">
<btrix-button
variant="primary"
raised
icon
?disabled=${!this.inputValue ||
this.isRegexInvalid ||
this.isSubmitting}
?loading=${this.isSubmitting}
@click=${this.onButtonClick}
>
<sl-icon name="plus-lg"></sl-icon>
</btrix-button>
</div>
</div>
</div>
</fieldset>
`;
}
private onInput = debounce(200)((e: any) => {
this.inputValue = e.target.value;
}) as any;
private onButtonClick() {
this.handleAdd();
}
private onKeyDown = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === "Enter") {
this.handleAdd();
}
};
private checkInputValidity(): void {
let isValid = true;
if (!this.inputValue || this.inputValue.length < MIN_LENGTH) {
isValid = false;
} else if (this.selectValue === "regex") {
try {
// Check if valid regex
new RegExp(this.inputValue);
} catch (err: any) {
this.fieldErrorMessage = err.message;
isValid = false;
}
}
this.isRegexInvalid = !isValid;
}
private async dispatchChangeEvent() {
await this.updateComplete;
this.dispatchEvent(
new CustomEvent("on-change", {
detail: {
value:
this.selectValue === "text"
? regexEscape(this.inputValue)
: this.inputValue,
valid: !this.isRegexInvalid,
},
}) as ExclusionChangeEvent
);
}
private async handleAdd() {
this.onInput.flush();
await this.updateComplete;
if (!this.inputValue) return;
let regex = this.inputValue;
if (this.selectValue === "text") {
regex = regexEscape(this.inputValue);
}
this.dispatchEvent(
new CustomEvent("on-add", {
detail: {
regex,
onSuccess: () => {
this.inputValue = "";
},
},
}) as ExclusionAddEvent
);
}
/**
* Stop propgation of sl-select events.
* Prevents bug where sl-dialog closes when dropdown closes
* https://github.com/shoelace-style/shoelace/issues/170
*/
private stopProp(e: CustomEvent) {
e.stopPropagation();
}
}