@automattic/newspack-blocks 4.24.1 → 4.25.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 +15 -0
- package/dist/author-profile/view-rtl.css +1 -1
- package/dist/author-profile/view.asset.php +1 -1
- package/dist/author-profile/view.css +1 -1
- package/dist/blocks/author-list/block.json +2 -1
- package/dist/blocks/author-profile/block.json +18 -1
- package/dist/blocks/carousel/block.json +2 -1
- package/dist/blocks/donate/block.json +2 -1
- package/dist/blocks/homepage-articles/block.json +2 -1
- package/dist/blocks/iframe/block.json +2 -1
- package/dist/editor-rtl.css +2 -2
- package/dist/editor.asset.php +1 -1
- package/dist/editor.css +2 -2
- package/dist/editor.js +23 -19
- package/dist/placeholder_blocks.asset.php +1 -1
- package/dist/placeholder_blocks.js +1 -1
- package/includes/class-modal-checkout.php +43 -11
- package/includes/class-newspack-blocks-caching.php +13 -1
- package/includes/class-newspack-blocks.php +14 -7
- package/languages/newspack-blocks-de_DE-2d52b39fdbc5d6c94b3514803f3720b8.json +1 -1
- package/languages/newspack-blocks-de_DE-34e5c64f90b1444f3fc735376442eada.json +1 -1
- package/languages/newspack-blocks-de_DE-4fdea541976076f02d56139fb35e5b42.json +1 -1
- package/languages/newspack-blocks-de_DE-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-de_DE-78456b164809d080adecb4d2b3895802.json +1 -1
- package/languages/newspack-blocks-de_DE-9ef9b2c60c897ad79f92951e6e9949a1.json +1 -1
- package/languages/newspack-blocks-de_DE-eccbc51a43c04f59165364eda71e0be7.json +1 -1
- package/languages/newspack-blocks-de_DE-fbe7f8c598cf05d4603ba49fec909ded.json +1 -1
- package/languages/newspack-blocks-de_DE.po +594 -493
- package/languages/newspack-blocks-es_ES-2d52b39fdbc5d6c94b3514803f3720b8.json +1 -1
- package/languages/newspack-blocks-es_ES-34e5c64f90b1444f3fc735376442eada.json +1 -1
- package/languages/newspack-blocks-es_ES-4fdea541976076f02d56139fb35e5b42.json +1 -1
- package/languages/newspack-blocks-es_ES-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-es_ES-78456b164809d080adecb4d2b3895802.json +1 -1
- package/languages/newspack-blocks-es_ES-9ef9b2c60c897ad79f92951e6e9949a1.json +1 -1
- package/languages/newspack-blocks-es_ES-eccbc51a43c04f59165364eda71e0be7.json +1 -1
- package/languages/newspack-blocks-es_ES-fbe7f8c598cf05d4603ba49fec909ded.json +1 -1
- package/languages/newspack-blocks-es_ES.po +594 -493
- package/languages/newspack-blocks-fr_BE-2d52b39fdbc5d6c94b3514803f3720b8.json +1 -1
- package/languages/newspack-blocks-fr_BE-34e5c64f90b1444f3fc735376442eada.json +1 -1
- package/languages/newspack-blocks-fr_BE-4fdea541976076f02d56139fb35e5b42.json +1 -1
- package/languages/newspack-blocks-fr_BE-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-fr_BE-78456b164809d080adecb4d2b3895802.json +1 -1
- package/languages/newspack-blocks-fr_BE-9ef9b2c60c897ad79f92951e6e9949a1.json +1 -1
- package/languages/newspack-blocks-fr_BE-eccbc51a43c04f59165364eda71e0be7.json +1 -1
- package/languages/newspack-blocks-fr_BE-fbe7f8c598cf05d4603ba49fec909ded.json +1 -1
- package/languages/newspack-blocks-fr_BE.po +594 -493
- package/languages/newspack-blocks-nb_NO-2d52b39fdbc5d6c94b3514803f3720b8.json +1 -1
- package/languages/newspack-blocks-nb_NO-34e5c64f90b1444f3fc735376442eada.json +1 -1
- package/languages/newspack-blocks-nb_NO-4fdea541976076f02d56139fb35e5b42.json +1 -1
- package/languages/newspack-blocks-nb_NO-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-nb_NO-78456b164809d080adecb4d2b3895802.json +1 -1
- package/languages/newspack-blocks-nb_NO-9ef9b2c60c897ad79f92951e6e9949a1.json +1 -1
- package/languages/newspack-blocks-nb_NO-eccbc51a43c04f59165364eda71e0be7.json +1 -1
- package/languages/newspack-blocks-nb_NO-fbe7f8c598cf05d4603ba49fec909ded.json +1 -1
- package/languages/newspack-blocks-nb_NO.po +594 -493
- package/languages/newspack-blocks-pt_PT-2d52b39fdbc5d6c94b3514803f3720b8.json +1 -1
- package/languages/newspack-blocks-pt_PT-34e5c64f90b1444f3fc735376442eada.json +1 -1
- package/languages/newspack-blocks-pt_PT-4fdea541976076f02d56139fb35e5b42.json +1 -1
- package/languages/newspack-blocks-pt_PT-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-pt_PT-78456b164809d080adecb4d2b3895802.json +1 -1
- package/languages/newspack-blocks-pt_PT-9ef9b2c60c897ad79f92951e6e9949a1.json +1 -1
- package/languages/newspack-blocks-pt_PT-eccbc51a43c04f59165364eda71e0be7.json +1 -1
- package/languages/newspack-blocks-pt_PT-fbe7f8c598cf05d4603ba49fec909ded.json +1 -1
- package/languages/newspack-blocks-pt_PT.po +594 -493
- package/languages/newspack-blocks.pot +806 -671
- package/newspack-blocks.php +5 -5
- package/package.json +1 -1
- package/src/blocks/author-list/block.json +3 -2
- package/src/blocks/author-list/edit.js +15 -8
- package/src/blocks/author-list/index.js +4 -3
- package/src/blocks/author-list/view.php +1 -0
- package/src/blocks/author-profile/block.json +16 -2
- package/src/blocks/author-profile/class-wp-rest-newspack-authors-controller.php +180 -9
- package/src/blocks/author-profile/context.js +26 -0
- package/src/blocks/author-profile/edit.js +847 -84
- package/src/blocks/author-profile/editor.scss +18 -14
- package/src/blocks/author-profile/index.js +14 -9
- package/src/blocks/author-profile/single-author.js +6 -2
- package/src/blocks/author-profile/view.php +386 -15
- package/src/blocks/author-profile/view.scss +48 -19
- package/src/blocks/carousel/block.json +2 -1
- package/src/blocks/carousel/edit.js +120 -117
- package/src/blocks/carousel/index.js +2 -1
- package/src/blocks/carousel/view.php +1 -0
- package/src/blocks/donate/block.json +3 -2
- package/src/blocks/donate/edit/FrequencyBasedLayout.tsx +2 -3
- package/src/blocks/donate/edit/index.tsx +56 -39
- package/src/blocks/donate/index.js +2 -1
- package/src/blocks/donate/view.php +1 -0
- package/src/blocks/homepage-articles/block.json +2 -1
- package/src/blocks/homepage-articles/edit.tsx +42 -37
- package/src/blocks/homepage-articles/index.js +2 -1
- package/src/blocks/homepage-articles/view.php +13 -1
- package/src/blocks/iframe/block.json +3 -2
- package/src/blocks/iframe/edit.js +36 -34
- package/src/blocks/iframe/index.js +2 -1
- package/src/blocks/iframe/view.php +1 -0
- package/src/blocks/video-playlist/edit.js +22 -15
- package/src/blocks/video-playlist/index.js +1 -0
- package/src/blocks/video-playlist/view.php +1 -0
- package/src/setup/placeholder-blocks.js +1 -0
- package/vendor/composer/installed.php +2 -2
|
@@ -2,32 +2,102 @@
|
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
4
|
import apiFetch from '@wordpress/api-fetch';
|
|
5
|
-
import { BlockControls, InspectorControls } from '@wordpress/block-editor';
|
|
5
|
+
import { BlockControls, InnerBlocks, InspectorControls, useBlockProps } from '@wordpress/block-editor';
|
|
6
|
+
import { createBlocksFromInnerBlocksTemplate, getBlockType, registerBlockBindingsSource } from '@wordpress/blocks';
|
|
6
7
|
import {
|
|
8
|
+
Button,
|
|
9
|
+
Card,
|
|
10
|
+
CardBody,
|
|
7
11
|
Notice,
|
|
8
12
|
PanelBody,
|
|
9
13
|
Placeholder,
|
|
14
|
+
SelectControl,
|
|
10
15
|
Spinner,
|
|
11
16
|
ToggleControl,
|
|
12
17
|
Toolbar,
|
|
18
|
+
ToolbarButton,
|
|
19
|
+
ToolbarGroup,
|
|
20
|
+
Tooltip,
|
|
13
21
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
|
14
22
|
__experimentalUnitControl as UnitControl,
|
|
15
23
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
|
16
24
|
__experimentalToggleGroupControl as ToggleGroupControl,
|
|
17
25
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
|
18
26
|
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
|
27
|
+
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
|
28
|
+
__experimentalVStack as VStack,
|
|
19
29
|
} from '@wordpress/components';
|
|
20
|
-
import {
|
|
30
|
+
import { store as coreStore } from '@wordpress/core-data';
|
|
31
|
+
import { useEffect, useState, useMemo } from '@wordpress/element';
|
|
32
|
+
import { useDispatch, useSelect } from '@wordpress/data';
|
|
21
33
|
import { decodeEntities } from '@wordpress/html-entities';
|
|
22
34
|
import { pencil, postAuthor, pullLeft, pullRight } from '@wordpress/icons';
|
|
23
35
|
import { __, sprintf } from '@wordpress/i18n';
|
|
24
36
|
import { addQueryArgs } from '@wordpress/url';
|
|
25
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Register block bindings source for author data in the editor.
|
|
40
|
+
* This enables core blocks to display author data via bindings.
|
|
41
|
+
*
|
|
42
|
+
* Note: The binding reads from a global author object that is set by the
|
|
43
|
+
* AuthorContext.Provider. This is a workaround since bindings don't have
|
|
44
|
+
* direct access to React context.
|
|
45
|
+
*/
|
|
46
|
+
// Per-instance author map for block bindings.
|
|
47
|
+
// Each Author Profile block registers its author here keyed by clientId,
|
|
48
|
+
// so multiple instances on the same page don't overwrite each other.
|
|
49
|
+
window.__newspackAuthorsByBlock = window.__newspackAuthorsByBlock || {};
|
|
50
|
+
|
|
51
|
+
if ( typeof registerBlockBindingsSource === 'function' ) {
|
|
52
|
+
registerBlockBindingsSource( {
|
|
53
|
+
name: 'newspack-blocks/author',
|
|
54
|
+
label: __( 'Author Profile', 'newspack-blocks' ),
|
|
55
|
+
getValues( { bindings, clientId, select } ) {
|
|
56
|
+
// Find the parent Author Profile block and look up its author.
|
|
57
|
+
const parents = select( 'core/block-editor' ).getBlockParents( clientId );
|
|
58
|
+
const authorMap = window.__newspackAuthorsByBlock;
|
|
59
|
+
const parentId = parents.find( id => authorMap[ id ] );
|
|
60
|
+
const author = ( parentId && authorMap[ parentId ] ) || {};
|
|
61
|
+
// Return empty for missing fields so WordPress core shows each block's
|
|
62
|
+
// own `placeholder` attribute in its native greyed-out style.
|
|
63
|
+
// Skip placeholder authors entirely (Site Editor template context).
|
|
64
|
+
const isPlaceholder = author.id === 'placeholder';
|
|
65
|
+
return Object.fromEntries(
|
|
66
|
+
Object.entries( bindings ).map( ( [ attribute, { args } ] ) => {
|
|
67
|
+
const key = args?.key;
|
|
68
|
+
if ( ! key || isPlaceholder ) {
|
|
69
|
+
return [ attribute, '' ];
|
|
70
|
+
}
|
|
71
|
+
// Handle special cases.
|
|
72
|
+
if ( key === 'url' || key === 'archive_url' ) {
|
|
73
|
+
return [ attribute, author.url || '' ];
|
|
74
|
+
}
|
|
75
|
+
// "More by Author Name" link text.
|
|
76
|
+
if ( key === 'archive_link_text' ) {
|
|
77
|
+
const linkText = author.name
|
|
78
|
+
? sprintf(
|
|
79
|
+
/* translators: %s: author name */
|
|
80
|
+
__( 'More by %s', 'newspack-blocks' ),
|
|
81
|
+
author.name
|
|
82
|
+
)
|
|
83
|
+
: '';
|
|
84
|
+
// Return HTML with link tag for editor preview.
|
|
85
|
+
const linkUrl = author.url || '#';
|
|
86
|
+
return [ attribute, linkText ? `<a href="${ linkUrl }" class="no-op">${ linkText }</a>` : '' ];
|
|
87
|
+
}
|
|
88
|
+
return [ attribute, author[ key ] || '' ];
|
|
89
|
+
} )
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
} );
|
|
93
|
+
}
|
|
94
|
+
|
|
26
95
|
/**
|
|
27
96
|
* Internal dependencies
|
|
28
97
|
*/
|
|
29
98
|
import { SingleAuthor } from './single-author';
|
|
30
99
|
import { AuthorDisplaySettings } from '../shared/author';
|
|
100
|
+
import { AuthorContext } from './context';
|
|
31
101
|
|
|
32
102
|
/**
|
|
33
103
|
* External dependencies
|
|
@@ -102,15 +172,212 @@ export const avatarSizeOptions = [
|
|
|
102
172
|
},
|
|
103
173
|
];
|
|
104
174
|
|
|
105
|
-
|
|
175
|
+
// Helper to create a bound paragraph block with custom list view name.
|
|
176
|
+
// If wrapInLink is true, the content will be wrapped in an anchor tag for editor preview.
|
|
177
|
+
const createBoundParagraph = ( key, className, name, placeholder, wrapInLink = false ) => {
|
|
178
|
+
const attributes = {
|
|
179
|
+
metadata: {
|
|
180
|
+
name, // Custom name shown in list view.
|
|
181
|
+
bindings: {
|
|
182
|
+
content: {
|
|
183
|
+
source: 'newspack-blocks/author',
|
|
184
|
+
args: { key },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
className,
|
|
189
|
+
placeholder: placeholder || name,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// If wrapInLink is true, set initial content with link wrapper for editor preview.
|
|
193
|
+
if ( wrapInLink ) {
|
|
194
|
+
const linkText = placeholder || name;
|
|
195
|
+
attributes.content = `<a href="#" class="no-op">${ linkText }</a>`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [ 'core/paragraph', attributes ];
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Template for nested inner blocks.
|
|
202
|
+
// Each author field is a separate block that can be reordered or removed.
|
|
203
|
+
// Block bindings connect core block attributes to author data via 'newspack-blocks/author' source.
|
|
204
|
+
const NESTED_TEMPLATE = [
|
|
205
|
+
[
|
|
206
|
+
'core/columns',
|
|
207
|
+
{ isStackedOnMobile: true, className: 'author-profile-columns', templateLock: 'insert' },
|
|
208
|
+
[
|
|
209
|
+
[
|
|
210
|
+
'core/column',
|
|
211
|
+
{
|
|
212
|
+
className: 'author-profile-avatar-column',
|
|
213
|
+
templateLock: 'insert',
|
|
214
|
+
allowedBlocks: [ 'newspack/avatar' ],
|
|
215
|
+
},
|
|
216
|
+
[ [ 'newspack/avatar', { size: 128 } ] ],
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
'core/column',
|
|
220
|
+
{
|
|
221
|
+
className: 'author-profile-content-column',
|
|
222
|
+
templateLock: false,
|
|
223
|
+
allowedBlocks: [ 'core/heading', 'core/paragraph', 'core/separator', 'core/spacer', 'newspack/author-profile-social' ],
|
|
224
|
+
style: {
|
|
225
|
+
spacing: {
|
|
226
|
+
blockGap: 'var:preset|spacing|20',
|
|
227
|
+
},
|
|
228
|
+
elements: {
|
|
229
|
+
link: {
|
|
230
|
+
color: {
|
|
231
|
+
text: 'var:preset|color|contrast-3',
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
textColor: 'contrast-3',
|
|
237
|
+
fontSize: 'small',
|
|
238
|
+
},
|
|
239
|
+
[
|
|
240
|
+
[
|
|
241
|
+
'core/heading',
|
|
242
|
+
{
|
|
243
|
+
level: 3,
|
|
244
|
+
metadata: {
|
|
245
|
+
name: __( 'Author Name', 'newspack-blocks' ),
|
|
246
|
+
bindings: {
|
|
247
|
+
content: {
|
|
248
|
+
source: 'newspack-blocks/author',
|
|
249
|
+
args: { key: 'name' },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
className: 'author-name',
|
|
254
|
+
placeholder: __( 'Author Name', 'newspack-blocks' ),
|
|
255
|
+
textColor: 'contrast',
|
|
256
|
+
fontSize: 'large',
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
[
|
|
260
|
+
'core/paragraph',
|
|
261
|
+
{
|
|
262
|
+
metadata: {
|
|
263
|
+
name: __( 'Job Title', 'newspack-blocks' ),
|
|
264
|
+
bindings: {
|
|
265
|
+
content: {
|
|
266
|
+
source: 'newspack-blocks/author',
|
|
267
|
+
args: { key: 'newspack_job_title' },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
className: 'author-job-title',
|
|
272
|
+
placeholder: __( 'Job Title', 'newspack-blocks' ),
|
|
273
|
+
style: {
|
|
274
|
+
typography: {
|
|
275
|
+
fontStyle: 'normal',
|
|
276
|
+
fontWeight: '600',
|
|
277
|
+
},
|
|
278
|
+
elements: {
|
|
279
|
+
link: {
|
|
280
|
+
color: {
|
|
281
|
+
text: 'var:preset|color|contrast',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
textColor: 'contrast',
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
createBoundParagraph( 'newspack_role', 'author-role', __( 'Role', 'newspack-blocks' ) ),
|
|
290
|
+
createBoundParagraph( 'newspack_employer', 'author-employer', __( 'Employer', 'newspack-blocks' ) ),
|
|
291
|
+
createBoundParagraph( 'bio', 'author-bio', __( 'Biography', 'newspack-blocks' ) ),
|
|
292
|
+
createBoundParagraph(
|
|
293
|
+
'archive_link_text',
|
|
294
|
+
'author-archive-link',
|
|
295
|
+
sprintf(
|
|
296
|
+
/* translators: %s: author name. */
|
|
297
|
+
__( 'More by %s', 'newspack-blocks' ),
|
|
298
|
+
__( 'Author Name', 'newspack-blocks' )
|
|
299
|
+
),
|
|
300
|
+
undefined,
|
|
301
|
+
true
|
|
302
|
+
),
|
|
303
|
+
[
|
|
304
|
+
'newspack/author-profile-social',
|
|
305
|
+
{
|
|
306
|
+
style: {
|
|
307
|
+
spacing: {
|
|
308
|
+
padding: {
|
|
309
|
+
top: 'var:preset|spacing|20',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
],
|
|
316
|
+
],
|
|
317
|
+
],
|
|
318
|
+
],
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
// Module-level cache for social icon SVGs so multiple block instances share one fetch.
|
|
322
|
+
let socialIconSvgsCache = null;
|
|
323
|
+
const fetchSocialIconSvgs = () => {
|
|
324
|
+
if ( ! socialIconSvgsCache ) {
|
|
325
|
+
socialIconSvgsCache = apiFetch( { path: '/newspack/v1/social-icons' } ).catch( () => ( {} ) );
|
|
326
|
+
}
|
|
327
|
+
return socialIconSvgsCache;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Placeholder author for Site Editor template context.
|
|
331
|
+
// Builds the social entries from the SVG map so every supported service gets an
|
|
332
|
+
// inner block in the template. Publishers can then remove the ones they don't need.
|
|
333
|
+
const DEFAULT_PLACEHOLDER_AUTHOR = Object.freeze( {
|
|
334
|
+
id: 'placeholder',
|
|
335
|
+
url: '#',
|
|
336
|
+
avatar: '', // Empty triggers the avatar block's built-in placeholder rendering.
|
|
337
|
+
social: Object.freeze( {} ),
|
|
338
|
+
email: Object.freeze( { url: 'mailto:placeholder@example.com', svg: '' } ),
|
|
339
|
+
newspack_phone_number: Object.freeze( { url: 'tel:0000000000', svg: '' } ),
|
|
340
|
+
} );
|
|
341
|
+
|
|
342
|
+
const getPlaceholderAuthor = ( socialIconSvgs = {} ) => {
|
|
343
|
+
const hasSocialSvgs = Object.keys( socialIconSvgs ).length > 0;
|
|
344
|
+
if ( ! hasSocialSvgs ) {
|
|
345
|
+
return DEFAULT_PLACEHOLDER_AUTHOR;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const social = Object.fromEntries(
|
|
349
|
+
Object.entries( socialIconSvgs )
|
|
350
|
+
.filter( ( [ key ] ) => ! [ 'email', 'phone' ].includes( key ) ) // Exclude top-level properties on the author object.
|
|
351
|
+
.map( ( [ service, svg ] ) => [ service, { url: '#', svg: svg || '' } ] )
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
...DEFAULT_PLACEHOLDER_AUTHOR,
|
|
356
|
+
social,
|
|
357
|
+
email: { url: 'mailto:placeholder@example.com', svg: socialIconSvgs.email || '' },
|
|
358
|
+
newspack_phone_number: { url: 'tel:0000000000', svg: socialIconSvgs.phone || '' },
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const AuthorProfile = ( { attributes, setAttributes, context, clientId } ) => {
|
|
363
|
+
const { replaceInnerBlocks } = useDispatch( 'core/block-editor' );
|
|
364
|
+
|
|
365
|
+
// ALL HOOKS MUST BE CALLED UNCONDITIONALLY (React rules of hooks)
|
|
106
366
|
const [ author, setAuthor ] = useState( null );
|
|
367
|
+
const [ contextualAuthors, setContextualAuthors ] = useState( [] );
|
|
107
368
|
const [ suggestions, setSuggestions ] = useState( null );
|
|
108
369
|
const [ error, setError ] = useState( null );
|
|
109
370
|
const [ isLoading, setIsLoading ] = useState( false );
|
|
110
371
|
const [ maxItemsToSuggest, setMaxItemsToSuggest ] = useState( 0 );
|
|
372
|
+
const [ showSpecificSelector, setShowSpecificSelector ] = useState( false );
|
|
373
|
+
const [ previewAuthorIndex, setPreviewAuthorIndex ] = useState( 0 );
|
|
374
|
+
const [ socialIconSvgs, setSocialIconSvgs ] = useState( {} );
|
|
375
|
+
|
|
111
376
|
const {
|
|
112
377
|
authorId,
|
|
113
378
|
isGuestAuthor,
|
|
379
|
+
isContextual,
|
|
380
|
+
layoutVersion,
|
|
114
381
|
showSocial,
|
|
115
382
|
showEmail,
|
|
116
383
|
textSize,
|
|
@@ -119,13 +386,94 @@ const AuthorProfile = ( { attributes, setAttributes } ) => {
|
|
|
119
386
|
avatarBorderRadius,
|
|
120
387
|
avatarSize,
|
|
121
388
|
avatarHideDefault,
|
|
389
|
+
showEmptyBio,
|
|
122
390
|
} = attributes;
|
|
391
|
+
const blockProps = useBlockProps();
|
|
392
|
+
|
|
393
|
+
// Get post ID from block context or editor
|
|
394
|
+
const editorPostId = useSelect( select => select( 'core/editor' )?.getCurrentPostId?.(), [] );
|
|
395
|
+
const postId = context?.postId || editorPostId;
|
|
396
|
+
|
|
397
|
+
// Check if custom byline is active and extract referenced author IDs.
|
|
398
|
+
// Returns IDs as a comma-separated string to avoid new array references on each render.
|
|
399
|
+
const { customBylineActive, bylineAuthorIdsStr } = useSelect(
|
|
400
|
+
select => {
|
|
401
|
+
if ( ! isContextual ) {
|
|
402
|
+
return { customBylineActive: false, bylineAuthorIdsStr: '' };
|
|
403
|
+
}
|
|
404
|
+
const meta = select( 'core/editor' )?.getEditedPostAttribute?.( 'meta' );
|
|
405
|
+
const isActive = meta?._newspack_byline_active ?? false;
|
|
406
|
+
if ( ! isActive ) {
|
|
407
|
+
return { customBylineActive: false, bylineAuthorIdsStr: '' };
|
|
408
|
+
}
|
|
409
|
+
const byline = meta?._newspack_byline ?? '';
|
|
410
|
+
const ids = [ ...byline.matchAll( /\[Author\s+id\s*=\s*(\d+)\]/gi ) ].map( m => m[ 1 ] );
|
|
411
|
+
return { customBylineActive: true, bylineAuthorIdsStr: ids.join( ',' ) };
|
|
412
|
+
},
|
|
413
|
+
[ isContextual ]
|
|
414
|
+
);
|
|
415
|
+
const bylineAuthorIds = bylineAuthorIdsStr ? bylineAuthorIdsStr.split( ',' ).map( Number ) : [];
|
|
416
|
+
|
|
417
|
+
// Detect Site Editor template context where real author data is not meaningful.
|
|
418
|
+
const isTemplateLikeContext = useSelect( select => {
|
|
419
|
+
const postType = select( 'core/editor' )?.getCurrentPostType?.();
|
|
420
|
+
return postType === 'wp_template' || postType === 'wp_template_part';
|
|
421
|
+
}, [] );
|
|
422
|
+
|
|
423
|
+
// Fetch social icon SVGs for the placeholder in template context.
|
|
424
|
+
useEffect( () => {
|
|
425
|
+
if ( ! isTemplateLikeContext ) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
fetchSocialIconSvgs().then( setSocialIconSvgs );
|
|
429
|
+
}, [ isTemplateLikeContext ] );
|
|
430
|
+
|
|
431
|
+
// Nested inner blocks mode is enabled automatically in block themes when
|
|
432
|
+
// Newspack Plugin is active (provides the avatar and social links blocks).
|
|
433
|
+
const isNestedMode = useSelect( select => {
|
|
434
|
+
const theme = select( coreStore ).getCurrentTheme();
|
|
435
|
+
return ( theme?.is_block_theme ?? false ) && !! getBlockType( 'newspack/avatar' ) && !! getBlockType( 'newspack/author-profile-social' );
|
|
436
|
+
}, [] );
|
|
123
437
|
|
|
438
|
+
// Set layoutVersion to 2 for brand new blocks in block themes.
|
|
439
|
+
// This persists the mode choice and enables InnerBlocks-based layout.
|
|
440
|
+
// Only converts unconfigured blocks to preserve existing blocks created in classic themes.
|
|
441
|
+
const isUnconfiguredBlock = authorId === 0 && ! isContextual;
|
|
124
442
|
useEffect( () => {
|
|
125
|
-
if (
|
|
126
|
-
|
|
443
|
+
if ( isNestedMode && layoutVersion === 1 && isUnconfiguredBlock ) {
|
|
444
|
+
setAttributes( { layoutVersion: 2 } );
|
|
127
445
|
}
|
|
128
|
-
}, [
|
|
446
|
+
}, [ isNestedMode, layoutVersion, isUnconfiguredBlock, setAttributes ] );
|
|
447
|
+
|
|
448
|
+
// Fetch author for specific mode
|
|
449
|
+
useEffect( () => {
|
|
450
|
+
if ( isContextual || 0 === authorId ) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
getAuthorById();
|
|
454
|
+
}, [ authorId, avatarHideDefault, isGuestAuthor, isContextual ] );
|
|
455
|
+
|
|
456
|
+
// Fetch authors for contextual mode
|
|
457
|
+
useEffect( () => {
|
|
458
|
+
if ( ! isContextual || isTemplateLikeContext ) {
|
|
459
|
+
setContextualAuthors( [] );
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// When custom byline is active, fetch the specific byline authors.
|
|
463
|
+
if ( customBylineActive ) {
|
|
464
|
+
if ( bylineAuthorIds.length ) {
|
|
465
|
+
getBylineAuthors();
|
|
466
|
+
} else {
|
|
467
|
+
setContextualAuthors( [] );
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if ( ! postId ) {
|
|
472
|
+
setContextualAuthors( [] );
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
getContextualAuthors();
|
|
476
|
+
}, [ isContextual, postId, avatarHideDefault, showEmail, customBylineActive, bylineAuthorIdsStr, isTemplateLikeContext ] );
|
|
129
477
|
|
|
130
478
|
const getAuthorById = async () => {
|
|
131
479
|
setError( null );
|
|
@@ -169,20 +517,142 @@ const AuthorProfile = ( { attributes, setAttributes } ) => {
|
|
|
169
517
|
setIsLoading( false );
|
|
170
518
|
};
|
|
171
519
|
|
|
520
|
+
const getContextualAuthors = async () => {
|
|
521
|
+
setError( null );
|
|
522
|
+
setIsLoading( true );
|
|
523
|
+
try {
|
|
524
|
+
// Only fetch email if showEmail is enabled (privacy consideration)
|
|
525
|
+
const fields = [ 'id', 'name', 'bio', 'social', 'avatar', 'url' ];
|
|
526
|
+
if ( showEmail ) {
|
|
527
|
+
fields.push( 'email' );
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const params = {
|
|
531
|
+
post_id: postId,
|
|
532
|
+
fields: fields.join( ',' ),
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if ( avatarHideDefault ) {
|
|
536
|
+
params.avatar_hide_default = 1;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const response = await apiFetch( {
|
|
540
|
+
path: addQueryArgs( '/newspack-blocks/v1/authors', params ),
|
|
541
|
+
} );
|
|
542
|
+
|
|
543
|
+
setContextualAuthors( response || [] );
|
|
544
|
+
} catch ( e ) {
|
|
545
|
+
setError( e.message || e || __( 'Error fetching authors for this post.', 'newspack-blocks' ) );
|
|
546
|
+
setContextualAuthors( [] );
|
|
547
|
+
}
|
|
548
|
+
setIsLoading( false );
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const getBylineAuthors = async () => {
|
|
552
|
+
setError( null );
|
|
553
|
+
setIsLoading( true );
|
|
554
|
+
try {
|
|
555
|
+
const fields = [ 'id', 'name', 'bio', 'social', 'avatar', 'url' ];
|
|
556
|
+
if ( showEmail ) {
|
|
557
|
+
fields.push( 'email' );
|
|
558
|
+
}
|
|
559
|
+
const results = await Promise.all(
|
|
560
|
+
bylineAuthorIds.map( id => {
|
|
561
|
+
const params = {
|
|
562
|
+
author_id: id,
|
|
563
|
+
is_guest_author: 0,
|
|
564
|
+
fields: fields.join( ',' ),
|
|
565
|
+
};
|
|
566
|
+
if ( avatarHideDefault ) {
|
|
567
|
+
params.avatar_hide_default = 1;
|
|
568
|
+
}
|
|
569
|
+
return apiFetch( {
|
|
570
|
+
path: addQueryArgs( '/newspack-blocks/v1/authors', params ),
|
|
571
|
+
} );
|
|
572
|
+
} )
|
|
573
|
+
);
|
|
574
|
+
setContextualAuthors( results.flat().filter( Boolean ) );
|
|
575
|
+
} catch ( e ) {
|
|
576
|
+
setError( e.message || e || __( 'Error fetching byline authors.', 'newspack-blocks' ) );
|
|
577
|
+
setContextualAuthors( [] );
|
|
578
|
+
}
|
|
579
|
+
setIsLoading( false );
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// Memoize authors for rendering based on mode
|
|
583
|
+
const authorsToRender = useMemo( () => {
|
|
584
|
+
let authors;
|
|
585
|
+
if ( isContextual ) {
|
|
586
|
+
if ( isTemplateLikeContext ) {
|
|
587
|
+
return [ getPlaceholderAuthor( socialIconSvgs ) ];
|
|
588
|
+
}
|
|
589
|
+
authors = contextualAuthors;
|
|
590
|
+
} else {
|
|
591
|
+
authors = author ? [ author ] : [];
|
|
592
|
+
}
|
|
593
|
+
if ( ! showEmptyBio ) {
|
|
594
|
+
authors = authors.filter( a => a.bio );
|
|
595
|
+
}
|
|
596
|
+
return authors;
|
|
597
|
+
}, [ isContextual, showEmptyBio, isTemplateLikeContext, socialIconSvgs, contextualAuthors, author ] );
|
|
598
|
+
|
|
599
|
+
// Reset preview index when authors list changes (e.g., switching posts)
|
|
600
|
+
useEffect( () => {
|
|
601
|
+
setPreviewAuthorIndex( 0 );
|
|
602
|
+
}, [ authorsToRender.length ] );
|
|
603
|
+
|
|
604
|
+
// Register author in the per-instance map for block bindings (nested mode only).
|
|
605
|
+
// Each Author Profile block stores its author keyed by clientId, so bindings
|
|
606
|
+
// in child blocks can look up the correct author via getBlockParents().
|
|
607
|
+
useEffect( () => {
|
|
608
|
+
if ( layoutVersion !== 2 ) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const safeIndex = Math.min( previewAuthorIndex, Math.max( 0, authorsToRender.length - 1 ) );
|
|
612
|
+
const previewAuthor = authorsToRender[ safeIndex ] || null;
|
|
613
|
+
window.__newspackAuthorsByBlock[ clientId ] = previewAuthor;
|
|
614
|
+
return () => {
|
|
615
|
+
delete window.__newspackAuthorsByBlock[ clientId ];
|
|
616
|
+
};
|
|
617
|
+
}, [ authorsToRender, previewAuthorIndex, layoutVersion, clientId ] );
|
|
618
|
+
|
|
172
619
|
// Combine social links and email, which are shown together.
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
620
|
+
const getSocialLinks = authorData => {
|
|
621
|
+
const socialLinks = ( showSocial && authorData?.social ) || {};
|
|
622
|
+
if ( showEmail && authorData?.email ) {
|
|
623
|
+
socialLinks.email = authorData.email;
|
|
624
|
+
} else {
|
|
625
|
+
delete socialLinks.email;
|
|
626
|
+
}
|
|
627
|
+
return socialLinks;
|
|
628
|
+
};
|
|
179
629
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
630
|
+
// Determine if we're in nested layout mode (publisher-controlled composition).
|
|
631
|
+
// In nested mode, hide field toggles since publishers control display by adding/removing blocks.
|
|
632
|
+
const isNestedLayout = layoutVersion === 2;
|
|
633
|
+
|
|
634
|
+
const resetLayout = () => {
|
|
635
|
+
replaceInnerBlocks( clientId, createBlocksFromInnerBlocksTemplate( NESTED_TEMPLATE ), false );
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Inspector controls for display settings
|
|
639
|
+
const inspectorControls = (
|
|
640
|
+
<InspectorControls>
|
|
641
|
+
{ isNestedLayout && (
|
|
642
|
+
<PanelBody title={ __( 'Display', 'newspack-blocks' ) }>
|
|
643
|
+
<ToggleControl
|
|
644
|
+
label={ __( 'Show authors without bio', 'newspack-blocks' ) }
|
|
645
|
+
help={ __( 'Display author profiles even if their bio is empty.', 'newspack-blocks' ) }
|
|
646
|
+
checked={ showEmptyBio }
|
|
647
|
+
onChange={ () => setAttributes( { showEmptyBio: ! showEmptyBio } ) }
|
|
648
|
+
__nextHasNoMarginBottom
|
|
649
|
+
/>
|
|
650
|
+
</PanelBody>
|
|
651
|
+
) }
|
|
652
|
+
{ ! isNestedLayout && (
|
|
653
|
+
<PanelBody title={ __( 'Settings', 'newspack-blocks' ) }>
|
|
184
654
|
<ToggleGroupControl
|
|
185
|
-
label={ __( 'Text
|
|
655
|
+
label={ __( 'Text size', 'newspack-blocks' ) }
|
|
186
656
|
value={ textSize }
|
|
187
657
|
onChange={ value => setAttributes( { textSize: value } ) }
|
|
188
658
|
isBlock
|
|
@@ -194,17 +664,22 @@ const AuthorProfile = ( { attributes, setAttributes } ) => {
|
|
|
194
664
|
</ToggleGroupControl>
|
|
195
665
|
<AuthorDisplaySettings attributes={ attributes } setAttributes={ setAttributes } />
|
|
196
666
|
</PanelBody>
|
|
667
|
+
) }
|
|
668
|
+
{ /* In nested mode, avatar is controlled via the inner newspack/avatar block */ }
|
|
669
|
+
{ ! isNestedLayout && (
|
|
197
670
|
<PanelBody title={ __( 'Avatar', 'newspack-blocks' ) }>
|
|
198
671
|
<ToggleControl
|
|
199
672
|
label={ __( 'Display avatar', 'newspack-blocks' ) }
|
|
200
673
|
checked={ showAvatar }
|
|
201
674
|
onChange={ () => setAttributes( { showAvatar: ! showAvatar } ) }
|
|
675
|
+
__nextHasNoMarginBottom
|
|
202
676
|
/>
|
|
203
677
|
{ showAvatar && (
|
|
204
678
|
<ToggleControl
|
|
205
679
|
label={ __( 'Hide default avatar', 'newspack-blocks' ) }
|
|
206
680
|
checked={ avatarHideDefault }
|
|
207
681
|
onChange={ () => setAttributes( { avatarHideDefault: ! avatarHideDefault } ) }
|
|
682
|
+
__nextHasNoMarginBottom
|
|
208
683
|
/>
|
|
209
684
|
) }
|
|
210
685
|
{ showAvatar && (
|
|
@@ -234,69 +709,153 @@ const AuthorProfile = ( { attributes, setAttributes } ) => {
|
|
|
234
709
|
/>
|
|
235
710
|
) }
|
|
236
711
|
</PanelBody>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
712
|
+
) }
|
|
713
|
+
</InspectorControls>
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
// Loading placeholder shared between nested and flat mode.
|
|
717
|
+
const loadingPlaceholder = (
|
|
718
|
+
<div { ...blockProps }>
|
|
719
|
+
{ inspectorControls }
|
|
720
|
+
<Placeholder className="newspack-blocks-author-profile" icon={ postAuthor } label={ __( 'Author Profile', 'newspack-blocks' ) }>
|
|
721
|
+
<VStack alignment="center" style={ { width: '100%' } }>
|
|
722
|
+
<Spinner style={ { margin: '0' } } />
|
|
723
|
+
<span style={ { fontWeight: '500' } }>{ __( 'Fetching authors…', 'newspack-blocks' ) }</span>
|
|
724
|
+
</VStack>
|
|
725
|
+
</Placeholder>
|
|
726
|
+
</div>
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Block controls for avatar alignment and edit button
|
|
730
|
+
const blockControls = authorsToRender.length > 0 && (
|
|
731
|
+
<BlockControls>
|
|
732
|
+
{ ! isNestedLayout && showAvatar && ! attributes.className?.includes( 'is-style-center' ) && (
|
|
733
|
+
<Toolbar
|
|
734
|
+
controls={ [
|
|
735
|
+
{
|
|
736
|
+
icon: pullLeft,
|
|
737
|
+
title: __( 'Show avatar on left', 'newspack-blocks' ),
|
|
738
|
+
isActive: avatarAlignment === 'left',
|
|
739
|
+
onClick: () => setAttributes( { avatarAlignment: 'left' } ),
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
icon: pullRight,
|
|
743
|
+
title: __( 'Show avatar on right', 'newspack-blocks' ),
|
|
744
|
+
isActive: avatarAlignment === 'right',
|
|
745
|
+
onClick: () => setAttributes( { avatarAlignment: 'right' } ),
|
|
746
|
+
},
|
|
747
|
+
] }
|
|
748
|
+
/>
|
|
749
|
+
) }
|
|
750
|
+
{ ! isContextual && (
|
|
751
|
+
<Toolbar
|
|
752
|
+
controls={ [
|
|
753
|
+
{
|
|
754
|
+
icon: pencil,
|
|
755
|
+
title: __( 'Edit selection', 'newspack-blocks' ),
|
|
756
|
+
onClick: () => {
|
|
757
|
+
setAttributes( { authorId: 0 } );
|
|
758
|
+
setAuthor( null );
|
|
267
759
|
},
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
760
|
+
},
|
|
761
|
+
] }
|
|
762
|
+
/>
|
|
763
|
+
) }
|
|
764
|
+
{ isNestedLayout && (
|
|
765
|
+
<ToolbarGroup>
|
|
766
|
+
<Tooltip text={ __( 'Reset layout', 'newspack-blocks' ) }>
|
|
767
|
+
<ToolbarButton label={ __( 'Reset layout', 'newspack-blocks' ) } onClick={ resetLayout }>
|
|
768
|
+
{ __( 'Reset', 'newspack-blocks' ) }
|
|
769
|
+
</ToolbarButton>
|
|
770
|
+
</Tooltip>
|
|
771
|
+
</ToolbarGroup>
|
|
271
772
|
) }
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
773
|
+
</BlockControls>
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
// Mode selection placeholder for new blocks (shared by nested and flat modes).
|
|
777
|
+
const modeSelectionPlaceholder = (
|
|
778
|
+
<div { ...blockProps }>
|
|
779
|
+
{ inspectorControls }
|
|
780
|
+
<Placeholder
|
|
781
|
+
className="newspack-blocks-author-profile"
|
|
782
|
+
icon={ postAuthor }
|
|
783
|
+
label={ __( 'Author Profile', 'newspack-blocks' ) }
|
|
784
|
+
instructions={ __( 'Select a type to start with.', 'newspack-blocks' ) }
|
|
785
|
+
>
|
|
786
|
+
<Button variant="primary" onClick={ () => setAttributes( { isContextual: true } ) }>
|
|
787
|
+
{ __( 'Contextual', 'newspack-blocks' ) }
|
|
788
|
+
</Button>
|
|
789
|
+
<Button variant="secondary" onClick={ () => setShowSpecificSelector( true ) }>
|
|
790
|
+
{ __( 'Specific', 'newspack-blocks' ) }
|
|
791
|
+
</Button>
|
|
792
|
+
</Placeholder>
|
|
793
|
+
</div>
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
// NESTED MODE: Use InnerBlocks for publisher-controlled layout (layoutVersion 2)
|
|
797
|
+
// This respects the block's saved mode regardless of current theme
|
|
798
|
+
if ( isNestedLayout ) {
|
|
799
|
+
// A v2 block opened in a classic theme can't render its inner blocks properly.
|
|
800
|
+
if ( ! isNestedMode ) {
|
|
801
|
+
return (
|
|
802
|
+
<div { ...blockProps }>
|
|
803
|
+
{ inspectorControls }
|
|
804
|
+
<Placeholder className="newspack-blocks-author-profile" icon={ postAuthor } label={ __( 'Author Profile', 'newspack-blocks' ) }>
|
|
805
|
+
<Notice status="warning" isDismissible={ false }>
|
|
806
|
+
{ __(
|
|
807
|
+
'This block was created with a block theme and is not supported in the current theme. It will render using the classic layout on the frontend.',
|
|
808
|
+
'newspack-blocks'
|
|
809
|
+
) }
|
|
279
810
|
</Notice>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
811
|
+
</Placeholder>
|
|
812
|
+
</div>
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Mode selection for new blocks in nested mode
|
|
817
|
+
if ( ! authorId && ! isContextual && ! showSpecificSelector ) {
|
|
818
|
+
return modeSelectionPlaceholder;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Loading state
|
|
822
|
+
if ( isLoading ) {
|
|
823
|
+
return loadingPlaceholder;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Custom byline with no real authors referenced
|
|
827
|
+
if ( isContextual && customBylineActive && ! bylineAuthorIds.length ) {
|
|
828
|
+
return (
|
|
829
|
+
<div { ...blockProps }>
|
|
830
|
+
{ inspectorControls }
|
|
831
|
+
<div className="newspack-author-profile-disabled">
|
|
832
|
+
<Notice status="warning" isDismissible={ false }>
|
|
833
|
+
{ __( 'Author bio is hidden because Custom Byline is active on this post.', 'newspack-blocks' ) }
|
|
834
|
+
</Notice>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Specific mode: show author search when no author selected
|
|
841
|
+
if ( ! isContextual && ! authorId ) {
|
|
842
|
+
return (
|
|
843
|
+
<div { ...blockProps }>
|
|
844
|
+
{ inspectorControls }
|
|
845
|
+
<Placeholder className="newspack-blocks-author-profile" icon={ postAuthor } label={ __( 'Author Profile', 'newspack-blocks' ) }>
|
|
846
|
+
{ error && (
|
|
847
|
+
<Notice status="error" isDismissible={ false }>
|
|
848
|
+
{ error }
|
|
849
|
+
</Notice>
|
|
850
|
+
) }
|
|
288
851
|
<AutocompleteWithSuggestions
|
|
289
852
|
label={ __( 'Search for an author to display', 'newspack-blocks' ) }
|
|
290
853
|
help={ __( 'Begin typing name, click autocomplete result to select.', 'newspack-blocks' ) }
|
|
291
854
|
fetchSuggestions={ async ( search = null, offset = 0 ) => {
|
|
292
|
-
// Reset suggestions in state.
|
|
293
855
|
setSuggestions( null );
|
|
294
|
-
|
|
295
|
-
// If we already have a selected author, no need to fetch suggestions.
|
|
296
856
|
if ( authorId && ! error ) {
|
|
297
857
|
return [];
|
|
298
858
|
}
|
|
299
|
-
|
|
300
859
|
const response = await apiFetch( {
|
|
301
860
|
parse: false,
|
|
302
861
|
path: addQueryArgs( '/newspack-blocks/v1/authors', {
|
|
@@ -305,41 +864,32 @@ const AuthorProfile = ( { attributes, setAttributes } ) => {
|
|
|
305
864
|
fields: 'id,name',
|
|
306
865
|
} ),
|
|
307
866
|
} );
|
|
308
|
-
|
|
309
|
-
const total = parseInt( response.headers.get( 'x-wp-total' ) || 0 );
|
|
867
|
+
const total = parseInt( response.headers.get( 'x-wp-total' ) || 0, 10 );
|
|
310
868
|
const authors = await response.json();
|
|
311
|
-
|
|
312
|
-
// Set max items for "load more" functionality in suggestions list.
|
|
313
869
|
if ( ! maxItemsToSuggest && ! search ) {
|
|
314
870
|
setMaxItemsToSuggest( total );
|
|
315
871
|
}
|
|
316
|
-
|
|
317
872
|
const _suggestions = authors.map( _author => ( {
|
|
318
873
|
value: _author.id,
|
|
319
874
|
label: decodeEntities( _author.name ) || __( '(no name)', 'newspack-blocks' ),
|
|
320
875
|
isGuestAuthor: _author.is_guest,
|
|
321
876
|
} ) );
|
|
322
|
-
|
|
323
877
|
setSuggestions( _suggestions );
|
|
324
|
-
|
|
325
878
|
return _suggestions;
|
|
326
879
|
} }
|
|
327
880
|
maxItemsToSuggest={ maxItemsToSuggest }
|
|
328
881
|
onChange={ items => {
|
|
329
882
|
let selectionIsGuest = false;
|
|
330
883
|
const selection = items[ 0 ];
|
|
331
|
-
|
|
332
|
-
// We need to check whether the selected author is a guest author or not.
|
|
333
884
|
if ( suggestions ) {
|
|
334
885
|
suggestions.forEach( suggestion => {
|
|
335
|
-
if ( parseInt( selection?.value ) === parseInt( suggestion?.value ) && suggestion?.isGuestAuthor ) {
|
|
886
|
+
if ( parseInt( selection?.value, 10 ) === parseInt( suggestion?.value, 10 ) && suggestion?.isGuestAuthor ) {
|
|
336
887
|
selectionIsGuest = true;
|
|
337
888
|
}
|
|
338
889
|
} );
|
|
339
890
|
}
|
|
340
|
-
|
|
341
891
|
setAttributes( {
|
|
342
|
-
authorId: parseInt( selection?.value || 0 ),
|
|
892
|
+
authorId: parseInt( selection?.value || 0, 10 ),
|
|
343
893
|
isGuestAuthor: selectionIsGuest,
|
|
344
894
|
} );
|
|
345
895
|
} }
|
|
@@ -347,10 +897,223 @@ const AuthorProfile = ( { attributes, setAttributes } ) => {
|
|
|
347
897
|
postTypeLabelPlural={ __( 'authors', 'newspack-blocks' ) }
|
|
348
898
|
selectedItems={ [] }
|
|
349
899
|
/>
|
|
900
|
+
</Placeholder>
|
|
901
|
+
</div>
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Contextual mode: no authors found
|
|
906
|
+
if ( isContextual && ! authorsToRender.length ) {
|
|
907
|
+
return (
|
|
908
|
+
<div { ...blockProps }>
|
|
909
|
+
{ inspectorControls }
|
|
910
|
+
<Placeholder className="newspack-blocks-author-profile" icon={ postAuthor } label={ __( 'Author Profile', 'newspack-blocks' ) }>
|
|
911
|
+
{ __( 'No authors found for this post.', 'newspack-blocks' ) }
|
|
912
|
+
</Placeholder>
|
|
913
|
+
</div>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Get preview author (bounds-checked)
|
|
918
|
+
const safeIndex = Math.min( previewAuthorIndex, authorsToRender.length - 1 );
|
|
919
|
+
const previewAuthor = authorsToRender[ safeIndex ];
|
|
920
|
+
|
|
921
|
+
// Set in the per-instance map synchronously so bindings have access on first render.
|
|
922
|
+
// The useEffect handles cleanup when component unmounts.
|
|
923
|
+
window.__newspackAuthorsByBlock[ clientId ] = previewAuthor;
|
|
924
|
+
|
|
925
|
+
const nestedBlockProps = {
|
|
926
|
+
...blockProps,
|
|
927
|
+
className: `${ blockProps.className } wp-block-newspack-blocks-author-profile is-nested-mode`,
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
return (
|
|
931
|
+
<AuthorContext.Provider value={ previewAuthor }>
|
|
932
|
+
<div { ...nestedBlockProps }>
|
|
933
|
+
{ inspectorControls }
|
|
934
|
+
{ blockControls }
|
|
935
|
+
{ /* Author selector: only shown in contextual mode with multiple authors */ }
|
|
936
|
+
{ isContextual && authorsToRender.length > 1 && (
|
|
937
|
+
<Card isRounded={ false } size="small" style={ { marginBottom: '32px' } } variant="secondary">
|
|
938
|
+
<CardBody>
|
|
939
|
+
<SelectControl
|
|
940
|
+
label={ __( 'Preview author', 'newspack-blocks' ) }
|
|
941
|
+
value={ safeIndex }
|
|
942
|
+
options={ authorsToRender.map( ( a, index ) => ( {
|
|
943
|
+
label: a.name,
|
|
944
|
+
value: index,
|
|
945
|
+
} ) ) }
|
|
946
|
+
onChange={ value => setPreviewAuthorIndex( parseInt( value, 10 ) ) }
|
|
947
|
+
help={ sprintf(
|
|
948
|
+
/* translators: %d: number of authors */
|
|
949
|
+
__( 'Previewing 1 of %d authors. All authors display on frontend.', 'newspack-blocks' ),
|
|
950
|
+
authorsToRender.length
|
|
951
|
+
) }
|
|
952
|
+
__next40pxDefaultSize
|
|
953
|
+
__nextHasNoMarginBottom
|
|
954
|
+
/>
|
|
955
|
+
</CardBody>
|
|
956
|
+
</Card>
|
|
350
957
|
) }
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
958
|
+
{ /* Key forces re-render when author changes, which re-evaluates bindings */ }
|
|
959
|
+
<InnerBlocks
|
|
960
|
+
key={ `author-${ previewAuthor?.id || 'none' }` }
|
|
961
|
+
template={ NESTED_TEMPLATE }
|
|
962
|
+
templateLock="insert"
|
|
963
|
+
allowedBlocks={ [ 'core/columns' ] }
|
|
964
|
+
/>
|
|
965
|
+
</div>
|
|
966
|
+
</AuthorContext.Provider>
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// MODE SELECTION: Show mode selector for NEW blocks (no authorId and not contextual)
|
|
971
|
+
if ( ! authorId && ! isContextual && ! showSpecificSelector ) {
|
|
972
|
+
return modeSelectionPlaceholder;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// CONTEXTUAL MODE
|
|
976
|
+
if ( isContextual ) {
|
|
977
|
+
// Loading state
|
|
978
|
+
if ( isLoading ) {
|
|
979
|
+
return loadingPlaceholder;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Custom byline with no real authors referenced
|
|
983
|
+
if ( customBylineActive && ! bylineAuthorIds.length ) {
|
|
984
|
+
return (
|
|
985
|
+
<div { ...blockProps }>
|
|
986
|
+
{ inspectorControls }
|
|
987
|
+
<div className="newspack-author-profile-disabled">
|
|
988
|
+
<Notice status="warning" isDismissible={ false }>
|
|
989
|
+
{ __( 'Author bio is hidden because Custom Byline is active on this post.', 'newspack-blocks' ) }
|
|
990
|
+
</Notice>
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// No authors found
|
|
997
|
+
if ( ! authorsToRender.length ) {
|
|
998
|
+
return (
|
|
999
|
+
<div { ...blockProps }>
|
|
1000
|
+
{ inspectorControls }
|
|
1001
|
+
<Placeholder className="newspack-blocks-author-profile" icon={ postAuthor } label={ __( 'Author Profile', 'newspack-blocks' ) }>
|
|
1002
|
+
{ __( 'No authors found for this post.', 'newspack-blocks' ) }
|
|
1003
|
+
</Placeholder>
|
|
1004
|
+
</div>
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Render contextual authors
|
|
1009
|
+
return (
|
|
1010
|
+
<div { ...blockProps }>
|
|
1011
|
+
{ inspectorControls }
|
|
1012
|
+
{ blockControls }
|
|
1013
|
+
{ authorsToRender.map( authorData => (
|
|
1014
|
+
<SingleAuthor
|
|
1015
|
+
key={ authorData.id }
|
|
1016
|
+
author={ { ...authorData, social: getSocialLinks( authorData ) } }
|
|
1017
|
+
attributes={ attributes }
|
|
1018
|
+
/>
|
|
1019
|
+
) ) }
|
|
1020
|
+
</div>
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// SPECIFIC MODE: Author selected - render it
|
|
1025
|
+
if ( author ) {
|
|
1026
|
+
return (
|
|
1027
|
+
<div { ...blockProps }>
|
|
1028
|
+
{ inspectorControls }
|
|
1029
|
+
{ blockControls }
|
|
1030
|
+
<SingleAuthor author={ { ...author, social: getSocialLinks( author ) } } attributes={ attributes } />
|
|
1031
|
+
</div>
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// SPECIFIC MODE: No author selected - show search
|
|
1036
|
+
return (
|
|
1037
|
+
<div { ...blockProps }>
|
|
1038
|
+
{ inspectorControls }
|
|
1039
|
+
<Placeholder className="newspack-blocks-author-profile" icon={ postAuthor } label={ __( 'Author Profile', 'newspack-blocks' ) }>
|
|
1040
|
+
{ error && (
|
|
1041
|
+
<Notice status="error" isDismissible={ false }>
|
|
1042
|
+
{ error }
|
|
1043
|
+
</Notice>
|
|
1044
|
+
) }
|
|
1045
|
+
{ isLoading && (
|
|
1046
|
+
<VStack alignment="center" style={ { width: '100%' } }>
|
|
1047
|
+
<Spinner style={ { margin: '0' } } />
|
|
1048
|
+
<span style={ { fontWeight: '500' } }>{ __( 'Fetching authors…', 'newspack-blocks' ) }</span>
|
|
1049
|
+
</VStack>
|
|
1050
|
+
) }
|
|
1051
|
+
{ ! isLoading && (
|
|
1052
|
+
<AutocompleteWithSuggestions
|
|
1053
|
+
label={ __( 'Search for an author to display', 'newspack-blocks' ) }
|
|
1054
|
+
help={ __( 'Begin typing name, click autocomplete result to select.', 'newspack-blocks' ) }
|
|
1055
|
+
fetchSuggestions={ async ( search = null, offset = 0 ) => {
|
|
1056
|
+
// Reset suggestions in state.
|
|
1057
|
+
setSuggestions( null );
|
|
1058
|
+
|
|
1059
|
+
// If we already have a selected author, no need to fetch suggestions.
|
|
1060
|
+
if ( authorId && ! error ) {
|
|
1061
|
+
return [];
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const response = await apiFetch( {
|
|
1065
|
+
parse: false,
|
|
1066
|
+
path: addQueryArgs( '/newspack-blocks/v1/authors', {
|
|
1067
|
+
search,
|
|
1068
|
+
offset,
|
|
1069
|
+
fields: 'id,name',
|
|
1070
|
+
} ),
|
|
1071
|
+
} );
|
|
1072
|
+
|
|
1073
|
+
const total = parseInt( response.headers.get( 'x-wp-total' ) || 0, 10 );
|
|
1074
|
+
const authors = await response.json();
|
|
1075
|
+
|
|
1076
|
+
// Set max items for "load more" functionality in suggestions list.
|
|
1077
|
+
if ( ! maxItemsToSuggest && ! search ) {
|
|
1078
|
+
setMaxItemsToSuggest( total );
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const _suggestions = authors.map( _author => ( {
|
|
1082
|
+
value: _author.id,
|
|
1083
|
+
label: decodeEntities( _author.name ) || __( '(no name)', 'newspack-blocks' ),
|
|
1084
|
+
isGuestAuthor: _author.is_guest,
|
|
1085
|
+
} ) );
|
|
1086
|
+
|
|
1087
|
+
setSuggestions( _suggestions );
|
|
1088
|
+
|
|
1089
|
+
return _suggestions;
|
|
1090
|
+
} }
|
|
1091
|
+
maxItemsToSuggest={ maxItemsToSuggest }
|
|
1092
|
+
onChange={ items => {
|
|
1093
|
+
let selectionIsGuest = false;
|
|
1094
|
+
const selection = items[ 0 ];
|
|
1095
|
+
|
|
1096
|
+
// We need to check whether the selected author is a guest author or not.
|
|
1097
|
+
if ( suggestions ) {
|
|
1098
|
+
suggestions.forEach( suggestion => {
|
|
1099
|
+
if ( parseInt( selection?.value, 10 ) === parseInt( suggestion?.value, 10 ) && suggestion?.isGuestAuthor ) {
|
|
1100
|
+
selectionIsGuest = true;
|
|
1101
|
+
}
|
|
1102
|
+
} );
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
setAttributes( {
|
|
1106
|
+
authorId: parseInt( selection?.value || 0, 10 ),
|
|
1107
|
+
isGuestAuthor: selectionIsGuest,
|
|
1108
|
+
} );
|
|
1109
|
+
} }
|
|
1110
|
+
postTypeLabel={ __( 'author', 'newspack-blocks' ) }
|
|
1111
|
+
postTypeLabelPlural={ __( 'authors', 'newspack-blocks' ) }
|
|
1112
|
+
selectedItems={ [] }
|
|
1113
|
+
/>
|
|
1114
|
+
) }
|
|
1115
|
+
</Placeholder>
|
|
1116
|
+
</div>
|
|
354
1117
|
);
|
|
355
1118
|
};
|
|
356
1119
|
|