@limetech/lime-elements
Version:
725 lines (724 loc) • 26.1 kB
JavaScript
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"
}];
}
}