@tiptap/extensions 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +18 -0
  3. package/dist/character-count/index.cjs +129 -0
  4. package/dist/character-count/index.cjs.map +1 -0
  5. package/dist/character-count/index.d.cts +62 -0
  6. package/dist/character-count/index.d.ts +62 -0
  7. package/dist/character-count/index.js +102 -0
  8. package/dist/character-count/index.js.map +1 -0
  9. package/dist/drop-cursor/index.cjs +47 -0
  10. package/dist/drop-cursor/index.cjs.map +1 -0
  11. package/dist/drop-cursor/index.d.cts +31 -0
  12. package/dist/drop-cursor/index.d.ts +31 -0
  13. package/dist/drop-cursor/index.js +20 -0
  14. package/dist/drop-cursor/index.js.map +1 -0
  15. package/dist/focus/index.cjs +95 -0
  16. package/dist/focus/index.cjs.map +1 -0
  17. package/dist/focus/index.d.cts +28 -0
  18. package/dist/focus/index.d.ts +28 -0
  19. package/dist/focus/index.js +68 -0
  20. package/dist/focus/index.js.map +1 -0
  21. package/dist/gap-cursor/index.cjs +51 -0
  22. package/dist/gap-cursor/index.cjs.map +1 -0
  23. package/dist/gap-cursor/index.d.cts +25 -0
  24. package/dist/gap-cursor/index.d.ts +25 -0
  25. package/dist/gap-cursor/index.js +24 -0
  26. package/dist/gap-cursor/index.js.map +1 -0
  27. package/dist/index.cjs +421 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +272 -0
  30. package/dist/index.d.ts +272 -0
  31. package/dist/index.js +387 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/placeholder/index.cjs +88 -0
  34. package/dist/placeholder/index.cjs.map +1 -0
  35. package/dist/placeholder/index.d.cts +59 -0
  36. package/dist/placeholder/index.d.ts +59 -0
  37. package/dist/placeholder/index.js +61 -0
  38. package/dist/placeholder/index.js.map +1 -0
  39. package/dist/selection/index.cjs +63 -0
  40. package/dist/selection/index.cjs.map +1 -0
  41. package/dist/selection/index.d.cts +17 -0
  42. package/dist/selection/index.d.ts +17 -0
  43. package/dist/selection/index.js +36 -0
  44. package/dist/selection/index.js.map +1 -0
  45. package/dist/trailing-node/index.cjs +78 -0
  46. package/dist/trailing-node/index.cjs.map +1 -0
  47. package/dist/trailing-node/index.d.cts +28 -0
  48. package/dist/trailing-node/index.d.ts +28 -0
  49. package/dist/trailing-node/index.js +51 -0
  50. package/dist/trailing-node/index.js.map +1 -0
  51. package/dist/undo-redo/index.cjs +66 -0
  52. package/dist/undo-redo/index.cjs.map +1 -0
  53. package/dist/undo-redo/index.d.cts +44 -0
  54. package/dist/undo-redo/index.d.ts +44 -0
  55. package/dist/undo-redo/index.js +39 -0
  56. package/dist/undo-redo/index.js.map +1 -0
  57. package/package.json +114 -0
  58. package/src/character-count/character-count.ts +195 -0
  59. package/src/character-count/index.ts +1 -0
  60. package/src/drop-cursor/drop-cursor.ts +47 -0
  61. package/src/drop-cursor/index.ts +1 -0
  62. package/src/focus/focus.ts +110 -0
  63. package/src/focus/index.ts +1 -0
  64. package/src/gap-cursor/gap-cursor.ts +47 -0
  65. package/src/gap-cursor/index.ts +1 -0
  66. package/src/index.ts +8 -0
  67. package/src/placeholder/index.ts +1 -0
  68. package/src/placeholder/placeholder.ts +129 -0
  69. package/src/selection/index.ts +1 -0
  70. package/src/selection/selection.ts +51 -0
  71. package/src/trailing-node/index.ts +1 -0
  72. package/src/trailing-node/trailing-node.ts +84 -0
  73. package/src/undo-redo/index.ts +1 -0
  74. package/src/undo-redo/undo-redo.ts +86 -0
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@tiptap/extensions",
3
+ "description": "various extensions for tiptap",
4
+ "version": "3.0.0-beta.0",
5
+ "homepage": "https://tiptap.dev",
6
+ "keywords": [
7
+ "tiptap",
8
+ "tiptap extension"
9
+ ],
10
+ "license": "MIT",
11
+ "funding": {
12
+ "type": "github",
13
+ "url": "https://github.com/sponsors/ueberdosis"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": {
19
+ "import": "./dist/index.d.ts",
20
+ "require": "./dist/index.d.cts"
21
+ },
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs"
24
+ },
25
+ "./character-count": {
26
+ "types": {
27
+ "import": "./dist/character-count/index.d.ts",
28
+ "require": "./dist/character-count/index.d.cts"
29
+ },
30
+ "import": "./dist/character-count/index.js",
31
+ "require": "./dist/character-count/index.cjs"
32
+ },
33
+ "./drop-cursor": {
34
+ "types": {
35
+ "import": "./dist/drop-cursor/index.d.ts",
36
+ "require": "./dist/drop-cursor/index.d.cts"
37
+ },
38
+ "import": "./dist/drop-cursor/index.js",
39
+ "require": "./dist/drop-cursor/index.cjs"
40
+ },
41
+ "./focus": {
42
+ "types": {
43
+ "import": "./dist/focus/index.d.ts",
44
+ "require": "./dist/focus/index.d.cts"
45
+ },
46
+ "import": "./dist/focus/index.js",
47
+ "require": "./dist/focus/index.cjs"
48
+ },
49
+ "./gap-cursor": {
50
+ "types": {
51
+ "import": "./dist/gap-cursor/index.d.ts",
52
+ "require": "./dist/gap-cursor/index.d.cts"
53
+ },
54
+ "import": "./dist/gap-cursor/index.js",
55
+ "require": "./dist/gap-cursor/index.cjs"
56
+ },
57
+ "./undo-redo": {
58
+ "types": {
59
+ "import": "./dist/undo-redo/index.d.ts",
60
+ "require": "./dist/undo-redo/index.d.cts"
61
+ },
62
+ "import": "./dist/undo-redo/index.js",
63
+ "require": "./dist/undo-redo/index.cjs"
64
+ },
65
+ "./placeholder": {
66
+ "types": {
67
+ "import": "./dist/placeholder/index.d.ts",
68
+ "require": "./dist/placeholder/index.d.cts"
69
+ },
70
+ "import": "./dist/placeholder/index.js",
71
+ "require": "./dist/placeholder/index.cjs"
72
+ },
73
+ "./selection": {
74
+ "types": {
75
+ "import": "./dist/selection/index.d.ts",
76
+ "require": "./dist/selection/index.d.cts"
77
+ },
78
+ "import": "./dist/selection/index.js",
79
+ "require": "./dist/selection/index.cjs"
80
+ },
81
+ "./trailing-node": {
82
+ "types": {
83
+ "import": "./dist/trailing-node/index.d.ts",
84
+ "require": "./dist/trailing-node/index.d.cts"
85
+ },
86
+ "import": "./dist/trailing-node/index.js",
87
+ "require": "./dist/trailing-node/index.cjs"
88
+ }
89
+ },
90
+ "main": "dist/index.cjs",
91
+ "module": "dist/index.js",
92
+ "types": "dist/index.d.ts",
93
+ "files": [
94
+ "src",
95
+ "dist"
96
+ ],
97
+ "devDependencies": {
98
+ "@tiptap/core": "^3.0.0-beta.0",
99
+ "@tiptap/pm": "^3.0.0-beta.0"
100
+ },
101
+ "peerDependencies": {
102
+ "@tiptap/core": "^3.0.0-beta.0",
103
+ "@tiptap/pm": "^3.0.0-beta.0"
104
+ },
105
+ "repository": {
106
+ "type": "git",
107
+ "url": "https://github.com/ueberdosis/tiptap",
108
+ "directory": "packages/extension"
109
+ },
110
+ "scripts": {
111
+ "build": "tsup",
112
+ "lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
113
+ }
114
+ }
@@ -0,0 +1,195 @@
1
+ import { Extension } from '@tiptap/core'
2
+ import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
3
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
4
+
5
+ export interface CharacterCountOptions {
6
+ /**
7
+ * The maximum number of characters that should be allowed. Defaults to `0`.
8
+ * @default null
9
+ * @example 180
10
+ */
11
+ limit: number | null | undefined
12
+ /**
13
+ * The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.
14
+ * If set to `nodeSize`, the nodeSize of the document is used.
15
+ * @default 'textSize'
16
+ * @example 'textSize'
17
+ */
18
+ mode: 'textSize' | 'nodeSize'
19
+ /**
20
+ * The text counter function to use. Defaults to a simple character count.
21
+ * @default (text) => text.length
22
+ * @example (text) => [...new Intl.Segmenter().segment(text)].length
23
+ */
24
+ textCounter: (text: string) => number
25
+ /**
26
+ * The word counter function to use. Defaults to a simple word count.
27
+ * @default (text) => text.split(' ').filter(word => word !== '').length
28
+ * @example (text) => text.split(/\s+/).filter(word => word !== '').length
29
+ */
30
+ wordCounter: (text: string) => number
31
+ }
32
+
33
+ export interface CharacterCountStorage {
34
+ /**
35
+ * Get the number of characters for the current document.
36
+ * @param options The options for the character count. (optional)
37
+ * @param options.node The node to get the characters from. Defaults to the current document.
38
+ * @param options.mode The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.
39
+ */
40
+ characters: (options?: { node?: ProseMirrorNode; mode?: 'textSize' | 'nodeSize' }) => number
41
+
42
+ /**
43
+ * Get the number of words for the current document.
44
+ * @param options The options for the character count. (optional)
45
+ * @param options.node The node to get the words from. Defaults to the current document.
46
+ */
47
+ words: (options?: { node?: ProseMirrorNode }) => number
48
+ }
49
+
50
+ declare module '@tiptap/core' {
51
+ interface Storage {
52
+ characterCount: CharacterCountStorage
53
+ }
54
+ }
55
+
56
+ /**
57
+ * This extension allows you to count the characters and words of your document.
58
+ * @see https://tiptap.dev/api/extensions/character-count
59
+ */
60
+ export const CharacterCount = Extension.create<CharacterCountOptions, CharacterCountStorage>({
61
+ name: 'characterCount',
62
+
63
+ addOptions() {
64
+ return {
65
+ limit: null,
66
+ mode: 'textSize',
67
+ textCounter: text => text.length,
68
+ wordCounter: text => text.split(' ').filter(word => word !== '').length,
69
+ }
70
+ },
71
+
72
+ addStorage() {
73
+ return {
74
+ characters: () => 0,
75
+ words: () => 0,
76
+ }
77
+ },
78
+
79
+ onBeforeCreate() {
80
+ this.storage.characters = options => {
81
+ const node = options?.node || this.editor.state.doc
82
+ const mode = options?.mode || this.options.mode
83
+
84
+ if (mode === 'textSize') {
85
+ const text = node.textBetween(0, node.content.size, undefined, ' ')
86
+
87
+ return this.options.textCounter(text)
88
+ }
89
+
90
+ return node.nodeSize
91
+ }
92
+
93
+ this.storage.words = options => {
94
+ const node = options?.node || this.editor.state.doc
95
+ const text = node.textBetween(0, node.content.size, ' ', ' ')
96
+
97
+ return this.options.wordCounter(text)
98
+ }
99
+ },
100
+
101
+ addProseMirrorPlugins() {
102
+ let initialEvaluationDone = false
103
+
104
+ return [
105
+ new Plugin({
106
+ key: new PluginKey('characterCount'),
107
+ appendTransaction: (transactions, oldState, newState) => {
108
+ if (initialEvaluationDone) {
109
+ return
110
+ }
111
+
112
+ const limit = this.options.limit
113
+
114
+ if (limit === null || limit === undefined || limit === 0) {
115
+ initialEvaluationDone = true
116
+ return
117
+ }
118
+
119
+ const initialContentSize = this.storage.characters({ node: newState.doc })
120
+
121
+ if (initialContentSize > limit) {
122
+ const over = initialContentSize - limit
123
+ const from = 0
124
+ const to = over
125
+
126
+ console.warn(
127
+ `[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`,
128
+ )
129
+ const tr = newState.tr.deleteRange(from, to)
130
+
131
+ initialEvaluationDone = true
132
+ return tr
133
+ }
134
+
135
+ initialEvaluationDone = true
136
+ },
137
+ filterTransaction: (transaction, state) => {
138
+ const limit = this.options.limit
139
+
140
+ // Nothing has changed or no limit is defined. Ignore it.
141
+ if (!transaction.docChanged || limit === 0 || limit === null || limit === undefined) {
142
+ return true
143
+ }
144
+
145
+ const oldSize = this.storage.characters({ node: state.doc })
146
+ const newSize = this.storage.characters({ node: transaction.doc })
147
+
148
+ // Everything is in the limit. Good.
149
+ if (newSize <= limit) {
150
+ return true
151
+ }
152
+
153
+ // The limit has already been exceeded but will be reduced.
154
+ if (oldSize > limit && newSize > limit && newSize <= oldSize) {
155
+ return true
156
+ }
157
+
158
+ // The limit has already been exceeded and will be increased further.
159
+ if (oldSize > limit && newSize > limit && newSize > oldSize) {
160
+ return false
161
+ }
162
+
163
+ const isPaste = transaction.getMeta('paste')
164
+
165
+ // Block all exceeding transactions that were not pasted.
166
+ if (!isPaste) {
167
+ return false
168
+ }
169
+
170
+ // For pasted content, we try to remove the exceeding content.
171
+ const pos = transaction.selection.$head.pos
172
+ const over = newSize - limit
173
+ const from = pos - over
174
+ const to = pos
175
+
176
+ // It’s probably a bad idea to mutate transactions within `filterTransaction`
177
+ // but for now this is working fine.
178
+ transaction.deleteRange(from, to)
179
+
180
+ // In some situations, the limit will continue to be exceeded after trimming.
181
+ // This happens e.g. when truncating within a complex node (e.g. table)
182
+ // and ProseMirror has to close this node again.
183
+ // If this is the case, we prevent the transaction completely.
184
+ const updatedSize = this.storage.characters({ node: transaction.doc })
185
+
186
+ if (updatedSize > limit) {
187
+ return false
188
+ }
189
+
190
+ return true
191
+ },
192
+ }),
193
+ ]
194
+ },
195
+ })
@@ -0,0 +1 @@
1
+ export * from './character-count.js'
@@ -0,0 +1,47 @@
1
+ import { Extension } from '@tiptap/core'
2
+ import { dropCursor } from '@tiptap/pm/dropcursor'
3
+
4
+ export interface DropcursorOptions {
5
+ /**
6
+ * The color of the drop cursor
7
+ * @default 'currentColor'
8
+ * @example 'red'
9
+ */
10
+ color: string | undefined
11
+
12
+ /**
13
+ * The width of the drop cursor
14
+ * @default 1
15
+ * @example 2
16
+ */
17
+ width: number | undefined
18
+
19
+ /**
20
+ * The class of the drop cursor
21
+ * @default undefined
22
+ * @example 'drop-cursor'
23
+ */
24
+ class: string | undefined
25
+ }
26
+
27
+ /**
28
+ * This extension allows you to add a drop cursor to your editor.
29
+ * A drop cursor is a line that appears when you drag and drop content
30
+ * in-between nodes.
31
+ * @see https://tiptap.dev/api/extensions/dropcursor
32
+ */
33
+ export const Dropcursor = Extension.create<DropcursorOptions>({
34
+ name: 'dropCursor',
35
+
36
+ addOptions() {
37
+ return {
38
+ color: 'currentColor',
39
+ width: 1,
40
+ class: undefined,
41
+ }
42
+ },
43
+
44
+ addProseMirrorPlugins() {
45
+ return [dropCursor(this.options)]
46
+ },
47
+ })
@@ -0,0 +1 @@
1
+ export * from './drop-cursor.js'
@@ -0,0 +1,110 @@
1
+ import { Extension } from '@tiptap/core'
2
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
3
+ import { Decoration, DecorationSet } from '@tiptap/pm/view'
4
+
5
+ export interface FocusOptions {
6
+ /**
7
+ * The class name that should be added to the focused node.
8
+ * @default 'has-focus'
9
+ * @example 'is-focused'
10
+ */
11
+ className: string
12
+
13
+ /**
14
+ * The mode by which the focused node is determined.
15
+ * - All: All nodes are marked as focused.
16
+ * - Deepest: Only the deepest node is marked as focused.
17
+ * - Shallowest: Only the shallowest node is marked as focused.
18
+ *
19
+ * @default 'all'
20
+ * @example 'deepest'
21
+ * @example 'shallowest'
22
+ */
23
+ mode: 'all' | 'deepest' | 'shallowest'
24
+ }
25
+
26
+ /**
27
+ * This extension allows you to add a class to the focused node.
28
+ * @see https://www.tiptap.dev/api/extensions/focus
29
+ */
30
+ export const Focus = Extension.create<FocusOptions>({
31
+ name: 'focus',
32
+
33
+ addOptions() {
34
+ return {
35
+ className: 'has-focus',
36
+ mode: 'all',
37
+ }
38
+ },
39
+
40
+ addProseMirrorPlugins() {
41
+ return [
42
+ new Plugin({
43
+ key: new PluginKey('focus'),
44
+ props: {
45
+ decorations: ({ doc, selection }) => {
46
+ const { isEditable, isFocused } = this.editor
47
+ const { anchor } = selection
48
+ const decorations: Decoration[] = []
49
+
50
+ if (!isEditable || !isFocused) {
51
+ return DecorationSet.create(doc, [])
52
+ }
53
+
54
+ // Maximum Levels
55
+ let maxLevels = 0
56
+
57
+ if (this.options.mode === 'deepest') {
58
+ doc.descendants((node, pos) => {
59
+ if (node.isText) {
60
+ return
61
+ }
62
+
63
+ const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1
64
+
65
+ if (!isCurrent) {
66
+ return false
67
+ }
68
+
69
+ maxLevels += 1
70
+ })
71
+ }
72
+
73
+ // Loop through current
74
+ let currentLevel = 0
75
+
76
+ doc.descendants((node, pos) => {
77
+ if (node.isText) {
78
+ return false
79
+ }
80
+
81
+ const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1
82
+
83
+ if (!isCurrent) {
84
+ return false
85
+ }
86
+
87
+ currentLevel += 1
88
+
89
+ const outOfScope =
90
+ (this.options.mode === 'deepest' && maxLevels - currentLevel > 0) ||
91
+ (this.options.mode === 'shallowest' && currentLevel > 1)
92
+
93
+ if (outOfScope) {
94
+ return this.options.mode === 'deepest'
95
+ }
96
+
97
+ decorations.push(
98
+ Decoration.node(pos, pos + node.nodeSize, {
99
+ class: this.options.className,
100
+ }),
101
+ )
102
+ })
103
+
104
+ return DecorationSet.create(doc, decorations)
105
+ },
106
+ },
107
+ }),
108
+ ]
109
+ },
110
+ })
@@ -0,0 +1 @@
1
+ export * from './focus.js'
@@ -0,0 +1,47 @@
1
+ import type { ParentConfig } from '@tiptap/core'
2
+ import { callOrReturn, Extension, getExtensionField } from '@tiptap/core'
3
+ import { gapCursor } from '@tiptap/pm/gapcursor'
4
+
5
+ declare module '@tiptap/core' {
6
+ interface NodeConfig<Options, Storage> {
7
+ /**
8
+ * A function to determine whether the gap cursor is allowed at the current position. Must return `true` or `false`.
9
+ * @default null
10
+ */
11
+ allowGapCursor?:
12
+ | boolean
13
+ | null
14
+ | ((this: {
15
+ name: string
16
+ options: Options
17
+ storage: Storage
18
+ parent: ParentConfig<NodeConfig<Options>>['allowGapCursor']
19
+ }) => boolean | null)
20
+ }
21
+ }
22
+
23
+ /**
24
+ * This extension allows you to add a gap cursor to your editor.
25
+ * A gap cursor is a cursor that appears when you click on a place
26
+ * where no content is present, for example inbetween nodes.
27
+ * @see https://tiptap.dev/api/extensions/gapcursor
28
+ */
29
+ export const Gapcursor = Extension.create({
30
+ name: 'gapCursor',
31
+
32
+ addProseMirrorPlugins() {
33
+ return [gapCursor()]
34
+ },
35
+
36
+ extendNodeSchema(extension) {
37
+ const context = {
38
+ name: extension.name,
39
+ options: extension.options,
40
+ storage: extension.storage,
41
+ }
42
+
43
+ return {
44
+ allowGapCursor: callOrReturn(getExtensionField(extension, 'allowGapCursor', context)) ?? null,
45
+ }
46
+ },
47
+ })
@@ -0,0 +1 @@
1
+ export * from './gap-cursor.js'
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './character-count/index.js'
2
+ export * from './drop-cursor/index.js'
3
+ export * from './focus/index.js'
4
+ export * from './gap-cursor/index.js'
5
+ export * from './placeholder/index.js'
6
+ export * from './selection/index.js'
7
+ export * from './trailing-node/index.js'
8
+ export * from './undo-redo/index.js'
@@ -0,0 +1 @@
1
+ export * from './placeholder.js'
@@ -0,0 +1,129 @@
1
+ import type { Editor } from '@tiptap/core'
2
+ import { Extension, isNodeEmpty } from '@tiptap/core'
3
+ import type { Node as ProsemirrorNode } from '@tiptap/pm/model'
4
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
5
+ import { Decoration, DecorationSet } from '@tiptap/pm/view'
6
+
7
+ export interface PlaceholderOptions {
8
+ /**
9
+ * **The class name for the empty editor**
10
+ * @default 'is-editor-empty'
11
+ */
12
+ emptyEditorClass: string
13
+
14
+ /**
15
+ * **The class name for empty nodes**
16
+ * @default 'is-empty'
17
+ */
18
+ emptyNodeClass: string
19
+
20
+ /**
21
+ * **The placeholder content**
22
+ *
23
+ * You can use a function to return a dynamic placeholder or a string.
24
+ * @default 'Write something …'
25
+ */
26
+ placeholder:
27
+ | ((PlaceholderProps: { editor: Editor; node: ProsemirrorNode; pos: number; hasAnchor: boolean }) => string)
28
+ | string
29
+
30
+ /**
31
+ * **Checks if the placeholder should be only shown when the editor is editable.**
32
+ *
33
+ * If true, the placeholder will only be shown when the editor is editable.
34
+ * If false, the placeholder will always be shown.
35
+ * @default true
36
+ */
37
+ showOnlyWhenEditable: boolean
38
+
39
+ /**
40
+ * **Checks if the placeholder should be only shown when the current node is empty.**
41
+ *
42
+ * If true, the placeholder will only be shown when the current node is empty.
43
+ * If false, the placeholder will be shown when any node is empty.
44
+ * @default true
45
+ */
46
+ showOnlyCurrent: boolean
47
+
48
+ /**
49
+ * **Controls if the placeholder should be shown for all descendents.**
50
+ *
51
+ * If true, the placeholder will be shown for all descendents.
52
+ * If false, the placeholder will only be shown for the current node.
53
+ * @default false
54
+ */
55
+ includeChildren: boolean
56
+ }
57
+
58
+ /**
59
+ * This extension allows you to add a placeholder to your editor.
60
+ * A placeholder is a text that appears when the editor or a node is empty.
61
+ * @see https://www.tiptap.dev/api/extensions/placeholder
62
+ */
63
+ export const Placeholder = Extension.create<PlaceholderOptions>({
64
+ name: 'placeholder',
65
+
66
+ addOptions() {
67
+ return {
68
+ emptyEditorClass: 'is-editor-empty',
69
+ emptyNodeClass: 'is-empty',
70
+ placeholder: 'Write something …',
71
+ showOnlyWhenEditable: true,
72
+ showOnlyCurrent: true,
73
+ includeChildren: false,
74
+ }
75
+ },
76
+
77
+ addProseMirrorPlugins() {
78
+ return [
79
+ new Plugin({
80
+ key: new PluginKey('placeholder'),
81
+ props: {
82
+ decorations: ({ doc, selection }) => {
83
+ const active = this.editor.isEditable || !this.options.showOnlyWhenEditable
84
+ const { anchor } = selection
85
+ const decorations: Decoration[] = []
86
+
87
+ if (!active) {
88
+ return null
89
+ }
90
+
91
+ const isEmptyDoc = this.editor.isEmpty
92
+
93
+ doc.descendants((node, pos) => {
94
+ const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize
95
+ const isEmpty = !node.isLeaf && isNodeEmpty(node)
96
+
97
+ if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {
98
+ const classes = [this.options.emptyNodeClass]
99
+
100
+ if (isEmptyDoc) {
101
+ classes.push(this.options.emptyEditorClass)
102
+ }
103
+
104
+ const decoration = Decoration.node(pos, pos + node.nodeSize, {
105
+ class: classes.join(' '),
106
+ 'data-placeholder':
107
+ typeof this.options.placeholder === 'function'
108
+ ? this.options.placeholder({
109
+ editor: this.editor,
110
+ node,
111
+ pos,
112
+ hasAnchor,
113
+ })
114
+ : this.options.placeholder,
115
+ })
116
+
117
+ decorations.push(decoration)
118
+ }
119
+
120
+ return this.options.includeChildren
121
+ })
122
+
123
+ return DecorationSet.create(doc, decorations)
124
+ },
125
+ },
126
+ }),
127
+ ]
128
+ },
129
+ })
@@ -0,0 +1 @@
1
+ export * from './selection.js'