UNPKG

@limetech/lime-elements

Version:
678 lines (677 loc) • 28.8 kB
import { Host, h } from "@stencil/core"; import { createRandomString } from "../../util/random-string"; /** * A rich text editor that offers a rich text editing experience with markdown support, * in the sense that you can easily type markdown syntax and see the rendered * result as rich text in real-time. For instance, you can type `# Hello, world!` * and see it directly turning to a heading 1 (an `<h1>` HTML element). * * Naturally, you can use standard keyboard hotkeys such as <kbd>Ctrl</kbd> + <kbd>B</kbd> * to toggle bold text, <kbd>Ctrl</kbd> + <kbd>I</kbd> to toggle italic text, and so on. * * @exampleComponent limel-example-text-editor-basic * @exampleComponent limel-example-text-editor-as-form-component * @exampleComponent limel-example-text-editor-with-markdown * @exampleComponent limel-example-text-editor-with-html * @exampleComponent limel-example-text-editor-with-tables * @exampleComponent limel-example-text-editor-with-inline-images-file-storage * @exampleComponent limel-example-text-editor-with-inline-images-base64 * @exampleComponent limel-example-text-editor-allow-resize * @exampleComponent limel-example-text-editor-size * @exampleComponent limel-example-text-editor-ui * @exampleComponent limel-example-text-editor-custom-element * @exampleComponent limel-example-text-editor-triggers * @exampleComponent limel-example-text-editor-composite * @beta */ export class TextEditor { constructor() { /** * The type of content that the editor should handle and emit, defaults to `markdown` * * Assumed to be set only once, so not reactive to changes */ this.contentType = 'markdown'; /** * Defines the language for translations. */ this.language = 'en'; /** * Set to `true` to disable the field. * Use `disabled` to indicate that the field can normally be interacted * with, but is currently disabled. This tells the user that if certain * requirements are met, the field may become enabled again. */ this.disabled = false; /** * Set to `true` to make the component read-only. * Use `readonly` when the field is only there to present the data it holds, * and will not become possible for the current user to edit. * :::note * Consider that it might be better to use `limel-markdown` * instead of `limel-text-editor` when the goal is visualizing data. * ::: */ this.readonly = false; /** * Set to `true` to indicate that the current value of the editor is * invalid. */ this.invalid = false; /** * A list of custom elements * * Any `CustomElement` that should be used inside the text editor needs * to be defined here. * * @private * @alpha */ this.customElements = []; /** * A set of trigger characters * * Defining a character here will enable trigger events to be sent if the * character is detected in the editor. * * @private * @alpha */ this.triggers = []; /** * Set to `true` to indicate that the field is required. * * :::important * An empty but required field is not automatically considered invalid. * You must make sure to check the validity of the field on your own, * and properly handle the `invalid` state. * ::: */ this.required = false; /** * Set to `true` to allow the user to vertically resize the editor. * Set to `false` to disable the resize functionality. */ this.allowResize = true; /** * Specifies the visual appearance of the editor. * * - `standard`: The default editor appearance with a full toolbar and * standard layout. * - `minimal`: A compact editor appearance, ideal for limited space * scenarios such as mobile devices. In this mode, the toolbar is hidden * until the editor is focused. * - `no-toolbar`: A basic textarea appearance without any text styling toolbar. * This mode is suitable for scenarios where you want to provide a simple * text input without any visible formatting options; but still provide * support for markdown syntax and rich text, using hotkeys or when pasting. */ this.ui = 'standard'; this.renderHelperLine = () => { if (!this.helperText) { return; } return (h("limel-helper-line", { helperText: this.helperText, helperTextId: this.helperTextId, invalid: this.isInvalid() })); }; this.isInvalid = () => { if (this.readonly) { // A readonly field can never be invalid. return false; } if (this.invalid) { return true; } }; this.handleChange = (event) => { event.stopPropagation(); this.change.emit(event.detail); }; this.handleImagePasted = (event) => { event.stopPropagation(); this.imagePasted.emit(event.detail); }; this.handleMetadataChange = (event) => { event.stopPropagation(); this.metadataChange.emit(event.detail); }; this.handleImageRemoved = (event) => { event.stopPropagation(); this.imageRemoved.emit(event.detail); }; this.helperTextId = createRandomString(); this.editorId = createRandomString(); } render() { return (h(Host, { key: '31661aea27e0dea61827560c031587bd208f8bed' }, h("limel-notched-outline", { key: '81571a171ba0b629117d8b153e091e6d34c98c70', labelId: this.editorId, label: this.label, required: this.required, invalid: this.invalid, disabled: this.disabled, readonly: this.readonly, hasValue: !!this.value, hasFloatingLabel: true }, this.renderEditor(), this.renderPlaceholder()), this.renderHelperLine())); } renderEditor() { if (this.readonly) { return (h("limel-markdown", { slot: "content", value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId })); } return (h("limel-prosemirror-adapter", { slot: "content", "aria-placeholder": this.placeholder, contentType: this.contentType, onChange: this.handleChange, onImagePasted: this.handleImagePasted, onImageRemoved: this.handleImageRemoved, onMetadataChange: this.handleMetadataChange, customElements: this.customElements, value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId, "aria-disabled": this.disabled, "aria-invalid": this.invalid, "aria-required": this.required, language: this.language, triggerCharacters: this.triggers, disabled: this.disabled, ui: this.ui })); } renderPlaceholder() { if (!this.placeholder || this.value) { return; } return (h("span", { class: "placeholder", "aria-hidden": "true", slot: "content" }, this.placeholder)); } static get is() { return "limel-text-editor"; } static get encapsulation() { return "shadow"; } static get delegatesFocus() { return true; } static get originalStyleUrls() { return { "$": ["text-editor.scss"] }; } static get styleUrls() { return { "$": ["text-editor.css"] }; } static get properties() { return { "contentType": { "type": "string", "mutable": false, "complexType": { "original": "'markdown' | 'html'", "resolved": "\"html\" | \"markdown\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The type of content that the editor should handle and emit, defaults to `markdown`\n\nAssumed to be set only once, so not reactive to changes" }, "getter": false, "setter": false, "reflect": false, "attribute": "content-type", "defaultValue": "'markdown'" }, "language": { "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." }, "getter": false, "setter": false, "reflect": true, "attribute": "language", "defaultValue": "'en'" }, "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Set to `true` to disable the field.\nUse `disabled` to indicate that the field can normally be interacted\nwith, but is currently disabled. This tells the user that if certain\nrequirements are met, the field may become enabled again." }, "getter": false, "setter": false, "reflect": true, "attribute": "disabled", "defaultValue": "false" }, "readonly": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Set to `true` to make the component read-only.\nUse `readonly` when the field is only there to present the data it holds,\nand will not become possible for the current user to edit.\n:::note\nConsider that it might be better to use `limel-markdown`\ninstead of `limel-text-editor` when the goal is visualizing data.\n:::" }, "getter": false, "setter": false, "reflect": true, "attribute": "readonly", "defaultValue": "false" }, "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" }, "placeholder": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The placeholder text shown inside the input field,\nwhen the field is empty." }, "getter": false, "setter": false, "reflect": true, "attribute": "placeholder" }, "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The label of the editor" }, "getter": false, "setter": false, "reflect": true, "attribute": "label" }, "invalid": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Set to `true` to indicate that the current value of the editor is\ninvalid." }, "getter": false, "setter": false, "reflect": true, "attribute": "invalid", "defaultValue": "false" }, "value": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Description of the text inside the editor as markdown" }, "getter": false, "setter": false, "reflect": true, "attribute": "value" }, "customElements": { "type": "unknown", "mutable": false, "complexType": { "original": "CustomElementDefinition[]", "resolved": "CustomElementDefinition[]", "references": { "CustomElementDefinition": { "location": "import", "path": "../../global/shared-types/custom-element.types", "id": "src/global/shared-types/custom-element.types.ts::CustomElementDefinition", "referenceLocation": "CustomElementDefinition" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }], "text": "A list of custom elements\n\nAny `CustomElement` that should be used inside the text editor needs\nto be defined here." }, "getter": false, "setter": false, "defaultValue": "[]" }, "triggers": { "type": "unknown", "mutable": false, "complexType": { "original": "TriggerCharacter[]", "resolved": "TriggerCharacter[]", "references": { "TriggerCharacter": { "location": "import", "path": "./text-editor.types", "id": "src/components/text-editor/text-editor.types.ts::TriggerCharacter", "referenceLocation": "TriggerCharacter" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }], "text": "A set of trigger characters\n\nDefining a character here will enable trigger events to be sent if the\ncharacter is detected in the editor." }, "getter": false, "setter": false, "defaultValue": "[]" }, "required": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Set to `true` to indicate that the field is required.\n\n:::important\nAn empty but required field is not automatically considered invalid.\nYou must make sure to check the validity of the field on your own,\nand properly handle the `invalid` state.\n:::" }, "getter": false, "setter": false, "reflect": true, "attribute": "required", "defaultValue": "false" }, "allowResize": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to allow the user to vertically resize the editor.\nSet to `false` to disable the resize functionality." }, "getter": false, "setter": false, "reflect": true, "attribute": "allow-resize", "defaultValue": "true" }, "ui": { "type": "string", "mutable": false, "complexType": { "original": "EditorUiType", "resolved": "\"minimal\" | \"no-toolbar\" | \"standard\"", "references": { "EditorUiType": { "location": "import", "path": "./types", "id": "src/components/text-editor/types.ts::EditorUiType", "referenceLocation": "EditorUiType" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "Specifies the visual appearance of the editor.\n\n- `standard`: The default editor appearance with a full toolbar and\n standard layout.\n- `minimal`: A compact editor appearance, ideal for limited space\n scenarios such as mobile devices. In this mode, the toolbar is hidden\n until the editor is focused.\n- `no-toolbar`: A basic textarea appearance without any text styling toolbar.\n This mode is suitable for scenarios where you want to provide a simple\n text input without any visible formatting options; but still provide\n support for markdown syntax and rich text, using hotkeys or when pasting." }, "getter": false, "setter": false, "reflect": true, "attribute": "ui", "defaultValue": "'standard'" } }; } static get events() { return [{ "method": "change", "name": "change", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Dispatched when a change is made to the editor" }, "complexType": { "original": "string", "resolved": "string", "references": {} } }, { "method": "imagePasted", "name": "imagePasted", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }], "text": "Dispatched when a image is pasted into the editor" }, "complexType": { "original": "ImageInserter", "resolved": "ImageInserter", "references": { "ImageInserter": { "location": "import", "path": "./text-editor.types", "id": "src/components/text-editor/text-editor.types.ts::ImageInserter", "referenceLocation": "ImageInserter" } } } }, { "method": "imageRemoved", "name": "imageRemoved", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }, { "name": "deprecated", "text": "- This event is deprecated and will be removed in a future version.\nUse the `metadataChange` event instead to track image removals." }], "text": "Dispatched when a image is removed from the editor" }, "complexType": { "original": "EditorImage", "resolved": "EditorImage", "references": { "EditorImage": { "location": "import", "path": "./text-editor.types", "id": "src/components/text-editor/text-editor.types.ts::EditorImage", "referenceLocation": "EditorImage" } } } }, { "method": "metadataChange", "name": "metadataChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }], "text": "Dispatched when the metadata of the editor changes" }, "complexType": { "original": "EditorMetadata", "resolved": "EditorMetadata", "references": { "EditorMetadata": { "location": "import", "path": "./text-editor.types", "id": "src/components/text-editor/text-editor.types.ts::EditorMetadata", "referenceLocation": "EditorMetadata" } } } }, { "method": "triggerStart", "name": "triggerStart", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }], "text": "Dispatched if a trigger character is detected." }, "complexType": { "original": "TriggerEventDetail", "resolved": "TriggerEventDetail", "references": { "TriggerEventDetail": { "location": "import", "path": "./text-editor.types", "id": "src/components/text-editor/text-editor.types.ts::TriggerEventDetail", "referenceLocation": "TriggerEventDetail" } } } }, { "method": "triggerStop", "name": "triggerStop", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }], "text": "Dispatched if a trigger session is ended. That is if the selection\ngoes outside the trigger input or if something is inserted using the\nsupplied `TextEditor` insert function." }, "complexType": { "original": "TriggerEventDetail", "resolved": "TriggerEventDetail", "references": { "TriggerEventDetail": { "location": "import", "path": "./text-editor.types", "id": "src/components/text-editor/text-editor.types.ts::TriggerEventDetail", "referenceLocation": "TriggerEventDetail" } } } }, { "method": "triggerChange", "name": "triggerChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "private", "text": undefined }, { "name": "alpha", "text": undefined }], "text": "Dispatched if a input is changed during an active trigger." }, "complexType": { "original": "TriggerEventDetail", "resolved": "TriggerEventDetail", "references": { "TriggerEventDetail": { "location": "import", "path": "./text-editor.types", "id": "src/components/text-editor/text-editor.types.ts::TriggerEventDetail", "referenceLocation": "TriggerEventDetail" } } } }]; } }