Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chars limit / chars count #629

Closed
NitzanShifrin opened this issue Mar 11, 2020 · 16 comments
Closed

Chars limit / chars count #629

NitzanShifrin opened this issue Mar 11, 2020 · 16 comments
Labels
Type: Feature The issue or pullrequest is a new feature

Comments

@NitzanShifrin
Copy link

Is there any way to get the number of chars in the editor?
Not working well when adding elements (<span> aaa </span>) into it

@NitzanShifrin NitzanShifrin added the Type: Feature The issue or pullrequest is a new feature label Mar 11, 2020
@BrianHung
Copy link
Contributor

Find the highest-most node, and get the length of its textContent.

@Leecason
Copy link
Contributor

Leecason commented Mar 19, 2020

chars count: https://github.com/Leecason/element-tiptap/blob/master/src/components/ElementTiptap.vue#L116

@andreasvirkus
Copy link

andreasvirkus commented Mar 20, 2020

Is there an easy way to enforce a character limit for the editor?
(I'm aware of #293, but there doesn't seem to be a resolution under that discussion)

@BrianHung
Copy link
Contributor

BrianHung commented Mar 21, 2020

@andreasvirkus

Did you try this solution mentioned in that thread? #293 (comment)

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.

@andreasvirkus
Copy link

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

@BrianHung
Copy link
Contributor

BrianHung commented Mar 29, 2020

@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’ll 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.

@andreasvirkus
Copy link

andreasvirkus commented Apr 2, 2020

@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

@philippkuehn
Copy link
Contributor

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

@andreasvirkus
Copy link

andreasvirkus commented Apr 2, 2020

@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?

@simpsonphile
Copy link

same problem, any solutions?

@andreasvirkus
Copy link

@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 #629 (comment)), but it suffices for now

@simpsonphile
Copy link

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

@hanspagel
Copy link
Contributor

Thanks for sharing!

@hanspagel hanspagel mentioned this issue Sep 11, 2020
6 tasks
@denis3509
Copy link

denis3509 commented Nov 18, 2020

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;
         }

       }

     })
   ];
 }
}

@hanspagel
Copy link
Contributor

I’m closing this here for now. This issue is added to #819 and we’ll consider to add it to tiptap v2. Thanks everyone!

@hanspagel
Copy link
Contributor

Just wanted let you know that we added a CharacterCount extension to tiptap 2. Thanks again for sharing! 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Feature The issue or pullrequest is a new feature
Projects
None yet
Development

No branches or pull requests

8 participants