From 04cca77ae6d84e0f7c3aceef6c0bc3682258c5c9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 18 Jan 2025 11:12:43 +0000 Subject: [PATCH] Lexical: Added color picker/indicator to form fields --- resources/icons/editor/color-display.svg | 10 ++++ resources/js/wysiwyg/todo.md | 4 -- .../ui/defaults/buttons/inline-formats.ts | 14 +++++ .../js/wysiwyg/ui/defaults/forms/tables.ts | 21 +++---- resources/js/wysiwyg/ui/defaults/toolbars.ts | 8 +-- .../ui/framework/blocks/color-field.ts | 56 +++++++++++++++++++ .../ui/framework/blocks/color-picker.ts | 19 +++---- .../wysiwyg/ui/framework/blocks/link-field.ts | 2 - resources/js/wysiwyg/ui/framework/forms.ts | 17 ++++-- resources/sass/_editor.scss | 10 ++++ 10 files changed, 125 insertions(+), 36 deletions(-) create mode 100644 resources/icons/editor/color-display.svg create mode 100644 resources/js/wysiwyg/ui/framework/blocks/color-field.ts diff --git a/resources/icons/editor/color-display.svg b/resources/icons/editor/color-display.svg new file mode 100644 index 000000000..86be9a7bf --- /dev/null +++ b/resources/icons/editor/color-display.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index a49cccd26..695e8cb69 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -10,13 +10,9 @@ ## Secondary Todo -- Color picker support in table form color fields -- Color picker for color controls - Table caption text support - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts) - Deep check of translation coverage -- About button & view -- Mobile display and handling ## Bugs diff --git a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts index c3726acf0..c5b7ad29a 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts @@ -12,6 +12,8 @@ import subscriptIcon from "@icons/editor/subscript.svg"; import codeIcon from "@icons/editor/code.svg"; import formatClearIcon from "@icons/editor/format-clear.svg"; import {$selectionContainsTextFormat} from "../../../utils/selection"; +import {$patchStyleText} from "@lexical/selection"; +import {context} from "esbuild"; function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition { return { @@ -32,6 +34,18 @@ export const underline: EditorButtonDefinition = buildFormatButton('Underline', export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon}; export const highlightColor: EditorBasicButtonDefinition = {label: 'Background color', icon: highlightIcon}; +function colorAction(context: EditorUiContext, property: string, color: string): void { + context.editor.update(() => { + const selection = $getSelection(); + if (selection) { + $patchStyleText(selection, {[property]: color || null}); + } + }); +} + +export const textColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'color', color); +export const highlightColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'color', color); + export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon); export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon); export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon); diff --git a/resources/js/wysiwyg/ui/defaults/forms/tables.ts b/resources/js/wysiwyg/ui/defaults/forms/tables.ts index 63fa24c80..b592d7c67 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/tables.ts @@ -1,6 +1,6 @@ import { EditorFormDefinition, - EditorFormFieldDefinition, + EditorFormFieldDefinition, EditorFormFields, EditorFormTabs, EditorSelectFormFieldDefinition } from "../../framework/forms"; @@ -17,6 +17,7 @@ import { import {formatSizeValue} from "../../../utils/dom"; import {TableCellNode, TableNode, TableRowNode} from "@lexical/table"; import {CommonBlockAlignment} from "lexical/nodes/common"; +import {colorFieldBuilder} from "../../framework/blocks/color-field"; const borderStyleInput: EditorSelectFormFieldDefinition = { label: 'Border style', @@ -145,15 +146,15 @@ export const cellProperties: EditorFormDefinition = { } as EditorSelectFormFieldDefinition, ]; - const advancedFields: EditorFormFieldDefinition[] = [ + const advancedFields: EditorFormFields = [ { label: 'Border width', // inline-style: border-width name: 'border_width', type: 'text', }, borderStyleInput, // inline-style: border-style - borderColorInput, // inline-style: border-color - backgroundColorInput, // inline-style: background-color + colorFieldBuilder(borderColorInput), + colorFieldBuilder(backgroundColorInput), ]; return new EditorFormTabs([ @@ -210,8 +211,8 @@ export const rowProperties: EditorFormDefinition = { type: 'text', }, borderStyleInput, // style on tr: height - borderColorInput, // style on tr: height - backgroundColorInput, // style on tr: height + colorFieldBuilder(borderColorInput), + colorFieldBuilder(backgroundColorInput), ], }; @@ -305,10 +306,10 @@ export const tableProperties: EditorFormDefinition = { alignmentInput, // alignment class ]; - const advancedFields: EditorFormFieldDefinition[] = [ - borderStyleInput, // Style - border-style - borderColorInput, // Style - border-color - backgroundColorInput, // Style - background-color + const advancedFields: EditorFormFields = [ + borderStyleInput, + colorFieldBuilder(borderColorInput), + colorFieldBuilder(backgroundColorInput), ]; return new EditorFormTabs([ diff --git a/resources/js/wysiwyg/ui/defaults/toolbars.ts b/resources/js/wysiwyg/ui/defaults/toolbars.ts index 61baa3c32..b09a7530f 100644 --- a/resources/js/wysiwyg/ui/defaults/toolbars.ts +++ b/resources/js/wysiwyg/ui/defaults/toolbars.ts @@ -44,11 +44,11 @@ import { } from "./buttons/block-formats"; import { bold, clearFormating, code, - highlightColor, + highlightColor, highlightColorAction, italic, strikethrough, subscript, superscript, - textColor, + textColor, textColorAction, underline } from "./buttons/inline-formats"; import { @@ -114,10 +114,10 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai new EditorButton(italic), new EditorButton(underline), new EditorDropdownButton({ button: new EditorColorButton(textColor, 'color') }, [ - new EditorColorPicker('color'), + new EditorColorPicker(textColorAction), ]), new EditorDropdownButton({button: new EditorColorButton(highlightColor, 'background-color')}, [ - new EditorColorPicker('background-color'), + new EditorColorPicker(highlightColorAction), ]), new EditorButton(strikethrough), new EditorButton(superscript), diff --git a/resources/js/wysiwyg/ui/framework/blocks/color-field.ts b/resources/js/wysiwyg/ui/framework/blocks/color-field.ts new file mode 100644 index 000000000..8c8f167d9 --- /dev/null +++ b/resources/js/wysiwyg/ui/framework/blocks/color-field.ts @@ -0,0 +1,56 @@ +import {EditorContainerUiElement, EditorUiBuilderDefinition, EditorUiContext} from "../core"; +import {EditorFormField, EditorFormFieldDefinition} from "../forms"; +import {EditorColorPicker} from "./color-picker"; +import {EditorDropdownButton} from "./dropdown-button"; + +import colorDisplayIcon from "@icons/editor/color-display.svg" + +export class EditorColorField extends EditorContainerUiElement { + protected input: EditorFormField; + protected pickerButton: EditorDropdownButton; + + constructor(input: EditorFormField) { + super([]); + + this.input = input; + + this.pickerButton = new EditorDropdownButton({ + button: { icon: colorDisplayIcon, label: 'Select color'} + }, [ + new EditorColorPicker(this.onColorSelect.bind(this)) + ]); + this.addChildren(this.pickerButton, this.input); + } + + protected buildDOM(): HTMLElement { + const dom = this.input.getDOMElement(); + dom.append(this.pickerButton.getDOMElement()); + dom.classList.add('editor-color-field-container'); + + const field = dom.querySelector('input') as HTMLInputElement; + field.addEventListener('change', () => { + this.setIconColor(field.value); + }); + + return dom; + } + + onColorSelect(color: string, context: EditorUiContext): void { + this.input.setValue(color); + } + + setIconColor(color: string) { + const icon = this.getDOMElement().querySelector('svg .editor-icon-color-display'); + if (icon) { + icon.setAttribute('fill', color || 'url(#pattern2)'); + } + } +} + +export function colorFieldBuilder(field: EditorFormFieldDefinition): EditorUiBuilderDefinition { + return { + build() { + return new EditorColorField(new EditorFormField(field)); + } + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts b/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts index 65623e1b2..c742ddc77 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts @@ -1,6 +1,4 @@ -import {EditorUiElement} from "../core"; -import {$getSelection} from "lexical"; -import {$patchStyleText} from "@lexical/selection"; +import {EditorUiContext, EditorUiElement} from "../core"; import {el} from "../../../utils/dom"; import removeIcon from "@icons/editor/color-clear.svg"; @@ -38,13 +36,15 @@ const colorChoices = [ const storageKey = 'bs-lexical-custom-colors'; +export type EditorColorPickerCallback = (color: string, context: EditorUiContext) => void; + export class EditorColorPicker extends EditorUiElement { - protected styleProperty: string; + protected callback: EditorColorPickerCallback; - constructor(styleProperty: string) { + constructor(callback: EditorColorPickerCallback) { super(); - this.styleProperty = styleProperty; + this.callback = callback; } buildDOM(): HTMLElement { @@ -131,11 +131,6 @@ export class EditorColorPicker extends EditorUiElement { } setColor(color: string) { - this.getContext().editor.update(() => { - const selection = $getSelection(); - if (selection) { - $patchStyleText(selection, {[this.styleProperty]: color || null}); - } - }); + this.callback(color, this.getContext()); } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/blocks/link-field.ts b/resources/js/wysiwyg/ui/framework/blocks/link-field.ts index f88b22c3f..880238a9a 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/link-field.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/link-field.ts @@ -44,7 +44,6 @@ export class LinkField extends EditorContainerUiElement { updateFormFromHeader(header: HeadingNode) { this.getHeaderIdAndText(header).then(({id, text}) => { - console.log('updating form', id, text); const modal = this.getContext().manager.getActiveModal('link'); if (modal) { modal.getForm().setValues({ @@ -60,7 +59,6 @@ export class LinkField extends EditorContainerUiElement { return new Promise((res) => { this.getContext().editor.update(() => { let id = header.getId(); - console.log('header', id, header.__id); if (!id) { id = 'header-' + uniqueIdSmall(); header.setId(id); diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts index 36371e302..771ab0bdf 100644 --- a/resources/js/wysiwyg/ui/framework/forms.ts +++ b/resources/js/wysiwyg/ui/framework/forms.ts @@ -19,15 +19,17 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti valuesByLabel: Record } +export type EditorFormFields = (EditorFormFieldDefinition|EditorUiBuilderDefinition)[]; + interface EditorFormTabDefinition { label: string; - contents: EditorFormFieldDefinition[]; + contents: EditorFormFields; } export interface EditorFormDefinition { submitText: string; action: (formData: FormData, context: EditorUiContext) => Promise; - fields: (EditorFormFieldDefinition|EditorUiBuilderDefinition)[]; + fields: EditorFormFields; } export class EditorFormField extends EditorUiElement { @@ -41,6 +43,7 @@ export class EditorFormField extends EditorUiElement { setValue(value: string) { const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement; input.value = value; + input.dispatchEvent(new Event('change')); } getName(): string { @@ -155,11 +158,17 @@ export class EditorForm extends EditorContainerUiElement { export class EditorFormTab extends EditorContainerUiElement { protected definition: EditorFormTabDefinition; - protected fields: EditorFormField[]; + protected fields: EditorUiElement[]; protected id: string; constructor(definition: EditorFormTabDefinition) { - const fields = definition.contents.map(fieldDef => new EditorFormField(fieldDef)); + const fields = definition.contents.map(fieldDef => { + if (isUiBuilderDefinition(fieldDef)) { + return fieldDef.build(); + } + return new EditorFormField(fieldDef) + }); + super(fields); this.definition = definition; diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 2446c1416..9f7694e85 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -649,6 +649,16 @@ textarea.editor-form-field-input { width: $inputWidth - 40px; } } +.editor-color-field-container { + position: relative; + input { + padding-left: 36px; + } + .editor-dropdown-menu-container { + position: absolute; + bottom: 0; + } +} // Editor theme styles .editor-theme-bold {