OverviewFilesCommitsIssuesDocsReleasesPeers

holepunchto-deps / @holepunchto / keet-editor / src / extensions / link.js

141 lines · javascript ref:
import { Link as LinkTipTap } from '@tiptap/extension-link'
import { Plugin, PluginKey } from 'prosemirror-state'
import { find, registerCustomProtocol, init, reset } from 'linkifyjs'

import { NODE_TYPE, LINK_PROTOCOLS_DEFAULT, NODE_ATTR } from '../constants.js'

function gifPreviewHandler (options) {
  return new Plugin({
    key: new PluginKey('handleGifPreview'),
    props: {
      handlePaste: (view, event, slice) => {
        let textContent = ''

        slice.content.forEach((node) => {
          textContent += node.textContent
        })

        const links = find(textContent).filter((link) => link.href.includes('.gif'))

        for (const link of links) {
          window.fetch(link.href)
            .then((response) => {
              if (!response.ok) {
                throw new Error('Error fetching url')
              }

              const contentType = response.headers.get('content-type')
              if (contentType !== 'image/gif') {
                throw new Error('Not a gif')
              }

              return response.blob()
            })
            .then(async (blob) => {
              try {
                const { state, dispatch } = view
                const { schema, doc } = state
                const name = options.createRandomName(link.href)

                const file = new window.File([blob], name, { type: 'image/gif' })
                const { id, file: { url } } = await options.editor.commands.upload([file])

                const node = schema.nodes[NODE_TYPE.FILE_IMAGE].create({
                  [NODE_ATTR.ID]: id,
                  src: url,
                  name,
                  alt: name,
                  title: name
                })

                const transaction = view.state.tr.insert(doc.content.size, node)
                dispatch(transaction)
              } catch (e) {
                console.log('ERROR', e)
              }
            })
            .catch((e) => console.log('ERROR', e))
        }

        return false
      }
    }
  })
}

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

  onCreate () {
    reset()
    // reset before creation prevents following console warning

    // linkifyjs: already initialized - will not register
    // custom scheme "pear" until manual call of linkify.init().
    // Register all schemes and plugins before invoking linkify the first time.

    // LinkTipTap.onCreate
    if (this.options.validate && !this.options.shouldAutoLink) {
      // Copy the validate function to the shouldAutoLink option
      this.options.shouldAutoLink = this.options.validate
      console.warn(
        'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.'
      )
    }

    this.options.protocols.forEach(protocol => {
      if (typeof protocol === 'string') {
        registerCustomProtocol(protocol)
        return
      }
      registerCustomProtocol(protocol.scheme, protocol.optionalSlashes)
    })

    init() // linkify js expect init to be called after registerCustomProtocol
  },

  addAttributes () {
    return {
      href: {
        default: null,
        parseHTML (element) {
          return element.getAttribute('href')
        }
      },
      pasted: { default: false },
      target: {
        default: this.options.HTMLAttributes.target
      },
      rel: {
        default: this.options.HTMLAttributes.rel
      }
    }
  },

  onDestroy () {
    // We need this listener to overwrite the Link internal onDestroy to
    // disable reseting linkify, going to be reset on pre create
  },

  addOptions () {
    return {
      ...this.parent?.(),
      protocols: LINK_PROTOCOLS_DEFAULT
    }
  },

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

    return [
      gifPreviewHandler({
        editor: this.editor,
        type: this.type,
        ...this.options
      }),
      ...plugins
    ]
  }

})