@wordpress/editor 14.39.0 → 14.39.1-next.v.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.
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // packages/editor/src/components/post-revisions-preview/preserve-client-ids.js
21
+ var preserve_client_ids_exports = {};
22
+ __export(preserve_client_ids_exports, {
23
+ preserveClientIds: () => preserveClientIds
24
+ });
25
+ module.exports = __toCommonJS(preserve_client_ids_exports);
26
+ var import_array = require("diff/lib/diff/array");
27
+ function preserveClientIds(newBlocks, prevBlocks) {
28
+ if (!prevBlocks?.length || !newBlocks?.length) {
29
+ return newBlocks;
30
+ }
31
+ const newSigs = newBlocks.map((block) => block.name);
32
+ const prevSigs = prevBlocks.map((block) => block.name);
33
+ const diffResult = (0, import_array.diffArrays)(prevSigs, newSigs);
34
+ let newIndex = 0;
35
+ let prevIndex = 0;
36
+ const result = [];
37
+ for (const chunk of diffResult) {
38
+ if (chunk.removed) {
39
+ prevIndex += chunk.count;
40
+ } else if (chunk.added) {
41
+ for (let i = 0; i < chunk.count; i++) {
42
+ result.push(newBlocks[newIndex++]);
43
+ }
44
+ } else {
45
+ for (let i = 0; i < chunk.count; i++) {
46
+ const newBlock = newBlocks[newIndex++];
47
+ const prevBlock = prevBlocks[prevIndex++];
48
+ result.push({
49
+ ...newBlock,
50
+ clientId: prevBlock.clientId,
51
+ innerBlocks: preserveClientIds(
52
+ newBlock.innerBlocks,
53
+ prevBlock.innerBlocks
54
+ )
55
+ });
56
+ }
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+ // Annotate the CommonJS export names for ESM import in node:
62
+ 0 && (module.exports = {
63
+ preserveClientIds
64
+ });
65
+ //# sourceMappingURL=preserve-client-ids.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/components/post-revisions-preview/preserve-client-ids.js"],
4
+ "sourcesContent": ["/**\n * External dependencies\n */\nimport { diffArrays } from 'diff/lib/diff/array';\n\n/**\n * Preserves clientIds from previously rendered blocks to prevent flashing.\n * Uses LCS algorithm to match blocks by name.\n *\n * This compares the newly parsed blocks against the last rendered blocks\n * to maintain React key stability.\n *\n * @param {Array} newBlocks Newly parsed blocks with fresh clientIds.\n * @param {Array} prevBlocks Previously rendered blocks with stable clientIds.\n * @return {Array} Blocks with preserved clientIds where possible.\n */\nexport function preserveClientIds( newBlocks, prevBlocks ) {\n\tif ( ! prevBlocks?.length || ! newBlocks?.length ) {\n\t\treturn newBlocks;\n\t}\n\n\t// Create signatures for LCS matching using block name.\n\tconst newSigs = newBlocks.map( ( block ) => block.name );\n\tconst prevSigs = prevBlocks.map( ( block ) => block.name );\n\n\tconst diffResult = diffArrays( prevSigs, newSigs );\n\n\tlet newIndex = 0;\n\tlet prevIndex = 0;\n\tconst result = [];\n\n\tfor ( const chunk of diffResult ) {\n\t\tif ( chunk.removed ) {\n\t\t\t// Blocks only in prev render - skip them.\n\t\t\tprevIndex += chunk.count;\n\t\t} else if ( chunk.added ) {\n\t\t\t// Blocks only in new render - keep new clientIds.\n\t\t\tfor ( let i = 0; i < chunk.count; i++ ) {\n\t\t\t\tresult.push( newBlocks[ newIndex++ ] );\n\t\t\t}\n\t\t} else {\n\t\t\t// Matched blocks - preserve clientIds from prev render.\n\t\t\tfor ( let i = 0; i < chunk.count; i++ ) {\n\t\t\t\tconst newBlock = newBlocks[ newIndex++ ];\n\t\t\t\tconst prevBlock = prevBlocks[ prevIndex++ ];\n\t\t\t\tresult.push( {\n\t\t\t\t\t...newBlock,\n\t\t\t\t\tclientId: prevBlock.clientId,\n\t\t\t\t\tinnerBlocks: preserveClientIds(\n\t\t\t\t\t\tnewBlock.innerBlocks,\n\t\t\t\t\t\tprevBlock.innerBlocks\n\t\t\t\t\t),\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAA2B;AAapB,SAAS,kBAAmB,WAAW,YAAa;AAC1D,MAAK,CAAE,YAAY,UAAU,CAAE,WAAW,QAAS;AAClD,WAAO;AAAA,EACR;AAGA,QAAM,UAAU,UAAU,IAAK,CAAE,UAAW,MAAM,IAAK;AACvD,QAAM,WAAW,WAAW,IAAK,CAAE,UAAW,MAAM,IAAK;AAEzD,QAAM,iBAAa,yBAAY,UAAU,OAAQ;AAEjD,MAAI,WAAW;AACf,MAAI,YAAY;AAChB,QAAM,SAAS,CAAC;AAEhB,aAAY,SAAS,YAAa;AACjC,QAAK,MAAM,SAAU;AAEpB,mBAAa,MAAM;AAAA,IACpB,WAAY,MAAM,OAAQ;AAEzB,eAAU,IAAI,GAAG,IAAI,MAAM,OAAO,KAAM;AACvC,eAAO,KAAM,UAAW,UAAW,CAAE;AAAA,MACtC;AAAA,IACD,OAAO;AAEN,eAAU,IAAI,GAAG,IAAI,MAAM,OAAO,KAAM;AACvC,cAAM,WAAW,UAAW,UAAW;AACvC,cAAM,YAAY,WAAY,WAAY;AAC1C,eAAO,KAAM;AAAA,UACZ,GAAG;AAAA,UACH,UAAU,UAAU;AAAA,UACpB,aAAa;AAAA,YACZ,SAAS;AAAA,YACT,UAAU;AAAA,UACX;AAAA,QACD,CAAE;AAAA,MACH;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;",
6
+ "names": []
7
+ }
@@ -41,6 +41,7 @@ var import_element = require("@wordpress/element");
41
41
  var import_lock_unlock = require("../../lock-unlock.cjs");
42
42
  var import_store = require("../../store/index.cjs");
43
43
  var import_visual_editor = __toESM(require("../visual-editor/index.cjs"));
44
+ var import_preserve_client_ids = require("./preserve-client-ids.cjs");
44
45
  var import_jsx_runtime = require("react/jsx-runtime");
45
46
  var { ExperimentalBlockEditorProvider } = (0, import_lock_unlock.unlock)(import_block_editor.privateApis);
46
47
  function RevisionsCanvas() {
@@ -57,10 +58,11 @@ function RevisionsCanvas() {
57
58
  },
58
59
  []
59
60
  );
61
+ const previousBlocksRef = (0, import_element.useRef)([]);
60
62
  const blocks = (0, import_element.useMemo)(() => {
61
- const parsedBlocks = (0, import_blocks.parse)(revision?.content?.raw ?? "");
63
+ let parsedBlocks = (0, import_blocks.parse)(revision?.content?.raw ?? "");
62
64
  if (postType === "wp_navigation") {
63
- return [
65
+ parsedBlocks = [
64
66
  (0, import_blocks.createBlock)(
65
67
  "core/navigation",
66
68
  { templateLock: false },
@@ -68,7 +70,12 @@ function RevisionsCanvas() {
68
70
  )
69
71
  ];
70
72
  }
71
- return parsedBlocks;
73
+ const blocksWithStableIds = (0, import_preserve_client_ids.preserveClientIds)(
74
+ parsedBlocks,
75
+ previousBlocksRef.current
76
+ );
77
+ previousBlocksRef.current = blocksWithStableIds;
78
+ return blocksWithStableIds;
72
79
  }, [revision?.content?.raw, postType]);
73
80
  const settings = (0, import_element.useMemo)(
74
81
  () => ({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/components/post-revisions-preview/revisions-canvas.js"],
4
- "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { Spinner } from '@wordpress/components';\nimport {\n\tprivateApis as blockEditorPrivateApis,\n\tstore as blockEditorStore,\n} from '@wordpress/block-editor';\nimport { createBlock, parse } from '@wordpress/blocks';\nimport { useSelect } from '@wordpress/data';\nimport { useMemo } from '@wordpress/element';\n\n/**\n * Internal dependencies\n */\nimport { unlock } from '../../lock-unlock';\nimport { store as editorStore } from '../../store';\nimport VisualEditor from '../visual-editor';\n\nconst { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );\n\n/**\n * Canvas component that renders a post revision in read-only mode.\n *\n * @return {JSX.Element} The revisions canvas component.\n */\nexport default function RevisionsCanvas() {\n\tconst { revision, postType, blockEditorSettings } = useSelect(\n\t\t( select ) => {\n\t\t\tconst { getCurrentRevision, getCurrentPostType } = unlock(\n\t\t\t\tselect( editorStore )\n\t\t\t);\n\t\t\treturn {\n\t\t\t\trevision: getCurrentRevision(),\n\t\t\t\tpostType: getCurrentPostType(),\n\t\t\t\tblockEditorSettings: select( blockEditorStore ).getSettings(),\n\t\t\t};\n\t\t},\n\t\t[]\n\t);\n\n\tconst blocks = useMemo( () => {\n\t\tconst parsedBlocks = parse( revision?.content?.raw ?? '' );\n\t\tif ( postType === 'wp_navigation' ) {\n\t\t\treturn [\n\t\t\t\tcreateBlock(\n\t\t\t\t\t'core/navigation',\n\t\t\t\t\t{ templateLock: false },\n\t\t\t\t\tparsedBlocks\n\t\t\t\t),\n\t\t\t];\n\t\t}\n\t\treturn parsedBlocks;\n\t}, [ revision?.content?.raw, postType ] );\n\n\tconst settings = useMemo(\n\t\t() => ( {\n\t\t\t...blockEditorSettings,\n\t\t\tisPreviewMode: true,\n\t\t} ),\n\t\t[ blockEditorSettings ]\n\t);\n\n\treturn revision ? (\n\t\t<ExperimentalBlockEditorProvider value={ blocks } settings={ settings }>\n\t\t\t<VisualEditor />\n\t\t</ExperimentalBlockEditorProvider>\n\t) : (\n\t\t<div className=\"editor-revisions-canvas__loading\">\n\t\t\t<Spinner />\n\t\t</div>\n\t);\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,wBAAwB;AACxB,0BAGO;AACP,oBAAmC;AACnC,kBAA0B;AAC1B,qBAAwB;AAKxB,yBAAuB;AACvB,mBAAqC;AACrC,2BAAyB;AAgDtB;AA9CH,IAAM,EAAE,gCAAgC,QAAI,2BAAQ,oBAAAA,WAAuB;AAO5D,SAAR,kBAAmC;AACzC,QAAM,EAAE,UAAU,UAAU,oBAAoB,QAAI;AAAA,IACnD,CAAE,WAAY;AACb,YAAM,EAAE,oBAAoB,mBAAmB,QAAI;AAAA,QAClD,OAAQ,aAAAC,KAAY;AAAA,MACrB;AACA,aAAO;AAAA,QACN,UAAU,mBAAmB;AAAA,QAC7B,UAAU,mBAAmB;AAAA,QAC7B,qBAAqB,OAAQ,oBAAAC,KAAiB,EAAE,YAAY;AAAA,MAC7D;AAAA,IACD;AAAA,IACA,CAAC;AAAA,EACF;AAEA,QAAM,aAAS,wBAAS,MAAM;AAC7B,UAAM,mBAAe,qBAAO,UAAU,SAAS,OAAO,EAAG;AACzD,QAAK,aAAa,iBAAkB;AACnC,aAAO;AAAA,YACN;AAAA,UACC;AAAA,UACA,EAAE,cAAc,MAAM;AAAA,UACtB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AACA,WAAO;AAAA,EACR,GAAG,CAAE,UAAU,SAAS,KAAK,QAAS,CAAE;AAExC,QAAM,eAAW;AAAA,IAChB,OAAQ;AAAA,MACP,GAAG;AAAA,MACH,eAAe;AAAA,IAChB;AAAA,IACA,CAAE,mBAAoB;AAAA,EACvB;AAEA,SAAO,WACN,4CAAC,mCAAgC,OAAQ,QAAS,UACjD,sDAAC,qBAAAC,SAAA,EAAa,GACf,IAEA,4CAAC,SAAI,WAAU,oCACd,sDAAC,6BAAQ,GACV;AAEF;",
4
+ "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { Spinner } from '@wordpress/components';\nimport {\n\tprivateApis as blockEditorPrivateApis,\n\tstore as blockEditorStore,\n} from '@wordpress/block-editor';\nimport { createBlock, parse } from '@wordpress/blocks';\nimport { useSelect } from '@wordpress/data';\nimport { useMemo, useRef } from '@wordpress/element';\n\n/**\n * Internal dependencies\n */\nimport { unlock } from '../../lock-unlock';\nimport { store as editorStore } from '../../store';\nimport VisualEditor from '../visual-editor';\nimport { preserveClientIds } from './preserve-client-ids';\n\nconst { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );\n\n/**\n * Canvas component that renders a post revision in read-only mode.\n *\n * @return {JSX.Element} The revisions canvas component.\n */\nexport default function RevisionsCanvas() {\n\tconst { revision, postType, blockEditorSettings } = useSelect(\n\t\t( select ) => {\n\t\t\tconst { getCurrentRevision, getCurrentPostType } = unlock(\n\t\t\t\tselect( editorStore )\n\t\t\t);\n\t\t\treturn {\n\t\t\t\trevision: getCurrentRevision(),\n\t\t\t\tpostType: getCurrentPostType(),\n\t\t\t\tblockEditorSettings: select( blockEditorStore ).getSettings(),\n\t\t\t};\n\t\t},\n\t\t[]\n\t);\n\n\t// Track previously rendered blocks to preserve clientIds between renders.\n\tconst previousBlocksRef = useRef( [] );\n\n\tconst blocks = useMemo( () => {\n\t\tlet parsedBlocks = parse( revision?.content?.raw ?? '' );\n\t\tif ( postType === 'wp_navigation' ) {\n\t\t\tparsedBlocks = [\n\t\t\t\tcreateBlock(\n\t\t\t\t\t'core/navigation',\n\t\t\t\t\t{ templateLock: false },\n\t\t\t\t\tparsedBlocks\n\t\t\t\t),\n\t\t\t];\n\t\t}\n\n\t\t// Preserve clientIds from previous render to prevent React unmount/remount.\n\t\tconst blocksWithStableIds = preserveClientIds(\n\t\t\tparsedBlocks,\n\t\t\tpreviousBlocksRef.current\n\t\t);\n\n\t\t// Update ref for next render.\n\t\tpreviousBlocksRef.current = blocksWithStableIds;\n\n\t\treturn blocksWithStableIds;\n\t}, [ revision?.content?.raw, postType ] );\n\n\tconst settings = useMemo(\n\t\t() => ( {\n\t\t\t...blockEditorSettings,\n\t\t\tisPreviewMode: true,\n\t\t} ),\n\t\t[ blockEditorSettings ]\n\t);\n\n\treturn revision ? (\n\t\t<ExperimentalBlockEditorProvider value={ blocks } settings={ settings }>\n\t\t\t<VisualEditor />\n\t\t</ExperimentalBlockEditorProvider>\n\t) : (\n\t\t<div className=\"editor-revisions-canvas__loading\">\n\t\t\t<Spinner />\n\t\t</div>\n\t);\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,wBAAwB;AACxB,0BAGO;AACP,oBAAmC;AACnC,kBAA0B;AAC1B,qBAAgC;AAKhC,yBAAuB;AACvB,mBAAqC;AACrC,2BAAyB;AACzB,iCAAkC;AA6D/B;AA3DH,IAAM,EAAE,gCAAgC,QAAI,2BAAQ,oBAAAA,WAAuB;AAO5D,SAAR,kBAAmC;AACzC,QAAM,EAAE,UAAU,UAAU,oBAAoB,QAAI;AAAA,IACnD,CAAE,WAAY;AACb,YAAM,EAAE,oBAAoB,mBAAmB,QAAI;AAAA,QAClD,OAAQ,aAAAC,KAAY;AAAA,MACrB;AACA,aAAO;AAAA,QACN,UAAU,mBAAmB;AAAA,QAC7B,UAAU,mBAAmB;AAAA,QAC7B,qBAAqB,OAAQ,oBAAAC,KAAiB,EAAE,YAAY;AAAA,MAC7D;AAAA,IACD;AAAA,IACA,CAAC;AAAA,EACF;AAGA,QAAM,wBAAoB,uBAAQ,CAAC,CAAE;AAErC,QAAM,aAAS,wBAAS,MAAM;AAC7B,QAAI,mBAAe,qBAAO,UAAU,SAAS,OAAO,EAAG;AACvD,QAAK,aAAa,iBAAkB;AACnC,qBAAe;AAAA,YACd;AAAA,UACC;AAAA,UACA,EAAE,cAAc,MAAM;AAAA,UACtB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAGA,UAAM,0BAAsB;AAAA,MAC3B;AAAA,MACA,kBAAkB;AAAA,IACnB;AAGA,sBAAkB,UAAU;AAE5B,WAAO;AAAA,EACR,GAAG,CAAE,UAAU,SAAS,KAAK,QAAS,CAAE;AAExC,QAAM,eAAW;AAAA,IAChB,OAAQ;AAAA,MACP,GAAG;AAAA,MACH,eAAe;AAAA,IAChB;AAAA,IACA,CAAE,mBAAoB;AAAA,EACvB;AAEA,SAAO,WACN,4CAAC,mCAAgC,OAAQ,QAAS,UACjD,sDAAC,qBAAAC,SAAA,EAAa,GACf,IAEA,4CAAC,SAAI,WAAU,oCACd,sDAAC,6BAAQ,GACV;AAEF;",
6
6
  "names": ["blockEditorPrivateApis", "editorStore", "blockEditorStore", "VisualEditor"]
7
7
  }
@@ -0,0 +1,40 @@
1
+ // packages/editor/src/components/post-revisions-preview/preserve-client-ids.js
2
+ import { diffArrays } from "diff/lib/diff/array";
3
+ function preserveClientIds(newBlocks, prevBlocks) {
4
+ if (!prevBlocks?.length || !newBlocks?.length) {
5
+ return newBlocks;
6
+ }
7
+ const newSigs = newBlocks.map((block) => block.name);
8
+ const prevSigs = prevBlocks.map((block) => block.name);
9
+ const diffResult = diffArrays(prevSigs, newSigs);
10
+ let newIndex = 0;
11
+ let prevIndex = 0;
12
+ const result = [];
13
+ for (const chunk of diffResult) {
14
+ if (chunk.removed) {
15
+ prevIndex += chunk.count;
16
+ } else if (chunk.added) {
17
+ for (let i = 0; i < chunk.count; i++) {
18
+ result.push(newBlocks[newIndex++]);
19
+ }
20
+ } else {
21
+ for (let i = 0; i < chunk.count; i++) {
22
+ const newBlock = newBlocks[newIndex++];
23
+ const prevBlock = prevBlocks[prevIndex++];
24
+ result.push({
25
+ ...newBlock,
26
+ clientId: prevBlock.clientId,
27
+ innerBlocks: preserveClientIds(
28
+ newBlock.innerBlocks,
29
+ prevBlock.innerBlocks
30
+ )
31
+ });
32
+ }
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+ export {
38
+ preserveClientIds
39
+ };
40
+ //# sourceMappingURL=preserve-client-ids.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/components/post-revisions-preview/preserve-client-ids.js"],
4
+ "sourcesContent": ["/**\n * External dependencies\n */\nimport { diffArrays } from 'diff/lib/diff/array';\n\n/**\n * Preserves clientIds from previously rendered blocks to prevent flashing.\n * Uses LCS algorithm to match blocks by name.\n *\n * This compares the newly parsed blocks against the last rendered blocks\n * to maintain React key stability.\n *\n * @param {Array} newBlocks Newly parsed blocks with fresh clientIds.\n * @param {Array} prevBlocks Previously rendered blocks with stable clientIds.\n * @return {Array} Blocks with preserved clientIds where possible.\n */\nexport function preserveClientIds( newBlocks, prevBlocks ) {\n\tif ( ! prevBlocks?.length || ! newBlocks?.length ) {\n\t\treturn newBlocks;\n\t}\n\n\t// Create signatures for LCS matching using block name.\n\tconst newSigs = newBlocks.map( ( block ) => block.name );\n\tconst prevSigs = prevBlocks.map( ( block ) => block.name );\n\n\tconst diffResult = diffArrays( prevSigs, newSigs );\n\n\tlet newIndex = 0;\n\tlet prevIndex = 0;\n\tconst result = [];\n\n\tfor ( const chunk of diffResult ) {\n\t\tif ( chunk.removed ) {\n\t\t\t// Blocks only in prev render - skip them.\n\t\t\tprevIndex += chunk.count;\n\t\t} else if ( chunk.added ) {\n\t\t\t// Blocks only in new render - keep new clientIds.\n\t\t\tfor ( let i = 0; i < chunk.count; i++ ) {\n\t\t\t\tresult.push( newBlocks[ newIndex++ ] );\n\t\t\t}\n\t\t} else {\n\t\t\t// Matched blocks - preserve clientIds from prev render.\n\t\t\tfor ( let i = 0; i < chunk.count; i++ ) {\n\t\t\t\tconst newBlock = newBlocks[ newIndex++ ];\n\t\t\t\tconst prevBlock = prevBlocks[ prevIndex++ ];\n\t\t\t\tresult.push( {\n\t\t\t\t\t...newBlock,\n\t\t\t\t\tclientId: prevBlock.clientId,\n\t\t\t\t\tinnerBlocks: preserveClientIds(\n\t\t\t\t\t\tnewBlock.innerBlocks,\n\t\t\t\t\t\tprevBlock.innerBlocks\n\t\t\t\t\t),\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n"],
5
+ "mappings": ";AAGA,SAAS,kBAAkB;AAapB,SAAS,kBAAmB,WAAW,YAAa;AAC1D,MAAK,CAAE,YAAY,UAAU,CAAE,WAAW,QAAS;AAClD,WAAO;AAAA,EACR;AAGA,QAAM,UAAU,UAAU,IAAK,CAAE,UAAW,MAAM,IAAK;AACvD,QAAM,WAAW,WAAW,IAAK,CAAE,UAAW,MAAM,IAAK;AAEzD,QAAM,aAAa,WAAY,UAAU,OAAQ;AAEjD,MAAI,WAAW;AACf,MAAI,YAAY;AAChB,QAAM,SAAS,CAAC;AAEhB,aAAY,SAAS,YAAa;AACjC,QAAK,MAAM,SAAU;AAEpB,mBAAa,MAAM;AAAA,IACpB,WAAY,MAAM,OAAQ;AAEzB,eAAU,IAAI,GAAG,IAAI,MAAM,OAAO,KAAM;AACvC,eAAO,KAAM,UAAW,UAAW,CAAE;AAAA,MACtC;AAAA,IACD,OAAO;AAEN,eAAU,IAAI,GAAG,IAAI,MAAM,OAAO,KAAM;AACvC,cAAM,WAAW,UAAW,UAAW;AACvC,cAAM,YAAY,WAAY,WAAY;AAC1C,eAAO,KAAM;AAAA,UACZ,GAAG;AAAA,UACH,UAAU,UAAU;AAAA,UACpB,aAAa;AAAA,YACZ,SAAS;AAAA,YACT,UAAU;AAAA,UACX;AAAA,QACD,CAAE;AAAA,MACH;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;",
6
+ "names": []
7
+ }
@@ -6,10 +6,11 @@ import {
6
6
  } from "@wordpress/block-editor";
7
7
  import { createBlock, parse } from "@wordpress/blocks";
8
8
  import { useSelect } from "@wordpress/data";
9
- import { useMemo } from "@wordpress/element";
9
+ import { useMemo, useRef } from "@wordpress/element";
10
10
  import { unlock } from "../../lock-unlock.mjs";
11
11
  import { store as editorStore } from "../../store/index.mjs";
12
12
  import VisualEditor from "../visual-editor/index.mjs";
13
+ import { preserveClientIds } from "./preserve-client-ids.mjs";
13
14
  import { jsx } from "react/jsx-runtime";
14
15
  var { ExperimentalBlockEditorProvider } = unlock(blockEditorPrivateApis);
15
16
  function RevisionsCanvas() {
@@ -26,10 +27,11 @@ function RevisionsCanvas() {
26
27
  },
27
28
  []
28
29
  );
30
+ const previousBlocksRef = useRef([]);
29
31
  const blocks = useMemo(() => {
30
- const parsedBlocks = parse(revision?.content?.raw ?? "");
32
+ let parsedBlocks = parse(revision?.content?.raw ?? "");
31
33
  if (postType === "wp_navigation") {
32
- return [
34
+ parsedBlocks = [
33
35
  createBlock(
34
36
  "core/navigation",
35
37
  { templateLock: false },
@@ -37,7 +39,12 @@ function RevisionsCanvas() {
37
39
  )
38
40
  ];
39
41
  }
40
- return parsedBlocks;
42
+ const blocksWithStableIds = preserveClientIds(
43
+ parsedBlocks,
44
+ previousBlocksRef.current
45
+ );
46
+ previousBlocksRef.current = blocksWithStableIds;
47
+ return blocksWithStableIds;
41
48
  }, [revision?.content?.raw, postType]);
42
49
  const settings = useMemo(
43
50
  () => ({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/components/post-revisions-preview/revisions-canvas.js"],
4
- "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { Spinner } from '@wordpress/components';\nimport {\n\tprivateApis as blockEditorPrivateApis,\n\tstore as blockEditorStore,\n} from '@wordpress/block-editor';\nimport { createBlock, parse } from '@wordpress/blocks';\nimport { useSelect } from '@wordpress/data';\nimport { useMemo } from '@wordpress/element';\n\n/**\n * Internal dependencies\n */\nimport { unlock } from '../../lock-unlock';\nimport { store as editorStore } from '../../store';\nimport VisualEditor from '../visual-editor';\n\nconst { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );\n\n/**\n * Canvas component that renders a post revision in read-only mode.\n *\n * @return {JSX.Element} The revisions canvas component.\n */\nexport default function RevisionsCanvas() {\n\tconst { revision, postType, blockEditorSettings } = useSelect(\n\t\t( select ) => {\n\t\t\tconst { getCurrentRevision, getCurrentPostType } = unlock(\n\t\t\t\tselect( editorStore )\n\t\t\t);\n\t\t\treturn {\n\t\t\t\trevision: getCurrentRevision(),\n\t\t\t\tpostType: getCurrentPostType(),\n\t\t\t\tblockEditorSettings: select( blockEditorStore ).getSettings(),\n\t\t\t};\n\t\t},\n\t\t[]\n\t);\n\n\tconst blocks = useMemo( () => {\n\t\tconst parsedBlocks = parse( revision?.content?.raw ?? '' );\n\t\tif ( postType === 'wp_navigation' ) {\n\t\t\treturn [\n\t\t\t\tcreateBlock(\n\t\t\t\t\t'core/navigation',\n\t\t\t\t\t{ templateLock: false },\n\t\t\t\t\tparsedBlocks\n\t\t\t\t),\n\t\t\t];\n\t\t}\n\t\treturn parsedBlocks;\n\t}, [ revision?.content?.raw, postType ] );\n\n\tconst settings = useMemo(\n\t\t() => ( {\n\t\t\t...blockEditorSettings,\n\t\t\tisPreviewMode: true,\n\t\t} ),\n\t\t[ blockEditorSettings ]\n\t);\n\n\treturn revision ? (\n\t\t<ExperimentalBlockEditorProvider value={ blocks } settings={ settings }>\n\t\t\t<VisualEditor />\n\t\t</ExperimentalBlockEditorProvider>\n\t) : (\n\t\t<div className=\"editor-revisions-canvas__loading\">\n\t\t\t<Spinner />\n\t\t</div>\n\t);\n}\n"],
5
- "mappings": ";AAGA,SAAS,eAAe;AACxB;AAAA,EACC,eAAe;AAAA,EACf,SAAS;AAAA,OACH;AACP,SAAS,aAAa,aAAa;AACnC,SAAS,iBAAiB;AAC1B,SAAS,eAAe;AAKxB,SAAS,cAAc;AACvB,SAAS,SAAS,mBAAmB;AACrC,OAAO,kBAAkB;AAgDtB;AA9CH,IAAM,EAAE,gCAAgC,IAAI,OAAQ,sBAAuB;AAO5D,SAAR,kBAAmC;AACzC,QAAM,EAAE,UAAU,UAAU,oBAAoB,IAAI;AAAA,IACnD,CAAE,WAAY;AACb,YAAM,EAAE,oBAAoB,mBAAmB,IAAI;AAAA,QAClD,OAAQ,WAAY;AAAA,MACrB;AACA,aAAO;AAAA,QACN,UAAU,mBAAmB;AAAA,QAC7B,UAAU,mBAAmB;AAAA,QAC7B,qBAAqB,OAAQ,gBAAiB,EAAE,YAAY;AAAA,MAC7D;AAAA,IACD;AAAA,IACA,CAAC;AAAA,EACF;AAEA,QAAM,SAAS,QAAS,MAAM;AAC7B,UAAM,eAAe,MAAO,UAAU,SAAS,OAAO,EAAG;AACzD,QAAK,aAAa,iBAAkB;AACnC,aAAO;AAAA,QACN;AAAA,UACC;AAAA,UACA,EAAE,cAAc,MAAM;AAAA,UACtB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AACA,WAAO;AAAA,EACR,GAAG,CAAE,UAAU,SAAS,KAAK,QAAS,CAAE;AAExC,QAAM,WAAW;AAAA,IAChB,OAAQ;AAAA,MACP,GAAG;AAAA,MACH,eAAe;AAAA,IAChB;AAAA,IACA,CAAE,mBAAoB;AAAA,EACvB;AAEA,SAAO,WACN,oBAAC,mCAAgC,OAAQ,QAAS,UACjD,8BAAC,gBAAa,GACf,IAEA,oBAAC,SAAI,WAAU,oCACd,8BAAC,WAAQ,GACV;AAEF;",
4
+ "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { Spinner } from '@wordpress/components';\nimport {\n\tprivateApis as blockEditorPrivateApis,\n\tstore as blockEditorStore,\n} from '@wordpress/block-editor';\nimport { createBlock, parse } from '@wordpress/blocks';\nimport { useSelect } from '@wordpress/data';\nimport { useMemo, useRef } from '@wordpress/element';\n\n/**\n * Internal dependencies\n */\nimport { unlock } from '../../lock-unlock';\nimport { store as editorStore } from '../../store';\nimport VisualEditor from '../visual-editor';\nimport { preserveClientIds } from './preserve-client-ids';\n\nconst { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );\n\n/**\n * Canvas component that renders a post revision in read-only mode.\n *\n * @return {JSX.Element} The revisions canvas component.\n */\nexport default function RevisionsCanvas() {\n\tconst { revision, postType, blockEditorSettings } = useSelect(\n\t\t( select ) => {\n\t\t\tconst { getCurrentRevision, getCurrentPostType } = unlock(\n\t\t\t\tselect( editorStore )\n\t\t\t);\n\t\t\treturn {\n\t\t\t\trevision: getCurrentRevision(),\n\t\t\t\tpostType: getCurrentPostType(),\n\t\t\t\tblockEditorSettings: select( blockEditorStore ).getSettings(),\n\t\t\t};\n\t\t},\n\t\t[]\n\t);\n\n\t// Track previously rendered blocks to preserve clientIds between renders.\n\tconst previousBlocksRef = useRef( [] );\n\n\tconst blocks = useMemo( () => {\n\t\tlet parsedBlocks = parse( revision?.content?.raw ?? '' );\n\t\tif ( postType === 'wp_navigation' ) {\n\t\t\tparsedBlocks = [\n\t\t\t\tcreateBlock(\n\t\t\t\t\t'core/navigation',\n\t\t\t\t\t{ templateLock: false },\n\t\t\t\t\tparsedBlocks\n\t\t\t\t),\n\t\t\t];\n\t\t}\n\n\t\t// Preserve clientIds from previous render to prevent React unmount/remount.\n\t\tconst blocksWithStableIds = preserveClientIds(\n\t\t\tparsedBlocks,\n\t\t\tpreviousBlocksRef.current\n\t\t);\n\n\t\t// Update ref for next render.\n\t\tpreviousBlocksRef.current = blocksWithStableIds;\n\n\t\treturn blocksWithStableIds;\n\t}, [ revision?.content?.raw, postType ] );\n\n\tconst settings = useMemo(\n\t\t() => ( {\n\t\t\t...blockEditorSettings,\n\t\t\tisPreviewMode: true,\n\t\t} ),\n\t\t[ blockEditorSettings ]\n\t);\n\n\treturn revision ? (\n\t\t<ExperimentalBlockEditorProvider value={ blocks } settings={ settings }>\n\t\t\t<VisualEditor />\n\t\t</ExperimentalBlockEditorProvider>\n\t) : (\n\t\t<div className=\"editor-revisions-canvas__loading\">\n\t\t\t<Spinner />\n\t\t</div>\n\t);\n}\n"],
5
+ "mappings": ";AAGA,SAAS,eAAe;AACxB;AAAA,EACC,eAAe;AAAA,EACf,SAAS;AAAA,OACH;AACP,SAAS,aAAa,aAAa;AACnC,SAAS,iBAAiB;AAC1B,SAAS,SAAS,cAAc;AAKhC,SAAS,cAAc;AACvB,SAAS,SAAS,mBAAmB;AACrC,OAAO,kBAAkB;AACzB,SAAS,yBAAyB;AA6D/B;AA3DH,IAAM,EAAE,gCAAgC,IAAI,OAAQ,sBAAuB;AAO5D,SAAR,kBAAmC;AACzC,QAAM,EAAE,UAAU,UAAU,oBAAoB,IAAI;AAAA,IACnD,CAAE,WAAY;AACb,YAAM,EAAE,oBAAoB,mBAAmB,IAAI;AAAA,QAClD,OAAQ,WAAY;AAAA,MACrB;AACA,aAAO;AAAA,QACN,UAAU,mBAAmB;AAAA,QAC7B,UAAU,mBAAmB;AAAA,QAC7B,qBAAqB,OAAQ,gBAAiB,EAAE,YAAY;AAAA,MAC7D;AAAA,IACD;AAAA,IACA,CAAC;AAAA,EACF;AAGA,QAAM,oBAAoB,OAAQ,CAAC,CAAE;AAErC,QAAM,SAAS,QAAS,MAAM;AAC7B,QAAI,eAAe,MAAO,UAAU,SAAS,OAAO,EAAG;AACvD,QAAK,aAAa,iBAAkB;AACnC,qBAAe;AAAA,QACd;AAAA,UACC;AAAA,UACA,EAAE,cAAc,MAAM;AAAA,UACtB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAGA,UAAM,sBAAsB;AAAA,MAC3B;AAAA,MACA,kBAAkB;AAAA,IACnB;AAGA,sBAAkB,UAAU;AAE5B,WAAO;AAAA,EACR,GAAG,CAAE,UAAU,SAAS,KAAK,QAAS,CAAE;AAExC,QAAM,WAAW;AAAA,IAChB,OAAQ;AAAA,MACP,GAAG;AAAA,MACH,eAAe;AAAA,IAChB;AAAA,IACA,CAAE,mBAAoB;AAAA,EACvB;AAEA,SAAO,WACN,oBAAC,mCAAgC,OAAQ,QAAS,UACjD,8BAAC,gBAAa,GACf,IAEA,oBAAC,SAAI,WAAU,oCACd,8BAAC,WAAQ,GACV;AAEF;",
6
6
  "names": []
7
7
  }
@@ -3947,6 +3947,11 @@ div.dataviews-view-list {
3947
3947
  gap: 16px;
3948
3948
  }
3949
3949
 
3950
+ .dataforms-layouts-details__summary-content {
3951
+ display: inline-flex;
3952
+ min-height: 24px;
3953
+ }
3954
+
3950
3955
  .dataforms-layouts-details__content {
3951
3956
  padding-top: 12px;
3952
3957
  }
@@ -3950,6 +3950,11 @@ div.dataviews-view-list {
3950
3950
  gap: 16px;
3951
3951
  }
3952
3952
 
3953
+ .dataforms-layouts-details__summary-content {
3954
+ display: inline-flex;
3955
+ min-height: 24px;
3956
+ }
3957
+
3953
3958
  .dataforms-layouts-details__content {
3954
3959
  padding-top: 12px;
3955
3960
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Preserves clientIds from previously rendered blocks to prevent flashing.
3
+ * Uses LCS algorithm to match blocks by name.
4
+ *
5
+ * This compares the newly parsed blocks against the last rendered blocks
6
+ * to maintain React key stability.
7
+ *
8
+ * @param {Array} newBlocks Newly parsed blocks with fresh clientIds.
9
+ * @param {Array} prevBlocks Previously rendered blocks with stable clientIds.
10
+ * @return {Array} Blocks with preserved clientIds where possible.
11
+ */
12
+ export function preserveClientIds(newBlocks: any[], prevBlocks: any[]): any[];
13
+ //# sourceMappingURL=preserve-client-ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preserve-client-ids.d.ts","sourceRoot":"","sources":["../../../src/components/post-revisions-preview/preserve-client-ids.js"],"names":[],"mappings":"AAKA;;;;;;;;;;GAUG;AACH,8EA0CC"}
@@ -1 +1 @@
1
- {"version":3,"file":"revisions-canvas.d.ts","sourceRoot":"","sources":["../../../src/components/post-revisions-preview/revisions-canvas.js"],"names":[],"mappings":"AAqBA;;;;GAIG;AACH,2CAFY,GAAG,CAAC,OAAO,CAgDtB"}
1
+ {"version":3,"file":"revisions-canvas.d.ts","sourceRoot":"","sources":["../../../src/components/post-revisions-preview/revisions-canvas.js"],"names":[],"mappings":"AAsBA;;;;GAIG;AACH,2CAFY,GAAG,CAAC,OAAO,CA6DtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/editor",
3
- "version": "14.39.0",
3
+ "version": "14.39.1-next.v.0+5aba098fc",
4
4
  "description": "Enhanced block editor for WordPress posts.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -61,50 +61,51 @@
61
61
  ],
62
62
  "dependencies": {
63
63
  "@floating-ui/react-dom": "2.0.8",
64
- "@wordpress/a11y": "^4.39.0",
65
- "@wordpress/api-fetch": "^7.39.0",
66
- "@wordpress/base-styles": "^6.15.0",
67
- "@wordpress/blob": "^4.39.0",
68
- "@wordpress/block-editor": "^15.12.0",
69
- "@wordpress/blocks": "^15.12.0",
70
- "@wordpress/commands": "^1.39.0",
71
- "@wordpress/components": "^32.1.0",
72
- "@wordpress/compose": "^7.39.0",
73
- "@wordpress/core-data": "^7.39.0",
74
- "@wordpress/data": "^10.39.0",
75
- "@wordpress/dataviews": "^11.3.0",
76
- "@wordpress/date": "^5.39.0",
77
- "@wordpress/deprecated": "^4.39.0",
78
- "@wordpress/dom": "^4.39.0",
79
- "@wordpress/element": "^6.39.0",
80
- "@wordpress/fields": "^0.31.0",
81
- "@wordpress/global-styles-engine": "^1.6.0",
82
- "@wordpress/global-styles-ui": "^1.6.0",
83
- "@wordpress/hooks": "^4.39.0",
84
- "@wordpress/html-entities": "^4.39.0",
85
- "@wordpress/i18n": "^6.12.0",
86
- "@wordpress/icons": "^11.6.0",
87
- "@wordpress/interface": "^9.24.0",
88
- "@wordpress/keyboard-shortcuts": "^5.39.0",
89
- "@wordpress/keycodes": "^4.39.0",
90
- "@wordpress/media-editor": "^0.2.0",
91
- "@wordpress/media-fields": "^0.4.0",
92
- "@wordpress/media-utils": "^5.39.0",
93
- "@wordpress/notices": "^5.39.0",
94
- "@wordpress/patterns": "^2.39.0",
95
- "@wordpress/plugins": "^7.39.0",
96
- "@wordpress/preferences": "^4.39.0",
97
- "@wordpress/private-apis": "^1.39.0",
98
- "@wordpress/reusable-blocks": "^5.39.0",
99
- "@wordpress/rich-text": "^7.39.0",
100
- "@wordpress/server-side-render": "^6.15.0",
101
- "@wordpress/url": "^4.39.0",
102
- "@wordpress/warning": "^3.39.0",
103
- "@wordpress/wordcount": "^4.39.0",
64
+ "@wordpress/a11y": "^4.39.1-next.v.0+5aba098fc",
65
+ "@wordpress/api-fetch": "^7.39.1-next.v.0+5aba098fc",
66
+ "@wordpress/base-styles": "^6.15.1-next.v.0+5aba098fc",
67
+ "@wordpress/blob": "^4.39.1-next.v.0+5aba098fc",
68
+ "@wordpress/block-editor": "^15.12.1-next.v.0+5aba098fc",
69
+ "@wordpress/blocks": "^15.12.1-next.v.0+5aba098fc",
70
+ "@wordpress/commands": "^1.39.1-next.v.0+5aba098fc",
71
+ "@wordpress/components": "^32.1.1-next.v.0+5aba098fc",
72
+ "@wordpress/compose": "^7.39.1-next.v.0+5aba098fc",
73
+ "@wordpress/core-data": "^7.39.1-next.v.0+5aba098fc",
74
+ "@wordpress/data": "^10.39.1-next.v.0+5aba098fc",
75
+ "@wordpress/dataviews": "^11.4.1-next.v.0+5aba098fc",
76
+ "@wordpress/date": "^5.39.1-next.v.0+5aba098fc",
77
+ "@wordpress/deprecated": "^4.39.1-next.v.0+5aba098fc",
78
+ "@wordpress/dom": "^4.39.1-next.v.0+5aba098fc",
79
+ "@wordpress/element": "^6.39.1-next.v.0+5aba098fc",
80
+ "@wordpress/fields": "^0.31.1-next.v.0+5aba098fc",
81
+ "@wordpress/global-styles-engine": "^1.6.1-next.v.0+5aba098fc",
82
+ "@wordpress/global-styles-ui": "^1.6.1-next.v.0+5aba098fc",
83
+ "@wordpress/hooks": "^4.39.1-next.v.0+5aba098fc",
84
+ "@wordpress/html-entities": "^4.39.1-next.v.0+5aba098fc",
85
+ "@wordpress/i18n": "^6.12.1-next.v.0+5aba098fc",
86
+ "@wordpress/icons": "^11.6.1-next.v.0+5aba098fc",
87
+ "@wordpress/interface": "^9.24.1-next.v.0+5aba098fc",
88
+ "@wordpress/keyboard-shortcuts": "^5.39.1-next.v.0+5aba098fc",
89
+ "@wordpress/keycodes": "^4.39.1-next.v.0+5aba098fc",
90
+ "@wordpress/media-editor": "^0.2.1-next.v.0+5aba098fc",
91
+ "@wordpress/media-fields": "^0.4.1-next.v.0+5aba098fc",
92
+ "@wordpress/media-utils": "^5.39.1-next.v.0+5aba098fc",
93
+ "@wordpress/notices": "^5.39.1-next.v.0+5aba098fc",
94
+ "@wordpress/patterns": "^2.39.1-next.v.0+5aba098fc",
95
+ "@wordpress/plugins": "^7.39.1-next.v.0+5aba098fc",
96
+ "@wordpress/preferences": "^4.39.1-next.v.0+5aba098fc",
97
+ "@wordpress/private-apis": "^1.39.1-next.v.0+5aba098fc",
98
+ "@wordpress/reusable-blocks": "^5.39.1-next.v.0+5aba098fc",
99
+ "@wordpress/rich-text": "^7.39.1-next.v.0+5aba098fc",
100
+ "@wordpress/server-side-render": "^6.15.1-next.v.0+5aba098fc",
101
+ "@wordpress/url": "^4.39.1-next.v.0+5aba098fc",
102
+ "@wordpress/warning": "^3.39.1-next.v.0+5aba098fc",
103
+ "@wordpress/wordcount": "^4.39.1-next.v.0+5aba098fc",
104
104
  "change-case": "^4.1.2",
105
105
  "client-zip": "^2.4.5",
106
106
  "clsx": "^2.1.1",
107
107
  "date-fns": "^3.6.0",
108
+ "diff": "^4.0.2",
108
109
  "fast-deep-equal": "^3.1.3",
109
110
  "memize": "^2.1.0",
110
111
  "react-autosize-textarea": "^7.1.0",
@@ -121,5 +122,5 @@
121
122
  "publishConfig": {
122
123
  "access": "public"
123
124
  },
124
- "gitHead": "eee1cfb1472f11183e40fb77465a5f13145df7ad"
125
+ "gitHead": "d730f9e00f5462d1b9d2660632850f5f43ccff44"
125
126
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { diffArrays } from 'diff/lib/diff/array';
5
+
6
+ /**
7
+ * Preserves clientIds from previously rendered blocks to prevent flashing.
8
+ * Uses LCS algorithm to match blocks by name.
9
+ *
10
+ * This compares the newly parsed blocks against the last rendered blocks
11
+ * to maintain React key stability.
12
+ *
13
+ * @param {Array} newBlocks Newly parsed blocks with fresh clientIds.
14
+ * @param {Array} prevBlocks Previously rendered blocks with stable clientIds.
15
+ * @return {Array} Blocks with preserved clientIds where possible.
16
+ */
17
+ export function preserveClientIds( newBlocks, prevBlocks ) {
18
+ if ( ! prevBlocks?.length || ! newBlocks?.length ) {
19
+ return newBlocks;
20
+ }
21
+
22
+ // Create signatures for LCS matching using block name.
23
+ const newSigs = newBlocks.map( ( block ) => block.name );
24
+ const prevSigs = prevBlocks.map( ( block ) => block.name );
25
+
26
+ const diffResult = diffArrays( prevSigs, newSigs );
27
+
28
+ let newIndex = 0;
29
+ let prevIndex = 0;
30
+ const result = [];
31
+
32
+ for ( const chunk of diffResult ) {
33
+ if ( chunk.removed ) {
34
+ // Blocks only in prev render - skip them.
35
+ prevIndex += chunk.count;
36
+ } else if ( chunk.added ) {
37
+ // Blocks only in new render - keep new clientIds.
38
+ for ( let i = 0; i < chunk.count; i++ ) {
39
+ result.push( newBlocks[ newIndex++ ] );
40
+ }
41
+ } else {
42
+ // Matched blocks - preserve clientIds from prev render.
43
+ for ( let i = 0; i < chunk.count; i++ ) {
44
+ const newBlock = newBlocks[ newIndex++ ];
45
+ const prevBlock = prevBlocks[ prevIndex++ ];
46
+ result.push( {
47
+ ...newBlock,
48
+ clientId: prevBlock.clientId,
49
+ innerBlocks: preserveClientIds(
50
+ newBlock.innerBlocks,
51
+ prevBlock.innerBlocks
52
+ ),
53
+ } );
54
+ }
55
+ }
56
+ }
57
+
58
+ return result;
59
+ }
@@ -8,7 +8,7 @@ import {
8
8
  } from '@wordpress/block-editor';
9
9
  import { createBlock, parse } from '@wordpress/blocks';
10
10
  import { useSelect } from '@wordpress/data';
11
- import { useMemo } from '@wordpress/element';
11
+ import { useMemo, useRef } from '@wordpress/element';
12
12
 
13
13
  /**
14
14
  * Internal dependencies
@@ -16,6 +16,7 @@ import { useMemo } from '@wordpress/element';
16
16
  import { unlock } from '../../lock-unlock';
17
17
  import { store as editorStore } from '../../store';
18
18
  import VisualEditor from '../visual-editor';
19
+ import { preserveClientIds } from './preserve-client-ids';
19
20
 
20
21
  const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );
21
22
 
@@ -39,10 +40,13 @@ export default function RevisionsCanvas() {
39
40
  []
40
41
  );
41
42
 
43
+ // Track previously rendered blocks to preserve clientIds between renders.
44
+ const previousBlocksRef = useRef( [] );
45
+
42
46
  const blocks = useMemo( () => {
43
- const parsedBlocks = parse( revision?.content?.raw ?? '' );
47
+ let parsedBlocks = parse( revision?.content?.raw ?? '' );
44
48
  if ( postType === 'wp_navigation' ) {
45
- return [
49
+ parsedBlocks = [
46
50
  createBlock(
47
51
  'core/navigation',
48
52
  { templateLock: false },
@@ -50,7 +54,17 @@ export default function RevisionsCanvas() {
50
54
  ),
51
55
  ];
52
56
  }
53
- return parsedBlocks;
57
+
58
+ // Preserve clientIds from previous render to prevent React unmount/remount.
59
+ const blocksWithStableIds = preserveClientIds(
60
+ parsedBlocks,
61
+ previousBlocksRef.current
62
+ );
63
+
64
+ // Update ref for next render.
65
+ previousBlocksRef.current = blocksWithStableIds;
66
+
67
+ return blocksWithStableIds;
54
68
  }, [ revision?.content?.raw, postType ] );
55
69
 
56
70
  const settings = useMemo(
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { preserveClientIds } from '../preserve-client-ids';
5
+
6
+ describe( 'preserveClientIds', () => {
7
+ it( 'should return newBlocks when prevBlocks is empty', () => {
8
+ const newBlocks = [
9
+ { name: 'core/paragraph', clientId: 'new-1', attributes: {} },
10
+ ];
11
+ expect( preserveClientIds( newBlocks, [] ) ).toBe( newBlocks );
12
+ expect( preserveClientIds( newBlocks, null ) ).toBe( newBlocks );
13
+ expect( preserveClientIds( newBlocks, undefined ) ).toBe( newBlocks );
14
+ } );
15
+
16
+ it( 'should return newBlocks when newBlocks is empty', () => {
17
+ const prevBlocks = [
18
+ { name: 'core/paragraph', clientId: 'prev-1', attributes: {} },
19
+ ];
20
+ expect( preserveClientIds( [], prevBlocks ) ).toEqual( [] );
21
+ expect( preserveClientIds( null, prevBlocks ) ).toBe( null );
22
+ expect( preserveClientIds( undefined, prevBlocks ) ).toBe( undefined );
23
+ } );
24
+
25
+ it( 'should preserve clientIds for identical blocks', () => {
26
+ const prevBlocks = [
27
+ {
28
+ name: 'core/paragraph',
29
+ clientId: 'prev-1',
30
+ attributes: { content: 'Hello' },
31
+ originalContent: 'Hello',
32
+ innerBlocks: [],
33
+ },
34
+ {
35
+ name: 'core/heading',
36
+ clientId: 'prev-2',
37
+ attributes: { level: 2 },
38
+ originalContent: 'Title',
39
+ innerBlocks: [],
40
+ },
41
+ ];
42
+ const newBlocks = [
43
+ {
44
+ name: 'core/paragraph',
45
+ clientId: 'new-1',
46
+ attributes: { content: 'Hello' },
47
+ originalContent: 'Hello',
48
+ innerBlocks: [],
49
+ },
50
+ {
51
+ name: 'core/heading',
52
+ clientId: 'new-2',
53
+ attributes: { level: 2 },
54
+ originalContent: 'Title',
55
+ innerBlocks: [],
56
+ },
57
+ ];
58
+
59
+ const result = preserveClientIds( newBlocks, prevBlocks );
60
+
61
+ expect( result[ 0 ].clientId ).toBe( 'prev-1' );
62
+ expect( result[ 1 ].clientId ).toBe( 'prev-2' );
63
+ } );
64
+
65
+ it( 'should keep new clientIds for added blocks', () => {
66
+ const prevBlocks = [
67
+ {
68
+ name: 'core/paragraph',
69
+ clientId: 'prev-1',
70
+ attributes: { content: 'First' },
71
+ originalContent: 'First',
72
+ innerBlocks: [],
73
+ },
74
+ ];
75
+ const newBlocks = [
76
+ {
77
+ name: 'core/paragraph',
78
+ clientId: 'new-1',
79
+ attributes: { content: 'First' },
80
+ originalContent: 'First',
81
+ innerBlocks: [],
82
+ },
83
+ {
84
+ name: 'core/paragraph',
85
+ clientId: 'new-2',
86
+ attributes: { content: 'Second' },
87
+ originalContent: 'Second',
88
+ innerBlocks: [],
89
+ },
90
+ ];
91
+
92
+ const result = preserveClientIds( newBlocks, prevBlocks );
93
+
94
+ expect( result[ 0 ].clientId ).toBe( 'prev-1' );
95
+ expect( result[ 1 ].clientId ).toBe( 'new-2' );
96
+ } );
97
+
98
+ it( 'should handle removed blocks', () => {
99
+ const prevBlocks = [
100
+ {
101
+ name: 'core/paragraph',
102
+ clientId: 'prev-1',
103
+ attributes: { content: 'First' },
104
+ originalContent: 'First',
105
+ innerBlocks: [],
106
+ },
107
+ {
108
+ name: 'core/paragraph',
109
+ clientId: 'prev-2',
110
+ attributes: { content: 'Second' },
111
+ originalContent: 'Second',
112
+ innerBlocks: [],
113
+ },
114
+ ];
115
+ const newBlocks = [
116
+ {
117
+ name: 'core/paragraph',
118
+ clientId: 'new-2',
119
+ attributes: { content: 'Second' },
120
+ originalContent: 'Second',
121
+ innerBlocks: [],
122
+ },
123
+ ];
124
+
125
+ const result = preserveClientIds( newBlocks, prevBlocks );
126
+
127
+ expect( result ).toHaveLength( 1 );
128
+ // Matches by name only, so first paragraph matches first paragraph.
129
+ expect( result[ 0 ].clientId ).toBe( 'prev-1' );
130
+ } );
131
+
132
+ it( 'should preserve clientIds for inner blocks recursively', () => {
133
+ const prevBlocks = [
134
+ {
135
+ name: 'core/group',
136
+ clientId: 'prev-group',
137
+ attributes: {},
138
+ originalContent: '',
139
+ innerBlocks: [
140
+ {
141
+ name: 'core/paragraph',
142
+ clientId: 'prev-inner-1',
143
+ attributes: { content: 'Inner' },
144
+ originalContent: 'Inner',
145
+ innerBlocks: [],
146
+ },
147
+ ],
148
+ },
149
+ ];
150
+ const newBlocks = [
151
+ {
152
+ name: 'core/group',
153
+ clientId: 'new-group',
154
+ attributes: {},
155
+ originalContent: '',
156
+ innerBlocks: [
157
+ {
158
+ name: 'core/paragraph',
159
+ clientId: 'new-inner-1',
160
+ attributes: { content: 'Inner' },
161
+ originalContent: 'Inner',
162
+ innerBlocks: [],
163
+ },
164
+ ],
165
+ },
166
+ ];
167
+
168
+ const result = preserveClientIds( newBlocks, prevBlocks );
169
+
170
+ expect( result[ 0 ].clientId ).toBe( 'prev-group' );
171
+ expect( result[ 0 ].innerBlocks[ 0 ].clientId ).toBe( 'prev-inner-1' );
172
+ } );
173
+
174
+ it( 'should preserve clientId even when attributes differ (matches by name only)', () => {
175
+ const prevBlocks = [
176
+ {
177
+ name: 'core/paragraph',
178
+ clientId: 'prev-1',
179
+ attributes: { content: 'Old content' },
180
+ originalContent: 'Old content',
181
+ innerBlocks: [],
182
+ },
183
+ ];
184
+ const newBlocks = [
185
+ {
186
+ name: 'core/paragraph',
187
+ clientId: 'new-1',
188
+ attributes: { content: 'New content' },
189
+ originalContent: 'New content',
190
+ innerBlocks: [],
191
+ },
192
+ ];
193
+
194
+ const result = preserveClientIds( newBlocks, prevBlocks );
195
+
196
+ expect( result[ 0 ].clientId ).toBe( 'prev-1' );
197
+ } );
198
+
199
+ it( 'should handle blocks with same name but different content using LCS', () => {
200
+ const prevBlocks = [
201
+ {
202
+ name: 'core/paragraph',
203
+ clientId: 'prev-a',
204
+ attributes: { content: 'A' },
205
+ originalContent: 'A',
206
+ innerBlocks: [],
207
+ },
208
+ {
209
+ name: 'core/paragraph',
210
+ clientId: 'prev-b',
211
+ attributes: { content: 'B' },
212
+ originalContent: 'B',
213
+ innerBlocks: [],
214
+ },
215
+ {
216
+ name: 'core/paragraph',
217
+ clientId: 'prev-c',
218
+ attributes: { content: 'C' },
219
+ originalContent: 'C',
220
+ innerBlocks: [],
221
+ },
222
+ ];
223
+ const newBlocks = [
224
+ {
225
+ name: 'core/paragraph',
226
+ clientId: 'new-a',
227
+ attributes: { content: 'A' },
228
+ originalContent: 'A',
229
+ innerBlocks: [],
230
+ },
231
+ {
232
+ name: 'core/paragraph',
233
+ clientId: 'new-c',
234
+ attributes: { content: 'C' },
235
+ originalContent: 'C',
236
+ innerBlocks: [],
237
+ },
238
+ ];
239
+
240
+ const result = preserveClientIds( newBlocks, prevBlocks );
241
+
242
+ // Matches by name only, so matches in order (first to first, second to second).
243
+ expect( result[ 0 ].clientId ).toBe( 'prev-a' );
244
+ expect( result[ 1 ].clientId ).toBe( 'prev-b' );
245
+ } );
246
+ } );