$getSelection,
$isTextNode,
BaseSelection,
- LexicalEditor, TextFormatType
+ LexicalEditor, LexicalNode, TextFormatType
} from "lexical";
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
}
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 {
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,
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 = {
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);
-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',
export interface EditorFormDefinition {
submitText: string;
- cancelText: string;
action: (formData: FormData, context: EditorUiContext) => boolean;
- cancel: () => void;
fields: EditorFormFieldDefinition[];
}
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;
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'}, [
});
cancelButton.addEventListener('click', (event) => {
- this.definition.cancel();
+ if (this.onCancel) {
+ this.onCancel();
+ }
});
return form;
+import {EditorFormModal, EditorFormModalDefinition} from "./modals";
+import {EditorUiContext} from "./core";
+export class EditorUIManager {
+ protected modalDefinitionsByKey: Record<string, EditorFormModalDefinition> = {};
+ protected context: EditorUiContext|null = null;
+ setContext(context: EditorUiContext) {
+ this.context = context;
+ }
-export class EditorUIManager {
+ 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());
- // Todo - Register and show modal via this
- // (Part of UI context)
+ return modal;
+ }
}
\ No newline at end of file
--- /dev/null
+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
} 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();
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, () => {
.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