@tiptap/extension-invisible-characters 2.22.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 (56) hide show
  1. package/README.md +14 -0
  2. package/dist/index.cjs +310 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +10 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +300 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.umd.js +312 -0
  9. package/dist/index.umd.js.map +1 -0
  10. package/dist/invisible-characters.d.ts +27 -0
  11. package/dist/invisible-characters.d.ts.map +1 -0
  12. package/dist/plugin/InvisibleCharacter.d.ts +19 -0
  13. package/dist/plugin/InvisibleCharacter.d.ts.map +1 -0
  14. package/dist/plugin/InvisibleCharactersPlugin.d.ts +10 -0
  15. package/dist/plugin/InvisibleCharactersPlugin.d.ts.map +1 -0
  16. package/dist/plugin/InvisibleNode.d.ts +21 -0
  17. package/dist/plugin/InvisibleNode.d.ts.map +1 -0
  18. package/dist/plugin/index.d.ts +2 -0
  19. package/dist/plugin/index.d.ts.map +1 -0
  20. package/dist/plugin/invisible-characters/hardBreak.d.ts +6 -0
  21. package/dist/plugin/invisible-characters/hardBreak.d.ts.map +1 -0
  22. package/dist/plugin/invisible-characters/paragraph.d.ts +6 -0
  23. package/dist/plugin/invisible-characters/paragraph.d.ts.map +1 -0
  24. package/dist/plugin/invisible-characters/space.d.ts +6 -0
  25. package/dist/plugin/invisible-characters/space.d.ts.map +1 -0
  26. package/dist/plugin/reducers.d.ts +4 -0
  27. package/dist/plugin/reducers.d.ts.map +1 -0
  28. package/dist/plugin/style.d.ts +3 -0
  29. package/dist/plugin/style.d.ts.map +1 -0
  30. package/dist/plugin/utils/create-decoration-widget.d.ts +4 -0
  31. package/dist/plugin/utils/create-decoration-widget.d.ts.map +1 -0
  32. package/dist/plugin/utils/create-style-tag.d.ts +3 -0
  33. package/dist/plugin/utils/create-style-tag.d.ts.map +1 -0
  34. package/dist/plugin/utils/get-updated-ranges.d.ts +5 -0
  35. package/dist/plugin/utils/get-updated-ranges.d.ts.map +1 -0
  36. package/dist/plugin/utils/text-between.d.ts +5 -0
  37. package/dist/plugin/utils/text-between.d.ts.map +1 -0
  38. package/dist/types.d.ts +18 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/package.json +48 -0
  41. package/src/index.ts +11 -0
  42. package/src/invisible-characters.ts +99 -0
  43. package/src/plugin/InvisibleCharacter.ts +47 -0
  44. package/src/plugin/InvisibleCharactersPlugin.ts +79 -0
  45. package/src/plugin/InvisibleNode.ts +61 -0
  46. package/src/plugin/index.ts +1 -0
  47. package/src/plugin/invisible-characters/hardBreak.ts +12 -0
  48. package/src/plugin/invisible-characters/paragraph.ts +12 -0
  49. package/src/plugin/invisible-characters/space.ts +12 -0
  50. package/src/plugin/reducers.ts +14 -0
  51. package/src/plugin/style.ts +43 -0
  52. package/src/plugin/utils/create-decoration-widget.ts +28 -0
  53. package/src/plugin/utils/create-style-tag.ts +21 -0
  54. package/src/plugin/utils/get-updated-ranges.ts +20 -0
  55. package/src/plugin/utils/text-between.ts +22 -0
  56. package/src/types.ts +21 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAE/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,KAAK,CAAC,kBAAkB,GAAG,aAAa,CAAC,CAAC;IACpD,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,aAAa,CAAC;CAC5B"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@tiptap/extension-invisible-characters",
3
+ "description": "invisible characters extension for tiptap",
4
+ "version": "2.22.0",
5
+ "homepage": "https://tiptap.dev/docs/editor/extensions/functionality/invisiblecharacters",
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": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "main": "dist/index.cjs",
24
+ "module": "dist/index.js",
25
+ "umd": "dist/index.umd.js",
26
+ "types": "dist/index.d.ts",
27
+ "files": [
28
+ "src",
29
+ "dist"
30
+ ],
31
+ "devDependencies": {
32
+ "@tiptap/core": "^2.22.0",
33
+ "@tiptap/extension-text-style": "^2.22.0"
34
+ },
35
+ "peerDependencies": {
36
+ "@tiptap/core": "^2.7.0",
37
+ "@tiptap/extension-text-style": "^2.7.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/ueberdosis/tiptap",
42
+ "directory": "packages/extension-invisible-characters"
43
+ },
44
+ "scripts": {
45
+ "clean": "rm -rf dist",
46
+ "build": "npm run clean && rollup -c"
47
+ }
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { InvisibleCharacters } from './invisible-characters.js'
2
+
3
+ export * from './invisible-characters.js'
4
+ export * from './plugin/invisible-characters/hardBreak.js'
5
+ export * from './plugin/invisible-characters/paragraph.js'
6
+ export * from './plugin/invisible-characters/space.js'
7
+ export * from './plugin/InvisibleCharacter.js'
8
+ export * from './plugin/InvisibleNode.js'
9
+ export * from './types.js'
10
+
11
+ export default InvisibleCharacters
@@ -0,0 +1,99 @@
1
+ import { Extension } from '@tiptap/core'
2
+
3
+ import { InvisibleCharactersPlugin, InvisibleCharactersPluginKey } from './plugin/index.js'
4
+ import { HardBreakNode } from './plugin/invisible-characters/hardBreak.js'
5
+ import { ParagraphNode } from './plugin/invisible-characters/paragraph.js'
6
+ import { SpaceCharacter } from './plugin/invisible-characters/space.js'
7
+ import { InvisibleCharactersOptions } from './types.js'
8
+
9
+ export {
10
+ HardBreakNode,
11
+ ParagraphNode,
12
+ SpaceCharacter,
13
+ }
14
+
15
+ declare module '@tiptap/core' {
16
+ interface Commands<ReturnType> {
17
+ invisibleCharacters: {
18
+ /**
19
+ * Show invisible characters
20
+ */
21
+ showInvisibleCharacters: (show?: boolean) => ReturnType
22
+
23
+ /**
24
+ * Hide invisible characters
25
+ */
26
+ hideInvisibleCharacters: () => ReturnType
27
+
28
+ /**
29
+ * Toggle invisible characters
30
+ */
31
+ toggleInvisibleCharacters: () => ReturnType
32
+ }
33
+ }
34
+ }
35
+
36
+ export const InvisibleCharacters = Extension.create<InvisibleCharactersOptions>({
37
+ name: 'invisibleCharacters',
38
+
39
+ addOptions() {
40
+ return {
41
+ visible: true,
42
+ builders: [
43
+ new SpaceCharacter(),
44
+ new ParagraphNode(),
45
+ new HardBreakNode(),
46
+ ],
47
+ injectCSS: true,
48
+ injectNonce: undefined,
49
+ }
50
+ },
51
+
52
+ addProseMirrorPlugins() {
53
+ return [
54
+ InvisibleCharactersPlugin(this.editor.state, this.options),
55
+ ]
56
+ },
57
+
58
+ addStorage() {
59
+ return {
60
+ visibility: () => this.options.visible,
61
+ }
62
+ },
63
+
64
+ onBeforeCreate() {
65
+ this.storage.visibility = () => {
66
+ return InvisibleCharactersPluginKey.getState(this.editor.state)?.visible
67
+ }
68
+ },
69
+
70
+ addCommands() {
71
+ return {
72
+ showInvisibleCharacters: (visibility = true) => ({ dispatch, tr }) => {
73
+ if (dispatch) {
74
+ tr.setMeta('setInvisibleCharactersVisible', visibility)
75
+ }
76
+
77
+ return true
78
+ },
79
+ hideInvisibleCharacters: () => ({ dispatch, tr }) => {
80
+ if (dispatch) {
81
+ tr.setMeta('setInvisibleCharactersVisible', false)
82
+ }
83
+
84
+ return true
85
+ },
86
+ toggleInvisibleCharacters: () => ({ dispatch, tr, state }) => {
87
+ const visibility = !InvisibleCharactersPluginKey.getState(state)?.visible
88
+
89
+ if (dispatch) {
90
+ tr.setMeta('setInvisibleCharactersVisible', visibility)
91
+ }
92
+
93
+ return true
94
+ },
95
+ }
96
+ },
97
+ })
98
+
99
+ export default InvisibleCharacters
@@ -0,0 +1,47 @@
1
+ import { Node } from '@tiptap/pm/model'
2
+ import { DecorationSet } from '@tiptap/pm/view'
3
+
4
+ import { createDecorationWidget } from './utils/create-decoration-widget.js'
5
+ import { textBetween } from './utils/text-between.js'
6
+
7
+ export interface InvisibleCharacterOptions {
8
+ type: string;
9
+ predicate: (value: string) => boolean
10
+ priority?: number
11
+ content?: string
12
+ }
13
+
14
+ export class InvisibleCharacter {
15
+ predicate: (value: string) => boolean
16
+
17
+ content?: string
18
+
19
+ type: string
20
+
21
+ priority: number
22
+
23
+ constructor(options: InvisibleCharacterOptions) {
24
+ this.predicate = options.predicate
25
+ this.type = options.type
26
+ this.content = options.content
27
+ this.priority = options.priority || 100
28
+ }
29
+
30
+ createDecoration(from: number, to: number, doc: Node, decorations: DecorationSet) {
31
+ const textContent = textBetween(from, to, doc)
32
+
33
+ return textContent.reduce((oldDecorations, currentPosition) => {
34
+ return currentPosition.text.split('').reduce((innerDecorations, char, i) => {
35
+ return this.test(char)
36
+ ? innerDecorations.add(doc, [createDecorationWidget(currentPosition.pos + i, this.type, this.content)])
37
+ : innerDecorations
38
+ }, oldDecorations)
39
+ }, decorations)
40
+ }
41
+
42
+ test(value: string) {
43
+ return this.predicate(value)
44
+ }
45
+ }
46
+
47
+ export default InvisibleCharacter
@@ -0,0 +1,79 @@
1
+ import {
2
+ AllSelection, EditorState, Plugin, PluginKey,
3
+ } from '@tiptap/pm/state'
4
+ import { DecorationSet } from '@tiptap/pm/view'
5
+
6
+ import { InvisibleCharactersOptions, PluginState } from '../types.js'
7
+ import { stateReducer } from './reducers.js'
8
+ import { style } from './style.js'
9
+ import { createStyleTag } from './utils/create-style-tag.js'
10
+ import { getUpdatedRanges } from './utils/get-updated-ranges.js'
11
+
12
+ export const InvisibleCharactersPluginKey = new PluginKey<PluginState>('invisibleCharacters')
13
+
14
+ export const InvisibleCharactersPlugin = (state: EditorState, options: InvisibleCharactersOptions) => {
15
+ const key = InvisibleCharactersPluginKey
16
+ const emptyDecorationSet = DecorationSet.create(state.doc, [])
17
+
18
+ const addDecorationsBetweenPositions = (
19
+ from: number,
20
+ to: number,
21
+ editorState: EditorState,
22
+ decorations: DecorationSet,
23
+ ) => {
24
+ return options.builders.sort((builderA, builderB) => {
25
+ if (builderA.priority > builderB.priority) {
26
+ return 1
27
+ }
28
+
29
+ return -1
30
+ }).reduce((newDecos, builder) => {
31
+ return builder.createDecoration(from, to, editorState.doc, newDecos)
32
+ }, decorations)
33
+ }
34
+
35
+ return new Plugin({
36
+ key,
37
+
38
+ state: {
39
+ init: () => {
40
+ const { $from, $to } = new AllSelection(state.doc)
41
+
42
+ if (options.injectCSS && document) {
43
+ createStyleTag(style, options.injectNonce)
44
+ }
45
+
46
+ return {
47
+ visible: options.visible,
48
+ decorations: addDecorationsBetweenPositions($from.pos, $to.pos, state, DecorationSet.empty),
49
+ }
50
+ },
51
+ apply: (tr, pluginState, _, currentState) => {
52
+ const newPluginState = stateReducer(pluginState, tr.getMeta('setInvisibleCharactersVisible'))
53
+
54
+ const decorations = getUpdatedRanges(tr).reduce((nextDecorations, [from, to]) => {
55
+ return addDecorationsBetweenPositions(from, to, currentState, nextDecorations)
56
+ }, newPluginState.decorations.map(tr.mapping, tr.doc))
57
+
58
+ return {
59
+ ...newPluginState,
60
+ decorations,
61
+ }
62
+ },
63
+ },
64
+
65
+ props: {
66
+ decorations(editorState) {
67
+ const pluginState = this.getState(editorState)
68
+
69
+ const visible = pluginState?.visible
70
+ const decorations = pluginState?.decorations
71
+
72
+ return visible ? decorations : emptyDecorationSet
73
+ },
74
+ },
75
+
76
+ })
77
+ }
78
+
79
+ export default InvisibleCharactersPlugin
@@ -0,0 +1,61 @@
1
+ import { Node } from '@tiptap/pm/model'
2
+ import { DecorationSet } from '@tiptap/pm/view'
3
+
4
+ import { createDecorationWidget } from './utils/create-decoration-widget.js'
5
+
6
+ export interface InvisibleNodeOptions {
7
+ content?: string
8
+ position?: (node: Node, pos: number) => number;
9
+ predicate: (value: Node) => boolean
10
+ priority?: number
11
+ type: string;
12
+ }
13
+
14
+ /** Default position for nodes is always at the end of the node */
15
+ const defaultPosition = (node: Node, pos: number) => pos + node.nodeSize - 1
16
+
17
+ export class InvisibleNode {
18
+ predicate: (value: Node) => boolean
19
+
20
+ position: (node: Node, pos: number) => number
21
+
22
+ content ?: string
23
+
24
+ type: string
25
+
26
+ priority: number
27
+
28
+ constructor(options: InvisibleNodeOptions) {
29
+ this.predicate = options.predicate
30
+ this.type = options.type
31
+ this.position = options.position || defaultPosition
32
+ this.content = options.content
33
+ this.priority = options.priority || 100
34
+ }
35
+
36
+ createDecoration(from: number, to: number, doc: Node, decorations: DecorationSet) {
37
+ let newDecorations = decorations
38
+
39
+ doc.nodesBetween(from, to, (node, pos) => {
40
+ if (this.test(node)) {
41
+ const decorationPosition = this.position(node, pos)
42
+ const oldDecorations = newDecorations.find(
43
+ decorationPosition,
44
+ decorationPosition,
45
+ spec => spec.key === this.type,
46
+ )
47
+
48
+ newDecorations = newDecorations
49
+ .remove(oldDecorations)
50
+ .add(doc, [createDecorationWidget(decorationPosition, this.type, this.content)])
51
+ }
52
+ })
53
+ return newDecorations
54
+ }
55
+
56
+ test(value: Node) {
57
+ return this.predicate(value)
58
+ }
59
+ }
60
+
61
+ export default InvisibleNode
@@ -0,0 +1 @@
1
+ export * from './InvisibleCharactersPlugin.js'
@@ -0,0 +1,12 @@
1
+ import { InvisibleNode } from '../InvisibleNode.js'
2
+
3
+ export class HardBreakNode extends InvisibleNode {
4
+ constructor() {
5
+ super({
6
+ type: 'break',
7
+ predicate: node => node.type === node.type.schema.nodes.hardBreak,
8
+ })
9
+ }
10
+ }
11
+
12
+ export default HardBreakNode
@@ -0,0 +1,12 @@
1
+ import { InvisibleNode } from '../InvisibleNode.js'
2
+
3
+ export class ParagraphNode extends InvisibleNode {
4
+ constructor() {
5
+ super({
6
+ type: 'paragraph',
7
+ predicate: node => node.type === node.type.schema.nodes.paragraph,
8
+ })
9
+ }
10
+ }
11
+
12
+ export default ParagraphNode
@@ -0,0 +1,12 @@
1
+ import { InvisibleCharacter } from '../InvisibleCharacter.js'
2
+
3
+ export class SpaceCharacter extends InvisibleCharacter {
4
+ constructor() {
5
+ super({
6
+ type: 'space',
7
+ predicate: (char: string) => char === ' ',
8
+ })
9
+ }
10
+ }
11
+
12
+ export default SpaceCharacter
@@ -0,0 +1,14 @@
1
+ import { PluginState } from '../types.js'
2
+
3
+ export const stateReducer = (
4
+ state: PluginState,
5
+ visible: boolean,
6
+ ): PluginState => {
7
+ if (visible === undefined) {
8
+ return state
9
+ }
10
+
11
+ return { ...state, visible }
12
+ }
13
+
14
+ export default stateReducer
@@ -0,0 +1,43 @@
1
+ export const style = `.Tiptap-invisible-character {
2
+ height: 0;
3
+ padding: 0;
4
+ pointer-events: none;
5
+ user-select: none;
6
+ width: 0;
7
+ }
8
+
9
+ .Tiptap-invisible-character::before {
10
+ caret-color: inherit;
11
+ color: #aaa;
12
+ display: inline-block;
13
+ font-style: normal;
14
+ font-weight: 400;
15
+ line-height: 1em;
16
+ width: 0;
17
+ }
18
+
19
+ .Tiptap-invisible-character--space::before {
20
+ content: '·'
21
+ }
22
+
23
+ .Tiptap-invisible-character--break::before {
24
+ content: '¬'
25
+ }
26
+
27
+ .Tiptap-invisible-character--paragraph::before {
28
+ content: '¶'
29
+ }
30
+
31
+ .Tiptap-invisible-character + img.ProseMirror-separator {
32
+ height: 0 !important;
33
+ pointer-events: none;
34
+ user-select: none;
35
+ width: 0 !important;
36
+ }
37
+
38
+ .is-empty[data-placeholder].has-focus > .Tiptap-invisible-character {
39
+ display: none;
40
+ }
41
+ `
42
+
43
+ export default style
@@ -0,0 +1,28 @@
1
+ import { Decoration } from '@tiptap/pm/view'
2
+
3
+ export const createDecorationWidget = (pos: number, type: string, content?: string) => {
4
+ const createElement = () => {
5
+ const newElement = document.createElement('span')
6
+
7
+ newElement.classList.add('Tiptap-invisible-character')
8
+ newElement.classList.add(`Tiptap-invisible-character--${type}`)
9
+
10
+ if (content) {
11
+ newElement.textContent = content
12
+ }
13
+
14
+ return newElement
15
+ }
16
+
17
+ // return Decoration.inline(pos - 1, pos, {
18
+ // class: `Tiptap-invisible-character Tiptap-invisible-character--${type}`,
19
+ // })
20
+
21
+ return Decoration.widget(pos, createElement, {
22
+ key: type,
23
+ marks: [],
24
+ side: 1000,
25
+ })
26
+ }
27
+
28
+ export default createDecorationWidget
@@ -0,0 +1,21 @@
1
+ export const createStyleTag = (style: string, nonce?: string): HTMLStyleElement => {
2
+ const tiptapStyleTag = (<HTMLStyleElement>document.querySelector('style[data-tiptap-extension-invisible-characters-style]'))
3
+
4
+ if (tiptapStyleTag !== null) {
5
+ return tiptapStyleTag
6
+ }
7
+
8
+ const styleNode = document.createElement('style')
9
+
10
+ if (nonce) {
11
+ styleNode.setAttribute('nonce', nonce)
12
+ }
13
+
14
+ styleNode.setAttribute('data-tiptap-extension-invisible-characters-style', '')
15
+ styleNode.innerHTML = style
16
+ document.getElementsByTagName('head')[0].appendChild(styleNode)
17
+
18
+ return styleNode
19
+ }
20
+
21
+ export default createStyleTag
@@ -0,0 +1,20 @@
1
+ import { Transaction } from '@tiptap/pm/state'
2
+
3
+ type StepRange = [from: number, to: number];
4
+
5
+ export const getUpdatedRanges = ({ mapping }: Transaction): StepRange[] => {
6
+ const ranges: StepRange[] = []
7
+
8
+ mapping.maps.forEach((stepMap, i) => {
9
+ stepMap.forEach((_oldStart, _oldEnd, newStart, newEnd) => {
10
+ ranges.push([
11
+ mapping.slice(i + 1).map(newStart),
12
+ mapping.slice(i + 1).map(newEnd),
13
+ ])
14
+ })
15
+ })
16
+
17
+ return ranges
18
+ }
19
+
20
+ export default getUpdatedRanges
@@ -0,0 +1,22 @@
1
+ import { Node } from '@tiptap/pm/model'
2
+
3
+ import { Position } from '../../types.js'
4
+
5
+ export const textBetween = (from: number, to: number, doc: Node) => {
6
+ const positions: Position[] = []
7
+
8
+ doc.nodesBetween(from, to, (node, pos) => {
9
+ if (node.isText) {
10
+ const offset = Math.max(from, pos) - pos
11
+
12
+ positions.push({
13
+ pos: pos + offset,
14
+ text: node.text?.slice(offset, to - pos) || '',
15
+ })
16
+ }
17
+ })
18
+
19
+ return positions
20
+ }
21
+
22
+ export default textBetween
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { DecorationSet } from '@tiptap/pm/view'
2
+
3
+ import { InvisibleCharacter } from './plugin/InvisibleCharacter.js'
4
+ import { InvisibleNode } from './plugin/InvisibleNode.js'
5
+
6
+ export interface InvisibleCharactersOptions {
7
+ visible: boolean,
8
+ builders: Array<InvisibleCharacter | InvisibleNode>,
9
+ injectCSS: boolean,
10
+ injectNonce: string | undefined,
11
+ }
12
+
13
+ export interface Position {
14
+ pos: number,
15
+ text: string,
16
+ }
17
+
18
+ export interface PluginState {
19
+ visible: boolean,
20
+ decorations: DecorationSet,
21
+ }