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 |