const editor = createEditor(config);
editor.setRootElement(editArea);
+ const context: EditorUiContext = buildEditorUI(container, editArea, editWrap, editor, options);
mergeRegister(
registerRichText(editor),
registerHistory(editor, createEmptyHistoryState(), 300),
- registerShortcuts(editor),
+ registerShortcuts(context),
registerTableResizer(editor, editWrap),
registerTableSelectionHandler(editor),
registerTaskListHandler(editor, editArea),
console.log(editor.getEditorState().toJSON());
};
- const context: EditorUiContext = buildEditorUI(container, editArea, editWrap, editor, options);
registerCommonNodeMutationListeners(context);
return new SimpleWysiwygEditorInterface(editor);
-import {COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
+import {$getSelection, COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
import {
cycleSelectionCalloutFormats,
- formatCodeBlock,
+ formatCodeBlock, insertOrUpdateLink,
toggleSelectionAsBlockquote,
- toggleSelectionAsHeading,
+ toggleSelectionAsHeading, toggleSelectionAsList,
toggleSelectionAsParagraph
} from "../utils/formats";
import {HeadingTagType} from "@lexical/rich-text";
+import {EditorUiContext} from "../ui/framework/core";
+import {$getNodeFromSelection} from "../utils/selection";
+import {$isLinkNode, LinkNode} from "@lexical/link";
+import {$showLinkForm} from "../ui/defaults/forms/objects";
+import {showLinkSelector} from "../utils/links";
function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
toggleSelectionAsHeading(editor, tag);
return true;
}
-type ShortcutAction = (editor: LexicalEditor) => boolean;
+type ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boolean;
const actionsByKeys: Record<string, ShortcutAction> = {
- // Save draft
'ctrl+s': () => {
window.$events.emit('editor-save-draft');
return true;
'ctrl+shift+e': toggleInlineCode,
'ctrl+9': wrapFormatAction(cycleSelectionCalloutFormats),
- // TODO Lists
- // TODO Links
- // TODO Link selector
+ 'ctrl+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')),
+ 'ctrl+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')),
+ 'ctrl+k': (editor, context) => {
+ editor.getEditorState().read(() => {
+ const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;
+ $showLinkForm(selectedLink, context);
+ });
+ return true;
+ },
+ 'ctrl+shift+k': (editor, context) => {
+ showLinkSelector(entity => {
+ insertOrUpdateLink(editor, {
+ text: entity.name,
+ title: entity.link,
+ target: '',
+ url: entity.link,
+ });
+ });
+ return true;
+ },
};
-function createKeyDownListener(editor: LexicalEditor): (e: KeyboardEvent) => void {
+function createKeyDownListener(context: EditorUiContext): (e: KeyboardEvent) => void {
return (event: KeyboardEvent) => {
// TODO - Mac Cmd support
const combo = `${event.ctrlKey ? 'ctrl+' : ''}${event.shiftKey ? 'shift+' : ''}${event.key}`.toLowerCase();
- console.log(`pressed: ${combo}`);
+ // console.log(`pressed: ${combo}`);
if (actionsByKeys[combo]) {
- const handled = actionsByKeys[combo](editor);
+ const handled = actionsByKeys[combo](context.editor, context);
if (handled) {
event.stopPropagation();
event.preventDefault();
}, COMMAND_PRIORITY_HIGH);
}
-export function registerShortcuts(editor: LexicalEditor) {
- const listener = createKeyDownListener(editor);
- overrideDefaultCommands(editor);
+export function registerShortcuts(context: EditorUiContext) {
+ const listener = createKeyDownListener(context);
+ overrideDefaultCommands(context.editor);
- return editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
+ return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
// add the listener to the current root element
rootElement?.addEventListener('keydown', listener);
// remove the listener from the old root element
## In progress
-- Keyboard shortcuts support
+//
## Main Todo
- Media resize support (like images)
- Table caption text support
- Table Cut/Copy/Paste column
+- Mac: Shortcut support via command.
## Secondary Todo
-import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
+import {$isListNode, ListNode, ListType} from "@lexical/list";
import {EditorButtonDefinition} from "../../framework/buttons";
import {EditorUiContext} from "../../framework/core";
-import {$getSelection, BaseSelection, LexicalNode} from "lexical";
+import {BaseSelection, LexicalNode} from "lexical";
import listBulletIcon from "@icons/editor/list-bullet.svg";
import listNumberedIcon from "@icons/editor/list-numbered.svg";
import listCheckIcon from "@icons/editor/list-check.svg";
import {$selectionContainsNodeType} from "../../../utils/selection";
+import {toggleSelectionAsList} from "../../../utils/formats";
function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
label,
icon,
action(context: EditorUiContext) {
- context.editor.getEditorState().read(() => {
- const selection = $getSelection();
- if (this.isActive(selection, context)) {
- removeList(context.editor);
- } else {
- insertList(context.editor, type);
- }
- });
+ toggleSelectionAsList(context.editor, type);
},
isActive(selection: BaseSelection|null): boolean {
return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
import linkIcon from "@icons/editor/link.svg";
import {EditorUiContext} from "../../framework/core";
import {
- $createNodeSelection,
$createTextNode,
$getRoot,
$getSelection, $insertNodes,
- $setSelection,
BaseSelection,
ElementNode
} from "lexical";
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../../nodes/horizontal-rule";
import codeBlockIcon from "@icons/editor/code-block.svg";
-import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../../nodes/code-block";
+import {$isCodeBlockNode} from "../../../nodes/code-block";
import editIcon from "@icons/edit.svg";
import diagramIcon from "@icons/editor/diagram.svg";
import {$createDiagramNode, DiagramNode} from "../../../nodes/diagram";
} from "../../../utils/selection";
import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
-import {$showImageForm} from "../forms/objects";
+import {$showImageForm, $showLinkForm} from "../forms/objects";
import {formatCodeBlock} from "../../../utils/formats";
export const link: EditorButtonDefinition = {
label: 'Insert/edit link',
icon: linkIcon,
action(context: EditorUiContext) {
- 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);
+ const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;
+ $showLinkForm(selectedLink, context);
});
},
isActive(selection: BaseSelection | null): boolean {
EditorSelectFormFieldDefinition
} from "../../framework/forms";
import {EditorUiContext} from "../../framework/core";
-import {$createTextNode, $getSelection, $insertNodes} from "lexical";
+import {$createNodeSelection, $createTextNode, $getSelection, $insertNodes, $setSelection} from "lexical";
import {$isImageNode, ImageNode} from "../../../nodes/image";
-import {$createLinkNode, $isLinkNode} from "@lexical/link";
+import {$createLinkNode, $isLinkNode, LinkNode} from "@lexical/link";
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
import {$insertNodeToNearestRoot} from "@lexical/utils";
import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection";
import searchIcon from "@icons/search.svg";
import {showLinkSelector} from "../../../utils/links";
import {LinkField} from "../../framework/blocks/link-field";
+import {insertOrUpdateLink} from "../../../utils/formats";
export function $showImageForm(image: ImageNode, context: EditorUiContext) {
const imageModal: EditorFormModal = context.manager.createModal('image');
],
};
-export const link: EditorFormDefinition = {
- submitText: 'Apply',
- async action(formData, context: EditorUiContext) {
- context.editor.update(() => {
+export function $showLinkForm(link: LinkNode|null, context: EditorUiContext) {
+ const linkModal = context.manager.createModal('link');
- const url = formData.get('url')?.toString() || '';
- const title = formData.get('title')?.toString() || ''
- const target = formData.get('target')?.toString() || '';
- const text = formData.get('text')?.toString() || '';
+ let formDefaults = {};
+ if (link) {
+ formDefaults = {
+ url: link.getURL(),
+ text: link.getTextContent(),
+ title: link.getTitle(),
+ target: link.getTarget(),
+ }
- const selection = $getSelection();
- let link = $getNodeFromSelection(selection, $isLinkNode);
- if ($isLinkNode(link)) {
- link.setURL(url);
- link.setTarget(target);
- link.setTitle(title);
- } else {
- link = $createLinkNode(url, {
- title: title,
- target: target,
- });
+ context.editor.update(() => {
+ const selection = $createNodeSelection();
+ selection.add(link.getKey());
+ $setSelection(selection);
+ });
+ }
- $insertNodes([link]);
- }
+ linkModal.show(formDefaults);
+}
- if ($isLinkNode(link)) {
- for (const child of link.getChildren()) {
- child.remove(true);
- }
- link.append($createTextNode(text));
- }
+export const link: EditorFormDefinition = {
+ submitText: 'Apply',
+ async action(formData, context: EditorUiContext) {
+ insertOrUpdateLink(context.editor, {
+ url: formData.get('url')?.toString() || '',
+ title: formData.get('title')?.toString() || '',
+ target: formData.get('target')?.toString() || '',
+ text: formData.get('text')?.toString() || '',
});
return true;
},
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
-import {$getSelection, LexicalEditor, LexicalNode} from "lexical";
+import {$createTextNode, $getSelection, $insertNodes, LexicalEditor, LexicalNode} from "lexical";
import {
$getBlockElementNodesInSelection,
$getNodeFromSelection,
- $insertNewBlockNodeAtSelection,
+ $insertNewBlockNodeAtSelection, $selectionContainsNodeType,
$toggleSelectionBlockNodeType,
getLastSelection
} from "./selection";
import {$createCustomQuoteNode} from "../nodes/custom-quote";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
+import {insertList, ListNode, ListType, removeList} from "@lexical/list";
+import {$isCustomListNode} from "../nodes/custom-list";
+import {$createLinkNode, $isLinkNode} from "@lexical/link";
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
return $isCustomHeadingNode(node) && (node as HeadingNode).getTag() === tag;
});
}
+export function toggleSelectionAsList(editor: LexicalEditor, type: ListType) {
+ editor.getEditorState().read(() => {
+ const selection = $getSelection();
+ const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
+ return $isCustomListNode(node) && (node as ListNode).getListType() === type;
+ });
+
+ if (listSelected) {
+ removeList(editor);
+ } else {
+ insertList(editor, type);
+ }
+ });
+}
+
export function formatCodeBlock(editor: LexicalEditor) {
editor.getEditorState().read(() => {
const selection = $getSelection();
}
}
});
+}
+
+export function insertOrUpdateLink(editor: LexicalEditor, linkDetails: {text: string, title: string, target: string, url: string}) {
+ editor.update(() => {
+ const selection = $getSelection();
+ let link = $getNodeFromSelection(selection, $isLinkNode);
+ if ($isLinkNode(link)) {
+ link.setURL(linkDetails.url);
+ link.setTarget(linkDetails.target);
+ link.setTitle(linkDetails.title);
+ } else {
+ link = $createLinkNode(linkDetails.url, {
+ title: linkDetails.title,
+ target: linkDetails.target,
+ });
+
+ $insertNodes([link]);
+ }
+
+ if ($isLinkNode(link)) {
+ for (const child of link.getChildren()) {
+ child.remove(true);
+ }
+ link.append($createTextNode(linkDetails.text));
+ }
+ });
}
\ No newline at end of file