@vaadin/rich-text-editor
Version:
vaadin-rich-text-editor
505 lines (467 loc) • 18.4 kB
JavaScript
/**
* @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 };