diff --git a/resources/js/wysiwyg/helpers.ts b/resources/js/wysiwyg/helpers.ts index 7218f1ae6..40379cc27 100644 --- a/resources/js/wysiwyg/helpers.ts +++ b/resources/js/wysiwyg/helpers.ts @@ -3,7 +3,7 @@ import { $getSelection, $isTextNode, BaseSelection, - LexicalEditor, TextFormatType + LexicalEditor, LexicalNode, TextFormatType } from "lexical"; import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes"; import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils"; @@ -28,23 +28,27 @@ export function el(tag: string, attrs: Record<string, string> = {}, children: (s } export function selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean { + return getNodeFromSelection(selection, matcher) !== null; +} + +export function getNodeFromSelection(selection: BaseSelection|null, matcher: LexicalNodeMatcher): LexicalNode|null { if (!selection) { - return false; + return null; } for (const node of selection.getNodes()) { if (matcher(node)) { - return true; + return node; } for (const parent of node.getParents()) { if (matcher(parent)) { - return true; + return parent; } } } - return false; + return null; } export function selectionContainsTextFormat(selection: BaseSelection|null, format: TextFormatType): boolean { diff --git a/resources/js/wysiwyg/ui/defaults/button-definitions.ts b/resources/js/wysiwyg/ui/defaults/button-definitions.ts index da0a1e2c5..f5be82519 100644 --- a/resources/js/wysiwyg/ui/defaults/button-definitions.ts +++ b/resources/js/wysiwyg/ui/defaults/button-definitions.ts @@ -1,13 +1,19 @@ import {EditorButtonDefinition} from "../framework/buttons"; import { - $createParagraphNode, - $isParagraphNode, + $createNodeSelection, + $createParagraphNode, $getSelection, + $isParagraphNode, $setSelection, BaseSelection, FORMAT_TEXT_COMMAND, LexicalNode, REDO_COMMAND, TextFormatType, UNDO_COMMAND } from "lexical"; -import {selectionContainsNodeType, selectionContainsTextFormat, toggleSelectionBlockNodeType} from "../../helpers"; +import { + getNodeFromSelection, + selectionContainsNodeType, + selectionContainsTextFormat, + toggleSelectionBlockNodeType +} from "../../helpers"; import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout"; import { $createHeadingNode, @@ -17,7 +23,7 @@ import { HeadingNode, HeadingTagType } from "@lexical/rich-text"; -import {$isLinkNode, $toggleLink} from "@lexical/link"; +import {$isLinkNode, $toggleLink, LinkNode} from "@lexical/link"; import {EditorUiContext} from "../framework/core"; export const undo: EditorButtonDefinition = { @@ -133,9 +139,29 @@ export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'co export const link: EditorButtonDefinition = { label: 'Insert/edit link', action(context: EditorUiContext) { - context.editor.update(() => { - $toggleLink('http://example.com'); - }) + const linkModal = context.manager.createModal('link'); + context.editor.getEditorState().read(() => { + const selection = $getSelection(); + const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null; + + let formDefaults = {}; + if (selectedLink) { + formDefaults = { + url: selectedLink.getURL(), + text: selectedLink.getTextContent(), + title: selectedLink.getTitle(), + target: selectedLink.getTarget(), + } + + context.editor.update(() => { + const selection = $createNodeSelection(); + selection.add(selectedLink.getKey()); + $setSelection(selection); + }); + } + + linkModal.show(formDefaults); + }); }, isActive(selection: BaseSelection|null): boolean { return selectionContainsNodeType(selection, $isLinkNode); diff --git a/resources/js/wysiwyg/ui/defaults/form-definitions.ts b/resources/js/wysiwyg/ui/defaults/form-definitions.ts index c8477d9f2..457efa421 100644 --- a/resources/js/wysiwyg/ui/defaults/form-definitions.ts +++ b/resources/js/wysiwyg/ui/defaults/form-definitions.ts @@ -1,19 +1,26 @@ -import {EditorFormDefinition, EditorFormFieldDefinition, EditorSelectFormFieldDefinition} from "../framework/forms"; +import {EditorFormDefinition, EditorSelectFormFieldDefinition} from "../framework/forms"; import {EditorUiContext} from "../framework/core"; +import {$createLinkNode} from "@lexical/link"; +import {$createTextNode, $getSelection} from "lexical"; export const link: EditorFormDefinition = { submitText: 'Apply', - cancelText: 'Cancel', action(formData, context: EditorUiContext) { - // Todo - console.log('link-form-action', formData); + context.editor.update(() => { + + const selection = $getSelection(); + + const linkNode = $createLinkNode(formData.get('url')?.toString() || '', { + title: formData.get('title')?.toString() || '', + target: formData.get('target')?.toString() || '', + }); + linkNode.append($createTextNode(formData.get('text')?.toString() || '')); + + selection?.insertNodes([linkNode]); + }); return true; }, - cancel() { - // Todo - console.log('link-form-cancel'); - }, fields: [ { label: 'URL', diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts index 0fce73c12..c6338f798 100644 --- a/resources/js/wysiwyg/ui/framework/forms.ts +++ b/resources/js/wysiwyg/ui/framework/forms.ts @@ -15,9 +15,7 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti export interface EditorFormDefinition { submitText: string; - cancelText: string; action: (formData: FormData, context: EditorUiContext) => boolean; - cancel: () => void; fields: EditorFormFieldDefinition[]; } @@ -29,6 +27,15 @@ export class EditorFormField extends EditorUiElement { this.definition = definition; } + setValue(value: string) { + const input = this.getDOMElement().querySelector('input,select') as HTMLInputElement; + input.value = value; + } + + getName(): string { + return this.definition.name; + } + protected buildDOM(): HTMLElement { const id = `editor-form-field-${this.definition.name}-${Date.now()}`; let input: HTMLElement; @@ -51,14 +58,38 @@ export class EditorFormField extends EditorUiElement { export class EditorForm extends EditorContainerUiElement { protected definition: EditorFormDefinition; + protected onCancel: null|(() => void) = null; constructor(definition: EditorFormDefinition) { super(definition.fields.map(fieldDefinition => new EditorFormField(fieldDefinition))); this.definition = definition; } + setValues(values: Record<string, string>) { + for (const name of Object.keys(values)) { + const field = this.getFieldByName(name); + if (field) { + field.setValue(values[name]); + } + } + } + + setOnCancel(callback: () => void) { + this.onCancel = callback; + } + + protected getFieldByName(name: string): EditorFormField|null { + for (const child of this.children as EditorFormField[]) { + if (child.getName() === name) { + return child; + } + } + + return null; + } + protected buildDOM(): HTMLElement { - const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans(this.definition.cancelText)]); + const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans('Cancel')]); const form = el('form', {}, [ ...this.children.map(child => child.getDOMElement()), el('div', {class: 'editor-form-actions'}, [ @@ -74,7 +105,9 @@ export class EditorForm extends EditorContainerUiElement { }); cancelButton.addEventListener('click', (event) => { - this.definition.cancel(); + if (this.onCancel) { + this.onCancel(); + } }); return form; diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index f1a34c92a..c3fe9ecd8 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -1,11 +1,38 @@ - - - +import {EditorFormModal, EditorFormModalDefinition} from "./modals"; +import {EditorUiContext} from "./core"; export class EditorUIManager { - // Todo - Register and show modal via this - // (Part of UI context) + protected modalDefinitionsByKey: Record<string, EditorFormModalDefinition> = {}; + protected context: EditorUiContext|null = null; + + setContext(context: EditorUiContext) { + this.context = context; + } + + getContext(): EditorUiContext { + if (this.context === null) { + throw new Error(`Context attempted to be used without being set`); + } + + return this.context; + } + + registerModal(key: string, modalDefinition: EditorFormModalDefinition) { + this.modalDefinitionsByKey[key] = modalDefinition; + } + + createModal(key: string): EditorFormModal { + const modalDefinition = this.modalDefinitionsByKey[key]; + if (!modalDefinition) { + console.error(`Attempted to show modal of key [${key}] but no modal registered for that key`); + } + + const modal = new EditorFormModal(modalDefinition); + modal.setContext(this.getContext()); + + return modal; + } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/modals.ts b/resources/js/wysiwyg/ui/framework/modals.ts new file mode 100644 index 000000000..e2a6b3f33 --- /dev/null +++ b/resources/js/wysiwyg/ui/framework/modals.ts @@ -0,0 +1,63 @@ +import {EditorForm, EditorFormDefinition} from "./forms"; +import {el} from "../../helpers"; +import {EditorContainerUiElement} from "./containers"; + + +export interface EditorModalDefinition { + title: string; +} + +export interface EditorFormModalDefinition extends EditorModalDefinition { + form: EditorFormDefinition; +} + +export class EditorFormModal extends EditorContainerUiElement { + protected definition: EditorFormModalDefinition; + + constructor(definition: EditorFormModalDefinition) { + super([new EditorForm(definition.form)]); + this.definition = definition; + } + + show(defaultValues: Record<string, string>) { + const dom = this.getDOMElement(); + document.body.append(dom); + + const form = this.getForm(); + form.setValues(defaultValues); + form.setOnCancel(this.hide.bind(this)); + } + + hide() { + this.getDOMElement().remove(); + } + + protected getForm(): EditorForm { + return this.children[0] as EditorForm; + } + + protected buildDOM(): HTMLElement { + const closeButton = el('button', {class: 'editor-modal-close', type: 'button', title: this.trans('Close')}, ['x']); + closeButton.addEventListener('click', this.hide.bind(this)); + + const modal = el('div', {class: 'editor-modal editor-form-modal'}, [ + el('div', {class: 'editor-modal-header'}, [ + el('div', {class: 'editor-modal-title'}, [this.trans(this.definition.title)]), + closeButton, + ]), + el('div', {class: 'editor-modal-body'}, [ + this.getForm().getDOMElement(), + ]), + ]); + + const wrapper = el('div', {class: 'editor-modal-wrapper'}, [modal]); + + wrapper.addEventListener('click', event => { + if (event.target && !modal.contains(event.target as HTMLElement)) { + this.hide(); + } + }); + + return wrapper; + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 5a0d7fd2d..7e1f8d981 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -6,8 +6,7 @@ import { } from "lexical"; import {getMainEditorFullToolbar} from "./toolbars"; import {EditorUIManager} from "./framework/manager"; -import {EditorForm} from "./framework/forms"; -import {link} from "./defaults/form-definitions"; +import {link as linkFormDefinition} from "./defaults/form-definitions"; export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { const manager = new EditorUIManager(); @@ -16,16 +15,18 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { manager, translate: (text: string): string => text, }; + manager.setContext(context); // Create primary toolbar const toolbar = getMainEditorFullToolbar(); toolbar.setContext(context); element.before(toolbar.getDOMElement()); - // Form test - const linkForm = new EditorForm(link); - linkForm.setContext(context); - element.before(linkForm.getDOMElement()); + // Register modals + manager.registerModal('link', { + title: 'Insert/Edit link', + form: linkFormDefinition, + }); // Update button states on editor selection change editor.registerCommand(SELECTION_CHANGE_COMMAND, () => { diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 48912be8b..2633e8539 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -46,4 +46,30 @@ .editor-format-menu .editor-dropdown-menu { min-width: 320px; +} + +// Modals +.editor-modal-wrapper { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + z-index: 999; + background-color: rgba(0, 0, 0, 0.5); + width: 100%; + height: 100%; +} +.editor-modal { + background-color: #FFF; + border: 1px solid #DDD; + padding: 1rem; + border-radius: 4px; +} +.editor-modal-header { + display: flex; + justify-content: space-between; + margin-bottom: 1rem; +} +.editor-modal-title { + font-weight: 700; } \ No newline at end of file