]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Added color picker/indicator to form fields
authorDan Brown <redacted>
Sat, 18 Jan 2025 11:12:43 +0000 (11:12 +0000)
committerDan Brown <redacted>
Sat, 18 Jan 2025 11:12:43 +0000 (11:12 +0000)
resources/icons/editor/color-display.svg [new file with mode: 0644]
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
resources/js/wysiwyg/ui/defaults/forms/tables.ts
resources/js/wysiwyg/ui/defaults/toolbars.ts
resources/js/wysiwyg/ui/framework/blocks/color-field.ts [new file with mode: 0644]
resources/js/wysiwyg/ui/framework/blocks/color-picker.ts
resources/js/wysiwyg/ui/framework/blocks/link-field.ts
resources/js/wysiwyg/ui/framework/forms.ts
resources/sass/_editor.scss

diff --git a/resources/icons/editor/color-display.svg b/resources/icons/editor/color-display.svg
new file mode 100644 (file)
index 0000000..86be9a7
--- /dev/null
@@ -0,0 +1,10 @@
+<svg version="1.1" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+  <pattern id="pattern2" x="0.40000001" patternTransform="scale(200)" preserveAspectRatio="xMidYMid" xlink:href="#Checkerboard"/>
+  <pattern id="Checkerboard" width="2" height="2" fill="#b6b6b6" patternTransform="translate(0) scale(10)" patternUnits="userSpaceOnUse" preserveAspectRatio="xMidYMid">
+   <rect width="1" height="1"/>
+   <rect x="1" y="1" width="1" height="1"/>
+  </pattern>
+ </defs>
+ <rect class="editor-icon-color-display" x="103.53" y="-856.47" width="752.94" height="752.94" rx="47.059" ry="47.059" fill="url(#pattern2)" stroke="#666" stroke-linecap="square" stroke-linejoin="round" stroke-width="47.059"/>
+</svg>
index a49cccd26dcbf775be02def3cf574bf25fa05f17..695e8cb69ed02c7220f38e563cae46f7785bed13 100644 (file)
 
 ## 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
 
index c3726acf0010bf0cecaefc08b9a4d146b729cb35..c5b7ad29ad9c6be63548854d506f1e8944ef231f 100644 (file)
@@ -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);
index 63fa24c800feebbb121c82e384843ea496bd94e3..b592d7c67e65911d2de5f50e044d564d9506c5ec 100644 (file)
@@ -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([
index 61baa3c3260a8ffce186cf2ede150c8419d304dc..b09a7530f1f21773bb27c76486ebe6fc7c16f6fe 100644 (file)
@@ -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 (file)
index 0000000..8c8f167
--- /dev/null
@@ -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
index 65623e1b21ded8caf1e02a96aaa56a00d57185fb..c742ddc7723f76556b013815eb5f80476f562bba 100644 (file)
@@ -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
index f88b22c3f05f901123c39417aac4e03d17c72e15..880238a9a4cf4cdabedf3f0ebda329c7427845d5 100644 (file)
@@ -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);
index 36371e30238d9c38061db80aae889812d6234588..771ab0bdfe5e7f4c8a9a3d760f8f3ac2984e2eee 100644 (file)
@@ -19,15 +19,17 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti
     valuesByLabel: Record<string, string>
 }
 
+export type EditorFormFields = (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];
+
 interface EditorFormTabDefinition {
     label: string;
-    contents: EditorFormFieldDefinition[];
+    contents: EditorFormFields;
 }
 
 export interface EditorFormDefinition {
     submitText: string;
     action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
-    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;
index 2446c141670e932881749a8b46993c28b9991d34..9f7694e858c0a9a310d910940f49468e53db73ca 100644 (file)
@@ -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 {
Morty Proxy This is a proxified and sanitized view of the page, visit original site.