@wordpress/block-editor 15.14.0 → 15.14.1-next.v.202603102151.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.
Files changed (52) hide show
  1. package/build/components/block-tools/index.cjs +4 -3
  2. package/build/components/block-tools/index.cjs.map +2 -2
  3. package/build/components/iframe/index.cjs +14 -6
  4. package/build/components/iframe/index.cjs.map +2 -2
  5. package/build/components/observe-typing/index.cjs +9 -13
  6. package/build/components/observe-typing/index.cjs.map +2 -2
  7. package/build/components/provider/index.cjs +22 -6
  8. package/build/components/provider/index.cjs.map +2 -2
  9. package/build/hooks/block-fields/index.cjs +52 -31
  10. package/build/hooks/block-fields/index.cjs.map +2 -2
  11. package/build/hooks/cross-origin-isolation.cjs +7 -73
  12. package/build/hooks/cross-origin-isolation.cjs.map +2 -2
  13. package/build/private-apis.cjs +1 -0
  14. package/build/private-apis.cjs.map +2 -2
  15. package/build/store/private-keys.cjs +3 -0
  16. package/build/store/private-keys.cjs.map +2 -2
  17. package/build/store/selectors.cjs +9 -7
  18. package/build/store/selectors.cjs.map +2 -2
  19. package/build-module/components/block-tools/index.mjs +4 -3
  20. package/build-module/components/block-tools/index.mjs.map +2 -2
  21. package/build-module/components/iframe/index.mjs +14 -6
  22. package/build-module/components/iframe/index.mjs.map +2 -2
  23. package/build-module/components/observe-typing/index.mjs +9 -13
  24. package/build-module/components/observe-typing/index.mjs.map +2 -2
  25. package/build-module/components/provider/index.mjs +22 -6
  26. package/build-module/components/provider/index.mjs.map +2 -2
  27. package/build-module/hooks/block-fields/index.mjs +53 -32
  28. package/build-module/hooks/block-fields/index.mjs.map +2 -2
  29. package/build-module/hooks/cross-origin-isolation.mjs +7 -73
  30. package/build-module/hooks/cross-origin-isolation.mjs.map +2 -2
  31. package/build-module/private-apis.mjs +3 -1
  32. package/build-module/private-apis.mjs.map +2 -2
  33. package/build-module/store/private-keys.mjs +2 -0
  34. package/build-module/store/private-keys.mjs.map +2 -2
  35. package/build-module/store/selectors.mjs +9 -7
  36. package/build-module/store/selectors.mjs.map +2 -2
  37. package/build-style/style-rtl.css +8 -5
  38. package/build-style/style.css +8 -5
  39. package/package.json +39 -39
  40. package/src/components/block-tools/index.js +11 -4
  41. package/src/components/iframe/index.js +19 -6
  42. package/src/components/observe-typing/index.js +10 -14
  43. package/src/components/provider/index.js +47 -5
  44. package/src/components/responsive-block-control/style.scss +1 -0
  45. package/src/hooks/block-fields/index.js +44 -19
  46. package/src/hooks/block-fields/styles.scss +7 -9
  47. package/src/hooks/cross-origin-isolation.js +8 -107
  48. package/src/hooks/test/cross-origin-isolation.js +11 -42
  49. package/src/private-apis.js +2 -0
  50. package/src/store/private-keys.js +1 -0
  51. package/src/store/selectors.js +27 -9
  52. package/src/store/test/selectors.js +540 -0
@@ -1,34 +1,12 @@
1
1
  /**
2
- * WordPress dependencies
3
- */
4
- import { addFilter } from '@wordpress/hooks';
5
- import { createHigherOrderComponent } from '@wordpress/compose';
6
-
7
- /**
8
- * Adds crossorigin and credentialless attributes to elements as needed.
2
+ * Adds crossorigin="anonymous" to an element if missing.
9
3
  *
10
4
  * @param {Element} el The element to modify.
11
5
  */
12
- function addCrossOriginAttributes( el ) {
13
- // Add the crossorigin attribute if missing.
6
+ function addCrossOriginAttribute( el ) {
14
7
  if ( ! el.hasAttribute( 'crossorigin' ) ) {
15
8
  el.setAttribute( 'crossorigin', 'anonymous' );
16
9
  }
17
-
18
- // For iframes, add the credentialless attribute.
19
- if ( el.nodeName === 'IFRAME' && ! el.hasAttribute( 'credentialless' ) ) {
20
- // Do not modify the iframed editor canvas.
21
- if ( el.getAttribute( 'src' )?.startsWith( 'blob:' ) ) {
22
- return;
23
- }
24
-
25
- el.setAttribute( 'credentialless', '' );
26
-
27
- // Reload the iframe to ensure the new attribute is taken into account.
28
- const origSrc = el.getAttribute( 'src' ) || '';
29
- el.setAttribute( 'src', '' );
30
- el.setAttribute( 'src', origSrc );
31
- }
32
10
  }
33
11
 
34
12
  // Only add the mutation observer if the site is cross-origin isolated.
@@ -50,58 +28,17 @@ if ( window.crossOriginIsolated ) {
50
28
  }
51
29
 
52
30
  el.querySelectorAll(
53
- 'img,source,script,video,link,iframe'
31
+ 'img,source,script,video,link'
54
32
  ).forEach( ( v ) => {
55
- addCrossOriginAttributes( v );
33
+ addCrossOriginAttribute( v );
56
34
  } );
57
35
 
58
- if ( el.nodeName === 'IFRAME' ) {
59
- const iframeNode = el;
60
-
61
- /*
62
- * Sandboxed iframes should not get modified. For example embedding a tweet served in a sandboxed
63
- * iframe, the tweet itself would not be modified.
64
- */
65
- const isEmbedSandboxIframe =
66
- iframeNode.classList.contains(
67
- 'components-sandbox'
68
- );
69
-
70
- if ( ! isEmbedSandboxIframe ) {
71
- iframeNode.addEventListener( 'load', () => {
72
- try {
73
- if (
74
- iframeNode.contentDocument &&
75
- iframeNode.contentDocument.body
76
- ) {
77
- observer.observe(
78
- iframeNode.contentDocument,
79
- {
80
- childList: true,
81
- attributes: true,
82
- subtree: true,
83
- }
84
- );
85
- }
86
- } catch ( e ) {
87
- // Iframe may be cross-origin or otherwise inaccessible.
88
- // Silently ignore these cases.
89
- }
90
- } );
91
- }
92
- }
93
-
94
36
  if (
95
- [
96
- 'IMG',
97
- 'SOURCE',
98
- 'SCRIPT',
99
- 'VIDEO',
100
- 'LINK',
101
- 'IFRAME',
102
- ].includes( el.nodeName )
37
+ [ 'IMG', 'SOURCE', 'SCRIPT', 'VIDEO', 'LINK' ].includes(
38
+ el.nodeName
39
+ )
103
40
  ) {
104
- addCrossOriginAttributes( el );
41
+ addCrossOriginAttribute( el );
105
42
  }
106
43
  } );
107
44
  } );
@@ -134,39 +71,3 @@ if ( window.crossOriginIsolated ) {
134
71
 
135
72
  startObservingBody();
136
73
  }
137
-
138
- // Only apply the embed preview filter when cross-origin isolated.
139
- if ( window.crossOriginIsolated ) {
140
- const supportsCredentialless =
141
- 'credentialless' in window.HTMLIFrameElement.prototype;
142
-
143
- const disableEmbedPreviews = createHigherOrderComponent(
144
- ( BlockEdit ) =>
145
- function DisableEmbedPreviews( props ) {
146
- if ( 'core/embed' !== props.name ) {
147
- return <BlockEdit { ...props } />;
148
- }
149
-
150
- // List of embeds that do not support a preview is from packages/block-library/src/embed/variations.js.
151
- const previewable =
152
- supportsCredentialless &&
153
- ! [ 'facebook', 'smugmug' ].includes(
154
- props.attributes.providerNameSlug
155
- );
156
-
157
- return (
158
- <BlockEdit
159
- { ...props }
160
- attributes={ { ...props.attributes, previewable } }
161
- />
162
- );
163
- },
164
- 'withDisabledEmbedPreview'
165
- );
166
-
167
- addFilter(
168
- 'editor.BlockEdit',
169
- 'media-experiments/disable-embed-previews',
170
- disableEmbedPreviews
171
- );
172
- }
@@ -152,60 +152,29 @@ describe( 'cross-origin-isolation', () => {
152
152
  expect( observeSpy ).not.toHaveBeenCalled();
153
153
  } );
154
154
 
155
- it( 'should handle iframe contentDocument errors gracefully', () => {
155
+ it( 'should add crossorigin="anonymous" to images', async () => {
156
156
  Object.defineProperty( window, 'crossOriginIsolated', {
157
157
  value: true,
158
158
  writable: true,
159
159
  configurable: true,
160
160
  } );
161
161
 
162
- // Re-import the module
162
+ // Re-import the module to trigger the side effects
163
163
  jest.isolateModules( () => {
164
164
  require( '../cross-origin-isolation' );
165
165
  } );
166
166
 
167
- // Create an iframe that throws when accessing contentDocument
168
- const iframe = document.createElement( 'iframe' );
169
- Object.defineProperty( iframe, 'contentDocument', {
170
- get() {
171
- throw new Error( 'Cross-origin access denied' );
172
- },
173
- } );
174
-
175
- // This should not throw an error
176
- expect( () => {
177
- document.body.appendChild( iframe );
178
- iframe.dispatchEvent( new Event( 'load' ) );
179
- } ).not.toThrow();
180
- } );
181
-
182
- it( 'should register embed preview filter when cross-origin isolated', () => {
183
- Object.defineProperty( window, 'crossOriginIsolated', {
184
- value: true,
185
- writable: true,
186
- configurable: true,
187
- } );
188
-
189
- const hasFilter = jest.spyOn(
190
- require( '@wordpress/hooks' ),
191
- 'hasFilter'
192
- );
167
+ // Create an image and add it to the DOM
168
+ const img = document.createElement( 'img' );
169
+ img.setAttribute( 'src', 'https://example.com/image.jpg' );
170
+ document.body.appendChild( img );
193
171
 
194
- // Re-import the module to register filters
195
- jest.isolateModules( () => {
196
- require( '../cross-origin-isolation' );
197
- } );
172
+ // Wait for MutationObserver callback to fire (async microtask).
173
+ await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
198
174
 
199
- // The module should register a filter when cross-origin isolated
200
- // We can't easily test the filter itself without a full React environment,
201
- // but we can verify the module loads without errors
202
- expect( () => {
203
- require( '@wordpress/hooks' ).hasFilter(
204
- 'editor.BlockEdit',
205
- 'media-experiments/disable-embed-previews'
206
- );
207
- } ).not.toThrow();
175
+ // The image should get the crossorigin attribute
176
+ expect( img ).toHaveAttribute( 'crossorigin', 'anonymous' );
208
177
 
209
- hasFilter.mockRestore();
178
+ document.body.removeChild( img );
210
179
  } );
211
180
  } );
@@ -48,6 +48,7 @@ import {
48
48
  deviceTypeKey,
49
49
  isIsolatedEditorKey,
50
50
  isNavigationOverlayContextKey,
51
+ mediaUploadOnSuccessKey,
51
52
  } from './store/private-keys';
52
53
  import { requiresWrapperOnCopy } from './components/writing-flow/utils';
53
54
  import { PrivateRichText } from './components/rich-text/';
@@ -129,6 +130,7 @@ lock( privateApis, {
129
130
  deviceTypeKey,
130
131
  isIsolatedEditorKey,
131
132
  isNavigationOverlayContextKey,
133
+ mediaUploadOnSuccessKey,
132
134
  useBlockElement,
133
135
  useBlockElementRef,
134
136
  LinkPicker,
@@ -10,3 +10,4 @@ export const deviceTypeKey = Symbol( 'deviceTypeKey' );
10
10
  export const isNavigationOverlayContextKey = Symbol(
11
11
  'isNavigationOverlayContext'
12
12
  );
13
+ export const mediaUploadOnSuccessKey = Symbol( 'mediaUploadOnSuccess' );
@@ -1741,7 +1741,12 @@ const canInsertBlockTypeUnmemoized = (
1741
1741
  }
1742
1742
 
1743
1743
  // In content only mode, check if this container allows insertion.
1744
+ // We need the `isParentSectionBlock` check because section blocks
1745
+ // (synced patterns, contentOnly groups) have a `getBlockEditingMode`
1746
+ // of 'default', not 'contentOnly' — the 'contentOnly' mode is only
1747
+ // set on their *children*.
1744
1748
  if (
1749
+ isWithinSection &&
1745
1750
  ( isParentSectionBlock || blockEditingMode === 'contentOnly' ) &&
1746
1751
  ! isContainerInsertableToInContentOnlyMode(
1747
1752
  state,
@@ -1749,13 +1754,14 @@ const canInsertBlockTypeUnmemoized = (
1749
1754
  rootClientId
1750
1755
  )
1751
1756
  ) {
1757
+ const defaultBlockName = getDefaultBlockName();
1752
1758
  // Allow inserting the default block anywhere that another default block already exists
1753
1759
  // when in contentOnly mode.
1754
- if ( blockName === getDefaultBlockName() ) {
1760
+ if ( blockName === defaultBlockName ) {
1755
1761
  const existingBlocks = getBlockOrder( state, rootClientId );
1756
1762
  const hasDefaultBlock = existingBlocks.some(
1757
1763
  ( clientId ) =>
1758
- getBlockName( state, clientId ) === getDefaultBlockName()
1764
+ getBlockName( state, clientId ) === defaultBlockName
1759
1765
  );
1760
1766
  if ( ! hasDefaultBlock ) {
1761
1767
  return false;
@@ -1938,11 +1944,18 @@ export function canRemoveBlock( state, clientId ) {
1938
1944
 
1939
1945
  const rootBlockEditingMode = getBlockEditingMode( state, rootClientId );
1940
1946
  const blockName = getBlockName( state, clientId );
1941
- // Check if the parent container allows insertion/removal in contentOnly mode.
1947
+ const defaultBlockName = getDefaultBlockName();
1948
+
1949
+ // Check if the parent container allows insertion/removal in contentOnly
1950
+ // mode. We need the `isParentSectionBlock` check because section blocks
1951
+ // (synced patterns, contentOnly groups) have a `getBlockEditingMode` of
1952
+ // 'default', not 'contentOnly' — the 'contentOnly' mode is only set on
1953
+ // their *children*.
1942
1954
  if (
1955
+ isWithinSection &&
1943
1956
  ( isParentSectionBlock ||
1944
- rootBlockEditingMode === 'contentOnly' ||
1945
- blockName === getDefaultBlockName() ) &&
1957
+ blockName === defaultBlockName ||
1958
+ rootBlockEditingMode === 'contentOnly' ) &&
1946
1959
  ! isContainerInsertableToInContentOnlyMode(
1947
1960
  state,
1948
1961
  getBlockName( state, clientId ),
@@ -1951,10 +1964,10 @@ export function canRemoveBlock( state, clientId ) {
1951
1964
  ) {
1952
1965
  // Allow removing the default block when other default blocks exist
1953
1966
  // in contentOnly mode.
1954
- if ( blockName === getDefaultBlockName() ) {
1967
+ if ( blockName === defaultBlockName ) {
1955
1968
  const existingBlocks = getBlockOrder( state, rootClientId );
1956
1969
  const defaultBlocks = existingBlocks.filter(
1957
- ( id ) => getBlockName( state, id ) === getDefaultBlockName()
1970
+ ( id ) => getBlockName( state, id ) === defaultBlockName
1958
1971
  );
1959
1972
  // Allow removal if there are other default blocks besides this one
1960
1973
  if ( defaultBlocks.length > 1 ) {
@@ -2016,11 +2029,16 @@ export function canMoveBlock( state, clientId ) {
2016
2029
  return false;
2017
2030
  }
2018
2031
 
2019
- // If the parent is a section or is `contentOnly`, then check is the inner block
2020
- // should be allowed to move.
2032
+ // If the block is within a section and the parent is either a section
2033
+ // block itself or has contentOnly editing mode, check whether the inner
2034
+ // block should be allowed to move. We need the `isParentSectionBlock`
2035
+ // check because section blocks (synced patterns, contentOnly groups)
2036
+ // have a `getBlockEditingMode` of 'default', not 'contentOnly' — the
2037
+ // 'contentOnly' mode is only set on their *children*.
2021
2038
  const isParentSectionBlock = !! isSectionBlock( state, rootClientId );
2022
2039
  const rootBlockEditingMode = getBlockEditingMode( state, rootClientId );
2023
2040
  if (
2041
+ isBlockWithinSection &&
2024
2042
  ( isParentSectionBlock || rootBlockEditingMode === 'contentOnly' ) &&
2025
2043
  ! isContainerInsertableToInContentOnlyMode(
2026
2044
  state,