1 import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons";
24 getBlockElementNodesInSelection,
25 getNodeFromSelection, insertNewBlockNodeAtSelection, selectionContainsElementFormat,
26 selectionContainsNodeType,
27 selectionContainsTextFormat,
28 toggleSelectionBlockNodeType
29 } from "../../helpers";
30 import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
38 } from "@lexical/rich-text";
39 import {$isLinkNode, LinkNode} from "@lexical/link";
40 import {EditorUiContext} from "../framework/core";
41 import {$isImageNode, ImageNode} from "../../nodes/image";
42 import {$createDetailsNode, $isDetailsNode} from "../../nodes/details";
43 import {getEditorContentAsHtml} from "../../actions";
44 import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
45 import undoIcon from "@icons/editor/undo.svg";
46 import redoIcon from "@icons/editor/redo.svg";
47 import boldIcon from "@icons/editor/bold.svg";
48 import italicIcon from "@icons/editor/italic.svg";
49 import underlinedIcon from "@icons/editor/underlined.svg";
50 import textColorIcon from "@icons/editor/text-color.svg";
51 import highlightIcon from "@icons/editor/highlighter.svg";
52 import strikethroughIcon from "@icons/editor/strikethrough.svg";
53 import superscriptIcon from "@icons/editor/superscript.svg";
54 import subscriptIcon from "@icons/editor/subscript.svg";
55 import codeIcon from "@icons/editor/code.svg";
56 import formatClearIcon from "@icons/editor/format-clear.svg";
57 import alignLeftIcon from "@icons/editor/align-left.svg";
58 import alignCenterIcon from "@icons/editor/align-center.svg";
59 import alignRightIcon from "@icons/editor/align-right.svg";
60 import alignJustifyIcon from "@icons/editor/align-justify.svg";
61 import listBulletIcon from "@icons/editor/list-bullet.svg";
62 import listNumberedIcon from "@icons/editor/list-numbered.svg";
63 import listCheckIcon from "@icons/editor/list-check.svg";
64 import linkIcon from "@icons/editor/link.svg";
65 import unlinkIcon from "@icons/editor/unlink.svg";
66 import tableIcon from "@icons/editor/table.svg";
67 import imageIcon from "@icons/editor/image.svg";
68 import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
69 import codeBlockIcon from "@icons/editor/code-block.svg";
70 import detailsIcon from "@icons/editor/details.svg";
71 import sourceIcon from "@icons/editor/source-view.svg";
72 import fullscreenIcon from "@icons/editor/fullscreen.svg";
73 import editIcon from "@icons/edit.svg";
74 import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
75 import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
77 export const undo: EditorButtonDefinition = {
80 action(context: EditorUiContext) {
81 context.editor.dispatchCommand(UNDO_COMMAND, undefined);
83 isActive(selection: BaseSelection|null): boolean {
86 setup(context: EditorUiContext, button: EditorButton) {
87 button.toggleDisabled(true);
89 context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => {
90 button.toggleDisabled(!payload)
92 }, COMMAND_PRIORITY_LOW);
96 export const redo: EditorButtonDefinition = {
99 action(context: EditorUiContext) {
100 context.editor.dispatchCommand(REDO_COMMAND, undefined);
102 isActive(selection: BaseSelection|null): boolean {
105 setup(context: EditorUiContext, button: EditorButton) {
106 button.toggleDisabled(true);
108 context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => {
109 button.toggleDisabled(!payload)
111 }, COMMAND_PRIORITY_LOW);
115 function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
117 label: `${name} Callout`,
118 action(context: EditorUiContext) {
119 toggleSelectionBlockNodeType(
121 (node) => $isCalloutNodeOfCategory(node, category),
122 () => $createCalloutNode(category),
125 isActive(selection: BaseSelection|null): boolean {
126 return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
131 export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
132 export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
133 export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
134 export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
136 const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
137 return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
140 function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
143 action(context: EditorUiContext) {
144 toggleSelectionBlockNodeType(
146 (node) => isHeaderNodeOfTag(node, tag),
147 () => $createHeadingNode(tag),
150 isActive(selection: BaseSelection|null): boolean {
151 return selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
156 export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
157 export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
158 export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
159 export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
161 export const blockquote: EditorButtonDefinition = {
163 action(context: EditorUiContext) {
164 toggleSelectionBlockNodeType(context.editor, $isQuoteNode, $createQuoteNode);
166 isActive(selection: BaseSelection|null): boolean {
167 return selectionContainsNodeType(selection, $isQuoteNode);
171 export const paragraph: EditorButtonDefinition = {
173 action(context: EditorUiContext) {
174 toggleSelectionBlockNodeType(context.editor, $isParagraphNode, $createParagraphNode);
176 isActive(selection: BaseSelection|null): boolean {
177 return selectionContainsNodeType(selection, $isParagraphNode);
181 function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
185 action(context: EditorUiContext) {
186 context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
188 isActive(selection: BaseSelection|null): boolean {
189 return selectionContainsTextFormat(selection, format);
194 export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
195 export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
196 export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
197 export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
198 export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};
200 export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
201 export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
202 export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
203 export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
204 export const clearFormating: EditorButtonDefinition = {
205 label: 'Clear formatting',
206 icon: formatClearIcon,
207 action(context: EditorUiContext) {
208 context.editor.update(() => {
209 const selection = $getSelection();
210 for (const node of selection?.getNodes() || []) {
211 if ($isTextNode(node)) {
223 function setAlignmentForSection(alignment: ElementFormatType): void {
224 const selection = $getSelection();
225 const elements = getBlockElementNodesInSelection(selection);
226 for (const node of elements) {
227 node.setFormat(alignment);
231 export const alignLeft: EditorButtonDefinition = {
234 action(context: EditorUiContext) {
235 context.editor.update(() => setAlignmentForSection('left'));
237 isActive(selection: BaseSelection|null) {
238 return selectionContainsElementFormat(selection, 'left');
242 export const alignCenter: EditorButtonDefinition = {
243 label: 'Align center',
244 icon: alignCenterIcon,
245 action(context: EditorUiContext) {
246 context.editor.update(() => setAlignmentForSection('center'));
248 isActive(selection: BaseSelection|null) {
249 return selectionContainsElementFormat(selection, 'center');
253 export const alignRight: EditorButtonDefinition = {
254 label: 'Align right',
255 icon: alignRightIcon,
256 action(context: EditorUiContext) {
257 context.editor.update(() => setAlignmentForSection('right'));
259 isActive(selection: BaseSelection|null) {
260 return selectionContainsElementFormat(selection, 'right');
264 export const alignJustify: EditorButtonDefinition = {
265 label: 'Align justify',
266 icon: alignJustifyIcon,
267 action(context: EditorUiContext) {
268 context.editor.update(() => setAlignmentForSection('justify'));
270 isActive(selection: BaseSelection|null) {
271 return selectionContainsElementFormat(selection, 'justify');
276 function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
280 action(context: EditorUiContext) {
281 context.editor.getEditorState().read(() => {
282 const selection = $getSelection();
283 if (this.isActive(selection, context)) {
284 removeList(context.editor);
286 insertList(context.editor, type);
290 isActive(selection: BaseSelection|null): boolean {
291 return selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
292 return $isListNode(node) && (node as ListNode).getListType() === type;
298 export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
299 export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
300 export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
303 export const link: EditorButtonDefinition = {
304 label: 'Insert/edit link',
306 action(context: EditorUiContext) {
307 const linkModal = context.manager.createModal('link');
308 context.editor.getEditorState().read(() => {
309 const selection = $getSelection();
310 const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
312 let formDefaults = {};
315 url: selectedLink.getURL(),
316 text: selectedLink.getTextContent(),
317 title: selectedLink.getTitle(),
318 target: selectedLink.getTarget(),
321 context.editor.update(() => {
322 const selection = $createNodeSelection();
323 selection.add(selectedLink.getKey());
324 $setSelection(selection);
328 linkModal.show(formDefaults);
331 isActive(selection: BaseSelection|null): boolean {
332 return selectionContainsNodeType(selection, $isLinkNode);
336 export const unlink: EditorButtonDefinition = {
337 label: 'Remove link',
339 action(context: EditorUiContext) {
340 context.editor.update(() => {
341 const selection = context.lastSelection;
342 const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
343 const selectionPoints = selection?.getStartEndPoints();
346 const newNode = $createTextNode(selectedLink.getTextContent());
347 selectedLink.replace(newNode);
348 if (selectionPoints?.length === 2) {
349 newNode.select(selectionPoints[0].offset, selectionPoints[1].offset);
356 isActive(selection: BaseSelection|null): boolean {
361 export const table: EditorBasicButtonDefinition = {
366 export const image: EditorButtonDefinition = {
367 label: 'Insert/Edit Image',
369 action(context: EditorUiContext) {
370 const imageModal = context.manager.createModal('image');
371 const selection = context.lastSelection;
372 const selectedImage = getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
374 context.editor.getEditorState().read(() => {
375 let formDefaults = {};
378 src: selectedImage.getSrc(),
379 alt: selectedImage.getAltText(),
380 height: selectedImage.getHeight(),
381 width: selectedImage.getWidth(),
384 context.editor.update(() => {
385 const selection = $createNodeSelection();
386 selection.add(selectedImage.getKey());
387 $setSelection(selection);
391 imageModal.show(formDefaults);
394 isActive(selection: BaseSelection|null): boolean {
395 return selectionContainsNodeType(selection, $isImageNode);
399 export const horizontalRule: EditorButtonDefinition = {
400 label: 'Insert horizontal line',
401 icon: horizontalRuleIcon,
402 action(context: EditorUiContext) {
403 context.editor.update(() => {
404 insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
407 isActive(selection: BaseSelection|null): boolean {
408 return selectionContainsNodeType(selection, $isHorizontalRuleNode);
412 export const codeBlock: EditorButtonDefinition = {
413 label: 'Insert code block',
415 action(context: EditorUiContext) {
416 context.editor.getEditorState().read(() => {
417 const selection = $getSelection();
418 const codeBlock = getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
419 if (codeBlock === null) {
420 context.editor.update(() => {
421 const codeBlock = $createCodeBlockNode();
422 codeBlock.setCode(selection?.getTextContent() || '');
423 insertNewBlockNodeAtSelection(codeBlock, true);
424 $openCodeEditorForNode(context.editor, codeBlock);
425 codeBlock.selectStart();
428 $openCodeEditorForNode(context.editor, codeBlock);
432 isActive(selection: BaseSelection|null): boolean {
433 return selectionContainsNodeType(selection, $isCodeBlockNode);
437 export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
438 label: 'Edit code block',
442 export const details: EditorButtonDefinition = {
443 label: 'Insert collapsible block',
445 action(context: EditorUiContext) {
446 context.editor.update(() => {
447 const selection = $getSelection();
448 const detailsNode = $createDetailsNode();
449 const selectionNodes = selection?.getNodes() || [];
450 const topLevels = selectionNodes.map(n => n.getTopLevelElement())
451 .filter(n => n !== null) as ElementNode[];
452 const uniqueTopLevels = [...new Set(topLevels)];
454 if (uniqueTopLevels.length > 0) {
455 uniqueTopLevels[0].insertAfter(detailsNode);
457 $getRoot().append(detailsNode);
460 for (const node of uniqueTopLevels) {
461 detailsNode.append(node);
465 isActive(selection: BaseSelection|null): boolean {
466 return selectionContainsNodeType(selection, $isDetailsNode);
470 export const source: EditorButtonDefinition = {
471 label: 'Source code',
473 async action(context: EditorUiContext) {
474 const modal = context.manager.createModal('source');
475 const source = await getEditorContentAsHtml(context.editor);
476 modal.show({source});
483 export const fullscreen: EditorButtonDefinition = {
485 icon: fullscreenIcon,
486 async action(context: EditorUiContext, button: EditorButton) {
487 const isFullScreen = context.containerDOM.classList.contains('fullscreen');
488 context.containerDOM.classList.toggle('fullscreen', !isFullScreen);
489 (context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);
490 button.setActiveState(!isFullScreen);
492 isActive(selection, context: EditorUiContext) {
493 return context.containerDOM.classList.contains('fullscreen');