UNPKG

@udecode/plate-list

Version:

List plugin for Plate

1,700 lines (1,660 loc) 53.8 kB
// src/react/ListPlugin.tsx import { Key, toPlatePlugin } from "@udecode/plate/react"; // src/lib/BaseListPlugin.ts import { bindFirst, createSlatePlugin, createTSlatePlugin as createTSlatePlugin2, HtmlPlugin } from "@udecode/plate"; // src/lib/transforms/moveListItems.ts import { PathApi as PathApi5 } from "@udecode/plate"; // src/lib/queries/isListNested.ts var isListNested = (editor, listPath) => { const listParentNode = editor.api.parent(listPath)?.[0]; return listParentNode?.type === editor.getType(BaseListItemPlugin); }; // src/lib/transforms/moveListItemDown.ts import { match as match2, PathApi as PathApi2 } from "@udecode/plate"; // src/lib/queries/getHighestEmptyList.ts import { PathApi } from "@udecode/plate"; // src/lib/queries/getListTypes.ts var getListTypes = (editor) => { return [ editor.getType(BaseNumberedListPlugin), editor.getType(BaseBulletedListPlugin) ]; }; // src/lib/queries/getHighestEmptyList.ts var getHighestEmptyList = (editor, { diffListPath, liPath }) => { const list = editor.api.above({ at: liPath, match: { type: getListTypes(editor) } }); if (!list) return; const [listNode, listPath] = list; if (!diffListPath || !PathApi.equals(listPath, diffListPath)) { if (listNode.children.length < 2) { const liParent = editor.api.above({ at: listPath, match: { type: editor.getType(BaseListItemPlugin) } }); if (liParent) { return getHighestEmptyList(editor, { diffListPath, liPath: liParent[1] }) || listPath; } } return liPath; } }; // src/lib/queries/getListItemEntry.ts import { NodeApi, RangeApi } from "@udecode/plate"; var getListItemEntry = (editor, { at = editor.selection } = {}) => { const liType = editor.getType(BaseListItemPlugin); let _at; if (RangeApi.isRange(at) && !RangeApi.isCollapsed(at)) { _at = at.focus.path; } else if (RangeApi.isRange(at)) { _at = at.anchor.path; } else { _at = at; } if (_at) { const node = NodeApi.get(editor, _at); if (node) { const listItem = editor.api.above({ at: _at, match: { type: liType } }); if (listItem) { const list = editor.api.parent(listItem[1]); return { list, listItem }; } } } }; // src/lib/queries/getListRoot.ts var getListRoot = (editor, at = editor.selection) => { if (!at) return; const parentList = editor.api.above({ at, match: { type: [ editor.getType(BaseBulletedListPlugin), editor.getType(BaseNumberedListPlugin) ] } }); if (parentList) { const [, parentListPath] = parentList; return getListRoot(editor, parentListPath) ?? parentList; } }; // src/lib/queries/getTodoListItemEntry.ts import { NodeApi as NodeApi2, RangeApi as RangeApi2 } from "@udecode/plate"; // src/lib/BaseTodoListPlugin.ts import { createTSlatePlugin } from "@udecode/plate"; // src/lib/withTodoList.ts var withTodoList = ({ editor, getOptions, tf: { insertBreak } }) => ({ transforms: { insertBreak() { const insertBreakTodoList = () => { if (!editor.selection) return; const res = getTodoListItemEntry(editor); if (res) { const inserted = insertTodoListItem(editor, getOptions()); if (inserted) return true; } }; if (insertBreakTodoList()) return; insertBreak(); } } }); // src/lib/BaseTodoListPlugin.ts var BaseTodoListPlugin = createTSlatePlugin({ key: "action_item", node: { isElement: true } }).overrideEditor(withTodoList); // src/lib/queries/getTodoListItemEntry.ts var getTodoListItemEntry = (editor, { at = editor.selection } = {}) => { const todoType = editor.getType(BaseTodoListPlugin); let _at; if (RangeApi2.isRange(at) && !RangeApi2.isCollapsed(at)) { _at = at.focus.path; } else if (RangeApi2.isRange(at)) { _at = at.anchor.path; } else { _at = at; } if (_at) { const node = NodeApi2.get(editor, _at); if (node) { const listItem = editor.api.above({ at: _at, match: { type: todoType } }); if (listItem) { const list = editor.api.parent(listItem[1]); return { list, listItem }; } } } }; // src/lib/queries/hasListChild.ts import { match } from "@udecode/plate"; var hasListChild = (editor, node) => node.children.some((n) => match(n, [], { type: getListTypes(editor) })); // src/lib/queries/isAcrossListItems.ts import { RangeApi as RangeApi3 } from "@udecode/plate"; var isAcrossListItems = (editor, at = editor.selection) => { if (!at || RangeApi3.isCollapsed(at)) { return false; } const isAcrossBlocks = editor.api.isAt({ at, blocks: true }); if (!isAcrossBlocks) return false; return editor.api.some({ at, match: { type: editor.getType(BaseListItemPlugin) } }); }; // src/lib/queries/isListRoot.ts import { ElementApi } from "@udecode/plate"; var isListRoot = (editor, node) => ElementApi.isElement(node) && getListTypes(editor).includes(node.type); // src/lib/transforms/moveListItemDown.ts var moveListItemDown = (editor, { list, listItem }) => { let moved = false; const [listNode] = list; const [, listItemPath] = listItem; const previousListItemPath = PathApi2.previous(listItemPath); if (!previousListItemPath) { return; } const previousSiblingItem = editor.api.node(previousListItemPath); if (previousSiblingItem) { const [previousNode, previousPath] = previousSiblingItem; const sublist = previousNode.children.find( (n) => match2(n, [], { type: getListTypes(editor) }) ); const newPath = previousPath.concat( sublist ? [1, sublist.children.length] : [1] ); editor.tf.withoutNormalizing(() => { if (!sublist) { editor.tf.wrapNodes( { children: [], type: listNode.type }, { at: listItemPath } ); } editor.tf.moveNodes({ at: listItemPath, to: newPath }); moved = true; }); } return moved; }; // src/lib/transforms/moveListItemUp.ts import { NodeApi as NodeApi5, PathApi as PathApi4 } from "@udecode/plate"; // src/lib/transforms/moveListItemsToList.ts import { NodeApi as NodeApi3, PathApi as PathApi3 } from "@udecode/plate"; var moveListItemsToList = (editor, { deleteFromList = true, fromList, fromListItem, fromStartIndex, to: _to, toList, toListIndex = null }) => { let fromListPath; let moved = false; editor.tf.withoutNormalizing(() => { if (fromListItem) { const fromListItemSublist = editor.api.descendant({ at: fromListItem[1], match: { type: getListTypes(editor) } }); if (!fromListItemSublist) return; fromListPath = fromListItemSublist?.[1]; } else if (fromList) { fromListPath = fromList[1]; } else { return; } let to = null; if (_to) to = _to; if (toList) { if (toListIndex === null) { const lastChildPath = NodeApi3.lastChild(editor, toList[1])?.[1]; to = lastChildPath ? PathApi3.next(lastChildPath) : toList[1].concat([0]); } else { to = toList[1].concat([toListIndex]); } } if (!to) return; moved = editor.tf.moveNodes({ at: fromListPath, children: true, fromIndex: fromStartIndex, to }); if (deleteFromList) { editor.tf.delete({ at: fromListPath }); } }); return moved; }; // src/lib/transforms/unwrapList.ts import { ElementApi as ElementApi2, NodeApi as NodeApi4 } from "@udecode/plate"; var unwrapList = (editor, { at } = {}) => { const ancestorListTypeCheck = () => { if (editor.api.above({ at, match: { type: getListTypes(editor) } })) { return true; } if (!at && editor.selection) { const commonNode = NodeApi4.common( editor, editor.selection.anchor.path, editor.selection.focus.path ); if (ElementApi2.isElement(commonNode[0]) && getListTypes(editor).includes(commonNode[0].type)) { return true; } } return false; }; editor.tf.withoutNormalizing(() => { do { editor.tf.unwrapNodes({ at, match: { type: editor.getType(BaseListItemPlugin) }, split: true }); editor.tf.unwrapNodes({ at, match: { type: [ editor.getType(BaseBulletedListPlugin), editor.getType(BaseNumberedListPlugin) ] }, split: true }); } while (ancestorListTypeCheck()); }); }; // src/lib/transforms/moveListItemUp.ts var moveListItemUp = (editor, { list, listItem }) => { const move = () => { const [listNode, listPath] = list; const [liNode, liPath] = listItem; const liParent = editor.api.above({ at: listPath, match: { type: editor.getType(BaseListItemPlugin) } }); if (!liParent) { let toListPath2; try { toListPath2 = PathApi4.next(listPath); } catch (error) { return; } const condA = hasListChild(editor, liNode); const condB = !NodeApi5.isLastChild(editor, liPath); if (condA || condB) { editor.tf.insertNodes( { children: [], type: listNode.type }, { at: toListPath2 } ); } if (condA) { const toListNode = NodeApi5.get(editor, toListPath2); if (!toListNode) return; moveListItemsToList(editor, { fromListItem: listItem, toList: [toListNode, toListPath2] }); } if (condB) { const toListNode = NodeApi5.get(editor, toListPath2); if (!toListNode) return; moveListItemsToList(editor, { deleteFromList: false, fromList: list, fromStartIndex: liPath.at(-1) + 1, toList: [toListNode, toListPath2] }); } unwrapList(editor, { at: liPath.concat(0) }); return true; } const [, liParentPath] = liParent; const toListPath = liPath.concat([1]); if (!NodeApi5.isLastChild(editor, liPath)) { if (!hasListChild(editor, liNode)) { editor.tf.insertNodes( { children: [], type: listNode.type }, { at: toListPath } ); } const toListNode = NodeApi5.get(editor, toListPath); if (!toListNode) return; moveListItemsToList(editor, { deleteFromList: false, fromListItem: liParent, fromStartIndex: liPath.at(-1) + 1, toList: [toListNode, toListPath] }); } const movedUpLiPath = PathApi4.next(liParentPath); editor.tf.moveNodes({ at: liPath, to: movedUpLiPath }); return true; }; let moved = false; editor.tf.withoutNormalizing(() => { moved = move(); }); return moved; }; // src/lib/transforms/removeFirstListItem.ts var removeFirstListItem = (editor, { list, listItem }) => { const [, listPath] = list; if (!isListNested(editor, listPath)) { moveListItemUp(editor, { list, listItem }); return true; } return false; }; // src/lib/transforms/moveListItems.ts var moveListItems = (editor, { at = editor.selection ?? void 0, enableResetOnShiftTab, increase = true } = {}) => { const _nodes = editor.api.nodes({ at, match: { type: editor.getType(BaseListItemContentPlugin) } }); const lics = Array.from(_nodes); if (lics.length === 0) return; const highestLicPaths = []; const highestLicPathRefs = []; lics.forEach((lic) => { const licPath = lic[1]; const liPath = PathApi5.parent(licPath); const isAncestor = highestLicPaths.some((path) => { const highestLiPath = PathApi5.parent(path); return PathApi5.isAncestor(highestLiPath, liPath); }); if (!isAncestor) { highestLicPaths.push(licPath); highestLicPathRefs.push(editor.api.pathRef(licPath)); } }); const licPathRefsToMove = increase ? highestLicPathRefs : highestLicPathRefs.reverse(); return editor.tf.withoutNormalizing(() => { let moved = false; licPathRefsToMove.forEach((licPathRef) => { const licPath = licPathRef.unref(); if (!licPath) return; const listItem = editor.api.parent(licPath); if (!listItem) return; const parentList = editor.api.parent(listItem[1]); if (!parentList) return; let _moved; if (increase) { _moved = moveListItemDown(editor, { list: parentList, listItem }); } else if (isListNested(editor, parentList[1])) { _moved = moveListItemUp(editor, { list: parentList, listItem }); } else if (enableResetOnShiftTab) { _moved = removeFirstListItem(editor, { list: parentList, listItem }); } moved = _moved || moved; }); return moved; }); }; // src/lib/transforms/insertListItem.ts import { PathApi as PathApi6 } from "@udecode/plate"; var insertListItem = (editor) => { const liType = editor.getType(BaseListItemPlugin); const licType = editor.getType(BaseListItemContentPlugin); if (!editor.selection) { return false; } const licEntry = editor.api.above({ match: { type: licType } }); if (!licEntry) return false; const [, paragraphPath] = licEntry; const listItemEntry = editor.api.parent(paragraphPath); if (!listItemEntry) return false; const [listItemNode, listItemPath] = listItemEntry; if (listItemNode.type !== liType) return false; let success = false; editor.tf.withoutNormalizing(() => { if (!editor.api.isCollapsed()) { editor.tf.delete(); } const isStart = editor.api.isStart(editor.selection.focus, paragraphPath); const isEnd = editor.api.isEmpty(editor.selection, { after: true }); const nextParagraphPath = PathApi6.next(paragraphPath); const nextListItemPath = PathApi6.next(listItemPath); if (isStart) { editor.tf.insertNodes( { children: [{ children: [{ text: "" }], type: licType }], type: liType }, { at: listItemPath } ); success = true; return; } if (isEnd) { const marks = editor.api.marks() || {}; editor.tf.insertNodes( { children: [{ children: [{ text: "", ...marks }], type: licType }], type: liType }, { at: nextListItemPath } ); editor.tf.select(nextListItemPath); } else { editor.tf.withoutNormalizing(() => { editor.tf.splitNodes(); editor.tf.wrapNodes( { children: [], type: liType }, { at: nextParagraphPath } ); editor.tf.moveNodes({ at: nextParagraphPath, to: nextListItemPath }); editor.tf.select(nextListItemPath); editor.tf.collapse({ edge: "start" }); }); } if (listItemNode.children.length > 1) { editor.tf.moveNodes({ at: nextParagraphPath, to: nextListItemPath.concat(1) }); } success = true; }); return success; }; // src/lib/transforms/insertTodoListItem.ts import { PathApi as PathApi7 } from "@udecode/plate"; var insertTodoListItem = (editor, { inheritCheckStateOnLineEndBreak = false, inheritCheckStateOnLineStartBreak = false }) => { const todoType = editor.getType(BaseTodoListPlugin); if (!editor.selection) { return false; } const todoEntry = editor.api.above({ match: { type: todoType } }); if (!todoEntry) return false; const [todo, paragraphPath] = todoEntry; let success = false; editor.tf.withoutNormalizing(() => { if (!editor.api.isCollapsed()) { editor.tf.delete(); } const isStart = editor.api.isStart(editor.selection.focus, paragraphPath); const isEnd = editor.api.isEmpty(editor.selection, { after: true }); const nextParagraphPath = PathApi7.next(paragraphPath); if (isStart) { editor.tf.insertNodes( { checked: inheritCheckStateOnLineStartBreak ? todo.checked : false, children: [{ text: "" }], type: todoType }, { at: paragraphPath } ); success = true; return; } if (isEnd) { const marks = editor.api.marks() || {}; editor.tf.insertNodes( { checked: inheritCheckStateOnLineEndBreak ? todo.checked : false, children: [{ text: "", ...marks }], type: todoType }, { at: nextParagraphPath } ); editor.tf.select(nextParagraphPath); } else { editor.tf.withoutNormalizing(() => { editor.tf.splitNodes(); }); } success = true; }); return success; }; // src/lib/transforms/moveListItemSublistItemsToListItemSublist.ts import { NodeApi as NodeApi6, PathApi as PathApi8 } from "@udecode/plate"; var moveListItemSublistItemsToListItemSublist = (editor, { fromListItem, start, toListItem }) => { const [, fromListItemPath] = fromListItem; const [, toListItemPath] = toListItem; let moved = false; editor.tf.withoutNormalizing(() => { const fromListItemSublist = editor.api.descendant({ at: fromListItemPath, match: { type: getListTypes(editor) } }); if (!fromListItemSublist) return; const [, fromListItemSublistPath] = fromListItemSublist; const toListItemSublist = editor.api.descendant({ at: toListItemPath, match: { type: getListTypes(editor) } }); let to; if (!toListItemSublist) { const fromList = editor.api.parent(fromListItemPath); if (!fromList) return; const [fromListNode] = fromList; const fromListType = fromListNode.type; const toListItemSublistPath = toListItemPath.concat([1]); editor.tf.insertNodes( { children: [], type: fromListType }, { at: toListItemSublistPath } ); to = toListItemSublistPath.concat([0]); } else if (start) { const [, toListItemSublistPath] = toListItemSublist; to = toListItemSublistPath.concat([0]); } else { to = PathApi8.next(NodeApi6.lastChild(editor, toListItemSublist[1])[1]); } moved = editor.tf.moveNodes({ at: fromListItemSublistPath, children: true, to }); editor.tf.delete({ at: fromListItemSublistPath }); }); return moved; }; // src/lib/transforms/removeListItem.ts import { deleteMerge, PathApi as PathApi9 } from "@udecode/plate"; var removeListItem = (editor, { list, listItem, reverse = true }) => { const [liNode, liPath] = listItem; if (editor.api.isExpanded() || !hasListChild(editor, liNode)) { return false; } const previousLiPath = PathApi9.previous(liPath); let success = false; editor.tf.withoutNormalizing(() => { if (previousLiPath) { const previousLi = editor.api.node(previousLiPath); if (!previousLi) return; let tempLiPath = PathApi9.next(liPath); editor.tf.insertNodes( { children: [ { children: [{ text: "" }], type: editor.getType(BaseListItemContentPlugin) } ], type: editor.getType(BaseListItemPlugin) }, { at: tempLiPath } ); const tempLi = editor.api.node(tempLiPath); if (!tempLi) return; const tempLiPathRef = editor.api.pathRef(tempLi[1]); moveListItemSublistItemsToListItemSublist(editor, { fromListItem: listItem, toListItem: tempLi }); deleteMerge(editor, { reverse }); tempLiPath = tempLiPathRef.unref(); moveListItemSublistItemsToListItemSublist(editor, { fromListItem: [tempLi[0], tempLiPath], toListItem: previousLi }); editor.tf.removeNodes({ at: tempLiPath }); success = true; return; } moveListItemsToList(editor, { fromListItem: listItem, toList: list, toListIndex: 1 }); }); return success; }; // src/lib/transforms/toggleList.ts import { BaseParagraphPlugin, ElementApi as ElementApi3, NodeApi as NodeApi7, RangeApi as RangeApi4 } from "@udecode/plate"; var toggleList = (editor, { type }) => editor.tf.withoutNormalizing(() => { if (!editor.selection) { return; } const { validLiChildrenTypes } = editor.getOptions(BaseListPlugin); if (editor.api.isCollapsed() || !editor.api.isAt({ blocks: true })) { const res = getListItemEntry(editor); if (res) { const { list } = res; if (list[0].type === type) { unwrapList(editor); } else { editor.tf.setNodes( { type }, { at: editor.selection, mode: "lowest", match: (n) => ElementApi3.isElement(n) && getListTypes(editor).includes(n.type) } ); } } else { const list = { children: [], type }; editor.tf.wrapNodes(list); const _nodes = editor.api.nodes({ match: { type: editor.getType(BaseParagraphPlugin) } }); const nodes = Array.from(_nodes); const blockAbove = editor.api.block({ match: { type: validLiChildrenTypes } }); if (!blockAbove) { editor.tf.setNodes({ type: editor.getType(BaseListItemContentPlugin) }); } const listItem = { children: [], type: editor.getType(BaseListItemPlugin) }; for (const [, path] of nodes) { editor.tf.wrapNodes(listItem, { at: path }); } } } else { const [startPoint, endPoint] = RangeApi4.edges(editor.selection); const commonEntry = NodeApi7.common( editor, startPoint.path, endPoint.path ); if (getListTypes(editor).includes(commonEntry[0].type) || commonEntry[0].type === editor.getType(BaseListItemPlugin)) { if (commonEntry[0].type === type) { unwrapList(editor); } else { const startList = editor.api.node({ at: RangeApi4.start(editor.selection), match: { type: getListTypes(editor) }, mode: "lowest" }); const endList = editor.api.node({ at: RangeApi4.end(editor.selection), match: { type: getListTypes(editor) }, mode: "lowest" }); const rangeLength = Math.min( startList[1].length, endList[1].length ); editor.tf.setNodes( { type }, { at: editor.selection, mode: "all", match: (n, path) => ElementApi3.isElement(n) && getListTypes(editor).includes(n.type) && path.length >= rangeLength } ); } } else { const rootPathLength = commonEntry[1].length; const _nodes = editor.api.nodes({ mode: "all" }); const nodes = Array.from(_nodes).filter( ([, path]) => path.length === rootPathLength + 1 ); nodes.forEach((n) => { if (getListTypes(editor).includes(n[0].type)) { editor.tf.setNodes( { type }, { at: n[1], mode: "all", match: (_n) => ElementApi3.isElement(_n) && getListTypes(editor).includes(_n.type) } ); } else { if (!validLiChildrenTypes?.includes(n[0].type)) { editor.tf.setNodes( { type: editor.getType(BaseListItemContentPlugin) }, { at: n[1] } ); } const listItem = { children: [], type: editor.getType(BaseListItemPlugin) }; editor.tf.wrapNodes(listItem, { at: n[1] }); const list = { children: [], type }; editor.tf.wrapNodes(list, { at: n[1] }); } }); } } }); var toggleBulletedList = (editor) => toggleList(editor, { type: editor.getType(BaseBulletedListPlugin) }); var toggleNumberedList = (editor) => toggleList(editor, { type: editor.getType(BaseNumberedListPlugin) }); // src/lib/BaseListPlugin.ts var BaseBulletedListPlugin = createSlatePlugin({ key: "ul", node: { isElement: true }, parsers: { html: { deserializer: { rules: [ { validNodeName: "UL" } ] } } } }); var BaseNumberedListPlugin = createSlatePlugin({ key: "ol", node: { isElement: true }, parsers: { html: { deserializer: { rules: [{ validNodeName: "OL" }] } } } }); var BaseListItemPlugin = createSlatePlugin({ key: "li", inject: { plugins: { [HtmlPlugin.key]: { parser: { preInsert: ({ editor, type }) => { return editor.api.some({ match: { type } }); } } } } }, node: { isElement: true }, parsers: { html: { deserializer: { rules: [{ validNodeName: "LI" }] } } } }); var BaseListItemContentPlugin = createSlatePlugin({ key: "lic", node: { isElement: true } }); var BaseListPlugin = createTSlatePlugin2({ key: "list", // TODO react // extendEditor: withList, plugins: [ BaseBulletedListPlugin, BaseNumberedListPlugin, BaseListItemPlugin, BaseListItemContentPlugin ] }).extendEditorTransforms(({ editor }) => ({ toggle: { bulletedList: bindFirst(toggleBulletedList, editor), list: bindFirst(toggleList, editor), numberedList: bindFirst(toggleNumberedList, editor) } })); // src/lib/withDeleteForwardList.ts import { NodeApi as NodeApi8, PathApi as PathApi10 } from "@udecode/plate"; var selectionIsNotInAListHandler = (editor) => { const pointAfterSelection = editor.api.after(editor.selection.focus); if (pointAfterSelection) { const nextSiblingListRes = getListItemEntry(editor, { at: pointAfterSelection }); if (nextSiblingListRes) { const { listItem } = nextSiblingListRes; const parentBlockEntity = editor.api.block({ at: editor.selection.anchor }); if (!editor.api.string(parentBlockEntity[1])) { editor.tf.removeNodes(); return true; } if (hasListChild(editor, listItem[0])) { const sublistRes = getListItemEntry(editor, { at: [...listItem[1], 1, 0, 0] }); moveListItemUp(editor, sublistRes); } } } return false; }; var selectionIsInAListHandler = (editor, res, defaultDelete, unit = "character") => { const { listItem } = res; if (!hasListChild(editor, listItem[0])) { const liType = editor.getType(BaseListItemPlugin); const _nodes = editor.api.nodes({ at: listItem[1], mode: "lowest", match: (node, path) => { if (path.length === 0) { return false; } const isNodeLi = node.type === liType; const isSiblingOfNodeLi = NodeApi8.get(editor, PathApi10.next(path))?.type === liType; return isNodeLi && isSiblingOfNodeLi; } }); const liWithSiblings = Array.from(_nodes, (entry) => entry[1])[0]; if (!liWithSiblings) { const pointAfterListItem2 = editor.api.after(listItem[1]); if (pointAfterListItem2) { const nextSiblingListRes = getListItemEntry(editor, { at: pointAfterListItem2 }); if (nextSiblingListRes) { const listRoot = getListRoot(editor, listItem[1]); moveListItemsToList(editor, { deleteFromList: true, fromList: nextSiblingListRes.list, toList: listRoot }); return true; } } return false; } const siblingListItem = editor.api.node( PathApi10.next(liWithSiblings) ); if (!siblingListItem) return false; const siblingList = editor.api.parent(siblingListItem[1]); if (siblingList && removeListItem(editor, { list: siblingList, listItem: siblingListItem, reverse: false })) { return true; } const pointAfterListItem = editor.api.after(editor.selection.focus); if (!pointAfterListItem || !isAcrossListItems(editor, { anchor: editor.selection.anchor, focus: pointAfterListItem })) { return false; } const licType = editor.getType(BaseListItemContentPlugin); const _licNodes = editor.api.nodes({ at: pointAfterListItem.path, mode: "lowest", match: (node) => node.type === licType }); const nextSelectableLic = [..._licNodes][0]; if (nextSelectableLic[0].children.length < 2) return false; defaultDelete(unit); const leftoverListItem = editor.api.node( PathApi10.parent(nextSelectableLic[1]) ); if (leftoverListItem && leftoverListItem[0].children.length === 0) { editor.tf.removeNodes({ at: leftoverListItem[1] }); } return true; } const nestedList = editor.api.node( PathApi10.next([...listItem[1], 0]) ); if (!nestedList) return false; const nestedListItem = Array.from( NodeApi8.children(editor, nestedList[1]) )[0]; if (removeFirstListItem(editor, { list: nestedList, listItem: nestedListItem })) { return true; } if (removeListItem(editor, { list: nestedList, listItem: nestedListItem })) { return true; } return false; }; var withDeleteForwardList = ({ editor, tf: { deleteForward } }) => ({ transforms: { deleteForward(unit) { const deleteForwardList = () => { let skipDefaultDelete = false; if (!editor?.selection) { return skipDefaultDelete; } if (!editor.api.isAt({ end: true })) { return skipDefaultDelete; } editor.tf.withoutNormalizing(() => { const res = getListItemEntry(editor, {}); if (!res) { skipDefaultDelete = selectionIsNotInAListHandler(editor); return; } skipDefaultDelete = selectionIsInAListHandler( editor, res, deleteForward, unit ); }); return skipDefaultDelete; }; if (deleteForwardList()) return; deleteForward(unit); } } }); // src/lib/withDeleteFragmentList.ts import { deleteMerge as deleteMerge2 } from "@udecode/plate"; var getLiStart = (editor) => { const start = editor.api.start(editor.selection); return editor.api.above({ at: start, match: { type: editor.getType(BaseListItemPlugin) } }); }; var withDeleteFragmentList = ({ editor, tf: { deleteFragment } }) => ({ transforms: { deleteFragment(direction) { const deleteFragmentList = () => { let deleted = false; editor.tf.withoutNormalizing(() => { if (!isAcrossListItems(editor)) return; const end = editor.api.end(editor.selection); const liEnd = editor.api.above({ at: end, match: { type: editor.getType(BaseListItemPlugin) } }); const liEndCanBeDeleted = liEnd && !hasListChild(editor, liEnd[0]); const liEndPathRef = liEndCanBeDeleted ? editor.api.pathRef(liEnd[1]) : void 0; if (!getLiStart(editor) || !liEnd) { deleted = false; return; } deleteMerge2(editor); const liStart = getLiStart(editor); if (liEndPathRef) { const liEndPath = liEndPathRef.unref(); const listStart = liStart && editor.api.parent(liStart[1]); const deletePath = getHighestEmptyList(editor, { diffListPath: listStart?.[1], liPath: liEndPath }); if (deletePath) { editor.tf.removeNodes({ at: deletePath }); } deleted = true; } }); return deleted; }; if (deleteFragmentList()) return; deleteFragment(direction); } } }); // src/lib/withInsertFragmentList.ts import { ElementApi as ElementApi4, NodeApi as NodeApi9, PathApi as PathApi11 } from "@udecode/plate"; var withInsertFragmentList = ({ editor, tf: { insertFragment } }) => { const listItemType = editor.getType(BaseListItemPlugin); const listItemContentType = editor.getType(BaseListItemContentPlugin); const getFirstAncestorOfType = (root, entry, type) => { let ancestor = PathApi11.parent(entry[1]); while (NodeApi9.get(root, ancestor).type !== type) { ancestor = PathApi11.parent(ancestor); } return [NodeApi9.get(root, ancestor), ancestor]; }; const findListItemsWithContent = (first) => { let prev = null; let node = first; while (isListRoot(editor, node) || node.type === listItemType && node.children[0].type !== listItemContentType) { prev = node; [node] = node.children; } return prev ? prev.children : [node]; }; const trimList = (listRoot) => { if (!isListRoot(editor, listRoot)) { return [listRoot]; } const _texts = NodeApi9.texts(listRoot); const textEntries = Array.from(_texts); const commonAncestorEntry = textEntries.reduce( (commonAncestor, textEntry) => PathApi11.isAncestor(commonAncestor[1], textEntry[1]) ? commonAncestor : NodeApi9.common(listRoot, textEntry[1], commonAncestor[1]), // any list item would do, we grab the first one getFirstAncestorOfType(listRoot, textEntries[0], listItemType) ); const [first, ...rest] = isListRoot( editor, commonAncestorEntry[0] ) ? commonAncestorEntry[0].children : [commonAncestorEntry[0]]; return [...findListItemsWithContent(first), ...rest]; }; const wrapNodeIntoListItem = (node) => { return node.type === listItemType ? node : { children: [node], type: listItemType }; }; const isSingleLic = (fragment) => { const isFragmentOnlyListRoot = fragment.length === 1 && isListRoot(editor, fragment[0]); return isFragmentOnlyListRoot && [...NodeApi9.nodes({ children: fragment })].filter( (entry) => ElementApi4.isElement(entry[0]) ).filter(([node]) => node.type === listItemContentType).length === 1; }; const getTextAndListItemNodes = (fragment, liEntry, licEntry) => { const [, liPath] = liEntry; const [licNode, licPath] = licEntry; const isEmptyNode = !NodeApi9.string(licNode); const [first, ...rest] = fragment.flatMap(trimList).map(wrapNodeIntoListItem); let textNode; let listItemNodes; if (isListRoot(editor, fragment[0])) { if (isSingleLic(fragment)) { textNode = first; listItemNodes = rest; } else if (isEmptyNode) { const li = NodeApi9.get(editor, liPath); const [, ...currentSublists] = li.children; const [newLic, ...newSublists] = first.children; editor.tf.insertNodes(newLic, { at: PathApi11.next(licPath), select: true }); editor.tf.removeNodes({ at: licPath }); if (newSublists?.length) { if (currentSublists?.length) { const path = [...liPath, 1, 0]; editor.tf.insertNodes(newSublists[0].children, { at: path, select: true }); } else { editor.tf.insertNodes(newSublists, { at: PathApi11.next(licPath), select: true }); } } textNode = { text: "" }; listItemNodes = rest; } else { textNode = { text: "" }; listItemNodes = [first, ...rest]; } } else { textNode = first; listItemNodes = rest; } return { listItemNodes, textNode }; }; return { transforms: { insertFragment(fragment) { let liEntry = editor.api.node({ match: { type: listItemType }, mode: "lowest" }); if (!liEntry) { return insertFragment( isListRoot(editor, fragment[0]) ? [{ text: "" }, ...fragment] : fragment ); } insertFragment([{ text: "" }]); liEntry = editor.api.node({ match: { type: listItemType }, mode: "lowest" }); if (!liEntry) { return insertFragment( isListRoot(editor, fragment[0]) ? [{ text: "" }, ...fragment] : fragment ); } const licEntry = editor.api.node({ match: { type: listItemContentType }, mode: "lowest" }); if (!licEntry) { return insertFragment( isListRoot(editor, fragment[0]) ? [{ text: "" }, ...fragment] : fragment ); } const { listItemNodes, textNode } = getTextAndListItemNodes( fragment, liEntry, licEntry ); insertFragment([textNode]); const [, liPath] = liEntry; return editor.tf.insertNodes(listItemNodes, { at: PathApi11.next(liPath), select: true }); } } }; }; // src/lib/withNormalizeList.ts import { BaseParagraphPlugin as BaseParagraphPlugin2, ElementApi as ElementApi5, match as match5, NodeApi as NodeApi11, PathApi as PathApi14 } from "@udecode/plate"; // src/lib/normalizers/normalizeListItem.ts import { match as match3, NodeApi as NodeApi10, PathApi as PathApi12 } from "@udecode/plate"; var getDeepInlineChildren = (editor, { children }) => { const inlineChildren = []; for (const child of children) { if (editor.api.isBlock(child[0])) { inlineChildren.push( ...getDeepInlineChildren(editor, { children: Array.from(NodeApi10.children(editor, child[1])) }) ); } else { inlineChildren.push(child); } } return inlineChildren; }; var normalizeListItem = (editor, { listItem, validLiChildrenTypes = [] }) => { let changed = false; const allValidLiChildrenTypes = /* @__PURE__ */ new Set([ editor.getType(BaseBulletedListPlugin), editor.getType(BaseListItemContentPlugin), editor.getType(BaseNumberedListPlugin), ...validLiChildrenTypes ]); const [, liPath] = listItem; const liChildren = Array.from( NodeApi10.children(editor, listItem[1]) ); const invalidLiChildrenPathRefs = liChildren.filter(([child]) => !allValidLiChildrenTypes.has(child.type)).map(([, childPath]) => editor.api.pathRef(childPath)); const firstLiChild = liChildren[0]; const [firstLiChildNode, firstLiChildPath] = firstLiChild ?? []; if (!firstLiChild || !editor.api.isBlock(firstLiChildNode)) { editor.tf.insertNodes( editor.api.create.block({ type: editor.getType(BaseListItemContentPlugin) }), { at: liPath.concat([0]) } ); return true; } if (editor.api.isBlock(firstLiChildNode) && !match3(firstLiChildNode, [], { type: editor.getType(BaseListItemContentPlugin) })) { if (match3(firstLiChildNode, [], { type: getListTypes(editor) })) { const parent = editor.api.parent(listItem[1]); const sublist = firstLiChild; const children = Array.from( NodeApi10.children(editor, firstLiChild[1]) ).reverse(); children.forEach((c) => { moveListItemUp(editor, { list: sublist, listItem: c }); }); editor.tf.removeNodes({ at: [...parent[1], 0] }); return true; } if (validLiChildrenTypes.includes(firstLiChildNode.type)) { return true; } editor.tf.setNodes( { type: editor.getType(BaseListItemContentPlugin) }, { at: firstLiChildPath } ); changed = true; } const licChildren = Array.from(NodeApi10.children(editor, firstLiChild[1])); if (licChildren.length > 0) { const blockPathRefs = []; const inlineChildren = []; for (const licChild of licChildren) { if (!editor.api.isBlock(licChild[0])) { break; } blockPathRefs.push(editor.api.pathRef(licChild[1])); inlineChildren.push( ...getDeepInlineChildren(editor, { children: Array.from(NodeApi10.children(editor, licChild[1])) }) ); } const to = PathApi12.next(licChildren.at(-1)[1]); inlineChildren.reverse().forEach(([, path]) => { editor.tf.moveNodes({ at: path, to }); }); blockPathRefs.forEach((pathRef) => { const path = pathRef.unref(); path && editor.tf.removeNodes({ at: path }); }); if (blockPathRefs.length > 0) { changed = true; } } if (changed) return true; invalidLiChildrenPathRefs.reverse().forEach((ref) => { const path = ref.unref(); path && editor.tf.moveNodes({ at: path, to: firstLiChildPath.concat([0]) }); }); return invalidLiChildrenPathRefs.length > 0; }; // src/lib/normalizers/normalizeNestedList.ts import { match as match4, PathApi as PathApi13 } from "@udecode/plate"; var normalizeNestedList = (editor, { nestedListItem }) => { const [, path] = nestedListItem; const parentNode = editor.api.parent(path); const hasParentList = parentNode && match4(parentNode[0], [], { type: getListTypes(editor) }); if (!hasParentList) { return false; } const previousListItemPath = PathApi13.previous(path); if (!previousListItemPath) { return false; } const previousSiblingItem = editor.api.node(previousListItemPath); if (previousSiblingItem) { const [, previousPath] = previousSiblingItem; const newPath = previousPath.concat([1]); editor.tf.moveNodes({ at: path, to: newPath }); return true; } }; // src/lib/withNormalizeList.ts var withNormalizeList = ({ editor, getOptions, tf: { normalizeNode } }) => ({ transforms: { normalizeNode([node, path]) { const liType = editor.getType(BaseListItemPlugin); const licType = editor.getType(BaseListItemContentPlugin); const defaultType = editor.getType(BaseParagraphPlugin2); if (!ElementApi5.isElement(node)) { return normalizeNode([node, path]); } if (isListRoot(editor, node)) { const nonLiChild = Array.from(NodeApi11.children(editor, path)).find( ([child]) => child.type !== liType ); if (nonLiChild) { return editor.tf.wrapNodes( { children: [], type: liType }, { at: nonLiChild[1] } ); } } if (match5(node, [], { type: getListTypes(editor) })) { if (node.children.length === 0 || !node.children.some((item) => item.type === liType)) { return editor.tf.removeNodes({ at: path }); } const nextPath = PathApi14.next(path); const nextNode = NodeApi11.get(editor, nextPath); if (nextNode?.type === node.type) { moveListItemsToList(editor, { deleteFromList: true, fromList: [nextNode, nextPath], toList: [node, path] }); } const prevPath = PathApi14.previous(path); const prevNode = NodeApi11.get(editor, prevPath); if (prevNode?.type === node.type) { editor.tf.normalizeNode([prevNode, prevPath]); return; } if (normalizeNestedList(editor, { nestedListItem: [node, path] })) { return; } } if (node.type === editor.getType(BaseListItemPlugin) && normalizeListItem(editor, { listItem: [node, path], validLiChildrenTypes: getOptions().validLiChildrenTypes })) { return; } if (node.type === licType && licType !== defaultType && editor.api.parent(path)?.[0].type !== liType) { editor.tf.setNodes({ type: defaultType }, { at: path }); return; } normalizeNode([node, path]); } } }); // src/react/onKeyDownList.ts import { Hotkeys, RangeApi as RangeApi5 } from "@udecode/plate"; var onKeyDownList = ({ editor, event, getOptions }) => { if (event.defaultPrevented) return; const isTab = Hotkeys.isTab(editor, event); const isUntab = Hotkeys.isUntab(editor, event); let workRange = editor.selection; if (editor.selection && (isTab || isUntab)) { const { selection } = editor; if (!editor.api.isCollapsed()) { const { anchor, focus } = RangeApi5.isBackward(selection) ? { anchor: { ...selection.focus }, focus: { ...selection.anchor } } : { anchor: { ...selection.anchor }, focus: { ...selection.focus } }; const unhangRange = editor.api.unhangRange({ anchor, focus }); if (unhangRange) { workRange = unhangRange; editor.tf.select(unhangRange); } } const listSelected = editor.api.some({ match: { type: editor.getType(BaseListItemPlugin) } }); if (workRange && listSelected) { event.preventDefault(); moveListItems(editor, { at: workRange, enableResetOnShiftTab: getOptions().enableResetOnShiftTab, increase: isTab }); return true; } } }; // src/react/withDeleteBackwardList.ts import { BaseParagraphPlugin as BaseParagraphPlugin3, deleteMerge as deleteMerge3, PathApi as PathApi15 } from "@udecode/plate"; import { BaseResetNodePlugin } from "@udecode/plate-reset-node"; import { onKeyDownResetNode, SIMULATE_BACKSPACE } from "@udecode/plate-reset-node/react"; import { getEditorPlugin } from "@udecode/plate/react"; var withDeleteBackwardList = ({ editor, tf: { deleteBackward } }) => ({ transforms: { deleteBackward(unit) { const deleteBackwardList = () => { const res = getListItemEntry(editor, {}); let moved = false; if (res) { const { list, listItem } = res; if (editor.api.isAt({ start: true, match: (node) => node.type === editor.getType(BaseListItemPlugin) })) { editor.tf.withoutNormalizing(() => { moved = removeFirstListItem(editor, { list, listItem }); if (moved) return true; moved = removeListItem(editor, { list, listItem }); if (moved) return true; if (!PathApi15.hasPrevious(listItem[1]) && !isListNested(editor, list[1])) { onKeyDownResetNode({ ...getEditorPlugin( editor, BaseResetNodePlugin.configure({ options: { rules: [ { defaultType: editor.getType(BaseParagraphPlugin3), hotkey: "backspace", types: [editor.getType(BaseListItemPlugin)], predicate: () => editor.api.isAt({ start: true }), onReset: (e) => unwrapList(e) } ] } }) ), event: SIMULATE_BACKSPACE }); moved = true; return; } const pointBeforeListItem = editor.api.before( editor.selection.focus ); let currentLic; let hasMultipleChildren = false; if (pointBeforeListItem && isAcrossListItems(editor, { anchor: editor.selection.anchor, focus: pointBeforeListItem })) { const licType = editor.getType(BaseListItemContentPlugin); const _licNodes = editor.api.nodes({ at: listItem[1], mode: "lowest", match: (node) => node.type === licType }); currentLic = [..._licNodes][0]; hasMultipleChildren = currentLic[0].children.length > 1; } deleteMerge3(editor, { reverse: true, unit }); moved = true; if (!currentLic || !hasMultipleChildren) return; const leftoverListItem = editor.api.node( PathApi15.parent(currentLic[1]) ); if (leftoverListItem && leftoverListItem[0].children.length === 0) { editor.tf.removeNodes({ at: leftoverListItem[1] }); } }); } } return moved; }; if (deleteBackwardList()) return; deleteBackward(unit); } } }); // src/react/withInsertBreakList.ts import { BaseParagraphPlugin as BaseParagraphPlugin4, createTSlatePlugin as createTSlatePlugin3 } from "@udecode/plate"; import { onKeyDownResetNode as onKeyDownResetNode2, SIMULATE_BACKSPACE as SIMULATE_BACKSPACE2 } from "@udecode/plate-reset-node/react"; import { getEditorPlugin as getEditorPlugin2 } from "@udecode/plate/react"; var withInsertBreakList = ({ editor, tf: { insertBreak } }) => ({ transforms: { insertBreak() { const insertBreakList = () => { if (!editor.selection) return; const res = getListItemEntry(editor, {}); let moved; if (res) { const { list, listItem } = res; if (editor.api.isEmpty(editor.selection, { block: true })) { moved = moveListItemUp(editor, { list, listItem }); if (moved) return true; } } const didReset = onKeyDownResetNode2({ ...getEditorPlugin2( editor, createTSlatePlugin3({ options: { rules: [ { defaultType: editor.getType(BaseParagraphPlugin4), types: [editor.getType(BaseListItemPlugin)], predicate: () => !moved && editor.api.isEmpty(editor.selection, { block: true }), onReset: (_editor) => unwrapList(_editor) } ] } }) ), event: SIMULATE_BACKSPACE2 }); if (didReset) return true; if (!moved) { const inserted = insertListItem(editor); if (inserted) return true; }