1 import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons";
4 $createParagraphNode, $createTextNode, $getRoot, $getSelection,
5 $isParagraphNode, $isTextNode, $setSelection,
6 BaseSelection, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, ElementNode, FORMAT_TEXT_COMMAND,
8 REDO_COMMAND, TextFormatType,
12 getNodeFromSelection, insertNewBlockNodeAtSelection,
13 selectionContainsNodeType,
14 selectionContainsTextFormat,
15 toggleSelectionBlockNodeType
16 } from "../../helpers";
17 import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
25 } from "@lexical/rich-text";
26 import {$isLinkNode, LinkNode} from "@lexical/link";
27 import {EditorUiContext} from "../framework/core";
28 import {$isImageNode, ImageNode} from "../../nodes/image";
29 import {$createDetailsNode, $isDetailsNode} from "../../nodes/details";
30 import {getEditorContentAsHtml} from "../../actions";
31 import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
32 import undoIcon from "@icons/editor/undo.svg"
33 import redoIcon from "@icons/editor/redo.svg"
34 import boldIcon from "@icons/editor/bold.svg"
35 import italicIcon from "@icons/editor/italic.svg"
36 import underlinedIcon from "@icons/editor/underlined.svg"
37 import textColorIcon from "@icons/editor/text-color.svg";
38 import highlightIcon from "@icons/editor/highlighter.svg";
39 import strikethroughIcon from "@icons/editor/strikethrough.svg"
40 import superscriptIcon from "@icons/editor/superscript.svg"
41 import subscriptIcon from "@icons/editor/subscript.svg"
42 import codeIcon from "@icons/editor/code.svg"
43 import formatClearIcon from "@icons/editor/format-clear.svg"
44 import listBulletIcon from "@icons/editor/list-bullet.svg"
45 import listNumberedIcon from "@icons/editor/list-numbered.svg"
46 import listCheckIcon from "@icons/editor/list-check.svg"
47 import linkIcon from "@icons/editor/link.svg"
48 import unlinkIcon from "@icons/editor/unlink.svg"
49 import tableIcon from "@icons/editor/table.svg"
50 import imageIcon from "@icons/editor/image.svg"
51 import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"
52 import codeBlockIcon from "@icons/editor/code-block.svg"
53 import detailsIcon from "@icons/editor/details.svg"
54 import sourceIcon from "@icons/editor/source-view.svg"
55 import fullscreenIcon from "@icons/editor/fullscreen.svg"
56 import editIcon from "@icons/edit.svg"
57 import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
58 import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
60 export const undo: EditorButtonDefinition = {
63 action(context: EditorUiContext) {
64 context.editor.dispatchCommand(UNDO_COMMAND, undefined);
66 isActive(selection: BaseSelection|null): boolean {
69 setup(context: EditorUiContext, button: EditorButton) {
70 button.toggleDisabled(true);
72 context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => {
73 button.toggleDisabled(!payload)
75 }, COMMAND_PRIORITY_LOW);
79 export const redo: EditorButtonDefinition = {
82 action(context: EditorUiContext) {
83 context.editor.dispatchCommand(REDO_COMMAND, undefined);
85 isActive(selection: BaseSelection|null): boolean {
88 setup(context: EditorUiContext, button: EditorButton) {
89 button.toggleDisabled(true);
91 context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => {
92 button.toggleDisabled(!payload)
94 }, COMMAND_PRIORITY_LOW);
98 function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
100 label: `${name} Callout`,
101 action(context: EditorUiContext) {
102 toggleSelectionBlockNodeType(
104 (node) => $isCalloutNodeOfCategory(node, category),
105 () => $createCalloutNode(category),
108 isActive(selection: BaseSelection|null): boolean {
109 return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
114 export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
115 export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
116 export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
117 export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
119 const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
120 return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
123 function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
126 action(context: EditorUiContext) {
127 toggleSelectionBlockNodeType(
129 (node) => isHeaderNodeOfTag(node, tag),
130 () => $createHeadingNode(tag),
133 isActive(selection: BaseSelection|null): boolean {
134 return selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
139 export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
140 export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
141 export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
142 export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
144 export const blockquote: EditorButtonDefinition = {
146 action(context: EditorUiContext) {
147 toggleSelectionBlockNodeType(context.editor, $isQuoteNode, $createQuoteNode);
149 isActive(selection: BaseSelection|null): boolean {
150 return selectionContainsNodeType(selection, $isQuoteNode);
154 export const paragraph: EditorButtonDefinition = {
156 action(context: EditorUiContext) {
157 toggleSelectionBlockNodeType(context.editor, $isParagraphNode, $createParagraphNode);
159 isActive(selection: BaseSelection|null): boolean {
160 return selectionContainsNodeType(selection, $isParagraphNode);
164 function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
168 action(context: EditorUiContext) {
169 context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
171 isActive(selection: BaseSelection|null): boolean {
172 return selectionContainsTextFormat(selection, format);
177 export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
178 export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
179 export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
180 export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
181 export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};
183 export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
184 export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
185 export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
186 export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
187 export const clearFormating: EditorButtonDefinition = {
188 label: 'Clear formatting',
189 icon: formatClearIcon,
190 action(context: EditorUiContext) {
191 context.editor.update(() => {
192 const selection = $getSelection();
193 for (const node of selection?.getNodes() || []) {
194 if ($isTextNode(node)) {
206 function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
210 action(context: EditorUiContext) {
211 context.editor.getEditorState().read(() => {
212 const selection = $getSelection();
213 if (this.isActive(selection, context)) {
214 removeList(context.editor);
216 insertList(context.editor, type);
220 isActive(selection: BaseSelection|null): boolean {
221 return selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
222 return $isListNode(node) && (node as ListNode).getListType() === type;
228 export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
229 export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
230 export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
233 export const link: EditorButtonDefinition = {
234 label: 'Insert/edit link',
236 action(context: EditorUiContext) {
237 const linkModal = context.manager.createModal('link');
238 context.editor.getEditorState().read(() => {
239 const selection = $getSelection();
240 const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
242 let formDefaults = {};
245 url: selectedLink.getURL(),
246 text: selectedLink.getTextContent(),
247 title: selectedLink.getTitle(),
248 target: selectedLink.getTarget(),
251 context.editor.update(() => {
252 const selection = $createNodeSelection();
253 selection.add(selectedLink.getKey());
254 $setSelection(selection);
258 linkModal.show(formDefaults);
261 isActive(selection: BaseSelection|null): boolean {
262 return selectionContainsNodeType(selection, $isLinkNode);
266 export const unlink: EditorButtonDefinition = {
267 label: 'Remove link',
269 action(context: EditorUiContext) {
270 context.editor.update(() => {
271 const selection = context.lastSelection;
272 const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
273 const selectionPoints = selection?.getStartEndPoints();
276 const newNode = $createTextNode(selectedLink.getTextContent());
277 selectedLink.replace(newNode);
278 if (selectionPoints?.length === 2) {
279 newNode.select(selectionPoints[0].offset, selectionPoints[1].offset);
286 isActive(selection: BaseSelection|null): boolean {
291 export const table: EditorBasicButtonDefinition = {
296 export const image: EditorButtonDefinition = {
297 label: 'Insert/Edit Image',
299 action(context: EditorUiContext) {
300 const imageModal = context.manager.createModal('image');
301 const selection = context.lastSelection;
302 const selectedImage = getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
304 context.editor.getEditorState().read(() => {
305 let formDefaults = {};
308 src: selectedImage.getSrc(),
309 alt: selectedImage.getAltText(),
310 height: selectedImage.getHeight(),
311 width: selectedImage.getWidth(),
314 context.editor.update(() => {
315 const selection = $createNodeSelection();
316 selection.add(selectedImage.getKey());
317 $setSelection(selection);
321 imageModal.show(formDefaults);
324 isActive(selection: BaseSelection|null): boolean {
325 return selectionContainsNodeType(selection, $isImageNode);
329 export const horizontalRule: EditorButtonDefinition = {
330 label: 'Insert horizontal line',
331 icon: horizontalRuleIcon,
332 action(context: EditorUiContext) {
333 context.editor.update(() => {
334 insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
337 isActive(selection: BaseSelection|null): boolean {
338 return selectionContainsNodeType(selection, $isHorizontalRuleNode);
342 export const codeBlock: EditorButtonDefinition = {
343 label: 'Insert code block',
345 action(context: EditorUiContext) {
346 context.editor.getEditorState().read(() => {
347 const selection = $getSelection();
348 const codeBlock = getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
349 if (codeBlock === null) {
350 context.editor.update(() => {
351 const codeBlock = $createCodeBlockNode();
352 codeBlock.setCode(selection?.getTextContent() || '');
353 insertNewBlockNodeAtSelection(codeBlock, true);
354 $openCodeEditorForNode(context.editor, codeBlock);
355 codeBlock.selectStart();
358 $openCodeEditorForNode(context.editor, codeBlock);
362 isActive(selection: BaseSelection|null): boolean {
363 return selectionContainsNodeType(selection, $isCodeBlockNode);
367 export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
368 label: 'Edit code block',
372 export const details: EditorButtonDefinition = {
373 label: 'Insert collapsible block',
375 action(context: EditorUiContext) {
376 context.editor.update(() => {
377 const selection = $getSelection();
378 const detailsNode = $createDetailsNode();
379 const selectionNodes = selection?.getNodes() || [];
380 const topLevels = selectionNodes.map(n => n.getTopLevelElement())
381 .filter(n => n !== null) as ElementNode[];
382 const uniqueTopLevels = [...new Set(topLevels)];
384 if (uniqueTopLevels.length > 0) {
385 uniqueTopLevels[0].insertAfter(detailsNode);
387 $getRoot().append(detailsNode);
390 for (const node of uniqueTopLevels) {
391 detailsNode.append(node);
395 isActive(selection: BaseSelection|null): boolean {
396 return selectionContainsNodeType(selection, $isDetailsNode);
400 export const source: EditorButtonDefinition = {
401 label: 'Source code',
403 async action(context: EditorUiContext) {
404 const modal = context.manager.createModal('source');
405 const source = await getEditorContentAsHtml(context.editor);
406 modal.show({source});
413 export const fullscreen: EditorButtonDefinition = {
415 icon: fullscreenIcon,
416 async action(context: EditorUiContext, button: EditorButton) {
417 const isFullScreen = context.containerDOM.classList.contains('fullscreen');
418 context.containerDOM.classList.toggle('fullscreen', !isFullScreen);
419 (context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);
420 button.setActiveState(!isFullScreen);
422 isActive(selection, context: EditorUiContext) {
423 return context.containerDOM.classList.contains('fullscreen');