Lexical: Updated task list to use/support old format
This commit is contained in:
		
							parent
							
								
									fe05cff64f
								
							
						
					
					
						commit
						13f8f39dd5
					
				|  | @ -10,6 +10,7 @@ import {el} from "./helpers"; | |||
| import {EditorUiContext} from "./ui/framework/core"; | ||||
| import {listen as listenToCommonEvents} from "./common-events"; | ||||
| import {handleDropEvents} from "./drop-handling"; | ||||
| import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler"; | ||||
| 
 | ||||
| export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface { | ||||
|     const config: CreateEditorArgs = { | ||||
|  | @ -47,6 +48,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st | |||
|         registerRichText(editor), | ||||
|         registerHistory(editor, createEmptyHistoryState(), 300), | ||||
|         registerTableResizer(editor, editWrap), | ||||
|         registerTaskListHandler(editor, editArea), | ||||
|     ); | ||||
| 
 | ||||
|     listenToCommonEvents(editor); | ||||
|  |  | |||
|  | @ -0,0 +1,92 @@ | |||
| import {$isListNode, ListItemNode, ListNode, SerializedListItemNode} from "@lexical/list"; | ||||
| import {EditorConfig} from "lexical/LexicalEditor"; | ||||
| import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical"; | ||||
| import {el} from "../helpers"; | ||||
| 
 | ||||
| function updateListItemChecked( | ||||
|     dom: HTMLElement, | ||||
|     listItemNode: ListItemNode, | ||||
| ): void { | ||||
|     // Only set task list attrs for leaf list items
 | ||||
|     const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild()); | ||||
|     dom.classList.toggle('task-list-item', shouldBeTaskItem); | ||||
|     if (listItemNode.__checked) { | ||||
|         dom.setAttribute('checked', 'checked'); | ||||
|     } else { | ||||
|         dom.removeAttribute('checked'); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class CustomListItemNode extends ListItemNode { | ||||
|     static getType(): string { | ||||
|         return 'custom-list-item'; | ||||
|     } | ||||
| 
 | ||||
|     static clone(node: CustomListItemNode): CustomListItemNode { | ||||
|         return new CustomListItemNode(node.__value, node.__checked, node.__key); | ||||
|     } | ||||
| 
 | ||||
|     createDOM(config: EditorConfig): HTMLElement { | ||||
|         const element = document.createElement('li'); | ||||
|         const parent = this.getParent(); | ||||
| 
 | ||||
|         if ($isListNode(parent) && parent.getListType() === 'check') { | ||||
|             updateListItemChecked(element, this); | ||||
|         } | ||||
| 
 | ||||
|         element.value = this.__value; | ||||
| 
 | ||||
|         return element; | ||||
|     } | ||||
| 
 | ||||
|     updateDOM( | ||||
|         prevNode: ListItemNode, | ||||
|         dom: HTMLElement, | ||||
|         config: EditorConfig, | ||||
|     ): boolean { | ||||
|         const parent = this.getParent(); | ||||
|         if ($isListNode(parent) && parent.getListType() === 'check') { | ||||
|             updateListItemChecked(dom, this); | ||||
|         } | ||||
|         // @ts-expect-error - this is always HTMLListItemElement
 | ||||
|         dom.value = this.__value; | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     exportDOM(editor: LexicalEditor): DOMExportOutput { | ||||
|         const element = this.createDOM(editor._config); | ||||
|         element.style.textAlign = this.getFormatType(); | ||||
| 
 | ||||
|         if (element.classList.contains('task-list-item')) { | ||||
|             const input = el('input', { | ||||
|                 type: 'checkbox', | ||||
|                 disabled: 'disabled', | ||||
|             }); | ||||
|             if (element.hasAttribute('checked')) { | ||||
|                 input.setAttribute('checked', 'checked'); | ||||
|                 element.removeAttribute('checked'); | ||||
|             } | ||||
| 
 | ||||
|             element.prepend(input); | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             element, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     exportJSON(): SerializedListItemNode { | ||||
|         return { | ||||
|             ...super.exportJSON(), | ||||
|             type: 'custom-list-item', | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function $isCustomListItemNode( | ||||
|     node: LexicalNode | null | undefined, | ||||
| ): node is CustomListItemNode { | ||||
|     return node instanceof CustomListItemNode; | ||||
| } | ||||
|  | @ -19,6 +19,7 @@ import {CodeBlockNode} from "./code-block"; | |||
| import {DiagramNode} from "./diagram"; | ||||
| import {EditorUiContext} from "../ui/framework/core"; | ||||
| import {MediaNode} from "./media"; | ||||
| import {CustomListItemNode} from "./custom-list-item"; | ||||
| 
 | ||||
| /** | ||||
|  * Load the nodes for lexical. | ||||
|  | @ -29,7 +30,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | | |||
|         HeadingNode, // Todo - Create custom
 | ||||
|         QuoteNode, // Todo - Create custom
 | ||||
|         ListNode, // Todo - Create custom
 | ||||
|         ListItemNode, | ||||
|         CustomListItemNode, | ||||
|         CustomTableNode, | ||||
|         TableRowNode, | ||||
|         TableCellNode, | ||||
|  | @ -53,6 +54,12 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | | |||
|                 return new CustomTableNode(); | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             replace: ListItemNode, | ||||
|             with: (node: ListItemNode) => { | ||||
|                 return new CustomListItemNode(node.__value, node.__checked); | ||||
|             } | ||||
|         } | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ | |||
| - Image paste upload | ||||
| - Keyboard shortcuts support | ||||
| - Add ID support to all block types | ||||
| - Task list render/import from existing format | ||||
| - Link popup menu for cross-content reference | ||||
| - Link heading-based ID reference menu | ||||
| - Image gallery integration for insert | ||||
|  |  | |||
|  | @ -0,0 +1,59 @@ | |||
| import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical"; | ||||
| import {$isCustomListItemNode} from "../../../nodes/custom-list-item"; | ||||
| 
 | ||||
| class TaskListHandler { | ||||
|     protected editorContainer: HTMLElement; | ||||
|     protected editor: LexicalEditor; | ||||
| 
 | ||||
|     constructor(editor: LexicalEditor, editorContainer: HTMLElement) { | ||||
|         this.editor = editor; | ||||
|         this.editorContainer = editorContainer; | ||||
|         this.setupListeners(); | ||||
|     } | ||||
| 
 | ||||
|     protected setupListeners() { | ||||
|         this.handleClick = this.handleClick.bind(this); | ||||
|         this.editorContainer.addEventListener('click', this.handleClick); | ||||
|     } | ||||
| 
 | ||||
|     handleClick(event: MouseEvent) { | ||||
|         const target = event.target; | ||||
|         if (target instanceof HTMLElement && target.nodeName === 'LI' && target.classList.contains('task-list-item')) { | ||||
|             this.handleTaskListItemClick(target, event); | ||||
|             event.preventDefault(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     handleTaskListItemClick(listItem: HTMLElement, event: MouseEvent) { | ||||
|         const bounds = listItem.getBoundingClientRect(); | ||||
|         const withinBounds = event.clientX <= bounds.right | ||||
|             && event.clientX >= bounds.left | ||||
|             && event.clientY >= bounds.top | ||||
|             && event.clientY <= bounds.bottom; | ||||
| 
 | ||||
|         // Outside task list item bounds means we're probably clicking the pseudo-element
 | ||||
|         if (withinBounds) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.editor.update(() => { | ||||
|             const node = $getNearestNodeFromDOMNode(listItem); | ||||
|             if ($isCustomListItemNode(node)) { | ||||
|                 node.setChecked(!node.getChecked()); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     teardown() { | ||||
|         this.editorContainer.removeEventListener('click', this.handleClick); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export function registerTaskListHandler(editor: LexicalEditor, editorContainer: HTMLElement): (() => void) { | ||||
|     const handler = new TaskListHandler(editor, editorContainer); | ||||
| 
 | ||||
|     return () => { | ||||
|         handler.teardown(); | ||||
|     }; | ||||
| } | ||||
|  | @ -324,6 +324,37 @@ body.editor-is-fullscreen { | |||
|   outline: 2px dashed var(--editor-color-primary); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fake task list checkboxes | ||||
|  */ | ||||
| .editor-content-area .task-list-item { | ||||
|   margin-left: 0; | ||||
|   position: relative; | ||||
| } | ||||
| .editor-content-area .task-list-item > input[type="checkbox"] { | ||||
|   display: none; | ||||
| } | ||||
| .editor-content-area .task-list-item:before { | ||||
|   content: ''; | ||||
|   display: inline-block; | ||||
|   border: 2px solid #CCC; | ||||
|   width: 12px; | ||||
|   height: 12px; | ||||
|   border-radius: 2px; | ||||
|   margin-right: 8px; | ||||
|   vertical-align: text-top; | ||||
|   cursor: pointer; | ||||
|   position: absolute; | ||||
|   left: -24px; | ||||
|   top: 4px; | ||||
| } | ||||
| .editor-content-area .task-list-item[checked]:before { | ||||
|   background-color: #CCC; | ||||
|   background-image: url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m8.4856 20.274-6.736-6.736 2.9287-2.7823 3.8073 3.8073 10.836-10.836 2.9287 2.9287z" stroke-width="1.4644"/></svg>'); | ||||
|   background-position: 50% 50%; | ||||
|   background-size: 100% 100%; | ||||
| } | ||||
| 
 | ||||
| // Editor form elements | ||||
| .editor-form-field-wrapper { | ||||
|   margin-bottom: .5rem; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue