Lexical: Added color picker/indicator to form fields

This commit is contained in:
Dan Brown 2025-01-18 11:12:43 +00:00
parent c091f67db3
commit 04cca77ae6
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 125 additions and 36 deletions

View File

@ -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>

After

Width:  |  Height:  |  Size: 762 B

View File

@ -10,13 +10,9 @@
## Secondary Todo ## Secondary Todo
- Color picker support in table form color fields
- Color picker for color controls
- Table caption text support - 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) - 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 - Deep check of translation coverage
- About button & view
- Mobile display and handling
## Bugs ## Bugs

View File

@ -12,6 +12,8 @@ import subscriptIcon from "@icons/editor/subscript.svg";
import codeIcon from "@icons/editor/code.svg"; import codeIcon from "@icons/editor/code.svg";
import formatClearIcon from "@icons/editor/format-clear.svg"; import formatClearIcon from "@icons/editor/format-clear.svg";
import {$selectionContainsTextFormat} from "../../../utils/selection"; import {$selectionContainsTextFormat} from "../../../utils/selection";
import {$patchStyleText} from "@lexical/selection";
import {context} from "esbuild";
function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition { function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
return { return {
@ -32,6 +34,18 @@ export const underline: EditorButtonDefinition = buildFormatButton('Underline',
export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon}; export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
export const highlightColor: EditorBasicButtonDefinition = {label: 'Background color', icon: highlightIcon}; 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 strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon); export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon); export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);

View File

@ -1,6 +1,6 @@
import { import {
EditorFormDefinition, EditorFormDefinition,
EditorFormFieldDefinition, EditorFormFieldDefinition, EditorFormFields,
EditorFormTabs, EditorFormTabs,
EditorSelectFormFieldDefinition EditorSelectFormFieldDefinition
} from "../../framework/forms"; } from "../../framework/forms";
@ -17,6 +17,7 @@ import {
import {formatSizeValue} from "../../../utils/dom"; import {formatSizeValue} from "../../../utils/dom";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table"; import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {CommonBlockAlignment} from "lexical/nodes/common"; import {CommonBlockAlignment} from "lexical/nodes/common";
import {colorFieldBuilder} from "../../framework/blocks/color-field";
const borderStyleInput: EditorSelectFormFieldDefinition = { const borderStyleInput: EditorSelectFormFieldDefinition = {
label: 'Border style', label: 'Border style',
@ -145,15 +146,15 @@ export const cellProperties: EditorFormDefinition = {
} as EditorSelectFormFieldDefinition, } as EditorSelectFormFieldDefinition,
]; ];
const advancedFields: EditorFormFieldDefinition[] = [ const advancedFields: EditorFormFields = [
{ {
label: 'Border width', // inline-style: border-width label: 'Border width', // inline-style: border-width
name: 'border_width', name: 'border_width',
type: 'text', type: 'text',
}, },
borderStyleInput, // inline-style: border-style borderStyleInput, // inline-style: border-style
borderColorInput, // inline-style: border-color colorFieldBuilder(borderColorInput),
backgroundColorInput, // inline-style: background-color colorFieldBuilder(backgroundColorInput),
]; ];
return new EditorFormTabs([ return new EditorFormTabs([
@ -210,8 +211,8 @@ export const rowProperties: EditorFormDefinition = {
type: 'text', type: 'text',
}, },
borderStyleInput, // style on tr: height borderStyleInput, // style on tr: height
borderColorInput, // style on tr: height colorFieldBuilder(borderColorInput),
backgroundColorInput, // style on tr: height colorFieldBuilder(backgroundColorInput),
], ],
}; };
@ -305,10 +306,10 @@ export const tableProperties: EditorFormDefinition = {
alignmentInput, // alignment class alignmentInput, // alignment class
]; ];
const advancedFields: EditorFormFieldDefinition[] = [ const advancedFields: EditorFormFields = [
borderStyleInput, // Style - border-style borderStyleInput,
borderColorInput, // Style - border-color colorFieldBuilder(borderColorInput),
backgroundColorInput, // Style - background-color colorFieldBuilder(backgroundColorInput),
]; ];
return new EditorFormTabs([ return new EditorFormTabs([

View File

@ -44,11 +44,11 @@ import {
} from "./buttons/block-formats"; } from "./buttons/block-formats";
import { import {
bold, clearFormating, code, bold, clearFormating, code,
highlightColor, highlightColor, highlightColorAction,
italic, italic,
strikethrough, subscript, strikethrough, subscript,
superscript, superscript,
textColor, textColor, textColorAction,
underline underline
} from "./buttons/inline-formats"; } from "./buttons/inline-formats";
import { import {
@ -114,10 +114,10 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai
new EditorButton(italic), new EditorButton(italic),
new EditorButton(underline), new EditorButton(underline),
new EditorDropdownButton({ button: new EditorColorButton(textColor, 'color') }, [ new EditorDropdownButton({ button: new EditorColorButton(textColor, 'color') }, [
new EditorColorPicker('color'), new EditorColorPicker(textColorAction),
]), ]),
new EditorDropdownButton({button: new EditorColorButton(highlightColor, 'background-color')}, [ new EditorDropdownButton({button: new EditorColorButton(highlightColor, 'background-color')}, [
new EditorColorPicker('background-color'), new EditorColorPicker(highlightColorAction),
]), ]),
new EditorButton(strikethrough), new EditorButton(strikethrough),
new EditorButton(superscript), new EditorButton(superscript),

View File

@ -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));
}
}
}

View File

@ -1,6 +1,4 @@
import {EditorUiElement} from "../core"; import {EditorUiContext, EditorUiElement} from "../core";
import {$getSelection} from "lexical";
import {$patchStyleText} from "@lexical/selection";
import {el} from "../../../utils/dom"; import {el} from "../../../utils/dom";
import removeIcon from "@icons/editor/color-clear.svg"; import removeIcon from "@icons/editor/color-clear.svg";
@ -38,13 +36,15 @@ const colorChoices = [
const storageKey = 'bs-lexical-custom-colors'; const storageKey = 'bs-lexical-custom-colors';
export type EditorColorPickerCallback = (color: string, context: EditorUiContext) => void;
export class EditorColorPicker extends EditorUiElement { export class EditorColorPicker extends EditorUiElement {
protected styleProperty: string; protected callback: EditorColorPickerCallback;
constructor(styleProperty: string) { constructor(callback: EditorColorPickerCallback) {
super(); super();
this.styleProperty = styleProperty; this.callback = callback;
} }
buildDOM(): HTMLElement { buildDOM(): HTMLElement {
@ -131,11 +131,6 @@ export class EditorColorPicker extends EditorUiElement {
} }
setColor(color: string) { setColor(color: string) {
this.getContext().editor.update(() => { this.callback(color, this.getContext());
const selection = $getSelection();
if (selection) {
$patchStyleText(selection, {[this.styleProperty]: color || null});
}
});
} }
} }

View File

@ -44,7 +44,6 @@ export class LinkField extends EditorContainerUiElement {
updateFormFromHeader(header: HeadingNode) { updateFormFromHeader(header: HeadingNode) {
this.getHeaderIdAndText(header).then(({id, text}) => { this.getHeaderIdAndText(header).then(({id, text}) => {
console.log('updating form', id, text);
const modal = this.getContext().manager.getActiveModal('link'); const modal = this.getContext().manager.getActiveModal('link');
if (modal) { if (modal) {
modal.getForm().setValues({ modal.getForm().setValues({
@ -60,7 +59,6 @@ export class LinkField extends EditorContainerUiElement {
return new Promise((res) => { return new Promise((res) => {
this.getContext().editor.update(() => { this.getContext().editor.update(() => {
let id = header.getId(); let id = header.getId();
console.log('header', id, header.__id);
if (!id) { if (!id) {
id = 'header-' + uniqueIdSmall(); id = 'header-' + uniqueIdSmall();
header.setId(id); header.setId(id);

View File

@ -19,15 +19,17 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti
valuesByLabel: Record<string, string> valuesByLabel: Record<string, string>
} }
export type EditorFormFields = (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];
interface EditorFormTabDefinition { interface EditorFormTabDefinition {
label: string; label: string;
contents: EditorFormFieldDefinition[]; contents: EditorFormFields;
} }
export interface EditorFormDefinition { export interface EditorFormDefinition {
submitText: string; submitText: string;
action: (formData: FormData, context: EditorUiContext) => Promise<boolean>; action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
fields: (EditorFormFieldDefinition|EditorUiBuilderDefinition)[]; fields: EditorFormFields;
} }
export class EditorFormField extends EditorUiElement { export class EditorFormField extends EditorUiElement {
@ -41,6 +43,7 @@ export class EditorFormField extends EditorUiElement {
setValue(value: string) { setValue(value: string) {
const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement; const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement;
input.value = value; input.value = value;
input.dispatchEvent(new Event('change'));
} }
getName(): string { getName(): string {
@ -155,11 +158,17 @@ export class EditorForm extends EditorContainerUiElement {
export class EditorFormTab extends EditorContainerUiElement { export class EditorFormTab extends EditorContainerUiElement {
protected definition: EditorFormTabDefinition; protected definition: EditorFormTabDefinition;
protected fields: EditorFormField[]; protected fields: EditorUiElement[];
protected id: string; protected id: string;
constructor(definition: EditorFormTabDefinition) { 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); super(fields);
this.definition = definition; this.definition = definition;

View File

@ -649,6 +649,16 @@ textarea.editor-form-field-input {
width: $inputWidth - 40px; 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 styles
.editor-theme-bold { .editor-theme-bold {