-import {EditorButtonDefinition} from "../framework/buttons";
+import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../framework/buttons";
import {
$createNodeSelection,
$createParagraphNode, $getRoot, $getSelection, $insertNodes,
$isParagraphNode, $isTextNode, $setSelection,
BaseSelection, ElementNode, FORMAT_TEXT_COMMAND,
LexicalNode,
- REDO_COMMAND, TextFormatType,
+ REDO_COMMAND, TextFormatType, TextNode,
UNDO_COMMAND
} from "lexical";
import {
export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold');
export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic');
export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline');
-// Todo - Text color
-// Todo - Highlight color
+export const textColor: EditorBasicButtonDefinition = {label: 'Text color'};
+export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color'};
+
export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough');
export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript');
export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript');
isActive() {
return false;
}
-};
+};
\ No newline at end of file
--- /dev/null
+import {el} from "../../../helpers";
+import {EditorUiElement} from "../core";
+import {$getSelection} from "lexical";
+import {$patchStyleText} from "@lexical/selection";
+
+const colorChoices = [
+ '#000000',
+ '#ffffff',
+
+ '#BFEDD2',
+ '#FBEEB8',
+ '#F8CAC6',
+ '#ECCAFA',
+ '#C2E0F4',
+
+ '#2DC26B',
+ '#F1C40F',
+ '#E03E2D',
+ '#B96AD9',
+ '#3598DB',
+
+ '#169179',
+ '#E67E23',
+ '#BA372A',
+ '#843FA1',
+ '#236FA1',
+
+ '#ECF0F1',
+ '#CED4D9',
+ '#95A5A6',
+ '#7E8C8D',
+ '#34495E',
+];
+
+export class EditorColorPicker extends EditorUiElement {
+
+ protected styleProperty: string;
+
+ constructor(styleProperty: string) {
+ super();
+ this.styleProperty = styleProperty;
+ }
+
+ buildDOM(): HTMLElement {
+
+ const colorOptions = colorChoices.map(choice => {
+ return el('div', {
+ class: 'editor-color-select-option',
+ style: `background-color: ${choice}`,
+ 'data-color': choice,
+ 'aria-label': choice,
+ });
+ });
+
+ colorOptions.push(el('div', {
+ class: 'editor-color-select-option',
+ 'data-color': '',
+ title: 'Clear color',
+ }, ['x']));
+
+ const colorRows = [];
+ for (let i = 0; i < colorOptions.length; i+=5) {
+ const options = colorOptions.slice(i, i + 5);
+ colorRows.push(el('div', {
+ class: 'editor-color-select-row',
+ }, options));
+ }
+
+ const wrapper = el('div', {
+ class: 'editor-color-select',
+ }, colorRows);
+
+ wrapper.addEventListener('click', this.onClick.bind(this));
+
+ return wrapper;
+ }
+
+ onClick(event: MouseEvent) {
+ const colorEl = (event.target as HTMLElement).closest('[data-color]') as HTMLElement;
+ if (!colorEl) return;
+
+ const color = colorEl.dataset.color as string;
+ this.getContext().editor.update(() => {
+ const selection = $getSelection();
+ if (selection) {
+ $patchStyleText(selection, {[this.styleProperty]: color || null});
+ }
+ });
+ }
+}
\ No newline at end of file
--- /dev/null
+import {el} from "../../../helpers";
+import {handleDropdown} from "../helpers/dropdowns";
+import {EditorContainerUiElement, EditorUiElement} from "../core";
+import {EditorBasicButtonDefinition, EditorButton} from "../buttons";
+
+export class EditorDropdownButton extends EditorContainerUiElement {
+ protected button: EditorButton;
+ protected childItems: EditorUiElement[];
+ protected open: boolean = false;
+
+ constructor(buttonDefinition: EditorBasicButtonDefinition, children: EditorUiElement[]) {
+ super(children);
+ this.childItems = children
+
+ this.button = new EditorButton({
+ ...buttonDefinition,
+ action() {
+ return false;
+ },
+ isActive: () => {
+ return this.open;
+ }
+ });
+
+ this.children.push(this.button);
+ }
+
+ protected buildDOM(): HTMLElement {
+ const button = this.button.getDOMElement();
+
+ const childElements: HTMLElement[] = this.childItems.map(child => child.getDOMElement());
+ const menu = el('div', {
+ class: 'editor-dropdown-menu',
+ hidden: 'true',
+ }, childElements);
+
+ const wrapper = el('div', {
+ class: 'editor-dropdown-menu-container',
+ }, [button, menu]);
+
+ handleDropdown(button, menu, () => {
+ this.open = true;
+ this.getContext().manager.triggerStateUpdate(this.button);
+ }, () => {
+ this.open = false;
+ this.getContext().manager.triggerStateUpdate(this.button);
+ });
+
+ return wrapper;
+ }
+}
\ No newline at end of file
--- /dev/null
+import {el} from "../../../helpers";
+import {EditorUiStateUpdate, EditorContainerUiElement} from "../core";
+import {EditorButton} from "../buttons";
+import {handleDropdown} from "../helpers/dropdowns";
+
+export class EditorFormatMenu extends EditorContainerUiElement {
+ buildDOM(): HTMLElement {
+ const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());
+ const menu = el('div', {
+ class: 'editor-format-menu-dropdown editor-dropdown-menu editor-menu-list',
+ hidden: 'true',
+ }, childElements);
+
+ const toggle = el('button', {
+ class: 'editor-format-menu-toggle editor-button',
+ type: 'button',
+ }, [this.trans('Formats')]);
+
+ const wrapper = el('div', {
+ class: 'editor-format-menu editor-dropdown-menu-container',
+ }, [toggle, menu]);
+
+ handleDropdown(toggle, menu);
+
+ return wrapper;
+ }
+
+ updateState(state: EditorUiStateUpdate) {
+ super.updateState(state);
+
+ for (const child of this.children) {
+ if (child instanceof EditorButton && child.isActive()) {
+ this.updateToggleLabel(child.getLabel());
+ return;
+ }
+ }
+
+ this.updateToggleLabel(this.trans('Formats'));
+ }
+
+ protected updateToggleLabel(text: string): void {
+ const button = this.getDOMElement().querySelector('button');
+ if (button) {
+ button.innerText = text;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+import {el} from "../../../helpers";
+import {EditorButton, EditorButtonDefinition} from "../buttons";
+
+export class FormatPreviewButton extends EditorButton {
+ protected previewSampleElement: HTMLElement;
+
+ constructor(previewSampleElement: HTMLElement,definition: EditorButtonDefinition) {
+ super(definition);
+ this.previewSampleElement = previewSampleElement;
+ }
+
+ protected buildDOM(): HTMLButtonElement {
+ const button = super.buildDOM();
+ button.innerHTML = '';
+
+ const preview = el('span', {
+ class: 'editor-button-format-preview'
+ }, [this.getLabel()]);
+
+ const stylesToApply = this.getStylesFromPreview();
+ for (const style of Object.keys(stylesToApply)) {
+ preview.style.setProperty(style, stylesToApply[style]);
+ }
+
+ button.append(preview);
+ return button;
+ }
+
+ protected getStylesFromPreview(): Record<string, string> {
+ const wrap = el('div', {style: 'display: none', hidden: 'true', class: 'page-content'});
+ const sampleClone = this.previewSampleElement.cloneNode() as HTMLElement;
+ sampleClone.textContent = this.getLabel();
+ wrap.append(sampleClone);
+ document.body.append(wrap);
+
+ const propertiesToFetch = ['color', 'font-size', 'background-color', 'border-inline-start'];
+ const propertiesToReturn: Record<string, string> = {};
+
+ const computed = window.getComputedStyle(sampleClone);
+ for (const property of propertiesToFetch) {
+ propertiesToReturn[property] = computed.getPropertyValue(property);
+ }
+ wrap.remove();
+
+ return propertiesToReturn;
+ }
+}
\ No newline at end of file
import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
import {el} from "../../helpers";
-export interface EditorButtonDefinition {
+export interface EditorBasicButtonDefinition {
label: string;
+}
+
+export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
action: (context: EditorUiContext) => void;
isActive: (selection: BaseSelection|null) => boolean;
}
return this.trans(this.definition.label);
}
}
-
-export class FormatPreviewButton extends EditorButton {
- protected previewSampleElement: HTMLElement;
-
- constructor(previewSampleElement: HTMLElement,definition: EditorButtonDefinition) {
- super(definition);
- this.previewSampleElement = previewSampleElement;
- }
-
- protected buildDOM(): HTMLButtonElement {
- const button = super.buildDOM();
- button.innerHTML = '';
-
- const preview = el('span', {
- class: 'editor-button-format-preview'
- }, [this.getLabel()]);
-
- const stylesToApply = this.getStylesFromPreview();
- for (const style of Object.keys(stylesToApply)) {
- preview.style.setProperty(style, stylesToApply[style]);
- }
-
- button.append(preview);
- return button;
- }
-
- protected getStylesFromPreview(): Record<string, string> {
- const wrap = el('div', {style: 'display: none', hidden: 'true', class: 'page-content'});
- const sampleClone = this.previewSampleElement.cloneNode() as HTMLElement;
- sampleClone.textContent = this.getLabel();
- wrap.append(sampleClone);
- document.body.append(wrap);
-
- const propertiesToFetch = ['color', 'font-size', 'background-color', 'border-inline-start'];
- const propertiesToReturn: Record<string, string> = {};
-
- const computed = window.getComputedStyle(sampleClone);
- for (const property of propertiesToFetch) {
- propertiesToReturn[property] = computed.getPropertyValue(property);
- }
- wrap.remove();
-
- return propertiesToReturn;
- }
-}
\ No newline at end of file
+++ /dev/null
-import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
-import {el} from "../../helpers";
-import {EditorButton} from "./buttons";
-
-export class EditorContainerUiElement extends EditorUiElement {
- protected children : EditorUiElement[];
-
- constructor(children: EditorUiElement[]) {
- super();
- this.children = children;
- }
-
- protected buildDOM(): HTMLElement {
- return el('div', {}, this.getChildren().map(child => child.getDOMElement()));
- }
-
- getChildren(): EditorUiElement[] {
- return this.children;
- }
-
- updateState(state: EditorUiStateUpdate): void {
- for (const child of this.children) {
- child.updateState(state);
- }
- }
-
- setContext(context: EditorUiContext) {
- super.setContext(context);
- for (const child of this.getChildren()) {
- child.setContext(context);
- }
- }
-}
-
-export class EditorSimpleClassContainer extends EditorContainerUiElement {
- protected className;
-
- constructor(className: string, children: EditorUiElement[]) {
- super(children);
- this.className = className;
- }
-
- protected buildDOM(): HTMLElement {
- return el('div', {
- class: this.className,
- }, this.getChildren().map(child => child.getDOMElement()));
- }
-}
-
-export class EditorFormatMenu extends EditorContainerUiElement {
- buildDOM(): HTMLElement {
- const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());
- const menu = el('div', {
- class: 'editor-format-menu-dropdown editor-dropdown-menu editor-menu-list',
- hidden: 'true',
- }, childElements);
-
- const toggle = el('button', {
- class: 'editor-format-menu-toggle editor-button',
- type: 'button',
- }, [this.trans('Formats')]);
-
- const wrapper = el('div', {
- class: 'editor-format-menu editor-dropdown-menu-container',
- }, [toggle, menu]);
-
- let clickListener: Function|null = null;
-
- const hide = () => {
- menu.hidden = true;
- if (clickListener) {
- window.removeEventListener('click', clickListener as EventListener);
- }
- };
-
- const show = () => {
- menu.hidden = false
- clickListener = (event: MouseEvent) => {
- if (!wrapper.contains(event.target as HTMLElement)) {
- hide();
- }
- }
- window.addEventListener('click', clickListener as EventListener);
- };
-
- toggle.addEventListener('click', event => {
- menu.hasAttribute('hidden') ? show() : hide();
- });
- menu.addEventListener('mouseleave', hide);
-
- return wrapper;
- }
-
- updateState(state: EditorUiStateUpdate) {
- super.updateState(state);
-
- for (const child of this.children) {
- if (child instanceof EditorButton && child.isActive()) {
- this.updateToggleLabel(child.getLabel());
- return;
- }
- }
-
- this.updateToggleLabel(this.trans('Formats'));
- }
-
- protected updateToggleLabel(text: string): void {
- const button = this.getDOMElement().querySelector('button');
- if (button) {
- button.innerText = text;
- }
- }
-}
\ No newline at end of file
import {BaseSelection, LexicalEditor} from "lexical";
import {EditorUIManager} from "./manager";
+import {el} from "../../helpers";
export type EditorUiStateUpdate = {
editor: LexicalEditor,
updateState(state: EditorUiStateUpdate): void {
return;
}
-}
\ No newline at end of file
+}
+
+export class EditorContainerUiElement extends EditorUiElement {
+ protected children : EditorUiElement[];
+
+ constructor(children: EditorUiElement[]) {
+ super();
+ this.children = children;
+ }
+
+ protected buildDOM(): HTMLElement {
+ return el('div', {}, this.getChildren().map(child => child.getDOMElement()));
+ }
+
+ getChildren(): EditorUiElement[] {
+ return this.children;
+ }
+
+ updateState(state: EditorUiStateUpdate): void {
+ for (const child of this.children) {
+ child.updateState(state);
+ }
+ }
+
+ setContext(context: EditorUiContext) {
+ super.setContext(context);
+ for (const child of this.getChildren()) {
+ child.setContext(context);
+ }
+ }
+}
+
+export class EditorSimpleClassContainer extends EditorContainerUiElement {
+ protected className;
+
+ constructor(className: string, children: EditorUiElement[]) {
+ super(children);
+ this.className = className;
+ }
+
+ protected buildDOM(): HTMLElement {
+ return el('div', {
+ class: this.className,
+ }, this.getChildren().map(child => child.getDOMElement()));
+ }
+}
+
-import {EditorUiContext, EditorUiElement} from "./core";
-import {EditorContainerUiElement} from "./containers";
+import {EditorUiContext, EditorUiElement, EditorContainerUiElement} from "./core";
import {el} from "../../helpers";
export interface EditorFormFieldDefinition {
--- /dev/null
+
+
+
+export function handleDropdown(toggle: HTMLElement, menu: HTMLElement, onOpen: Function|undefined = undefined, onClose: Function|undefined = undefined) {
+ let clickListener: Function|null = null;
+
+ const hide = () => {
+ menu.hidden = true;
+ if (clickListener) {
+ window.removeEventListener('click', clickListener as EventListener);
+ }
+ if (onClose) {
+ onClose();
+ }
+ };
+
+ const show = () => {
+ menu.hidden = false
+ clickListener = (event: MouseEvent) => {
+ if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) {
+ hide();
+ }
+ }
+ window.addEventListener('click', clickListener as EventListener);
+ if (onOpen) {
+ onOpen();
+ }
+ };
+
+ toggle.addEventListener('click', event => {
+ menu.hasAttribute('hidden') ? show() : hide();
+ });
+ menu.addEventListener('mouseleave', hide);
+}
\ No newline at end of file
import {EditorFormModal, EditorFormModalDefinition} from "./modals";
-import {EditorUiContext} from "./core";
+import {EditorUiContext, EditorUiElement} from "./core";
import {EditorDecorator} from "./decorator";
return this.context;
}
+ triggerStateUpdate(element: EditorUiElement) {
+ element.updateState({
+ selection: null,
+ editor: this.getContext().editor
+ });
+ }
+
registerModal(key: string, modalDefinition: EditorFormModalDefinition) {
this.modalDefinitionsByKey[key] = modalDefinition;
}
import {EditorForm, EditorFormDefinition} from "./forms";
import {el} from "../../helpers";
-import {EditorContainerUiElement} from "./containers";
+import {EditorContainerUiElement} from "./core";
export interface EditorModalDefinition {
-import {EditorButton, FormatPreviewButton} from "./framework/buttons";
+import {EditorButton} from "./framework/buttons";
import {
blockquote, bold, clearFormating, code,
dangerCallout, details,
- h2, h3, h4, h5, image,
+ h2, h3, h4, h5, highlightColor, image,
infoCallout, italic, link, paragraph,
redo, source, strikethrough, subscript,
- successCallout, superscript, underline,
+ successCallout, superscript, textColor, underline,
undo,
warningCallout
} from "./defaults/button-definitions";
-import {EditorContainerUiElement, EditorFormatMenu, EditorSimpleClassContainer} from "./framework/containers";
+import {EditorContainerUiElement, EditorSimpleClassContainer} from "./framework/core";
import {el} from "../helpers";
+import {EditorFormatMenu} from "./framework/blocks/format-menu";
+import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
+import {EditorDropdownButton} from "./framework/blocks/dropdown-button";
+import {EditorColorPicker} from "./framework/blocks/color-picker";
export function getMainEditorFullToolbar(): EditorContainerUiElement {
new EditorButton(bold),
new EditorButton(italic),
new EditorButton(underline),
+ new EditorDropdownButton(textColor, [
+ new EditorColorPicker('color'),
+ ]),
+ new EditorDropdownButton(highlightColor, [
+ new EditorColorPicker('background-color'),
+ ]),
new EditorButton(strikethrough),
new EditorButton(superscript),
new EditorButton(subscript),
font-weight: 700;
}
+// Specific UI elements
+.editor-color-select-row {
+ display: flex;
+}
+.editor-color-select-option {
+ width: 28px;
+ height: 28px;
+ cursor: pointer;
+}
+.editor-color-select-option:hover {
+ border-radius: 3px;
+ box-sizing: border-box;
+ z-index: 3;
+ box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.25);
+}
+
// In-editor elements
.editor-image-wrap {
position: relative;