UNPKG

@udecode/plate-list

Version:

List plugin for Plate

866 lines (832 loc) 24 kB
// src/react/ListPlugin.tsx import { toPlatePlugin } from "@udecode/plate/react"; // src/lib/BaseListPlugin.tsx import React from "react"; import { createTSlatePlugin, isDefined as isDefined6, isHtmlBlockElement, KEYS as KEYS19, postCleanHtml, traverseHtmlElements } from "@udecode/plate"; // src/lib/queries/areEqListStyleType.ts import { KEYS } from "@udecode/plate"; // src/lib/types.ts var ULIST_STYLE_TYPES = [ "disc" /* Disc */, "circle" /* Circle */, "square" /* Square */, "disclosure-open" /* DisclosureOpen */, "disclosure-closed" /* DisclosureClosed */ ]; // src/lib/queries/areEqListStyleType.ts var areEqListStyleType = (editor, entries, { listStyleType = "disc" /* Disc */ }) => { let eqListStyleType = true; for (const entry of entries) { const [block] = entry; if (listStyleType === KEYS.listTodo) { if (!block.hasOwnProperty(KEYS.listChecked)) { eqListStyleType = false; break; } continue; } if (!block[KEYS.listType] || block[KEYS.listType] !== listStyleType) { eqListStyleType = false; break; } } return eqListStyleType; }; // src/lib/queries/getListAbove.ts import { isDefined, KEYS as KEYS2 } from "@udecode/plate"; var getListAbove = (editor, options) => { return editor.api.above({ ...options, match: (node) => isDefined(node[KEYS2.listType]) }); }; // src/lib/queries/getListSiblings.ts import { KEYS as KEYS4 } from "@udecode/plate"; // src/lib/queries/getNextList.ts import { NodeApi, PathApi } from "@udecode/plate"; // src/lib/queries/getSiblingList.ts import { isDefined as isDefined2, KEYS as KEYS3 } from "@udecode/plate"; var getSiblingList = (editor, [node, path], { breakOnEqIndentNeqListStyleType = true, breakOnListRestart = false, breakOnLowerIndent = true, breakQuery, eqIndent = true, getNextEntry, getPreviousEntry, query }) => { if (!getPreviousEntry && !getNextEntry) return; const getSiblingEntry = getNextEntry ?? getPreviousEntry; let nextEntry = getSiblingEntry([node, path]); while (true) { if (!nextEntry) return; const [nextNode, nextPath] = nextEntry; const indent = node[KEYS3.indent]; const nextIndent = nextNode[KEYS3.indent]; if (breakQuery?.(nextNode, node)) return; if (!isDefined2(nextIndent)) return; if (breakOnListRestart) { if (getPreviousEntry && node[KEYS3.listRestart]) { return; } if (getNextEntry && nextNode[KEYS3.listRestart]) { return; } } if (breakOnLowerIndent && nextIndent < indent) return; if (breakOnEqIndentNeqListStyleType && nextIndent === indent && nextNode[KEYS3.listType] !== node[KEYS3.listType]) return; let valid = !query || query(nextNode, node); if (valid) { valid = !eqIndent || nextIndent === indent; if (valid) return [nextNode, nextPath]; } nextEntry = getSiblingEntry(nextEntry); } }; // src/lib/queries/getNextList.ts var getNextList = (editor, entry, options) => { return getSiblingList(editor, entry, { getNextEntry: ([, currPath]) => { const nextPath = PathApi.next(currPath); const nextNode = NodeApi.get(editor, nextPath); if (!nextNode) return; return [nextNode, nextPath]; }, ...options, getPreviousEntry: void 0 }); }; // src/lib/queries/getPreviousList.ts import { NodeApi as NodeApi2, PathApi as PathApi2 } from "@udecode/plate"; var getPreviousList = (editor, entry, options) => { return getSiblingList(editor, entry, { getPreviousEntry: ([, currPath]) => { const prevPath = PathApi2.previous(currPath); if (!prevPath) return; const prevNode = NodeApi2.get(editor, prevPath); if (!prevNode) return; return [prevNode, prevPath]; }, ...options, getNextEntry: void 0 }); }; // src/lib/queries/getListSiblings.ts var getListSiblings = (editor, entry, { current = true, next = true, previous = true, ...options } = {}) => { const siblings = []; const node = entry[0]; if (!node[KEYS4.listType] && !node.hasOwnProperty(KEYS4.listChecked)) { return siblings; } let iterEntry = entry; if (previous) { while (true) { const prevEntry = getPreviousList(editor, iterEntry, options); if (!prevEntry) break; siblings.push(prevEntry); iterEntry = prevEntry; } } if (current) { siblings.push(entry); } if (next) { iterEntry = entry; while (true) { const nextEntry = getNextList(editor, iterEntry, options); if (!nextEntry) break; siblings.push(nextEntry); iterEntry = nextEntry; } } return siblings; }; // src/lib/queries/isOrderedList.ts function isOrderedList(element) { return !!element.listStyleType && !ULIST_STYLE_TYPES.includes(element.listStyleType); } // src/lib/queries/someList.ts import { KEYS as KEYS5 } from "@udecode/plate"; var someList = (editor, type) => { return !!editor.selection && editor.api.some({ match: (n) => { const isHasProperty = n.hasOwnProperty(KEYS5.listChecked); if (isHasProperty) { return false; } const list = n[KEYS5.listType]; return Array.isArray(type) ? type.includes(list) : list === type; } }); }; // src/lib/queries/someTodoList.ts import { KEYS as KEYS6 } from "@udecode/plate"; var someTodoList = (editor) => { return editor.api.some({ at: editor.selection, match: (n) => { const list = n[KEYS6.listType]; const isHasProperty = n.hasOwnProperty(KEYS6.listChecked); return n.type === "p" && isHasProperty && list === KEYS6.listTodo; } }); }; // src/lib/withList.ts import { KEYS as KEYS18, PathApi as PathApi3 } from "@udecode/plate"; // src/lib/normalizers/normalizeListNotIndented.ts import { isDefined as isDefined3, KEYS as KEYS7 } from "@udecode/plate"; var normalizeListNotIndented = (editor, [node, path]) => { if (!isDefined3(node[KEYS7.indent]) && (node[KEYS7.listType] || node[KEYS7.listStart])) { editor.tf.unsetNodes([KEYS7.listType, KEYS7.listStart], { at: path }); return true; } }; // src/lib/normalizers/normalizeListStart.ts import { isDefined as isDefined4, KEYS as KEYS8 } from "@udecode/plate"; var getListExpectedListStart = (entry, prevEntry) => { const [node] = entry; const [prevNode] = prevEntry ?? [null]; const restart = node[KEYS8.listRestart] ?? null; const restartPolite = node[KEYS8.listRestartPolite] ?? null; if (restart) { return restart; } if (restartPolite && !prevNode) { return restartPolite; } if (prevNode) { const prevListStart = prevNode[KEYS8.listStart] ?? 1; return prevListStart + 1; } return 1; }; var normalizeListStart = (editor, entry, options) => { return editor.tf.withoutNormalizing(() => { const [node, path] = entry; const listStyleType = node[KEYS8.listType]; const listStart = node[KEYS8.listStart]; if (!listStyleType) return; const prevEntry = getPreviousList(editor, entry, options); const expectedListStart = getListExpectedListStart(entry, prevEntry); if (isDefined4(listStart) && expectedListStart === 1) { editor.tf.unsetNodes(KEYS8.listStart, { at: path }); return true; } if (listStart !== expectedListStart && expectedListStart > 1) { editor.tf.setNodes({ [KEYS8.listStart]: expectedListStart }, { at: path }); return true; } return false; }); }; // src/lib/normalizers/withInsertBreakList.ts import { isDefined as isDefined5, KEYS as KEYS9 } from "@udecode/plate"; var withInsertBreakList = ({ editor, tf: { insertBreak } }) => { return { transforms: { insertBreak() { const nodeEntry = editor.api.above(); if (!nodeEntry) return insertBreak(); const [node, path] = nodeEntry; if (!isDefined5(node[KEYS9.listType]) || node[KEYS9.listType] !== KEYS9.listTodo || editor.api.isExpanded() || !editor.api.isEnd(editor.selection?.focus, path)) { return insertBreak(); } editor.tf.withoutNormalizing(() => { insertBreak(); const newEntry = editor.api.above(); if (newEntry) { editor.tf.setNodes( { checked: false }, { at: newEntry[1] } ); } }); } } }; }; // src/lib/transforms/indentList.ts import { KEYS as KEYS10 } from "@udecode/plate"; import { setIndent } from "@udecode/plate-indent"; var indentList = (editor, { listStyleType = "disc" /* Disc */, ...options } = {}) => { setIndent(editor, { offset: 1, setNodesProps: () => ({ [KEYS10.listType]: listStyleType }), ...options }); }; var indentTodo = (editor, { listStyleType = "disc" /* Disc */, ...options } = {}) => { setIndent(editor, { offset: 1, setNodesProps: () => ({ [KEYS10.listChecked]: false, [KEYS10.listType]: listStyleType }), ...options }); }; // src/lib/transforms/outdentList.ts import { KEYS as KEYS11 } from "@udecode/plate"; import { setIndent as setIndent2 } from "@udecode/plate-indent"; var outdentList = (editor, options = {}) => { setIndent2(editor, { offset: -1, unsetNodesProps: [KEYS11.listType, KEYS11.listChecked], ...options }); }; // src/lib/transforms/setListNode.ts import { KEYS as KEYS12 } from "@udecode/plate"; var setListNode = (editor, { at, indent = 0, listStyleType = "disc" /* Disc */ }) => { const newIndent = indent || indent + 1; editor.tf.setNodes( { [KEYS12.indent]: newIndent, [KEYS12.listType]: listStyleType }, { at } ); }; var setIndentTodoNode = (editor, { at, indent = 0, listStyleType = KEYS12.listTodo }) => { const newIndent = indent || indent + 1; editor.tf.setNodes( { [KEYS12.indent]: newIndent, [KEYS12.listChecked]: false, [KEYS12.listType]: listStyleType }, { at } ); }; // src/lib/transforms/setListNodes.ts import { KEYS as KEYS13 } from "@udecode/plate"; var setListNodes = (editor, entries, { listStyleType = "disc" /* Disc */ }) => { editor.tf.withoutNormalizing(() => { entries.forEach((entry) => { const [node, path] = entry; let indent = node[KEYS13.indent] ?? 0; indent = node[KEYS13.listType] || node.hasOwnProperty(KEYS13.listChecked) ? indent : indent + 1; if (listStyleType === "todo") { editor.tf.unsetNodes(KEYS13.listType, { at: path }); setIndentTodoNode(editor, { at: path, indent, listStyleType }); return; } editor.tf.unsetNodes(KEYS13.listChecked, { at: path }); setListNode(editor, { at: path, indent, listStyleType }); }); }); }; // src/lib/transforms/setListSiblingNodes.ts import { KEYS as KEYS14 } from "@udecode/plate"; var setListSiblingNodes = (editor, entry, { getSiblingListOptions, listStyleType = "disc" /* Disc */ }) => { editor.tf.withoutNormalizing(() => { const siblings = getListSiblings(editor, entry, getSiblingListOptions); siblings.forEach(([node, path]) => { if (listStyleType === KEYS14.listTodo) { editor.tf.unsetNodes(KEYS14.listType, { at: path }); setIndentTodoNode(editor, { at: path, indent: node[KEYS14.indent], listStyleType }); } else { editor.tf.unsetNodes(KEYS14.listChecked, { at: path }); setListNode(editor, { at: path, indent: node[KEYS14.indent], listStyleType }); } }); }); }; // src/lib/transforms/toggleList.ts import { KEYS as KEYS17 } from "@udecode/plate"; // src/lib/transforms/toggleListSet.ts import { KEYS as KEYS15 } from "@udecode/plate"; var toggleListSet = (editor, [node, _path], { listStyleType = "disc" /* Disc */, ...options }) => { if (node.hasOwnProperty(KEYS15.listChecked) || node[KEYS15.listType]) return; if (listStyleType === "todo") { indentTodo(editor, { listStyleType, ...options }); } else { indentList(editor, { listStyleType, ...options }); } return true; }; // src/lib/transforms/toggleListUnset.ts import { KEYS as KEYS16 } from "@udecode/plate"; var toggleListUnset = (editor, [node, path], { listStyleType = "disc" /* Disc */ }) => { if (listStyleType === KEYS16.listTodo && node.hasOwnProperty(KEYS16.listChecked)) { editor.tf.unsetNodes(KEYS16.listChecked, { at: path }); outdentList(editor, { listStyleType }); return true; } if (listStyleType === node[KEYS16.listType]) { editor.tf.unsetNodes([KEYS16.listType], { at: path }); outdentList(editor, { listStyleType }); return true; } }; // src/lib/transforms/toggleList.ts var toggleList = (editor, options, getSiblingListOptions) => { const { listRestart, listRestartPolite, listStyleType } = options; const setList = (() => { const { getSiblingListOptions: _getSiblingListOptions } = editor.getOptions(BaseListPlugin); if (editor.api.isCollapsed()) { const entry = editor.api.block(); if (!entry) return null; if (toggleListSet(editor, entry, options)) { return true; } if (toggleListUnset(editor, entry, { listStyleType })) { return false; } setListSiblingNodes(editor, entry, { getSiblingListOptions: { ..._getSiblingListOptions, ...getSiblingListOptions }, listStyleType }); return true; } if (editor.api.isExpanded()) { const _entries = editor.api.nodes({ block: true }); const entries = [..._entries]; const eqListStyleType = areEqListStyleType(editor, entries, { listStyleType }); if (eqListStyleType) { editor.tf.withoutNormalizing(() => { entries.forEach((entry) => { const [node, path] = entry; const indent = node[KEYS17.indent]; editor.tf.unsetNodes(KEYS17.listType, { at: path }); if (indent > 1) { editor.tf.setNodes({ [KEYS17.indent]: indent - 1 }, { at: path }); } else { editor.tf.unsetNodes([KEYS17.indent, KEYS17.listChecked], { at: path }); } }); }); return false; } setListNodes(editor, entries, { listStyleType }); return true; } return null; })(); const restartValue = listRestart || listRestartPolite; const isRestart = !!listRestart; if (setList && restartValue) { const atStart = editor.api.start(editor.selection); const entry = getListAbove(editor, { at: atStart }); if (!entry) return; const isFirst = !getPreviousList(editor, entry); if (!isRestart && (!isFirst || restartValue <= 0)) return; if (isRestart && restartValue === 1 && isFirst) return; const prop = isRestart ? KEYS17.listRestart : KEYS17.listRestartPolite; editor.tf.setNodes({ [prop]: restartValue }, { at: entry[1] }); } }; // src/lib/withNormalizeList.ts var withNormalizeList = ({ editor, getOptions, tf: { normalizeNode } }) => { return { transforms: { normalizeNode([node, path]) { const normalized = editor.tf.withoutNormalizing(() => { if (normalizeListNotIndented(editor, [node, path])) return true; if (normalizeListStart( editor, [node, path], getOptions().getSiblingListOptions )) return true; }); if (normalized) return; return normalizeNode([node, path]); } } }; }; // src/lib/withList.ts var withList = (ctx) => { const { editor, getOptions, tf: { apply, resetBlock } } = ctx; return { transforms: { resetBlock(options) { if (editor.api.block(options)?.[0]?.[KEYS18.listType]) { outdentList(editor); return; } return resetBlock(options); }, ...withNormalizeList(ctx).transforms, // ...withDeleteBackwardList(ctx).transforms, ...withInsertBreakList(ctx).transforms, apply(operation) { const { getSiblingListOptions } = getOptions(); if (operation.type === "insert_node") { const listStyleType = operation.node[KEYS18.listType]; if (listStyleType && ["lower-roman", "upper-roman"].includes( listStyleType )) { const prevNodeEntry = getPreviousList( editor, [operation.node, operation.path], { breakOnEqIndentNeqListStyleType: false, eqIndent: false, ...getSiblingListOptions } ); if (prevNodeEntry) { const prevListStyleType = prevNodeEntry[0][KEYS18.listType]; if (prevListStyleType === "lower-alpha" /* LowerAlpha */ && listStyleType === "lower-roman" /* LowerRoman */) { operation.node[KEYS18.listType] = "lower-alpha" /* LowerAlpha */; } else if (prevListStyleType === "upper-alpha" /* UpperAlpha */ && listStyleType === "upper-roman" /* UpperRoman */) { operation.node[KEYS18.listType] = "upper-alpha" /* UpperAlpha */; } } } } if (operation.type === "split_node" && operation.properties[KEYS18.listType]) { operation.properties[KEYS18.listRestart] = void 0; operation.properties[KEYS18.listRestartPolite] = void 0; } apply(operation); const affectedPaths = []; switch (operation.type) { case "insert_node": case "remove_node": case "set_node": { affectedPaths.push(operation.path); break; } case "merge_node": { affectedPaths.push(PathApi3.previous(operation.path)); break; } case "move_node": { affectedPaths.push(operation.path, operation.newPath); break; } case "split_node": { affectedPaths.push(operation.path, PathApi3.next(operation.path)); break; } } const isListItem = (node) => KEYS18.listType in node; affectedPaths.forEach((affectedPath) => { let entry = editor.api.node(affectedPath); if (!entry) return; if (!isListItem(entry[0])) { entry = editor.api.node(PathApi3.next(affectedPath)); } while (entry && isListItem(entry[0])) { const normalized = normalizeListStart( editor, entry, getSiblingListOptions ); if (normalized) break; entry = getNextList( editor, entry, { ...getSiblingListOptions, breakOnEqIndentNeqListStyleType: false, breakOnLowerIndent: false, eqIndent: false } ); } }); } } }; }; // src/lib/BaseListPlugin.tsx var BaseListPlugin = createTSlatePlugin({ key: KEYS19.list, inject: { plugins: { [KEYS19.html]: { parser: { transformData: ({ data }) => { const document = new DOMParser().parseFromString(data, "text/html"); const { body } = document; traverseHtmlElements(body, (element) => { if (element.tagName === "LI") { const { childNodes } = element; const liChildren = []; childNodes.forEach((child) => { if (isHtmlBlockElement(child)) { liChildren.push(...child.childNodes); } else { liChildren.push(child); } }); element.replaceChildren(...liChildren); return false; } return true; }); return postCleanHtml(body.innerHTML); } } } } }, options: { getListStyleType: (element) => element.style.listStyleType }, parsers: { html: { deserializer: { isElement: true, rules: [ { validNodeName: "LI" } ], parse: ({ editor, element, getOptions }) => { return { // gdoc uses aria-level attribute indent: Number(element.getAttribute("aria-level")), listStyleType: getOptions().getListStyleType?.(element), type: editor.getType(KEYS19.p) }; } } } }, render: { belowNodes: (props) => { if (!props.element.listStyleType) return; return (props2) => /* @__PURE__ */ React.createElement(List, { ...props2 }); } }, rules: { break: { empty: "reset", splitReset: false }, delete: { start: "reset" }, merge: { removeEmpty: false }, match: ({ node }) => { return isDefined6(node[KEYS19.listType]); } } }).overrideEditor(withList); function List(props) { const { listStart, listStyleType } = props.element; const List2 = isOrderedList(props.element) ? "ol" : "ul"; return /* @__PURE__ */ React.createElement( List2, { style: { listStyleType, margin: 0, padding: 0, position: "relative" }, start: listStart }, /* @__PURE__ */ React.createElement("li", null, props.children) ); } // src/react/ListPlugin.tsx var ListPlugin = toPlatePlugin(BaseListPlugin); // src/react/hooks/useListToolbarButton.ts import { useEditorRef, useEditorSelector } from "@udecode/plate/react"; var useListToolbarButtonState = ({ nodeType = "disc" /* Disc */ } = {}) => { const pressed = useEditorSelector( (editor) => someList(editor, nodeType), [nodeType] ); return { nodeType, pressed }; }; var useListToolbarButton = ({ nodeType, pressed }) => { const editor = useEditorRef(); return { props: { pressed, onClick: () => { toggleList(editor, { listStyleType: nodeType }); }, onMouseDown: (e) => { e.preventDefault(); } } }; }; // src/react/hooks/useTodoListElement.ts import { useEditorRef as useEditorRef2, useReadOnly } from "@udecode/plate/react"; var useTodoListElementState = ({ element }) => { const editor = useEditorRef2(); const { checked } = element; const readOnly = useReadOnly(); return { checked, editor, element, readOnly }; }; var useTodoListElement = (state) => { const { checked, editor, element, readOnly } = state; return { checkboxProps: { checked: !!checked, onCheckedChange: (value) => { if (readOnly) return; const path = editor.api.findPath(element); if (!path) return; editor.tf.setNodes({ checked: value }, { at: path }); }, onMouseDown: (e) => { e.preventDefault(); } } }; }; // src/react/hooks/useTodoListToolbarButton.ts import { useEditorRef as useEditorRef3, useEditorSelector as useEditorSelector2 } from "@udecode/plate/react"; var useIndentTodoToolBarButtonState = ({ nodeType = "disc" /* Disc */ } = {}) => { const pressed = useEditorSelector2( (editor) => someTodoList(editor), [nodeType] ); return { nodeType, pressed }; }; var useIndentTodoToolBarButton = ({ nodeType, pressed }) => { const editor = useEditorRef3(); return { props: { pressed, onClick: () => { toggleList(editor, { listStyleType: nodeType }); }, onMouseDown: (e) => { e.preventDefault(); } } }; }; export { ListPlugin, useIndentTodoToolBarButton, useIndentTodoToolBarButtonState, useListToolbarButton, useListToolbarButtonState, useTodoListElement, useTodoListElementState }; //# sourceMappingURL=index.mjs.map