UNPKG

@atlaskit/editor-plugin-local-id

Version:

LocalId plugin for @atlaskit/editor-core

241 lines (236 loc) 12.7 kB
import _toArray from "@babel/runtime/helpers/toArray"; function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } import { OverrideDocumentStep } from '@atlaskit/adf-schema/steps/override-document-step'; import { tintDirtyTransaction } from '@atlaskit/editor-common/collab'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { stepHasSlice } from '@atlaskit/editor-common/utils'; import { PluginKey } from '@atlaskit/editor-prosemirror/state'; import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled'; import { addMissingLocalIds } from '../editor-commands'; import { batchAddLocalIdToNodes, localIdNotEmpty } from './add-missing-local-ids'; import { generateShortUUID, generatedShortUUIDs } from './generateShortUUID'; export var localIdPluginKey = new PluginKey('localIdPlugin'); var generateUUID = function generateUUID() { return generateShortUUID(); }; // Fallback for Safari which doesn't support requestIdleCallback var requestIdleCallbackWithFallback = function requestIdleCallbackWithFallback(callback) { if (typeof requestIdleCallback !== 'undefined') { requestIdleCallback(callback); } else { // Fallback to requestAnimationFrame for Safari requestAnimationFrame(callback); } }; export var createPlugin = function createPlugin(api) { // Track if we've initialized existing UUIDs for this plugin instance var hasInitializedExistingUUIDs = false; return new SafePlugin({ key: localIdPluginKey, view: function view(editorView) { /** * This performs a one-time scan of the document to add local IDs * to nodes that don't have them. It's designed to run only once per * editor instance to avoid performance issues. */ if (api !== null && api !== void 0 && api.collabEdit) { return { update: function update() {} }; } requestIdleCallbackWithFallback(function () { if (isExperimentEnabled('platform_editor_local_id_reliability')) { api === null || api === void 0 || api.core.actions.execute(addMissingLocalIds()); return; } var tr = editorView.state.tr; var nodesToUpdate = new Map(); // position -> localId var _editorView$state$sch = editorView.state.schema.nodes, text = _editorView$state$sch.text, hardBreak = _editorView$state$sch.hardBreak, mediaGroup = _editorView$state$sch.mediaGroup; // Media group is ignored for now // https://bitbucket.org/atlassian/adf-schema/src/fb2236147a0c2bc9c8efbdb75fd8f8c411df44ba/packages/adf-schema/src/next-schema/nodes/mediaGroup.ts#lines-12 var ignoredNodeTypes = mediaGroup ? [text.name, hardBreak.name, mediaGroup.name] : [text.name, hardBreak.name]; editorView.state.doc.descendants(function (node, pos) { var _node$type$spec$attrs; if (!ignoredNodeTypes.includes(node.type.name) && !node.attrs.localId && !!((_node$type$spec$attrs = node.type.spec.attrs) !== null && _node$type$spec$attrs !== void 0 && _node$type$spec$attrs.localId)) { nodesToUpdate.set(pos, generateUUID()); } return true; // Continue traversing }); if (nodesToUpdate.size > 0) { batchAddLocalIdToNodes(nodesToUpdate, tr); tintDirtyTransaction(tr); editorView.dispatch(tr); } }); return { update: function update() {} }; }, /** * Handles adding local IDs to new nodes that are created and have the localId attribute * This ensures uniqueness of localIds on nodes being created or edited */ appendTransaction: function appendTransaction(transactions, _oldState, newState) { var _api$composition$shar; if (api !== null && api !== void 0 && (_api$composition$shar = api.composition.sharedState.currentState()) !== null && _api$composition$shar !== void 0 && _api$composition$shar.isComposing) { return undefined; } var modified = false; var tr = newState.tr; var _newState$schema$node = newState.schema.nodes, text = _newState$schema$node.text, hardBreak = _newState$schema$node.hardBreak, mediaGroup = _newState$schema$node.mediaGroup; // Media group is ignored for now // https://bitbucket.org/atlassian/adf-schema/src/fb2236147a0c2bc9c8efbdb75fd8f8c411df44ba/packages/adf-schema/src/next-schema/nodes/mediaGroup.ts#lines-12 var ignoredNodeTypes = [text === null || text === void 0 ? void 0 : text.name, hardBreak === null || hardBreak === void 0 ? void 0 : hardBreak.name, mediaGroup === null || mediaGroup === void 0 ? void 0 : mediaGroup.name]; var addedNodes = new Set(); // A single PMNode reference can appear at multiple positions in the doc (e.g. // `createTable` from `prosemirror-utils` reuses cell node objects across // non-header rows), so we track every position per node identity. var positionsByNode = new Map(); var localIds = new Set(); var nodesToUpdate = new Map(); // position -> localId // Process only the nodes added in the transactions transactions.forEach(function (transaction) { if (!transaction.docChanged) { return; } if (transaction.getMeta('uiEvent') === 'cut' || // We skip remote transactions as we don't want to affect transactions // created by other collaborators. // // Applying a template to a blank page arrives as a remote // OverrideDocumentStep (a full-document replacement with no localIds). // We still want those template nodes to get localIds, so we let such // transactions through. Ordinary remote edits are still skipped, and // existing localIds are never overwritten. Boolean(transaction.getMeta('isRemote')) && !transaction.steps.some(function (step) { return step instanceof OverrideDocumentStep; })) { return; } // Ignore local ID updates for certain transactions // this is purposely not a public API as we should not use // this except in some circumstances (ie. streaming) if (transaction.getMeta('ignoreLocalIdUpdate')) { return; } transaction.steps.forEach(function (step) { // Steps with a slice are scanned for nodes that need localIds. // OverrideDocumentStep (template replacement) has no `slice`, so we also // scan it so the inserted template nodes get localIds. var isTemplateOverrideStep = step instanceof OverrideDocumentStep; if (!stepHasSlice(step) && !isTemplateOverrideStep) { return; } step.getMap().forEach(function (oldStart, oldEnd, newStart, newEnd) { // Scan the changed range to find all nodes tr.doc.nodesBetween(newStart, Math.min(newEnd, tr.doc.content.size), function (node, pos) { var _node$type$spec$attrs2, _positionsByNode$get; if (ignoredNodeTypes.includes(node.type.name) || !((_node$type$spec$attrs2 = node.type.spec.attrs) !== null && _node$type$spec$attrs2 !== void 0 && _node$type$spec$attrs2.localId)) { return true; } modified = true; // Always add to addedNodes for duplicate prevention addedNodes.add(node); var positions = (_positionsByNode$get = positionsByNode.get(node)) !== null && _positionsByNode$get !== void 0 ? _positionsByNode$get : new Set(); positions.add(pos); positionsByNode.set(node, positions); return true; }); }); }); }); if (addedNodes.size > 0) { newState.doc.descendants(function (node) { var _node$attrs; // Also track existing UUIDs in the global Set for short UUID collision detection if ((_node$attrs = node.attrs) !== null && _node$attrs !== void 0 && _node$attrs.localId && !hasInitializedExistingUUIDs) { generatedShortUUIDs.add(node.attrs.localId); } if (addedNodes.has(node)) { return true; } localIds.add(node.attrs.localId); return true; }); hasInitializedExistingUUIDs = true; // Also ensure the added have no duplicates var seenIds = new Set(); var _iterator = _createForOfIteratorHelper(addedNodes), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var node = _step.value; var positions = positionsByNode.get(node); if (!positions || positions.size === 0) { continue; } var existingId = node.attrs.localId; var needsNewIds = (isExperimentEnabled('platform_editor_local_id_reliability') ? !localIdNotEmpty(existingId) : !existingId) || localIds.has(existingId) || seenIds.has(existingId); if (needsNewIds) { // No usable localId: assign a fresh unique one to every position. var _iterator2 = _createForOfIteratorHelper(positions), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var pos = _step2.value; var newId = generateUUID(); nodesToUpdate.set(pos, newId); seenIds.add(newId); modified = true; } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } else if (positions.size > 1) { // Shared node reference: keep the existing id at the first position, // assign fresh ones to the rest so they don't share the same localId. seenIds.add(existingId); var _Array$from = Array.from(positions), _Array$from2 = _toArray(_Array$from), _first = _Array$from2[0], rest = _arrayLikeToArray(_Array$from2).slice(1); var _iterator3 = _createForOfIteratorHelper(rest), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var _pos = _step3.value; var _newId = generateUUID(); nodesToUpdate.set(_pos, _newId); seenIds.add(_newId); modified = true; } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } else if (existingId) { seenIds.add(existingId); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } // Apply local ID updates based on the improvements feature flag: // - When enabled: Batch all updates into a single BatchAttrsStep // - When disabled: Individual steps were already applied above during node processing if (modified && nodesToUpdate.size > 0) { batchAddLocalIdToNodes(nodesToUpdate, tr); } return modified ? tr : undefined; } }); };