$createParagraphNode,
$isParagraphNode,
BaseSelection, FORMAT_TEXT_COMMAND,
- LexicalEditor,
LexicalNode,
REDO_COMMAND, TextFormatType,
UNDO_COMMAND
HeadingTagType
} from "@lexical/rich-text";
import {$isLinkNode, $toggleLink} from "@lexical/link";
+import {EditorUiContext} from "../framework/core";
export const undo: EditorButtonDefinition = {
label: 'Undo',
- action(editor: LexicalEditor) {
- editor.dispatchCommand(UNDO_COMMAND, undefined);
+ action(context: EditorUiContext) {
+ context.editor.dispatchCommand(UNDO_COMMAND, undefined);
},
isActive(selection: BaseSelection|null): boolean {
return false;
export const redo: EditorButtonDefinition = {
label: 'Redo',
- action(editor: LexicalEditor) {
- editor.dispatchCommand(REDO_COMMAND, undefined);
+ action(context: EditorUiContext) {
+ context.editor.dispatchCommand(REDO_COMMAND, undefined);
},
isActive(selection: BaseSelection|null): boolean {
return false;
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
return {
label: `${name} Callout`,
- action(editor: LexicalEditor) {
+ action(context: EditorUiContext) {
toggleSelectionBlockNodeType(
- editor,
+ context.editor,
(node) => $isCalloutNodeOfCategory(node, category),
() => $createCalloutNode(category),
)
function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
return {
label: name,
- action(editor: LexicalEditor) {
+ action(context: EditorUiContext) {
toggleSelectionBlockNodeType(
- editor,
+ context.editor,
(node) => isHeaderNodeOfTag(node, tag),
() => $createHeadingNode(tag),
)
export const blockquote: EditorButtonDefinition = {
label: 'Blockquote',
- action(editor: LexicalEditor) {
- toggleSelectionBlockNodeType(editor, $isQuoteNode, $createQuoteNode);
+ action(context: EditorUiContext) {
+ toggleSelectionBlockNodeType(context.editor, $isQuoteNode, $createQuoteNode);
},
isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isQuoteNode);
export const paragraph: EditorButtonDefinition = {
label: 'Paragraph',
- action(editor: LexicalEditor) {
- toggleSelectionBlockNodeType(editor, $isParagraphNode, $createParagraphNode);
+ action(context: EditorUiContext) {
+ toggleSelectionBlockNodeType(context.editor, $isParagraphNode, $createParagraphNode);
},
isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isParagraphNode);
function buildFormatButton(label: string, format: TextFormatType): EditorButtonDefinition {
return {
label: label,
- action(editor: LexicalEditor) {
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
+ action(context: EditorUiContext) {
+ context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
},
isActive(selection: BaseSelection|null): boolean {
return selectionContainsTextFormat(selection, format);
export const link: EditorButtonDefinition = {
label: 'Insert/edit link',
- action(editor: LexicalEditor) {
- editor.update(() => {
+ action(context: EditorUiContext) {
+ context.editor.update(() => {
$toggleLink('http://example.com');
})
},
--- /dev/null
+import {EditorFormDefinition, EditorFormFieldDefinition, EditorSelectFormFieldDefinition} from "../framework/forms";
+import {EditorUiContext} from "../framework/core";
+
+
+export const link: EditorFormDefinition = {
+ submitText: 'Apply',
+ cancelText: 'Cancel',
+ action(formData, context: EditorUiContext) {
+ // Todo
+ console.log('link-form-action', formData);
+ return true;
+ },
+ cancel() {
+ // Todo
+ console.log('link-form-cancel');
+ },
+ fields: [
+ {
+ label: 'URL',
+ name: 'url',
+ type: 'text',
+ },
+ {
+ label: 'Text to display',
+ name: 'text',
+ type: 'text',
+ },
+ {
+ label: 'Title',
+ name: 'title',
+ type: 'text',
+ },
+ {
+ label: 'Open link in...',
+ name: 'target',
+ type: 'select',
+ valuesByLabel: {
+ 'Current window': '',
+ 'New window': '_blank',
+ }
+ } as EditorSelectFormFieldDefinition,
+ ],
+};
\ No newline at end of file
-import {BaseSelection, LexicalEditor} from "lexical";
-import {EditorUiElement, EditorUiStateUpdate} from "./base-elements";
+import {BaseSelection} from "lexical";
+import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
import {el} from "../../helpers";
export interface EditorButtonDefinition {
label: string;
- action: (editor: LexicalEditor) => void;
+ action: (context: EditorUiContext) => void;
isActive: (selection: BaseSelection|null) => boolean;
}
export class EditorButton extends EditorUiElement {
protected definition: EditorButtonDefinition;
+ protected active: boolean = false;
constructor(definition: EditorButtonDefinition) {
super();
const button = el('button', {
type: 'button',
class: 'editor-button',
- }, [this.definition.label]) as HTMLButtonElement;
+ }, [this.getLabel()]) as HTMLButtonElement;
button.addEventListener('click', this.onClick.bind(this));
}
protected onClick() {
- this.definition.action(this.getContext().editor);
+ this.definition.action(this.getContext());
}
updateActiveState(selection: BaseSelection|null) {
- const isActive = this.definition.isActive(selection);
- this.dom?.classList.toggle('editor-button-active', isActive);
+ this.active = this.definition.isActive(selection);
+ this.dom?.classList.toggle('editor-button-active', this.active);
}
updateState(state: EditorUiStateUpdate): void {
this.updateActiveState(state.selection);
}
+
+ isActive(): boolean {
+ return this.active;
+ }
+
+ getLabel(): string {
+ return this.trans(this.definition.label);
+ }
}
export class FormatPreviewButton extends EditorButton {
const preview = el('span', {
class: 'editor-button-format-preview'
- }, [this.definition.label]);
+ }, [this.getLabel()]);
const stylesToApply = this.getStylesFromPreview();
console.log(stylesToApply);
protected getStylesFromPreview(): Record<string, string> {
const wrap = el('div', {style: 'display: none', hidden: 'true', class: 'page-content'});
const sampleClone = this.previewSampleElement.cloneNode() as HTMLElement;
- sampleClone.textContent = this.definition.label;
+ sampleClone.textContent = this.getLabel();
wrap.append(sampleClone);
document.body.append(wrap);
-import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./base-elements";
+import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
import {el} from "../../helpers";
+import {EditorButton} from "./buttons";
export class EditorContainerUiElement extends EditorUiElement {
protected children : EditorUiElement[];
}
setContext(context: EditorUiContext) {
+ super.setContext(context);
for (const child of this.getChildren()) {
child.setContext(context);
}
}, childElements);
const toggle = el('button', {
- class: 'editor-format-menu-toggle',
+ class: 'editor-format-menu-toggle editor-button',
type: 'button',
- }, ['Formats']);
+ }, [this.trans('Formats')]);
const wrapper = el('div', {
class: 'editor-format-menu editor-dropdown-menu-container',
return wrapper;
}
+
+ updateState(state: EditorUiStateUpdate) {
+ super.updateState(state);
+
+ for (const child of this.children) {
+ if (child instanceof EditorButton && child.isActive()) {
+ this.updateToggleLabel(child.getLabel());
+ return;
+ }
+ }
+
+ this.updateToggleLabel(this.trans('Formats'));
+ }
+
+ protected updateToggleLabel(text: string): void {
+ const button = this.getDOMElement().querySelector('button');
+ if (button) {
+ button.innerText = text;
+ }
+ }
}
\ No newline at end of file
import {BaseSelection, LexicalEditor} from "lexical";
+import {EditorUIManager} from "./manager";
export type EditorUiStateUpdate = {
editor: LexicalEditor,
export type EditorUiContext = {
editor: LexicalEditor,
+ translate: (text: string) => string,
+ manager: EditorUIManager,
};
export abstract class EditorUiElement {
return this.dom;
}
- abstract updateState(state: EditorUiStateUpdate): void;
+ trans(text: string) {
+ return this.getContext().translate(text);
+ }
+
+ updateState(state: EditorUiStateUpdate): void {
+ return;
+ }
}
\ No newline at end of file
--- /dev/null
+import {EditorUiContext, EditorUiElement} from "./core";
+import {EditorContainerUiElement} from "./containers";
+import {el} from "../../helpers";
+
+export interface EditorFormFieldDefinition {
+ label: string;
+ name: string;
+ type: 'text' | 'select';
+}
+
+export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
+ type: 'select',
+ valuesByLabel: Record<string, string>
+}
+
+export interface EditorFormDefinition {
+ submitText: string;
+ cancelText: string;
+ action: (formData: FormData, context: EditorUiContext) => boolean;
+ cancel: () => void;
+ fields: EditorFormFieldDefinition[];
+}
+
+export class EditorFormField extends EditorUiElement {
+ protected definition: EditorFormFieldDefinition;
+
+ constructor(definition: EditorFormFieldDefinition) {
+ super();
+ this.definition = definition;
+ }
+
+ protected buildDOM(): HTMLElement {
+ const id = `editor-form-field-${this.definition.name}-${Date.now()}`;
+ let input: HTMLElement;
+
+ if (this.definition.type === 'select') {
+ const options = (this.definition as EditorSelectFormFieldDefinition).valuesByLabel
+ const labels = Object.keys(options);
+ const optionElems = labels.map(label => el('option', {value: options[label]}, [label]));
+ input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems);
+ } else {
+ input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
+ }
+
+ return el('div', {class: 'editor-form-field-wrapper'}, [
+ el('label', {class: 'editor-form-field-label', for: id}, [this.trans(this.definition.label)]),
+ input,
+ ]);
+ }
+}
+
+export class EditorForm extends EditorContainerUiElement {
+ protected definition: EditorFormDefinition;
+
+ constructor(definition: EditorFormDefinition) {
+ super(definition.fields.map(fieldDefinition => new EditorFormField(fieldDefinition)));
+ this.definition = definition;
+ }
+
+ protected buildDOM(): HTMLElement {
+ const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans(this.definition.cancelText)]);
+ const form = el('form', {}, [
+ ...this.children.map(child => child.getDOMElement()),
+ el('div', {class: 'editor-form-actions'}, [
+ cancelButton,
+ el('button', {type: 'submit', class: 'editor-form-action-primary'}, [this.trans(this.definition.submitText)]),
+ ])
+ ]);
+
+ form.addEventListener('submit', (event) => {
+ event.preventDefault();
+ const formData = new FormData(form as HTMLFormElement);
+ this.definition.action(formData, this.getContext());
+ });
+
+ cancelButton.addEventListener('click', (event) => {
+ this.definition.cancel();
+ });
+
+ return form;
+ }
+}
\ No newline at end of file
--- /dev/null
+
+
+
+
+
+export class EditorUIManager {
+
+ // Todo - Register and show modal via this
+ // (Part of UI context)
+
+}
\ No newline at end of file
SELECTION_CHANGE_COMMAND
} from "lexical";
import {getMainEditorFullToolbar} from "./toolbars";
+import {EditorUIManager} from "./framework/manager";
+import {EditorForm} from "./framework/forms";
+import {link} from "./defaults/form-definitions";
export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
+ const manager = new EditorUIManager();
+ const context = {
+ editor,
+ manager,
+ translate: (text: string): string => text,
+ };
+
+ // Create primary toolbar
const toolbar = getMainEditorFullToolbar();
- toolbar.setContext({editor});
+ toolbar.setContext(context);
element.before(toolbar.getDOMElement());
+ // Form test
+ const linkForm = new EditorForm(link);
+ linkForm.setContext(context);
+ element.before(linkForm.getDOMElement());
+
// Update button states on editor selection change
editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
const selection = $getSelection();