PluginProbe ʕ •ᴥ•ʔ
AlphaListing / 4.4.0
AlphaListing v4.4.0
trunk 4.3.4 4.3.5 4.3.6 4.3.7 4.4.0
alphalisting / scripts / blocks / edit.js
alphalisting / scripts / blocks Last commit date
attributes.json 1 month ago constants.js 1 month ago edit.js 1 month ago index.js 1 month ago shortcode-attributes.js 1 month ago shortcode-upgrader.js 1 month ago
edit.js
915 lines
1 /**
2 * Retrieves the translation of text.
3 *
4 * @see https://developer.wordpress.org/block-editor/packages/packages-i18n/
5 */
6
7 import { useEffect, useMemo } from '@wordpress/element';
8 import { FormTokenField, PanelBody, Placeholder, RangeControl, SelectControl, Spinner, ToggleControl, TextControl, __experimentalUnitControl as UnitControl, __experimentalSpacer as Spacer } from '@wordpress/components';
9 import * as ServerSideRenderModule from '@wordpress/server-side-render';
10 import { __ } from '@wordpress/i18n';
11 import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
12 import { useSelect } from '@wordpress/data';
13 import { store as coreStore } from '@wordpress/core-data';
14 import { pin } from '@wordpress/icons';
15 import { addFilter, applyFilters } from '@wordpress/hooks';
16 import {v4 as uuid} from 'uuid';
17
18 /**
19 * Internal dependencies
20 */
21 import { MAX_COLUMN_WIDTH, MAX_POSTS_COLUMNS } from './constants';
22
23 import defaults from './attributes.json';
24 import ItemSelection from '../components/ItemSelection';
25 import DisplayOptions from '../components/DisplayOptions';
26 import Extensions from '../components/Extensions';
27 import AZInspectorControls from '../components/AZInspectorControls';
28 import PostParent from '../components/PostParent';
29
30 /**
31 * Filters to kill any stale state when selections are changed in the editor
32 */
33 addFilter(
34 'alphalisting_selection_changed_for__display',
35 'alphalisting',
36 ( attributes ) => ( {
37 ...attributes,
38 'post-type': defaults['post-type'].default,
39 taxonomy: defaults.taxonomy.default,
40 terms: [ ...defaults.terms.default ],
41 'exclude-posts': [ ...defaults['exclude-posts'].default ],
42 'exclude-terms': [ ...defaults['exclude-terms'].default ],
43 'parent-post': defaults['parent-post'].default,
44 'parent-term': defaults['parent-term'].default,
45 'hide-empty-terms': defaults['hide-empty-terms'].default,
46 'get-all-children': defaults['get-all-children'].default,
47 } ),
48 5
49 );
50 addFilter(
51 'alphalisting_selection_changed_for__post-type',
52 'alphalisting',
53 ( attributes ) => ( {
54 ...attributes,
55 taxonomy: defaults.taxonomy.default,
56 terms: [ ...defaults.terms.default ],
57 'exclude-posts': [ ...defaults['exclude-posts'].default ],
58 'parent-post': defaults['parent-post'].default,
59 } ),
60 5
61 );
62 addFilter(
63 'alphalisting_selection_changed_for__taxonomy',
64 'alphalisting',
65 ( attributes ) => ( {
66 ...attributes,
67 terms: [ ...defaults.terms.default ],
68 'exclude-terms': [ ...defaults['exclude-terms'].default ],
69 'parent-term': defaults['parent-term'].default,
70 'get-all-children': defaults['get-all-children'].default,
71 } ),
72 5
73 );
74
75 const ServerSideRender = ServerSideRenderModule.ServerSideRender || ServerSideRenderModule.default;
76 const displayTypes = applyFilters(
77 'alphalisting_display_types',
78 [
79 { value: 'posts', label: __( 'Posts', 'alphalisting' ) },
80 { value: 'terms', label: __( 'Taxonomy Terms', 'alphalisting' ) },
81 ]
82 );
83 const LENGTH_VALUE_PATTERN = /^([0-9]+(?:\.[0-9]+)?)\s*(px|em|rem|%|ch)$/i;
84 const UNIT_LESS_ZERO_PATTERN = /^0+(?:\.0+)?$/;
85 const LENGTH_UNITS = [
86 { value: 'px', label: 'px', step: 1, min: 0, max: MAX_COLUMN_WIDTH },
87 { value: 'em', label: 'em', step: 0.1, min: 0 },
88 { value: 'rem', label: 'rem', step: 0.1, min: 0 },
89 { value: '%', label: '%', step: 1, min: 0, max: 100 },
90 { value: 'ch', label: 'ch', step: 1, min: 0, max: 100 },
91 ];
92
93 /**
94 * Convert an arbitrary value into a bounded integer column count.
95 *
96 * @param {unknown} value Raw attribute value.
97 * @return {number} Sanitized column count.
98 */
99 const sanitizeColumnCount = ( value ) => {
100 if ( typeof value === 'number' && Number.isFinite( value ) ) {
101 value = Math.trunc( value );
102 } else if ( typeof value === 'string' && value.trim() !== '' && ! Number.isNaN( Number( value ) ) ) {
103 value = Math.trunc( Number( value ) );
104 } else {
105 value = defaults.columns.default;
106 }
107
108 if ( value < 1 ) {
109 return 1;
110 }
111
112 if ( value > MAX_POSTS_COLUMNS ) {
113 return MAX_POSTS_COLUMNS;
114 }
115
116 return value;
117 };
118
119 /**
120 * Ensure a CSS length string uses an allowed unit and within safe bounds.
121 *
122 * @param {unknown} value Raw attribute value.
123 * @param {string} fallback Default value when validation fails.
124 * @return {string} Sanitized CSS length string.
125 */
126 const sanitizeLengthValue = ( value, fallback ) => {
127 if ( typeof value !== 'string' ) {
128 if ( typeof value === 'number' && Number.isFinite( value ) ) {
129 value = String( value );
130 } else {
131 return fallback;
132 }
133 }
134
135 const trimmed = value.trim();
136
137 if ( trimmed === '' ) {
138 return fallback;
139 }
140
141 if ( UNIT_LESS_ZERO_PATTERN.test( trimmed ) ) {
142 return '0';
143 }
144
145 const match = trimmed.match( LENGTH_VALUE_PATTERN );
146
147 if ( ! match ) {
148 return fallback;
149 }
150
151 const numeric = Number.parseFloat( match[ 1 ] );
152 const unit = match[ 2 ].toLowerCase();
153
154 if ( ! Number.isFinite( numeric ) || numeric < 0 ) {
155 return fallback;
156 }
157
158 let bounded = numeric;
159
160 if ( unit === 'px' ) {
161 bounded = Math.min( bounded, MAX_COLUMN_WIDTH );
162 }
163
164 if ( unit === '%' || unit === 'ch' ) {
165 bounded = Math.min( bounded, 100 );
166 }
167
168 if ( Number.isInteger( bounded ) ) {
169 return `${ bounded }${ unit }`;
170 }
171
172 return `${ parseFloat( bounded.toFixed( 4 ) ) }${ unit }`;
173 };
174
175 /**
176 * Convert a value into a positive integer or null when invalid.
177 *
178 * @param {unknown} value Raw attribute value.
179 * @return {?number} Sanitized integer or null when invalid.
180 */
181 const sanitizePositiveInteger = ( value ) => {
182 if ( typeof value === 'number' && Number.isFinite( value ) ) {
183 value = Math.trunc( value );
184 } else if ( typeof value === 'string' ) {
185 const trimmed = value.trim();
186
187 if ( trimmed === '' ) {
188 return null;
189 }
190
191 const numeric = Number( trimmed );
192
193 if ( Number.isNaN( numeric ) ) {
194 return null;
195 }
196
197 value = Math.trunc( numeric );
198 } else if ( Array.isArray( value ) ) {
199 // When the attribute is serialized as a list of tokens already.
200 return null;
201 } else {
202 return null;
203 }
204
205 if ( value <= 0 ) {
206 return null;
207 }
208
209 return value;
210 };
211
212 /**
213 * Ensure a list of identifiers only contains unique positive integers.
214 *
215 * @param {unknown} values Raw attribute value.
216 * @return {number[]} Sanitized list of identifiers.
217 */
218 const sanitizeNumericTokenList = ( values ) => {
219 if ( typeof values === 'string' ) {
220 values = values.split( ',' );
221 }
222
223 if ( ! Array.isArray( values ) ) {
224 return [];
225 }
226
227 const sanitized = values
228 .map( sanitizePositiveInteger )
229 .filter( ( value ) => value !== null );
230
231 return Array.from( new Set( sanitized ) );
232 };
233
234 /**
235 * Ensure a list of tokens is stored as unique, trimmed strings.
236 *
237 * @param {unknown} values Raw attribute value.
238 * @return {string[]} Sanitized list of string tokens.
239 */
240 const sanitizeStringTokenList = ( values ) => {
241 if ( typeof values === 'string' ) {
242 values = values.split( ',' );
243 }
244
245 if ( ! Array.isArray( values ) ) {
246 return [];
247 }
248
249 const sanitized = values
250 .map( ( value ) => {
251 if ( typeof value === 'string' ) {
252 return value.trim();
253 }
254
255 if ( typeof value === 'number' && Number.isFinite( value ) ) {
256 return String( value );
257 }
258
259 return '';
260 } )
261 .filter( ( value ) => value !== '' );
262
263 return Array.from( new Set( sanitized ) );
264 };
265
266 /**
267 * Normalize selected post types into unique string slugs.
268 *
269 * @param {unknown} values Raw attribute value.
270 * @return {string[]} Sanitized post type slugs.
271 */
272 const sanitizePostTypeList = ( values ) => {
273 if ( typeof values === 'string' ) {
274 values = values.split( ',' );
275 }
276
277 return sanitizeStringTokenList( values );
278 };
279
280 const A_Z_Listing_Edit = ( { attributes, setAttributes } ) => {
281 const blockProps = useBlockProps();
282 const { postTypes, allTaxonomies } = useSelect( ( select ) => {
283 const { getPostTypes, getTaxonomies } = select( coreStore );
284 const excludedPostTypes = [ 'attachment' ];
285 const filteredPostTypes = getPostTypes( { per_page: -1 } )?.filter(
286 ( { viewable, slug } ) =>
287 viewable && ! excludedPostTypes.includes( slug )
288 );
289 const taxonomies = getTaxonomies() ?? [];
290 return { postTypes: filteredPostTypes, allTaxonomies: taxonomies };
291 } );
292
293 const postTypesMap = useMemo( () => {
294 if ( ! postTypes?.length ) return;
295 return postTypes.reduce( ( accumulator, type ) => {
296 accumulator[ type.slug ] = type;
297 return accumulator;
298 }, {} );
299 }, [ postTypes ] );
300
301 const postTypesSelectOptions = useMemo( () => {
302 if ( ! postTypes?.length ) return;
303 return ( postTypes ).map( ( { labels, slug } ) => ( {
304 label: labels.name,
305 value: slug,
306 } ) );
307 }, [ postTypes ] );
308
309 const postTypesTaxonomiesMap = useMemo( () => {
310 if ( ! postTypes?.length ) return;
311 return postTypes.reduce( ( accumulator, type ) => {
312 accumulator[ type.slug ] = type.taxonomies;
313 return accumulator;
314 }, {} );
315 }, [ postTypes ] );
316
317 const postTypesTaxonomiesSelectOptions = useMemo( () => {
318 let postTaxonomies = [];
319 const selectedPostTypes = sanitizePostTypeList( attributes['post-type'] ?? defaults['post-type'].default );
320 if ( attributes['display'] === 'posts' && selectedPostTypes.length > 0 && postTypesTaxonomiesMap ) {
321 postTaxonomies = selectedPostTypes.flatMap(
322 ( postType ) => postTypesTaxonomiesMap[ postType ] || []
323 );
324 postTaxonomies = Array.from( new Set( postTaxonomies ) );
325 }
326 return [ { label: '', slug: '' } ].concat(
327 allTaxonomies
328 .filter( ( tax ) => postTaxonomies.includes( tax.slug ) )
329 .map( ( tax ) => ( {
330 label: tax.name,
331 value: tax.slug,
332 } ) )
333 );
334 }, [ attributes.display, attributes['post-type'], postTypesTaxonomiesMap, allTaxonomies ]);
335
336 const taxonomiesSelectOptions = useMemo( () => {
337 return [ { label: '', slug: '' } ].concat(
338 allTaxonomies.map( ( tax ) => ( {
339 label: tax.name,
340 value: tax.slug,
341 } ) )
342 );
343 }, [ allTaxonomies ] );
344
345 const validationErrors = useMemo( () => {
346 const errors = [];
347 if ( 'terms' === attributes.display && ! attributes.taxonomy ) {
348 errors.push(
349 __(
350 `You must set a taxonomy when display mode is set to 'terms'.`,
351 'alphalisting'
352 )
353 );
354 }
355 return errors;
356 }, [ attributes.display, attributes.taxonomy ] );
357
358 const sanitizedColumns = useMemo(
359 () => sanitizeColumnCount( attributes.columns ),
360 [ attributes.columns ]
361 );
362
363 const sanitizedColumnWidth = useMemo(
364 () => sanitizeLengthValue(
365 attributes['column-width'] ?? defaults['column-width'].default,
366 defaults['column-width'].default
367 ),
368 [ attributes['column-width'] ]
369 );
370
371 const sanitizedColumnGap = useMemo(
372 () => sanitizeLengthValue(
373 attributes['column-gap'] ?? defaults['column-gap'].default,
374 defaults['column-gap'].default
375 ),
376 [ attributes['column-gap'] ]
377 );
378
379 useEffect( () => {
380 if ( typeof attributes.columns !== 'undefined' && attributes.columns !== sanitizedColumns ) {
381 setAttributes( { columns: sanitizedColumns } );
382 }
383 }, [ attributes.columns, sanitizedColumns, setAttributes ] );
384
385 useEffect( () => {
386 if ( typeof attributes['column-width'] !== 'undefined' && attributes['column-width'] !== sanitizedColumnWidth ) {
387 setAttributes( { 'column-width': sanitizedColumnWidth } );
388 }
389 }, [ attributes['column-width'], sanitizedColumnWidth, setAttributes ] );
390
391 useEffect( () => {
392 if ( typeof attributes['column-gap'] !== 'undefined' && attributes['column-gap'] !== sanitizedColumnGap ) {
393 setAttributes( { 'column-gap': sanitizedColumnGap } );
394 }
395 }, [ attributes['column-gap'], sanitizedColumnGap, setAttributes ] );
396
397 useEffect( () => {
398 if ( typeof attributes['parent-term'] === 'number' ) {
399 setAttributes( { 'parent-term': String( attributes['parent-term'] ) } );
400 }
401 }, [ attributes['parent-term'], setAttributes ] );
402
403 useEffect( () => {
404 if ( typeof attributes['exclude-posts'] === 'undefined' ) {
405 return;
406 }
407
408 const sanitized = sanitizeNumericTokenList( attributes['exclude-posts'] );
409
410 if ( JSON.stringify( sanitized ) !== JSON.stringify( attributes['exclude-posts'] ) ) {
411 setAttributes( { 'exclude-posts': sanitized } );
412 }
413 }, [ attributes['exclude-posts'], setAttributes ] );
414
415 useEffect( () => {
416 if ( typeof attributes['exclude-terms'] === 'undefined' ) {
417 return;
418 }
419
420 const sanitized = sanitizeNumericTokenList( attributes['exclude-terms'] )
421 .map( ( token ) => token.toString() );
422
423 if ( JSON.stringify( sanitized ) !== JSON.stringify( attributes['exclude-terms'] ) ) {
424 setAttributes( { 'exclude-terms': sanitized } );
425 }
426 }, [ attributes['exclude-terms'], setAttributes ] );
427
428 useEffect( () => {
429 if ( typeof attributes.terms === 'undefined' ) {
430 return;
431 }
432
433 let sanitized;
434
435 if ( attributes.display === 'terms' ) {
436 sanitized = sanitizeNumericTokenList( attributes.terms )
437 .map( ( token ) => token.toString() );
438 } else {
439 sanitized = sanitizeStringTokenList( attributes.terms );
440 }
441
442 if ( JSON.stringify( sanitized ) !== JSON.stringify( attributes.terms ) ) {
443 setAttributes( { terms: sanitized } );
444 }
445 }, [ attributes.terms, attributes.display, setAttributes ] );
446
447 useEffect( () => {
448 if ( typeof attributes['post-type'] === 'undefined' ) {
449 return;
450 }
451
452 const sanitized = sanitizePostTypeList( attributes['post-type'] );
453 const normalized = sanitized.join( ',' );
454 const current = Array.isArray( attributes['post-type'] )
455 ? attributes['post-type'].join( ',' )
456 : String( attributes['post-type'] ?? '' ).trim();
457
458 if ( normalized !== current ) {
459 setAttributes( { 'post-type': normalized } );
460 }
461 }, [ attributes['post-type'], setAttributes ] );
462
463 const excludePostsTokens = useMemo(
464 () => sanitizeNumericTokenList( attributes['exclude-posts'] ).map( ( token ) => token.toString() ),
465 [ attributes['exclude-posts'] ]
466 );
467
468 const excludeTermsTokens = useMemo(
469 () => sanitizeNumericTokenList( attributes['exclude-terms'] ).map( ( token ) => token.toString() ),
470 [ attributes['exclude-terms'] ]
471 );
472 const selectedPostTypes = useMemo(
473 () => sanitizePostTypeList( attributes['post-type'] ?? defaults['post-type'].default ),
474 [ attributes['post-type'] ]
475 );
476 const selectedPostTypeForParent = selectedPostTypes.length === 1
477 ? selectedPostTypes[ 0 ]
478 : '';
479
480 const parentTermValue = typeof attributes['parent-term'] === 'undefined'
481 ? defaults['parent-term'].default
482 : String( attributes['parent-term'] ?? '' );
483 const hasTermParentSelection = attributes.display === 'terms'
484 && ( parentTermValue.trim() !== '' );
485 const showDescendantsToggle = hasTermParentSelection;
486
487 const inspectorControls = (
488 <InspectorControls>
489 <AZInspectorControls.Slot>
490 { ( fills ) => (
491 <>
492 <PanelBody title={ __( 'Listing selection', 'alphalisting' ) }>
493 <ItemSelection.Slot>
494 { ( subFills ) => (
495 <>
496 <SelectControl
497 label={ __( 'Display mode', 'alphalisting' ) }
498 value={ attributes.display ?? defaults['display'].default }
499 options={ displayTypes }
500 onChange={ ( value ) =>
501 setAttributes(
502 applyFilters(
503 'alphalisting_selection_changed_for__display',
504 { display: value }
505 )
506 )
507 }
508 __next40pxDefaultSize
509 __nextHasNoMarginBottom
510 />
511
512 { 'posts' === attributes.display && (
513 <SelectControl
514 label={ __( 'Post Type', 'alphalisting' ) }
515 help={ __( 'Select the post types to display. Hold Ctrl to select multiple.', 'alphalisting' ) }
516 value={ selectedPostTypes }
517 options={ postTypesSelectOptions }
518 onChange={ ( value ) =>
519 setAttributes(
520 applyFilters(
521 'alphalisting_selection_changed_for__post-type',
522 { 'post-type': sanitizePostTypeList( value ).join( ',' ) }
523 )
524 )
525 }
526 multiple
527 __next40pxDefaultSize
528 __nextHasNoMarginBottom
529 />
530 ) }
531
532 { (
533 'posts' === attributes.display &&
534 selectedPostTypeForParent &&
535 postTypesMap && postTypesMap[ selectedPostTypeForParent ]?.hierarchical
536 ) && (
537 <PostParent
538 pageId={ attributes['parent-post'] ?? defaults['parent-post'].default }
539 postTypeSlug={ selectedPostTypeForParent }
540 onChange={ ( parentId ) => setAttributes( { 'parent-post': parentId } ) }
541 />
542 ) }
543
544 { (
545 ( 'posts' === attributes.display && postTypesTaxonomiesSelectOptions.length > 1 ) ||
546 'terms' === attributes.display
547 ) && (
548 <SelectControl
549 label={ __( 'Taxonomy', 'alphalisting' ) }
550 value={ attributes.taxonomy ?? '' }
551 options={
552 'posts' === attributes.display
553 ? postTypesTaxonomiesSelectOptions
554 : taxonomiesSelectOptions
555 }
556 onChange={ ( value ) =>
557 setAttributes(
558 applyFilters(
559 'alphalisting_selection_changed_for__taxonomy',
560 { taxonomy: value }
561 )
562 )
563 }
564 __next40pxDefaultSize
565 __nextHasNoMarginBottom
566 />
567 ) }
568
569 { 'posts' === attributes.display && !! attributes.taxonomy && (
570 <FormTokenField
571 label={ __( 'Taxonomy terms', 'alphalisting' ) }
572 value={ attributes.terms ?? [] }
573 onChange={ ( value ) =>
574 setAttributes( {
575 terms: sanitizeStringTokenList( value ),
576 } )
577 }
578 __next40pxDefaultSize
579 __nextHasNoMarginBottom
580 />
581 ) }
582
583 { 'posts' === attributes.display && (
584 <>
585 <Spacer marginBottom={'16px'} />
586 <FormTokenField
587 label={ __( 'Exclude post IDs', 'alphalisting' ) }
588 value={ excludePostsTokens }
589 onChange={ ( value ) =>
590 setAttributes( {
591 'exclude-posts': sanitizeNumericTokenList( value ),
592 } )
593 }
594 help={ __( 'Provide numeric post IDs to omit from the listing.', 'alphalisting' ) }
595 __next40pxDefaultSize
596 __nextHasNoMarginBottom
597 />
598 </>
599 ) }
600
601 { 'terms' === attributes.display && (
602 <TextControl
603 label={ __( 'Parent term (slug or ID)', 'alphalisting' ) }
604 value={ parentTermValue }
605 onChange={ ( value ) =>
606 setAttributes( {
607 'parent-term': value.trim(),
608 } )
609 }
610 __next40pxDefaultSize
611 __nextHasNoMarginBottom
612 />
613 ) }
614
615 { ( 'terms' === attributes.display ) && (
616 <>
617 <Spacer marginBottom={'16px'} />
618 <FormTokenField
619 label={ __( 'Exclude term IDs', 'alphalisting' ) }
620 value={ excludeTermsTokens }
621 onChange={ ( value ) =>
622 setAttributes( {
623 'exclude-terms': sanitizeNumericTokenList( value )
624 .map( ( token ) => token.toString() ),
625 } )
626 }
627 help={ __( 'Provide numeric term IDs to omit from the listing.', 'alphalisting' ) }
628 __next40pxDefaultSize
629 __nextHasNoMarginBottom
630 />
631 <Spacer marginBottom={'16px'} />
632 </>
633 ) }
634
635 { showDescendantsToggle && (
636 <ToggleControl
637 label={ __( 'Include all descendants', 'alphalisting' ) }
638 help={ __( 'Include items from all levels below the selected parent.', 'alphalisting' ) }
639 checked={ !! attributes['get-all-children'] }
640 onChange={ ( value ) =>
641 setAttributes( { 'get-all-children': value } )
642 }
643 __next40pxDefaultSize
644 __nextHasNoMarginBottom
645 />
646 ) }
647
648 { 'terms' === attributes.display && (
649 <ToggleControl
650 label={ __( 'Hide empty terms', 'alphalisting' ) }
651 checked={ !! attributes['hide-empty-terms'] }
652 onChange={ ( value ) =>
653 setAttributes( { 'hide-empty-terms': value } )
654 }
655 __next40pxDefaultSize
656 __nextHasNoMarginBottom
657 />
658 ) }
659
660 { subFills }
661 </>
662 ) }
663 </ItemSelection.Slot>
664 </PanelBody>
665
666 <PanelBody title={ __( 'Display options', 'alphalisting' ) }>
667 <DisplayOptions.Slot>
668 { ( subFills ) => (
669 <>
670 <TextControl
671 label={ __( 'Listing ID', 'alphalisting' ) }
672 value={ attributes['instance-id'] ?? uuid() }
673 onChange={ (value) =>
674 setAttributes( { 'instance-id': value } )
675 }
676 __next40pxDefaultSize
677 __nextHasNoMarginBottom
678 />
679 <TextControl
680 label={ __( 'CSS class names', 'alphalisting' ) }
681 value={ attributes.className ?? '' }
682 onChange={ ( value ) =>
683 setAttributes( { className: value } )
684 }
685 __next40pxDefaultSize
686 __nextHasNoMarginBottom
687 />
688 <TextControl
689 label={ __( 'Alphabet', 'alphalisting' ) }
690 value={ attributes.alphabet ?? defaults['alphabet'].default }
691 onChange={ ( value ) =>
692 setAttributes( { alphabet: value } )
693 }
694 __next40pxDefaultSize
695 __nextHasNoMarginBottom
696 />
697 <SelectControl
698 label={ __( 'Numbers', 'alphalisting' ) }
699 value={ attributes.numbers ?? defaults['numbers'].default }
700 options={ [
701 {
702 value: 'hide',
703 label: __(
704 'Hide numbers',
705 'alphalisting'
706 ),
707 },
708 {
709 value: 'before',
710 label: __(
711 'Prepend before alphabet',
712 'alphalisting'
713 ),
714 },
715 {
716 value: 'after',
717 label: __(
718 'Append after alphabet',
719 'alphalisting'
720 ),
721 },
722 ] }
723 onChange={ ( value ) =>
724 setAttributes(
725 applyFilters(
726 'alphalisting_selection_changed_for__numbers',
727 { numbers: value }
728 )
729 )
730 }
731 __next40pxDefaultSize
732 __nextHasNoMarginBottom
733 />
734
735 <RangeControl
736 label={ __( 'Group letters', 'alphalisting' ) }
737 help={ __(
738 'The number of letters to include in a single group',
739 'alphalisting'
740 ) }
741 value={ attributes.grouping ?? defaults['grouping'].default }
742 min={ 1 }
743 max={ 10 }
744 onChange={ ( value ) =>
745 setAttributes(
746 applyFilters(
747 'alphalisting_selection_changed_for__grouping',
748 { grouping: value }
749 )
750 )
751 }
752 withInputField
753 __next40pxDefaultSize
754 __nextHasNoMarginBottom
755 />
756
757 { 'hide' !== attributes.numbers &&
758 ! (
759 1 < attributes.grouping
760 ) && (
761 <ToggleControl
762 label={ __(
763 'Group numbers',
764 'alphalisting'
765 ) }
766 help={ __(
767 'Group 0-9 as a single letter',
768 'alphalisting'
769 ) }
770 checked={ !! attributes['group-numbers'] }
771 onChange={ ( value ) =>
772 setAttributes(
773 applyFilters(
774 'alphalisting_selection_changed_for__group-numbers',
775 {
776 'group-numbers': !! value,
777 }
778 )
779 )
780 }
781 __next40pxDefaultSize
782 __nextHasNoMarginBottom
783 />
784 ) }
785
786 <ToggleControl
787 label={ __( 'Display symbols entry first', 'alphalisting' ) }
788 checked={ !!attributes['symbols-first'] }
789 onChange={ ( value ) =>
790 setAttributes( { 'symbols-first': value } )
791 }
792 __next40pxDefaultSize
793 __nextHasNoMarginBottom
794 />
795 <ToggleControl
796 label={ __( 'Show back to top link', 'alphalisting' ) }
797 checked={ !! attributes['back-to-top'] }
798 onChange={ ( value ) =>
799 setAttributes( { 'back-to-top': value } )
800 }
801 __next40pxDefaultSize
802 __nextHasNoMarginBottom
803 />
804
805 <RangeControl
806 label={ __( 'Columns', 'alphalisting' ) }
807 value={ sanitizedColumns }
808 onChange={ ( value ) =>
809 setAttributes( { columns: sanitizeColumnCount( value ) } )
810 }
811 min={ 1 }
812 max={ MAX_POSTS_COLUMNS }
813 withInputField
814 required
815 __next40pxDefaultSize
816 __nextHasNoMarginBottom
817 />
818
819 <UnitControl
820 label={ __( 'Column width', 'alphalisting' ) }
821 value={ sanitizedColumnWidth }
822 units={ LENGTH_UNITS }
823 onChange={ ( nextValue ) =>
824 setAttributes( {
825 'column-width': sanitizeLengthValue(
826 nextValue,
827 defaults['column-width'].default
828 ),
829 } )
830 }
831 __next40pxDefaultSize
832 __nextHasNoMarginBottom
833 />
834 <UnitControl
835 label={ __( 'Column gap', 'alphalisting' ) }
836 value={ sanitizedColumnGap }
837 units={ LENGTH_UNITS }
838 onChange={ ( nextValue ) =>
839 setAttributes( {
840 'column-gap': sanitizeLengthValue(
841 nextValue,
842 defaults['column-gap'].default
843 ),
844 } )
845 }
846 __next40pxDefaultSize
847 __nextHasNoMarginBottom
848 />
849 { subFills }
850 </>
851 ) }
852 </DisplayOptions.Slot>
853 </PanelBody>
854
855 <Extensions.Slot>
856 { ( subFills ) => ( <> { subFills } </> ) }
857 </Extensions.Slot>
858
859 { fills }
860 </>
861 ) }
862 </AZInspectorControls.Slot>
863 </InspectorControls>
864 );
865
866 const errors = applyFilters(
867 'alphalisting-validation-errors',
868 validationErrors
869 );
870
871 return (
872 <div { ...blockProps }>
873 { inspectorControls }
874
875 { errors.length > 0 ? (
876 <Placeholder icon={ pin } label={ __( 'AlphaListing', 'alphalisting' ) }>
877 { __( 'The AlphaListing configuration is incomplete:', 'alphalisting' ) }
878 <ul>
879 { errors.map( ( error, idx ) => (
880 <li key={ idx }>{ error }</li>
881 ) ) }
882 </ul>
883 </Placeholder>
884 ) : (
885 <ServerSideRender
886 block="alphalisting/block"
887 attributes={ attributes }
888 LoadingResponsePlaceholder={ () => <Spinner /> }
889 ErrorResponsePlaceholder={ () => (
890 <Placeholder
891 icon={ pin }
892 label={ __( 'AlphaListing', 'alphalisting' ) }
893 >
894 { __( 'Error Loading the listing...', 'alphalisting' ) }
895 </Placeholder>
896 ) }
897 EmptyResponsePlaceholder={ () => (
898 <Placeholder
899 icon={ pin }
900 label={ __( 'AlphaListing', 'alphalisting' ) }
901 >
902 { __(
903 'The listing has returned an empty page. This is likely an error.',
904 'alphalisting'
905 ) }
906 </Placeholder>
907 ) }
908 />
909 ) }
910 </div>
911 );
912 }
913
914 export default A_Z_Listing_Edit;
915