Lexical: Added table toolbar, organised button code
1
resources/icons/editor/table-delete-column.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14c1.1 0 2 .9 2 2zm-2 0V5h-4v2.2h-2V5h-2v2.2H9V5H5v14h4v-2.1h2V19h2v-2.1h2V19Z"/><path d="M14.829 10.585 13.415 12l1.414 1.414c.943.943-.472 2.357-1.414 1.414L12 13.414l-1.414 1.414c-.944.944-2.358-.47-1.414-1.414L10.586 12l-1.414-1.415c-.943-.942.471-2.357 1.414-1.414L12 10.585l1.344-1.343c1.111-1.112 2.2.627 1.485 1.343z" style="fill-rule:nonzero"/></svg>
|
After Width: | Height: | Size: 481 B |
1
resources/icons/editor/table-delete-row.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14v-4h-2.2v-2H19v-2h-2.2V9H19V5H5v4h2.1v2H5v2h2.1v2H5Z"/><path d="M13.415 14.829 12 13.415l-1.414 1.414c-.943.943-2.357-.472-1.414-1.414L10.586 12l-1.414-1.414c-.944-.944.47-2.358 1.414-1.414L12 10.586l1.415-1.414c.942-.943 2.357.471 1.414 1.414L13.415 12l1.343 1.344c1.112 1.111-.627 2.2-1.343 1.485z" style="fill-rule:nonzero"/></svg>
|
After Width: | Height: | Size: 481 B |
1
resources/icons/editor/table-delete.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14V5H5v14z"/><path d="m13.711 15.423-1.71-1.712-1.712 1.712c-1.14 1.14-2.852-.57-1.71-1.712l1.71-1.71-1.71-1.712c-1.143-1.142.568-2.853 1.71-1.71L12 10.288l1.711-1.71c1.141-1.142 2.852.57 1.712 1.71L13.71 12l1.626 1.626c1.345 1.345-.76 2.663-1.626 1.797z" style="fill-rule:nonzero;stroke-width:1.20992"/></svg>
|
After Width: | Height: | Size: 455 B |
1
resources/icons/editor/table-insert-column-after.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 5h-5v14h5c1.235 0 1.234 2 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11c1.229 0 1.236 2 0 2zm-7 6V5H5v6zm0 8v-6H5v6zm11.076-6h-2v2c0 1.333-2 1.333-2 0v-2h-2c-1.335 0-1.335-2 0-2h2V9c0-1.333 2-1.333 2 0v2h1.9c1.572 0 1.113 2 .1 2z"/></svg>
|
After Width: | Height: | Size: 304 B |
1
resources/icons/editor/table-insert-column-before.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 19h5V5H8C6.764 5 6.766 3 8 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8c-1.229 0-1.236-2 0-2zm7-6v6h4v-6zm0-8v6h4V5ZM3.924 11h2V9c0-1.333 2-1.333 2 0v2h2c1.335 0 1.335 2 0 2h-2v2c0 1.333-2 1.333-2 0v-2h-1.9c-1.572 0-1.113-2-.1-2z"/></svg>
|
After Width: | Height: | Size: 303 B |
1
resources/icons/editor/table-insert-row-above.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 8v5h14V8c0-1.235 2-1.234 2 0v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8C3 6.77 5 6.764 5 8zm6 7H5v4h6zm8 0h-6v4h6zM13 3.924v2h2c1.333 0 1.333 2 0 2h-2v2c0 1.335-2 1.335-2 0v-2H9c-1.333 0-1.333-2 0-2h2v-1.9c0-1.572 2-1.113 2-.1z"/></svg>
|
After Width: | Height: | Size: 300 B |
1
resources/icons/editor/table-insert-row-below.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 16v-5H5v5c0 1.235-2 1.234-2 0V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11c0 1.229-2 1.236-2 0zm-6-7h6V5h-6zM5 9h6V5H5Zm6 11.076v-2H9c-1.333 0-1.333-2 0-2h2v-2c0-1.335 2-1.335 2 0v2h2c1.333 0 1.333 2 0 2h-2v1.9c0 1.572-2 1.113-2 .1z"/></svg>
|
After Width: | Height: | Size: 305 B |
@ -44,10 +44,19 @@ export function $getNodeFromSelection(selection: BaseSelection|null, matcher: Le
|
||||
return node;
|
||||
}
|
||||
|
||||
for (const parent of node.getParents()) {
|
||||
if (matcher(parent)) {
|
||||
return parent;
|
||||
}
|
||||
const matchedParent = $getParentOfType(node, matcher);
|
||||
if (matchedParent) {
|
||||
return matchedParent;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode|null {
|
||||
for (const parent of node.getParents()) {
|
||||
if (matcher(parent)) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,22 @@
|
||||
|
||||
## In progress
|
||||
|
||||
- Add Type: Video/media/embed
|
||||
- TinyMce media embed supported:
|
||||
- iframe
|
||||
- embed
|
||||
- object
|
||||
- video - Can take sources
|
||||
- audio - Can take sources
|
||||
- Pretty much all attributes look like they were supported.
|
||||
- Core old logic seen here: https://github.com/tinymce/tinymce/blob/main/modules/tinymce/src/plugins/media/main/ts/core/DataToHtml.ts
|
||||
- Copy/store attributes on node based on allow list?
|
||||
- width, height, src, controls, etc... Take valid values from MDN
|
||||
|
||||
## Main Todo
|
||||
|
||||
- Alignments: Use existing classes for blocks
|
||||
- Alignments: Handle inline block content (image, video)
|
||||
- Add Type: Video/media/embed
|
||||
- Table features
|
||||
- Image paste upload
|
||||
- Keyboard shortcuts support
|
||||
|
@ -1,528 +0,0 @@
|
||||
import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons";
|
||||
import {
|
||||
$createNodeSelection,
|
||||
$createParagraphNode,
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$isParagraphNode,
|
||||
$isTextNode,
|
||||
$setSelection,
|
||||
BaseSelection,
|
||||
CAN_REDO_COMMAND,
|
||||
CAN_UNDO_COMMAND,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
ElementFormatType,
|
||||
ElementNode,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
LexicalNode,
|
||||
REDO_COMMAND,
|
||||
TextFormatType,
|
||||
UNDO_COMMAND
|
||||
} from "lexical";
|
||||
import {
|
||||
$getBlockElementNodesInSelection,
|
||||
$getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsElementFormat,
|
||||
$selectionContainsNodeType,
|
||||
$selectionContainsTextFormat,
|
||||
$toggleSelectionBlockNodeType
|
||||
} from "../../helpers";
|
||||
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
|
||||
import {
|
||||
$createHeadingNode,
|
||||
$createQuoteNode,
|
||||
$isHeadingNode,
|
||||
$isQuoteNode,
|
||||
HeadingNode,
|
||||
HeadingTagType
|
||||
} from "@lexical/rich-text";
|
||||
import {$isLinkNode, LinkNode} from "@lexical/link";
|
||||
import {EditorUiContext} from "../framework/core";
|
||||
import {$isImageNode, ImageNode} from "../../nodes/image";
|
||||
import {$createDetailsNode, $isDetailsNode} from "../../nodes/details";
|
||||
import {getEditorContentAsHtml} from "../../actions";
|
||||
import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
|
||||
import undoIcon from "@icons/editor/undo.svg";
|
||||
import redoIcon from "@icons/editor/redo.svg";
|
||||
import boldIcon from "@icons/editor/bold.svg";
|
||||
import italicIcon from "@icons/editor/italic.svg";
|
||||
import underlinedIcon from "@icons/editor/underlined.svg";
|
||||
import textColorIcon from "@icons/editor/text-color.svg";
|
||||
import highlightIcon from "@icons/editor/highlighter.svg";
|
||||
import strikethroughIcon from "@icons/editor/strikethrough.svg";
|
||||
import superscriptIcon from "@icons/editor/superscript.svg";
|
||||
import subscriptIcon from "@icons/editor/subscript.svg";
|
||||
import codeIcon from "@icons/editor/code.svg";
|
||||
import formatClearIcon from "@icons/editor/format-clear.svg";
|
||||
import alignLeftIcon from "@icons/editor/align-left.svg";
|
||||
import alignCenterIcon from "@icons/editor/align-center.svg";
|
||||
import alignRightIcon from "@icons/editor/align-right.svg";
|
||||
import alignJustifyIcon from "@icons/editor/align-justify.svg";
|
||||
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 linkIcon from "@icons/editor/link.svg";
|
||||
import unlinkIcon from "@icons/editor/unlink.svg";
|
||||
import tableIcon from "@icons/editor/table.svg";
|
||||
import imageIcon from "@icons/editor/image.svg";
|
||||
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
|
||||
import codeBlockIcon from "@icons/editor/code-block.svg";
|
||||
import diagramIcon from "@icons/editor/diagram.svg";
|
||||
import detailsIcon from "@icons/editor/details.svg";
|
||||
import sourceIcon from "@icons/editor/source-view.svg";
|
||||
import fullscreenIcon from "@icons/editor/fullscreen.svg";
|
||||
import editIcon from "@icons/edit.svg";
|
||||
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
|
||||
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
|
||||
import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
|
||||
|
||||
export const undo: EditorButtonDefinition = {
|
||||
label: 'Undo',
|
||||
icon: undoIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.dispatchCommand(UNDO_COMMAND, undefined);
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return false;
|
||||
},
|
||||
setup(context: EditorUiContext, button: EditorButton) {
|
||||
button.toggleDisabled(true);
|
||||
|
||||
context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => {
|
||||
button.toggleDisabled(!payload)
|
||||
return false;
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
export const redo: EditorButtonDefinition = {
|
||||
label: 'Redo',
|
||||
icon: redoIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.dispatchCommand(REDO_COMMAND, undefined);
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return false;
|
||||
},
|
||||
setup(context: EditorUiContext, button: EditorButton) {
|
||||
button.toggleDisabled(true);
|
||||
|
||||
context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => {
|
||||
button.toggleDisabled(!payload)
|
||||
return false;
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
|
||||
return {
|
||||
label: `${name} Callout`,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType(
|
||||
(node) => $isCalloutNodeOfCategory(node, category),
|
||||
() => $createCalloutNode(category),
|
||||
)
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
|
||||
export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
|
||||
export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
|
||||
export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
|
||||
|
||||
const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
|
||||
return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
|
||||
};
|
||||
|
||||
function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
|
||||
return {
|
||||
label: name,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType(
|
||||
(node) => isHeaderNodeOfTag(node, tag),
|
||||
() => $createHeadingNode(tag),
|
||||
)
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
|
||||
export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
|
||||
export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
|
||||
export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
|
||||
|
||||
export const blockquote: EditorButtonDefinition = {
|
||||
label: 'Blockquote',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isQuoteNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const paragraph: EditorButtonDefinition = {
|
||||
label: 'Paragraph',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isParagraphNode);
|
||||
}
|
||||
}
|
||||
|
||||
function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
|
||||
return {
|
||||
label: label,
|
||||
icon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsTextFormat(selection, format);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
|
||||
export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
|
||||
export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
|
||||
export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
|
||||
export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};
|
||||
|
||||
export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
|
||||
export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
|
||||
export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
|
||||
export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
|
||||
export const clearFormating: EditorButtonDefinition = {
|
||||
label: 'Clear formatting',
|
||||
icon: formatClearIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
for (const node of selection?.getNodes() || []) {
|
||||
if ($isTextNode(node)) {
|
||||
node.setFormat(0);
|
||||
node.setStyle('');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function setAlignmentForSection(alignment: ElementFormatType): void {
|
||||
const selection = $getSelection();
|
||||
const elements = $getBlockElementNodesInSelection(selection);
|
||||
for (const node of elements) {
|
||||
node.setFormat(alignment);
|
||||
}
|
||||
}
|
||||
|
||||
export const alignLeft: EditorButtonDefinition = {
|
||||
label: 'Align left',
|
||||
icon: alignLeftIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('left'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'left');
|
||||
}
|
||||
};
|
||||
|
||||
export const alignCenter: EditorButtonDefinition = {
|
||||
label: 'Align center',
|
||||
icon: alignCenterIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('center'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'center');
|
||||
}
|
||||
};
|
||||
|
||||
export const alignRight: EditorButtonDefinition = {
|
||||
label: 'Align right',
|
||||
icon: alignRightIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('right'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'right');
|
||||
}
|
||||
};
|
||||
|
||||
export const alignJustify: EditorButtonDefinition = {
|
||||
label: 'Align justify',
|
||||
icon: alignJustifyIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('justify'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'justify');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
|
||||
return {
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
|
||||
return $isListNode(node) && (node as ListNode).getListType() === type;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
|
||||
export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
|
||||
export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
|
||||
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isLinkNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const unlink: EditorButtonDefinition = {
|
||||
label: 'Remove link',
|
||||
icon: unlinkIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = context.lastSelection;
|
||||
const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
|
||||
const selectionPoints = selection?.getStartEndPoints();
|
||||
|
||||
if (selectedLink) {
|
||||
const newNode = $createTextNode(selectedLink.getTextContent());
|
||||
selectedLink.replace(newNode);
|
||||
if (selectionPoints?.length === 2) {
|
||||
newNode.select(selectionPoints[0].offset, selectionPoints[1].offset);
|
||||
} else {
|
||||
newNode.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const table: EditorBasicButtonDefinition = {
|
||||
label: 'Table',
|
||||
icon: tableIcon,
|
||||
};
|
||||
|
||||
export const image: EditorButtonDefinition = {
|
||||
label: 'Insert/Edit Image',
|
||||
icon: imageIcon,
|
||||
action(context: EditorUiContext) {
|
||||
const imageModal = context.manager.createModal('image');
|
||||
const selection = context.lastSelection;
|
||||
const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
|
||||
|
||||
context.editor.getEditorState().read(() => {
|
||||
let formDefaults = {};
|
||||
if (selectedImage) {
|
||||
formDefaults = {
|
||||
src: selectedImage.getSrc(),
|
||||
alt: selectedImage.getAltText(),
|
||||
height: selectedImage.getHeight(),
|
||||
width: selectedImage.getWidth(),
|
||||
}
|
||||
|
||||
context.editor.update(() => {
|
||||
const selection = $createNodeSelection();
|
||||
selection.add(selectedImage.getKey());
|
||||
$setSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
imageModal.show(formDefaults);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isImageNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const horizontalRule: EditorButtonDefinition = {
|
||||
label: 'Insert horizontal line',
|
||||
icon: horizontalRuleIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const codeBlock: EditorButtonDefinition = {
|
||||
label: 'Insert code block',
|
||||
icon: codeBlockIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const selection = $getSelection();
|
||||
const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
|
||||
if (codeBlock === null) {
|
||||
context.editor.update(() => {
|
||||
const codeBlock = $createCodeBlockNode();
|
||||
codeBlock.setCode(selection?.getTextContent() || '');
|
||||
$insertNewBlockNodeAtSelection(codeBlock, true);
|
||||
$openCodeEditorForNode(context.editor, codeBlock);
|
||||
codeBlock.selectStart();
|
||||
});
|
||||
} else {
|
||||
$openCodeEditorForNode(context.editor, codeBlock);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isCodeBlockNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
|
||||
label: 'Edit code block',
|
||||
icon: editIcon,
|
||||
});
|
||||
|
||||
export const diagram: EditorButtonDefinition = {
|
||||
label: 'Insert/edit drawing',
|
||||
icon: diagramIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const selection = $getSelection();
|
||||
const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null);
|
||||
if (diagramNode === null) {
|
||||
context.editor.update(() => {
|
||||
const diagram = $createDiagramNode();
|
||||
$insertNewBlockNodeAtSelection(diagram, true);
|
||||
$openDrawingEditorForNode(context, diagram);
|
||||
diagram.selectStart();
|
||||
});
|
||||
} else {
|
||||
$openDrawingEditorForNode(context, diagramNode);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isDiagramNode);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const details: EditorButtonDefinition = {
|
||||
label: 'Insert collapsible block',
|
||||
icon: detailsIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
const detailsNode = $createDetailsNode();
|
||||
const selectionNodes = selection?.getNodes() || [];
|
||||
const topLevels = selectionNodes.map(n => n.getTopLevelElement())
|
||||
.filter(n => n !== null) as ElementNode[];
|
||||
const uniqueTopLevels = [...new Set(topLevels)];
|
||||
|
||||
if (uniqueTopLevels.length > 0) {
|
||||
uniqueTopLevels[0].insertAfter(detailsNode);
|
||||
} else {
|
||||
$getRoot().append(detailsNode);
|
||||
}
|
||||
|
||||
for (const node of uniqueTopLevels) {
|
||||
detailsNode.append(node);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isDetailsNode);
|
||||
}
|
||||
}
|
||||
|
||||
export const source: EditorButtonDefinition = {
|
||||
label: 'Source code',
|
||||
icon: sourceIcon,
|
||||
async action(context: EditorUiContext) {
|
||||
const modal = context.manager.createModal('source');
|
||||
const source = await getEditorContentAsHtml(context.editor);
|
||||
modal.show({source});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const fullscreen: EditorButtonDefinition = {
|
||||
label: 'Fullscreen',
|
||||
icon: fullscreenIcon,
|
||||
async action(context: EditorUiContext, button: EditorButton) {
|
||||
const isFullScreen = context.containerDOM.classList.contains('fullscreen');
|
||||
context.containerDOM.classList.toggle('fullscreen', !isFullScreen);
|
||||
(context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);
|
||||
button.setActiveState(!isFullScreen);
|
||||
},
|
||||
isActive(selection, context: EditorUiContext) {
|
||||
return context.containerDOM.classList.contains('fullscreen');
|
||||
}
|
||||
};
|
61
resources/js/wysiwyg/ui/defaults/buttons/alignments.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {$getSelection, BaseSelection, ElementFormatType} from "lexical";
|
||||
import {$getBlockElementNodesInSelection, $selectionContainsElementFormat} from "../../../helpers";
|
||||
import {EditorButtonDefinition} from "../../framework/buttons";
|
||||
import alignLeftIcon from "@icons/editor/align-left.svg";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import alignCenterIcon from "@icons/editor/align-center.svg";
|
||||
import alignRightIcon from "@icons/editor/align-right.svg";
|
||||
import alignJustifyIcon from "@icons/editor/align-justify.svg";
|
||||
|
||||
|
||||
function setAlignmentForSection(alignment: ElementFormatType): void {
|
||||
const selection = $getSelection();
|
||||
const elements = $getBlockElementNodesInSelection(selection);
|
||||
for (const node of elements) {
|
||||
node.setFormat(alignment);
|
||||
}
|
||||
}
|
||||
|
||||
export const alignLeft: EditorButtonDefinition = {
|
||||
label: 'Align left',
|
||||
icon: alignLeftIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('left'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'left');
|
||||
}
|
||||
};
|
||||
|
||||
export const alignCenter: EditorButtonDefinition = {
|
||||
label: 'Align center',
|
||||
icon: alignCenterIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('center'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'center');
|
||||
}
|
||||
};
|
||||
|
||||
export const alignRight: EditorButtonDefinition = {
|
||||
label: 'Align right',
|
||||
icon: alignRightIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('right'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'right');
|
||||
}
|
||||
};
|
||||
|
||||
export const alignJustify: EditorButtonDefinition = {
|
||||
label: 'Align justify',
|
||||
icon: alignJustifyIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => setAlignmentForSection('justify'));
|
||||
},
|
||||
isActive(selection: BaseSelection|null) {
|
||||
return $selectionContainsElementFormat(selection, 'justify');
|
||||
}
|
||||
};
|
85
resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout";
|
||||
import {EditorButtonDefinition} from "../../framework/buttons";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../helpers";
|
||||
import {$createParagraphNode, $isParagraphNode, BaseSelection, LexicalNode} from "lexical";
|
||||
import {
|
||||
$createHeadingNode,
|
||||
$createQuoteNode,
|
||||
$isHeadingNode,
|
||||
$isQuoteNode,
|
||||
HeadingNode,
|
||||
HeadingTagType
|
||||
} from "@lexical/rich-text";
|
||||
|
||||
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
|
||||
return {
|
||||
label: `${name} Callout`,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType(
|
||||
(node) => $isCalloutNodeOfCategory(node, category),
|
||||
() => $createCalloutNode(category),
|
||||
)
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
|
||||
export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
|
||||
export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
|
||||
export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
|
||||
|
||||
const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
|
||||
return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
|
||||
};
|
||||
|
||||
function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
|
||||
return {
|
||||
label: name,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType(
|
||||
(node) => isHeaderNodeOfTag(node, tag),
|
||||
() => $createHeadingNode(tag),
|
||||
)
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
|
||||
export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
|
||||
export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
|
||||
export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
|
||||
|
||||
export const blockquote: EditorButtonDefinition = {
|
||||
label: 'Blockquote',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isQuoteNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const paragraph: EditorButtonDefinition = {
|
||||
label: 'Paragraph',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isParagraphNode);
|
||||
}
|
||||
}
|
81
resources/js/wysiwyg/ui/defaults/buttons/controls.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import {EditorButton, EditorButtonDefinition} from "../../framework/buttons";
|
||||
import undoIcon from "@icons/editor/undo.svg";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {
|
||||
BaseSelection,
|
||||
CAN_REDO_COMMAND,
|
||||
CAN_UNDO_COMMAND,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
REDO_COMMAND,
|
||||
UNDO_COMMAND
|
||||
} from "lexical";
|
||||
import redoIcon from "@icons/editor/redo.svg";
|
||||
import sourceIcon from "@icons/editor/source-view.svg";
|
||||
import {getEditorContentAsHtml} from "../../../actions";
|
||||
import fullscreenIcon from "@icons/editor/fullscreen.svg";
|
||||
|
||||
export const undo: EditorButtonDefinition = {
|
||||
label: 'Undo',
|
||||
icon: undoIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.dispatchCommand(UNDO_COMMAND, undefined);
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return false;
|
||||
},
|
||||
setup(context: EditorUiContext, button: EditorButton) {
|
||||
button.toggleDisabled(true);
|
||||
|
||||
context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => {
|
||||
button.toggleDisabled(!payload)
|
||||
return false;
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
export const redo: EditorButtonDefinition = {
|
||||
label: 'Redo',
|
||||
icon: redoIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.dispatchCommand(REDO_COMMAND, undefined);
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return false;
|
||||
},
|
||||
setup(context: EditorUiContext, button: EditorButton) {
|
||||
button.toggleDisabled(true);
|
||||
|
||||
context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => {
|
||||
button.toggleDisabled(!payload)
|
||||
return false;
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const source: EditorButtonDefinition = {
|
||||
label: 'Source code',
|
||||
icon: sourceIcon,
|
||||
async action(context: EditorUiContext) {
|
||||
const modal = context.manager.createModal('source');
|
||||
const source = await getEditorContentAsHtml(context.editor);
|
||||
modal.show({source});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const fullscreen: EditorButtonDefinition = {
|
||||
label: 'Fullscreen',
|
||||
icon: fullscreenIcon,
|
||||
async action(context: EditorUiContext, button: EditorButton) {
|
||||
const isFullScreen = context.containerDOM.classList.contains('fullscreen');
|
||||
context.containerDOM.classList.toggle('fullscreen', !isFullScreen);
|
||||
(context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);
|
||||
button.setActiveState(!isFullScreen);
|
||||
},
|
||||
isActive(selection, context: EditorUiContext) {
|
||||
return context.containerDOM.classList.contains('fullscreen');
|
||||
}
|
||||
};
|
56
resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {$getSelection, $isTextNode, BaseSelection, FORMAT_TEXT_COMMAND, TextFormatType} from "lexical";
|
||||
import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../../framework/buttons";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {$selectionContainsTextFormat} from "../../../helpers";
|
||||
import boldIcon from "@icons/editor/bold.svg";
|
||||
import italicIcon from "@icons/editor/italic.svg";
|
||||
import underlinedIcon from "@icons/editor/underlined.svg";
|
||||
import textColorIcon from "@icons/editor/text-color.svg";
|
||||
import highlightIcon from "@icons/editor/highlighter.svg";
|
||||
import strikethroughIcon from "@icons/editor/strikethrough.svg";
|
||||
import superscriptIcon from "@icons/editor/superscript.svg";
|
||||
import subscriptIcon from "@icons/editor/subscript.svg";
|
||||
import codeIcon from "@icons/editor/code.svg";
|
||||
import formatClearIcon from "@icons/editor/format-clear.svg";
|
||||
|
||||
function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
|
||||
return {
|
||||
label: label,
|
||||
icon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsTextFormat(selection, format);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
|
||||
export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
|
||||
export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
|
||||
export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
|
||||
export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};
|
||||
|
||||
export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
|
||||
export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
|
||||
export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
|
||||
export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
|
||||
export const clearFormating: EditorButtonDefinition = {
|
||||
label: 'Clear formatting',
|
||||
icon: formatClearIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
for (const node of selection?.getNodes() || []) {
|
||||
if ($isTextNode(node)) {
|
||||
node.setFormat(0);
|
||||
node.setStyle('');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
35
resources/js/wysiwyg/ui/defaults/buttons/lists.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
|
||||
import {EditorButtonDefinition} from "../../framework/buttons";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {$getSelection, BaseSelection, LexicalNode} from "lexical";
|
||||
import {$selectionContainsNodeType} from "../../../helpers";
|
||||
import listBulletIcon from "@icons/editor/list-bullet.svg";
|
||||
import listNumberedIcon from "@icons/editor/list-numbered.svg";
|
||||
import listCheckIcon from "@icons/editor/list-check.svg";
|
||||
|
||||
|
||||
function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
|
||||
return {
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
|
||||
return $isListNode(node) && (node as ListNode).getListType() === type;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
|
||||
export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
|
||||
export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
|
215
resources/js/wysiwyg/ui/defaults/buttons/objects.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import {EditorButtonDefinition} from "../../framework/buttons";
|
||||
import linkIcon from "@icons/editor/link.svg";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {
|
||||
$createNodeSelection,
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$setSelection,
|
||||
BaseSelection,
|
||||
ElementNode
|
||||
} from "lexical";
|
||||
import {$getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsNodeType} from "../../../helpers";
|
||||
import {$isLinkNode, LinkNode} from "@lexical/link";
|
||||
import unlinkIcon from "@icons/editor/unlink.svg";
|
||||
import imageIcon from "@icons/editor/image.svg";
|
||||
import {$isImageNode, ImageNode} from "../../../nodes/image";
|
||||
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 editIcon from "@icons/edit.svg";
|
||||
import diagramIcon from "@icons/editor/diagram.svg";
|
||||
import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../../nodes/diagram";
|
||||
import detailsIcon from "@icons/editor/details.svg";
|
||||
import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details";
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isLinkNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const unlink: EditorButtonDefinition = {
|
||||
label: 'Remove link',
|
||||
icon: unlinkIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = context.lastSelection;
|
||||
const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
|
||||
const selectionPoints = selection?.getStartEndPoints();
|
||||
|
||||
if (selectedLink) {
|
||||
const newNode = $createTextNode(selectedLink.getTextContent());
|
||||
selectedLink.replace(newNode);
|
||||
if (selectionPoints?.length === 2) {
|
||||
newNode.select(selectionPoints[0].offset, selectionPoints[1].offset);
|
||||
} else {
|
||||
newNode.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const image: EditorButtonDefinition = {
|
||||
label: 'Insert/Edit Image',
|
||||
icon: imageIcon,
|
||||
action(context: EditorUiContext) {
|
||||
const imageModal = context.manager.createModal('image');
|
||||
const selection = context.lastSelection;
|
||||
const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
|
||||
|
||||
context.editor.getEditorState().read(() => {
|
||||
let formDefaults = {};
|
||||
if (selectedImage) {
|
||||
formDefaults = {
|
||||
src: selectedImage.getSrc(),
|
||||
alt: selectedImage.getAltText(),
|
||||
height: selectedImage.getHeight(),
|
||||
width: selectedImage.getWidth(),
|
||||
}
|
||||
|
||||
context.editor.update(() => {
|
||||
const selection = $createNodeSelection();
|
||||
selection.add(selectedImage.getKey());
|
||||
$setSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
imageModal.show(formDefaults);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isImageNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const horizontalRule: EditorButtonDefinition = {
|
||||
label: 'Insert horizontal line',
|
||||
icon: horizontalRuleIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const codeBlock: EditorButtonDefinition = {
|
||||
label: 'Insert code block',
|
||||
icon: codeBlockIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const selection = $getSelection();
|
||||
const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
|
||||
if (codeBlock === null) {
|
||||
context.editor.update(() => {
|
||||
const codeBlock = $createCodeBlockNode();
|
||||
codeBlock.setCode(selection?.getTextContent() || '');
|
||||
$insertNewBlockNodeAtSelection(codeBlock, true);
|
||||
$openCodeEditorForNode(context.editor, codeBlock);
|
||||
codeBlock.selectStart();
|
||||
});
|
||||
} else {
|
||||
$openCodeEditorForNode(context.editor, codeBlock);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isCodeBlockNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
|
||||
label: 'Edit code block',
|
||||
icon: editIcon,
|
||||
});
|
||||
|
||||
export const diagram: EditorButtonDefinition = {
|
||||
label: 'Insert/edit drawing',
|
||||
icon: diagramIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const selection = $getSelection();
|
||||
const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null);
|
||||
if (diagramNode === null) {
|
||||
context.editor.update(() => {
|
||||
const diagram = $createDiagramNode();
|
||||
$insertNewBlockNodeAtSelection(diagram, true);
|
||||
$openDrawingEditorForNode(context, diagram);
|
||||
diagram.selectStart();
|
||||
});
|
||||
} else {
|
||||
$openDrawingEditorForNode(context, diagramNode);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isDiagramNode);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const details: EditorButtonDefinition = {
|
||||
label: 'Insert collapsible block',
|
||||
icon: detailsIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
const detailsNode = $createDetailsNode();
|
||||
const selectionNodes = selection?.getNodes() || [];
|
||||
const topLevels = selectionNodes.map(n => n.getTopLevelElement())
|
||||
.filter(n => n !== null) as ElementNode[];
|
||||
const uniqueTopLevels = [...new Set(topLevels)];
|
||||
|
||||
if (uniqueTopLevels.length > 0) {
|
||||
uniqueTopLevels[0].insertAfter(detailsNode);
|
||||
} else {
|
||||
$getRoot().append(detailsNode);
|
||||
}
|
||||
|
||||
for (const node of uniqueTopLevels) {
|
||||
detailsNode.append(node);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive(selection: BaseSelection|null): boolean {
|
||||
return $selectionContainsNodeType(selection, $isDetailsNode);
|
||||
}
|
||||
}
|
122
resources/js/wysiwyg/ui/defaults/buttons/tables.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../../framework/buttons";
|
||||
import tableIcon from "@icons/editor/table.svg";
|
||||
import deleteIcon from "@icons/editor/table-delete.svg";
|
||||
import deleteColumnIcon from "@icons/editor/table-delete-column.svg";
|
||||
import deleteRowIcon from "@icons/editor/table-delete-row.svg";
|
||||
import insertColumnAfterIcon from "@icons/editor/table-insert-column-after.svg";
|
||||
import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg";
|
||||
import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
|
||||
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {$getBlockElementNodesInSelection, $getNodeFromSelection, $getParentOfType} from "../../../helpers";
|
||||
import {$getSelection} from "lexical";
|
||||
import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
|
||||
import {
|
||||
$deleteTableColumn, $deleteTableColumn__EXPERIMENTAL,
|
||||
$deleteTableRow__EXPERIMENTAL,
|
||||
$getTableRowIndexFromTableCellNode, $insertTableColumn, $insertTableColumn__EXPERIMENTAL,
|
||||
$insertTableRow, $insertTableRow__EXPERIMENTAL,
|
||||
$isTableCellNode,
|
||||
$isTableRowNode,
|
||||
TableCellNode
|
||||
} from "@lexical/table";
|
||||
|
||||
|
||||
export const table: EditorBasicButtonDefinition = {
|
||||
label: 'Table',
|
||||
icon: tableIcon,
|
||||
};
|
||||
|
||||
export const deleteTable: EditorButtonDefinition = {
|
||||
label: 'Delete table',
|
||||
icon: deleteIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const table = $getNodeFromSelection($getSelection(), $isCustomTableNode);
|
||||
if (table) {
|
||||
table.remove();
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const insertRowAbove: EditorButtonDefinition = {
|
||||
label: 'Insert row above',
|
||||
icon: insertRowAboveIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$insertTableRow__EXPERIMENTAL(false);
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const insertRowBelow: EditorButtonDefinition = {
|
||||
label: 'Insert row below',
|
||||
icon: insertRowBelowIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$insertTableRow__EXPERIMENTAL(true);
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRow: EditorButtonDefinition = {
|
||||
label: 'Delete row',
|
||||
icon: deleteRowIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$deleteTableRow__EXPERIMENTAL();
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const insertColumnBefore: EditorButtonDefinition = {
|
||||
label: 'Insert column before',
|
||||
icon: insertColumnBeforeIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$insertTableColumn__EXPERIMENTAL(false);
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const insertColumnAfter: EditorButtonDefinition = {
|
||||
label: 'Insert column after',
|
||||
icon: insertColumnAfterIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$insertTableColumn__EXPERIMENTAL(true);
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteColumn: EditorButtonDefinition = {
|
||||
label: 'Delete column',
|
||||
icon: deleteColumnIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$deleteTableColumn__EXPERIMENTAL();
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
@ -3,7 +3,7 @@ import {
|
||||
getCodeToolbarContent,
|
||||
getImageToolbarContent,
|
||||
getLinkToolbarContent,
|
||||
getMainEditorFullToolbar
|
||||
getMainEditorFullToolbar, getTableToolbarContent
|
||||
} from "./toolbars";
|
||||
import {EditorUIManager} from "./framework/manager";
|
||||
import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
|
||||
@ -61,6 +61,14 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
|
||||
content: getCodeToolbarContent(),
|
||||
});
|
||||
|
||||
manager.registerContextToolbar('table', {
|
||||
selector: 'td,th',
|
||||
content: getTableToolbarContent(),
|
||||
displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
|
||||
return originalTarget.closest('table') as HTMLTableElement;
|
||||
}
|
||||
});
|
||||
|
||||
// Register image decorator listener
|
||||
manager.registerDecoratorType('image', ImageDecorator);
|
||||
manager.registerDecoratorType('code', CodeBlockDecorator);
|
||||
|
@ -1,17 +1,4 @@
|
||||
import {EditorButton} from "./framework/buttons";
|
||||
import {
|
||||
alignCenter, alignJustify,
|
||||
alignLeft,
|
||||
alignRight,
|
||||
blockquote, bold, bulletList, clearFormating, code, codeBlock,
|
||||
dangerCallout, details, diagram, editCodeBlock, fullscreen,
|
||||
h2, h3, h4, h5, highlightColor, horizontalRule, image,
|
||||
infoCallout, italic, link, numberList, paragraph,
|
||||
redo, source, strikethrough, subscript,
|
||||
successCallout, superscript, table, taskList, textColor, underline,
|
||||
undo, unlink,
|
||||
warningCallout
|
||||
} from "./defaults/button-definitions";
|
||||
import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core";
|
||||
import {el} from "../helpers";
|
||||
import {EditorFormatMenu} from "./framework/blocks/format-menu";
|
||||
@ -21,6 +8,48 @@ import {EditorColorPicker} from "./framework/blocks/color-picker";
|
||||
import {EditorTableCreator} from "./framework/blocks/table-creator";
|
||||
import {EditorColorButton} from "./framework/blocks/color-button";
|
||||
import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
|
||||
import {
|
||||
deleteColumn,
|
||||
deleteRow,
|
||||
deleteTable, insertColumnAfter,
|
||||
insertColumnBefore,
|
||||
insertRowAbove,
|
||||
insertRowBelow,
|
||||
table
|
||||
} from "./defaults/buttons/tables";
|
||||
import {fullscreen, redo, source, undo} from "./defaults/buttons/controls";
|
||||
import {
|
||||
blockquote, dangerCallout,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
infoCallout,
|
||||
paragraph,
|
||||
successCallout,
|
||||
warningCallout
|
||||
} from "./defaults/buttons/block-formats";
|
||||
import {
|
||||
bold, clearFormating, code,
|
||||
highlightColor,
|
||||
italic,
|
||||
strikethrough, subscript,
|
||||
superscript,
|
||||
textColor,
|
||||
underline
|
||||
} from "./defaults/buttons/inline-formats";
|
||||
import {alignCenter, alignJustify, alignLeft, alignRight} from "./defaults/buttons/alignments";
|
||||
import {bulletList, numberList, taskList} from "./defaults/buttons/lists";
|
||||
import {
|
||||
codeBlock,
|
||||
details,
|
||||
diagram,
|
||||
editCodeBlock,
|
||||
horizontalRule,
|
||||
image,
|
||||
link,
|
||||
unlink
|
||||
} from "./defaults/buttons/objects";
|
||||
|
||||
export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
||||
return new EditorSimpleClassContainer('editor-toolbar-main', [
|
||||
@ -129,4 +158,23 @@ export function getCodeToolbarContent(): EditorUiElement[] {
|
||||
return [
|
||||
new EditorButton(editCodeBlock),
|
||||
];
|
||||
}
|
||||
|
||||
export function getTableToolbarContent(): EditorUiElement[] {
|
||||
return [
|
||||
new EditorOverflowContainer(2, [
|
||||
// Todo - Table properties
|
||||
new EditorButton(deleteTable),
|
||||
]),
|
||||
new EditorOverflowContainer(3, [
|
||||
new EditorButton(insertRowAbove),
|
||||
new EditorButton(insertRowBelow),
|
||||
new EditorButton(deleteRow),
|
||||
]),
|
||||
new EditorOverflowContainer(3, [
|
||||
new EditorButton(insertColumnBefore),
|
||||
new EditorButton(insertColumnAfter),
|
||||
new EditorButton(deleteColumn),
|
||||
]),
|
||||
];
|
||||
}
|