mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
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);
|
||||
|
92
resources/js/wysiwyg/nodes/custom-list-item.ts
Normal file
92
resources/js/wysiwyg/nodes/custom-list-item.ts
Normal 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;
|
||||
}
|
@ -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
Block a user