@atlaskit/editor-plugin-synced-block 10.5.0 → 10.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/AGENTS.md CHANGED
@@ -98,8 +98,8 @@ Behind the `editor_synced_block_perf` experiment, `main.ts`:
98
98
  2. Calls `editor-commands/createSyncedBlock` → inserts `bodiedSyncBlock` node into document (marked
99
99
  as **pending creation** — it is not persisted yet)
100
100
  3. `pm-plugins/main.ts` detects new node → `handle-bodied-sync-block-creation.ts` updates
101
- `sourceManager` state. Persistence happens later when the product layer calls `flush()`
102
- (via `flushBodiedSyncBlocks`), which creates the block in the Block Service and then reconciles
101
+ `sourceManager` state. Persistence happens later when the product layer calls `flush()` (via
102
+ `flushBodiedSyncBlocks`), which creates the block in the Block Service and then reconciles
103
103
  identifiers via `commitPendingCreation()`
104
104
  4. `menu-and-toolbar-experiences.ts` fires the experience event
105
105
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 10.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`564443b22eeeb`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/564443b22eeeb) -
8
+ Fix source synced block unsync deleting the entire block and its content instead of unwrapping it
9
+ when the cursor is inside the block (behind `platform_editor_blocks_patch_6`).
10
+
11
+ This release also adds a new `./types` subpath export so consumers can import the `SyncBlockAttrs`
12
+ type without reaching into the package internals:
13
+
14
+ ```ts
15
+ import type { SyncBlockAttrs } from '@atlaskit/editor-plugin-synced-block/types';
16
+ ```
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+
3
22
  ## 10.5.0
4
23
 
5
24
  ### Minor Changes
@@ -379,6 +379,12 @@ var unsync = exports.unsync = function unsync(storeManager, isBodiedSyncBlock, v
379
379
  return false;
380
380
  }
381
381
  if (isBodiedSyncBlock) {
382
+ // Signal the unsync intent. This transaction is intercepted by the plugin's
383
+ // filterTransaction, which shows the deletion-confirmation modal and, on confirm,
384
+ // recomputes the actual document change from the live document keyed off the
385
+ // `deletionReason: 'source-block-unsynced'` meta (see handleBodiedSyncBlockRemoval /
386
+ // recomputeUnsyncTransaction). The real unwrap-vs-delete decision therefore lives in the
387
+ // confirm path, not here (EDITOR-8230).
382
388
  var content = syncBlock === null || syncBlock === void 0 ? void 0 : syncBlock.node.content;
383
389
  var tr = state.tr;
384
390
  tr.replaceWith(syncBlock.pos, syncBlock.pos + syncBlock.node.nodeSize, content).setMeta('deletionReason', 'source-block-unsynced');
@@ -56,7 +56,15 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
56
56
  // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
57
57
  api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
58
58
  var tr = _ref2.tr;
59
- var recomputedTr = (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
59
+ // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
60
+ // content inline, whereas delete removes the whole block. Before this branch,
61
+ // the confirm path always recomputed a full delete, so unsyncing a source block
62
+ // silently deleted its content. When the deletion was actually triggered by
63
+ // unsync, recompute an unwrap (replaceWith content) instead of a delete.
64
+ var isUnsync = (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_6') && deletionReason === 'source-block-unsynced';
65
+ var recomputedTr = isUnsync ? (0, _recomputeDeleteTransaction.recomputeUnsyncTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
66
+ return node.attrs;
67
+ })) : (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
60
68
  return node.attrs;
61
69
  }));
62
70
 
@@ -3,7 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.recomputeDeleteTransaction = void 0;
6
+ exports.recomputeUnsyncTransaction = exports.recomputeDeleteTransaction = void 0;
7
+ var _model = require("@atlaskit/editor-prosemirror/model");
8
+ 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; } } }; }
9
+ 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; } }
10
+ 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; }
7
11
  /**
8
12
  * Whether `node` is the source block requested by `wanted`.
9
13
  *
@@ -95,4 +99,65 @@ var recomputeDeleteTransaction = exports.recomputeDeleteTransaction = function r
95
99
  tr.delete(pos, pos + node.nodeSize);
96
100
  });
97
101
  return tr;
102
+ };
103
+
104
+ /**
105
+ * Recompute a source `bodiedSyncBlock` *unsync* from the live document.
106
+ *
107
+ * Unsync differs from delete: the sync wrapper must be removed while its content
108
+ * is preserved inline in the document. Instead of `tr.delete(pos, pos +
109
+ * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
110
+ * matched source block with its own content.
111
+ *
112
+ * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
113
+ * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
114
+ * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
115
+ * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
116
+ * block (e.g. the block's final panel/empty paragraph) when the unwrapped
117
+ * content meets a document boundary, silently losing content (EDITOR-8230). A
118
+ * zero-open `replace` drops the wrapper's own open ends while keeping every child
119
+ * of the fragment as its own top-level node.
120
+ *
121
+ * Like `recomputeDeleteTransaction`, targets are located in the *current*
122
+ * document (`tr.doc`) so the operation is robust to intervening local edits and
123
+ * remote collab changes while the confirmation modal is open, and it handles
124
+ * partial matches gracefully (missing blocks are skipped).
125
+ *
126
+ * Replacements are applied in reverse document order so earlier replacements do
127
+ * not invalidate the positions of later ones.
128
+ *
129
+ * @returns the mutated transaction when at least one target node was found and
130
+ * unwrapped, otherwise `undefined` when none of the targets exist any more.
131
+ */
132
+ var recomputeUnsyncTransaction = exports.recomputeUnsyncTransaction = function recomputeUnsyncTransaction(tr, isSourceBlock, syncBlockIds) {
133
+ var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
134
+ if (matches.length === 0) {
135
+ return undefined;
136
+ }
137
+
138
+ // Unwrap in reverse document order so positions remain valid across edits.
139
+ // Use a raw zero-open Slice replace (not `replaceWith`) so the block's content
140
+ // is inserted verbatim as top-level nodes without ProseMirror's replaceRange
141
+ // heuristics collapsing a trailing block (EDITOR-8230).
142
+ var orderedMatches = matches.sort(function (a, b) {
143
+ return b.pos - a.pos;
144
+ });
145
+ var _iterator = _createForOfIteratorHelper(orderedMatches),
146
+ _step;
147
+ try {
148
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
149
+ var _step$value = _step.value,
150
+ node = _step$value.node,
151
+ pos = _step$value.pos;
152
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace — the
153
+ // perf rule misfires on the method name, and there is nothing to hoist.
154
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
155
+ tr.replace(pos, pos + node.nodeSize, new _model.Slice(node.content, 0, 0));
156
+ }
157
+ } catch (err) {
158
+ _iterator.e(err);
159
+ } finally {
160
+ _iterator.f();
161
+ }
162
+ return tr;
98
163
  };
@@ -381,6 +381,12 @@ export const unsync = (storeManager, isBodiedSyncBlock, view) => {
381
381
  return false;
382
382
  }
383
383
  if (isBodiedSyncBlock) {
384
+ // Signal the unsync intent. This transaction is intercepted by the plugin's
385
+ // filterTransaction, which shows the deletion-confirmation modal and, on confirm,
386
+ // recomputes the actual document change from the live document keyed off the
387
+ // `deletionReason: 'source-block-unsynced'` meta (see handleBodiedSyncBlockRemoval /
388
+ // recomputeUnsyncTransaction). The real unwrap-vs-delete decision therefore lives in the
389
+ // confirm path, not here (EDITOR-8230).
384
390
  const content = syncBlock === null || syncBlock === void 0 ? void 0 : syncBlock.node.content;
385
391
  const {
386
392
  tr
@@ -2,7 +2,7 @@ import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
2
2
  import { fg } from '@atlaskit/platform-feature-flags';
3
3
  import { FLAG_ID } from '../../types';
4
4
  import { syncedBlockPluginKey } from '../main';
5
- import { recomputeDeleteTransaction } from './recompute-delete-transaction';
5
+ import { recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction';
6
6
  const onRetry = (api, syncBlockStore) => () => {
7
7
  var _api$core;
8
8
  api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
@@ -42,7 +42,13 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
42
42
  api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
43
43
  tr
44
44
  }) => {
45
- const recomputedTr = recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs));
45
+ // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
46
+ // content inline, whereas delete removes the whole block. Before this branch,
47
+ // the confirm path always recomputed a full delete, so unsyncing a source block
48
+ // silently deleted its content. When the deletion was actually triggered by
49
+ // unsync, recompute an unwrap (replaceWith content) instead of a delete.
50
+ const isUnsync = fg('platform_editor_blocks_patch_6') && deletionReason === 'source-block-unsynced';
51
+ const recomputedTr = isUnsync ? recomputeUnsyncTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs)) : recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs));
46
52
 
47
53
  // The target node(s) no longer exist in the live document (e.g.
48
54
  // a remote collaborator already removed them). There is nothing
@@ -1,3 +1,4 @@
1
+ import { Slice } from '@atlaskit/editor-prosemirror/model';
1
2
  /**
2
3
  * Whether `node` is the source block requested by `wanted`.
3
4
  *
@@ -87,4 +88,55 @@ export const recomputeDeleteTransaction = (tr, isSourceBlock, syncBlockIds) => {
87
88
  tr.delete(pos, pos + node.nodeSize);
88
89
  });
89
90
  return tr;
91
+ };
92
+
93
+ /**
94
+ * Recompute a source `bodiedSyncBlock` *unsync* from the live document.
95
+ *
96
+ * Unsync differs from delete: the sync wrapper must be removed while its content
97
+ * is preserved inline in the document. Instead of `tr.delete(pos, pos +
98
+ * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
99
+ * matched source block with its own content.
100
+ *
101
+ * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
102
+ * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
103
+ * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
104
+ * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
105
+ * block (e.g. the block's final panel/empty paragraph) when the unwrapped
106
+ * content meets a document boundary, silently losing content (EDITOR-8230). A
107
+ * zero-open `replace` drops the wrapper's own open ends while keeping every child
108
+ * of the fragment as its own top-level node.
109
+ *
110
+ * Like `recomputeDeleteTransaction`, targets are located in the *current*
111
+ * document (`tr.doc`) so the operation is robust to intervening local edits and
112
+ * remote collab changes while the confirmation modal is open, and it handles
113
+ * partial matches gracefully (missing blocks are skipped).
114
+ *
115
+ * Replacements are applied in reverse document order so earlier replacements do
116
+ * not invalidate the positions of later ones.
117
+ *
118
+ * @returns the mutated transaction when at least one target node was found and
119
+ * unwrapped, otherwise `undefined` when none of the targets exist any more.
120
+ */
121
+ export const recomputeUnsyncTransaction = (tr, isSourceBlock, syncBlockIds) => {
122
+ const matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
123
+ if (matches.length === 0) {
124
+ return undefined;
125
+ }
126
+
127
+ // Unwrap in reverse document order so positions remain valid across edits.
128
+ // Use a raw zero-open Slice replace (not `replaceWith`) so the block's content
129
+ // is inserted verbatim as top-level nodes without ProseMirror's replaceRange
130
+ // heuristics collapsing a trailing block (EDITOR-8230).
131
+ const orderedMatches = matches.sort((a, b) => b.pos - a.pos);
132
+ for (const {
133
+ node,
134
+ pos
135
+ } of orderedMatches) {
136
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace — the
137
+ // perf rule misfires on the method name, and there is nothing to hoist.
138
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
139
+ tr.replace(pos, pos + node.nodeSize, new Slice(node.content, 0, 0));
140
+ }
141
+ return tr;
90
142
  };
@@ -372,6 +372,12 @@ export var unsync = function unsync(storeManager, isBodiedSyncBlock, view) {
372
372
  return false;
373
373
  }
374
374
  if (isBodiedSyncBlock) {
375
+ // Signal the unsync intent. This transaction is intercepted by the plugin's
376
+ // filterTransaction, which shows the deletion-confirmation modal and, on confirm,
377
+ // recomputes the actual document change from the live document keyed off the
378
+ // `deletionReason: 'source-block-unsynced'` meta (see handleBodiedSyncBlockRemoval /
379
+ // recomputeUnsyncTransaction). The real unwrap-vs-delete decision therefore lives in the
380
+ // confirm path, not here (EDITOR-8230).
375
381
  var content = syncBlock === null || syncBlock === void 0 ? void 0 : syncBlock.node.content;
376
382
  var tr = state.tr;
377
383
  tr.replaceWith(syncBlock.pos, syncBlock.pos + syncBlock.node.nodeSize, content).setMeta('deletionReason', 'source-block-unsynced');
@@ -5,7 +5,7 @@ import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
5
5
  import { fg } from '@atlaskit/platform-feature-flags';
6
6
  import { FLAG_ID } from '../../types';
7
7
  import { syncedBlockPluginKey } from '../main';
8
- import { recomputeDeleteTransaction } from './recompute-delete-transaction';
8
+ import { recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction';
9
9
  var onRetry = function onRetry(api, syncBlockStore) {
10
10
  return function () {
11
11
  var _api$core;
@@ -49,7 +49,15 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
49
49
  // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
50
50
  api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
51
51
  var tr = _ref2.tr;
52
- var recomputedTr = recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
52
+ // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
53
+ // content inline, whereas delete removes the whole block. Before this branch,
54
+ // the confirm path always recomputed a full delete, so unsyncing a source block
55
+ // silently deleted its content. When the deletion was actually triggered by
56
+ // unsync, recompute an unwrap (replaceWith content) instead of a delete.
57
+ var isUnsync = fg('platform_editor_blocks_patch_6') && deletionReason === 'source-block-unsynced';
58
+ var recomputedTr = isUnsync ? recomputeUnsyncTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
59
+ return node.attrs;
60
+ })) : recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
53
61
  return node.attrs;
54
62
  }));
55
63
 
@@ -1,3 +1,7 @@
1
+ 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; } } }; }
2
+ 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; } }
3
+ 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; }
4
+ import { Slice } from '@atlaskit/editor-prosemirror/model';
1
5
  /**
2
6
  * Whether `node` is the source block requested by `wanted`.
3
7
  *
@@ -89,4 +93,65 @@ export var recomputeDeleteTransaction = function recomputeDeleteTransaction(tr,
89
93
  tr.delete(pos, pos + node.nodeSize);
90
94
  });
91
95
  return tr;
96
+ };
97
+
98
+ /**
99
+ * Recompute a source `bodiedSyncBlock` *unsync* from the live document.
100
+ *
101
+ * Unsync differs from delete: the sync wrapper must be removed while its content
102
+ * is preserved inline in the document. Instead of `tr.delete(pos, pos +
103
+ * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
104
+ * matched source block with its own content.
105
+ *
106
+ * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
107
+ * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
108
+ * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
109
+ * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
110
+ * block (e.g. the block's final panel/empty paragraph) when the unwrapped
111
+ * content meets a document boundary, silently losing content (EDITOR-8230). A
112
+ * zero-open `replace` drops the wrapper's own open ends while keeping every child
113
+ * of the fragment as its own top-level node.
114
+ *
115
+ * Like `recomputeDeleteTransaction`, targets are located in the *current*
116
+ * document (`tr.doc`) so the operation is robust to intervening local edits and
117
+ * remote collab changes while the confirmation modal is open, and it handles
118
+ * partial matches gracefully (missing blocks are skipped).
119
+ *
120
+ * Replacements are applied in reverse document order so earlier replacements do
121
+ * not invalidate the positions of later ones.
122
+ *
123
+ * @returns the mutated transaction when at least one target node was found and
124
+ * unwrapped, otherwise `undefined` when none of the targets exist any more.
125
+ */
126
+ export var recomputeUnsyncTransaction = function recomputeUnsyncTransaction(tr, isSourceBlock, syncBlockIds) {
127
+ var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
128
+ if (matches.length === 0) {
129
+ return undefined;
130
+ }
131
+
132
+ // Unwrap in reverse document order so positions remain valid across edits.
133
+ // Use a raw zero-open Slice replace (not `replaceWith`) so the block's content
134
+ // is inserted verbatim as top-level nodes without ProseMirror's replaceRange
135
+ // heuristics collapsing a trailing block (EDITOR-8230).
136
+ var orderedMatches = matches.sort(function (a, b) {
137
+ return b.pos - a.pos;
138
+ });
139
+ var _iterator = _createForOfIteratorHelper(orderedMatches),
140
+ _step;
141
+ try {
142
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
143
+ var _step$value = _step.value,
144
+ node = _step$value.node,
145
+ pos = _step$value.pos;
146
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace — the
147
+ // perf rule misfires on the method name, and there is nothing to hoist.
148
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
149
+ tr.replace(pos, pos + node.nodeSize, new Slice(node.content, 0, 0));
150
+ }
151
+ } catch (err) {
152
+ _iterator.e(err);
153
+ } finally {
154
+ _iterator.f();
155
+ }
156
+ return tr;
92
157
  };
@@ -27,3 +27,32 @@ import type { SyncBlockAttrs } from '../../types';
27
27
  * collaborator already removed them all — so there is nothing to delete.
28
28
  */
29
29
  export declare const recomputeDeleteTransaction: (tr: Transaction, isSourceBlock: (node: PMNode) => boolean, syncBlockIds: SyncBlockAttrs[]) => Transaction | undefined;
30
+ /**
31
+ * Recompute a source `bodiedSyncBlock` *unsync* from the live document.
32
+ *
33
+ * Unsync differs from delete: the sync wrapper must be removed while its content
34
+ * is preserved inline in the document. Instead of `tr.delete(pos, pos +
35
+ * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
36
+ * matched source block with its own content.
37
+ *
38
+ * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
39
+ * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
40
+ * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
41
+ * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
42
+ * block (e.g. the block's final panel/empty paragraph) when the unwrapped
43
+ * content meets a document boundary, silently losing content (EDITOR-8230). A
44
+ * zero-open `replace` drops the wrapper's own open ends while keeping every child
45
+ * of the fragment as its own top-level node.
46
+ *
47
+ * Like `recomputeDeleteTransaction`, targets are located in the *current*
48
+ * document (`tr.doc`) so the operation is robust to intervening local edits and
49
+ * remote collab changes while the confirmation modal is open, and it handles
50
+ * partial matches gracefully (missing blocks are skipped).
51
+ *
52
+ * Replacements are applied in reverse document order so earlier replacements do
53
+ * not invalidate the positions of later ones.
54
+ *
55
+ * @returns the mutated transaction when at least one target node was found and
56
+ * unwrapped, otherwise `undefined` when none of the targets exist any more.
57
+ */
58
+ export declare const recomputeUnsyncTransaction: (tr: Transaction, isSourceBlock: (node: PMNode) => boolean, syncBlockIds: SyncBlockAttrs[]) => Transaction | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "10.5.0",
3
+ "version": "10.6.0",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -46,7 +46,7 @@
46
46
  "@atlaskit/platform-feature-flags": "^2.1.0",
47
47
  "@atlaskit/primitives": "^22.2.0",
48
48
  "@atlaskit/spinner": "20.1.1",
49
- "@atlaskit/tmp-editor-statsig": "^135.3.0",
49
+ "@atlaskit/tmp-editor-statsig": "^135.5.0",
50
50
  "@atlaskit/tokens": "16.3.0",
51
51
  "@atlaskit/tooltip": "^23.2.0",
52
52
  "@atlaskit/visually-hidden": "^4.2.0",
@@ -110,6 +110,9 @@
110
110
  "platform_editor_blocks_patch_4": {
111
111
  "type": "boolean"
112
112
  },
113
+ "platform_editor_blocks_patch_6": {
114
+ "type": "boolean"
115
+ },
113
116
  "platform_synced_block_patch_13": {
114
117
  "type": "boolean"
115
118
  },
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@atlaskit/editor-plugin-synced-block/types",
3
+ "main": "../dist/cjs/types/index.js",
4
+ "module": "../dist/esm/types/index.js",
5
+ "module:es2019": "../dist/es2019/types/index.js",
6
+ "sideEffects": false,
7
+ "types": "../dist/types/types/index.d.ts"
8
+ }