1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-28 17:02:04 +03:00

Lexical: Updated task list to use/support old format

This commit is contained in:
Dan Brown
2024-07-30 14:42:19 +01:00
parent fe05cff64f
commit 13f8f39dd5
6 changed files with 192 additions and 2 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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);
}
}
];
}

View File

@ -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

View File

@ -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();
};
}