ueberdosis / tiptap

The headless rich text editor framework for web artisans.
https://tiptap.dev
MIT License
27.52k stars 2.29k forks source link

Gapcursor with Code block #1195

Closed andreasvirkus closed 3 years ago

andreasvirkus commented 3 years ago

Description Gapcursor does not play nice with a code block atm.

Steps to reproduce the bug Steps to reproduce the behavior:

  1. Go to https://tiptap.dev
  2. Create a new code block as the first Node of the editor
  3. Try to click above the code block (or below)
  4. No gapcursor is placed outside of the code block

Expected behavior The gapcursor can be placed before or after a code block, like with an Image node

Screenshot, video, or GIF If applicable, add screenshots to help explain your problem.

https://user-images.githubusercontent.com/9140811/115612461-f371b480-a2f3-11eb-9267-d4159d27a13a.mov

Environment?

philippkuehn commented 3 years ago

Since the gapcursor extension is basically a wrapper around prosemirror-gapcursor and marijn don't want to change that behavior I’m not sure how we should handle that right now.

BrianHung commented 3 years ago

You could get the expected behavior by setting isolating = true in the NodeSpec of codeblock because the gapcursor uses:

(before.childCount == 0 && !before.inlineContent) || before.isAtom || before.type.spec.isolating

If you do that, you'll lose some behaviors like "if two codeblocks are adjacent, pressing backspace on the second codeblock will not merge it with the first one".

philippkuehn commented 3 years ago

isolating has some other weird side effects (up and down cursor navigation within cursor navigation isn't working as expected) so this is not an option I think.

andreasvirkus commented 3 years ago

I think we'll just live with the current situation then. Thanks for explaining! We've added some arrow shortcuts to our editor as a compromise. @techstonia is hopefully planning on proposing these changes in a PR for you soon, if that's fine. Slack's inline code also works the same way (right arrow to escape)

export const CustomCode = Code.extend({
  addKeyboardShortcuts() {
    return {
      'Mod-e': () => this.editor.commands.toggleCode(),
      ArrowRight: () => {
        const state = this.editor.state
        const { from, to } = state.selection

        if (from > 1 && from === to) {
          let codeOnLeft = false
          state.doc.nodesBetween(from - 1, to - 1, (node) => {
            const code = node.marks.find((markItem) => markItem.type.name === 'code')
            if (code) codeOnLeft = true
          })

          let noCodeUnderCursor = true
          state.doc.nodesBetween(from, to, (node) => {
            const code = node.marks.find((markItem) => markItem.type.name === 'code')
            if (code) noCodeUnderCursor = false
          })

          let nothingOnRight = true
          state.doc.nodesBetween(from + 1, to + 1, (node) => {
            if (node) nothingOnRight = false
          })

          if (codeOnLeft && noCodeUnderCursor && nothingOnRight) {
            return this.editor.chain().unsetCode().insertContent(' ').run()
          }
        }

        return false
      },
      Backspace: () => {
        const { state } = this.editor
        const { $anchor } = state.selection
        const { from, to } = state.selection

        let commentStartsWithCode = false
        state.doc.nodesBetween(1, 2, (node) => {
          const code = node.marks.find((markItem) => markItem.type.name === 'code')
          if (code) commentStartsWithCode = true
        })

        if (!commentStartsWithCode) return false

        const noCodeOnRight = !$anchor.nodeAfter?.marks.find((markItem) => markItem.type.name === 'code')
        if (this.editor.isActive('code') && from === 2 && noCodeOnRight) {
          return this.editor.chain().deleteRange({ from: 1, to: 2 }).unsetCode().run()
        }

        // Remove code formatting when deleting everything in editor
        if (from === 1 && to === state.doc.content.size - 1) {
          return this.editor.commands.clearContent()
        }

        return false
      },
    }
  },
})

export const CustomCodeBlock = CodeBlock.extend({
  addKeyboardShortcuts() {
    return {
      'Mod-Alt-c': () => this.editor.commands.toggleCodeBlock(),

      // remove code block when at start of document or code block is empty
      Backspace: () => {
        const { state } = this.editor
        const { empty, $anchor } = state.selection
        const isAtStart = $anchor.pos === 1

        if (!empty || $anchor.parent.type.name !== 'codeBlock') {
          return false
        }

        if (isAtStart || !$anchor.parent.textContent.length) {
          return this.editor.commands.clearNodes()
        }

        return false
      },
      ArrowDown: () => {
        const state = this.editor.state
        const { from, to } = state.selection

        if (from > 1 && from === to) {
          let inCodeBlock = false
          state.doc.nodesBetween(from - 1, to - 1, (node) => {
            if (node.type.name === 'codeBlock') inCodeBlock = true
          })

          let nothingOnRight = true
          state.doc.nodesBetween(from + 1, to + 1, (node) => {
            if (node) nothingOnRight = false
          })

          if (inCodeBlock && nothingOnRight) {
            return (this.editor.commands as any).setHardBreak()
          }
        }

        return false
      },
    }
  },
  addInputRules() {
    return [
      textblockTypeInputRule(/^```$/, this.type, ({ groups }: any) => groups),
      textblockTypeInputRule(tildeInputRegex, this.type, ({ groups }: any) => groups),
    ]
  },
})
philippkuehn commented 3 years ago

Yeah, we should still improve behavior.

Btw you can extend without overwriting with parent:

export const CustomCodeBlock = CodeBlock.extend({
  addKeyboardShortcuts() {
    return {
      ...this.parent.?(),
      // new shortcuts here
      ArrowDown: () => {
        // ...
      },
    }
  },
})
techstonia commented 3 years ago

@andreasvirkus I opened two PRs with the arrow shortcuts functionality https://github.com/ueberdosis/tiptap/pull/1200 https://github.com/ueberdosis/tiptap/pull/1204