Is there any way to get the number of chars in the editor?
Not working well when adding elements (<span> aaa </span>) into it
Find the highest-most node, and get the length of its textContent.
Is there an easy way to enforce a character limit for the editor?
(I'm aware of https://github.com/scrumpy/tiptap/issues/293, but there doesn't seem to be a resolution under that discussion)
@andreasvirkus
Did you try this solution mentioned in that thread? https://github.com/scrumpy/tiptap/issues/293#issuecomment-578231063
What's being proposed there is to create a plugin which handlesTextInput and overrides the default behavior by returning true. It should be easy to understand if you know the ProseMirror API which TipTap builds on.
Thanks @BrianHung. Do you happen to know of a way to cut the pasted text to keep everything that fits within the character limit? Couldn't find anything related to ProseMirror on that subject
@andreasvirkus
On a paste event (handlePaste), use the length of text being pasted and the current document length to create a cutoff number.
Then use that cutoff number to slice into the pasted text, followed by inserting the remaining text via a ProseMirror transaction (tr.insertText). You鈥檒l have to look into ProseMirror docs for api on that.
Alternatively, another strategy would be to create a plugin that monitors all transactions, and deletes text at the end of the document if the limit is exceeded.
Of course, the decision will depend on how you want to handle edge cases. For example, what if you pasted into the _middle_ of a document which causes the limit to be exceeded: should you cut the pasted text, or cut the end of the document? The former strategy of handleTextInput and handlePaste offers more nuance in dealing with that.
@BrianHung Thanks! I still have a hard time figuring out how to cut the slice parameter to size, since it's filled with nested Nodes/fragments of its own.
I propose that
a) an extension would be provided by tiptap
b) a guide on how to implement it, since it's quite cumbersome to figure out (the edge cases you mentioned don't need a solution, but could be mentioned in the guide as thinking points)
I'm willing to help write up the guide (if that's acceptable) once I have a solution to share
I had once built such an extension. Maybe this helps you?
import { Extension, Plugin } from 'tiptap'
export default class MaxSize extends Extension {
get name() {
return 'maxSize'
}
get defaultOptions() {
return {
maxSize: null,
}
}
get plugins() {
return [
new Plugin({
filterTransaction: transaction => {
if (!transaction.docChanged || !this.options.maxSize) {
return true
}
const size = transaction.doc && transaction.doc.textContent.length
const maxSizeReached = size > this.options.maxSize
return !maxSizeReached
}
}),
]
}
}
Use it: new MaxSize({ maxSize: 140 })
@philippkuehn thanks for sharing! Would there be any way to _not_ cancel the transaction completely and only insert part of the text that fits? Or since filterTransaction can only return a boolean value ProseMirror-side, i'd have to achieve that behaviour with editorProps.handlePaste?
same problem, any solutions?
@simpsonphile this is the way we've currently handled it
new Editor({
editorProps: {
handleTextInput(view) {
if (view.state.doc.textContent.length >= editorMaxLength && view.state.selection.empty) {
return true
}
},
handlePaste(view, event, slice) {
if (view.state.doc.textContent.length + slice.size > editorMaxLength) {
slice.content = new DocumentFragment()
}
},
}
})
The paste experience isn't the greatest (we don't handle the edge cases mentioned in https://github.com/ueberdosis/tiptap/issues/629#issuecomment-605609701), but it suffices for now
Ok I think I have found a solution. It also support pasting
import { Extension, Plugin } from 'tiptap'
export default class MaxSize extends Extension {
get name () {
return 'maxSize'
}
get defaultOptions () {
return {
maxSize: null
}
}
get plugins () {
return [
new Plugin({
appendTransaction: (transactions, oldState, newState) => {
const max = this.options.maxSize
const oldLength = oldState.doc.content.size
const newLength = newState.doc.content.size
if (newLength > max && newLength > oldLength) {
let newTr = newState.tr
newTr.insertText('', max + 1, newLength)
return newTr
}
}
})
]
}
}
Thanks for sharing!
Size limit (not text length). It counts not only text symbols, but supports cutting pasted content if it doesnt fits.
```
import { Extension, Plugin, TextSelection } from 'tiptap';
export default class MaxSize extends Extension {
get name() {
return 'maxSize';
}
get defaultOptions() {
return {
maxSize: 200
};
}
get plugins() {
return [
new Plugin({
appendTransaction: (transactions, oldState, newState) => {
let max = this.options.maxSize || this.defaultOptions.maxSize;
max = max +2;
const oldDocSize = oldState.doc.content.size;
const newDocSize = newState.doc.content.size;
const newResPos = newState.selection.$head;
if (newDocSize > oldDocSize && newDocSize > max) {
const overPaste = newDocSize - max;
const newTextSelection = TextSelection.create(
newState.doc,
(newResPos.pos - overPaste) > 0
? newResPos.pos - overPaste
: 0,
newResPos.pos);
let newTr = newState.tr;
newTr.setSelection(newTextSelection);
newTr.deleteSelection();
return newTr;
}
}
})
];
}
}
```
I鈥檓 closing this here for now. This issue is added to #819 and we鈥檒l consider to add it to tiptap v2. Thanks everyone!
Just wanted let you know that we added a CharacterCount extension to tiptap 2. Thanks again for sharing! 馃檶
Most helpful comment
Ok I think I have found a solution. It also support pasting