@tiptap/extension-unique-id 2.0.0-beta.1

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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/demos/setup/helper.d.ts +2 -0
  3. package/dist/demos/setup/react.d.ts +3 -0
  4. package/dist/demos/setup/vue.d.ts +3 -0
  5. package/dist/demos/vite.config.d.ts +2 -0
  6. package/dist/packages/extension-drag-handle/src/drag-handle.d.ts +13 -0
  7. package/dist/packages/extension-drag-handle/src/helpers/cloneElement.d.ts +1 -0
  8. package/dist/packages/extension-drag-handle/src/helpers/dragHandler.d.ts +2 -0
  9. package/dist/packages/extension-drag-handle/src/helpers/getComputedStyle.d.ts +1 -0
  10. package/dist/packages/extension-drag-handle/src/helpers/getInnerCoords.d.ts +5 -0
  11. package/dist/packages/extension-drag-handle/src/helpers/minMax.d.ts +1 -0
  12. package/dist/packages/extension-drag-handle/src/helpers/removeNode.d.ts +1 -0
  13. package/dist/packages/extension-drag-handle/src/index.d.ts +3 -0
  14. package/dist/packages/extension-node-range/src/helpers/NodeRangeBookmark.d.ts +10 -0
  15. package/dist/packages/extension-node-range/src/helpers/NodeRangeSelection.d.ts +23 -0
  16. package/dist/packages/extension-node-range/src/helpers/getNodeRangeDecorations.d.ts +3 -0
  17. package/dist/packages/extension-node-range/src/helpers/getSelectionRanges.d.ts +3 -0
  18. package/dist/packages/extension-node-range/src/helpers/isNodeRangeSelection.d.ts +2 -0
  19. package/dist/packages/extension-node-range/src/index.d.ts +7 -0
  20. package/dist/packages/extension-node-range/src/node-range.d.ts +6 -0
  21. package/dist/packages/extension-unique-id/src/helpers/arrayDifference.d.ts +9 -0
  22. package/dist/packages/extension-unique-id/src/helpers/combineTransactionSteps.d.ts +7 -0
  23. package/dist/packages/extension-unique-id/src/helpers/findDuplicates.d.ts +4 -0
  24. package/dist/packages/extension-unique-id/src/helpers/getChangedRanges.d.ts +12 -0
  25. package/dist/packages/extension-unique-id/src/helpers/removeDuplicates.d.ts +8 -0
  26. package/dist/packages/extension-unique-id/src/index.d.ts +3 -0
  27. package/dist/packages/extension-unique-id/src/unique-id.d.ts +9 -0
  28. package/dist/tiptap-extension-unique-id.cjs.js +291 -0
  29. package/dist/tiptap-extension-unique-id.cjs.js.map +1 -0
  30. package/dist/tiptap-extension-unique-id.esm.js +286 -0
  31. package/dist/tiptap-extension-unique-id.esm.js.map +1 -0
  32. package/dist/tiptap-extension-unique-id.umd.js +291 -0
  33. package/dist/tiptap-extension-unique-id.umd.js.map +1 -0
  34. package/package.json +34 -0
  35. package/src/helpers/arrayDifference.ts +35 -0
  36. package/src/helpers/combineTransactionSteps.ts +18 -0
  37. package/src/helpers/findDuplicates.ts +11 -0
  38. package/src/helpers/getChangedRanges.ts +78 -0
  39. package/src/helpers/removeDuplicates.ts +15 -0
  40. package/src/index.ts +5 -0
  41. package/src/unique-id.ts +245 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tiptap-extension-unique-id.umd.js","sources":["../src/helpers/combineTransactionSteps.ts","../src/helpers/removeDuplicates.ts","../src/helpers/getChangedRanges.ts","../src/helpers/findDuplicates.ts","../src/unique-id.ts"],"sourcesContent":["import { Node as ProseMirrorNode } from 'prosemirror-model'\nimport { Transaction } from 'prosemirror-state'\nimport { Transform } from 'prosemirror-transform'\n\n/**\n * Returns a new `Transform` based on all steps of the passed transactions.\n */\nexport default function combineTransactionSteps(oldDoc: ProseMirrorNode, transactions: Transaction[]): Transform {\n const transform = new Transform(oldDoc)\n\n transactions.forEach(transaction => {\n transaction.steps.forEach(step => {\n transform.step(step)\n })\n })\n\n return transform\n}\n","/**\n * Removes duplicated values within an array.\n * Supports numbers, strings and objects.\n */\nexport default function removeDuplicates<T>(array: T[], by = JSON.stringify): T[] {\n const seen: Record<any, any> = {}\n\n return array.filter(item => {\n const key = by(item)\n\n return Object.prototype.hasOwnProperty.call(seen, key)\n ? false\n : (seen[key] = true)\n })\n}\n","import { Transform, Step } from 'prosemirror-transform'\nimport removeDuplicates from './removeDuplicates'\n\nexport type ChangedRange = {\n oldStart: number,\n oldEnd: number,\n newStart: number,\n newEnd: number,\n}\n\n/**\n * Removes duplicated ranges and ranges that are\n * fully captured by other ranges.\n */\nfunction simplifyChangedRanges(changes: ChangedRange[]): ChangedRange[] {\n const uniqueChanges = removeDuplicates(changes)\n\n return uniqueChanges.length === 1\n ? uniqueChanges\n : uniqueChanges.filter((change, index) => {\n const rest = uniqueChanges.filter((_, i) => i !== index)\n\n return !rest.some(otherChange => {\n return change.oldStart >= otherChange.oldStart\n && change.oldEnd <= otherChange.oldEnd\n && change.newStart >= otherChange.newStart\n && change.newEnd <= otherChange.newEnd\n })\n })\n}\n\n/**\n * Returns a list of changed ranges\n * based on the first and last state of all steps.\n */\nexport default function getChangedRanges(transform: Transform): ChangedRange[] {\n const { mapping, steps } = transform\n const changes: ChangedRange[] = []\n\n mapping.maps.forEach((stepMap, index) => {\n // This accounts for step changes where no range was actually altered\n // e.g. when setting a mark, node attribute, etc.\n // @ts-ignore\n if (!stepMap.ranges.length) {\n const step = steps[index] as Step & {\n from?: number,\n to?: number,\n }\n\n if (step.from === undefined || step.to === undefined) {\n return\n }\n\n changes.push({\n oldStart: step.from,\n oldEnd: step.to,\n newStart: step.from,\n newEnd: step.to,\n })\n } else {\n stepMap.forEach((from, to) => {\n const newStart = mapping.slice(index).map(from, -1)\n const newEnd = mapping.slice(index).map(to)\n const oldStart = mapping.invert().map(newStart, -1)\n const oldEnd = mapping.invert().map(newEnd)\n\n changes.push({\n oldStart,\n oldEnd,\n newStart,\n newEnd,\n })\n })\n }\n })\n\n return simplifyChangedRanges(changes)\n}\n","import removeDuplicates from './removeDuplicates'\n\n/**\n * Returns a list of duplicated items within an array.\n */\nexport default function findDuplicates(items: any[]): any[] {\n const filtered = items.filter((el, index) => items.indexOf(el) !== index)\n const duplicates = removeDuplicates(filtered)\n\n return duplicates\n}\n","import { Extension, findChildren, findChildrenInRange } from '@tiptap/core'\nimport { Plugin, PluginKey, Transaction } from 'prosemirror-state'\nimport { Slice, Fragment, Node as ProseMirrorNode } from 'prosemirror-model'\nimport { v4 as uuidv4 } from 'uuid'\nimport combineTransactionSteps from './helpers/combineTransactionSteps'\nimport getChangedRanges from './helpers/getChangedRanges'\nimport findDuplicates from './helpers/findDuplicates'\n\nexport interface UniqueIdOptions {\n attributeName: string,\n types: string[],\n generateId: () => any,\n filterTransaction: ((transaction: Transaction) => boolean) | null,\n}\n\nexport const UniqueId = Extension.create<UniqueIdOptions>({\n name: 'uniqueId',\n\n // we’ll set a very high priority to make sure this runs first\n // and is compatible with `appendTransaction` hooks of other extensions\n priority: 10000,\n\n defaultOptions: {\n attributeName: 'id',\n types: [],\n generateId: () => uuidv4(),\n filterTransaction: null,\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n [this.options.attributeName]: {\n default: null,\n parseHTML: element => element.getAttribute(`data-${this.options.attributeName}`),\n renderHTML: attributes => {\n if (!attributes[this.options.attributeName]) {\n return {}\n }\n\n return {\n [`data-${this.options.attributeName}`]: attributes[this.options.attributeName],\n }\n },\n },\n },\n },\n ]\n },\n\n // check initial content for missing ids\n onCreate() {\n const { view, state } = this.editor\n const { tr, doc } = state\n const { types, attributeName, generateId } = this.options\n const nodesWithoutId = findChildren(doc, node => {\n return types.includes(node.type.name)\n && node.attrs[attributeName] === null\n })\n\n nodesWithoutId.forEach(({ node, pos }) => {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateId(),\n })\n })\n\n view.dispatch(tr)\n },\n\n addProseMirrorPlugins() {\n let dragSourceElement: Element | null = null\n let transformPasted = false\n\n return [\n new Plugin({\n key: new PluginKey('uniqueId'),\n\n appendTransaction: (transactions, oldState, newState) => {\n const docChanges = transactions.some(transaction => transaction.docChanged)\n && !oldState.doc.eq(newState.doc)\n const filterTransactions = this.options.filterTransaction\n && transactions.some(tr => !this.options.filterTransaction?.(tr))\n\n if (!docChanges || filterTransactions) {\n return\n }\n\n const { tr } = newState\n const { types, attributeName, generateId } = this.options\n const transform = combineTransactionSteps(oldState.doc, transactions)\n const { mapping } = transform\n\n // get changed ranges based on the old state\n const changes = getChangedRanges(transform)\n\n changes.forEach(change => {\n const newRange = {\n from: change.newStart,\n to: change.newEnd,\n }\n\n const newNodes = findChildrenInRange(newState.doc, newRange, node => {\n return types.includes(node.type.name)\n })\n\n const newIds = newNodes\n .map(({ node }) => node.attrs[attributeName])\n .filter(id => id !== null)\n\n const duplicatedNewIds = findDuplicates(newIds)\n\n newNodes.forEach(({ node, pos }) => {\n // instead of checking `node.attrs[attributeName]` directly\n // we look at the current state of the node within `tr.doc`.\n // this helps to prevent adding new ids to the same node\n // if the node changed multiple times within one transaction\n const id = tr.doc.nodeAt(pos)?.attrs[attributeName]\n\n if (id === null) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateId(),\n })\n\n return\n }\n\n // check if the node doesn’t exist in the old state\n const { deleted } = mapping.invert().mapResult(pos)\n const newNode = deleted && duplicatedNewIds.includes(id)\n\n if (newNode) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateId(),\n })\n }\n })\n\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n\n // we register a global drag handler to track the current drag source element\n view(view) {\n const handleDragstart = (event: DragEvent) => {\n dragSourceElement = view.dom.parentElement?.contains(event.target as Element)\n ? view.dom.parentElement\n : null\n }\n\n window.addEventListener('dragstart', handleDragstart)\n\n return {\n destroy() {\n window.removeEventListener('dragstart', handleDragstart)\n },\n }\n },\n\n props: {\n // `handleDOMEvents` is called before `transformPasted`\n // so we can do some checks before\n handleDOMEvents: {\n // only create new ids for dropped content while holding `alt`\n // or content is dragged from another editor\n drop: (view, event) => {\n if (\n dragSourceElement !== view.dom.parentElement\n || event.dataTransfer?.effectAllowed === 'copy'\n ) {\n dragSourceElement = null\n transformPasted = true\n }\n\n return false\n },\n // always create new ids on pasted content\n paste: () => {\n transformPasted = true\n\n return false\n },\n },\n\n // we’ll remove ids for every pasted node\n // so we can create a new one within `appendTransaction`\n transformPasted: slice => {\n if (!transformPasted) {\n return slice\n }\n\n const { types, attributeName } = this.options\n const removeId = (fragment: Fragment): Fragment => {\n const list: ProseMirrorNode[] = []\n\n fragment.forEach(node => {\n // don’t touch text nodes\n if (node.isText) {\n list.push(node)\n\n return\n }\n\n // check for any other child nodes\n if (!types.includes(node.type.name)) {\n list.push(node.copy(removeId(node.content)))\n\n return\n }\n\n // remove id\n const nodeWithoutId = node.type.create(\n {\n ...node.attrs,\n [attributeName]: null,\n },\n removeId(node.content),\n node.marks,\n )\n list.push(nodeWithoutId)\n })\n\n return Fragment.from(list)\n }\n\n // reset check\n transformPasted = false\n\n return new Slice(removeId(slice.content), slice.openStart, slice.openEnd)\n },\n },\n }),\n ]\n },\n\n})\n"],"names":["Transform","Extension","uuidv4","findChildren","Plugin","PluginKey","findChildrenInRange","Fragment","Slice"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@tiptap/extension-unique-id",
3
+ "description": "unique id extension for tiptap",
4
+ "version": "2.0.0-beta.1",
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
+ "main": "dist/tiptap-extension-unique-id.cjs.js",
16
+ "umd": "dist/tiptap-extension-unique-id.umd.js",
17
+ "module": "dist/tiptap-extension-unique-id.esm.js",
18
+ "types": "dist/packages/extension-unique-id/src/index.d.ts",
19
+ "files": [
20
+ "src",
21
+ "dist"
22
+ ],
23
+ "peerDependencies": {
24
+ "@tiptap/core": "^2.0.0-beta.1"
25
+ },
26
+ "dependencies": {
27
+ "@types/uuid": "^8.3.1",
28
+ "prosemirror-model": "^1.14.3",
29
+ "prosemirror-state": "^1.3.4",
30
+ "prosemirror-transform": "^1.3.2",
31
+ "uuid": "^8.3.2"
32
+ },
33
+ "gitHead": "23420cec8b845a4f13d75da37c451c43d0c4b66c"
34
+ }
@@ -0,0 +1,35 @@
1
+ import removeDuplicates from './removeDuplicates'
2
+
3
+ export interface ArrayDifference {
4
+ added: any[],
5
+ removed: any[],
6
+ common: any[],
7
+ }
8
+
9
+ /**
10
+ * Checks for added, removed and common items between two arrays.
11
+ */
12
+ export default function arrayDifference(array1: any[], array2: any[]): ArrayDifference {
13
+ const uniqueCombinedArray = removeDuplicates([...array1, ...array2])
14
+ const data: ArrayDifference = {
15
+ added: [],
16
+ removed: [],
17
+ common: [],
18
+ }
19
+
20
+ uniqueCombinedArray.forEach(item => {
21
+ if (!array1.includes(item) && array2.includes(item)) {
22
+ data.added.push(item)
23
+ }
24
+
25
+ if (array1.includes(item) && !array2.includes(item)) {
26
+ data.removed.push(item)
27
+ }
28
+
29
+ if (array1.includes(item) && array2.includes(item)) {
30
+ data.common.push(item)
31
+ }
32
+ })
33
+
34
+ return data
35
+ }
@@ -0,0 +1,18 @@
1
+ import { Node as ProseMirrorNode } from 'prosemirror-model'
2
+ import { Transaction } from 'prosemirror-state'
3
+ import { Transform } from 'prosemirror-transform'
4
+
5
+ /**
6
+ * Returns a new `Transform` based on all steps of the passed transactions.
7
+ */
8
+ export default function combineTransactionSteps(oldDoc: ProseMirrorNode, transactions: Transaction[]): Transform {
9
+ const transform = new Transform(oldDoc)
10
+
11
+ transactions.forEach(transaction => {
12
+ transaction.steps.forEach(step => {
13
+ transform.step(step)
14
+ })
15
+ })
16
+
17
+ return transform
18
+ }
@@ -0,0 +1,11 @@
1
+ import removeDuplicates from './removeDuplicates'
2
+
3
+ /**
4
+ * Returns a list of duplicated items within an array.
5
+ */
6
+ export default function findDuplicates(items: any[]): any[] {
7
+ const filtered = items.filter((el, index) => items.indexOf(el) !== index)
8
+ const duplicates = removeDuplicates(filtered)
9
+
10
+ return duplicates
11
+ }
@@ -0,0 +1,78 @@
1
+ import { Transform, Step } from 'prosemirror-transform'
2
+ import removeDuplicates from './removeDuplicates'
3
+
4
+ export type ChangedRange = {
5
+ oldStart: number,
6
+ oldEnd: number,
7
+ newStart: number,
8
+ newEnd: number,
9
+ }
10
+
11
+ /**
12
+ * Removes duplicated ranges and ranges that are
13
+ * fully captured by other ranges.
14
+ */
15
+ function simplifyChangedRanges(changes: ChangedRange[]): ChangedRange[] {
16
+ const uniqueChanges = removeDuplicates(changes)
17
+
18
+ return uniqueChanges.length === 1
19
+ ? uniqueChanges
20
+ : uniqueChanges.filter((change, index) => {
21
+ const rest = uniqueChanges.filter((_, i) => i !== index)
22
+
23
+ return !rest.some(otherChange => {
24
+ return change.oldStart >= otherChange.oldStart
25
+ && change.oldEnd <= otherChange.oldEnd
26
+ && change.newStart >= otherChange.newStart
27
+ && change.newEnd <= otherChange.newEnd
28
+ })
29
+ })
30
+ }
31
+
32
+ /**
33
+ * Returns a list of changed ranges
34
+ * based on the first and last state of all steps.
35
+ */
36
+ export default function getChangedRanges(transform: Transform): ChangedRange[] {
37
+ const { mapping, steps } = transform
38
+ const changes: ChangedRange[] = []
39
+
40
+ mapping.maps.forEach((stepMap, index) => {
41
+ // This accounts for step changes where no range was actually altered
42
+ // e.g. when setting a mark, node attribute, etc.
43
+ // @ts-ignore
44
+ if (!stepMap.ranges.length) {
45
+ const step = steps[index] as Step & {
46
+ from?: number,
47
+ to?: number,
48
+ }
49
+
50
+ if (step.from === undefined || step.to === undefined) {
51
+ return
52
+ }
53
+
54
+ changes.push({
55
+ oldStart: step.from,
56
+ oldEnd: step.to,
57
+ newStart: step.from,
58
+ newEnd: step.to,
59
+ })
60
+ } else {
61
+ stepMap.forEach((from, to) => {
62
+ const newStart = mapping.slice(index).map(from, -1)
63
+ const newEnd = mapping.slice(index).map(to)
64
+ const oldStart = mapping.invert().map(newStart, -1)
65
+ const oldEnd = mapping.invert().map(newEnd)
66
+
67
+ changes.push({
68
+ oldStart,
69
+ oldEnd,
70
+ newStart,
71
+ newEnd,
72
+ })
73
+ })
74
+ }
75
+ })
76
+
77
+ return simplifyChangedRanges(changes)
78
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Removes duplicated values within an array.
3
+ * Supports numbers, strings and objects.
4
+ */
5
+ export default function removeDuplicates<T>(array: T[], by = JSON.stringify): T[] {
6
+ const seen: Record<any, any> = {}
7
+
8
+ return array.filter(item => {
9
+ const key = by(item)
10
+
11
+ return Object.prototype.hasOwnProperty.call(seen, key)
12
+ ? false
13
+ : (seen[key] = true)
14
+ })
15
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { UniqueId } from './unique-id'
2
+
3
+ export * from './unique-id'
4
+
5
+ export default UniqueId
@@ -0,0 +1,245 @@
1
+ import { Extension, findChildren, findChildrenInRange } from '@tiptap/core'
2
+ import { Plugin, PluginKey, Transaction } from 'prosemirror-state'
3
+ import { Slice, Fragment, Node as ProseMirrorNode } from 'prosemirror-model'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+ import combineTransactionSteps from './helpers/combineTransactionSteps'
6
+ import getChangedRanges from './helpers/getChangedRanges'
7
+ import findDuplicates from './helpers/findDuplicates'
8
+
9
+ export interface UniqueIdOptions {
10
+ attributeName: string,
11
+ types: string[],
12
+ generateId: () => any,
13
+ filterTransaction: ((transaction: Transaction) => boolean) | null,
14
+ }
15
+
16
+ export const UniqueId = Extension.create<UniqueIdOptions>({
17
+ name: 'uniqueId',
18
+
19
+ // we’ll set a very high priority to make sure this runs first
20
+ // and is compatible with `appendTransaction` hooks of other extensions
21
+ priority: 10000,
22
+
23
+ defaultOptions: {
24
+ attributeName: 'id',
25
+ types: [],
26
+ generateId: () => uuidv4(),
27
+ filterTransaction: null,
28
+ },
29
+
30
+ addGlobalAttributes() {
31
+ return [
32
+ {
33
+ types: this.options.types,
34
+ attributes: {
35
+ [this.options.attributeName]: {
36
+ default: null,
37
+ parseHTML: element => element.getAttribute(`data-${this.options.attributeName}`),
38
+ renderHTML: attributes => {
39
+ if (!attributes[this.options.attributeName]) {
40
+ return {}
41
+ }
42
+
43
+ return {
44
+ [`data-${this.options.attributeName}`]: attributes[this.options.attributeName],
45
+ }
46
+ },
47
+ },
48
+ },
49
+ },
50
+ ]
51
+ },
52
+
53
+ // check initial content for missing ids
54
+ onCreate() {
55
+ const { view, state } = this.editor
56
+ const { tr, doc } = state
57
+ const { types, attributeName, generateId } = this.options
58
+ const nodesWithoutId = findChildren(doc, node => {
59
+ return types.includes(node.type.name)
60
+ && node.attrs[attributeName] === null
61
+ })
62
+
63
+ nodesWithoutId.forEach(({ node, pos }) => {
64
+ tr.setNodeMarkup(pos, undefined, {
65
+ ...node.attrs,
66
+ [attributeName]: generateId(),
67
+ })
68
+ })
69
+
70
+ view.dispatch(tr)
71
+ },
72
+
73
+ addProseMirrorPlugins() {
74
+ let dragSourceElement: Element | null = null
75
+ let transformPasted = false
76
+
77
+ return [
78
+ new Plugin({
79
+ key: new PluginKey('uniqueId'),
80
+
81
+ appendTransaction: (transactions, oldState, newState) => {
82
+ const docChanges = transactions.some(transaction => transaction.docChanged)
83
+ && !oldState.doc.eq(newState.doc)
84
+ const filterTransactions = this.options.filterTransaction
85
+ && transactions.some(tr => !this.options.filterTransaction?.(tr))
86
+
87
+ if (!docChanges || filterTransactions) {
88
+ return
89
+ }
90
+
91
+ const { tr } = newState
92
+ const { types, attributeName, generateId } = this.options
93
+ const transform = combineTransactionSteps(oldState.doc, transactions)
94
+ const { mapping } = transform
95
+
96
+ // get changed ranges based on the old state
97
+ const changes = getChangedRanges(transform)
98
+
99
+ changes.forEach(change => {
100
+ const newRange = {
101
+ from: change.newStart,
102
+ to: change.newEnd,
103
+ }
104
+
105
+ const newNodes = findChildrenInRange(newState.doc, newRange, node => {
106
+ return types.includes(node.type.name)
107
+ })
108
+
109
+ const newIds = newNodes
110
+ .map(({ node }) => node.attrs[attributeName])
111
+ .filter(id => id !== null)
112
+
113
+ const duplicatedNewIds = findDuplicates(newIds)
114
+
115
+ newNodes.forEach(({ node, pos }) => {
116
+ // instead of checking `node.attrs[attributeName]` directly
117
+ // we look at the current state of the node within `tr.doc`.
118
+ // this helps to prevent adding new ids to the same node
119
+ // if the node changed multiple times within one transaction
120
+ const id = tr.doc.nodeAt(pos)?.attrs[attributeName]
121
+
122
+ if (id === null) {
123
+ tr.setNodeMarkup(pos, undefined, {
124
+ ...node.attrs,
125
+ [attributeName]: generateId(),
126
+ })
127
+
128
+ return
129
+ }
130
+
131
+ // check if the node doesn’t exist in the old state
132
+ const { deleted } = mapping.invert().mapResult(pos)
133
+ const newNode = deleted && duplicatedNewIds.includes(id)
134
+
135
+ if (newNode) {
136
+ tr.setNodeMarkup(pos, undefined, {
137
+ ...node.attrs,
138
+ [attributeName]: generateId(),
139
+ })
140
+ }
141
+ })
142
+
143
+ })
144
+
145
+ if (!tr.steps.length) {
146
+ return
147
+ }
148
+
149
+ return tr
150
+ },
151
+
152
+ // we register a global drag handler to track the current drag source element
153
+ view(view) {
154
+ const handleDragstart = (event: DragEvent) => {
155
+ dragSourceElement = view.dom.parentElement?.contains(event.target as Element)
156
+ ? view.dom.parentElement
157
+ : null
158
+ }
159
+
160
+ window.addEventListener('dragstart', handleDragstart)
161
+
162
+ return {
163
+ destroy() {
164
+ window.removeEventListener('dragstart', handleDragstart)
165
+ },
166
+ }
167
+ },
168
+
169
+ props: {
170
+ // `handleDOMEvents` is called before `transformPasted`
171
+ // so we can do some checks before
172
+ handleDOMEvents: {
173
+ // only create new ids for dropped content while holding `alt`
174
+ // or content is dragged from another editor
175
+ drop: (view, event) => {
176
+ if (
177
+ dragSourceElement !== view.dom.parentElement
178
+ || event.dataTransfer?.effectAllowed === 'copy'
179
+ ) {
180
+ dragSourceElement = null
181
+ transformPasted = true
182
+ }
183
+
184
+ return false
185
+ },
186
+ // always create new ids on pasted content
187
+ paste: () => {
188
+ transformPasted = true
189
+
190
+ return false
191
+ },
192
+ },
193
+
194
+ // we’ll remove ids for every pasted node
195
+ // so we can create a new one within `appendTransaction`
196
+ transformPasted: slice => {
197
+ if (!transformPasted) {
198
+ return slice
199
+ }
200
+
201
+ const { types, attributeName } = this.options
202
+ const removeId = (fragment: Fragment): Fragment => {
203
+ const list: ProseMirrorNode[] = []
204
+
205
+ fragment.forEach(node => {
206
+ // don’t touch text nodes
207
+ if (node.isText) {
208
+ list.push(node)
209
+
210
+ return
211
+ }
212
+
213
+ // check for any other child nodes
214
+ if (!types.includes(node.type.name)) {
215
+ list.push(node.copy(removeId(node.content)))
216
+
217
+ return
218
+ }
219
+
220
+ // remove id
221
+ const nodeWithoutId = node.type.create(
222
+ {
223
+ ...node.attrs,
224
+ [attributeName]: null,
225
+ },
226
+ removeId(node.content),
227
+ node.marks,
228
+ )
229
+ list.push(nodeWithoutId)
230
+ })
231
+
232
+ return Fragment.from(list)
233
+ }
234
+
235
+ // reset check
236
+ transformPasted = false
237
+
238
+ return new Slice(removeId(slice.content), slice.openStart, slice.openEnd)
239
+ },
240
+ },
241
+ }),
242
+ ]
243
+ },
244
+
245
+ })