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 = {
registerRichText(editor),
registerHistory(editor, createEmptyHistoryState(), 300),
registerTableResizer(editor, editWrap),
+ registerTaskListHandler(editor, editArea),
);
listenToCommonEvents(editor);
--- /dev/null
+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;
+}
\ No newline at end of file
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.
HeadingNode, // Todo - Create custom
QuoteNode, // Todo - Create custom
ListNode, // Todo - Create custom
- ListItemNode,
+ CustomListItemNode,
CustomTableNode,
TableRowNode,
TableCellNode,
return new CustomTableNode();
}
},
+ {
+ replace: ListItemNode,
+ with: (node: ListItemNode) => {
+ return new CustomListItemNode(node.__value, node.__checked);
+ }
+ }
];
}
- 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
--- /dev/null
+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();
+ };
+}
\ No newline at end of file
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;