Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix List issues & add support for Mod keys #4210

Merged
merged 4 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions packages/core/src/commands/cut.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TextSelection } from '@tiptap/pm/state'

import { RawCommands } from '../types.js'

declare module '@tiptap/core' {
Expand All @@ -11,17 +13,17 @@ declare module '@tiptap/core' {
}
}

export const cut: RawCommands['cut'] = (originRange, targetPos) => ({ editor }) => {
export const cut: RawCommands['cut'] = (originRange, targetPos) => ({ editor, tr }) => {
const { state } = editor

const contentSlice = state.doc.slice(originRange.from, originRange.to)

return editor
.chain()
.deleteRange(originRange)
.command(({ commands, tr }) => {
return commands.insertContentAt(tr.mapping.map(targetPos), contentSlice.content.toJSON())
})
.focus()
.run()
tr.deleteRange(originRange.from, originRange.to)
const newPos = tr.mapping.map(targetPos)

tr.insert(newPos, contentSlice.content)

tr.setSelection(new TextSelection(tr.doc.resolve(newPos - 1)))

return true
}
2 changes: 2 additions & 0 deletions packages/core/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export * from './forEach.js'
export * from './insertContent.js'
export * from './insertContentAt.js'
export * from './join.js'
export * from './joinItemBackward.js'
export * from './joinItemForward.js'
export * from './keyboardShortcut.js'
export * from './lift.js'
export * from './liftEmptyBlock.js'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { RawCommands } from '@tiptap/core'
import { joinPoint } from '@tiptap/pm/transform'

export const joinListItemBackward: RawCommands['splitListItem'] = () => ({
import { RawCommands } from '../types.js'

declare module '@tiptap/core' {
interface Commands<ReturnType> {
joinItemBackward: {
/**
* Join two nodes Forwards.
*/
joinItemBackward: () => ReturnType
}
}
}

export const joinItemBackward: RawCommands['joinItemBackward'] = () => ({
tr, state, dispatch,
}) => {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { RawCommands } from '@tiptap/core'
import { joinPoint } from '@tiptap/pm/transform'

export const joinListItemForward: RawCommands['splitListItem'] = () => ({
import { RawCommands } from '../types.js'

declare module '@tiptap/core' {
interface Commands<ReturnType> {
joinItemForward: {
/**
* Join two nodes Forwards.
*/
joinItemForward: () => ReturnType
}
}
}

export const joinItemForward: RawCommands['joinItemForward'] = () => ({
state,
dispatch,
tr,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/extensions/keymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const Keymap = Extension.create({
addKeyboardShortcuts() {
const handleBackspace = () => this.editor.commands.first(({ commands }) => [
() => commands.undoInputRule(),

// maybe convert first text block node to default node
() => commands.command(({ tr }) => {
const { selection, doc } = tr
Expand All @@ -32,6 +33,7 @@ export const Keymap = Extension.create({

return commands.clearNodes()
}),

() => commands.deleteSelection(),
() => commands.joinBackward(),
() => commands.selectNodeBackward(),
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export * from './isNodeActive.js'
export * from './isNodeEmpty.js'
export * from './isNodeSelection.js'
export * from './isTextSelection.js'
export * from './listHelpers/index.js'
export * from './posToDOMRect.js'
export * from './resolveFocusPosition.js'
export * from './selectionToInsertionEnd.js'
Expand Down
22 changes: 20 additions & 2 deletions packages/core/src/helpers/isAtEndOfNode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { EditorState } from '@tiptap/pm/state'

export const istAtEndOfNode = (state: EditorState) => {
const { $from, $to } = state.selection
import { findParentNode } from './findParentNode.js'

export const isAtEndOfNode = (state: EditorState, nodeType?: string) => {
const { $from, $to, $anchor } = state.selection

if (nodeType) {
const parentNode = findParentNode(node => node.type.name === nodeType)(state.selection)

if (!parentNode) {
return false
}

const $parentPos = state.doc.resolve(parentNode.pos + 1)

if ($anchor.pos + 1 === $parentPos.end()) {
return true
}

return false
}

if ($to.parentOffset < $to.parent.nodeSize - 2 || $from.pos !== $to.pos) {
return false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getNodeType } from '@tiptap/core'
import { NodeType } from '@tiptap/pm/model'
import { EditorState } from '@tiptap/pm/state'
import { ResolvedPos } from 'prosemirror-model'

export const getCurrentListItemPos = (typeOrName: string, state: EditorState): ResolvedPos | undefined => {
import { getNodeType } from '../getNodeType.js'

export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
const { $from } = state.selection
const nodeType = getNodeType(typeOrName, state.schema)

Expand All @@ -23,8 +24,8 @@ export const getCurrentListItemPos = (typeOrName: string, state: EditorState): R
}

if (targetDepth === null) {
return
return null
}

return state.doc.resolve(currentPos)
return { $pos: state.doc.resolve(currentPos), depth: targetDepth }
}
16 changes: 16 additions & 0 deletions packages/core/src/helpers/listHelpers/getNextListDepth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EditorState } from '@tiptap/pm/state'

import { getNodeAtPosition } from '../getNodeAtPosition.js'
import { findListItemPos } from './findListItemPos.js'

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
}
76 changes: 76 additions & 0 deletions packages/core/src/helpers/listHelpers/handleBackspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Node } from '@tiptap/pm/model'

import { Editor } from '../../Editor.js'
import { isAtStartOfNode } from '../isAtStartOfNode.js'
import { isNodeActive } from '../isNodeActive.js'
import { findListItemPos } from './findListItemPos.js'
import { hasListBefore } from './hasListBefore.js'
import { hasListItemBefore } from './hasListItemBefore.js'
import { listItemHasSubList } from './listItemHasSubList.js'

export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
// this is required to still handle the undo handling
if (editor.commands.undoInputRule()) {
return true
}

// if the current item is NOT inside a list item &
// the previous item is a list (orderedList or bulletList)
// move the cursor into the list and delete the current item
if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
const { $anchor } = editor.state.selection

const $listPos = editor.state.doc.resolve($anchor.before() - 1)

const listDescendants: Array<{ node: Node, pos: number }> = []

$listPos.node().descendants((node, pos) => {
if (node.type.name === name) {
listDescendants.push({ node, pos })
}
})

const lastItem = listDescendants.at(-1)

if (!lastItem) {
return false
}

const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1)

return editor.chain().cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end()).joinForward().run()
}

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

// if the cursor is not at the start of a node
// do nothing and proceed
if (!isAtStartOfNode(editor.state)) {
return false
}

const listItemPos = findListItemPos(name, editor.state)

if (!listItemPos) {
return false
}

const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2)
const prevNode = $prev.node(listItemPos.depth)

const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode)

// if the previous item is a list item and doesn't have a sublist, join the list items
if (hasListItemBefore(name, editor.state) && !previousListItemHasSubList) {
return editor.commands.joinItemBackward()
}

// otherwise in the end, a backspace should
// always just lift the list item if
// joining / merging is not possible
return editor.chain().liftListItem(name).run()
}
38 changes: 38 additions & 0 deletions packages/core/src/helpers/listHelpers/handleDelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Editor } from '../../Editor.js'
import { isAtEndOfNode } from '../isAtEndOfNode.js'
import { isNodeActive } from '../isNodeActive.js'
import { nextListIsDeeper } from './nextListIsDeeper.js'
import { nextListIsHigher } from './nextListIsHigher.js'

export const handleDelete = (editor: Editor, name: string) => {
// if the cursor is not inside the current node type
// do nothing and proceed
if (!isNodeActive(editor.state, name)) {
return false
}

// if the cursor is not at the end of a node
// do nothing and proceed
if (!isAtEndOfNode(editor.state, name)) {
return false
}

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

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

return editor.commands.joinItemForward()
}
15 changes: 15 additions & 0 deletions packages/core/src/helpers/listHelpers/hasListBefore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { EditorState } from '@tiptap/pm/state'

export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => {
const { $anchor } = editorState.selection

const previousNodePos = Math.max(0, $anchor.pos - 2)

const previousNode = editorState.doc.resolve(previousNodePos).node()

if (!previousNode || !parentListTypes.includes(previousNode.type.name)) {
return false
}

return true
}
10 changes: 10 additions & 0 deletions packages/core/src/helpers/listHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export * from './findListItemPos.js'
export * from './getNextListDepth.js'
export * from './handleBackspace.js'
export * from './handleDelete.js'
export * from './hasListBefore.js'
export * from './hasListItemAfter.js'
export * from './hasListItemBefore.js'
export * from './listItemHasSubList.js'
export * from './nextListIsDeeper.js'
export * from './nextListIsHigher.js'
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getNodeType } from '@tiptap/core'
import { Node } from '@tiptap/pm/model'
import { EditorState } from '@tiptap/pm/state'

import { getNodeType } from '../getNodeType.js'

export const listItemHasSubList = (typeOrName: string, state: EditorState, node?: Node) => {
if (!node) {
return false
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/helpers/listHelpers/nextListIsDeeper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EditorState } from '@tiptap/pm/state'

import { findListItemPos } from './findListItemPos.js'
import { getNextListDepth } from './getNextListDepth.js'

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
}
19 changes: 19 additions & 0 deletions packages/core/src/helpers/listHelpers/nextListIsHigher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EditorState } from '@tiptap/pm/state'

import { findListItemPos } from './findListItemPos.js'
import { getNextListDepth } from './getNextListDepth.js'

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
}
Loading