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
}
})
]
}
})