Making a plugin to create links/URL

/**
 * This plugin allows for the creation of standalone links in the editor.
 * It handles the pasting of URLs and the conversion of selected text to links.
 */
function createStandaloneLinkPlugin() {
  return new Plugin({
    props: {
      handlePaste(view, event, slice) {
        event.preventDefault()

        const urlRegex =
          /^(?:https?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/

        const httpRegex = /^https?:\/\//

        let tr = view.state.tr
        const { empty, from, to } = tr.selection
        let handled = false

        // Check if pasted content is a single URL
        const pastedText = slice.content.textBetween(
          0,
          slice.content.size,
          '\n',
        )
        const isUrl = urlRegex.test(pastedText)

        if (isUrl && !empty) {
          // If there's a selection and pasted content is a URL, convert selection to link
          const url = httpRegex.test(pastedText)
            ? pastedText
            : `http://${pastedText}`
          const linkMark = view.state.schema?.marks?.['link']?.create({
            href: url,
          })
          if (linkMark) {
            tr = tr.addMark(from, to, linkMark)
          }
          handled = true
        } else {
          // this part will create a link if you paste a url without a selection
          tr = tr.replaceSelection(slice)
          const insertPos = from

          slice.content.nodesBetween(0, slice.content.size, (node, nodePos) => {
            if (node.isText && node.text) {
              const match = node.text.match(urlRegex)?.[0]
              if (match) {
                handled = true
                const url = httpRegex.test(match) ? match : `http://${match}`
                const linkFrom =
                  insertPos + nodePos + node.text.indexOf(match) - 1
                const linkTo = linkFrom + match.length
                const linkMark = view.state.schema?.marks?.['link']?.create({
                  href: url,
                })
                if (linkMark) {
                  tr = tr.addMark(linkFrom, linkTo, linkMark)
                }
              }
            }
          })
        }

        if (handled) {
          view.dispatch(tr)
          return true
        }
        return false
      },
    },
  })
}

I just made this plugin to make standalone links and an existing text node into a link.

Also, if you paste a URL in the editor the handlePaste function will handle it and convert it to a link text/ A tag in HTML

you also need a link attribute in you’re schema for this plugin to work

I am providing the link atri/markSpec to help start with the schema building feel free to edit it.

 link: {
      attrs: {
        href: { validate: 'string' },
        title: { default: null, validate: 'string|null' },
      },
      inclusive: false,
      parseDOM: [
        {
          tag: 'a[href]',
          getAttrs(dom: HTMLElement) {
            return {
              href: dom.getAttribute('href'),
              title: dom.getAttribute('title'),
            }
          },
        },
      ],
      toDOM(node) {
        const { href, title } = node.attrs
        return ['a', { href, title, target: '_blank' }, 0]
      },
    } as MarkSpec,

And here is a bonus Plugin that works with the above plugin that will show the URL below the link on hover

/**
 * This plugin underlines links on hover and logs their URLs to a tooltip.
 */
function createLinkHoverPlugin() {
  let tooltip: HTMLElement | null = null
  let hoveredLink: HTMLAnchorElement | null = null

  function createTooltip() {
    const el = document.createElement('div')
//Use your own class name or use object.assign and assign the inline style directly here
// I am using my customer tailwind class name
    el.className =
      'link-tooltip text-primary text-size-label bg-fill absolute outline-inner outline-default rounded-md p-xs z-[9999] hidden'

    document.body.appendChild(el)
    return el
  }

  function updateTooltip(view: EditorView, href: string, pos: number) {
    if (!tooltip) tooltip = createTooltip()
    tooltip.textContent = href
    setTimeout(() => {
      if (tooltip) tooltip.style.display = 'block'
    }, 500)

    const rect = view.coordsAtPos(pos)

    tooltip.style.left = `${rect.left}px`
    tooltip.style.top = `${rect.bottom + 3}px`
  }

  function hideTooltip() {
    if (tooltip) tooltip.style.display = 'none'
  }

  function addUnderline(element: HTMLAnchorElement) {
    element.style.textDecoration = 'underline'
  }

  function removeUnderline(element: HTMLAnchorElement) {
    element.style.textDecoration = ''
  }

  return new Plugin({
    view() {
      return {
        destroy() {
          if (tooltip) {
            document.body.removeChild(tooltip)
            tooltip = null
          }
          if (hoveredLink) {
            removeUnderline(hoveredLink)
            hoveredLink = null
          }
        },
      }
    },
    props: {
      handleDOMEvents: {
        mouseover(view, event) {
          if (!(event.target instanceof HTMLAnchorElement)) return false

          const pos = view.posAtDOM(event.target, 0)

          if (pos === null) return false
          if (event.target.tagName === 'A') {
            const link = event.target.href

            if (link) {
              updateTooltip(view, link, pos)
              addUnderline(event.target)
              hoveredLink = event.target
            } else {
              hideTooltip()
            }
          }
          return false
        },
        mouseout() {
          hideTooltip()
          if (hoveredLink) {
            removeUnderline(hoveredLink)
            hoveredLink = null
          }
          return false
        },
      },
    },
  })
}

I hope this helps someone

1 Like