get it working on web
This commit is contained in:
@@ -28,6 +28,7 @@ import {normalizeTextStyles} from '#/alf/typography'
|
|||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {Text} from '../../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
import {createSuggestion} from './web/Autocomplete'
|
import {createSuggestion} from './web/Autocomplete'
|
||||||
|
import {CharacterLimitDecorator} from './web/CharacterLimitDecorator'
|
||||||
import {type Emoji} from './web/EmojiPicker'
|
import {type Emoji} from './web/EmojiPicker'
|
||||||
import {LinkDecorator} from './web/LinkDecorator'
|
import {LinkDecorator} from './web/LinkDecorator'
|
||||||
import {TagDecorator} from './web/TagDecorator'
|
import {TagDecorator} from './web/TagDecorator'
|
||||||
@@ -94,6 +95,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
|||||||
TiptapText,
|
TiptapText,
|
||||||
History,
|
History,
|
||||||
Hardbreak,
|
Hardbreak,
|
||||||
|
CharacterLimitDecorator,
|
||||||
],
|
],
|
||||||
[autocomplete, placeholder],
|
[autocomplete, placeholder],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import {Extension} from '@tiptap/core'
|
||||||
|
import {type Node as ProsemirrorNode} from '@tiptap/pm/model'
|
||||||
|
import {Plugin, PluginKey} from '@tiptap/pm/state'
|
||||||
|
import {Decoration, DecorationSet} from '@tiptap/pm/view'
|
||||||
|
import Graphemer from 'graphemer'
|
||||||
|
|
||||||
|
import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
|
||||||
|
|
||||||
|
export const CharacterLimitDecorator = Extension.create({
|
||||||
|
name: 'characterLimit',
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [characterLimitPlugin()]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function getDecorations(doc: ProsemirrorNode, splitter: Graphemer) {
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
// Calculate the actual text content including mentions
|
||||||
|
let fullText = ''
|
||||||
|
doc.descendants(node => {
|
||||||
|
if (node.isText) {
|
||||||
|
fullText += node.text || ''
|
||||||
|
} else if (node.type.name === 'mention') {
|
||||||
|
fullText += `@${node.attrs?.id || ''}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const graphemes = splitter.splitGraphemes(fullText)
|
||||||
|
|
||||||
|
if (graphemes.length > MAX_GRAPHEME_LENGTH) {
|
||||||
|
// Find the string position where the limit is exceeded
|
||||||
|
const validText = graphemes.slice(0, MAX_GRAPHEME_LENGTH).join('')
|
||||||
|
const limitPos = validText.length
|
||||||
|
|
||||||
|
let currentPos = 0
|
||||||
|
|
||||||
|
// Walk through the document to find text nodes and mentions
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (node.isText) {
|
||||||
|
const nodeText = node.text || ''
|
||||||
|
const nodeStart = currentPos
|
||||||
|
const nodeEnd = currentPos + nodeText.length
|
||||||
|
|
||||||
|
// Check if this text node contains over-limit characters
|
||||||
|
if (nodeEnd > limitPos) {
|
||||||
|
const overLimitStart = Math.max(0, limitPos - nodeStart)
|
||||||
|
const overLimitEnd = nodeText.length
|
||||||
|
|
||||||
|
if (overLimitStart < overLimitEnd) {
|
||||||
|
// Create decoration for the over-limit portion
|
||||||
|
const from = pos + overLimitStart
|
||||||
|
const to = pos + overLimitEnd
|
||||||
|
|
||||||
|
decorations.push(
|
||||||
|
Decoration.inline(from, to, {
|
||||||
|
style: 'background-color: rgba(248, 113, 113, 0.3);',
|
||||||
|
class: 'character-limit-exceeded',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPos = nodeEnd
|
||||||
|
} else if (node.type.name === 'mention') {
|
||||||
|
// Handle mention nodes - they contribute to the text length
|
||||||
|
const mentionText = `@${node.attrs?.id || ''}`
|
||||||
|
const nodeStart = currentPos
|
||||||
|
const nodeEnd = currentPos + mentionText.length
|
||||||
|
|
||||||
|
// Check if this mention is over the limit
|
||||||
|
if (nodeStart >= limitPos) {
|
||||||
|
// Entire mention is over the limit
|
||||||
|
decorations.push(
|
||||||
|
Decoration.node(pos, pos + node.nodeSize, {
|
||||||
|
style: 'background-color: rgba(248, 113, 113, 0.3);',
|
||||||
|
class: 'character-limit-exceeded',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPos = nodeEnd
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorations)
|
||||||
|
}
|
||||||
|
|
||||||
|
function characterLimitPlugin() {
|
||||||
|
const splitter = new Graphemer()
|
||||||
|
|
||||||
|
const characterLimitPlugin: Plugin = new Plugin({
|
||||||
|
key: new PluginKey('characterLimit'),
|
||||||
|
|
||||||
|
state: {
|
||||||
|
init: (_, {doc}) => getDecorations(doc, splitter),
|
||||||
|
apply: (transaction, decorationSet) => {
|
||||||
|
if (transaction.docChanged) {
|
||||||
|
return getDecorations(transaction.doc, splitter)
|
||||||
|
}
|
||||||
|
return decorationSet.map(transaction.mapping, transaction.doc)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
return characterLimitPlugin.getState(state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return characterLimitPlugin
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user