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

A brand new CharacterCount extension #2256

Merged
merged 9 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions demos/src/Examples/Community/React/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ export default () => {
})

const percentage = editor
? Math.round((100 / limit) * editor.getCharacterCount())
? Math.round((100 / limit) * editor.storage.characterCount.characters())
: 0

return (
<div>
<EditorContent editor={editor} />
{editor
&& <div className={`character-count ${editor.getCharacterCount() === limit ? 'character-count--warning' : ''}`}>
&& <div className={`character-count ${editor.storage.characterCount.characters() === limit ? 'character-count--warning' : ''}`}>
<svg
height="20"
width="20"
Expand Down Expand Up @@ -73,7 +73,7 @@ export default () => {
</svg>

<div className="character-count__text">
{editor.getCharacterCount()}/{limit} characters
{editor.storage.characterCount.characters()}/{limit} characters
</div>
</div>
}
Expand Down
6 changes: 3 additions & 3 deletions demos/src/Examples/Community/Vue/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div>
<editor-content :editor="editor" />

<div v-if="editor" :class="{'character-count': true, 'character-count--warning': editor.getCharacterCount() === limit}">
<div v-if="editor" :class="{'character-count': true, 'character-count--warning': editor.storage.characterCount.characters() === limit}">
<svg
height="20"
width="20"
Expand Down Expand Up @@ -34,7 +34,7 @@
</svg>

<div class="character-count__text">
{{ editor.getCharacterCount() }}/{{ limit }} characters
{{ editor.storage.characterCount.characters() }}/{{ limit }} characters
</div>
</div>
</div>
Expand Down Expand Up @@ -87,7 +87,7 @@ export default {

computed: {
percentage() {
return Math.round((100 / this.limit) * this.editor.getCharacterCount())
return Math.round((100 / this.limit) * this.editor.storage.characterCount.characters())
},
},

Expand Down
4 changes: 3 additions & 1 deletion demos/src/Extensions/CharacterCount/React/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export default () => {
<EditorContent editor={editor} />

<div className="character-count">
{editor.getCharacterCount()}/{limit} characters
{editor.storage.characterCount.characters()}/{limit} characters
<br />
{editor.storage.characterCount.words()} words
</div>
</div>
)
Expand Down
4 changes: 3 additions & 1 deletion demos/src/Extensions/CharacterCount/Vue/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
<editor-content :editor="editor" />

<div class="character-count" v-if="editor">
{{ editor.getCharacterCount() }}/{{ limit }} characters
{{ editor.storage.characterCount.characters() }}/{{ limit }} characters
<br>
{{ editor.storage.characterCount.words() }} words
</div>
</div>
</template>
Expand Down
13 changes: 3 additions & 10 deletions docs/api/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)
Expand Down
50 changes: 37 additions & 13 deletions docs/api/extensions/character-count.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -28,21 +28,45 @@ CharacterCount.configure({
})
```

## Source code
[packages/extension-character-count/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-character-count/)
### mode

## Usage
https://embed.tiptap.dev/preview/Extensions/CharacterCount
The mode by which the size is calculated.

## 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.
Default: `'textSize'`

```js
new Editor({
onUpdate({ editor }) {
const wordCount = editor.state.doc.textContent.split(' ').length

console.log(wordCount)
},
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
4 changes: 4 additions & 0 deletions packages/core/src/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,12 @@ export class Editor extends EventEmitter<EditorEvents> {

/**
* 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
}

Expand Down
122 changes: 111 additions & 11 deletions packages/extension-character-count/src/character-count.ts
Original file line number Diff line number Diff line change
@@ -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 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<CharacterCountOptions>({
export const CharacterCount = Extension.create<CharacterCountOptions, CharacterCountStorage>({
name: 'characterCount',

addOptions() {
return {
limit: 0,
mode: 'textSize',
}
},

addProseMirrorPlugins() {
const { options } = this
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() {
return [
new Plugin({
key: new PluginKey('characterCount'),
filterTransaction: (transaction, state) => {
const limit = this.options.limit

// 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')

key: pluginKey,
// 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

appendTransaction: (transactions, oldState, newState) => {
const length = newState.doc.content.size
// It’s probably a bad idea to mutate transactions within `filterTransaction`
// but for now this is working fine.
transaction.deleteRange(from, to)

if (options.limit && length > options.limit) {
return newState.tr.insertText('', options.limit + 1, length)
// 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
},
}),
]
Expand Down