diff --git a/docs/api/editor.md b/docs/api/editor.md
index 7755a053..6b01fac4 100644
--- a/docs/api/editor.md
+++ b/docs/api/editor.md
@@ -92,13 +92,6 @@ editor.isActive('heading', { level: 2 })
editor.isActive({ textAlign: 'justify' })
```
-### getCharacterCount()
-Get the number of characters for the current document.
-
-```js
-editor.getCharacterCount()
-```
-
### registerPlugin()
Register a ProseMirror plugin.
@@ -124,14 +117,14 @@ editor.setOptions({
},
})
```
-
+
### setEditable()
Update editable state of the editor.
-
+
| Parameter | Type | Description |
| --------- | ------- | ------------------------------------------------------------- |
| editable | boolean | `true` when the user should be able to write into the editor. |
-
+
```js
// Make the editor read-only
editor.setEditable(false)
diff --git a/docs/api/extensions/character-count.md b/docs/api/extensions/character-count.md
index c82ab1a5..683f529c 100644
--- a/docs/api/extensions/character-count.md
+++ b/docs/api/extensions/character-count.md
@@ -18,7 +18,7 @@ npm install @tiptap/extension-character-count
### limit
-The maximum number of characters that should be allowed. |
+The maximum number of characters that should be allowed.
Default: `0`
@@ -28,21 +28,45 @@ CharacterCount.configure({
})
```
+### mode
+
+The mode by which the size is calculated.
+
+Default: `'textSize'`
+
+```js
+CharacterCount.configure({
+ mode: 'nodeSize',
+})
+```
+
+## Storage
+
+### characters()
+Get the number of characters for the current document.
+
+```js
+editor.storage.characterCount.characters()
+
+// Get the size of a specific node.
+editor.storage.characterCount.characters({ node: someCustomNode })
+
+// Overwrite the default `mode`.
+editor.storage.characterCount.characters({ mode: 'nodeSize' })
+```
+
+### words()
+Get the number of words for the current document.
+
+```js
+editor.storage.characterCount.words()
+
+// Get the number of words for a specific node.
+editor.storage.characterCount.words({ node: someCustomNode })
+```
+
## Source code
[packages/extension-character-count/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-character-count/)
## Usage
https://embed.tiptap.dev/preview/Extensions/CharacterCount
-
-## Count words, emojis, letters …
-Want to count words instead? Or emojis? Or the letter *a*? Sure, no problem. You can access the `textContent` directly and count whatever you’re into.
-
-```js
-new Editor({
- onUpdate({ editor }) {
- const wordCount = editor.state.doc.textContent.split(' ').length
-
- console.log(wordCount)
- },
-})
-```
diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts
index e3cf3f0f..f23c64a7 100644
--- a/packages/core/src/Editor.ts
+++ b/packages/core/src/Editor.ts
@@ -449,8 +449,12 @@ export class Editor extends EventEmitter {
/**
* Get the number of characters for the current document.
+ *
+ * @deprecated
*/
public getCharacterCount(): number {
+ console.warn('[tiptap warn]: "editor.getCharacterCount()" is deprecated. Please use "editor.storage.characterCount.characters()" instead.')
+
return this.state.doc.content.size - 2
}
diff --git a/packages/extension-character-count/src/character-count.ts b/packages/extension-character-count/src/character-count.ts
index 5e4c4a46..2aee88d0 100644
--- a/packages/extension-character-count/src/character-count.ts
+++ b/packages/extension-character-count/src/character-count.ts
@@ -1,35 +1,135 @@
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
-
-export const pluginKey = new PluginKey('characterLimit')
+import { Node as ProseMirrorNode } from 'prosemirror-model'
export interface CharacterCountOptions {
- limit?: number,
+ /**
+ * The maximum number of characters that should be allowed. Defaults to `0`.
+ */
+ limit: number,
+ /**
+ * The mode by which the size is calculated. Defaults to 'textSize'.
+ */
+ mode: 'textSize' | 'nodeSize',
}
-export const CharacterCount = Extension.create({
+export interface CharacterCountStorage {
+ /**
+ * Get the number of characters for the current document.
+ */
+ characters?: (options: {
+ node?: ProseMirrorNode,
+ mode?: 'textSize' | 'nodeSize',
+ }) => number,
+
+ /**
+ * Get the number of words for the current document.
+ */
+ words?: (options: {
+ node?: ProseMirrorNode,
+ }) => number,
+}
+
+export const CharacterCount = Extension.create({
name: 'characterCount',
addOptions() {
return {
limit: 0,
+ mode: 'textSize',
+ }
+ },
+
+ addStorage() {
+ return {
+ characters: undefined,
+ words: undefined,
+ }
+ },
+
+ onBeforeCreate() {
+ this.storage.characters = options => {
+ const node = options?.node || this.editor.state.doc
+ const mode = options?.mode || this.options.mode
+
+ if (mode === 'textSize') {
+ const text = node.textBetween(0, node.content.size, undefined, ' ')
+
+ return text.length
+ }
+
+ return node.nodeSize
+ }
+
+ this.storage.words = options => {
+ const node = options?.node || this.editor.state.doc
+ const text = node.textBetween(0, node.content.size, undefined, ' ')
+ const words = text
+ .split(' ')
+ .filter(word => word !== '')
+
+ return words.length
}
},
addProseMirrorPlugins() {
- const { options } = this
-
return [
new Plugin({
+ key: new PluginKey('characterCount'),
+ filterTransaction: (transaction, state) => {
+ const limit = this.options.limit
- key: pluginKey,
-
- appendTransaction: (transactions, oldState, newState) => {
- const length = newState.doc.content.size
-
- if (options.limit && length > options.limit) {
- return newState.tr.insertText('', options.limit + 1, length)
+ // Nothing has changed or no limit is defined. Ignore it.
+ if (!transaction.docChanged || limit === 0) {
+ return true
}
+
+ const oldSize = this.storage.characters?.({ node: state.doc }) || 0
+ const newSize = this.storage.characters?.({ node: transaction.doc }) || 0
+
+ // Everything is in the limit. Good.
+ if (newSize <= limit) {
+ return true
+ }
+
+ // The limit has already been exceeded but will be reduced.
+ if (oldSize > limit && newSize > limit && newSize <= oldSize) {
+ return true
+ }
+
+ // The limit has already been exceeded and will be increased further.
+ if (oldSize > limit && newSize > limit && newSize > oldSize) {
+ return false
+ }
+
+ const isPaste = transaction.getMeta('paste')
+
+ // Block all exceeding transactions that were not pasted.
+ if (!isPaste) {
+ return false
+ }
+
+ // For pasted content, we try to remove the exceeding content.
+ const pos = transaction.selection.$head.pos
+ const over = newSize - limit
+ const from = pos - over
+ const to = pos
+
+ // It’s probably a bad idea to mutate transactions within `filterTransaction`
+ // but for now this is working fine.
+ transaction.deleteRange(from, to)
+
+ // In some situations, the limit will continue to be exceeded after trimming.
+ // This happens e.g. when truncating within a complex node (e.g. table)
+ // and ProseMirror has to close this node again.
+ // If this is the case, we prevent the transaction completely.
+ const updatedSize = this.storage.characters?.({ node: transaction.doc }) || 0
+
+ if (updatedSize > limit) {
+ return false
+ }
+
+ return true
},
}),
]