OverviewFilesCommitsIssuesDocsReleasesPeers

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

177 lines · javascript ref:
import { Extension } from '@tiptap/core'
import { NODE_ATTR } from '../constants.js'
import { Plugin } from 'prosemirror-state'
import { getNodeTextLength, getDocTextLength } from './helpers/index.js'

export const Submit = Extension.create({
  name: 'submit',

  addOptions () {
    return {
      onSubmit: () => {},
      maxLength: () => 100000,
      isSubmitDisabledInitial: () => false
    }
  },

  addStorage () {
    return {
      charNumber: 0,
      isSubmitDisabled: true,
      isValid: true
    }
  },

  addCommands () {
    return {
      getFiles: () => ({ editor }) => {
        const { content } = editor.getJSON()
        return (content || []).reduce((acc, c) => {
          if (c.attrs?.[NODE_ATTR.ID]) acc.push(c.attrs[NODE_ATTR.ID])
          return acc
        }, [])
      },

      getLinkPreviews: () => ({ editor }) => {
        const { content } = editor.getJSON()
        return (content || []).reduce((acc, c) => {
          if (c.type === 'link-preview' && c.attrs?.url) {
            acc[c.attrs.url] = {
              url: c.attrs.url,
              title: c.attrs.title,
              image: c.attrs.image,
              description: c.attrs.description
            }
          }
          return acc
        }, {})
      },

      submit:
        () =>
          ({ editor, commands }) => {
            const { onSubmit } = this.options

            if (this.storage.isSubmitDisabled) {
              return false
            }

            const doc = editor.getJSON()
            const linkPreviews = commands.getLinkPreviews()
            const files = commands.getFiles()
            commands.clearContent(true)
            onSubmit(doc, files, linkPreviews)
            editor.emit?.('submit', { doc, files, linkPreviews })
            return true
          }
    }
  },

  addKeyboardShortcuts () {
    return {
      Enter: ({ editor }) => {
        const { emoji, mention } = editor.storage
        if (emoji?.suggestionOpen || mention?.suggestionOpen) {
          return false
        }
        editor.commands.submit()
        return true
      }
    }
  },

  addProseMirrorPlugins () {
    const limit = this.options.maxLength()

    return [
      new Plugin({
        state: {
          init: (_, state) => state.doc,
          apply: (tr, prev, oldState, newState) =>
            tr.docChanged ? newState.doc : prev
        },
        view: (view) => ({
          update: () => {
            const doc = view.state.doc
            const newCharNumber = getDocTextLength(doc)
            const newIsValid = newCharNumber <= limit
            const newIsSubmitDisabled =
              newCharNumber === 0 ||
              !newIsValid ||
              this.options.isSubmitDisabledInitial()

            const changed =
              this.storage.charNumber !== newCharNumber ||
              this.storage.isValid !== newIsValid ||
              this.storage.isSubmitDisabled !== newIsSubmitDisabled

            if (!changed) return

            this.storage.charNumber = newCharNumber
            this.storage.isValid = newIsValid
            this.storage.isSubmitDisabled = newIsSubmitDisabled

            view.dispatch(view.state.tr.setMeta('submit:update', Date.now()))
          }
        })
      }),
      new Plugin({
        appendTransaction: (transactions, oldState, newState) => {
          if (transactions.some(tr => tr.getMeta('truncate'))) return null
          const newTextLength = getDocTextLength(newState.doc)

          if (newTextLength <= limit) {
            return null
          }

          let virtualPos = 0
          let truncatePos = null

          newState.doc.descendants((node, pos) => {
            if (truncatePos !== null) return false

            let nodeContribution = 0

            nodeContribution = getNodeTextLength(node)
            if (virtualPos + nodeContribution > limit) {
              const overflow = virtualPos + nodeContribution - limit
              if (node.isText) {
                truncatePos = pos + (node.text.length - overflow)
              } else {
                truncatePos = pos
              }
              return false
            }

            virtualPos += nodeContribution
            return true
          })

          if (truncatePos === null) {
            truncatePos = newState.doc.content.size
          }

          const tr = newState.tr
          tr.delete(truncatePos, newState.doc.content.size)
          tr.setMeta('truncate', true)

          return tr
        },
        filterTransaction: (tr, state) => {
          if (this.storage.charNumber < limit) {
            return true
          }

          const newLen = getDocTextLength(tr.doc)
          if (newLen <= this.storage.charNumber) {
            return true
          }

          return false
        }
      })
    ]
  }

})