@tiptap/extension-unique-id 3.5.3 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +28 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -2
- package/dist/index.d.ts +57 -2
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/generate-unique-ids.ts +71 -0
- package/src/index.ts +1 -0
- package/src/unique-id.ts +18 -0
package/dist/index.cjs
CHANGED
|
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
UniqueID: () => UniqueID,
|
|
24
|
-
default: () => index_default
|
|
24
|
+
default: () => index_default,
|
|
25
|
+
generateUniqueIds: () => generateUniqueIds
|
|
25
26
|
});
|
|
26
27
|
module.exports = __toCommonJS(index_exports);
|
|
27
28
|
|
|
@@ -268,10 +269,35 @@ var UniqueID = import_core.Extension.create({
|
|
|
268
269
|
}
|
|
269
270
|
});
|
|
270
271
|
|
|
272
|
+
// src/generate-unique-ids.ts
|
|
273
|
+
var import_core2 = require("@tiptap/core");
|
|
274
|
+
var import_model2 = require("@tiptap/pm/model");
|
|
275
|
+
var import_state2 = require("@tiptap/pm/state");
|
|
276
|
+
function generateUniqueIds(doc, extensions) {
|
|
277
|
+
const uniqueIDExtension = extensions.find((ext) => ext.name === "uniqueID");
|
|
278
|
+
if (!uniqueIDExtension) {
|
|
279
|
+
throw new Error("UniqueID extension not found in the extensions array");
|
|
280
|
+
}
|
|
281
|
+
const { types, attributeName, generateID } = uniqueIDExtension.options;
|
|
282
|
+
const schema = (0, import_core2.getSchema)([...extensions.filter((ext) => ext.name !== "uniqueID"), uniqueIDExtension]);
|
|
283
|
+
const contentNode = import_model2.Node.fromJSON(schema, doc);
|
|
284
|
+
const nodesWithoutId = (0, import_core2.findChildren)(contentNode, (node) => {
|
|
285
|
+
return !node.attrs[attributeName] && types.includes(node.type.name);
|
|
286
|
+
});
|
|
287
|
+
let tr = import_state2.EditorState.create({
|
|
288
|
+
doc: contentNode
|
|
289
|
+
}).tr;
|
|
290
|
+
for (const { node, pos } of nodesWithoutId) {
|
|
291
|
+
tr = tr.setNodeAttribute(pos, attributeName, generateID({ node, pos }));
|
|
292
|
+
}
|
|
293
|
+
return tr.doc.toJSON();
|
|
294
|
+
}
|
|
295
|
+
|
|
271
296
|
// src/index.ts
|
|
272
297
|
var index_default = UniqueID;
|
|
273
298
|
// Annotate the CommonJS export names for ESM import in node:
|
|
274
299
|
0 && (module.exports = {
|
|
275
|
-
UniqueID
|
|
300
|
+
UniqueID,
|
|
301
|
+
generateUniqueIds
|
|
276
302
|
});
|
|
277
303
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/unique-id.ts","../src/helpers/removeDuplicates.ts","../src/helpers/findDuplicates.ts"],"sourcesContent":["import { UniqueID } from './unique-id.js'\n\nexport * from './unique-id.js'\n\nexport default UniqueID\n","import { combineTransactionSteps, Extension, findChildren, findChildrenInRange, getChangedRanges } 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 attributeName: string\n types: string[]\n generateID: (ctx: UniqueIDGenerationContext) => 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 addOptions() {\n return {\n attributeName: 'id',\n types: [],\n generateID: () => uuidv4(),\n filterTransaction: null,\n }\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 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, 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 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 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 { types, 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 tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n if (nextNode.node.attrs[attributeName]) {\n return\n }\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 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 { 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\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 * Removes duplicated values within an array.\n * Supports numbers, strings and objects.\n */\nexport 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) ? false : (seen[key] = true)\n })\n}\n","import { removeDuplicates } from './removeDuplicates.js'\n\n/**\n * Returns a list of duplicated items within an array.\n */\nexport 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"],"mappings":"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","names":["uuidv4","_a","tr"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/unique-id.ts","../src/helpers/removeDuplicates.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 { combineTransactionSteps, Extension, findChildren, findChildrenInRange, getChangedRanges } 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 * @default []\n */\n types: string[]\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\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 }\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 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, 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 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 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 { types, 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 tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n if (nextNode.node.attrs[attributeName]) {\n return\n }\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 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 { 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\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 * Removes duplicated values within an array.\n * Supports numbers, strings and objects.\n */\nexport 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) ? false : (seen[key] = true)\n })\n}\n","import { removeDuplicates } from './removeDuplicates.js'\n\n/**\n * Returns a list of duplicated items within an array.\n */\nexport 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 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 const { types, attributeName, generateID } = uniqueIDExtension.options\n\n // Convert the JSON content to a ProseMirror node\n const schema = getSchema([...extensions.filter(ext => ext.name !== 'uniqueID'), uniqueIDExtension])\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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
1
|
+
import { Extension, JSONContent, Extensions } from '@tiptap/core';
|
|
2
2
|
import { Node } from '@tiptap/pm/model';
|
|
3
3
|
import { Transaction } from '@tiptap/pm/state';
|
|
4
4
|
|
|
@@ -7,11 +7,66 @@ type UniqueIDGenerationContext = {
|
|
|
7
7
|
pos: number;
|
|
8
8
|
};
|
|
9
9
|
interface UniqueIDOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The name of the attribute to add the unique ID to.
|
|
12
|
+
* @default "id"
|
|
13
|
+
*/
|
|
10
14
|
attributeName: string;
|
|
15
|
+
/**
|
|
16
|
+
* The types of nodes to add unique IDs to.
|
|
17
|
+
* @default []
|
|
18
|
+
*/
|
|
11
19
|
types: string[];
|
|
20
|
+
/**
|
|
21
|
+
* The function that generates the unique ID. By default, a UUID v4 is
|
|
22
|
+
* generated. However, you can provide your own function to generate the
|
|
23
|
+
* unique ID based on the node type and the position.
|
|
24
|
+
*/
|
|
12
25
|
generateID: (ctx: UniqueIDGenerationContext) => any;
|
|
26
|
+
/**
|
|
27
|
+
* Ignore some mutations, for example applied from other users through the collaboration plugin.
|
|
28
|
+
*
|
|
29
|
+
* @default null
|
|
30
|
+
*/
|
|
13
31
|
filterTransaction: ((transaction: Transaction) => boolean) | null;
|
|
14
32
|
}
|
|
15
33
|
declare const UniqueID: Extension<UniqueIDOptions, any>;
|
|
16
34
|
|
|
17
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new document with unique IDs added to the nodes. Does the same
|
|
37
|
+
* thing as the UniqueID extension, but without the need to create an `Editor`
|
|
38
|
+
* instance. This lets you add unique IDs to the document in the server.
|
|
39
|
+
*
|
|
40
|
+
* When you call it, include the `UniqueID` extension in the `extensions` array.
|
|
41
|
+
* The configuration from the `UniqueID` extension will be picked up
|
|
42
|
+
* automatically, including its configuration options like `types` and
|
|
43
|
+
* `attributeName`.
|
|
44
|
+
*
|
|
45
|
+
* @see `UniqueID` extension for more information.
|
|
46
|
+
*
|
|
47
|
+
* @throws {Error} If the `UniqueID` extension is not found in the extensions array.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const doc = {
|
|
51
|
+
* type: 'doc',
|
|
52
|
+
* content: [
|
|
53
|
+
* { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }
|
|
54
|
+
* ]
|
|
55
|
+
* }
|
|
56
|
+
* const newDoc = addUniqueIds(doc, [StarterKit, UniqueID.configure({ types: ['paragraph', 'heading'] })])
|
|
57
|
+
* console.log(newDoc)
|
|
58
|
+
* // Result:
|
|
59
|
+
* // {
|
|
60
|
+
* // type: 'doc',
|
|
61
|
+
* // content: [
|
|
62
|
+
* // { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }], id: '123' }
|
|
63
|
+
* // ]
|
|
64
|
+
* // }
|
|
65
|
+
*
|
|
66
|
+
* @param doc - A Tiptap JSON document to add unique IDs to.
|
|
67
|
+
* @param extensions - The extensions to use. Must include the `UniqueID` extension.
|
|
68
|
+
* @returns The updated Tiptap JSON document, with the unique IDs added to the nodes.
|
|
69
|
+
*/
|
|
70
|
+
declare function generateUniqueIds(doc: JSONContent, extensions: Extensions): JSONContent;
|
|
71
|
+
|
|
72
|
+
export { UniqueID, type UniqueIDGenerationContext, type UniqueIDOptions, UniqueID as default, generateUniqueIds };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
1
|
+
import { Extension, JSONContent, Extensions } from '@tiptap/core';
|
|
2
2
|
import { Node } from '@tiptap/pm/model';
|
|
3
3
|
import { Transaction } from '@tiptap/pm/state';
|
|
4
4
|
|
|
@@ -7,11 +7,66 @@ type UniqueIDGenerationContext = {
|
|
|
7
7
|
pos: number;
|
|
8
8
|
};
|
|
9
9
|
interface UniqueIDOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The name of the attribute to add the unique ID to.
|
|
12
|
+
* @default "id"
|
|
13
|
+
*/
|
|
10
14
|
attributeName: string;
|
|
15
|
+
/**
|
|
16
|
+
* The types of nodes to add unique IDs to.
|
|
17
|
+
* @default []
|
|
18
|
+
*/
|
|
11
19
|
types: string[];
|
|
20
|
+
/**
|
|
21
|
+
* The function that generates the unique ID. By default, a UUID v4 is
|
|
22
|
+
* generated. However, you can provide your own function to generate the
|
|
23
|
+
* unique ID based on the node type and the position.
|
|
24
|
+
*/
|
|
12
25
|
generateID: (ctx: UniqueIDGenerationContext) => any;
|
|
26
|
+
/**
|
|
27
|
+
* Ignore some mutations, for example applied from other users through the collaboration plugin.
|
|
28
|
+
*
|
|
29
|
+
* @default null
|
|
30
|
+
*/
|
|
13
31
|
filterTransaction: ((transaction: Transaction) => boolean) | null;
|
|
14
32
|
}
|
|
15
33
|
declare const UniqueID: Extension<UniqueIDOptions, any>;
|
|
16
34
|
|
|
17
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new document with unique IDs added to the nodes. Does the same
|
|
37
|
+
* thing as the UniqueID extension, but without the need to create an `Editor`
|
|
38
|
+
* instance. This lets you add unique IDs to the document in the server.
|
|
39
|
+
*
|
|
40
|
+
* When you call it, include the `UniqueID` extension in the `extensions` array.
|
|
41
|
+
* The configuration from the `UniqueID` extension will be picked up
|
|
42
|
+
* automatically, including its configuration options like `types` and
|
|
43
|
+
* `attributeName`.
|
|
44
|
+
*
|
|
45
|
+
* @see `UniqueID` extension for more information.
|
|
46
|
+
*
|
|
47
|
+
* @throws {Error} If the `UniqueID` extension is not found in the extensions array.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const doc = {
|
|
51
|
+
* type: 'doc',
|
|
52
|
+
* content: [
|
|
53
|
+
* { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }
|
|
54
|
+
* ]
|
|
55
|
+
* }
|
|
56
|
+
* const newDoc = addUniqueIds(doc, [StarterKit, UniqueID.configure({ types: ['paragraph', 'heading'] })])
|
|
57
|
+
* console.log(newDoc)
|
|
58
|
+
* // Result:
|
|
59
|
+
* // {
|
|
60
|
+
* // type: 'doc',
|
|
61
|
+
* // content: [
|
|
62
|
+
* // { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }], id: '123' }
|
|
63
|
+
* // ]
|
|
64
|
+
* // }
|
|
65
|
+
*
|
|
66
|
+
* @param doc - A Tiptap JSON document to add unique IDs to.
|
|
67
|
+
* @param extensions - The extensions to use. Must include the `UniqueID` extension.
|
|
68
|
+
* @returns The updated Tiptap JSON document, with the unique IDs added to the nodes.
|
|
69
|
+
*/
|
|
70
|
+
declare function generateUniqueIds(doc: JSONContent, extensions: Extensions): JSONContent;
|
|
71
|
+
|
|
72
|
+
export { UniqueID, type UniqueIDGenerationContext, type UniqueIDOptions, UniqueID as default, generateUniqueIds };
|
package/dist/index.js
CHANGED
|
@@ -241,10 +241,35 @@ var UniqueID = Extension.create({
|
|
|
241
241
|
}
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
// src/generate-unique-ids.ts
|
|
245
|
+
import { findChildren as findChildren2, getSchema } from "@tiptap/core";
|
|
246
|
+
import { Node } from "@tiptap/pm/model";
|
|
247
|
+
import { EditorState } from "@tiptap/pm/state";
|
|
248
|
+
function generateUniqueIds(doc, extensions) {
|
|
249
|
+
const uniqueIDExtension = extensions.find((ext) => ext.name === "uniqueID");
|
|
250
|
+
if (!uniqueIDExtension) {
|
|
251
|
+
throw new Error("UniqueID extension not found in the extensions array");
|
|
252
|
+
}
|
|
253
|
+
const { types, attributeName, generateID } = uniqueIDExtension.options;
|
|
254
|
+
const schema = getSchema([...extensions.filter((ext) => ext.name !== "uniqueID"), uniqueIDExtension]);
|
|
255
|
+
const contentNode = Node.fromJSON(schema, doc);
|
|
256
|
+
const nodesWithoutId = findChildren2(contentNode, (node) => {
|
|
257
|
+
return !node.attrs[attributeName] && types.includes(node.type.name);
|
|
258
|
+
});
|
|
259
|
+
let tr = EditorState.create({
|
|
260
|
+
doc: contentNode
|
|
261
|
+
}).tr;
|
|
262
|
+
for (const { node, pos } of nodesWithoutId) {
|
|
263
|
+
tr = tr.setNodeAttribute(pos, attributeName, generateID({ node, pos }));
|
|
264
|
+
}
|
|
265
|
+
return tr.doc.toJSON();
|
|
266
|
+
}
|
|
267
|
+
|
|
244
268
|
// src/index.ts
|
|
245
269
|
var index_default = UniqueID;
|
|
246
270
|
export {
|
|
247
271
|
UniqueID,
|
|
248
|
-
index_default as default
|
|
272
|
+
index_default as default,
|
|
273
|
+
generateUniqueIds
|
|
249
274
|
};
|
|
250
275
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/unique-id.ts","../src/helpers/removeDuplicates.ts","../src/helpers/findDuplicates.ts","../src/index.ts"],"sourcesContent":["import { combineTransactionSteps, Extension, findChildren, findChildrenInRange, getChangedRanges } 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 attributeName: string\n types: string[]\n generateID: (ctx: UniqueIDGenerationContext) => 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 addOptions() {\n return {\n attributeName: 'id',\n types: [],\n generateID: () => uuidv4(),\n filterTransaction: null,\n }\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 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, 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 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 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 { types, 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 tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n if (nextNode.node.attrs[attributeName]) {\n return\n }\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 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 { 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\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 * Removes duplicated values within an array.\n * Supports numbers, strings and objects.\n */\nexport 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) ? false : (seen[key] = true)\n })\n}\n","import { removeDuplicates } from './removeDuplicates.js'\n\n/**\n * Returns a list of duplicated items within an array.\n */\nexport 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 { UniqueID } from './unique-id.js'\n\nexport * from './unique-id.js'\n\nexport default UniqueID\n"],"mappings":"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","names":["_a","tr"]}
|
|
1
|
+
{"version":3,"sources":["../src/unique-id.ts","../src/helpers/removeDuplicates.ts","../src/helpers/findDuplicates.ts","../src/generate-unique-ids.ts","../src/index.ts"],"sourcesContent":["import { combineTransactionSteps, Extension, findChildren, findChildrenInRange, getChangedRanges } 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 * @default []\n */\n types: string[]\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\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 }\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 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, 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 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 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 { types, 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 tr.setNodeMarkup(nextNode.pos, undefined, {\n ...nextNode.node.attrs,\n [attributeName]: id,\n })\n newIds[i + 1] = id\n\n if (nextNode.node.attrs[attributeName]) {\n return\n }\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 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 { 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\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 * Removes duplicated values within an array.\n * Supports numbers, strings and objects.\n */\nexport 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) ? false : (seen[key] = true)\n })\n}\n","import { removeDuplicates } from './removeDuplicates.js'\n\n/**\n * Returns a list of duplicated items within an array.\n */\nexport 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 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 const { types, attributeName, generateID } = uniqueIDExtension.options\n\n // Convert the JSON content to a ProseMirror node\n const schema = getSchema([...extensions.filter(ext => ext.name !== 'uniqueID'), uniqueIDExtension])\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"]}
|
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.
|
|
4
|
+
"version": "3.6.0",
|
|
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.
|
|
35
|
-
"@tiptap/pm": "^3.
|
|
34
|
+
"@tiptap/core": "^3.6.0",
|
|
35
|
+
"@tiptap/pm": "^3.6.0"
|
|
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.
|
|
43
|
-
"@tiptap/pm": "^3.
|
|
42
|
+
"@tiptap/core": "^3.6.0",
|
|
43
|
+
"@tiptap/pm": "^3.6.0"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Extensions, JSONContent } from '@tiptap/core'
|
|
2
|
+
import { findChildren, getSchema } from '@tiptap/core'
|
|
3
|
+
import { Node } from '@tiptap/pm/model'
|
|
4
|
+
import { EditorState } from '@tiptap/pm/state'
|
|
5
|
+
|
|
6
|
+
import type { UniqueID } from './unique-id.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new document with unique IDs added to the nodes. Does the same
|
|
10
|
+
* thing as the UniqueID extension, but without the need to create an `Editor`
|
|
11
|
+
* instance. This lets you add unique IDs to the document in the server.
|
|
12
|
+
*
|
|
13
|
+
* When you call it, include the `UniqueID` extension in the `extensions` array.
|
|
14
|
+
* The configuration from the `UniqueID` extension will be picked up
|
|
15
|
+
* automatically, including its configuration options like `types` and
|
|
16
|
+
* `attributeName`.
|
|
17
|
+
*
|
|
18
|
+
* @see `UniqueID` extension for more information.
|
|
19
|
+
*
|
|
20
|
+
* @throws {Error} If the `UniqueID` extension is not found in the extensions array.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const doc = {
|
|
24
|
+
* type: 'doc',
|
|
25
|
+
* content: [
|
|
26
|
+
* { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
* const newDoc = addUniqueIds(doc, [StarterKit, UniqueID.configure({ types: ['paragraph', 'heading'] })])
|
|
30
|
+
* console.log(newDoc)
|
|
31
|
+
* // Result:
|
|
32
|
+
* // {
|
|
33
|
+
* // type: 'doc',
|
|
34
|
+
* // content: [
|
|
35
|
+
* // { type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }], id: '123' }
|
|
36
|
+
* // ]
|
|
37
|
+
* // }
|
|
38
|
+
*
|
|
39
|
+
* @param doc - A Tiptap JSON document to add unique IDs to.
|
|
40
|
+
* @param extensions - The extensions to use. Must include the `UniqueID` extension.
|
|
41
|
+
* @returns The updated Tiptap JSON document, with the unique IDs added to the nodes.
|
|
42
|
+
*/
|
|
43
|
+
export function generateUniqueIds(doc: JSONContent, extensions: Extensions): JSONContent {
|
|
44
|
+
// Find the UniqueID extension in the extensions array. If it's not found, throw an error.
|
|
45
|
+
const uniqueIDExtension = extensions.find(ext => ext.name === 'uniqueID') as typeof UniqueID | undefined
|
|
46
|
+
if (!uniqueIDExtension) {
|
|
47
|
+
throw new Error('UniqueID extension not found in the extensions array')
|
|
48
|
+
}
|
|
49
|
+
const { types, attributeName, generateID } = uniqueIDExtension.options
|
|
50
|
+
|
|
51
|
+
// Convert the JSON content to a ProseMirror node
|
|
52
|
+
const schema = getSchema([...extensions.filter(ext => ext.name !== 'uniqueID'), uniqueIDExtension])
|
|
53
|
+
const contentNode = Node.fromJSON(schema, doc)
|
|
54
|
+
|
|
55
|
+
// Find nodes that don't have a unique ID
|
|
56
|
+
const nodesWithoutId = findChildren(contentNode, node => {
|
|
57
|
+
return !node.attrs[attributeName] && types.includes(node.type.name)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Edit the document to add unique IDs to the nodes that don't have a unique ID
|
|
61
|
+
let tr = EditorState.create({
|
|
62
|
+
doc: contentNode,
|
|
63
|
+
}).tr
|
|
64
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
65
|
+
for (const { node, pos } of nodesWithoutId) {
|
|
66
|
+
tr = tr.setNodeAttribute(pos, attributeName, generateID({ node, pos }))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Return the updated document
|
|
70
|
+
return tr.doc.toJSON()
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
package/src/unique-id.ts
CHANGED
|
@@ -13,9 +13,27 @@ export type UniqueIDGenerationContext = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface UniqueIDOptions {
|
|
16
|
+
/**
|
|
17
|
+
* The name of the attribute to add the unique ID to.
|
|
18
|
+
* @default "id"
|
|
19
|
+
*/
|
|
16
20
|
attributeName: string
|
|
21
|
+
/**
|
|
22
|
+
* The types of nodes to add unique IDs to.
|
|
23
|
+
* @default []
|
|
24
|
+
*/
|
|
17
25
|
types: string[]
|
|
26
|
+
/**
|
|
27
|
+
* The function that generates the unique ID. By default, a UUID v4 is
|
|
28
|
+
* generated. However, you can provide your own function to generate the
|
|
29
|
+
* unique ID based on the node type and the position.
|
|
30
|
+
*/
|
|
18
31
|
generateID: (ctx: UniqueIDGenerationContext) => any
|
|
32
|
+
/**
|
|
33
|
+
* Ignore some mutations, for example applied from other users through the collaboration plugin.
|
|
34
|
+
*
|
|
35
|
+
* @default null
|
|
36
|
+
*/
|
|
19
37
|
filterTransaction: ((transaction: Transaction) => boolean) | null
|
|
20
38
|
}
|
|
21
39
|
|