Skip to content

Commit 1ed5078

Browse files
committed
Merge branch 'develop' of https://github.com/10up/block-components into develop
2 parents 7b080e1 + cda38ab commit 1ed5078

File tree

9 files changed

+286
-19
lines changed

9 files changed

+286
-19
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@ A collection of components built to be used in the block editor. These component
1515
2. Within your block editor code, import the relevant component(s) e.g. `import { ContentPicker } from '@10up/block-components';`
1616
3. We highly recommend you use [10up-toolkit](https://github.com/10up/10up-toolkit) to build your block files as it handles dependency extraction for you.
1717

18+
## Importing individual modules
19+
20+
As of [v1.20.6](https://github.com/10up/block-components/releases/tag/v1.20.6), you can now import individual modules directly to reduce bundle size.
21+
22+
Instead of importing from the base entry point:
23+
```jsx
24+
import { ContentPicker } from '@10up/block-components';
25+
import { useAllTerms } from '@10up/block-components';
26+
import { registerBlockExtension } from '@10up/block-components';
27+
import { iconStore } from '@10up/block-components';
28+
```
29+
30+
You can import `components`, `hooks`, `apis`, and `stores` individually from their specific paths:
31+
```jsx
32+
import { ContentPicker } from '@10up/block-components/components/content-picker';
33+
import { useAllTerms } from '@10up/block-components/hooks/use-all-terms';
34+
import { registerBlockExtension } from '@10up/block-components/api/register-block-extension';
35+
import { iconStore } from '@10up/block-components/stores/icons';
36+
```
37+
38+
This approach offers improved tree-shaking and results in smaller bundles, especially when only using a subset of the library.
39+
40+
The original import paths are still fully supported and will continue to work as expected.
41+
1842
## APIs
1943

2044
- [registerBlockExtension](./api/register-block-extension/)

api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export {
22
registerBlockExtension,
33
// continue to export misspelled version of api for backwards compatibility
44
registerBlockExtension as registerBlockExtention,
5+
unregisterBlockExtension,
56
} from './register-block-extension';
67
export { registerIcons } from './register-icons';

api/register-block-extension/index.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { addFilter } from '@wordpress/hooks';
1+
import { addFilter, removeFilter } from '@wordpress/hooks';
22
import { createHigherOrderComponent } from '@wordpress/compose';
33
import clsx from 'clsx';
44
import { FC } from 'react';
@@ -178,4 +178,46 @@ function registerBlockExtension(
178178
);
179179
}
180180

181-
export { registerBlockExtension };
181+
/**
182+
* Unregister a block extension that was previously registered using registerBlockExtension.
183+
*
184+
* @param {string|string[]} blockName - The name of the block or an array of block names to unregister the extension from.
185+
* @param {string} extensionName - The name of the extension to unregister.
186+
*/
187+
function unregisterBlockExtension(blockName: string | string[], extensionName: string): void {
188+
if (!blockName || !extensionName) {
189+
return;
190+
}
191+
192+
const isMultiBlock = Array.isArray(blockName);
193+
194+
if (blockName === '*') {
195+
blockName = 'all'; // eslint-disable-line no-param-reassign
196+
}
197+
198+
// @ts-expect-error isMultiBlock verifies if this is an Array and supports join or not.
199+
const blockNamespace = isMultiBlock ? blockName.join('-') : blockName;
200+
201+
// Remove all the filters that were added by registerBlockExtension
202+
removeFilter(
203+
'blocks.registerBlockType',
204+
`namespace/${blockNamespace}/${extensionName}/addAttributesToBlock`,
205+
);
206+
207+
removeFilter(
208+
'editor.BlockEdit',
209+
`namespace/${blockNamespace}/${extensionName}/addSettingsToBlock`,
210+
);
211+
212+
removeFilter(
213+
'editor.BlockListBlock',
214+
`namespace/${blockNamespace}/${extensionName}/addAdditionalPropertiesInEditor`,
215+
);
216+
217+
removeFilter(
218+
'blocks.getSaveContent.extraProps',
219+
`namespace/${blockNamespace}/${extensionName}/addAdditionalPropertiesToSavedMarkup`,
220+
);
221+
}
222+
223+
export { registerBlockExtension, unregisterBlockExtension };

api/register-block-extension/readme.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const additionalAttributes = {
3535
* @param {object} props block props
3636
* @returns {JSX}
3737
*/
38-
function BlockEdit(props) {...}
38+
function BlockEdit(props) {...}
3939

4040
/**
4141
* generateClassNames
@@ -46,7 +46,7 @@ function BlockEdit(props) {...}
4646
* @param {object} attributes block attributes
4747
* @returns {string}
4848
*/
49-
function generateClassNames(attributes) {...}
49+
function generateClassNames(attributes) {...}
5050

5151
/**
5252
* generateInlineStyles
@@ -57,7 +57,7 @@ function generateClassNames(attributes) {...}
5757
* @param {object} attributes block attributes
5858
* @returns {string}
5959
*/
60-
function generateInlineStyles(attributes) {...}
60+
function generateInlineStyles(attributes) {...}
6161

6262
registerBlockExtension(
6363
'core/group', // also supports adding multiple blocks as an array
@@ -83,3 +83,38 @@ registerBlockExtension(
8383
| options.inlineStyleGenerator | `function` | Function that gets passed the attributes of the block to generate an inline style object |
8484
| options.Edit | `function` | BlockEdit component like in `registerBlockType` only without the actual block. So only using slots like the `InspectorControls` is advised. |
8585
| options.order | `string` | The order in which the extension should be called in relation to the original BlockEdit component. Can be `before` or `after`. Defaults to `after` |
86+
87+
---
88+
89+
# unregisterBlockExtension
90+
91+
The `unregisterBlockExtension` API allows you to remove block extensions that were previously registered using `registerBlockExtension`. This is particularly useful in child themes or plugins where you need to override or remove functionality provided by a parent theme or another plugin.
92+
93+
## Usage
94+
95+
```js
96+
import { unregisterBlockExtension } from '@10up/block-components';
97+
98+
// Unregister a previously registered block extension
99+
unregisterBlockExtension('core/group', 'background-patterns');
100+
101+
// Works with multiple blocks too (same as registerBlockExtension)
102+
unregisterBlockExtension(['core/group', 'core/columns'], 'background-patterns');
103+
104+
// Works with the wildcard selector
105+
unregisterBlockExtension('*', 'background-patterns');
106+
```
107+
108+
## Parameters
109+
110+
| Name | Type | Description |
111+
|---------------|------------------|----------------------------------------------------------------------|
112+
| blockName | `string|string[]`| Name of the block or array with multiple block names to unregister the extension from. Also supports `'*'` or `'all'` to target all blocks |
113+
| extensionName | `string` | Unique identifier of the extension to unregister |
114+
115+
## Notes
116+
117+
- The function safely handles cases where the extension was never registered or has already been unregistered
118+
- Both parameters are required - the function will return early if either is missing
119+
- The `blockName` parameter must match exactly what was used when registering the extension
120+
- For multi-block registrations, you must use the same array of block names or unregister each block individually

components/content-picker/PickedItem.tsx

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ export type PickedItemType = {
2121
uuid: string;
2222
title: string;
2323
url: string;
24+
status?: string; // Optional status field for checking trashed posts
2425
};
2526

26-
const PickedItemContainer = styled.div<{ isDragging?: boolean; isOrderable?: boolean }>`
27+
const PickedItemContainer = styled.div<{
28+
isDragging?: boolean;
29+
isOrderable?: boolean;
30+
isDeleted?: boolean;
31+
}>`
2732
box-sizing: border-box;
2833
position: relative;
2934
display: flex;
@@ -33,9 +38,18 @@ const PickedItemContainer = styled.div<{ isDragging?: boolean; isOrderable?: boo
3338
min-height: 36px;
3439
max-width: 100%;
3540
width: 100%;
36-
color: #1e1e1e;
37-
opacity: ${({ isDragging }) => (isDragging ? 0.5 : 1)};
38-
background: ${({ isDragging }) => (isDragging ? '#f0f0f0' : 'transparent')};
41+
color: ${({ isDeleted }) => (isDeleted ? '#cc1818' : '#1e1e1e')};
42+
opacity: ${({ isDragging, isDeleted }) => {
43+
if (isDragging) return 0.5;
44+
if (isDeleted) return 0.7;
45+
return 1;
46+
}};
47+
background: ${({ isDragging, isDeleted }) => {
48+
if (isDragging) return '#f0f0f0';
49+
if (isDeleted) return '#fef7f7';
50+
return 'transparent';
51+
}};
52+
border: ${({ isDeleted }) => (isDeleted ? '1px solid #f0b7b7' : 'none')};
3953
border-radius: 2px;
4054
transition: background-color 0.1s linear;
4155
cursor: ${({ isDragging, isOrderable }) => {
@@ -45,7 +59,7 @@ const PickedItemContainer = styled.div<{ isDragging?: boolean; isOrderable?: boo
4559
touch-action: none;
4660
4761
&:hover {
48-
background: #f0f0f0;
62+
background: ${({ isDeleted }) => (isDeleted ? '#fef0f0' : '#f0f0f0')};
4963
5064
.move-up-button,
5165
.move-down-button,
@@ -100,11 +114,12 @@ const ItemContent = styled.div`
100114
transition: padding-left 0.1s linear;
101115
`;
102116

103-
const ItemTitle = styled.span`
117+
const ItemTitle = styled.span<{ isDeleted?: boolean }>`
104118
font-size: 0.875rem;
105119
line-height: 1.4;
106120
font-weight: 500;
107-
color: #1e1e1e;
121+
color: ${({ isDeleted }) => (isDeleted ? '#cc1818' : '#1e1e1e')};
122+
font-style: ${({ isDeleted }) => (isDeleted ? 'italic' : 'normal')};
108123
`;
109124

110125
const ItemURL = styled.span`
@@ -157,6 +172,7 @@ interface PickedItemProps {
157172
onMoveUp?: () => void;
158173
onMoveDown?: () => void;
159174
PickedItemPreviewComponent?: React.ComponentType<{ item: PickedItemType }>;
175+
isDeleted?: boolean;
160176
}
161177

162178
/**
@@ -165,18 +181,24 @@ interface PickedItemProps {
165181
* @component
166182
* @param {object} props - The component props.
167183
* @param {PickedItemType} props.item - The picked item to display.
184+
* @param {boolean} props.isDeleted - Whether the item has been deleted.
168185
* @returns {*} React JSX
169186
*/
170-
const PickedItemPreview: React.FC<{ item: PickedItemType }> = ({ item }) => {
187+
const PickedItemPreview: React.FC<{ item: PickedItemType; isDeleted?: boolean }> = ({
188+
item,
189+
isDeleted = false,
190+
}) => {
171191
const decodedTitle = decodeEntities(item.title);
172192
return (
173193
<>
174-
<ItemTitle>
194+
<ItemTitle isDeleted={isDeleted}>
175195
<Truncate title={decodedTitle} aria-label={decodedTitle}>
176196
{decodedTitle}
177197
</Truncate>
178198
</ItemTitle>
179-
{item.url && <ItemURL>{filterURLForDisplay(safeDecodeURI(item.url)) || ''}</ItemURL>}
199+
{item.url && !isDeleted && (
200+
<ItemURL>{filterURLForDisplay(safeDecodeURI(item.url)) || ''}</ItemURL>
201+
)}
180202
</>
181203
);
182204
};
@@ -198,6 +220,7 @@ const PickedItem: React.FC<PickedItemProps> = ({
198220
onMoveUp,
199221
onMoveDown,
200222
PickedItemPreviewComponent,
223+
isDeleted = false,
201224
}) => {
202225
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
203226
id,
@@ -220,6 +243,7 @@ const PickedItem: React.FC<PickedItemProps> = ({
220243
{...listeners}
221244
isDragging={isDragging}
222245
isOrderable={isOrderable}
246+
isDeleted={isDeleted}
223247
>
224248
{isOrderable && (
225249
<DragHandleWrapper isDragging={isDragging}>
@@ -230,7 +254,7 @@ const PickedItem: React.FC<PickedItemProps> = ({
230254
{PickedItemPreviewComponent ? (
231255
<PickedItemPreviewComponent item={item} />
232256
) : (
233-
<PickedItemPreview item={item} />
257+
<PickedItemPreview item={item} isDeleted={isDeleted} />
234258
)}
235259
</ItemContent>
236260
<ButtonContainer>

components/content-picker/SortableList.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ const SortableList: React.FC<SortableListProps> = ({
104104
fields.push('title');
105105
fields.push('url');
106106
fields.push('subtype');
107+
fields.push('status'); // Include status to check for trashed posts
107108
} else {
108109
fields.push('name');
109110
fields.push('taxonomy');
@@ -126,6 +127,7 @@ const SortableList: React.FC<SortableListProps> = ({
126127
url: post.link,
127128
id: post.id,
128129
type: post.type,
130+
status: post.status, // Include status for trashed post detection
129131
};
130132
} else if (mode === 'user') {
131133
const user = result as User;
@@ -206,7 +208,73 @@ const SortableList: React.FC<SortableListProps> = ({
206208
const renderItems = (items: Array<PickedItemType>) => {
207209
return items.map((post, index) => {
208210
const preparedItem = preparedItems[post.uuid];
209-
if (!preparedItem) return null;
211+
212+
// If the item doesn't exist (was deleted) or is trashed, show a placeholder with remove option
213+
if (!preparedItem) {
214+
return (
215+
<PickedItem
216+
isOrderable={hasMultiplePosts && isOrderable}
217+
key={post.uuid}
218+
handleItemDelete={handleItemDelete}
219+
item={{
220+
id: post.id,
221+
type: post.type,
222+
uuid: post.uuid,
223+
title: __('(Item no longer exists)', '10up-block-components'),
224+
url: '',
225+
}}
226+
mode={mode}
227+
id={post.uuid}
228+
positionInSet={index + 1}
229+
setSize={items.length}
230+
onMoveUp={() => {
231+
if (index === 0) return;
232+
setPosts(arrayMove(posts, index, index - 1));
233+
}}
234+
onMoveDown={() => {
235+
if (index === items.length - 1) return;
236+
setPosts(arrayMove(posts, index, index + 1));
237+
}}
238+
PickedItemPreviewComponent={PickedItemPreviewComponent}
239+
isDeleted
240+
/>
241+
);
242+
}
243+
244+
// Check if the post is trashed (only for post mode)
245+
const isTrashOrDeleted =
246+
mode === 'post' && preparedItem && preparedItem.status === 'trash';
247+
248+
if (isTrashOrDeleted) {
249+
return (
250+
<PickedItem
251+
isOrderable={hasMultiplePosts && isOrderable}
252+
key={post.uuid}
253+
handleItemDelete={handleItemDelete}
254+
item={{
255+
id: preparedItem.id,
256+
type: preparedItem.type,
257+
uuid: preparedItem.uuid,
258+
title: __('(Item in trash)', '10up-block-components'),
259+
url: preparedItem.url,
260+
}}
261+
mode={mode}
262+
id={post.uuid}
263+
positionInSet={index + 1}
264+
setSize={items.length}
265+
onMoveUp={() => {
266+
if (index === 0) return;
267+
setPosts(arrayMove(posts, index, index - 1));
268+
}}
269+
onMoveDown={() => {
270+
if (index === items.length - 1) return;
271+
setPosts(arrayMove(posts, index, index + 1));
272+
}}
273+
PickedItemPreviewComponent={PickedItemPreviewComponent}
274+
isDeleted
275+
/>
276+
);
277+
}
210278

211279
const handleMoveUp = () => {
212280
if (index === 0) return;

0 commit comments

Comments
 (0)