UNPKG

@limetech/lime-elements

Version:
725 lines (724 loc) • 26.1 kB
import { h, Host, } from "@stencil/core"; import { createRandomString } from "../../util/random-string"; import CodeMirror from "codemirror"; import "codemirror/mode/css/css"; import "codemirror/mode/htmlmixed/htmlmixed"; import "codemirror/mode/javascript/javascript"; import "codemirror/mode/jinja2/jinja2"; import "codemirror/addon/selection/active-line"; import "codemirror/addon/edit/matchbrackets"; import "codemirror/addon/edit/matchtags"; import "codemirror/addon/lint/lint"; import "codemirror/addon/lint/json-lint"; import "codemirror/addon/fold/foldgutter"; import "codemirror/addon/fold/brace-fold"; import "codemirror/addon/fold/xml-fold"; import jslint from "jsonlint-mod"; import translate from "../../global/translations"; /** * @exampleComponent limel-example-code-editor-basic * @exampleComponent limel-example-code-editor-readonly-with-line-numbers * @exampleComponent limel-example-code-editor-fold-lint-wrap * @exampleComponent limel-example-code-editor-copy * @exampleComponent limel-example-code-editor-composite */ export class CodeEditor { constructor() { /** * The code to be rendered */ this.value = ''; /** * Set to `true` to make the editor read-only. * Use `readonly` when the editor is only there to present the data it holds, * and will not become possible for the current user to edit. */ this.readonly = false; /** * Set to `true` to disable the editor. * Use `disabled` to indicate that the editor can normally be interacted * with, but is currently disabled. This tells the user that if certain * requirements are met, the editor may become enabled again. */ this.disabled = false; /** * Set to `true` to indicate that the current value of the input editor is * invalid. */ this.invalid = false; /** * Set to `true` to indicate that the field is required. */ this.required = false; /** * Displays line numbers in the editor */ this.lineNumbers = false; /** * Wraps long lines instead of showing horizontal scrollbar */ this.lineWrapping = false; /** * Allows the user to fold code */ this.fold = false; /** * Enables linting of JSON content */ this.lint = false; /** * Select color scheme for the editor */ this.colorScheme = 'auto'; /** * Defines the language for translations. * Will translate the translatable strings on the components. */ this.translationLanguage = 'en'; /** * Set to false to hide the copy button */ this.showCopyButton = true; this.wasCopied = 'idle'; this.handleChangeDarkMode = () => { if (this.colorScheme !== 'auto') { return; } this.forceRedraw(); }; this.handleChange = () => { this.change.emit(this.editor.getValue()); }; this.handleResize = () => { if (!this.editor) { return; } this.editor.refresh(); }; this.renderHelperLine = () => { if (!this.helperText) { return; } return (h("limel-helper-line", { helperText: this.helperText, helperTextId: this.helperTextId, invalid: this.invalid })); }; this.copyCode = async () => { var _a, _b, _c; // Prefer the live editor content; fall back to the prop value const text = (_c = (_b = (_a = this.editor) === null || _a === void 0 ? void 0 : _a.getValue()) !== null && _b !== void 0 ? _b : this.value) !== null && _c !== void 0 ? _c : ''; try { await navigator.clipboard.writeText(text); this.wasCopied = 'success'; setTimeout(() => { this.wasCopied = 'idle'; }, 2000); } catch (error) { console.error('Failed to copy to clipboard:', error); this.wasCopied = 'failed'; } }; this.labelId = createRandomString(); this.helperTextId = createRandomString(); } connectedCallback() { this.observer = new ResizeObserver(this.handleResize); this.observer.observe(this.host); this.darkMode.addEventListener('change', this.handleChangeDarkMode); } disconnectedCallback() { var _a; this.observer.unobserve(this.host); (_a = this.editor) === null || _a === void 0 ? void 0 : _a.off('change', this.handleChange); this.editor = null; this.darkMode.removeEventListener('change', this.handleChangeDarkMode); const editorElement = this.host.shadowRoot.querySelector('.editor'); // eslint-disable-next-line no-unsafe-optional-chaining for (const child of editorElement === null || editorElement === void 0 ? void 0 : editorElement.childNodes) { child.remove(); } } componentDidRender() { if (this.editor) { return; } this.editor = this.createEditor(); this.updateInputFieldAccessibilityAttributes(); } watchValue(newValue) { if (!this.editor) { return; } const currentValue = this.editor.getValue(); if (newValue === currentValue) { // Circuit breaker for when the change comes from the editor itself // The caret position will be reset without this return; } this.editor.getDoc().setValue(newValue || ''); } watchDisabled() { this.updateEditorReadOnlyState(); this.updateInputFieldAccessibilityAttributes(); } watchReadonly() { this.updateEditorReadOnlyState(); this.updateInputFieldAccessibilityAttributes(); } watchInvalid() { this.updateInputFieldAccessibilityAttributes(); } watchRequired() { this.updateInputFieldAccessibilityAttributes(); } watchHelperText() { this.updateInputFieldAccessibilityAttributes(); } createEditor() { const options = this.getOptions(); const editor = CodeMirror(this.host.shadowRoot.querySelector('.editor'), options); editor.on('change', this.handleChange); // Replace tab with spaces and use the actual indent setting for // the space count editor.setOption('extraKeys', { Tab: (codeMirror) => { const spaces = ' '.repeat(codeMirror.getOption('indentUnit')); codeMirror.replaceSelection(spaces); }, }); return editor; } getOptions() { let mode = this.language; const TAB_SIZE = 4; let theme = 'lime light'; const gutters = []; if (this.isDarkMode()) { theme = 'lime dark'; } if (this.language === 'json') { mode = { name: 'application/json', json: true, }; if (this.lint) { gutters.push('CodeMirror-lint-markers'); if (!('jsonlint' in window)) { window['jsonlint'] = jslint; } } } else if (this.language === 'typescript') { mode = { name: 'application/typescript', typescript: true, }; } else if (this.language === 'html') { mode = 'htmlmixed'; } if (this.fold) { gutters.push('CodeMirror-foldgutter'); } return { mode: mode, value: this.value || '', theme: theme, readOnly: this.getReadOnlyOption(), tabSize: TAB_SIZE, indentUnit: TAB_SIZE, lineNumbers: this.lineNumbers, lineWrapping: this.lineWrapping, styleActiveLine: true, matchBrackets: true, matchTags: { bothTags: true }, lint: this.lint, foldGutter: this.fold, gutters: gutters, }; } isDarkMode() { if (this.colorScheme !== 'auto') { return this.colorScheme === 'dark'; } return this.darkMode.matches; } render() { const classList = { editor: true, readonly: this.readonly || this.disabled, disabled: this.disabled, 'is-dark-mode': this.isDarkMode(), 'is-light-mode': !this.isDarkMode(), }; return (h(Host, { key: '648df79d8ea88a3f87028743197214b45f3708af' }, this.renderCopyButton(), h("limel-notched-outline", { key: '8ec590da7d7cfb0c313ae61ecf517f36e978a756', labelId: this.labelId, label: this.label, required: this.required, invalid: this.invalid, disabled: this.disabled, readonly: this.readonly, hasValue: !!this.value, hasFloatingLabel: true }, h("div", { key: '3fd86155267f590d9578ed673d56e1e2fe6ee7ce', slot: "content", class: classList })), this.renderHelperLine())); } forceRedraw() { // eslint-disable-next-line sonarjs/pseudo-random this.random = Math.random(); } renderCopyButton() { var _a; const hasContent = !!(((_a = this.editor) === null || _a === void 0 ? void 0 : _a.getValue()) || this.value); if (!hasContent || this.disabled || !this.showCopyButton) { return; } return (h("button", { class: "copy-button", onClick: this.copyCode, type: "button", "aria-live": "polite", "aria-atomic": "true", "aria-label": this.getButtonAriaLabel() }, this.getButtonText())); } getButtonText() { if (this.wasCopied === 'success') { return translate.get('code-editor.copied', this.translationLanguage); } if (this.wasCopied === 'failed') { return translate.get('code-editor.copy-failed', this.translationLanguage); } return translate.get('code-editor.copy', this.translationLanguage); } getButtonAriaLabel() { const label = this.label || translate.get('code-editor', this.translationLanguage); const translationKeys = { success: 'code-editor.copied-aria-label', failed: 'code-editor.copy-failed-aria-label', idle: 'code-editor.copy-aria-label', }; const key = translationKeys[this.wasCopied]; return translate.get(key, this.translationLanguage, { label }); } get darkMode() { return matchMedia('(prefers-color-scheme: dark)'); } updateEditorReadOnlyState() { if (!this.editor) { return; } this.editor.setOption('readOnly', this.getReadOnlyOption()); } getReadOnlyOption() { if (this.disabled) { return 'nocursor'; } return this.readonly; } updateInputFieldAccessibilityAttributes() { if (!this.editor) { return; } const inputField = this.editor.getInputField(); if (!inputField) { return; } inputField.id = this.labelId; if (this.helperText) { inputField.setAttribute('aria-describedby', this.helperTextId); inputField.setAttribute('aria-controls', this.helperTextId); } else { inputField.removeAttribute('aria-describedby'); inputField.removeAttribute('aria-controls'); } if (this.required) { inputField.setAttribute('aria-required', 'true'); } else { inputField.removeAttribute('aria-required'); } if (this.invalid) { inputField.setAttribute('aria-invalid', 'true'); } else { inputField.removeAttribute('aria-invalid'); } if (this.disabled) { inputField.setAttribute('aria-disabled', 'true'); } else { inputField.removeAttribute('aria-disabled'); } if (this.readonly || this.disabled) { inputField.setAttribute('aria-readonly', 'true'); } else { inputField.removeAttribute('aria-readonly'); } } static get is() { return "limel-code-editor"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["code-editor.scss"] }; } static get styleUrls() { return { "$": ["code-editor.css"] }; } static get properties() { return { "value": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The code to be rendered" }, "getter": false, "setter": false, "reflect": false, "attribute": "value", "defaultValue": "''" }, "language": { "type": "string", "mutable": false, "complexType": { "original": "Language", "resolved": "\"css\" | \"html\" | \"javascript\" | \"jinja2\" | \"json\" | \"typescript\"", "references": { "Language": { "location": "import", "path": "./code-editor.types", "id": "src/components/code-editor/code-editor.types.ts::Language", "referenceLocation": "Language" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "The language of the code" }, "getter": false, "setter": false, "reflect": false, "attribute": "language" }, "readonly": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to make the editor read-only.\nUse `readonly` when the editor is only there to present the data it holds,\nand will not become possible for the current user to edit." }, "getter": false, "setter": false, "reflect": true, "attribute": "readonly", "defaultValue": "false" }, "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to disable the editor.\nUse `disabled` to indicate that the editor can normally be interacted\nwith, but is currently disabled. This tells the user that if certain\nrequirements are met, the editor may become enabled again." }, "getter": false, "setter": false, "reflect": true, "attribute": "disabled", "defaultValue": "false" }, "invalid": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to indicate that the current value of the input editor is\ninvalid." }, "getter": false, "setter": false, "reflect": true, "attribute": "invalid", "defaultValue": "false" }, "required": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to indicate that the field is required." }, "getter": false, "setter": false, "reflect": true, "attribute": "required", "defaultValue": "false" }, "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The input label." }, "getter": false, "setter": false, "reflect": true, "attribute": "label" }, "helperText": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Optional helper text to display below the input field when it has focus" }, "getter": false, "setter": false, "reflect": true, "attribute": "helper-text" }, "lineNumbers": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Displays line numbers in the editor" }, "getter": false, "setter": false, "reflect": true, "attribute": "line-numbers", "defaultValue": "false" }, "lineWrapping": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Wraps long lines instead of showing horizontal scrollbar" }, "getter": false, "setter": false, "reflect": true, "attribute": "line-wrapping", "defaultValue": "false" }, "fold": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Allows the user to fold code" }, "getter": false, "setter": false, "reflect": true, "attribute": "fold", "defaultValue": "false" }, "lint": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Enables linting of JSON content" }, "getter": false, "setter": false, "reflect": true, "attribute": "lint", "defaultValue": "false" }, "colorScheme": { "type": "string", "mutable": false, "complexType": { "original": "ColorScheme", "resolved": "\"auto\" | \"dark\" | \"light\"", "references": { "ColorScheme": { "location": "import", "path": "./code-editor.types", "id": "src/components/code-editor/code-editor.types.ts::ColorScheme", "referenceLocation": "ColorScheme" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Select color scheme for the editor" }, "getter": false, "setter": false, "reflect": true, "attribute": "color-scheme", "defaultValue": "'auto'" }, "translationLanguage": { "type": "string", "mutable": false, "complexType": { "original": "Languages", "resolved": "\"da\" | \"de\" | \"en\" | \"fi\" | \"fr\" | \"nb\" | \"nl\" | \"no\" | \"sv\"", "references": { "Languages": { "location": "import", "path": "../date-picker/date.types", "id": "src/components/date-picker/date.types.ts::Languages", "referenceLocation": "Languages" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Defines the language for translations.\nWill translate the translatable strings on the components." }, "getter": false, "setter": false, "reflect": true, "attribute": "translation-language", "defaultValue": "'en'" }, "showCopyButton": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to false to hide the copy button" }, "getter": false, "setter": false, "reflect": true, "attribute": "show-copy-button", "defaultValue": "true" } }; } static get states() { return { "random": {}, "wasCopied": {} }; } static get events() { return [{ "method": "change", "name": "change", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the code has changed. Will only be emitted when the code\narea has lost focus" }, "complexType": { "original": "string", "resolved": "string", "references": {} } }]; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "value", "methodName": "watchValue" }, { "propName": "disabled", "methodName": "watchDisabled" }, { "propName": "readonly", "methodName": "watchReadonly" }, { "propName": "invalid", "methodName": "watchInvalid" }, { "propName": "required", "methodName": "watchRequired" }, { "propName": "helperText", "methodName": "watchHelperText" }]; } }