1 import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../framework/buttons";
4 $createParagraphNode, $getRoot, $getSelection,
5 $isParagraphNode, $isTextNode, $setSelection,
6 BaseSelection, ElementNode, FORMAT_TEXT_COMMAND,
8 REDO_COMMAND, TextFormatType,
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 strikethroughIcon from "@icons/editor/strikethrough.svg"
38 import superscriptIcon from "@icons/editor/superscript.svg"
39 import subscriptIcon from "@icons/editor/subscript.svg"
40 import codeIcon from "@icons/editor/code.svg"
41 import formatClearIcon from "@icons/editor/format-clear.svg"
42 import listBulletIcon from "@icons/editor/list-bullet.svg"
43 import listNumberedIcon from "@icons/editor/list-numbered.svg"
44 import listCheckIcon from "@icons/editor/list-check.svg"
45 import linkIcon from "@icons/editor/link.svg"
46 import imageIcon from "@icons/editor/image.svg"
47 import detailsIcon from "@icons/editor/details.svg"
48 import sourceIcon from "@icons/editor/source-view.svg"
50 export const undo: EditorButtonDefinition = {
53 action(context: EditorUiContext) {
54 context.editor.dispatchCommand(UNDO_COMMAND, undefined);
56 isActive(selection: BaseSelection|null): boolean {
61 export const redo: EditorButtonDefinition = {
64 action(context: EditorUiContext) {
65 context.editor.dispatchCommand(REDO_COMMAND, undefined);
67 isActive(selection: BaseSelection|null): boolean {
72 function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
74 label: `${name} Callout`,
75 action(context: EditorUiContext) {
76 toggleSelectionBlockNodeType(
78 (node) => $isCalloutNodeOfCategory(node, category),
79 () => $createCalloutNode(category),
82 isActive(selection: BaseSelection|null): boolean {
83 return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
88 export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
89 export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
90 export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
91 export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
93 const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
94 return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
97 function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
100 action(context: EditorUiContext) {
101 toggleSelectionBlockNodeType(
103 (node) => isHeaderNodeOfTag(node, tag),
104 () => $createHeadingNode(tag),
107 isActive(selection: BaseSelection|null): boolean {
108 return selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
113 export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
114 export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
115 export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
116 export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
118 export const blockquote: EditorButtonDefinition = {
120 action(context: EditorUiContext) {
121 toggleSelectionBlockNodeType(context.editor, $isQuoteNode, $createQuoteNode);
123 isActive(selection: BaseSelection|null): boolean {
124 return selectionContainsNodeType(selection, $isQuoteNode);
128 export const paragraph: EditorButtonDefinition = {
130 action(context: EditorUiContext) {
131 toggleSelectionBlockNodeType(context.editor, $isParagraphNode, $createParagraphNode);
133 isActive(selection: BaseSelection|null): boolean {
134 return selectionContainsNodeType(selection, $isParagraphNode);
138 function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
142 action(context: EditorUiContext) {
143 context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
145 isActive(selection: BaseSelection|null): boolean {
146 return selectionContainsTextFormat(selection, format);
151 export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
152 export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
153 export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
154 export const textColor: EditorBasicButtonDefinition = {label: 'Text color'};
155 export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color'};
157 export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
158 export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
159 export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
160 export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
161 export const clearFormating: EditorButtonDefinition = {
162 label: 'Clear formatting',
163 icon: formatClearIcon,
164 action(context: EditorUiContext) {
165 context.editor.update(() => {
166 const selection = $getSelection();
167 for (const node of selection?.getNodes() || []) {
168 if ($isTextNode(node)) {
179 function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
183 action(context: EditorUiContext) {
184 context.editor.getEditorState().read(() => {
185 const selection = $getSelection();
186 if (this.isActive(selection)) {
187 removeList(context.editor);
189 insertList(context.editor, type);
193 isActive(selection: BaseSelection|null): boolean {
194 return selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
195 return $isListNode(node) && (node as ListNode).getListType() === type;
201 export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
202 export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
203 export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
206 export const link: EditorButtonDefinition = {
207 label: 'Insert/edit link',
209 action(context: EditorUiContext) {
210 const linkModal = context.manager.createModal('link');
211 context.editor.getEditorState().read(() => {
212 const selection = $getSelection();
213 const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
215 let formDefaults = {};
218 url: selectedLink.getURL(),
219 text: selectedLink.getTextContent(),
220 title: selectedLink.getTitle(),
221 target: selectedLink.getTarget(),
224 context.editor.update(() => {
225 const selection = $createNodeSelection();
226 selection.add(selectedLink.getKey());
227 $setSelection(selection);
231 linkModal.show(formDefaults);
234 isActive(selection: BaseSelection|null): boolean {
235 return selectionContainsNodeType(selection, $isLinkNode);
239 export const image: EditorButtonDefinition = {
240 label: 'Insert/Edit Image',
242 action(context: EditorUiContext) {
243 const imageModal = context.manager.createModal('image');
244 const selection = context.lastSelection;
245 const selectedImage = getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
247 context.editor.getEditorState().read(() => {
248 let formDefaults = {};
251 src: selectedImage.getSrc(),
252 alt: selectedImage.getAltText(),
253 height: selectedImage.getHeight(),
254 width: selectedImage.getWidth(),
257 context.editor.update(() => {
258 const selection = $createNodeSelection();
259 selection.add(selectedImage.getKey());
260 $setSelection(selection);
264 imageModal.show(formDefaults);
267 isActive(selection: BaseSelection|null): boolean {
268 return selectionContainsNodeType(selection, $isImageNode);
272 export const details: EditorButtonDefinition = {
273 label: 'Insert collapsible block',
275 action(context: EditorUiContext) {
276 context.editor.update(() => {
277 const selection = $getSelection();
278 const detailsNode = $createDetailsNode();
279 const selectionNodes = selection?.getNodes() || [];
280 const topLevels = selectionNodes.map(n => n.getTopLevelElement())
281 .filter(n => n !== null) as ElementNode[];
282 const uniqueTopLevels = [...new Set(topLevels)];
284 if (uniqueTopLevels.length > 0) {
285 uniqueTopLevels[0].insertAfter(detailsNode);
287 $getRoot().append(detailsNode);
290 for (const node of uniqueTopLevels) {
291 detailsNode.append(node);
295 isActive(selection: BaseSelection|null): boolean {
296 return selectionContainsNodeType(selection, $isDetailsNode);
300 export const source: EditorButtonDefinition = {
301 label: 'Source code',
303 async action(context: EditorUiContext) {
304 const modal = context.manager.createModal('source');
305 const source = await getEditorContentAsHtml(context.editor);
306 modal.show({source});