Skip to content

Commit

Permalink
feat: add color and background selection to rich-text-editor (#7392)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored May 13, 2024
1 parent cb71069 commit 81125b3
Show file tree
Hide file tree
Showing 21 changed files with 635 additions and 3 deletions.
91 changes: 91 additions & 0 deletions packages/rich-text-editor/src/vaadin-lit-rich-text-editor-popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @license
* Copyright (c) 2000 - 2024 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 { css, html, LitElement } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
import { overlayStyles } from '@vaadin/overlay/src/vaadin-overlay-styles.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { RichTextEditorPopupMixin } from './vaadin-rich-text-editor-popup-mixin.js';

/**
* An element used internally by `<vaadin-rich-text-editor>`. Not intended to be used separately.
* @private
*/
class RichTextEditorPopup extends RichTextEditorPopupMixin(PolylitMixin(LitElement)) {
static get is() {
return 'vaadin-rich-text-editor-popup';
}

static get styles() {
return css`
:host {
display: none;
}
`;
}

/** @protected */
render() {
return html`
<vaadin-rich-text-editor-popup-overlay
.renderer="${this.renderer}"
.opened="${this.opened}"
.positionTarget="${this.target}"
no-vertical-overlap
horizontal-align="start"
vertical-align="top"
focus-trap
@opened-changed="${this._onOpenedChanged}"
@vaadin-overlay-escape-press="${this._onOverlayEscapePress}"
></vaadin-rich-text-editor-popup-overlay>
`;
}

/** @private */
_onOpenedChanged(event) {
this.opened = event.detail.value;
}
}

defineCustomElement(RichTextEditorPopup);

export { RichTextEditorPopup };

/**
* An element used internally by `<vaadin-rich-text-editor>`. Not intended to be used separately.
* @private
*/
class RichTextEditorPopupOverlay extends PositionMixin(
OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LitElement)))),
) {
static get is() {
return 'vaadin-rich-text-editor-popup-overlay';
}

static get styles() {
return overlayStyles;
}

/** @protected */
render() {
return html`
<div id="backdrop" part="backdrop" hidden></div>
<div part="overlay" id="overlay">
<div part="content" id="content"><slot></slot></div>
</div>
`;
}
}

defineCustomElement(RichTextEditorPopupOverlay);
46 changes: 46 additions & 0 deletions packages/rich-text-editor/src/vaadin-lit-rich-text-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '@vaadin/button/src/vaadin-lit-button.js';
import '@vaadin/confirm-dialog/src/vaadin-lit-confirm-dialog.js';
import '@vaadin/text-field/src/vaadin-lit-text-field.js';
import '@vaadin/tooltip/src/vaadin-lit-tooltip.js';
import './vaadin-lit-rich-text-editor-popup.js';
import { html, LitElement } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
Expand Down Expand Up @@ -84,6 +85,25 @@ class RichTextEditor extends RichTextEditorMixin(ElementMixin(ThemableMixin(Poly
<vaadin-tooltip for="btn-strike" .text="${this.i18n.strike}"></vaadin-tooltip>
</span>
<span part="toolbar-group toolbar-group-style">
<!-- Color -->
<button
id="btn-color"
type="button"
part="toolbar-button toolbar-button-color"
@click="${this.__onColorClick}"
></button>
<vaadin-tooltip for="btn-color" .text="${this.i18n.color}"></vaadin-tooltip>
<!-- Background -->
<button
id="btn-background"
type="button"
part="toolbar-button toolbar-button-background"
@click="${this.__onBackgroundClick}"
></button>
<vaadin-tooltip for="btn-background" .text="${this.i18n.background}"></vaadin-tooltip>
</span>
<span part="toolbar-group toolbar-group-heading">
<!-- Header buttons -->
<button
Expand Down Expand Up @@ -265,9 +285,35 @@ class RichTextEditor extends RichTextEditorMixin(ElementMixin(ThemableMixin(Poly
${this.i18n.cancel}
</vaadin-button>
</vaadin-confirm-dialog>
<vaadin-rich-text-editor-popup
id="colorPopup"
.colors="${this.colorOptions}"
.opened="${this._colorEditing}"
@color-selected="${this.__onColorSelected}"
@opened-changed="${this.__onColorEditingChanged}"
></vaadin-rich-text-editor-popup>
<vaadin-rich-text-editor-popup
id="backgroundPopup"
.colors="${this.colorOptions}"
.opened="${this._backgroundEditing}"
@color-selected="${this.__onBackgroundSelected}"
@opened-changed="${this.__onBackgroundEditingChanged}"
></vaadin-rich-text-editor-popup>
`;
}

/** @private */
__onBackgroundEditingChanged(event) {
this._backgroundEditing = event.detail.value;
}

/** @private */
__onColorEditingChanged(event) {
this._colorEditing = event.detail.value;
}

/** @private */
_onLinkEditingChanged(event) {
this._linkEditing = event.detail.value;
Expand Down
12 changes: 12 additions & 0 deletions packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface RichTextEditorI18n {
italic: string;
underline: string;
strike: string;
color: string;
background: string;
h1: string;
h2: string;
h3: string;
Expand Down Expand Up @@ -96,6 +98,16 @@ export declare class RichTextEditorMixinClass {
*/
i18n: RichTextEditorI18n;

/**
* The list of colors used by the background and text color
* selection controls. Should contain an array of HEX strings.
*
* When user selects `#000000` (black) as a text color,
* or `#ffffff` (white) as a background color, it resets
* the corresponding format for the selected text.
*/
colorOptions: string[];

/**
* Sets content represented by HTML snippet into the editor.
* The snippet is interpreted by [Quill's Clipboard matchers](https://quilljs.com/docs/modules/clipboard/#matchers),
Expand Down
90 changes: 90 additions & 0 deletions packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ export const RichTextEditorMixin = (superClass) =>
italic: 'italic',
underline: 'underline',
strike: 'strike',
color: 'color',
background: 'background',
h1: 'h1',
h2: 'h2',
h3: 'h3',
Expand All @@ -169,6 +171,28 @@ export const RichTextEditorMixin = (superClass) =>
},
},

/**
* The list of colors used by the background and text color
* selection controls. Should contain an array of HEX strings.
*
* When user selects `#000000` (black) as a text color,
* or `#ffffff` (white) as a background color, it resets
* the corresponding format for the selected text.
*/
colorOptions: {
type: Array,
value: () => {
/* prettier-ignore */
return [
'#000000', '#e60000', '#ff9900', '#ffff00', '#008a00', '#0066cc', '#9933ff',
'#ffffff', '#facccc', '#ffebcc', '#ffffcc', '#cce8cc', '#cce0f5', '#ebd6ff',
'#bbbbbb', '#f06666', '#ffc266', '#ffff66', '#66b966', '#66a3e0', '#c285ff',
'#888888', '#a10000', '#b26b00', '#b2b200', '#006100', '#0047b2', '#6b24b2',
'#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466'
];
},
},

/** @private */
_editor: {
type: Object,
Expand Down Expand Up @@ -210,6 +234,30 @@ export const RichTextEditorMixin = (superClass) =>
type: String,
value: '',
},

/** @private */
_colorEditing: {
type: Boolean,
value: false,
},

/** @private */
_colorValue: {
type: String,
value: '',
},

/** @private */
_backgroundEditing: {
type: Boolean,
value: false,
},

/** @private */
_backgroundValue: {
type: String,
value: '',
},
};
}

Expand Down Expand Up @@ -315,6 +363,15 @@ export const RichTextEditorMixin = (superClass) =>
});
});

this._editor.on('editor-change', () => {
const selection = this._editor.getSelection();
if (selection) {
const format = this._editor.getFormat(selection.index, selection.length);
this._toolbar.style.setProperty('--_color-value', format.color || null);
this._toolbar.style.setProperty('--_background-value', format.background || null);
}
});

const TAB_KEY = 9;

editorContent.addEventListener('keydown', (e) => {
Expand Down Expand Up @@ -367,6 +424,9 @@ export const RichTextEditorMixin = (superClass) =>

this._addToolbarListeners();

this.$.backgroundPopup.target = this.shadowRoot.querySelector('#btn-background');
this.$.colorPopup.target = this.shadowRoot.querySelector('#btn-color');

requestAnimationFrame(() => {
this.$.linkDialog.$.dialog.$.overlay.addEventListener('vaadin-overlay-open', () => {
this.$.linkUrl.focus();
Expand Down Expand Up @@ -668,6 +728,36 @@ export const RichTextEditorMixin = (superClass) =>
}
}

/** @private */
__onColorClick() {
this._colorEditing = true;
}

/** @private */
__onColorSelected(event) {
const color = event.detail.color;
this._colorValue = color === '#000000' ? null : color;
this._markToolbarClicked();
this._editor.format('color', this._colorValue, SOURCE.USER);
this._toolbar.style.setProperty('--_color-value', this._colorValue);
this._colorEditing = false;
}

/** @private */
__onBackgroundClick() {
this._backgroundEditing = true;
}

/** @private */
__onBackgroundSelected(event) {
const color = event.detail.color;
this._backgroundValue = color === '#ffffff' ? null : color;
this._markToolbarClicked();
this._editor.format('background', this._backgroundValue, SOURCE.USER);
this._toolbar.style.setProperty('--_background-value', this._backgroundValue);
this._backgroundEditing = false;
}

/** @private */
__updateHtmlValue() {
const editor = this.shadowRoot.querySelector('.ql-editor');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @license
* Copyright (c) 2000 - 2024 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.
*/

/**
* @polymerMixin
*/
export const RichTextEditorPopupMixin = (superClass) =>
class RichTextEditorPopupMixinClass extends superClass {
static get properties() {
return {
target: {
type: Object,
},

opened: {
type: Boolean,
notify: true,
},

colors: {
type: Array,
},

renderer: {
type: Object,
},
};
}

static get observers() {
return ['__openedOrTargetChanged(opened, target)', '__colorsChanged(colors)'];
}

/** @private */
__colorsChanged(colors) {
this.renderer = (root) => {
if (!root.firstChild) {
colors.forEach((color) => {
const btn = document.createElement('button');
btn.style.background = color;
btn.dataset.color = color;
btn.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('color-selected', { detail: { color } }));
});
root.appendChild(btn);
});
}
};
}

/** @private */
__openedOrTargetChanged(opened, target) {
if (target) {
target.setAttribute('aria-expanded', opened ? 'true' : 'false');
}
}

/** @protected */
_onOverlayEscapePress() {
this.target.focus();
}
};
Loading

0 comments on commit 81125b3

Please sign in to comment.