@wordpress/block-editor 15.12.0 → 15.12.2-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.
- package/CHANGELOG.md +6 -0
- package/build/components/block-allowed-blocks/modal.cjs +1 -1
- package/build/components/block-allowed-blocks/modal.cjs.map +2 -2
- package/build/components/block-inspector/index.cjs +9 -9
- package/build/components/block-inspector/index.cjs.map +3 -3
- package/build/components/block-removal-warning-modal/index.cjs +30 -5
- package/build/components/block-removal-warning-modal/index.cjs.map +3 -3
- package/build/components/block-visibility/use-block-visibility.cjs +14 -29
- package/build/components/block-visibility/use-block-visibility.cjs.map +2 -2
- package/build/components/global-styles/hooks.cjs +7 -0
- package/build/components/global-styles/hooks.cjs.map +2 -2
- package/build/components/global-styles/typography-panel.cjs +71 -3
- package/build/components/global-styles/typography-panel.cjs.map +3 -3
- package/build/components/grid/grid-visualizer.cjs +49 -13
- package/build/components/grid/grid-visualizer.cjs.map +2 -2
- package/build/components/iframe/index.cjs +3 -1
- package/build/components/iframe/index.cjs.map +2 -2
- package/build/components/iframe/use-scale-canvas.cjs +1 -0
- package/build/components/iframe/use-scale-canvas.cjs.map +2 -2
- package/build/components/inspector-controls/last-item.cjs +41 -0
- package/build/components/inspector-controls/last-item.cjs.map +7 -0
- package/build/components/inspector-controls-tabs/styles-tab.cjs +3 -3
- package/build/components/inspector-controls-tabs/styles-tab.cjs.map +2 -2
- package/build/components/link-control/index.cjs +73 -2
- package/build/components/link-control/index.cjs.map +3 -3
- package/build/components/link-control/is-url-like.cjs +15 -3
- package/build/components/link-control/is-url-like.cjs.map +2 -2
- package/build/components/link-control/search-input.cjs +4 -1
- package/build/components/link-control/search-input.cjs.map +2 -2
- package/build/components/link-control/use-search-handler.cjs +1 -1
- package/build/components/link-control/use-search-handler.cjs.map +2 -2
- package/build/components/provider/use-block-sync.cjs +60 -8
- package/build/components/provider/use-block-sync.cjs.map +2 -2
- package/build/components/text-indent-control/index.cjs +121 -0
- package/build/components/text-indent-control/index.cjs.map +7 -0
- package/build/components/url-input/index.cjs +22 -2
- package/build/components/url-input/index.cjs.map +3 -3
- package/build/components/url-popover/image-url-input-ui.cjs +1 -1
- package/build/components/url-popover/image-url-input-ui.cjs.map +2 -2
- package/build/components/writing-flow/use-arrow-nav.cjs +0 -3
- package/build/components/writing-flow/use-arrow-nav.cjs.map +2 -2
- package/build/hooks/anchor.cjs +1 -1
- package/build/hooks/anchor.cjs.map +1 -1
- package/build/hooks/aria-label.cjs +2 -1
- package/build/hooks/aria-label.cjs.map +2 -2
- package/build/hooks/grid-visualizer.cjs +59 -6
- package/build/hooks/grid-visualizer.cjs.map +3 -3
- package/build/hooks/layout-child.cjs +47 -6
- package/build/hooks/layout-child.cjs.map +3 -3
- package/build/hooks/typography.cjs +2 -0
- package/build/hooks/typography.cjs.map +2 -2
- package/build/hooks/utils.cjs +4 -0
- package/build/hooks/utils.cjs.map +2 -2
- package/build/private-apis.cjs +2 -0
- package/build/private-apis.cjs.map +3 -3
- package/build/store/actions.cjs +2 -2
- package/build/store/actions.cjs.map +2 -2
- package/build-module/components/block-allowed-blocks/modal.mjs +2 -2
- package/build-module/components/block-allowed-blocks/modal.mjs.map +2 -2
- package/build-module/components/block-inspector/index.mjs +10 -9
- package/build-module/components/block-inspector/index.mjs.map +2 -2
- package/build-module/components/block-removal-warning-modal/index.mjs +34 -7
- package/build-module/components/block-removal-warning-modal/index.mjs.map +2 -2
- package/build-module/components/block-visibility/use-block-visibility.mjs +14 -29
- package/build-module/components/block-visibility/use-block-visibility.mjs.map +2 -2
- package/build-module/components/global-styles/hooks.mjs +7 -0
- package/build-module/components/global-styles/hooks.mjs.map +2 -2
- package/build-module/components/global-styles/typography-panel.mjs +73 -4
- package/build-module/components/global-styles/typography-panel.mjs.map +2 -2
- package/build-module/components/grid/grid-visualizer.mjs +50 -14
- package/build-module/components/grid/grid-visualizer.mjs.map +2 -2
- package/build-module/components/iframe/index.mjs +9 -2
- package/build-module/components/iframe/index.mjs.map +2 -2
- package/build-module/components/iframe/use-scale-canvas.mjs +1 -0
- package/build-module/components/iframe/use-scale-canvas.mjs.map +2 -2
- package/build-module/components/inspector-controls/last-item.mjs +23 -0
- package/build-module/components/inspector-controls/last-item.mjs.map +7 -0
- package/build-module/components/inspector-controls-tabs/styles-tab.mjs +3 -3
- package/build-module/components/inspector-controls-tabs/styles-tab.mjs.map +2 -2
- package/build-module/components/link-control/index.mjs +74 -3
- package/build-module/components/link-control/index.mjs.map +2 -2
- package/build-module/components/link-control/is-url-like.mjs +10 -3
- package/build-module/components/link-control/is-url-like.mjs.map +2 -2
- package/build-module/components/link-control/search-input.mjs +4 -1
- package/build-module/components/link-control/search-input.mjs.map +2 -2
- package/build-module/components/link-control/use-search-handler.mjs +2 -2
- package/build-module/components/link-control/use-search-handler.mjs.map +2 -2
- package/build-module/components/provider/use-block-sync.mjs +60 -8
- package/build-module/components/provider/use-block-sync.mjs.map +2 -2
- package/build-module/components/text-indent-control/index.mjs +110 -0
- package/build-module/components/text-indent-control/index.mjs.map +7 -0
- package/build-module/components/url-input/index.mjs +24 -4
- package/build-module/components/url-input/index.mjs.map +2 -2
- package/build-module/components/url-popover/image-url-input-ui.mjs +2 -2
- package/build-module/components/url-popover/image-url-input-ui.mjs.map +2 -2
- package/build-module/components/writing-flow/use-arrow-nav.mjs +0 -3
- package/build-module/components/writing-flow/use-arrow-nav.mjs.map +2 -2
- package/build-module/hooks/anchor.mjs +1 -1
- package/build-module/hooks/anchor.mjs.map +1 -1
- package/build-module/hooks/aria-label.mjs +2 -1
- package/build-module/hooks/aria-label.mjs.map +2 -2
- package/build-module/hooks/grid-visualizer.mjs +37 -6
- package/build-module/hooks/grid-visualizer.mjs.map +2 -2
- package/build-module/hooks/layout-child.mjs +37 -6
- package/build-module/hooks/layout-child.mjs.map +2 -2
- package/build-module/hooks/typography.mjs +2 -0
- package/build-module/hooks/typography.mjs.map +2 -2
- package/build-module/hooks/utils.mjs +4 -0
- package/build-module/hooks/utils.mjs.map +2 -2
- package/build-module/private-apis.mjs +2 -0
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/store/actions.mjs +2 -2
- package/build-module/store/actions.mjs.map +2 -2
- package/package.json +39 -39
- package/src/components/block-allowed-blocks/modal.js +2 -2
- package/src/components/block-inspector/index.js +19 -17
- package/src/components/block-removal-warning-modal/index.js +55 -19
- package/src/components/block-switcher/block-transformations-menu.native.js +1 -0
- package/src/components/block-toolbar/test/__snapshots__/block-toolbar-menu.native.js.snap +4 -6
- package/src/components/block-toolbar/test/block-toolbar-menu.native.js +2 -2
- package/src/components/block-visibility/use-block-visibility.js +17 -32
- package/src/components/global-styles/hooks.js +10 -0
- package/src/components/global-styles/typography-panel.js +78 -1
- package/src/components/grid/grid-visualizer.js +58 -12
- package/src/components/iframe/index.js +12 -2
- package/src/components/iframe/use-scale-canvas.js +1 -0
- package/src/components/inserter/menu.native.js +1 -0
- package/src/components/inspector-controls/last-item.js +29 -0
- package/src/components/inspector-controls-tabs/styles-tab.js +3 -3
- package/src/components/link-control/index.js +160 -3
- package/src/components/link-control/is-url-like.js +43 -8
- package/src/components/link-control/search-input.js +7 -0
- package/src/components/link-control/test/index.js +260 -0
- package/src/components/link-control/test/is-url-like.js +49 -1
- package/src/components/link-control/use-search-handler.js +2 -2
- package/src/components/provider/test/use-block-sync.js +105 -0
- package/src/components/provider/use-block-sync.js +118 -9
- package/src/components/text-indent-control/index.js +138 -0
- package/src/components/url-input/index.js +21 -2
- package/src/components/url-popover/image-url-input-ui.js +2 -2
- package/src/components/writing-flow/use-arrow-nav.js +0 -4
- package/src/hooks/anchor.js +1 -1
- package/src/hooks/aria-label.js +9 -1
- package/src/hooks/grid-visualizer.js +63 -24
- package/src/hooks/layout-child.js +45 -3
- package/src/hooks/typography.js +2 -0
- package/src/hooks/utils.js +4 -0
- package/src/private-apis.js +2 -0
- package/src/store/actions.js +8 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal dependencies
|
|
3
3
|
*/
|
|
4
|
-
import isURLLike from '../is-url-like';
|
|
4
|
+
import isURLLike, { isHashLink, isRelativePath } from '../is-url-like';
|
|
5
5
|
|
|
6
6
|
describe( 'isURLLike', () => {
|
|
7
7
|
it.each( [ 'https://wordpress.org', 'http://wordpress.org' ] )(
|
|
@@ -41,6 +41,13 @@ describe( 'isURLLike', () => {
|
|
|
41
41
|
expect( isURLLike( '#someinternallink' ) ).toBe( true );
|
|
42
42
|
} );
|
|
43
43
|
|
|
44
|
+
it.each( [ '/handbook', '/path/to/page', './relative', '../parent' ] )(
|
|
45
|
+
'returns true for relative paths (e.g. "%s")',
|
|
46
|
+
( testString ) => {
|
|
47
|
+
expect( isURLLike( testString ) ).toBe( true );
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
44
51
|
// use .each to test multiple cases
|
|
45
52
|
it.each( [
|
|
46
53
|
[ true, 'http://example.com' ],
|
|
@@ -64,3 +71,44 @@ describe( 'isURLLike', () => {
|
|
|
64
71
|
}
|
|
65
72
|
);
|
|
66
73
|
} );
|
|
74
|
+
|
|
75
|
+
describe( 'isHashLink', () => {
|
|
76
|
+
it( 'returns true for valid hash links', () => {
|
|
77
|
+
expect( isHashLink( '#section' ) ).toBe( true );
|
|
78
|
+
expect( isHashLink( '#top' ) ).toBe( true );
|
|
79
|
+
} );
|
|
80
|
+
|
|
81
|
+
it( 'returns false for invalid links that start with #', () => {
|
|
82
|
+
expect( isHashLink( '# test with space' ) ).toBe( false );
|
|
83
|
+
expect( isHashLink( '#test#multiple' ) ).toBe( false );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
it( 'returns false for non-hash links', () => {
|
|
87
|
+
expect( isHashLink( '/page' ) ).toBe( false );
|
|
88
|
+
expect( isHashLink( 'https://example.com' ) ).toBe( false );
|
|
89
|
+
expect( isHashLink( 'not a link' ) ).toBe( false );
|
|
90
|
+
} );
|
|
91
|
+
} );
|
|
92
|
+
|
|
93
|
+
describe( 'isRelativePath', () => {
|
|
94
|
+
it( 'returns true for absolute paths starting with /', () => {
|
|
95
|
+
expect( isRelativePath( '/handbook' ) ).toBe( true );
|
|
96
|
+
expect( isRelativePath( '/path/to/page' ) ).toBe( true );
|
|
97
|
+
} );
|
|
98
|
+
|
|
99
|
+
it( 'returns true for relative paths starting with ./', () => {
|
|
100
|
+
expect( isRelativePath( './page' ) ).toBe( true );
|
|
101
|
+
expect( isRelativePath( './nested/page' ) ).toBe( true );
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
it( 'returns true for parent relative paths starting with ../', () => {
|
|
105
|
+
expect( isRelativePath( '../page' ) ).toBe( true );
|
|
106
|
+
expect( isRelativePath( '../parent/page' ) ).toBe( true );
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
it( 'returns false for non-relative paths', () => {
|
|
110
|
+
expect( isRelativePath( 'https://example.com' ) ).toBe( false );
|
|
111
|
+
expect( isRelativePath( '#section' ) ).toBe( false );
|
|
112
|
+
expect( isRelativePath( 'www.example.com' ) ).toBe( false );
|
|
113
|
+
} );
|
|
114
|
+
} );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { getProtocol,
|
|
4
|
+
import { getProtocol, prependHTTPS } from '@wordpress/url';
|
|
5
5
|
import { useCallback } from '@wordpress/element';
|
|
6
6
|
import { useSelect } from '@wordpress/data';
|
|
7
7
|
|
|
@@ -41,7 +41,7 @@ export const handleDirectEntry = ( val ) => {
|
|
|
41
41
|
{
|
|
42
42
|
id: val,
|
|
43
43
|
title: val,
|
|
44
|
-
url: type === 'URL' ?
|
|
44
|
+
url: type === 'URL' ? prependHTTPS( val ) : val,
|
|
45
45
|
type,
|
|
46
46
|
},
|
|
47
47
|
] );
|
|
@@ -566,4 +566,109 @@ describe( 'useBlockSync hook', () => {
|
|
|
566
566
|
} )
|
|
567
567
|
);
|
|
568
568
|
} );
|
|
569
|
+
|
|
570
|
+
it( 'preserves external client IDs in onChange callback for inner block controllers', async () => {
|
|
571
|
+
const originalClientId = 'original-external-id';
|
|
572
|
+
const innerBlockClientId = 'inner-external-id';
|
|
573
|
+
const onChange = jest.fn();
|
|
574
|
+
const onInput = jest.fn();
|
|
575
|
+
const replaceInnerBlocks = jest.spyOn(
|
|
576
|
+
blockEditorActions,
|
|
577
|
+
'replaceInnerBlocks'
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
// Blocks with specific external client IDs
|
|
581
|
+
const controlledBlocks = [
|
|
582
|
+
{
|
|
583
|
+
name: 'test/test-block',
|
|
584
|
+
clientId: originalClientId,
|
|
585
|
+
innerBlocks: [
|
|
586
|
+
{
|
|
587
|
+
name: 'test/test-block',
|
|
588
|
+
clientId: innerBlockClientId,
|
|
589
|
+
innerBlocks: [],
|
|
590
|
+
attributes: { foo: 10 },
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
attributes: { foo: 1 },
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
|
|
597
|
+
let registry;
|
|
598
|
+
const setRegistry = ( reg ) => {
|
|
599
|
+
registry = reg;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
render(
|
|
603
|
+
<TestWrapper
|
|
604
|
+
setRegistry={ setRegistry }
|
|
605
|
+
value={ controlledBlocks }
|
|
606
|
+
onChange={ onChange }
|
|
607
|
+
onInput={ onInput }
|
|
608
|
+
/>
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// For the root case (no clientId), blocks are not cloned
|
|
612
|
+
// So the external IDs should be preserved as-is
|
|
613
|
+
expect( replaceInnerBlocks ).not.toHaveBeenCalled();
|
|
614
|
+
|
|
615
|
+
onChange.mockClear();
|
|
616
|
+
onInput.mockClear();
|
|
617
|
+
|
|
618
|
+
registry
|
|
619
|
+
.dispatch( blockEditorStore )
|
|
620
|
+
.updateBlockAttributes( originalClientId, { foo: 2 } );
|
|
621
|
+
|
|
622
|
+
// The onChange callback should receive blocks with the same external IDs
|
|
623
|
+
expect( onChange ).toHaveBeenCalledWith(
|
|
624
|
+
expect.arrayContaining( [
|
|
625
|
+
expect.objectContaining( {
|
|
626
|
+
clientId: originalClientId,
|
|
627
|
+
attributes: { foo: 2 },
|
|
628
|
+
innerBlocks: expect.arrayContaining( [
|
|
629
|
+
expect.objectContaining( {
|
|
630
|
+
clientId: innerBlockClientId,
|
|
631
|
+
} ),
|
|
632
|
+
] ),
|
|
633
|
+
} ),
|
|
634
|
+
] ),
|
|
635
|
+
expect.objectContaining( {
|
|
636
|
+
selection: expect.any( Object ),
|
|
637
|
+
} )
|
|
638
|
+
);
|
|
639
|
+
} );
|
|
640
|
+
|
|
641
|
+
it( 'clones blocks with new internal IDs for inner block controllers', async () => {
|
|
642
|
+
const originalClientId = 'original-external-id';
|
|
643
|
+
const replaceInnerBlocks = jest.spyOn(
|
|
644
|
+
blockEditorActions,
|
|
645
|
+
'replaceInnerBlocks'
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
// Blocks with specific external client IDs
|
|
649
|
+
const controlledBlocks = [
|
|
650
|
+
{
|
|
651
|
+
name: 'test/test-block',
|
|
652
|
+
clientId: originalClientId,
|
|
653
|
+
innerBlocks: [],
|
|
654
|
+
attributes: { foo: 1 },
|
|
655
|
+
},
|
|
656
|
+
];
|
|
657
|
+
|
|
658
|
+
render(
|
|
659
|
+
<TestWrapper
|
|
660
|
+
clientId="test-controller"
|
|
661
|
+
value={ controlledBlocks }
|
|
662
|
+
onChange={ jest.fn() }
|
|
663
|
+
onInput={ jest.fn() }
|
|
664
|
+
/>
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
// replaceInnerBlocks should have been called with cloned blocks
|
|
668
|
+
expect( replaceInnerBlocks ).toHaveBeenCalled();
|
|
669
|
+
const replacedBlocks = replaceInnerBlocks.mock.calls[ 0 ][ 1 ];
|
|
670
|
+
|
|
671
|
+
// The internal IDs should be different from the external IDs (due to cloning)
|
|
672
|
+
expect( replacedBlocks[ 0 ].clientId ).not.toBe( originalClientId );
|
|
673
|
+
} );
|
|
569
674
|
} );
|
|
@@ -12,6 +12,87 @@ import { store as blockEditorStore } from '../../store';
|
|
|
12
12
|
|
|
13
13
|
const noop = () => {};
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Clones a block and its inner blocks, building a bidirectional mapping
|
|
17
|
+
* between external (original) and internal (cloned) client IDs.
|
|
18
|
+
*
|
|
19
|
+
* This allows the block editor to use unique internal IDs while preserving
|
|
20
|
+
* stable external IDs for features like real-time collaboration.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} block The block to clone.
|
|
23
|
+
* @param {Object} mapping The mapping object with externalToInternal and internalToExternal Maps.
|
|
24
|
+
* @return {Object} The cloned block with a new clientId.
|
|
25
|
+
*/
|
|
26
|
+
function cloneBlockWithMapping( block, mapping ) {
|
|
27
|
+
const clonedBlock = cloneBlock( block );
|
|
28
|
+
|
|
29
|
+
// Build bidirectional mapping
|
|
30
|
+
mapping.externalToInternal.set( block.clientId, clonedBlock.clientId );
|
|
31
|
+
mapping.internalToExternal.set( clonedBlock.clientId, block.clientId );
|
|
32
|
+
|
|
33
|
+
// Recursively map inner blocks
|
|
34
|
+
if ( block.innerBlocks?.length ) {
|
|
35
|
+
clonedBlock.innerBlocks = block.innerBlocks.map( ( innerBlock ) => {
|
|
36
|
+
const clonedInner = cloneBlockWithMapping( innerBlock, mapping );
|
|
37
|
+
// The clonedBlock already has cloned inner blocks from cloneBlock(),
|
|
38
|
+
// but we need to use our mapped versions to maintain the mapping.
|
|
39
|
+
return clonedInner;
|
|
40
|
+
} );
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return clonedBlock;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Restores external (original) client IDs on blocks before passing them
|
|
48
|
+
* to onChange/onInput callbacks.
|
|
49
|
+
*
|
|
50
|
+
* @param {Object[]} blocks The blocks with internal client IDs.
|
|
51
|
+
* @param {Object} mapping The mapping object with internalToExternal Map.
|
|
52
|
+
* @return {Object[]} Blocks with external client IDs restored.
|
|
53
|
+
*/
|
|
54
|
+
function restoreExternalIds( blocks, mapping ) {
|
|
55
|
+
return blocks.map( ( block ) => {
|
|
56
|
+
const externalId = mapping.internalToExternal.get( block.clientId );
|
|
57
|
+
return {
|
|
58
|
+
...block,
|
|
59
|
+
// Use external ID if available, otherwise keep internal ID (for new blocks)
|
|
60
|
+
clientId: externalId ?? block.clientId,
|
|
61
|
+
innerBlocks: restoreExternalIds( block.innerBlocks, mapping ),
|
|
62
|
+
};
|
|
63
|
+
} );
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Restores external client IDs in selection state.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} selection The selection state with internal client IDs.
|
|
70
|
+
* @param {Object} mapping The mapping object with internalToExternal Map.
|
|
71
|
+
* @return {Object} Selection state with external client IDs.
|
|
72
|
+
*/
|
|
73
|
+
function restoreSelectionIds( selection, mapping ) {
|
|
74
|
+
const { selectionStart, selectionEnd, initialPosition } = selection;
|
|
75
|
+
|
|
76
|
+
const restoreClientId = ( selectionState ) => {
|
|
77
|
+
if ( ! selectionState?.clientId ) {
|
|
78
|
+
return selectionState;
|
|
79
|
+
}
|
|
80
|
+
const externalId = mapping.internalToExternal.get(
|
|
81
|
+
selectionState.clientId
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
...selectionState,
|
|
85
|
+
clientId: externalId ?? selectionState.clientId,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
selectionStart: restoreClientId( selectionStart ),
|
|
91
|
+
selectionEnd: restoreClientId( selectionEnd ),
|
|
92
|
+
initialPosition,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
15
96
|
/**
|
|
16
97
|
* A function to call when the block value has been updated in the block-editor
|
|
17
98
|
* store.
|
|
@@ -94,6 +175,13 @@ export default function useBlockSync( {
|
|
|
94
175
|
const pendingChangesRef = useRef( { incoming: null, outgoing: [] } );
|
|
95
176
|
const subscribedRef = useRef( false );
|
|
96
177
|
|
|
178
|
+
// Mapping between external (original) and internal (cloned) client IDs.
|
|
179
|
+
// This allows stable external IDs while using unique internal IDs.
|
|
180
|
+
const idMappingRef = useRef( {
|
|
181
|
+
externalToInternal: new Map(),
|
|
182
|
+
internalToExternal: new Map(),
|
|
183
|
+
} );
|
|
184
|
+
|
|
97
185
|
const setControlledBlocks = () => {
|
|
98
186
|
if ( ! controlledBlocks ) {
|
|
99
187
|
return;
|
|
@@ -110,8 +198,14 @@ export default function useBlockSync( {
|
|
|
110
198
|
// before the actual blocks get set properly in state.
|
|
111
199
|
registry.batch( () => {
|
|
112
200
|
setHasControlledInnerBlocks( clientId, true );
|
|
201
|
+
|
|
202
|
+
// Clear previous mappings and build new ones during cloning.
|
|
203
|
+
// This ensures the mapping stays in sync with the current blocks.
|
|
204
|
+
idMappingRef.current.externalToInternal.clear();
|
|
205
|
+
idMappingRef.current.internalToExternal.clear();
|
|
206
|
+
|
|
113
207
|
const storeBlocks = controlledBlocks.map( ( block ) =>
|
|
114
|
-
|
|
208
|
+
cloneBlockWithMapping( block, idMappingRef.current )
|
|
115
209
|
);
|
|
116
210
|
if ( subscribedRef.current ) {
|
|
117
211
|
pendingChangesRef.current.incoming = storeBlocks;
|
|
@@ -262,24 +356,39 @@ export default function useBlockSync( {
|
|
|
262
356
|
|
|
263
357
|
if ( areBlocksDifferent || didPersistenceChange ) {
|
|
264
358
|
isPersistent = newIsPersistent;
|
|
359
|
+
|
|
360
|
+
// For inner block controllers (clientId is set), restore external IDs
|
|
361
|
+
// before passing blocks to the parent. This maintains stable external
|
|
362
|
+
// IDs for features like real-time collaboration while using unique
|
|
363
|
+
// internal IDs in the block-editor store.
|
|
364
|
+
const blocksForParent = clientId
|
|
365
|
+
? restoreExternalIds( blocks, idMappingRef.current )
|
|
366
|
+
: blocks;
|
|
367
|
+
|
|
368
|
+
const selection = {
|
|
369
|
+
selectionStart: getSelectionStart(),
|
|
370
|
+
selectionEnd: getSelectionEnd(),
|
|
371
|
+
initialPosition: getSelectedBlocksInitialCaretPosition(),
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Also restore external IDs in selection state for inner block controllers.
|
|
375
|
+
const selectionForParent = clientId
|
|
376
|
+
? restoreSelectionIds( selection, idMappingRef.current )
|
|
377
|
+
: selection;
|
|
378
|
+
|
|
265
379
|
// We know that onChange/onInput will update controlledBlocks.
|
|
266
380
|
// We need to be aware that it was caused by an outgoing change
|
|
267
381
|
// so that we do not treat it as an incoming change later on,
|
|
268
382
|
// which would cause a block reset.
|
|
269
|
-
pendingChangesRef.current.outgoing.push(
|
|
383
|
+
pendingChangesRef.current.outgoing.push( blocksForParent );
|
|
270
384
|
|
|
271
385
|
// Inform the controlling entity that changes have been made to
|
|
272
386
|
// the block-editor store they should be aware about.
|
|
273
387
|
const updateParent = isPersistent
|
|
274
388
|
? onChangeRef.current
|
|
275
389
|
: onInputRef.current;
|
|
276
|
-
updateParent(
|
|
277
|
-
selection:
|
|
278
|
-
selectionStart: getSelectionStart(),
|
|
279
|
-
selectionEnd: getSelectionEnd(),
|
|
280
|
-
initialPosition:
|
|
281
|
-
getSelectedBlocksInitialCaretPosition(),
|
|
282
|
-
},
|
|
390
|
+
updateParent( blocksForParent, {
|
|
391
|
+
selection: selectionForParent,
|
|
283
392
|
} );
|
|
284
393
|
}
|
|
285
394
|
previousAreBlocksDifferent = areBlocksDifferent;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
__experimentalUnitControl as UnitControl,
|
|
6
|
+
__experimentalUseCustomUnits as useCustomUnits,
|
|
7
|
+
__experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue,
|
|
8
|
+
__experimentalView as View,
|
|
9
|
+
RangeControl,
|
|
10
|
+
__experimentalSpacer as Spacer,
|
|
11
|
+
Flex,
|
|
12
|
+
FlexItem,
|
|
13
|
+
BaseControl,
|
|
14
|
+
} from '@wordpress/components';
|
|
15
|
+
import { __ } from '@wordpress/i18n';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Internal dependencies
|
|
19
|
+
*/
|
|
20
|
+
import { useSettings } from '../../components/use-settings';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Control for line text indent.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} props Component props.
|
|
26
|
+
* @param {boolean} props.__next40pxDefaultSize Start opting into the larger default height that will become the default size in a future version.
|
|
27
|
+
* @param {string} props.value Currently selected text indent.
|
|
28
|
+
* @param {Function} props.onChange Handles change in text indent selection.
|
|
29
|
+
* @param {string|number|undefined} props.__unstableInputWidth Input width to pass through to inner UnitControl. Should be a valid CSS value.
|
|
30
|
+
* @param {boolean} props.withSlider Whether to show the slider control.
|
|
31
|
+
* @param {boolean} props.hasBottomMargin Whether to add bottom margin below the control.
|
|
32
|
+
* @param {string} props.help Help text to display below the control.
|
|
33
|
+
*
|
|
34
|
+
* @return {Element} Text indent control.
|
|
35
|
+
*/
|
|
36
|
+
export default function TextIndentControl( {
|
|
37
|
+
__next40pxDefaultSize = false,
|
|
38
|
+
value,
|
|
39
|
+
onChange,
|
|
40
|
+
__unstableInputWidth = '60px',
|
|
41
|
+
withSlider = false,
|
|
42
|
+
hasBottomMargin = false,
|
|
43
|
+
help,
|
|
44
|
+
...otherProps
|
|
45
|
+
} ) {
|
|
46
|
+
const [ availableUnits ] = useSettings( 'spacing.units' );
|
|
47
|
+
const units = useCustomUnits( {
|
|
48
|
+
availableUnits: availableUnits || [
|
|
49
|
+
'px',
|
|
50
|
+
'em',
|
|
51
|
+
'rem',
|
|
52
|
+
'ch',
|
|
53
|
+
'%',
|
|
54
|
+
'vw',
|
|
55
|
+
'vh',
|
|
56
|
+
],
|
|
57
|
+
defaultValues: { px: 16, em: 2, rem: 2, ch: 2 },
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
const [ valueQuantity, valueUnit ] = parseQuantityAndUnitFromRawValue(
|
|
61
|
+
value,
|
|
62
|
+
units
|
|
63
|
+
);
|
|
64
|
+
const isValueUnitRelative =
|
|
65
|
+
!! valueUnit &&
|
|
66
|
+
[ 'em', 'rem', '%', 'ch', 'vw', 'vh' ].includes( valueUnit );
|
|
67
|
+
|
|
68
|
+
if ( ! withSlider ) {
|
|
69
|
+
return (
|
|
70
|
+
<UnitControl
|
|
71
|
+
__next40pxDefaultSize={ __next40pxDefaultSize }
|
|
72
|
+
__shouldNotWarnDeprecated36pxSize
|
|
73
|
+
{ ...otherProps }
|
|
74
|
+
label={ __( 'Line indent' ) }
|
|
75
|
+
value={ value }
|
|
76
|
+
__unstableInputWidth={ __unstableInputWidth }
|
|
77
|
+
units={ units }
|
|
78
|
+
onChange={ onChange }
|
|
79
|
+
help={ help }
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View style={ hasBottomMargin ? { marginBottom: 12 } : undefined }>
|
|
86
|
+
<BaseControl.VisualLabel>
|
|
87
|
+
{ __( 'Line indent' ) }
|
|
88
|
+
</BaseControl.VisualLabel>
|
|
89
|
+
<Flex>
|
|
90
|
+
<FlexItem isBlock>
|
|
91
|
+
<UnitControl
|
|
92
|
+
__next40pxDefaultSize={ __next40pxDefaultSize }
|
|
93
|
+
__shouldNotWarnDeprecated36pxSize
|
|
94
|
+
label={ __( 'Line indent' ) }
|
|
95
|
+
labelPosition="top"
|
|
96
|
+
hideLabelFromVision
|
|
97
|
+
value={ value }
|
|
98
|
+
onChange={ onChange }
|
|
99
|
+
size={ otherProps.size }
|
|
100
|
+
units={ units }
|
|
101
|
+
__unstableInputWidth={ __unstableInputWidth }
|
|
102
|
+
min={ 0 }
|
|
103
|
+
/>
|
|
104
|
+
</FlexItem>
|
|
105
|
+
{ withSlider && (
|
|
106
|
+
<FlexItem isBlock>
|
|
107
|
+
<Spacer marginX={ 2 } marginBottom={ 0 }>
|
|
108
|
+
<RangeControl
|
|
109
|
+
__next40pxDefaultSize={ __next40pxDefaultSize }
|
|
110
|
+
__shouldNotWarnDeprecated36pxSize
|
|
111
|
+
label={ __( 'Line indent' ) }
|
|
112
|
+
hideLabelFromVision
|
|
113
|
+
value={ valueQuantity }
|
|
114
|
+
withInputField={ false }
|
|
115
|
+
onChange={ ( newValue ) => {
|
|
116
|
+
if ( newValue === undefined ) {
|
|
117
|
+
onChange?.( undefined );
|
|
118
|
+
} else {
|
|
119
|
+
onChange?.(
|
|
120
|
+
newValue + ( valueUnit ?? 'px' )
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
} }
|
|
124
|
+
min={ 0 }
|
|
125
|
+
max={ isValueUnitRelative ? 10 : 100 }
|
|
126
|
+
step={ isValueUnitRelative ? 0.1 : 1 }
|
|
127
|
+
initialPosition={ 0 }
|
|
128
|
+
/>
|
|
129
|
+
</Spacer>
|
|
130
|
+
</FlexItem>
|
|
131
|
+
) }
|
|
132
|
+
</Flex>
|
|
133
|
+
{ help && (
|
|
134
|
+
<p className="components-base-control__help">{ help }</p>
|
|
135
|
+
) }
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -12,10 +12,10 @@ import { UP, DOWN, ENTER, TAB } from '@wordpress/keycodes';
|
|
|
12
12
|
import {
|
|
13
13
|
BaseControl,
|
|
14
14
|
Button,
|
|
15
|
-
__experimentalInputControl as InputControl,
|
|
16
15
|
Spinner,
|
|
17
16
|
withSpokenMessages,
|
|
18
17
|
Popover,
|
|
18
|
+
privateApis as componentsPrivateApis,
|
|
19
19
|
} from '@wordpress/components';
|
|
20
20
|
import {
|
|
21
21
|
compose,
|
|
@@ -30,6 +30,9 @@ import { isURL } from '@wordpress/url';
|
|
|
30
30
|
* Internal dependencies
|
|
31
31
|
*/
|
|
32
32
|
import { store as blockEditorStore } from '../../store';
|
|
33
|
+
import { unlock } from '../../lock-unlock';
|
|
34
|
+
|
|
35
|
+
const { ValidatedInputControl } = unlock( componentsPrivateApis );
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Whether the argument is a function.
|
|
@@ -426,6 +429,8 @@ class URLInput extends Component {
|
|
|
426
429
|
hideLabelFromVision = false,
|
|
427
430
|
help = null,
|
|
428
431
|
disabled = false,
|
|
432
|
+
customValidity,
|
|
433
|
+
markWhenOptional,
|
|
429
434
|
} = this.props;
|
|
430
435
|
|
|
431
436
|
const {
|
|
@@ -473,13 +478,27 @@ class URLInput extends Component {
|
|
|
473
478
|
help,
|
|
474
479
|
};
|
|
475
480
|
|
|
481
|
+
const validationProps = {
|
|
482
|
+
customValidity,
|
|
483
|
+
// Suppress the "(Required)" indicator in the label.
|
|
484
|
+
// The field is still required for validation, but the indicator
|
|
485
|
+
// can be hidden when markWhenOptional is set to true.
|
|
486
|
+
...( markWhenOptional !== undefined && {
|
|
487
|
+
markWhenOptional,
|
|
488
|
+
} ),
|
|
489
|
+
};
|
|
490
|
+
|
|
476
491
|
if ( renderControl ) {
|
|
477
492
|
return renderControl( controlProps, inputProps, loading );
|
|
478
493
|
}
|
|
479
494
|
|
|
480
495
|
return (
|
|
481
496
|
<BaseControl { ...controlProps }>
|
|
482
|
-
<
|
|
497
|
+
<ValidatedInputControl
|
|
498
|
+
{ ...inputProps }
|
|
499
|
+
{ ...validationProps }
|
|
500
|
+
__next40pxDefaultSize
|
|
501
|
+
/>
|
|
483
502
|
{ loading && <Spinner /> }
|
|
484
503
|
</BaseControl>
|
|
485
504
|
);
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
fullscreen,
|
|
28
28
|
linkOff,
|
|
29
29
|
} from '@wordpress/icons';
|
|
30
|
-
import {
|
|
30
|
+
import { prependHTTPS } from '@wordpress/url';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Internal dependencies
|
|
@@ -156,7 +156,7 @@ const ImageURLInputUI = ( {
|
|
|
156
156
|
)?.linkDestination || LINK_DESTINATION_CUSTOM;
|
|
157
157
|
|
|
158
158
|
onChangeUrl( {
|
|
159
|
-
href:
|
|
159
|
+
href: prependHTTPS( urlInput ),
|
|
160
160
|
linkDestination: selectedDestination,
|
|
161
161
|
lightbox: { enabled: false },
|
|
162
162
|
} );
|
|
@@ -114,10 +114,6 @@ export function getClosestTabbable(
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function isTabCandidate( node ) {
|
|
117
|
-
if ( node.closest( '[inert]' ) ) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
117
|
// Skip if there's only one child that is content editable (and thus a
|
|
122
118
|
// better candidate).
|
|
123
119
|
if (
|
package/src/hooks/anchor.js
CHANGED
|
@@ -64,7 +64,7 @@ function BlockEditAnchorControlPure( { anchor, setAttributes } ) {
|
|
|
64
64
|
help={
|
|
65
65
|
<>
|
|
66
66
|
{ __(
|
|
67
|
-
'Enter a word or two
|
|
67
|
+
'Enter a word or two—without spaces—to make a unique web address just for this block, called an “anchor”. Then, you’ll be able to link directly to this section of your page.'
|
|
68
68
|
) }
|
|
69
69
|
{ isWeb && (
|
|
70
70
|
<>
|
package/src/hooks/aria-label.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
import { addFilter } from '@wordpress/hooks';
|
|
5
5
|
import { hasBlockSupport } from '@wordpress/blocks';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies
|
|
9
|
+
*/
|
|
10
|
+
import { shouldSkipSerialization } from './utils';
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Filters registered block settings, extending attributes with ariaLabel using aria-label
|
|
9
14
|
* of the first node.
|
|
@@ -42,7 +47,10 @@ export function addAttribute( settings ) {
|
|
|
42
47
|
* @return {Object} Filtered props applied to save element.
|
|
43
48
|
*/
|
|
44
49
|
export function addSaveProps( extraProps, blockType, attributes ) {
|
|
45
|
-
if (
|
|
50
|
+
if (
|
|
51
|
+
hasBlockSupport( blockType, 'ariaLabel' ) &&
|
|
52
|
+
! shouldSkipSerialization( blockType, 'ariaLabel', 'ariaLabel' )
|
|
53
|
+
) {
|
|
46
54
|
extraProps[ 'aria-label' ] =
|
|
47
55
|
attributes.ariaLabel === '' ? null : attributes.ariaLabel;
|
|
48
56
|
}
|