UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

163 lines (156 loc) 8.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.recomputeUnsyncTransaction = exports.recomputeDeleteTransaction = void 0; var _model = require("@atlaskit/editor-prosemirror/model"); 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; } /** * Whether `node` is the source block requested by `wanted`. * * Matching is done *per requested block* (rather than against global pools of * every requested localId/resourceId) so a node cannot be matched by combining * one block's `localId` with a different block's `resourceId`. We match on the * requested block's `localId` first and only fall back to its `resourceId` when * the `localId` has been regenerated (so it no longer matches the live node). */ var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) { var _ref = node.attrs, localId = _ref.localId, resourceId = _ref.resourceId; if (wanted.localId && localId && wanted.localId === localId) { return true; } return Boolean(wanted.resourceId && resourceId && wanted.resourceId === resourceId); }; /** * Locate the live positions of the source `bodiedSyncBlock` nodes identified by * `syncBlockIds`, matching first on `localId` and falling back to `resourceId`. * * This walks the *current* document (`tr.doc`) rather than relying on positions * captured when the delete was first triggered, so it is robust to intervening * local edits and remote collab changes that occur while the confirmation modal * is open. */ var findSourceBlocks = function findSourceBlocks(tr, isSourceBlock, syncBlockIds) { var matches = []; tr.doc.descendants(function (node, pos) { if (!isSourceBlock(node)) { // bodiedSyncBlock is always a top-level node, so there is no need to // descend into other branches looking for one. return false; } if (syncBlockIds.some(function (wanted) { return matchesRequestedBlock(node, wanted); })) { matches.push({ node: node, pos: pos }); } // Never recurse into a source block's body. return false; }); return matches; }; /** * Recompute a source `bodiedSyncBlock` delete from the live document. * * Instead of replaying a transaction that was stashed when the delete was first * triggered (and then manually rebased against every intervening edit — the * fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>" * and assorted position/open-depth errors), this finds the target node(s) by * `localId`/`resourceId` in the current state and issues a fresh * `tr.delete(pos, pos + nodeSize)` for each. * * Deletes are applied in reverse document order so earlier deletes do not * invalidate the positions of later ones. * * Partial matches are handled gracefully: when only some of the requested * blocks are still present (e.g. a remote collaborator removed the rest while * the confirmation modal was open), the found ones are deleted and the missing * ones are skipped. This is intentional — the backend deletion for every * requested block has already been issued by the store manager, so the local * transaction only needs to remove whatever is still in the live document. * * @returns the mutated transaction when at least one target node was found and * deleted (this may be a partial delete if some targets were already gone), * otherwise `undefined` when none of the targets exist any more — e.g. a remote * collaborator already removed them all — so there is nothing to delete. */ var recomputeDeleteTransaction = exports.recomputeDeleteTransaction = function recomputeDeleteTransaction(tr, isSourceBlock, syncBlockIds) { var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds); if (matches.length === 0) { return undefined; } // Delete in reverse document order so positions remain valid across deletes. matches.sort(function (a, b) { return b.pos - a.pos; }).forEach(function (_ref2) { var node = _ref2.node, pos = _ref2.pos; tr.delete(pos, pos + node.nodeSize); }); return tr; }; /** * Recompute a source `bodiedSyncBlock` *unsync* from the live document. * * Unsync differs from delete: the sync wrapper must be removed while its content * is preserved inline in the document. Instead of `tr.delete(pos, pos + * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each * matched source block with its own content. * * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content, * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing * block (e.g. the block's final panel/empty paragraph) when the unwrapped * content meets a document boundary, silently losing content (EDITOR-8230). A * zero-open `replace` drops the wrapper's own open ends while keeping every child * of the fragment as its own top-level node. * * Like `recomputeDeleteTransaction`, targets are located in the *current* * document (`tr.doc`) so the operation is robust to intervening local edits and * remote collab changes while the confirmation modal is open, and it handles * partial matches gracefully (missing blocks are skipped). * * Replacements are applied in reverse document order so earlier replacements do * not invalidate the positions of later ones. * * @returns the mutated transaction when at least one target node was found and * unwrapped, otherwise `undefined` when none of the targets exist any more. */ var recomputeUnsyncTransaction = exports.recomputeUnsyncTransaction = function recomputeUnsyncTransaction(tr, isSourceBlock, syncBlockIds) { var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds); if (matches.length === 0) { return undefined; } // Unwrap in reverse document order so positions remain valid across edits. // Use a raw zero-open Slice replace (not `replaceWith`) so the block's content // is inserted verbatim as top-level nodes without ProseMirror's replaceRange // heuristics collapsing a trailing block (EDITOR-8230). var orderedMatches = matches.sort(function (a, b) { return b.pos - a.pos; }); var _iterator = _createForOfIteratorHelper(orderedMatches), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var _step$value = _step.value, node = _step$value.node, pos = _step$value.pos; // This is ProseMirror's Transaction.replace (a document edit), not String.replace — the // perf rule misfires on the method name, and there is nothing to hoist. // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace tr.replace(pos, pos + node.nodeSize, new _model.Slice(node.content, 0, 0)); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return tr; };