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

[Bug]: link plugin doesn't detect link unless it's followed by whitespace #6251

Open
1 task done
duhaime opened this issue Apr 7, 2025 · 4 comments
Open
1 task done
Labels
Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug

Comments

@duhaime
Copy link

duhaime commented Apr 7, 2025

Affected Packages

extension-link

Version(s)

current

Bug Description

Hey all, possibly there's some config we can pass to avoid this behavior, but it looks like the Link component doesn't recognize a link unless the link text is followed by whitespace:

Screen.Recording.2025-04-07.at.10.15.18.AM.mov

Is there a way to get the editor to recognize the link without the trailing whitespace?

Browser Used

Chrome

Code Example URL

No response

Expected Behavior

I'd expect google.com to be highlighted / linkified before the trailing whitespace is added

Additional Context (Optional)

I'm checking the dependecy updates box below on the assumption that the live demo has updated deps

Dependency Updates

  • Yes, I've updated all my dependencies.
@duhaime duhaime added Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug labels Apr 7, 2025
@github-project-automation github-project-automation bot moved this to Needs Triage in Tiptap: Issues Apr 7, 2025
@bdbch
Copy link
Member

bdbch commented Apr 8, 2025

Hey @duhaime!

The reason this happens is that we're using a specific regex triggering as soon as a whitespace is inserted. First this helps us to only do it once when a full link was inserted and the full link is known to the editor to be set as the links href but also for performance reasons as we'd otherwise always would have to check the whole text node for links.

I checked what Notion does and it works the same over there. Is there a specific reason why you'd need to not want a whitespace as a trigger except maybe adding a link to the end of a sentence?

@alexvcasillas
Copy link
Contributor

Hey @duhaime this is actually not an issue, the editor requires a whitespace to properly trigger a lookup for links, if you don't add a whitespace, it means that the word didn't end, so it cannot be used as a lookup. It won't happen when you copy and paste links because there we we act at the moment of parsing the copied text so we can identify links. But while you're writing it requires a whispace so we can trigger for a link identification :)

@duhaime
Copy link
Author

duhaime commented Apr 8, 2025

hey guys! Thanks for the fast follow up. The intended behavior here is fundamentally up to you, but for what it's worth, the current logic feels flawed to me. I think Slack's model is much nicer--as soon as you type a valid link with a valid tld, the editor linkifies the mark.

Screen.Recording.2025-04-08.at.11.46.29.AM.mov

On tiptap, though, the logic is such that you can't end a sentence with a link :/

Screen.Recording.2025-04-08.at.11.51.28.AM.mov

@bagratinho
Copy link

@duhaime you can acheive the desired behaviour by extending tiptap Link extension like this

const LINK_AUTO_DETECT_DEBOUNCE_INTERVAL = 300

export const debouncedHandleTextInput = debounce((editor) => {
  if (!editor) return false

  const { state, dispatch } = editor.view
  const { schema, selection, doc } = state
  const { from } = selection

  const textBeforeCursor = doc.textBetween(0, from, ' ')
  const textAfterCursor = doc.textBetween(from, doc.nodeSize - 2, ' ')
  const wordsArray = textAfterCursor.split(/\s+/)

  const firstWord = wordsArray[0]
  const fullText = textBeforeCursor + firstWord

  const linkExtension = editor.extensionManager.extensions.find(ext => ext.name === 'link')
  const protocols = linkExtension?.options.protocols || []
  const links = find(fullText).filter(link => protocols.some(protocol => link.href.toLowerCase().startsWith(protocol)))

  if (links.length > 0) {
    const lastLink = links[links.length - 1]

    const linkStart = from - textBeforeCursor.length + lastLink.start
    const linkEnd = linkStart + (lastLink.end - lastLink.start)

    dispatch(
      state.tr.addMark(
        linkStart,
        linkEnd,
        schema.marks.link.create({ href: lastLink.href })
      )
    )
  }
}, LINK_AUTO_DETECT_DEBOUNCE_INTERVAL)

export const debouncedHandleDOMEvents = debounce((view) => {
  if (!view) return false

  const { state, dispatch } = view
  const { tr, schema } = state
  let modified = false

  state.doc.descendants((node, pos) => {
    if (!node.isText) return

    node.marks.forEach(mark => {
      if (mark.type.name === 'link') {
        const text = node.text
        const href = mark.attrs.href
        const detectedLinks = find(text)
        const validLink = detectedLinks.find(link => link.value === text)

        // Remove the mark if the link is no longer a valid link
        // Or update the mark if the link has changed
        if (!validLink) {
          tr.removeMark(pos, pos + node.nodeSize, schema.marks.link)
          modified = true
        } else if (validLink.href !== href) {
          tr.addMark(
            pos,
            pos + node.nodeSize,
            schema.marks.link.create({ href: validLink.href })
          )
          modified = true
        }
      }
    })
  })

  if (modified) {
    dispatch(tr)
  }
}, LINK_AUTO_DETECT_DEBOUNCE_INTERVAL)

function linkAutoDetectPlugin (editor) {
  return new Plugin({
    key: new PluginKey('linkAutoDetect'),
    props: {
      handleTextInput (view, from, to, text) {
        debouncedHandleTextInput(editor)
        return false
      },
      handleDOMEvents: {
        input (view) {
          debouncedHandleDOMEvents(view)
          return false
        }
      }
    }
  })
}

export const Link = LinkTipTap.extend({
  inclusive: false,

  addOptions () {
    return {
      ...this.parent?.(),
      autolink: false // Disables tiptap default autolink behavior with space character
    }
  },

  addProseMirrorPlugins () {
    const plugins = this.parent()

    return [
      linkAutoDetectPlugin(this.editor),
      ...plugins
    ]
  }
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Category: Open Source The issue or pull reuqest is related to the open source packages of Tiptap. Type: Bug The issue or pullrequest is related to a bug
Projects
Status: Needs Triage
Development

No branches or pull requests

4 participants