| 1 |
<?php |
| 2 |
/** |
| 3 |
* Standalone (theme_id 990) page-block manifest + dispatcher. |
| 4 |
* |
| 5 |
* Page blocks are the building blocks a site builder drops onto a normal page |
| 6 |
* (home, landing, agent) — a property list, a slider, a map, a search form, a |
| 7 |
* contact form, a featured property. They are the page-building counterpart to |
| 8 |
* Property sections (ADR-0005): same one-function-wrapped-per-builder pattern, |
| 9 |
* but driven by their own args (a query/config), never the single-property loop. |
| 10 |
* |
| 11 |
* One registry maps a block slug to its metadata (label, render callback, arg |
| 12 |
* schema, required assets). Every builder adapter (Shortcode, Gutenberg, |
| 13 |
* Elementor, future builders) loops this manifest, so adding a block is one |
| 14 |
* register call + one render fn and all builders pick it up. The dispatcher is the |
| 15 |
* single place that resolves the render fn, merges arg defaults, enqueues the |
| 16 |
* block's assets and fires the WooCommerce-style hooks. See docs/adr/0007 and |
| 17 |
* CONTEXT.md (Page block). |
| 18 |
* |
| 19 |
* @package Mlsimport |
| 20 |
*/ |
| 21 |
|
| 22 |
if ( ! defined( 'ABSPATH' ) ) { |
| 23 |
exit; |
| 24 |
} |
| 25 |
|
| 26 |
require_once __DIR__ . '/page-blocks.php'; |
| 27 |
require_once __DIR__ . '/page-blocks-categories.php'; |
| 28 |
require_once __DIR__ . '/page-blocks-agents.php'; |
| 29 |
require_once __DIR__ . '/class-mlsimport-property-section-assets.php'; |
| 30 |
|
| 31 |
/** |
| 32 |
* Register a page block in the manifest. |
| 33 |
* |
| 34 |
* @param string $slug Block slug (e.g. 'item_list'). |
| 35 |
* @param array $def { |
| 36 |
* @type string $label Editor-facing label. |
| 37 |
* @type callable $render Render fn: ( array $args ) => string. |
| 38 |
* @type array $args Arg schema: key => { type, label, default, options }. |
| 39 |
* @type array $assets Optional asset handles to enqueue when rendered. |
| 40 |
* } |
| 41 |
* @return void |
| 42 |
*/ |
| 43 |
function mlsimport_register_page_block( string $slug, array $def ): void { |
| 44 |
// Grab the shared registry by reference so the write persists. |
| 45 |
$registry =& mlsimport_page_block_registry(); |
| 46 |
|
| 47 |
// Store the block under its slug, filling each key with a safe default. |
| 48 |
$registry[ $slug ] = array( |
| 49 |
'label' => isset( $def['label'] ) ? (string) $def['label'] : $slug, |
| 50 |
'render' => isset( $def['render'] ) ? $def['render'] : '', |
| 51 |
'args' => isset( $def['args'] ) ? (array) $def['args'] : array(), |
| 52 |
'assets' => isset( $def['assets'] ) ? (array) $def['assets'] : array(), |
| 53 |
); |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* The registered page blocks, slug => definition. |
| 58 |
* |
| 59 |
* @return array |
| 60 |
*/ |
| 61 |
function mlsimport_get_page_blocks(): array { |
| 62 |
// Ensure the built-ins are registered before exposing the manifest. |
| 63 |
mlsimport_register_builtin_page_blocks(); |
| 64 |
/** Filter the page-block manifest: add/remove/reorder blocks globally. @since 6.4 */ |
| 65 |
return (array) apply_filters( 'mlsimport_page_blocks', mlsimport_page_block_registry() ); |
| 66 |
} |
| 67 |
|
| 68 |
/** |
| 69 |
* The default args for a block, taken from its schema. |
| 70 |
* |
| 71 |
* @param string $slug Block slug. |
| 72 |
* @return array key => default. |
| 73 |
*/ |
| 74 |
function mlsimport_page_block_defaults( string $slug ): array { |
| 75 |
// Unknown slug → no defaults. |
| 76 |
$registry = mlsimport_page_block_registry(); |
| 77 |
if ( ! isset( $registry[ $slug ] ) ) { |
| 78 |
return array(); |
| 79 |
} |
| 80 |
// Collapse the arg schema to a flat key => default map. |
| 81 |
$defaults = array(); |
| 82 |
foreach ( $registry[ $slug ]['args'] as $key => $schema ) { |
| 83 |
$defaults[ $key ] = isset( $schema['default'] ) ? $schema['default'] : ''; |
| 84 |
} |
| 85 |
return $defaults; |
| 86 |
} |
| 87 |
|
| 88 |
/** |
| 89 |
* Render a registered page block by slug (the one dispatch point for all builders). |
| 90 |
* |
| 91 |
* @param string $slug Block slug. |
| 92 |
* @param array $args Caller args (override the schema defaults). |
| 93 |
* @return string HTML, or '' for an unknown slug / uncallable render fn. |
| 94 |
*/ |
| 95 |
function mlsimport_render_page_block( string $slug, array $args = array() ): string { |
| 96 |
// Ensure built-ins exist, then resolve the block's definition. |
| 97 |
mlsimport_register_builtin_page_blocks(); |
| 98 |
$registry = mlsimport_page_block_registry(); |
| 99 |
// Unknown slug or non-callable render fn → nothing to render. |
| 100 |
if ( ! isset( $registry[ $slug ] ) || ! is_callable( $registry[ $slug ]['render'] ) ) { |
| 101 |
return ''; |
| 102 |
} |
| 103 |
|
| 104 |
// Schema defaults first; caller args win. |
| 105 |
$args = array_merge( mlsimport_page_block_defaults( $slug ), $args ); |
| 106 |
/** Filter the resolved block args before render. @since 6.4 */ |
| 107 |
$args = (array) apply_filters( 'mlsimport_page_block_args', $args, $slug ); |
| 108 |
|
| 109 |
// Action slot before the block (buffered so the function returns one string). |
| 110 |
ob_start(); |
| 111 |
/** Inject markup before any page block; $slug identifies which. @since 6.4 */ |
| 112 |
do_action( 'mlsimport_before_page_block', $slug, $args ); |
| 113 |
/** Inject markup before a page block. Dynamic by slug. @since 6.4 */ |
| 114 |
do_action( "mlsimport_before_page_block_{$slug}", $args ); |
| 115 |
$before = (string) ob_get_clean(); |
| 116 |
|
| 117 |
// Call the block's render fn to produce its core markup. |
| 118 |
$html = (string) call_user_func( $registry[ $slug ]['render'], $args ); |
| 119 |
|
| 120 |
// Data-driven: a block's assets load only when it actually renders output. |
| 121 |
if ( '' !== $html ) { |
| 122 |
Mlsimport_Property_Section_Assets::enqueue( $registry[ $slug ]['assets'] ); |
| 123 |
} |
| 124 |
|
| 125 |
/** Filter a single block's markup. Dynamic by slug. @since 6.4 */ |
| 126 |
$html = apply_filters( "mlsimport_page_block_{$slug}_html", $html, $args ); |
| 127 |
/** Filter any block's markup; $slug identifies which. @since 6.4 */ |
| 128 |
$html = apply_filters( 'mlsimport_page_block_html', $html, $slug, $args ); |
| 129 |
|
| 130 |
// Action slot after the block. |
| 131 |
ob_start(); |
| 132 |
/** Inject markup after a page block. Dynamic by slug. @since 6.4 */ |
| 133 |
do_action( "mlsimport_after_page_block_{$slug}", $args ); |
| 134 |
/** Inject markup after any page block; $slug identifies which. @since 6.4 */ |
| 135 |
do_action( 'mlsimport_after_page_block', $slug, $args ); |
| 136 |
$after = (string) ob_get_clean(); |
| 137 |
|
| 138 |
// Concatenate the buffered before/after slots around the block markup. |
| 139 |
return $before . $html . $after; |
| 140 |
} |
| 141 |
|
| 142 |
/** |
| 143 |
* Register the plugin's built-in page blocks. Idempotent. |
| 144 |
* |
| 145 |
* @return void |
| 146 |
*/ |
| 147 |
function mlsimport_register_builtin_page_blocks(): void { |
| 148 |
// Idempotence: register the built-ins at most once per request. |
| 149 |
static $done = false; |
| 150 |
if ( $done ) { |
| 151 |
return; |
| 152 |
} |
| 153 |
$done = true; |
| 154 |
|
| 155 |
// Asset-handle bundles reused across several block definitions below. |
| 156 |
$slider = array( 'mlsimport-splide', 'mlsimport-property-slider', 'mlsimport-glightbox', 'mlsimport-property-lightbox' ); |
| 157 |
$lead = array( 'mlsimport-property-lead' ); |
| 158 |
|
| 159 |
// Search field catalog (key => label) for the search-form repeater's field |
| 160 |
// picker: every taxonomy, then every fast-table column except the geo coords. |
| 161 |
require_once __DIR__ . '/class-mlsimport-page-block-search-fields.php'; |
| 162 |
$catalog = Mlsimport_Page_Block_Search_Fields::labels(); |
| 163 |
// The range-slider fields, which are the only rows whose slider bounds can be |
| 164 |
// overridden — used as the editor condition on the min/max row controls. |
| 165 |
$range_fields = Mlsimport_Page_Block_Search_Fields::range_fields(); |
| 166 |
|
| 167 |
// A field's width is its share of a 12-column row, shared verbatim by the search |
| 168 |
// and contact form repeaters so a site builder learns one vocabulary. |
| 169 |
$width_field = array( |
| 170 |
'type' => 'select', |
| 171 |
'label' => __( 'Width', 'mlsimport' ), |
| 172 |
'default' => '', |
| 173 |
// Labelled as fractions of the row — shorter to scan in the inspector than |
| 174 |
// words, and it reads as a proportion, which is what the value actually is. |
| 175 |
// The stored values are unchanged, so existing pages keep their widths. |
| 176 |
'options' => array( |
| 177 |
'' => __( '1/1 (full row)', 'mlsimport' ), |
| 178 |
'two_thirds' => __( '2/3', 'mlsimport' ), |
| 179 |
'half' => __( '1/2', 'mlsimport' ), |
| 180 |
'third' => __( '1/3', 'mlsimport' ), |
| 181 |
'quarter' => __( '1/4', 'mlsimport' ), |
| 182 |
), |
| 183 |
); |
| 184 |
|
| 185 |
// The size steps a form's buttons and fields step through, shared by the search and |
| 186 |
// contact forms so "medium" means the same thing on both. |
| 187 |
$size_options = array( |
| 188 |
'small' => __( 'Small', 'mlsimport' ), |
| 189 |
'medium' => __( 'Medium', 'mlsimport' ), |
| 190 |
'large' => __( 'Large', 'mlsimport' ), |
| 191 |
); |
| 192 |
|
| 193 |
// Repeater row schema for the search form (field key, optional label/placeholder, |
| 194 |
// and the row's width). |
| 195 |
$search_rows = array( |
| 196 |
'type' => 'repeater', |
| 197 |
'label' => __( 'Search fields', 'mlsimport' ), |
| 198 |
'default' => array( |
| 199 |
array( 'field' => 'location', 'label' => __( 'Location', 'mlsimport' ), 'placeholder' => __( 'City, area, county or ZIP', 'mlsimport' ), 'width' => 'half' ), |
| 200 |
array( 'field' => 'property_type', 'label' => __( 'Type', 'mlsimport' ), 'placeholder' => '', 'width' => 'half' ), |
| 201 |
array( 'field' => 'price', 'label' => __( 'Price', 'mlsimport' ), 'placeholder' => '', 'width' => 'third' ), |
| 202 |
array( 'field' => 'beds_baths', 'label' => __( 'Beds & Baths', 'mlsimport' ), 'placeholder' => '', 'width' => 'third' ), |
| 203 |
), |
| 204 |
'fields' => array( |
| 205 |
'field' => array( 'type' => 'select', 'label' => __( 'Field', 'mlsimport' ), 'default' => 'city', 'options' => $catalog ), |
| 206 |
'label' => array( 'type' => 'text', 'label' => __( 'Label', 'mlsimport' ), 'default' => '' ), |
| 207 |
'placeholder' => array( 'type' => 'text', 'label' => __( 'Placeholder (shown when labels are hidden)', 'mlsimport' ), 'default' => '' ), |
| 208 |
// Slider bounds, for the range fields only (price, living area, lot, year). |
| 209 |
// Left empty each field keeps the bounds computed from the live data; set, |
| 210 |
// they pin the slider to a chosen window. One pair covers every range field |
| 211 |
// rather than a price-only special case. |
| 212 |
// Text, not number: the editors coerce an emptied number control to 0, which |
| 213 |
// would read as a real bound of zero instead of "unset". |
| 214 |
'min_value' => array( |
| 215 |
'type' => 'text', |
| 216 |
'label' => __( 'Slider minimum', 'mlsimport' ), |
| 217 |
'default' => '', |
| 218 |
'condition' => array( 'field' => $range_fields ), |
| 219 |
), |
| 220 |
'max_value' => array( |
| 221 |
'type' => 'text', |
| 222 |
'label' => __( 'Slider maximum', 'mlsimport' ), |
| 223 |
'default' => '', |
| 224 |
'condition' => array( 'field' => $range_fields ), |
| 225 |
), |
| 226 |
'width' => $width_field, |
| 227 |
), |
| 228 |
); |
| 229 |
|
| 230 |
// Repeater row schema for the contact form (per-field label/name/type/etc.). |
| 231 |
// A field's width is its share of the row: two halves sit side by side, three |
| 232 |
// thirds make a three-up row, and the default (full) keeps a field on its own. |
| 233 |
$contact_rows = array( |
| 234 |
'type' => 'repeater', |
| 235 |
'label' => __( 'Form fields', 'mlsimport' ), |
| 236 |
'default' => array( |
| 237 |
array( 'name' => 'name', 'type' => 'text', 'label' => __( 'Name', 'mlsimport' ), 'placeholder' => '', 'options' => '', 'required' => 'yes', 'width' => 'half' ), |
| 238 |
array( 'name' => 'email', 'type' => 'email', 'label' => __( 'Email', 'mlsimport' ), 'placeholder' => '', 'options' => '', 'required' => 'yes', 'width' => 'half' ), |
| 239 |
array( 'name' => 'phone', 'type' => 'tel', 'label' => __( 'Phone', 'mlsimport' ), 'placeholder' => '', 'options' => '', 'required' => '', 'width' => '' ), |
| 240 |
array( 'name' => 'message', 'type' => 'textarea', 'label' => __( 'Message', 'mlsimport' ), 'placeholder' => '', 'options' => '', 'required' => '', 'width' => '' ), |
| 241 |
), |
| 242 |
'fields' => array( |
| 243 |
'label' => array( 'type' => 'text', 'label' => __( 'Label', 'mlsimport' ), 'default' => '' ), |
| 244 |
'name' => array( 'type' => 'text', 'label' => __( 'Field name', 'mlsimport' ), 'default' => '' ), |
| 245 |
'type' => array( |
| 246 |
'type' => 'select', |
| 247 |
'label' => __( 'Type', 'mlsimport' ), |
| 248 |
'default' => 'text', |
| 249 |
'options' => array( |
| 250 |
'text' => __( 'Text', 'mlsimport' ), |
| 251 |
'email' => __( 'Email', 'mlsimport' ), |
| 252 |
'tel' => __( 'Phone', 'mlsimport' ), |
| 253 |
'textarea' => __( 'Message', 'mlsimport' ), |
| 254 |
'select' => __( 'Dropdown', 'mlsimport' ), |
| 255 |
'radio' => __( 'Radio buttons', 'mlsimport' ), |
| 256 |
'checkbox' => __( 'Checkbox', 'mlsimport' ), |
| 257 |
), |
| 258 |
), |
| 259 |
'options' => array( 'type' => 'text', 'label' => __( 'Choices, comma separated (dropdown and radio only)', 'mlsimport' ), 'default' => '' ), |
| 260 |
'placeholder' => array( 'type' => 'text', 'label' => __( 'Placeholder', 'mlsimport' ), 'default' => '' ), |
| 261 |
'required' => array( 'type' => 'toggle', 'label' => __( 'Required', 'mlsimport' ), 'default' => '' ), |
| 262 |
'width' => $width_field, |
| 263 |
), |
| 264 |
); |
| 265 |
|
| 266 |
// Shared selection controls (taxonomy + sort + count, or explicit ids). |
| 267 |
$sort_options = array( |
| 268 |
'' => __( 'Default', 'mlsimport' ), |
| 269 |
'newest' => __( 'Newest', 'mlsimport' ), |
| 270 |
'price_low' => __( 'Price (low to high)', 'mlsimport' ), |
| 271 |
'price_high' => __( 'Price (high to low)', 'mlsimport' ), |
| 272 |
); |
| 273 |
$selection = array( |
| 274 |
'city' => array( 'type' => 'text', 'label' => __( 'City', 'mlsimport' ), 'default' => '' ), |
| 275 |
'property_type' => array( 'type' => 'text', 'label' => __( 'Property type', 'mlsimport' ), 'default' => '' ), |
| 276 |
'status' => array( 'type' => 'text', 'label' => __( 'Status', 'mlsimport' ), 'default' => '' ), |
| 277 |
'features' => array( 'type' => 'text', 'label' => __( 'Features (comma list)', 'mlsimport' ), 'default' => '' ), |
| 278 |
'sort' => array( 'type' => 'select', 'label' => __( 'Sort', 'mlsimport' ), 'default' => 'newest', 'options' => $sort_options ), |
| 279 |
'count' => array( 'type' => 'number', 'label' => __( 'How many', 'mlsimport' ), 'default' => 6 ), |
| 280 |
'ids' => array( 'type' => 'text', 'label' => __( 'Explicit IDs (comma list)', 'mlsimport' ), 'default' => '' ), |
| 281 |
); |
| 282 |
$ids_only = array( |
| 283 |
'ids' => array( 'type' => 'textarea', 'label' => __( 'Property WordPress Post ID (comma list)', 'mlsimport' ), 'default' => '' ), |
| 284 |
'count' => array( 'type' => 'number', 'label' => __( 'Per page', 'mlsimport' ), 'default' => 12 ), |
| 285 |
); |
| 286 |
|
| 287 |
// Half Map initial filter: every listings filter key as an optional text |
| 288 |
// preset — the exact set the standalone listings block exposes — so a builder |
| 289 |
// can pre-constrain the block (a "Miami condos" landing page). 'page' is |
| 290 |
// runtime paging, not a preset, so it is skipped. |
| 291 |
$listings_presets = array(); |
| 292 |
foreach ( Mlsimport_Standalone_Shortcodes::filter_keys() as $fkey ) { |
| 293 |
if ( 'page' === $fkey ) { |
| 294 |
continue; |
| 295 |
} |
| 296 |
$listings_presets[ $fkey ] = array( |
| 297 |
'type' => 'text', |
| 298 |
'label' => ucwords( str_replace( '_', ' ', $fkey ) ), |
| 299 |
'default' => '', |
| 300 |
); |
| 301 |
} |
| 302 |
// Agent is an agent post ID, not free text — the same control the List Items per |
| 303 |
// Agent block exposes, so a list/slider can be narrowed to one agent's listings. |
| 304 |
$listings_presets['agent'] = array( 'type' => 'number', 'label' => __( 'Agent', 'mlsimport' ), 'default' => 0 ); |
| 305 |
// Search fields per row — the search bar lays its fields out as this many columns |
| 306 |
// (shared by the Search Form, MLS Listings and Half Map widgets). 3–6 only. |
| 307 |
$fields_per_row_options = array( '3' => '3', '4' => '4', '5' => '5', '6' => '6' ); |
| 308 |
|
| 309 |
// Half Map = the shared initial-filter presets + search-bar display config + map layout. |
| 310 |
$half_map_args = $listings_presets; |
| 311 |
$half_map_args['search_fields'] = array( 'type' => 'text', 'label' => __( 'Search fields (comma list; empty = all)', 'mlsimport' ), 'default' => '' ); |
| 312 |
$half_map_args['fields_per_row'] = array( 'type' => 'select', 'label' => __( 'Search fields per row', 'mlsimport' ), 'default' => '3', 'options' => $fields_per_row_options ); |
| 313 |
$half_map_args['map_side'] = array( |
| 314 |
'type' => 'select', |
| 315 |
'label' => __( 'Map side', 'mlsimport' ), |
| 316 |
'default' => 'right', |
| 317 |
'options' => array( 'right' => __( 'Right', 'mlsimport' ), 'left' => __( 'Left', 'mlsimport' ) ), |
| 318 |
); |
| 319 |
$half_map_args['height'] = array( 'type' => 'text', 'label' => __( 'Height (CSS, e.g. 100vh)', 'mlsimport' ), 'default' => '100vh' ); |
| 320 |
|
| 321 |
// Property List = the same listings + initial filter + search bar as the Half |
| 322 |
// Map, shown above the grid and filtering it via AJAX, but no map pane. Same |
| 323 |
// manifest controls half_map exposes (presets + search_fields + fields_per_row) |
| 324 |
// plus a per-page size and a show/hide toggle for the bar (mirrors Search Results). |
| 325 |
$item_list_args = $listings_presets; |
| 326 |
// Sizing on this block is PAGINATION: "Per page" is the page size and the pager |
| 327 |
// governs the rest. The raw `limit` preset is dropped because render_grid lets a |
| 328 |
// preset limit win over count (see mlsimport_page_block_item_list), which would be |
| 329 |
// a second, hidden sizing control that silently defeats the pager. The slider drops |
| 330 |
// the same key for the mirror-image reason: it caps, and has no pager at all. |
| 331 |
unset( $item_list_args['limit'] ); |
| 332 |
// Geo bounds and freehand polygon are outputs of the map's draw tools, not values a |
| 333 |
// builder sets by hand — and this block has no map pane — so drop them from the inspector. |
| 334 |
unset( $item_list_args['lat_min'], $item_list_args['lat_max'], $item_list_args['lng_min'], $item_list_args['lng_max'], $item_list_args['polygon'] ); |
| 335 |
$item_list_args['count'] = array( 'type' => 'number', 'label' => __( 'Per page', 'mlsimport' ), 'default' => 12 ); |
| 336 |
$item_list_args['show_filter_bar'] = array( 'type' => 'toggle', 'label' => __( 'Show filter bar', 'mlsimport' ), 'default' => '1' ); |
| 337 |
$item_list_args['fields_per_row'] = array( 'type' => 'select', 'label' => __( 'Search fields per row', 'mlsimport' ), 'default' => '4', 'options' => $fields_per_row_options ); |
| 338 |
$item_list_args['search_fields'] = array( 'type' => 'text', 'label' => __( 'Search fields (comma list; empty = all)', 'mlsimport' ), 'default' => '' ); |
| 339 |
|
| 340 |
// Content Slider = friendly How-many + Order + explicit IDs, then the SAME initial- |
| 341 |
// filter presets the Half Map exposes (so city/type/status/features/price/beds/… |
| 342 |
// live in one place). The raw orderby/order/limit preset keys are dropped: the |
| 343 |
// slider's own Sort + How-many controls replace them (Selection maps them back). |
| 344 |
$slider_presets = $listings_presets; |
| 345 |
unset( $slider_presets['orderby'], $slider_presets['order'], $slider_presets['limit'] ); |
| 346 |
// Geo bounds and freehand polygon are outputs of the map's draw tools, not values a |
| 347 |
// builder sets by hand — and the slider has no map pane — so drop them from the |
| 348 |
// inspector, exactly as the Property List block does. |
| 349 |
unset( $slider_presets['lat_min'], $slider_presets['lat_max'], $slider_presets['lng_min'], $slider_presets['lng_max'], $slider_presets['polygon'] ); |
| 350 |
$slider_args = array( |
| 351 |
'count' => $selection['count'], |
| 352 |
'sort' => $selection['sort'], |
| 353 |
'ids' => $selection['ids'], |
| 354 |
) + $slider_presets; |
| 355 |
|
| 356 |
// Map with Listings = ONLY the shared initial-filter presets (city/type/status/price/ |
| 357 |
// beds/…) — the SAME rich pickers the Content Slider and Gutenberg map expose. A map |
| 358 |
// plots every match at its coordinates, so it carries no page size (the query map never |
| 359 |
// honours limit), no order (pins have no sequence) and no explicit-ID surface. This is |
| 360 |
// the exact key set the Elementor widget builds, so render_settings forwards the SELECT2 |
| 361 |
// term-picker (autocomplete) values and nothing else. Was a flat $selection (raw text |
| 362 |
// City/Type/Status/Features), which is why the Elementor widget showed plain text boxes. |
| 363 |
$map_args = $slider_presets; |
| 364 |
|
| 365 |
// Category widgets (Category Slider + Display Categories): one term multi-select |
| 366 |
// PER plugin taxonomy (Cities, Property Types, Areas, County, …). The operator |
| 367 |
// picks the exact terms to show as image tiles; picks from several taxonomies |
| 368 |
// combine, and the widget shows EXACTLY those terms (pick 2 → 2 tiles). Nothing |
| 369 |
// picked → nothing rendered. Each picker's option list — the taxonomy's terms — |
| 370 |
// is resolved lazily by the block-editor / Elementor adapters, never on the front |
| 371 |
// end (see mlsimport_category_term_options()). show-count is display-only (the |
| 372 |
// per-tile "N listings" tagline), not a tile count. |
| 373 |
$cat_taxonomies = class_exists( 'Mlsimport_Standalone_Cpt' ) ? Mlsimport_Standalone_Cpt::taxonomy_labels() : array( 'mlsimport_city' => __( 'Cities', 'mlsimport' ) ); |
| 374 |
$per_row_1_6 = array( '1' => '1', '2' => '2', '3' => '3', '4' => '4', '6' => '6' ); |
| 375 |
|
| 376 |
$cat_selection = array(); |
| 377 |
foreach ( $cat_taxonomies as $tax_slug => $tax_label ) { |
| 378 |
$cat_selection[ $tax_slug ] = array( |
| 379 |
'type' => 'multiselect', |
| 380 |
'label' => $tax_label, |
| 381 |
'default' => '', |
| 382 |
'taxonomy' => $tax_slug, |
| 383 |
); |
| 384 |
} |
| 385 |
$cat_selection += array( |
| 386 |
'show_count' => array( 'type' => 'toggle', 'label' => __( 'Show listing count', 'mlsimport' ), 'default' => '1' ), |
| 387 |
); |
| 388 |
|
| 389 |
// The built-in manifest: slug => { label, render fn, arg schema, assets }. |
| 390 |
$blocks = array( |
| 391 |
'category_slider' => array( |
| 392 |
'label' => __( 'Category Slider', 'mlsimport' ), |
| 393 |
'render' => 'mlsimport_page_block_category_slider', |
| 394 |
'args' => $cat_selection + array( |
| 395 |
'design' => array( 'type' => 'select', 'label' => __( 'Design type', 'mlsimport' ), 'default' => '1', 'options' => array( '1' => __( 'Design 1', 'mlsimport' ), '2' => __( 'Design 2', 'mlsimport' ), '3' => __( 'Design 3', 'mlsimport' ) ) ), |
| 396 |
'per_row' => array( 'type' => 'select', 'label' => __( 'Items per row', 'mlsimport' ), 'default' => '3', 'options' => $per_row_1_6 ), |
| 397 |
'item_height' => array( 'type' => 'text', 'label' => __( 'Item height (CSS, e.g. 260px)', 'mlsimport' ), 'default' => '260px' ), |
| 398 |
'gap' => array( 'type' => 'number', 'label' => __( 'Gap (px)', 'mlsimport' ), 'default' => 16 ), |
| 399 |
'border_radius' => array( 'type' => 'number', 'label' => __( 'Border radius (px)', 'mlsimport' ), 'default' => 8 ), |
| 400 |
'text_padding' => array( 'type' => 'number', 'label' => __( 'Interior text padding (px)', 'mlsimport' ), 'default' => 16 ), |
| 401 |
'title_margin' => array( 'type' => 'number', 'label' => __( 'Title bottom margin (px)', 'mlsimport' ), 'default' => 4 ), |
| 402 |
'tagline_margin' => array( 'type' => 'number', 'label' => __( 'Tagline bottom margin (px)', 'mlsimport' ), 'default' => 0 ), |
| 403 |
'title_size' => array( 'type' => 'number', 'label' => __( 'Title font size (px)', 'mlsimport' ), 'default' => 18 ), |
| 404 |
'title_color' => array( 'type' => 'text', 'label' => __( 'Title color (CSS)', 'mlsimport' ), 'default' => '#ffffff' ), |
| 405 |
), |
| 406 |
'assets' => $slider, |
| 407 |
), |
| 408 |
'category_list' => array( |
| 409 |
'label' => __( 'Display Categories', 'mlsimport' ), |
| 410 |
'render' => 'mlsimport_page_block_category_list', |
| 411 |
'args' => $cat_selection + array( |
| 412 |
'design' => array( 'type' => 'select', 'label' => __( 'Design type', 'mlsimport' ), 'default' => '1', 'options' => array( '1' => __( 'Design 1', 'mlsimport' ), '2' => __( 'Design 2', 'mlsimport' ), '3' => __( 'Design 3', 'mlsimport' ) ) ), |
| 413 |
'per_row' => array( 'type' => 'select', 'label' => __( 'Items per row', 'mlsimport' ), 'default' => '4', 'options' => $per_row_1_6 ), |
| 414 |
'display_grid' => array( 'type' => 'toggle', 'label' => __( 'Display as auto grid', 'mlsimport' ), 'default' => '' ), |
| 415 |
'min_width' => array( 'type' => 'number', 'label' => __( 'Unit minimum width (px, auto grid)', 'mlsimport' ), 'default' => 220 ), |
| 416 |
'gap' => array( 'type' => 'number', 'label' => __( 'Gap (px)', 'mlsimport' ), 'default' => 16 ), |
| 417 |
'item_height' => array( 'type' => 'text', 'label' => __( 'Item height (CSS, e.g. 220px)', 'mlsimport' ), 'default' => '220px' ), |
| 418 |
'text_padding' => array( 'type' => 'number', 'label' => __( 'Interior text padding (px)', 'mlsimport' ), 'default' => 14 ), |
| 419 |
'title_size' => array( 'type' => 'number', 'label' => __( 'Title font size (px)', 'mlsimport' ), 'default' => 16 ), |
| 420 |
'title_color' => array( 'type' => 'text', 'label' => __( 'Title color (CSS)', 'mlsimport' ), 'default' => '#ffffff' ), |
| 421 |
'border_radius' => array( 'type' => 'number', 'label' => __( 'Border radius (px)', 'mlsimport' ), 'default' => 8 ), |
| 422 |
'border_width' => array( 'type' => 'number', 'label' => __( 'Border width (px, design 3)', 'mlsimport' ), 'default' => 0 ), |
| 423 |
'border_color' => array( 'type' => 'text', 'label' => __( 'Border color (CSS, design 3)', 'mlsimport' ), 'default' => '#e0e0e0' ), |
| 424 |
), |
| 425 |
), |
| 426 |
'item_list' => array( |
| 427 |
'label' => __( 'Property List', 'mlsimport' ), |
| 428 |
'render' => 'mlsimport_page_block_item_list', |
| 429 |
'args' => $item_list_args, |
| 430 |
), |
| 431 |
'saved' => array( |
| 432 |
'label' => __( 'Saved Properties', 'mlsimport' ), |
| 433 |
'render' => 'mlsimport_page_block_saved', |
| 434 |
'args' => array( |
| 435 |
'count' => array( 'type' => 'number', 'label' => __( 'Per page', 'mlsimport' ), 'default' => 12 ), |
| 436 |
), |
| 437 |
), |
| 438 |
'property_list_filters' => array( |
| 439 |
'label' => __( 'Search Results', 'mlsimport' ), |
| 440 |
'render' => 'mlsimport_page_block_results', |
| 441 |
'args' => array( |
| 442 |
'count' => array( 'type' => 'number', 'label' => __( 'Per page', 'mlsimport' ), 'default' => 12 ), |
| 443 |
'show_filter_bar' => array( 'type' => 'toggle', 'label' => __( 'Show filter bar', 'mlsimport' ), 'default' => '1' ), |
| 444 |
'fields_per_row' => array( 'type' => 'select', 'label' => __( 'Search fields per row', 'mlsimport' ), 'default' => '4', 'options' => $fields_per_row_options ), |
| 445 |
'search_fields' => array( 'type' => 'text', 'label' => __( 'Search fields (comma list; empty = all)', 'mlsimport' ), 'default' => '' ), |
| 446 |
), |
| 447 |
), |
| 448 |
'list_by_id' => array( |
| 449 |
'label' => __( 'List Properties by ID', 'mlsimport' ), |
| 450 |
'render' => 'mlsimport_page_block_list_by_id', |
| 451 |
'args' => $ids_only, |
| 452 |
), |
| 453 |
'content_slider' => array( |
| 454 |
'label' => __( 'Property Slider', 'mlsimport' ), |
| 455 |
'render' => 'mlsimport_page_block_slider', |
| 456 |
'args' => $slider_args, |
| 457 |
'assets' => $slider, |
| 458 |
), |
| 459 |
'agents_directory' => array( |
| 460 |
'label' => __( 'Team Directory', 'mlsimport' ), |
| 461 |
'render' => 'mlsimport_page_block_agents_directory', |
| 462 |
'args' => array( |
| 463 |
'agents' => array( 'type' => 'multiselect', 'label' => __( 'Specific agents (empty = all)', 'mlsimport' ), 'default' => '', 'post_type' => 'mlsimport_agent' ), |
| 464 |
'count' => array( 'type' => 'number', 'label' => __( 'How many (0 = all)', 'mlsimport' ), 'default' => 8 ), |
| 465 |
'featured_only' => array( 'type' => 'toggle', 'label' => __( 'Verified agents only', 'mlsimport' ), 'default' => '' ), |
| 466 |
'sort' => array( |
| 467 |
'type' => 'select', |
| 468 |
'label' => __( 'Order', 'mlsimport' ), |
| 469 |
'default' => 'featured', |
| 470 |
'options' => array( |
| 471 |
'featured' => __( 'Verified first', 'mlsimport' ), |
| 472 |
'name_asc' => __( 'Alphabetical (A–Z)', 'mlsimport' ), |
| 473 |
'name_desc' => __( 'Alphabetical (Z–A)', 'mlsimport' ), |
| 474 |
), |
| 475 |
), |
| 476 |
'show_title' => array( 'type' => 'toggle', 'label' => __( 'Show job title', 'mlsimport' ), 'default' => '1' ), |
| 477 |
'show_office' => array( 'type' => 'toggle', 'label' => __( 'Show office name', 'mlsimport' ), 'default' => '1' ), |
| 478 |
'per_row' => array( 'type' => 'select', 'label' => __( 'Items per row', 'mlsimport' ), 'default' => '4', 'options' => array( '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6' ) ), |
| 479 |
'gap' => array( 'type' => 'number', 'label' => __( 'Gap (px)', 'mlsimport' ), 'default' => 16 ), |
| 480 |
'border_radius' => array( 'type' => 'number', 'label' => __( 'Border radius (px)', 'mlsimport' ), 'default' => 8 ), |
| 481 |
), |
| 482 |
), |
| 483 |
'featured_property' => array( |
| 484 |
'label' => __( 'Featured Property', 'mlsimport' ), |
| 485 |
'render' => 'mlsimport_page_block_featured', |
| 486 |
'args' => array( |
| 487 |
// Text, not number: live mode addresses the listing by ListingKey, |
| 488 |
// which can be alphanumeric; stored mode (int)-casts it anyway. |
| 489 |
'id' => array( 'type' => 'text', 'label' => __( 'Property ID', 'mlsimport' ), 'default' => '' ), |
| 490 |
'design' => array( |
| 491 |
'type' => 'select', |
| 492 |
'label' => __( 'Design', 'mlsimport' ), |
| 493 |
'default' => '1', |
| 494 |
'options' => array( '1' => '1', '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6' ), |
| 495 |
), |
| 496 |
), |
| 497 |
), |
| 498 |
'map_listings' => array( |
| 499 |
'label' => __( 'Map with Listings', 'mlsimport' ), |
| 500 |
'render' => 'mlsimport_page_block_map', |
| 501 |
'args' => $map_args, |
| 502 |
), |
| 503 |
'search_form' => array( |
| 504 |
'label' => __( 'Search Form', 'mlsimport' ), |
| 505 |
'render' => 'mlsimport_page_block_search_form', |
| 506 |
// No "fields per row" here: each row carries its own Width on the shared |
| 507 |
// 12-column grid, which is strictly more expressive. The listings grid and |
| 508 |
// half-map forms still use fields_per_row — they have no per-field settings. |
| 509 |
'args' => array( |
| 510 |
// A heading above the fields. Empty means no heading at all, so one |
| 511 |
// control does the work of a show/hide switcher plus a text box. |
| 512 |
'title' => array( 'type' => 'text', 'label' => __( 'Title (leave empty for none)', 'mlsimport' ), 'default' => '' ), |
| 513 |
'results_url' => array( 'type' => 'text', 'label' => __( 'Results page URL', 'mlsimport' ), 'default' => '' ), |
| 514 |
'hide_labels' => array( 'type' => 'toggle', 'label' => __( 'Hide labels (use placeholders)', 'mlsimport' ), 'default' => '' ), |
| 515 |
'fields' => $search_rows, |
| 516 |
'button_text' => array( 'type' => 'text', 'label' => __( 'Button text', 'mlsimport' ), 'default' => '' ), |
| 517 |
'button_color' => array( 'type' => 'color', 'label' => __( 'Button color', 'mlsimport' ), 'default' => '' ), |
| 518 |
'button_size' => array( |
| 519 |
'type' => 'select', |
| 520 |
'label' => __( 'Button size', 'mlsimport' ), |
| 521 |
'default' => 'medium', |
| 522 |
'options' => $size_options, |
| 523 |
), |
| 524 |
'button_icon' => array( 'type' => 'media', 'label' => __( 'Button icon', 'mlsimport' ), 'default' => '' ), |
| 525 |
// Defaults to a third, so out of the box the button sits INLINE as the |
| 526 |
// last cell of a row rather than claiming a full row of its own. |
| 527 |
'button_width' => array_merge( $width_field, array( 'label' => __( 'Button width', 'mlsimport' ), 'default' => 'third' ) ), |
| 528 |
), |
| 529 |
), |
| 530 |
'contact_form' => array( |
| 531 |
'label' => __( 'Contact Form', 'mlsimport' ), |
| 532 |
'render' => 'mlsimport_page_block_contact_form', |
| 533 |
// Same control vocabulary as the Search Form above — title, hide_labels and |
| 534 |
// the button_* set mean the same thing on both, so a site builder learns one |
| 535 |
// form and knows the other. |
| 536 |
'args' => array( |
| 537 |
'title' => array( 'type' => 'text', 'label' => __( 'Title (leave empty for none)', 'mlsimport' ), 'default' => '' ), |
| 538 |
'hide_labels' => array( 'type' => 'toggle', 'label' => __( 'Hide labels (use placeholders)', 'mlsimport' ), 'default' => '' ), |
| 539 |
'fields' => $contact_rows, |
| 540 |
// Field height/typography preset. Three steps rather than WpResidence's |
| 541 |
// five: the Style tab's Padding and Typography controls cover anything |
| 542 |
// between, so extra presets would just be two ways to say one thing. |
| 543 |
'input_size' => array( |
| 544 |
'type' => 'select', |
| 545 |
'label' => __( 'Field size', 'mlsimport' ), |
| 546 |
'default' => 'medium', |
| 547 |
'options' => $size_options, |
| 548 |
), |
| 549 |
// The GDPR/consent checkbox. Its wording is NOT repeated here: it comes |
| 550 |
// from the site-wide Consent label / Terms link text settings, the same |
| 551 |
// source the property lead forms use, so one edit changes every form. |
| 552 |
'show_consent' => array( 'type' => 'toggle', 'label' => __( 'Show consent checkbox', 'mlsimport' ), 'default' => '' ), |
| 553 |
'button_text' => array( 'type' => 'text', 'label' => __( 'Button text', 'mlsimport' ), 'default' => '' ), |
| 554 |
'button_color' => array( 'type' => 'color', 'label' => __( 'Button color', 'mlsimport' ), 'default' => '' ), |
| 555 |
'button_size' => array( |
| 556 |
'type' => 'select', |
| 557 |
'label' => __( 'Button size', 'mlsimport' ), |
| 558 |
'default' => 'medium', |
| 559 |
'options' => $size_options, |
| 560 |
), |
| 561 |
'button_width' => array_merge( $width_field, array( 'label' => __( 'Button width', 'mlsimport' ), 'default' => '' ) ), |
| 562 |
// Defaults to Left, not WpResidence's Justified: the button has always sat |
| 563 |
// left-aligned at its natural width here, and defaulting to Justified |
| 564 |
// would silently turn it into a full-width bar on every existing page. |
| 565 |
'button_align' => array( |
| 566 |
'type' => 'select', |
| 567 |
'label' => __( 'Button alignment', 'mlsimport' ), |
| 568 |
'default' => 'start', |
| 569 |
'options' => array( |
| 570 |
'start' => __( 'Left', 'mlsimport' ), |
| 571 |
'center' => __( 'Center', 'mlsimport' ), |
| 572 |
'end' => __( 'Right', 'mlsimport' ), |
| 573 |
'stretch' => __( 'Justified', 'mlsimport' ), |
| 574 |
), |
| 575 |
), |
| 576 |
), |
| 577 |
'assets' => $lead, |
| 578 |
), |
| 579 |
'half_map' => array( |
| 580 |
'label' => __( 'Half Map', 'mlsimport' ), |
| 581 |
'render' => 'mlsimport_page_block_half_map', |
| 582 |
'args' => $half_map_args, |
| 583 |
'assets' => array( 'mlsimport-half-map' ), |
| 584 |
), |
| 585 |
); |
| 586 |
|
| 587 |
// Push each built-in definition into the shared registry. |
| 588 |
foreach ( $blocks as $slug => $def ) { |
| 589 |
mlsimport_register_page_block( $slug, $def ); |
| 590 |
} |
| 591 |
} |
| 592 |
|
| 593 |
/** |
| 594 |
* Internal: the by-reference registry store. |
| 595 |
* |
| 596 |
* @return array |
| 597 |
*/ |
| 598 |
function &mlsimport_page_block_registry(): array { |
| 599 |
// One process-wide store, returned by reference so callers can mutate it. |
| 600 |
static $registry = array(); |
| 601 |
return $registry; |
| 602 |
} |
| 603 |
|