browsertrix/frontend/src/components/config-editor.ts
2022-04-06 17:40:25 -07:00

186 lines
5.1 KiB
TypeScript

import { property, state, query } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import {
parse as yamlToJson,
stringify as yamlStringify,
YAMLParseError,
} from "yaml";
import LiteElement, { html } from "../utils/LiteElement";
/**
* Usage example:
* ```ts
* <btrix-config-editor
* value=${value}
* @on-change=${handleChange}
* >
* </btrix-config-editor>
* ```
*
* @event on-change
*/
@localized()
export class ConfigEditor extends LiteElement {
@property({ type: String })
value = "";
@state()
errorMessage = "";
@query("#config-editor-textarea")
textareaElem?: HTMLTextAreaElement;
render() {
return html`
<article class="border rounded">
<header
class="flex items-center justify-between bg-neutral-50 border-b p-1"
>
<div class="px-1">
${this.errorMessage
? html`
<sl-icon
class="text-danger inline-block align-middle mr-1"
name="x-octagon"
></sl-icon>
<span
class="inline-block align-middle text-sm text-neutral-500"
>${msg("Invalid Configuration")}</span
>
`
: html`
<sl-icon
class="text-success inline-block align-middle mr-1"
name="check2"
></sl-icon>
<span
class="inline-block align-middle text-sm text-neutral-500"
>${msg("Valid Configuration")}</span
>
`}
</div>
<btrix-copy-button
.getValue=${() => this.textareaElem?.value}
></btrix-copy-button>
</header>
${this.renderTextArea()}
<div class="text-sm">
${this.errorMessage
? html`<btrix-alert type="danger">
<div class="whitespace-pre-wrap">${this.errorMessage}</div>
</btrix-alert> `
: ""}
</div>
</article>
`;
}
private renderTextArea() {
const lineCount = this.value.split("\n").length;
return html`
<div class="flex font-mono text-sm leading-relaxed py-2">
<div class="shrink-0 w-12 px-2 text-right text-neutral-300">
${[...new Array(lineCount)].map((line, i) => html`${i + 1}<br />`)}
</div>
<div class="flex-1 px-2 overflow-x-auto text-slate-600">
<textarea
name="config"
id="config-editor-textarea"
class="language-yaml block w-full h-full overflow-y-hidden outline-none resize-none"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
wrap="off"
rows=${lineCount}
.value=${this.value}
@keydown=${(e: any) => {
const textarea = e.target;
// Add indentation when pressing tab key instead of moving focus
if (e.keyCode === /* tab: */ 9) {
e.preventDefault();
textarea.setRangeText(
" ",
textarea.selectionStart,
textarea.selectionStart,
"end"
);
}
}}
@change=${(e: any) => {
e.stopPropagation();
this.onChange((e.target as HTMLTextAreaElement).value);
}}
@blur=${(e: any) => {
e.stopPropagation();
this.onBlur((e.target as HTMLTextAreaElement).value);
}}
@paste=${(e: any) => {
// Use timeout to get value after paste
window.setTimeout(() => {
this.onChange((e.target as HTMLTextAreaElement).value);
});
}}
></textarea>
</div>
</div>
`;
}
private handleParseError(error: Error) {
if (error instanceof YAMLParseError) {
const errorMessage = error.message.replace("YAMLParseError: ", "");
this.errorMessage = errorMessage;
} else {
this.errorMessage = msg("Invalid YAML or JSON");
console.debug(error);
}
}
private checkValidity(value: string) {
yamlToJson(value);
}
private onBlur(value: string) {
if (!value) {
this.textareaElem?.setCustomValidity(msg("Please fill out this field"));
this.textareaElem?.reportValidity();
}
}
private onChange(value: string) {
try {
this.checkValidity(value);
this.textareaElem?.setCustomValidity("");
this.errorMessage = "";
this.dispatchEvent(
new CustomEvent("on-change", {
detail: {
value: value,
},
})
);
} catch (e: any) {
this.textareaElem?.setCustomValidity(msg("Please fix errors"));
this.handleParseError(e);
}
this.textareaElem?.reportValidity();
}
/**
* 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();
}
}