Skip to content

Commit

Permalink
fix(lists): improve list behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
bdbch committed Mar 28, 2023
1 parent 0d1c2fd commit 684e48a
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 53 deletions.
29 changes: 29 additions & 0 deletions packages/core/src/helpers/getNodeAtPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Node, NodeType } from '@tiptap/pm/model'
import { EditorState } from '@tiptap/pm/state'

/**
* Finds the first node of a given type or name in the current selection.
* @param state The editor state.
* @param typeOrName The node type or name.
* @param pos The position to start searching from.
* @param maxDepth The maximum depth to search.
* @returns The node and the depth as an array.
*/
export const getNodeAtPosition = (state: EditorState, typeOrName: string | NodeType, pos: number, maxDepth = 20) => {
const $pos = state.doc.resolve(pos)

let currentDepth = maxDepth
let node: Node | null = null

while (currentDepth > 0 && node === null) {
const currentNode = $pos.node(currentDepth)

if (currentNode?.type.name === typeOrName) {
node = currentNode
} else {
currentDepth -= 1
}
}

return [node, currentDepth] as [Node | null, number]
}
3 changes: 3 additions & 0 deletions packages/core/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './getMarkAttributes'
export * from './getMarkRange'
export * from './getMarksBetween'
export * from './getMarkType'
export * from './getNodeAtPosition'
export * from './getNodeAttributes'
export * from './getNodeType'
export * from './getRenderedAttributes'
Expand All @@ -34,6 +35,8 @@ export * from './getTextContentFromNodes'
export * from './getTextSerializersFromSchema'
export * from './injectExtensionAttributesToParseRule'
export * from './isActive'
export * from './isAtEndOfNode'
export * from './isAtStartOfNode'
export * from './isExtensionRulesEnabled'
export * from './isList'
export * from './isMarkActive'
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/helpers/isAtEndOfNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EditorState } from '@tiptap/pm/state'

export const istAtEndOfNode = (state: EditorState) => {
const { $from, $to } = state.selection

if ($to.parentOffset < $to.parent.nodeSize - 2 || $from.pos !== $to.pos) {
return false
}

return true
}
11 changes: 11 additions & 0 deletions packages/core/src/helpers/isAtStartOfNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EditorState } from '@tiptap/pm/state'

export const isAtStartOfNode = (state: EditorState) => {
const { $from, $to } = state.selection

if ($from.parentOffset > 0 || $from.pos !== $to.pos) {
return false
}

return true
}
22 changes: 13 additions & 9 deletions packages/extension-list-item/src/commands/joinListItemBackward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import { joinPoint } from '@tiptap/pm/transform'
export const joinListItemBackward: RawCommands['splitListItem'] = () => ({
tr, state, dispatch,
}) => {
const point = joinPoint(state.doc, state.selection.$from.pos, -1)
try {
const point = joinPoint(state.doc, state.selection.$from.pos, -1)

if (point === null || point === undefined) {
return false
}
if (point === null || point === undefined) {
return false
}

tr.join(point, 2)
tr.join(point, 2)

if (dispatch) {
dispatch(tr)
}
if (dispatch) {
dispatch(tr)
}

return true
return true
} catch {
return false
}
}
22 changes: 13 additions & 9 deletions packages/extension-list-item/src/commands/joinListItemForward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ export const joinListItemForward: RawCommands['splitListItem'] = () => ({
dispatch,
tr,
}) => {
const point = joinPoint(state.doc, state.selection.$from.pos, +1)
try {
const point = joinPoint(state.doc, state.selection.$from.pos, +1)

if (point === null || point === undefined) {
return false
}
if (point === null || point === undefined) {
return false
}

tr.join(point, 2)
tr.join(point, 2)

if (dispatch) {
dispatch(tr)
}
if (dispatch) {
dispatch(tr)
}

return true
return true
} catch (e) {
return false
}
}
74 changes: 43 additions & 31 deletions packages/extension-list-item/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getNodeType } from '@tiptap/core'
import { getNodeAtPosition, getNodeType } from '@tiptap/core'
import { Node, NodeType } from '@tiptap/pm/model'
import { EditorState } from '@tiptap/pm/state'

Expand Down Expand Up @@ -28,36 +28,6 @@ export const findListItemPos = (typeOrName: string | NodeType, state: EditorStat
return { $pos: state.doc.resolve(currentPos), depth: targetDepth }
}

export const isNodeAtCursor = (typeOrName: string, state: EditorState) => {
const listItemPos = findListItemPos(typeOrName, state)

if (!listItemPos) {
return false
}

return true
}

export const isAtStartOfNode = (state: EditorState) => {
const { $from, $to } = state.selection

if ($from.parentOffset > 0 || $from.pos !== $to.pos) {
return false
}

return true
}

export const istAtEndOfNode = (state: EditorState) => {
const { $from, $to } = state.selection

if ($to.parentOffset < $to.parent.nodeSize - 2 || $from.pos !== $to.pos) {
return false
}

return true
}

export const hasPreviousListItem = (typeOrName: string, state: EditorState) => {
const listItemPos = findListItemPos(typeOrName, state)

Expand Down Expand Up @@ -94,3 +64,45 @@ export const listItemHasSubList = (typeOrName: string, state: EditorState, node?

return hasSubList
}

export const getNextListDepth = (typeOrName: string, state: EditorState) => {
const listItemPos = findListItemPos(typeOrName, state)

if (!listItemPos) {
return false
}

const [, depth] = getNodeAtPosition(state, typeOrName, listItemPos.$pos.pos + 4)

return depth
}

export const nextListIsDeeper = (typeOrName: string, state: EditorState) => {
const listDepth = getNextListDepth(typeOrName, state)
const listItemPos = findListItemPos(typeOrName, state)

if (!listItemPos || !listDepth) {
return false
}

if (listDepth > listItemPos.depth) {
return true
}

return false
}

export const nextListIsHigher = (typeOrName: string, state: EditorState) => {
const listDepth = getNextListDepth(typeOrName, state)
const listItemPos = findListItemPos(typeOrName, state)

if (!listItemPos || !listDepth) {
return false
}

if (listDepth < listItemPos.depth) {
return true
}

return false
}
22 changes: 18 additions & 4 deletions packages/extension-list-item/src/list-item.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { mergeAttributes, Node } from '@tiptap/core'
import {
isAtStartOfNode, isNodeActive, istAtEndOfNode, mergeAttributes, Node,
} from '@tiptap/core'
import { NodeType } from '@tiptap/pm/model'

import { joinListItemBackward } from './commands/joinListItemBackward'
import { joinListItemForward } from './commands/joinListItemForward'
import {
findListItemPos, hasPreviousListItem, isAtStartOfNode, isNodeAtCursor, istAtEndOfNode, listItemHasSubList,
findListItemPos, hasPreviousListItem, listItemHasSubList, nextListIsDeeper, nextListIsHigher,
} from './helpers'

declare module '@tiptap/core' {
Expand Down Expand Up @@ -63,7 +65,7 @@ export const ListItem = Node.create<ListItemOptions>({
Delete: ({ editor }) => {
// if the cursor is not inside the current node type
// do nothing and proceed
if (!isNodeAtCursor(this.name, editor.state)) {
if (!isNodeActive(editor.state, this.name)) {
return false
}

Expand All @@ -73,6 +75,18 @@ export const ListItem = Node.create<ListItemOptions>({
return false
}

// check if the next node is a list with a deeper depth
if (nextListIsDeeper(this.name, editor.state)) {
return editor.chain().focus(editor.state.selection.from + 4)
.lift(this.name)
.joinBackward()
.run()
}

if (nextListIsHigher(this.name, editor.state)) {
return editor.chain().joinForward().joinListItemForward(this.name).run()
}

// check if the next node is also a listItem
return editor.commands.joinListItemForward(this.name)
},
Expand All @@ -84,7 +98,7 @@ export const ListItem = Node.create<ListItemOptions>({

// if the cursor is not inside the current node type
// do nothing and proceed
if (!isNodeAtCursor(this.name, editor.state)) {
if (!isNodeActive(editor.state, this.name)) {
return false
}

Expand Down

0 comments on commit 684e48a

Please sign in to comment.