@tiptap/extension-unique-id 3.20.2 → 3.20.4
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.
- package/dist/index.cjs +50 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +50 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/unique-id.ts +74 -5
package/dist/index.cjs
CHANGED
|
@@ -68,6 +68,17 @@ var UniqueID = import_core.Extension.create({
|
|
|
68
68
|
updateDocument: true
|
|
69
69
|
};
|
|
70
70
|
},
|
|
71
|
+
/**
|
|
72
|
+
* Extension storage for coordination between `addProseMirrorPlugins` and `appendTransaction`.
|
|
73
|
+
* `needsInitialIdGeneration` is set to `true` when the Collaboration extension is
|
|
74
|
+
* detected but no provider is available in its options, deferring ID creation
|
|
75
|
+
* to the first `y-sync$` transaction.
|
|
76
|
+
*/
|
|
77
|
+
addStorage() {
|
|
78
|
+
return {
|
|
79
|
+
needsInitialIdGeneration: false
|
|
80
|
+
};
|
|
81
|
+
},
|
|
71
82
|
addGlobalAttributes() {
|
|
72
83
|
const types = resolveTypes(this.options.types, this.extensions);
|
|
73
84
|
return [
|
|
@@ -124,11 +135,10 @@ var UniqueID = import_core.Extension.create({
|
|
|
124
135
|
provider.off("synced", createIds);
|
|
125
136
|
}
|
|
126
137
|
};
|
|
127
|
-
if (
|
|
128
|
-
if (
|
|
129
|
-
|
|
138
|
+
if (collaboration) {
|
|
139
|
+
if (provider) {
|
|
140
|
+
provider.on("synced", createIds);
|
|
130
141
|
}
|
|
131
|
-
provider.on("synced", createIds);
|
|
132
142
|
} else {
|
|
133
143
|
return createIds();
|
|
134
144
|
}
|
|
@@ -137,6 +147,17 @@ var UniqueID = import_core.Extension.create({
|
|
|
137
147
|
if (!this.options.updateDocument) {
|
|
138
148
|
return [];
|
|
139
149
|
}
|
|
150
|
+
const extensionStorage = this.storage;
|
|
151
|
+
const collaboration = this.editor.extensionManager.extensions.find((ext) => ext.name === "collaboration");
|
|
152
|
+
const collaborationCaret = this.editor.extensionManager.extensions.find((ext) => ext.name === "collaborationCaret");
|
|
153
|
+
const collabExtensions = [collaboration, collaborationCaret].filter(Boolean);
|
|
154
|
+
const collabWithProvider = collabExtensions.find((ext) => {
|
|
155
|
+
var _a;
|
|
156
|
+
return (_a = ext == null ? void 0 : ext.options) == null ? void 0 : _a.provider;
|
|
157
|
+
});
|
|
158
|
+
if (collaboration && !collabWithProvider) {
|
|
159
|
+
extensionStorage.needsInitialIdGeneration = true;
|
|
160
|
+
}
|
|
140
161
|
let dragSourceElement = null;
|
|
141
162
|
let transformPasted = false;
|
|
142
163
|
const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions);
|
|
@@ -151,6 +172,31 @@ var UniqueID = import_core.Extension.create({
|
|
|
151
172
|
});
|
|
152
173
|
const isCollabTransaction = transactions.find((tr2) => tr2.getMeta("y-sync$"));
|
|
153
174
|
if (isCollabTransaction) {
|
|
175
|
+
if (extensionStorage.needsInitialIdGeneration) {
|
|
176
|
+
extensionStorage.needsInitialIdGeneration = false;
|
|
177
|
+
const { tr: tr2 } = newState;
|
|
178
|
+
const { attributeName: attributeName2, generateID: generateID2 } = this.options;
|
|
179
|
+
const allNodes = (0, import_core.findChildren)(newState.doc, (node) => types.includes(node.type.name));
|
|
180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
181
|
+
allNodes.forEach(({ node, pos }) => {
|
|
182
|
+
const currentId = node.attrs[attributeName2];
|
|
183
|
+
if (currentId === null || seen.has(currentId)) {
|
|
184
|
+
tr2.setNodeMarkup(pos, void 0, {
|
|
185
|
+
...node.attrs,
|
|
186
|
+
[attributeName2]: generateID2({ node, pos })
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
seen.add(currentId);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if (!tr2.steps.length) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
tr2.setStoredMarks(newState.tr.storedMarks);
|
|
196
|
+
tr2.setMeta("__uniqueIDTransaction", true);
|
|
197
|
+
tr2.setMeta("addToHistory", false);
|
|
198
|
+
return tr2;
|
|
199
|
+
}
|
|
154
200
|
return;
|
|
155
201
|
}
|
|
156
202
|
if (!hasDocChanges || filterTransactions) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/unique-id.ts","../src/helpers/findDuplicates.ts","../src/generate-unique-ids.ts"],"sourcesContent":["import { UniqueID } from './unique-id.js'\n\nexport * from './generate-unique-ids.js'\nexport * from './unique-id.js'\n\nexport default UniqueID\n","import {\n type Extensions,\n combineTransactionSteps,\n Extension,\n findChildren,\n findChildrenInRange,\n getChangedRanges,\n splitExtensions,\n} from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Fragment, Slice } from '@tiptap/pm/model'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { v4 as uuidv4 } from 'uuid'\n\nimport { findDuplicates } from './helpers/findDuplicates.js'\n\nexport type UniqueIDGenerationContext = {\n node: ProseMirrorNode\n pos: number\n}\n\nexport interface UniqueIDOptions {\n /**\n * The name of the attribute to add the unique ID to.\n * @default \"id\"\n */\n attributeName: string\n /**\n * The types of nodes to add unique IDs to.\n * Use `\"all\"` to add IDs to every node type except `doc` and `text`.\n * @default []\n */\n types: string[] | 'all'\n /**\n * The function that generates the unique ID. By default, a UUID v4 is\n * generated. However, you can provide your own function to generate the\n * unique ID based on the node type and the position.\n */\n generateID: (ctx: UniqueIDGenerationContext) => any\n /**\n * Ignore some mutations, for example applied from other users through the collaboration plugin.\n *\n * @default null\n */\n filterTransaction: ((transaction: Transaction) => boolean) | null\n /**\n * Whether to update the document by adding unique IDs to the nodes. Set this\n * property to `false` if the document is in `readonly` mode, is immutable, or\n * you don't want it to be modified.\n *\n * @default true\n */\n updateDocument: boolean\n}\n\nconst resolveTypes = (types: UniqueIDOptions['types'], extensions: Extensions): string[] => {\n if (types !== 'all') {\n return types\n }\n\n const { nodeExtensions } = splitExtensions(extensions)\n\n return nodeExtensions.map(extension => extension.name).filter(type => type !== 'doc' && type !== 'text')\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 addOptions() {\n return {\n attributeName: 'id',\n types: [],\n generateID: () => uuidv4(),\n filterTransaction: null,\n updateDocument: true,\n }\n },\n\n addGlobalAttributes() {\n const types = resolveTypes(this.options.types, this.extensions)\n\n return [\n {\n 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 if (!this.options.updateDocument) {\n return\n }\n\n const collaboration = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaboration')\n const collaborationCaret = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaborationCaret')\n\n const collabExtensions = [collaboration, collaborationCaret].filter(Boolean)\n const collab = collabExtensions.find(ext => ext?.options?.provider)\n const provider = collab?.options?.provider\n\n const createIds = () => {\n const { view, state } = this.editor\n const { tr, doc } = state\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n const { attributeName, generateID } = this.options\n const nodesWithoutId = findChildren(doc, node => {\n return types.includes(node.type.name) && node.attrs[attributeName] === null\n })\n\n nodesWithoutId.forEach(({ node, pos }) => {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n })\n\n tr.setMeta('addToHistory', false)\n\n view.dispatch(tr)\n\n if (provider) {\n provider.off('synced', createIds)\n }\n }\n\n /**\n * We need to handle collaboration a bit different here\n * because we can't automatically add IDs when the provider is not yet synced\n * otherwise we end up with empty paragraphs\n */\n if (collab) {\n if (!provider) {\n return createIds()\n }\n\n provider.on('synced', createIds)\n } else {\n return createIds()\n }\n },\n\n addProseMirrorPlugins() {\n if (!this.options.updateDocument) {\n return []\n }\n\n let dragSourceElement: Element | null = null\n let transformPasted = false\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n\n return [\n new Plugin({\n key: new PluginKey('uniqueID'),\n\n appendTransaction: (transactions, oldState, newState) => {\n const hasDocChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n const filterTransactions =\n this.options.filterTransaction && transactions.some(tr => !this.options.filterTransaction?.(tr))\n\n const isCollabTransaction = transactions.find(tr => tr.getMeta('y-sync$'))\n\n if (isCollabTransaction) {\n return\n }\n\n if (!hasDocChanges || filterTransactions) {\n return\n }\n\n const { tr } = newState\n\n const { attributeName, generateID } = this.options\n const transform = combineTransactionSteps(oldState.doc, transactions as Transaction[])\n const { mapping } = transform\n\n // get changed ranges based on the old state\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n const newNodes = findChildrenInRange(newState.doc, newRange, node => {\n return types.includes(node.type.name)\n })\n\n const newIds = newNodes.map(({ node }) => node.attrs[attributeName]).filter(id => id !== null)\n\n newNodes.forEach(({ node, pos }, i) => {\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({ node, pos }),\n })\n\n return\n }\n\n const nextNode = newNodes[i + 1]\n\n if (nextNode && node.content.size === 0) {\n const nextNodeInTr = tr.doc.nodeAt(nextNode.pos)\n if (nextNodeInTr?.attrs[attributeName] && nextNodeInTr.attrs[attributeName] !== id) {\n return\n }\n\n tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n const generatedId = generateID({ node, pos })\n\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generatedId,\n })\n newIds[i] = generatedId\n\n return tr\n }\n\n const duplicatedNewIds = findDuplicates(newIds)\n\n // check if the node doesn’t exist in the old state\n const { deleted } = mapping.invert().mapResult(pos)\n\n const newNode = deleted && duplicatedNewIds.includes(id)\n\n if (newNode) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n }\n })\n })\n\n if (!tr.steps.length) {\n return\n }\n\n // `tr.setNodeMarkup` resets the stored marks\n // so we'll restore them if they exist\n tr.setStoredMarks(newState.tr.storedMarks)\n\n // Mark this transaction as coming from UniqueID\n // to prevent infinite loops with other extensions (e.g., TrailingNode)\n tr.setMeta('__uniqueIDTransaction', true)\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\n // or 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 === 'copyMove' ||\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 { 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\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 * Returns a list of duplicated items within an array.\n */\nexport function findDuplicates(items: any[]): any[] {\n const seen = new Set()\n const duplicates = new Set<any>()\n\n items.forEach(item => {\n if (seen.has(item)) {\n duplicates.add(item)\n } else {\n seen.add(item)\n }\n })\n\n return Array.from(duplicates)\n}\n","import type { Extensions, JSONContent } from '@tiptap/core'\nimport { findChildren, getSchema } from '@tiptap/core'\nimport { Node } from '@tiptap/pm/model'\nimport { EditorState } from '@tiptap/pm/state'\n\nimport type { UniqueID } from './unique-id.js'\n\n/**\n * Creates a new document with unique IDs added to the nodes. Does the same\n * thing as the UniqueID extension, but without the need to create an `Editor`\n * instance. This lets you add unique IDs to the document in the server.\n *\n * When you call it, include the `UniqueID` extension in the `extensions` array.\n * The configuration from the `UniqueID` extension will be picked up\n * automatically, including its configuration options like `types` and\n * `attributeName`.\n *\n * @see `UniqueID` extension for more information.\n *\n * @throws {Error} If the `UniqueID` extension is not found in the extensions array.\n *\n * @example\n * const doc = {\n * type: 'doc',\n * content: [\n * { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }\n * ]\n * }\n * const newDoc = addUniqueIds(doc, [StarterKit, UniqueID.configure({ types: ['paragraph', 'heading'] })])\n * console.log(newDoc)\n * // Result:\n * // {\n * // type: 'doc',\n * // content: [\n * // { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }], id: '123' }\n * // ]\n * // }\n *\n * @param doc - A Tiptap JSON document to add unique IDs to.\n * @param extensions - The extensions to use. Must include the `UniqueID` extension.\n * @returns The updated Tiptap JSON document, with the unique IDs added to the nodes.\n */\nexport function generateUniqueIds(doc: JSONContent, extensions: Extensions): JSONContent {\n // Find the UniqueID extension in the extensions array. If it's not found, throw an error.\n const uniqueIDExtension = extensions.find(ext => ext.name === 'uniqueID') as typeof UniqueID | undefined\n if (!uniqueIDExtension) {\n throw new Error('UniqueID extension not found in the extensions array')\n }\n // Convert the JSON content to a ProseMirror node\n const schema = getSchema([...extensions.filter(ext => ext.name !== 'uniqueID'), uniqueIDExtension])\n const { types: configuredTypes, attributeName, generateID } = uniqueIDExtension.options\n const types =\n configuredTypes === 'all'\n ? Object.keys(schema.nodes).filter(type => type !== 'doc' && type !== 'text')\n : configuredTypes\n const contentNode = Node.fromJSON(schema, doc)\n\n // Find nodes that don't have a unique ID\n const nodesWithoutId = findChildren(contentNode, node => {\n return !node.attrs[attributeName] && types.includes(node.type.name)\n })\n\n // Edit the document to add unique IDs to the nodes that don't have a unique ID\n let tr = EditorState.create({\n doc: contentNode,\n }).tr\n // eslint-disable-next-line no-restricted-syntax\n for (const { node, pos } of nodesWithoutId) {\n tr = tr.setNodeAttribute(pos, attributeName, generateID({ node, pos }))\n }\n\n // Return the updated document\n return tr.doc.toJSON()\n}\n"],"mappings":"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","names":["uuidv4","_a","tr","import_core","import_model","import_state"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/unique-id.ts","../src/helpers/findDuplicates.ts","../src/generate-unique-ids.ts"],"sourcesContent":["import { UniqueID } from './unique-id.js'\n\nexport * from './generate-unique-ids.js'\nexport * from './unique-id.js'\n\nexport default UniqueID\n","import {\n type Extensions,\n combineTransactionSteps,\n Extension,\n findChildren,\n findChildrenInRange,\n getChangedRanges,\n splitExtensions,\n} from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Fragment, Slice } from '@tiptap/pm/model'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { v4 as uuidv4 } from 'uuid'\n\nimport { findDuplicates } from './helpers/findDuplicates.js'\n\nexport type UniqueIDGenerationContext = {\n node: ProseMirrorNode\n pos: number\n}\n\nexport interface UniqueIDOptions {\n /**\n * The name of the attribute to add the unique ID to.\n * @default \"id\"\n */\n attributeName: string\n /**\n * The types of nodes to add unique IDs to.\n * Use `\"all\"` to add IDs to every node type except `doc` and `text`.\n * @default []\n */\n types: string[] | 'all'\n /**\n * The function that generates the unique ID. By default, a UUID v4 is\n * generated. However, you can provide your own function to generate the\n * unique ID based on the node type and the position.\n */\n generateID: (ctx: UniqueIDGenerationContext) => any\n /**\n * Ignore some mutations, for example applied from other users through the collaboration plugin.\n *\n * @default null\n */\n filterTransaction: ((transaction: Transaction) => boolean) | null\n /**\n * Whether to update the document by adding unique IDs to the nodes. Set this\n * property to `false` if the document is in `readonly` mode, is immutable, or\n * you don't want it to be modified.\n *\n * @default true\n */\n updateDocument: boolean\n}\n\nconst resolveTypes = (types: UniqueIDOptions['types'], extensions: Extensions): string[] => {\n if (types !== 'all') {\n return types\n }\n\n const { nodeExtensions } = splitExtensions(extensions)\n\n return nodeExtensions.map(extension => extension.name).filter(type => type !== 'doc' && type !== 'text')\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 addOptions() {\n return {\n attributeName: 'id',\n types: [],\n generateID: () => uuidv4(),\n filterTransaction: null,\n updateDocument: true,\n }\n },\n\n /**\n * Extension storage for coordination between `addProseMirrorPlugins` and `appendTransaction`.\n * `needsInitialIdGeneration` is set to `true` when the Collaboration extension is\n * detected but no provider is available in its options, deferring ID creation\n * to the first `y-sync$` transaction.\n */\n addStorage() {\n return {\n needsInitialIdGeneration: false,\n }\n },\n\n addGlobalAttributes() {\n const types = resolveTypes(this.options.types, this.extensions)\n\n return [\n {\n 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 if (!this.options.updateDocument) {\n return\n }\n\n const collaboration = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaboration')\n const collaborationCaret = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaborationCaret')\n\n const collabExtensions = [collaboration, collaborationCaret].filter(Boolean)\n const collab = collabExtensions.find(ext => ext?.options?.provider)\n const provider = collab?.options?.provider\n\n const createIds = () => {\n const { view, state } = this.editor\n const { tr, doc } = state\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n const { attributeName, generateID } = this.options\n const nodesWithoutId = findChildren(doc, node => {\n return types.includes(node.type.name) && node.attrs[attributeName] === null\n })\n\n nodesWithoutId.forEach(({ node, pos }) => {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n })\n\n tr.setMeta('addToHistory', false)\n\n view.dispatch(tr)\n\n if (provider) {\n provider.off('synced', createIds)\n }\n }\n\n /**\n * We need to handle collaboration a bit different here\n * because we can't automatically add IDs when the provider is not yet synced\n * otherwise we end up with empty paragraphs\n */\n if (collaboration) {\n if (provider) {\n provider.on('synced', createIds)\n }\n // When collaboration is present but no provider is in extension options,\n // needsInitialIdGeneration was already set in addProseMirrorPlugins\n // (which runs synchronously during editor construction, before any\n // y-sync$ transaction can arrive).\n } else {\n return createIds()\n }\n },\n\n addProseMirrorPlugins() {\n if (!this.options.updateDocument) {\n return []\n }\n\n // Capture storage via closure so appendTransaction can access `needsInitialIdGeneration`.\n // `extensionManager.extensions` returns different objects than `this` due to\n // Tiptap's child-extension flattening, so we capture the reference directly.\n const extensionStorage = this.storage\n\n // Detect collaboration early — before any y-sync$ transaction can arrive.\n // onCreate runs on a deferred setTimeout(0), so a y-sync$ transaction could\n // be applied before it. Setting the flag here (synchronous during editor\n // construction) ensures the first y-sync$ is always handled.\n const collaboration = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaboration')\n const collaborationCaret = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaborationCaret')\n const collabExtensions = [collaboration, collaborationCaret].filter(Boolean)\n const collabWithProvider = collabExtensions.find(ext => ext?.options?.provider)\n\n if (collaboration && !collabWithProvider) {\n extensionStorage.needsInitialIdGeneration = true\n }\n\n let dragSourceElement: Element | null = null\n let transformPasted = false\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n\n return [\n new Plugin({\n key: new PluginKey('uniqueID'),\n\n appendTransaction: (transactions, oldState, newState) => {\n const hasDocChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n const filterTransactions =\n this.options.filterTransaction && transactions.some(tr => !this.options.filterTransaction?.(tr))\n\n const isCollabTransaction = transactions.find(tr => tr.getMeta('y-sync$'))\n\n if (isCollabTransaction) {\n if (extensionStorage.needsInitialIdGeneration) {\n extensionStorage.needsInitialIdGeneration = false\n\n // Run full-document ID creation after the first Yjs sync.\n // Use a seen-set so only the second+ occurrence of a duplicated\n // ID is regenerated, preserving one existing ID per value.\n const { tr } = newState\n const { attributeName, generateID } = this.options\n const allNodes = findChildren(newState.doc, node => types.includes(node.type.name))\n const seen = new Set<string>()\n\n allNodes.forEach(({ node, pos }) => {\n const currentId = node.attrs[attributeName]\n\n if (currentId === null || seen.has(currentId)) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n } else {\n seen.add(currentId)\n }\n })\n\n if (!tr.steps.length) {\n return\n }\n\n // Restore stored marks since setNodeMarkup resets them\n tr.setStoredMarks(newState.tr.storedMarks)\n // Mark this transaction as coming from UniqueID to prevent\n // infinite loops with other extensions (e.g., TrailingNode)\n tr.setMeta('__uniqueIDTransaction', true)\n tr.setMeta('addToHistory', false)\n return tr\n }\n\n return\n }\n\n if (!hasDocChanges || filterTransactions) {\n return\n }\n\n const { tr } = newState\n\n const { attributeName, generateID } = this.options\n const transform = combineTransactionSteps(oldState.doc, transactions as Transaction[])\n const { mapping } = transform\n\n // get changed ranges based on the old state\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n const newNodes = findChildrenInRange(newState.doc, newRange, node => {\n return types.includes(node.type.name)\n })\n\n const newIds = newNodes.map(({ node }) => node.attrs[attributeName]).filter(id => id !== null)\n\n newNodes.forEach(({ node, pos }, i) => {\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({ node, pos }),\n })\n\n return\n }\n\n const nextNode = newNodes[i + 1]\n\n if (nextNode && node.content.size === 0) {\n const nextNodeInTr = tr.doc.nodeAt(nextNode.pos)\n if (nextNodeInTr?.attrs[attributeName] && nextNodeInTr.attrs[attributeName] !== id) {\n return\n }\n\n tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n const generatedId = generateID({ node, pos })\n\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generatedId,\n })\n newIds[i] = generatedId\n\n return tr\n }\n\n const duplicatedNewIds = findDuplicates(newIds)\n\n // check if the node doesn’t exist in the old state\n const { deleted } = mapping.invert().mapResult(pos)\n\n const newNode = deleted && duplicatedNewIds.includes(id)\n\n if (newNode) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n }\n })\n })\n\n if (!tr.steps.length) {\n return\n }\n\n // `tr.setNodeMarkup` resets the stored marks\n // so we'll restore them if they exist\n tr.setStoredMarks(newState.tr.storedMarks)\n\n // Mark this transaction as coming from UniqueID\n // to prevent infinite loops with other extensions (e.g., TrailingNode)\n tr.setMeta('__uniqueIDTransaction', true)\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\n // or 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 === 'copyMove' ||\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 { 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\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 * Returns a list of duplicated items within an array.\n */\nexport function findDuplicates(items: any[]): any[] {\n const seen = new Set()\n const duplicates = new Set<any>()\n\n items.forEach(item => {\n if (seen.has(item)) {\n duplicates.add(item)\n } else {\n seen.add(item)\n }\n })\n\n return Array.from(duplicates)\n}\n","import type { Extensions, JSONContent } from '@tiptap/core'\nimport { findChildren, getSchema } from '@tiptap/core'\nimport { Node } from '@tiptap/pm/model'\nimport { EditorState } from '@tiptap/pm/state'\n\nimport type { UniqueID } from './unique-id.js'\n\n/**\n * Creates a new document with unique IDs added to the nodes. Does the same\n * thing as the UniqueID extension, but without the need to create an `Editor`\n * instance. This lets you add unique IDs to the document in the server.\n *\n * When you call it, include the `UniqueID` extension in the `extensions` array.\n * The configuration from the `UniqueID` extension will be picked up\n * automatically, including its configuration options like `types` and\n * `attributeName`.\n *\n * @see `UniqueID` extension for more information.\n *\n * @throws {Error} If the `UniqueID` extension is not found in the extensions array.\n *\n * @example\n * const doc = {\n * type: 'doc',\n * content: [\n * { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }\n * ]\n * }\n * const newDoc = addUniqueIds(doc, [StarterKit, UniqueID.configure({ types: ['paragraph', 'heading'] })])\n * console.log(newDoc)\n * // Result:\n * // {\n * // type: 'doc',\n * // content: [\n * // { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }], id: '123' }\n * // ]\n * // }\n *\n * @param doc - A Tiptap JSON document to add unique IDs to.\n * @param extensions - The extensions to use. Must include the `UniqueID` extension.\n * @returns The updated Tiptap JSON document, with the unique IDs added to the nodes.\n */\nexport function generateUniqueIds(doc: JSONContent, extensions: Extensions): JSONContent {\n // Find the UniqueID extension in the extensions array. If it's not found, throw an error.\n const uniqueIDExtension = extensions.find(ext => ext.name === 'uniqueID') as typeof UniqueID | undefined\n if (!uniqueIDExtension) {\n throw new Error('UniqueID extension not found in the extensions array')\n }\n // Convert the JSON content to a ProseMirror node\n const schema = getSchema([...extensions.filter(ext => ext.name !== 'uniqueID'), uniqueIDExtension])\n const { types: configuredTypes, attributeName, generateID } = uniqueIDExtension.options\n const types =\n configuredTypes === 'all'\n ? Object.keys(schema.nodes).filter(type => type !== 'doc' && type !== 'text')\n : configuredTypes\n const contentNode = Node.fromJSON(schema, doc)\n\n // Find nodes that don't have a unique ID\n const nodesWithoutId = findChildren(contentNode, node => {\n return !node.attrs[attributeName] && types.includes(node.type.name)\n })\n\n // Edit the document to add unique IDs to the nodes that don't have a unique ID\n let tr = EditorState.create({\n doc: contentNode,\n }).tr\n // eslint-disable-next-line no-restricted-syntax\n for (const { node, pos } of nodesWithoutId) {\n tr = tr.setNodeAttribute(pos, attributeName, generateID({ node, pos }))\n }\n\n // Return the updated document\n return tr.doc.toJSON()\n}\n"],"mappings":"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","names":["uuidv4","_a","tr","attributeName","generateID","import_core","import_model","import_state"]}
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,17 @@ var UniqueID = Extension.create({
|
|
|
47
47
|
updateDocument: true
|
|
48
48
|
};
|
|
49
49
|
},
|
|
50
|
+
/**
|
|
51
|
+
* Extension storage for coordination between `addProseMirrorPlugins` and `appendTransaction`.
|
|
52
|
+
* `needsInitialIdGeneration` is set to `true` when the Collaboration extension is
|
|
53
|
+
* detected but no provider is available in its options, deferring ID creation
|
|
54
|
+
* to the first `y-sync$` transaction.
|
|
55
|
+
*/
|
|
56
|
+
addStorage() {
|
|
57
|
+
return {
|
|
58
|
+
needsInitialIdGeneration: false
|
|
59
|
+
};
|
|
60
|
+
},
|
|
50
61
|
addGlobalAttributes() {
|
|
51
62
|
const types = resolveTypes(this.options.types, this.extensions);
|
|
52
63
|
return [
|
|
@@ -103,11 +114,10 @@ var UniqueID = Extension.create({
|
|
|
103
114
|
provider.off("synced", createIds);
|
|
104
115
|
}
|
|
105
116
|
};
|
|
106
|
-
if (
|
|
107
|
-
if (
|
|
108
|
-
|
|
117
|
+
if (collaboration) {
|
|
118
|
+
if (provider) {
|
|
119
|
+
provider.on("synced", createIds);
|
|
109
120
|
}
|
|
110
|
-
provider.on("synced", createIds);
|
|
111
121
|
} else {
|
|
112
122
|
return createIds();
|
|
113
123
|
}
|
|
@@ -116,6 +126,17 @@ var UniqueID = Extension.create({
|
|
|
116
126
|
if (!this.options.updateDocument) {
|
|
117
127
|
return [];
|
|
118
128
|
}
|
|
129
|
+
const extensionStorage = this.storage;
|
|
130
|
+
const collaboration = this.editor.extensionManager.extensions.find((ext) => ext.name === "collaboration");
|
|
131
|
+
const collaborationCaret = this.editor.extensionManager.extensions.find((ext) => ext.name === "collaborationCaret");
|
|
132
|
+
const collabExtensions = [collaboration, collaborationCaret].filter(Boolean);
|
|
133
|
+
const collabWithProvider = collabExtensions.find((ext) => {
|
|
134
|
+
var _a;
|
|
135
|
+
return (_a = ext == null ? void 0 : ext.options) == null ? void 0 : _a.provider;
|
|
136
|
+
});
|
|
137
|
+
if (collaboration && !collabWithProvider) {
|
|
138
|
+
extensionStorage.needsInitialIdGeneration = true;
|
|
139
|
+
}
|
|
119
140
|
let dragSourceElement = null;
|
|
120
141
|
let transformPasted = false;
|
|
121
142
|
const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions);
|
|
@@ -130,6 +151,31 @@ var UniqueID = Extension.create({
|
|
|
130
151
|
});
|
|
131
152
|
const isCollabTransaction = transactions.find((tr2) => tr2.getMeta("y-sync$"));
|
|
132
153
|
if (isCollabTransaction) {
|
|
154
|
+
if (extensionStorage.needsInitialIdGeneration) {
|
|
155
|
+
extensionStorage.needsInitialIdGeneration = false;
|
|
156
|
+
const { tr: tr2 } = newState;
|
|
157
|
+
const { attributeName: attributeName2, generateID: generateID2 } = this.options;
|
|
158
|
+
const allNodes = findChildren(newState.doc, (node) => types.includes(node.type.name));
|
|
159
|
+
const seen = /* @__PURE__ */ new Set();
|
|
160
|
+
allNodes.forEach(({ node, pos }) => {
|
|
161
|
+
const currentId = node.attrs[attributeName2];
|
|
162
|
+
if (currentId === null || seen.has(currentId)) {
|
|
163
|
+
tr2.setNodeMarkup(pos, void 0, {
|
|
164
|
+
...node.attrs,
|
|
165
|
+
[attributeName2]: generateID2({ node, pos })
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
seen.add(currentId);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (!tr2.steps.length) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
tr2.setStoredMarks(newState.tr.storedMarks);
|
|
175
|
+
tr2.setMeta("__uniqueIDTransaction", true);
|
|
176
|
+
tr2.setMeta("addToHistory", false);
|
|
177
|
+
return tr2;
|
|
178
|
+
}
|
|
133
179
|
return;
|
|
134
180
|
}
|
|
135
181
|
if (!hasDocChanges || filterTransactions) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/unique-id.ts","../src/helpers/findDuplicates.ts","../src/generate-unique-ids.ts","../src/index.ts"],"sourcesContent":["import {\n type Extensions,\n combineTransactionSteps,\n Extension,\n findChildren,\n findChildrenInRange,\n getChangedRanges,\n splitExtensions,\n} from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Fragment, Slice } from '@tiptap/pm/model'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { v4 as uuidv4 } from 'uuid'\n\nimport { findDuplicates } from './helpers/findDuplicates.js'\n\nexport type UniqueIDGenerationContext = {\n node: ProseMirrorNode\n pos: number\n}\n\nexport interface UniqueIDOptions {\n /**\n * The name of the attribute to add the unique ID to.\n * @default \"id\"\n */\n attributeName: string\n /**\n * The types of nodes to add unique IDs to.\n * Use `\"all\"` to add IDs to every node type except `doc` and `text`.\n * @default []\n */\n types: string[] | 'all'\n /**\n * The function that generates the unique ID. By default, a UUID v4 is\n * generated. However, you can provide your own function to generate the\n * unique ID based on the node type and the position.\n */\n generateID: (ctx: UniqueIDGenerationContext) => any\n /**\n * Ignore some mutations, for example applied from other users through the collaboration plugin.\n *\n * @default null\n */\n filterTransaction: ((transaction: Transaction) => boolean) | null\n /**\n * Whether to update the document by adding unique IDs to the nodes. Set this\n * property to `false` if the document is in `readonly` mode, is immutable, or\n * you don't want it to be modified.\n *\n * @default true\n */\n updateDocument: boolean\n}\n\nconst resolveTypes = (types: UniqueIDOptions['types'], extensions: Extensions): string[] => {\n if (types !== 'all') {\n return types\n }\n\n const { nodeExtensions } = splitExtensions(extensions)\n\n return nodeExtensions.map(extension => extension.name).filter(type => type !== 'doc' && type !== 'text')\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 addOptions() {\n return {\n attributeName: 'id',\n types: [],\n generateID: () => uuidv4(),\n filterTransaction: null,\n updateDocument: true,\n }\n },\n\n addGlobalAttributes() {\n const types = resolveTypes(this.options.types, this.extensions)\n\n return [\n {\n 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 if (!this.options.updateDocument) {\n return\n }\n\n const collaboration = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaboration')\n const collaborationCaret = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaborationCaret')\n\n const collabExtensions = [collaboration, collaborationCaret].filter(Boolean)\n const collab = collabExtensions.find(ext => ext?.options?.provider)\n const provider = collab?.options?.provider\n\n const createIds = () => {\n const { view, state } = this.editor\n const { tr, doc } = state\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n const { attributeName, generateID } = this.options\n const nodesWithoutId = findChildren(doc, node => {\n return types.includes(node.type.name) && node.attrs[attributeName] === null\n })\n\n nodesWithoutId.forEach(({ node, pos }) => {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n })\n\n tr.setMeta('addToHistory', false)\n\n view.dispatch(tr)\n\n if (provider) {\n provider.off('synced', createIds)\n }\n }\n\n /**\n * We need to handle collaboration a bit different here\n * because we can't automatically add IDs when the provider is not yet synced\n * otherwise we end up with empty paragraphs\n */\n if (collab) {\n if (!provider) {\n return createIds()\n }\n\n provider.on('synced', createIds)\n } else {\n return createIds()\n }\n },\n\n addProseMirrorPlugins() {\n if (!this.options.updateDocument) {\n return []\n }\n\n let dragSourceElement: Element | null = null\n let transformPasted = false\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n\n return [\n new Plugin({\n key: new PluginKey('uniqueID'),\n\n appendTransaction: (transactions, oldState, newState) => {\n const hasDocChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n const filterTransactions =\n this.options.filterTransaction && transactions.some(tr => !this.options.filterTransaction?.(tr))\n\n const isCollabTransaction = transactions.find(tr => tr.getMeta('y-sync$'))\n\n if (isCollabTransaction) {\n return\n }\n\n if (!hasDocChanges || filterTransactions) {\n return\n }\n\n const { tr } = newState\n\n const { attributeName, generateID } = this.options\n const transform = combineTransactionSteps(oldState.doc, transactions as Transaction[])\n const { mapping } = transform\n\n // get changed ranges based on the old state\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n const newNodes = findChildrenInRange(newState.doc, newRange, node => {\n return types.includes(node.type.name)\n })\n\n const newIds = newNodes.map(({ node }) => node.attrs[attributeName]).filter(id => id !== null)\n\n newNodes.forEach(({ node, pos }, i) => {\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({ node, pos }),\n })\n\n return\n }\n\n const nextNode = newNodes[i + 1]\n\n if (nextNode && node.content.size === 0) {\n const nextNodeInTr = tr.doc.nodeAt(nextNode.pos)\n if (nextNodeInTr?.attrs[attributeName] && nextNodeInTr.attrs[attributeName] !== id) {\n return\n }\n\n tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n const generatedId = generateID({ node, pos })\n\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generatedId,\n })\n newIds[i] = generatedId\n\n return tr\n }\n\n const duplicatedNewIds = findDuplicates(newIds)\n\n // check if the node doesn’t exist in the old state\n const { deleted } = mapping.invert().mapResult(pos)\n\n const newNode = deleted && duplicatedNewIds.includes(id)\n\n if (newNode) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n }\n })\n })\n\n if (!tr.steps.length) {\n return\n }\n\n // `tr.setNodeMarkup` resets the stored marks\n // so we'll restore them if they exist\n tr.setStoredMarks(newState.tr.storedMarks)\n\n // Mark this transaction as coming from UniqueID\n // to prevent infinite loops with other extensions (e.g., TrailingNode)\n tr.setMeta('__uniqueIDTransaction', true)\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\n // or 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 === 'copyMove' ||\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 { 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\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 * Returns a list of duplicated items within an array.\n */\nexport function findDuplicates(items: any[]): any[] {\n const seen = new Set()\n const duplicates = new Set<any>()\n\n items.forEach(item => {\n if (seen.has(item)) {\n duplicates.add(item)\n } else {\n seen.add(item)\n }\n })\n\n return Array.from(duplicates)\n}\n","import type { Extensions, JSONContent } from '@tiptap/core'\nimport { findChildren, getSchema } from '@tiptap/core'\nimport { Node } from '@tiptap/pm/model'\nimport { EditorState } from '@tiptap/pm/state'\n\nimport type { UniqueID } from './unique-id.js'\n\n/**\n * Creates a new document with unique IDs added to the nodes. Does the same\n * thing as the UniqueID extension, but without the need to create an `Editor`\n * instance. This lets you add unique IDs to the document in the server.\n *\n * When you call it, include the `UniqueID` extension in the `extensions` array.\n * The configuration from the `UniqueID` extension will be picked up\n * automatically, including its configuration options like `types` and\n * `attributeName`.\n *\n * @see `UniqueID` extension for more information.\n *\n * @throws {Error} If the `UniqueID` extension is not found in the extensions array.\n *\n * @example\n * const doc = {\n * type: 'doc',\n * content: [\n * { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }\n * ]\n * }\n * const newDoc = addUniqueIds(doc, [StarterKit, UniqueID.configure({ types: ['paragraph', 'heading'] })])\n * console.log(newDoc)\n * // Result:\n * // {\n * // type: 'doc',\n * // content: [\n * // { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }], id: '123' }\n * // ]\n * // }\n *\n * @param doc - A Tiptap JSON document to add unique IDs to.\n * @param extensions - The extensions to use. Must include the `UniqueID` extension.\n * @returns The updated Tiptap JSON document, with the unique IDs added to the nodes.\n */\nexport function generateUniqueIds(doc: JSONContent, extensions: Extensions): JSONContent {\n // Find the UniqueID extension in the extensions array. If it's not found, throw an error.\n const uniqueIDExtension = extensions.find(ext => ext.name === 'uniqueID') as typeof UniqueID | undefined\n if (!uniqueIDExtension) {\n throw new Error('UniqueID extension not found in the extensions array')\n }\n // Convert the JSON content to a ProseMirror node\n const schema = getSchema([...extensions.filter(ext => ext.name !== 'uniqueID'), uniqueIDExtension])\n const { types: configuredTypes, attributeName, generateID } = uniqueIDExtension.options\n const types =\n configuredTypes === 'all'\n ? Object.keys(schema.nodes).filter(type => type !== 'doc' && type !== 'text')\n : configuredTypes\n const contentNode = Node.fromJSON(schema, doc)\n\n // Find nodes that don't have a unique ID\n const nodesWithoutId = findChildren(contentNode, node => {\n return !node.attrs[attributeName] && types.includes(node.type.name)\n })\n\n // Edit the document to add unique IDs to the nodes that don't have a unique ID\n let tr = EditorState.create({\n doc: contentNode,\n }).tr\n // eslint-disable-next-line no-restricted-syntax\n for (const { node, pos } of nodesWithoutId) {\n tr = tr.setNodeAttribute(pos, attributeName, generateID({ node, pos }))\n }\n\n // Return the updated document\n return tr.doc.toJSON()\n}\n","import { UniqueID } from './unique-id.js'\n\nexport * from './generate-unique-ids.js'\nexport * from './unique-id.js'\n\nexport default UniqueID\n"],"mappings":"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","names":["_a","tr","findChildren"]}
|
|
1
|
+
{"version":3,"sources":["../src/unique-id.ts","../src/helpers/findDuplicates.ts","../src/generate-unique-ids.ts","../src/index.ts"],"sourcesContent":["import {\n type Extensions,\n combineTransactionSteps,\n Extension,\n findChildren,\n findChildrenInRange,\n getChangedRanges,\n splitExtensions,\n} from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Fragment, Slice } from '@tiptap/pm/model'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { v4 as uuidv4 } from 'uuid'\n\nimport { findDuplicates } from './helpers/findDuplicates.js'\n\nexport type UniqueIDGenerationContext = {\n node: ProseMirrorNode\n pos: number\n}\n\nexport interface UniqueIDOptions {\n /**\n * The name of the attribute to add the unique ID to.\n * @default \"id\"\n */\n attributeName: string\n /**\n * The types of nodes to add unique IDs to.\n * Use `\"all\"` to add IDs to every node type except `doc` and `text`.\n * @default []\n */\n types: string[] | 'all'\n /**\n * The function that generates the unique ID. By default, a UUID v4 is\n * generated. However, you can provide your own function to generate the\n * unique ID based on the node type and the position.\n */\n generateID: (ctx: UniqueIDGenerationContext) => any\n /**\n * Ignore some mutations, for example applied from other users through the collaboration plugin.\n *\n * @default null\n */\n filterTransaction: ((transaction: Transaction) => boolean) | null\n /**\n * Whether to update the document by adding unique IDs to the nodes. Set this\n * property to `false` if the document is in `readonly` mode, is immutable, or\n * you don't want it to be modified.\n *\n * @default true\n */\n updateDocument: boolean\n}\n\nconst resolveTypes = (types: UniqueIDOptions['types'], extensions: Extensions): string[] => {\n if (types !== 'all') {\n return types\n }\n\n const { nodeExtensions } = splitExtensions(extensions)\n\n return nodeExtensions.map(extension => extension.name).filter(type => type !== 'doc' && type !== 'text')\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 addOptions() {\n return {\n attributeName: 'id',\n types: [],\n generateID: () => uuidv4(),\n filterTransaction: null,\n updateDocument: true,\n }\n },\n\n /**\n * Extension storage for coordination between `addProseMirrorPlugins` and `appendTransaction`.\n * `needsInitialIdGeneration` is set to `true` when the Collaboration extension is\n * detected but no provider is available in its options, deferring ID creation\n * to the first `y-sync$` transaction.\n */\n addStorage() {\n return {\n needsInitialIdGeneration: false,\n }\n },\n\n addGlobalAttributes() {\n const types = resolveTypes(this.options.types, this.extensions)\n\n return [\n {\n 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 if (!this.options.updateDocument) {\n return\n }\n\n const collaboration = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaboration')\n const collaborationCaret = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaborationCaret')\n\n const collabExtensions = [collaboration, collaborationCaret].filter(Boolean)\n const collab = collabExtensions.find(ext => ext?.options?.provider)\n const provider = collab?.options?.provider\n\n const createIds = () => {\n const { view, state } = this.editor\n const { tr, doc } = state\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n const { attributeName, generateID } = this.options\n const nodesWithoutId = findChildren(doc, node => {\n return types.includes(node.type.name) && node.attrs[attributeName] === null\n })\n\n nodesWithoutId.forEach(({ node, pos }) => {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n })\n\n tr.setMeta('addToHistory', false)\n\n view.dispatch(tr)\n\n if (provider) {\n provider.off('synced', createIds)\n }\n }\n\n /**\n * We need to handle collaboration a bit different here\n * because we can't automatically add IDs when the provider is not yet synced\n * otherwise we end up with empty paragraphs\n */\n if (collaboration) {\n if (provider) {\n provider.on('synced', createIds)\n }\n // When collaboration is present but no provider is in extension options,\n // needsInitialIdGeneration was already set in addProseMirrorPlugins\n // (which runs synchronously during editor construction, before any\n // y-sync$ transaction can arrive).\n } else {\n return createIds()\n }\n },\n\n addProseMirrorPlugins() {\n if (!this.options.updateDocument) {\n return []\n }\n\n // Capture storage via closure so appendTransaction can access `needsInitialIdGeneration`.\n // `extensionManager.extensions` returns different objects than `this` due to\n // Tiptap's child-extension flattening, so we capture the reference directly.\n const extensionStorage = this.storage\n\n // Detect collaboration early — before any y-sync$ transaction can arrive.\n // onCreate runs on a deferred setTimeout(0), so a y-sync$ transaction could\n // be applied before it. Setting the flag here (synchronous during editor\n // construction) ensures the first y-sync$ is always handled.\n const collaboration = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaboration')\n const collaborationCaret = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaborationCaret')\n const collabExtensions = [collaboration, collaborationCaret].filter(Boolean)\n const collabWithProvider = collabExtensions.find(ext => ext?.options?.provider)\n\n if (collaboration && !collabWithProvider) {\n extensionStorage.needsInitialIdGeneration = true\n }\n\n let dragSourceElement: Element | null = null\n let transformPasted = false\n const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)\n\n return [\n new Plugin({\n key: new PluginKey('uniqueID'),\n\n appendTransaction: (transactions, oldState, newState) => {\n const hasDocChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n const filterTransactions =\n this.options.filterTransaction && transactions.some(tr => !this.options.filterTransaction?.(tr))\n\n const isCollabTransaction = transactions.find(tr => tr.getMeta('y-sync$'))\n\n if (isCollabTransaction) {\n if (extensionStorage.needsInitialIdGeneration) {\n extensionStorage.needsInitialIdGeneration = false\n\n // Run full-document ID creation after the first Yjs sync.\n // Use a seen-set so only the second+ occurrence of a duplicated\n // ID is regenerated, preserving one existing ID per value.\n const { tr } = newState\n const { attributeName, generateID } = this.options\n const allNodes = findChildren(newState.doc, node => types.includes(node.type.name))\n const seen = new Set<string>()\n\n allNodes.forEach(({ node, pos }) => {\n const currentId = node.attrs[attributeName]\n\n if (currentId === null || seen.has(currentId)) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n } else {\n seen.add(currentId)\n }\n })\n\n if (!tr.steps.length) {\n return\n }\n\n // Restore stored marks since setNodeMarkup resets them\n tr.setStoredMarks(newState.tr.storedMarks)\n // Mark this transaction as coming from UniqueID to prevent\n // infinite loops with other extensions (e.g., TrailingNode)\n tr.setMeta('__uniqueIDTransaction', true)\n tr.setMeta('addToHistory', false)\n return tr\n }\n\n return\n }\n\n if (!hasDocChanges || filterTransactions) {\n return\n }\n\n const { tr } = newState\n\n const { attributeName, generateID } = this.options\n const transform = combineTransactionSteps(oldState.doc, transactions as Transaction[])\n const { mapping } = transform\n\n // get changed ranges based on the old state\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n const newNodes = findChildrenInRange(newState.doc, newRange, node => {\n return types.includes(node.type.name)\n })\n\n const newIds = newNodes.map(({ node }) => node.attrs[attributeName]).filter(id => id !== null)\n\n newNodes.forEach(({ node, pos }, i) => {\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({ node, pos }),\n })\n\n return\n }\n\n const nextNode = newNodes[i + 1]\n\n if (nextNode && node.content.size === 0) {\n const nextNodeInTr = tr.doc.nodeAt(nextNode.pos)\n if (nextNodeInTr?.attrs[attributeName] && nextNodeInTr.attrs[attributeName] !== id) {\n return\n }\n\n tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n const generatedId = generateID({ node, pos })\n\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generatedId,\n })\n newIds[i] = generatedId\n\n return tr\n }\n\n const duplicatedNewIds = findDuplicates(newIds)\n\n // check if the node doesn’t exist in the old state\n const { deleted } = mapping.invert().mapResult(pos)\n\n const newNode = deleted && duplicatedNewIds.includes(id)\n\n if (newNode) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID({ node, pos }),\n })\n }\n })\n })\n\n if (!tr.steps.length) {\n return\n }\n\n // `tr.setNodeMarkup` resets the stored marks\n // so we'll restore them if they exist\n tr.setStoredMarks(newState.tr.storedMarks)\n\n // Mark this transaction as coming from UniqueID\n // to prevent infinite loops with other extensions (e.g., TrailingNode)\n tr.setMeta('__uniqueIDTransaction', true)\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\n // or 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 === 'copyMove' ||\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 { 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\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 * Returns a list of duplicated items within an array.\n */\nexport function findDuplicates(items: any[]): any[] {\n const seen = new Set()\n const duplicates = new Set<any>()\n\n items.forEach(item => {\n if (seen.has(item)) {\n duplicates.add(item)\n } else {\n seen.add(item)\n }\n })\n\n return Array.from(duplicates)\n}\n","import type { Extensions, JSONContent } from '@tiptap/core'\nimport { findChildren, getSchema } from '@tiptap/core'\nimport { Node } from '@tiptap/pm/model'\nimport { EditorState } from '@tiptap/pm/state'\n\nimport type { UniqueID } from './unique-id.js'\n\n/**\n * Creates a new document with unique IDs added to the nodes. Does the same\n * thing as the UniqueID extension, but without the need to create an `Editor`\n * instance. This lets you add unique IDs to the document in the server.\n *\n * When you call it, include the `UniqueID` extension in the `extensions` array.\n * The configuration from the `UniqueID` extension will be picked up\n * automatically, including its configuration options like `types` and\n * `attributeName`.\n *\n * @see `UniqueID` extension for more information.\n *\n * @throws {Error} If the `UniqueID` extension is not found in the extensions array.\n *\n * @example\n * const doc = {\n * type: 'doc',\n * content: [\n * { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }\n * ]\n * }\n * const newDoc = addUniqueIds(doc, [StarterKit, UniqueID.configure({ types: ['paragraph', 'heading'] })])\n * console.log(newDoc)\n * // Result:\n * // {\n * // type: 'doc',\n * // content: [\n * // { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }], id: '123' }\n * // ]\n * // }\n *\n * @param doc - A Tiptap JSON document to add unique IDs to.\n * @param extensions - The extensions to use. Must include the `UniqueID` extension.\n * @returns The updated Tiptap JSON document, with the unique IDs added to the nodes.\n */\nexport function generateUniqueIds(doc: JSONContent, extensions: Extensions): JSONContent {\n // Find the UniqueID extension in the extensions array. If it's not found, throw an error.\n const uniqueIDExtension = extensions.find(ext => ext.name === 'uniqueID') as typeof UniqueID | undefined\n if (!uniqueIDExtension) {\n throw new Error('UniqueID extension not found in the extensions array')\n }\n // Convert the JSON content to a ProseMirror node\n const schema = getSchema([...extensions.filter(ext => ext.name !== 'uniqueID'), uniqueIDExtension])\n const { types: configuredTypes, attributeName, generateID } = uniqueIDExtension.options\n const types =\n configuredTypes === 'all'\n ? Object.keys(schema.nodes).filter(type => type !== 'doc' && type !== 'text')\n : configuredTypes\n const contentNode = Node.fromJSON(schema, doc)\n\n // Find nodes that don't have a unique ID\n const nodesWithoutId = findChildren(contentNode, node => {\n return !node.attrs[attributeName] && types.includes(node.type.name)\n })\n\n // Edit the document to add unique IDs to the nodes that don't have a unique ID\n let tr = EditorState.create({\n doc: contentNode,\n }).tr\n // eslint-disable-next-line no-restricted-syntax\n for (const { node, pos } of nodesWithoutId) {\n tr = tr.setNodeAttribute(pos, attributeName, generateID({ node, pos }))\n }\n\n // Return the updated document\n return tr.doc.toJSON()\n}\n","import { UniqueID } from './unique-id.js'\n\nexport * from './generate-unique-ids.js'\nexport * from './unique-id.js'\n\nexport default UniqueID\n"],"mappings":"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","names":["_a","tr","attributeName","generateID","findChildren"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/extension-unique-id",
|
|
3
3
|
"description": "unique id extension for tiptap",
|
|
4
|
-
"version": "3.20.
|
|
4
|
+
"version": "3.20.4",
|
|
5
5
|
"homepage": "https://tiptap.dev/api/extensions/unique-id",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -31,16 +31,16 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@tiptap/core": "^3.20.
|
|
35
|
-
"@tiptap/pm": "^3.20.
|
|
34
|
+
"@tiptap/core": "^3.20.4",
|
|
35
|
+
"@tiptap/pm": "^3.20.4"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"uuid": "^10.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/uuid": "^10.0.0",
|
|
42
|
-
"@tiptap/core": "^3.20.
|
|
43
|
-
"@tiptap/pm": "^3.20.
|
|
42
|
+
"@tiptap/core": "^3.20.4",
|
|
43
|
+
"@tiptap/pm": "^3.20.4"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
package/src/unique-id.ts
CHANGED
|
@@ -81,6 +81,18 @@ export const UniqueID = Extension.create<UniqueIDOptions>({
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Extension storage for coordination between `addProseMirrorPlugins` and `appendTransaction`.
|
|
86
|
+
* `needsInitialIdGeneration` is set to `true` when the Collaboration extension is
|
|
87
|
+
* detected but no provider is available in its options, deferring ID creation
|
|
88
|
+
* to the first `y-sync$` transaction.
|
|
89
|
+
*/
|
|
90
|
+
addStorage() {
|
|
91
|
+
return {
|
|
92
|
+
needsInitialIdGeneration: false,
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
84
96
|
addGlobalAttributes() {
|
|
85
97
|
const types = resolveTypes(this.options.types, this.extensions)
|
|
86
98
|
|
|
@@ -149,12 +161,14 @@ export const UniqueID = Extension.create<UniqueIDOptions>({
|
|
|
149
161
|
* because we can't automatically add IDs when the provider is not yet synced
|
|
150
162
|
* otherwise we end up with empty paragraphs
|
|
151
163
|
*/
|
|
152
|
-
if (
|
|
153
|
-
if (
|
|
154
|
-
|
|
164
|
+
if (collaboration) {
|
|
165
|
+
if (provider) {
|
|
166
|
+
provider.on('synced', createIds)
|
|
155
167
|
}
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
// When collaboration is present but no provider is in extension options,
|
|
169
|
+
// needsInitialIdGeneration was already set in addProseMirrorPlugins
|
|
170
|
+
// (which runs synchronously during editor construction, before any
|
|
171
|
+
// y-sync$ transaction can arrive).
|
|
158
172
|
} else {
|
|
159
173
|
return createIds()
|
|
160
174
|
}
|
|
@@ -165,6 +179,24 @@ export const UniqueID = Extension.create<UniqueIDOptions>({
|
|
|
165
179
|
return []
|
|
166
180
|
}
|
|
167
181
|
|
|
182
|
+
// Capture storage via closure so appendTransaction can access `needsInitialIdGeneration`.
|
|
183
|
+
// `extensionManager.extensions` returns different objects than `this` due to
|
|
184
|
+
// Tiptap's child-extension flattening, so we capture the reference directly.
|
|
185
|
+
const extensionStorage = this.storage
|
|
186
|
+
|
|
187
|
+
// Detect collaboration early — before any y-sync$ transaction can arrive.
|
|
188
|
+
// onCreate runs on a deferred setTimeout(0), so a y-sync$ transaction could
|
|
189
|
+
// be applied before it. Setting the flag here (synchronous during editor
|
|
190
|
+
// construction) ensures the first y-sync$ is always handled.
|
|
191
|
+
const collaboration = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaboration')
|
|
192
|
+
const collaborationCaret = this.editor.extensionManager.extensions.find(ext => ext.name === 'collaborationCaret')
|
|
193
|
+
const collabExtensions = [collaboration, collaborationCaret].filter(Boolean)
|
|
194
|
+
const collabWithProvider = collabExtensions.find(ext => ext?.options?.provider)
|
|
195
|
+
|
|
196
|
+
if (collaboration && !collabWithProvider) {
|
|
197
|
+
extensionStorage.needsInitialIdGeneration = true
|
|
198
|
+
}
|
|
199
|
+
|
|
168
200
|
let dragSourceElement: Element | null = null
|
|
169
201
|
let transformPasted = false
|
|
170
202
|
const types = resolveTypes(this.options.types, this.editor.extensionManager.extensions)
|
|
@@ -182,6 +214,43 @@ export const UniqueID = Extension.create<UniqueIDOptions>({
|
|
|
182
214
|
const isCollabTransaction = transactions.find(tr => tr.getMeta('y-sync$'))
|
|
183
215
|
|
|
184
216
|
if (isCollabTransaction) {
|
|
217
|
+
if (extensionStorage.needsInitialIdGeneration) {
|
|
218
|
+
extensionStorage.needsInitialIdGeneration = false
|
|
219
|
+
|
|
220
|
+
// Run full-document ID creation after the first Yjs sync.
|
|
221
|
+
// Use a seen-set so only the second+ occurrence of a duplicated
|
|
222
|
+
// ID is regenerated, preserving one existing ID per value.
|
|
223
|
+
const { tr } = newState
|
|
224
|
+
const { attributeName, generateID } = this.options
|
|
225
|
+
const allNodes = findChildren(newState.doc, node => types.includes(node.type.name))
|
|
226
|
+
const seen = new Set<string>()
|
|
227
|
+
|
|
228
|
+
allNodes.forEach(({ node, pos }) => {
|
|
229
|
+
const currentId = node.attrs[attributeName]
|
|
230
|
+
|
|
231
|
+
if (currentId === null || seen.has(currentId)) {
|
|
232
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
233
|
+
...node.attrs,
|
|
234
|
+
[attributeName]: generateID({ node, pos }),
|
|
235
|
+
})
|
|
236
|
+
} else {
|
|
237
|
+
seen.add(currentId)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
if (!tr.steps.length) {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Restore stored marks since setNodeMarkup resets them
|
|
246
|
+
tr.setStoredMarks(newState.tr.storedMarks)
|
|
247
|
+
// Mark this transaction as coming from UniqueID to prevent
|
|
248
|
+
// infinite loops with other extensions (e.g., TrailingNode)
|
|
249
|
+
tr.setMeta('__uniqueIDTransaction', true)
|
|
250
|
+
tr.setMeta('addToHistory', false)
|
|
251
|
+
return tr
|
|
252
|
+
}
|
|
253
|
+
|
|
185
254
|
return
|
|
186
255
|
}
|
|
187
256
|
|