UNPKG

@vaadin/rich-text-editor

Version:
505 lines (467 loc) 18.4 kB
/** * @license * Copyright (c) 2000 - 2026 Vaadin Ltd. * * This program is available under Vaadin Commercial License and Service Terms. * * * See https://vaadin.com/commercial-license-and-service-terms for the full * license. */ import '@vaadin/button/src/vaadin-button.js'; import '@vaadin/confirm-dialog/src/vaadin-confirm-dialog.js'; import '@vaadin/text-field/src/vaadin-text-field.js'; import '@vaadin/tooltip/src/vaadin-tooltip.js'; import './vaadin-rich-text-editor-popup.js'; import { html, LitElement, render } from 'lit'; import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js'; import { defineCustomElement } from '@vaadin/component-base/src/define.js'; import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js'; import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js'; import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; import { richTextEditorStyles } from './styles/vaadin-rich-text-editor-base-styles.js'; import { RichTextEditorMixin } from './vaadin-rich-text-editor-mixin.js'; /** * `<vaadin-rich-text-editor>` is a Web Component for rich text editing. * It provides a set of toolbar controls to apply formatting on the content, * which is stored and can be accessed as HTML5 or JSON string. * * ```html * <vaadin-rich-text-editor></vaadin-rich-text-editor> * ``` * * Vaadin Rich Text Editor focuses on the structure, not the styling of content. * Therefore, the semantic HTML5 tags such as `<h1>`, `<strong>` and `<ul>` are used, * and CSS usage is limited to most common cases, like horizontal text alignment. * * ### Styling * * The following state attributes are available for styling: * * Attribute | Description * -------------|------------------------------ * `disabled` | Set to a disabled text editor * `readonly` | Set to a readonly text editor * * The following shadow DOM parts are available for styling: * * Part name | Description * -------------------------------------|---------------- * `content` | The content wrapper * `toolbar` | The toolbar wrapper * `toolbar-group` | The group for toolbar controls * `toolbar-group-history` | The group for history controls * `toolbar-group-emphasis` | The group for emphasis controls * `toolbar-group-heading` | The group for heading controls * `toolbar-group-style` | The group for style controls * `toolbar-group-glyph-transformation` | The group for glyph transformation controls * `toolbar-group-list` | The group for list controls * `toolbar-group-indent` | The group for indentation controls * `toolbar-group-alignment` | The group for alignment controls * `toolbar-group-rich-text` | The group for rich text controls * `toolbar-group-block` | The group for preformatted block controls * `toolbar-group-format` | The group for format controls * `toolbar-button` | The toolbar button (applies to all buttons) * `toolbar-button-pressed` | The toolbar button in pressed state (applies to all buttons) * `toolbar-button-undo` | The "undo" button * `toolbar-button-redo` | The "redo" button * `toolbar-button-bold` | The "bold" button * `toolbar-button-italic` | The "italic" button * `toolbar-button-underline` | The "underline" button * `toolbar-button-strike` | The "strike-through" button * `toolbar-button-color` | The "color" button * `toolbar-button-background` | The "background" button * `toolbar-button-h1` | The "header 1" button * `toolbar-button-h2` | The "header 2" button * `toolbar-button-h3` | The "header 3" button * `toolbar-button-subscript` | The "subscript" button * `toolbar-button-superscript` | The "superscript" button * `toolbar-button-list-ordered` | The "ordered list" button * `toolbar-button-list-bullet` | The "bullet list" button * `toolbar-button-outdent` | The "decrease indentation" button * `toolbar-button-indent` | The "increase indentation" button * `toolbar-button-align-left` | The "left align" button * `toolbar-button-align-center` | The "center align" button * `toolbar-button-align-right` | The "right align" button * `toolbar-button-image` | The "image" button * `toolbar-button-link` | The "link" button * `toolbar-button-blockquote` | The "blockquote" button * `toolbar-button-code-block` | The "code block" button * `toolbar-button-clean` | The "clean formatting" button * * The following custom CSS properties are available for styling: * * Custom CSS property | * :-------------------------------| * `--vaadin-rich-text-editor-background` | * `--vaadin-rich-text-editor-content-color` | * `--vaadin-rich-text-editor-content-font-size` | * `--vaadin-rich-text-editor-content-line-height` | * `--vaadin-rich-text-editor-content-padding` | * `--vaadin-rich-text-editor-toolbar-background` | * `--vaadin-rich-text-editor-toolbar-gap` | * `--vaadin-rich-text-editor-toolbar-padding` | * `--vaadin-rich-text-editor-toolbar-button-background` | * `--vaadin-rich-text-editor-toolbar-button-border-width` | * `--vaadin-rich-text-editor-toolbar-button-border-color` | * `--vaadin-rich-text-editor-toolbar-button-border-radius` | * `--vaadin-rich-text-editor-toolbar-button-text-color` | * `--vaadin-rich-text-editor-toolbar-button-padding` | * * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation. * * @fires {Event} change - Fired when the user commits a value change. * @fires {CustomEvent} html-value-changed - Fired when the `htmlValue` property changes. * @fires {CustomEvent} value-changed - Fired when the `value` property changes. * * @customElement vaadin-rich-text-editor * @extends HTMLElement * @mixes ElementMixin * @mixes RichTextEditorMixin * @mixes ThemableMixin */ class RichTextEditor extends RichTextEditorMixin( ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement)))), ) { static get is() { return 'vaadin-rich-text-editor'; } static get cvdlName() { return 'vaadin-rich-text-editor'; } static get styles() { return richTextEditorStyles; } static get lumoInjector() { return { ...super.lumoInjector, includeBaseStyles: true }; } /** @protected */ render() { return html` <div class="vaadin-rich-text-editor-container"> <!-- Create toolbar container --> <div part="toolbar" role="toolbar"> <span part="toolbar-group toolbar-group-history"> <!-- Undo and Redo --> <button id="btn-undo" type="button" part="toolbar-button toolbar-button-undo" aria-label="${this.__effectiveI18n.undo}" @click="${this._undo}" ></button> <button id="btn-redo" type="button" part="toolbar-button toolbar-button-redo" aria-label="${this.__effectiveI18n.redo}" @click="${this._redo}" ></button> </span> <span part="toolbar-group toolbar-group-emphasis"> <!-- Bold --> <button id="btn-bold" class="ql-bold" part="toolbar-button toolbar-button-bold" aria-label="${this.__effectiveI18n.bold}" ></button> <!-- Italic --> <button id="btn-italic" class="ql-italic" part="toolbar-button toolbar-button-italic" aria-label="${this.__effectiveI18n.italic}" ></button> <!-- Underline --> <button id="btn-underline" class="ql-underline" part="toolbar-button toolbar-button-underline" aria-label="${this.__effectiveI18n.underline}" ></button> <!-- Strike --> <button id="btn-strike" class="ql-strike" part="toolbar-button toolbar-button-strike" aria-label="${this.__effectiveI18n.strike}" ></button> </span> <span part="toolbar-group toolbar-group-style"> <!-- Color --> <button id="btn-color" type="button" part="toolbar-button toolbar-button-color" aria-label="${this.__effectiveI18n.color}" @click="${this.__onColorClick}" ></button> <!-- Background --> <button id="btn-background" type="button" part="toolbar-button toolbar-button-background" aria-label="${this.__effectiveI18n.background}" @click="${this.__onBackgroundClick}" ></button> </span> <span part="toolbar-group toolbar-group-heading"> <!-- Header buttons --> <button id="btn-h1" type="button" class="ql-header" value="1" part="toolbar-button toolbar-button-h1" aria-label="${this.__effectiveI18n.h1}" ></button> <button id="btn-h2" type="button" class="ql-header" value="2" part="toolbar-button toolbar-button-h2" aria-label="${this.__effectiveI18n.h2}" ></button> <button id="btn-h3" type="button" class="ql-header" value="3" part="toolbar-button toolbar-button-h3" aria-label="${this.__effectiveI18n.h3}" ></button> </span> <span part="toolbar-group toolbar-group-glyph-transformation"> <!-- Subscript and superscript --> <button id="btn-subscript" class="ql-script" value="sub" part="toolbar-button toolbar-button-subscript" aria-label="${this.__effectiveI18n.subscript}" ></button> <button id="btn-superscript" class="ql-script" value="super" part="toolbar-button toolbar-button-superscript" aria-label="${this.__effectiveI18n.superscript}" ></button> </span> <span part="toolbar-group toolbar-group-list"> <!-- List buttons --> <button id="btn-ol" type="button" class="ql-list" value="ordered" part="toolbar-button toolbar-button-list-ordered" aria-label="${this.__effectiveI18n.listOrdered}" ></button> <button id="btn-ul" type="button" class="ql-list" value="bullet" part="toolbar-button toolbar-button-list-bullet" aria-label="${this.__effectiveI18n.listBullet}" ></button> </span> <span part="toolbar-group toolbar-group-indent"> <!-- Decrease --> <button id="btn-outdent" type="button" class="ql-indent" value="-1" part="toolbar-button toolbar-button-outdent" aria-label="${this.__effectiveI18n.outdent}" ></button> <!-- Increase --> <button id="btn-indent" type="button" class="ql-indent" value="+1" part="toolbar-button toolbar-button-indent" aria-label="${this.__effectiveI18n.indent}" ></button> </span> <span part="toolbar-group toolbar-group-alignment"> <!-- Align buttons --> <button id="btn-left" type="button" class="ql-align" value="" part="toolbar-button toolbar-button-align-left" aria-label="${this.__effectiveI18n.alignLeft}" ></button> <button id="btn-center" type="button" class="ql-align" value="center" part="toolbar-button toolbar-button-align-center" aria-label="${this.__effectiveI18n.alignCenter}" ></button> <button id="btn-right" type="button" class="ql-align" value="right" part="toolbar-button toolbar-button-align-right" aria-label="${this.__effectiveI18n.alignRight}" ></button> </span> <span part="toolbar-group toolbar-group-rich-text"> <!-- Image --> <button id="btn-image" type="button" part="toolbar-button toolbar-button-image" aria-label="${this.__effectiveI18n.image}" @touchend="${this._onImageTouchEnd}" @click="${this._onImageClick}" ></button> <!-- Link --> <button id="btn-link" type="button" part="toolbar-button toolbar-button-link" aria-label="${this.__effectiveI18n.link}" @click="${this._onLinkClick}" ></button> </span> <span part="toolbar-group toolbar-group-block"> <!-- Blockquote --> <button id="btn-blockquote" type="button" class="ql-blockquote" part="toolbar-button toolbar-button-blockquote" aria-label="${this.__effectiveI18n.blockquote}" ></button> <!-- Code block --> <button id="btn-code" type="button" class="ql-code-block" part="toolbar-button toolbar-button-code-block" aria-label="${this.__effectiveI18n.codeBlock}" ></button> </span> <span part="toolbar-group toolbar-group-format"> <!-- Clean --> <button id="btn-clean" type="button" class="ql-clean" part="toolbar-button toolbar-button-clean" aria-label="${this.__effectiveI18n.clean}" ></button> </span> <input id="fileInput" type="file" accept="image/png, image/gif, image/jpeg, image/bmp, image/x-icon" @change="${this._uploadImage}" /> </div> <div part="content"></div> <div class="announcer" aria-live="polite"></div> </div> <slot name="tooltip"></slot> <slot name="link-dialog"></slot> <slot name="color-popup"></slot> <slot name="background-popup"></slot> `; } /** * Override update to render slotted overlays into light DOM after rendering shadow DOM. * @param changedProperties * @protected */ update(changedProperties) { super.update(changedProperties); this.__renderSlottedOverlays(); } /** @private */ __renderSlottedOverlays() { render( html` <vaadin-confirm-dialog slot="link-dialog" cancel-button-visible reject-theme="error" .opened="${this._linkEditing}" .header="${this.__effectiveI18n.linkDialogTitle}" .confirmText="${this.__effectiveI18n.ok}" .rejectText="${this.__effectiveI18n.remove}" .cancelText="${this.__effectiveI18n.cancel}" .rejectButtonVisible="${!!this._linkRange}" @confirm="${this._onLinkEditConfirm}" @cancel="${this._onLinkEditCancel}" @reject="${this._onLinkEditRemove}" @opened-changed="${this._onLinkEditingChanged}" > <vaadin-text-field .value="${this._linkUrl}" style="width: 100%;" @keydown="${this._onLinkKeydown}" @value-changed="${this._onLinkUrlChanged}" ></vaadin-text-field> </vaadin-confirm-dialog> <vaadin-rich-text-editor-popup slot="color-popup" .colors="${['#000000', ...[...this.colorOptions].filter((c) => c !== '#000000')]}" .opened="${this._colorEditing}" @color-selected="${this.__onColorSelected}" @opened-changed="${this.__onColorEditingChanged}" ></vaadin-rich-text-editor-popup> <vaadin-rich-text-editor-popup slot="background-popup" .colors="${['#ffffff', ...[...this.colorOptions].filter((c) => c !== '#ffffff')]}" .opened="${this._backgroundEditing}" @color-selected="${this.__onBackgroundSelected}" @opened-changed="${this.__onBackgroundEditingChanged}" ></vaadin-rich-text-editor-popup> `, this, { host: this }, ); } /** @private */ __onBackgroundEditingChanged(event) { this._backgroundEditing = event.detail.value; } /** @private */ __onColorEditingChanged(event) { this._colorEditing = event.detail.value; } /** @private */ _onLinkEditingChanged(event) { // Autofocus the URL field when the dialog opens if (event.detail.value) { const confirmDialog = event.target; const urlField = confirmDialog.querySelector('vaadin-text-field'); confirmDialog.$.overlay.addEventListener( 'vaadin-overlay-open', () => { urlField.focus({ focusVisible: isKeyboardActive() }); }, { once: true }, ); } this._linkEditing = event.detail.value; } /** @private */ _onLinkUrlChanged(event) { this._linkUrl = event.detail.value; } } defineCustomElement(RichTextEditor); export { RichTextEditor };