From ce8c9dd079ec354525bab57c852e8984f08ce25c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 28 Jul 2024 12:48:58 +0100 Subject: [PATCH] Lexical: Added form complex/tab ui support --- resources/js/wysiwyg/todo.md | 4 +- .../wysiwyg/ui/defaults/form-definitions.ts | 55 ++++--- resources/js/wysiwyg/ui/framework/core.ts | 8 ++ resources/js/wysiwyg/ui/framework/forms.ts | 134 ++++++++++++++++-- resources/sass/_editor.scss | 39 +++++ 5 files changed, 209 insertions(+), 31 deletions(-) diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 2125aa258..49f685bea 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -2,8 +2,7 @@ ## In progress -- Update forms to allow panels (Media) - - Will be used for table forms also. +// ## Main Todo @@ -23,6 +22,7 @@ - Image gallery integration for form - Drawing gallery integration - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts) +- Media resize support (like images) ## Bugs diff --git a/resources/js/wysiwyg/ui/defaults/form-definitions.ts b/resources/js/wysiwyg/ui/defaults/form-definitions.ts index a2242c338..6c0a54f23 100644 --- a/resources/js/wysiwyg/ui/defaults/form-definitions.ts +++ b/resources/js/wysiwyg/ui/defaults/form-definitions.ts @@ -1,4 +1,4 @@ -import {EditorFormDefinition, EditorSelectFormFieldDefinition} from "../framework/forms"; +import {EditorFormDefinition, EditorFormTabs, EditorSelectFormFieldDefinition} from "../framework/forms"; import {EditorUiContext} from "../framework/core"; import {$createLinkNode} from "@lexical/link"; import {$createTextNode, $getSelection, LexicalNode} from "lexical"; @@ -133,25 +133,40 @@ export const media: EditorFormDefinition = { }, fields: [ { - label: 'Source', - name: 'src', - type: 'text', - }, - { - label: 'Width', - name: 'width', - type: 'text', - }, - { - label: 'Height', - name: 'height', - type: 'text', - }, - // TODO - Tabbed interface to separate this option - { - label: 'Paste your embed code below:', - name: 'embed', - type: 'textarea', + build() { + return new EditorFormTabs([ + { + label: 'General', + contents: [ + { + label: 'Source', + name: 'src', + type: 'text', + }, + { + label: 'Width', + name: 'width', + type: 'text', + }, + { + label: 'Height', + name: 'height', + type: 'text', + }, + ], + }, + { + label: 'Embed', + contents: [ + { + label: 'Paste your embed code below:', + name: 'embed', + type: 'textarea', + }, + ], + } + ]) + } }, ], }; diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts index c8f390c48..f644bc37a 100644 --- a/resources/js/wysiwyg/ui/framework/core.ts +++ b/resources/js/wysiwyg/ui/framework/core.ts @@ -18,6 +18,14 @@ export type EditorUiContext = { options: Record; // General user options which may be used by sub elements }; +export interface EditorUiBuilderDefinition { + build: () => EditorUiElement; +} + +export function isUiBuilderDefinition(object: any): object is EditorUiBuilderDefinition { + return 'build' in object; +} + export abstract class EditorUiElement { protected dom: HTMLElement|null = null; private context: EditorUiContext|null = null; diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts index b641f993b..b225a3de2 100644 --- a/resources/js/wysiwyg/ui/framework/forms.ts +++ b/resources/js/wysiwyg/ui/framework/forms.ts @@ -1,5 +1,12 @@ -import {EditorUiContext, EditorUiElement, EditorContainerUiElement} from "./core"; +import { + EditorUiContext, + EditorUiElement, + EditorContainerUiElement, + EditorUiBuilderDefinition, + isUiBuilderDefinition +} from "./core"; import {el} from "../../helpers"; +import {uniqueId} from "../../../services/util"; export interface EditorFormFieldDefinition { label: string; @@ -12,10 +19,15 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti valuesByLabel: Record } +interface EditorFormTabDefinition { + label: string; + contents: EditorFormFieldDefinition[]; +} + export interface EditorFormDefinition { submitText: string; action: (formData: FormData, context: EditorUiContext) => Promise; - fields: EditorFormFieldDefinition[]; + fields: (EditorFormFieldDefinition|EditorUiBuilderDefinition)[]; } export class EditorFormField extends EditorUiElement { @@ -62,7 +74,14 @@ export class EditorForm extends EditorContainerUiElement { protected onCancel: null|(() => void) = null; constructor(definition: EditorFormDefinition) { - super(definition.fields.map(fieldDefinition => new EditorFormField(fieldDefinition))); + let children: (EditorFormField|EditorUiElement)[] = definition.fields.map(fieldDefinition => { + if (isUiBuilderDefinition(fieldDefinition)) { + return fieldDefinition.build(); + } + return new EditorFormField(fieldDefinition) + }); + + super(children); this.definition = definition; } @@ -80,13 +99,23 @@ export class EditorForm extends EditorContainerUiElement { } protected getFieldByName(name: string): EditorFormField|null { - for (const child of this.children as EditorFormField[]) { - if (child.getName() === name) { - return child; - } - } - return null; + const search = (children: EditorUiElement[]): EditorFormField|null => { + for (const child of children) { + if (child instanceof EditorFormField && child.getName() === name) { + return child; + } else if (child instanceof EditorContainerUiElement) { + const matchingChild = search(child.getChildren()); + if (matchingChild) { + return matchingChild; + } + } + } + + return null; + }; + + return search(this.getChildren()); } protected buildDOM(): HTMLElement { @@ -113,4 +142,91 @@ export class EditorForm extends EditorContainerUiElement { return form; } +} + +export class EditorFormTab extends EditorContainerUiElement { + + protected definition: EditorFormTabDefinition; + protected fields: EditorFormField[]; + protected id: string; + + constructor(definition: EditorFormTabDefinition) { + const fields = definition.contents.map(fieldDef => new EditorFormField(fieldDef)); + super(fields); + + this.definition = definition; + this.fields = fields; + this.id = uniqueId(); + } + + public getLabel(): string { + return this.getContext().translate(this.definition.label); + } + + public getId(): string { + return this.id; + } + + protected buildDOM(): HTMLElement { + return el( + 'div', + { + class: 'editor-form-tab-content', + role: 'tabpanel', + id: `editor-tabpanel-${this.id}`, + 'aria-labelledby': `editor-tab-${this.id}`, + }, + this.fields.map(f => f.getDOMElement()) + ); + } +} +export class EditorFormTabs extends EditorContainerUiElement { + + protected definitions: EditorFormTabDefinition[] = []; + protected tabs: EditorFormTab[] = []; + + constructor(definitions: EditorFormTabDefinition[]) { + const tabs: EditorFormTab[] = definitions.map(d => new EditorFormTab(d)); + super(tabs); + + this.definitions = definitions; + this.tabs = tabs; + } + + protected buildDOM(): HTMLElement { + const controls: HTMLElement[] = []; + const contents: HTMLElement[] = []; + + const selectTab = (tabIndex: number) => { + for (let i = 0; i < controls.length; i++) { + controls[i].setAttribute('aria-selected', (i === tabIndex) ? 'true' : 'false'); + } + for (let i = 0; i < contents.length; i++) { + contents[i].hidden = !(i === tabIndex); + } + }; + + for (const tab of this.tabs) { + const button = el('button', { + class: 'editor-form-tab-control', + type: 'button', + role: 'tab', + id: `editor-tab-${tab.getId()}`, + 'aria-controls': `editor-tabpanel-${tab.getId()}` + }, [tab.getLabel()]); + contents.push(tab.getDOMElement()); + controls.push(button); + + button.addEventListener('click', event => { + selectTab(controls.indexOf(button)); + }); + } + + selectTab(0); + + return el('div', {class: 'editor-form-tab-container'}, [ + el('div', {class: 'editor-form-tab-controls'}, controls), + el('div', {class: 'editor-form-tab-contents'}, contents), + ]); + } } \ No newline at end of file diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 17e4af97b..1e52ad6a9 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -398,6 +398,45 @@ textarea.editor-form-field-input { box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1); } } +.editor-form-tab-container { + display: flex; + flex-direction: row; + gap: 2rem; +} +.editor-form-tab-controls { + display: flex; + flex-direction: column; + align-items: stretch; + gap: .25rem; +} +.editor-form-tab-control { + font-weight: bold; + font-size: 14px; + color: #444; + border-bottom: 2px solid transparent; + position: relative; + cursor: pointer; + padding: .25rem .5rem; + text-align: start; + &[aria-selected="true"] { + border-color: var(--editor-color-primary); + color: var(--editor-color-primary); + } + &[aria-selected="true"]:after, &:hover:after { + background-color: var(--editor-color-primary); + opacity: .15; + content: ''; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } +} +.editor-form-tab-contents { + width: 360px; +} // Editor theme styles .editor-theme-bold {