PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.1
7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 6.0.5 All 36 releases
mlsimport / includes / standalone / page-blocks-categories.php

page-blocks-categories.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.1.1, at includes/standalone/page-blocks-categories.php

378 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Standalone (theme_id 990) Category widgets: Category Slider + Display Categories.
4 *
5 * The WPResidence "places" widgets, ported to the MLSImport standalone taxonomies.
6 * Both widgets show a set of taxonomy TERMS (property types, listing types, cities,
7 * areas, counties, …) as tiles — each tile is the term's featured image
8 * (Mlsimport_Term_Meta::IMAGE_KEY), its name, and an optional listing count, linking
9 * to the term archive. The Slider lays the tiles in a Splide carousel; the List lays
10 * them in a grid with three design types.
11 *
12 * ONE resolver (mlsimport_category_terms) and ONE tile renderer
13 * (mlsimport_category_tile) back BOTH widgets, and — through the page-block
14 * dispatcher — the Shortcode, Gutenberg and Elementor surfaces all emit the same
15 * markup. The pure helpers (id parsing + CSS-var string) carry no WordPress and are
16 * unit-tested in isolation. See page-block-registry.php and docs/adr/0007.
17 *
18 * @package Mlsimport
19 */
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 require_once __DIR__ . '/class-mlsimport-term-meta.php';
26
27 /**
28 * Parse a comma list of term ids into a list of positive ints (order preserved,
29 * duplicates and non-positive values dropped). Pure: no WordPress.
30 *
31 * @param mixed $raw Comma string (or already an array).
32 * @return int[]
33 */
34 function mlsimport_category_parse_ids( $raw ): array {
35 if ( is_array( $raw ) ) {
36 $parts = $raw;
37 } elseif ( is_string( $raw ) ) {
38 $parts = explode( ',', $raw );
39 } else {
40 $parts = array();
41 }
42
43 $ids = array();
44 foreach ( $parts as $part ) {
45 $id = (int) trim( (string) $part );
46 if ( $id > 0 && ! in_array( $id, $ids, true ) ) {
47 $ids[] = $id;
48 }
49 }
50 return $ids;
51 }
52
53 /**
54 * Build a CSS custom-property declaration string from a map, skipping empty
55 * values. Pure (no escaping, no WordPress): the caller wraps the result in an
56 * esc_attr'd style attribute. e.g. ['--a'=>'8px','--b'=>'','--c'=>'#fff'] =>
57 * "--a:8px;--c:#fff".
58 *
59 * @param array<string,string|int> $vars Custom property => value.
60 * @return string
61 */
62 function mlsimport_category_css_vars( array $vars ): string {
63 $out = array();
64 foreach ( $vars as $prop => $value ) {
65 $value = is_string( $value ) ? trim( $value ) : $value;
66 if ( '' === $value || null === $value ) {
67 continue;
68 }
69 $out[] = $prop . ':' . $value;
70 }
71 return implode( ';', $out );
72 }
73
74 /**
75 * Resolve a Category widget's selection into an ordered list of term view-models:
76 * id, name, url, count and image_url (the term featured image). Shared by both
77 * Category widgets across all builders.
78 *
79 * The operator picks terms per taxonomy — $args carries one entry per plugin
80 * taxonomy slug (e.g. $args['mlsimport_city'] = a term-id array, or a comma string
81 * on the shortcode). The widget shows EXACTLY the picked terms — in the plugin's
82 * taxonomy order then pick order — combining picks across taxonomies. A hand-picked
83 * term shows even with no listings.
84 *
85 * With NOTHING picked (a freshly-inserted widget), it falls back to a sensible
86 * default so the tile set is useful on sight rather than blank — see
87 * mlsimport_category_default_terms(). The fallback keys on whether any pick was MADE,
88 * not on whether the picks resolved: a deliberately picked but missing/deleted term
89 * id still renders empty (the picker had an intent; honour it), only a wholly
90 * unconfigured widget seeds the default.
91 *
92 * @param array $args Block args: one key per taxonomy slug.
93 * @return array<int,array<string,mixed>>
94 */
95 function mlsimport_category_terms( array $args ): array {
96 $valid = class_exists( 'Mlsimport_Standalone_Cpt' ) ? Mlsimport_Standalone_Cpt::taxonomy_slugs() : array();
97 if ( empty( $valid ) ) {
98 return array();
99 }
100
101 $out = array();
102 $picked = false;
103 foreach ( $valid as $taxonomy ) {
104 $ids = mlsimport_category_parse_ids( $args[ $taxonomy ] ?? '' );
105 if ( empty( $ids ) ) {
106 continue;
107 }
108 $picked = true;
109 $terms = get_terms(
110 array(
111 'taxonomy' => $taxonomy,
112 'include' => $ids,
113 'orderby' => 'include',
114 'hide_empty' => false,
115 )
116 );
117 if ( ! is_wp_error( $terms ) && is_array( $terms ) ) {
118 $out = array_merge( $out, mlsimport_category_term_vms( $terms ) );
119 }
120 }
121
122 // Unconfigured widget → seed a default set so it is useful on insert.
123 if ( ! $picked ) {
124 $out = mlsimport_category_default_terms( $valid );
125 }
126
127 return $out;
128 }
129
130 /**
131 * The default tile set for an unconfigured Category widget: the first plugin
132 * taxonomy (in taxonomy_slugs() order) that has any terms, its most-populated terms
133 * first, capped so a large catalog doesn't flood the widget. This makes the Category
134 * Slider / Display Categories useful the moment they are dropped in — no manual term
135 * picking required — while a configured widget still shows exactly what was picked.
136 *
137 * @param string[] $valid Plugin taxonomy slugs, in order.
138 * @return array<int,array<string,mixed>>
139 */
140 function mlsimport_category_default_terms( array $valid ): array {
141 /** Filter how many tiles an unconfigured Category widget seeds. @since 7.1 */
142 $limit = (int) apply_filters( 'mlsimport_category_default_count', 8 );
143 if ( $limit < 1 || ! function_exists( 'get_terms' ) ) {
144 return array();
145 }
146
147 foreach ( $valid as $taxonomy ) {
148 // hide_empty:false so a brand-new catalog (terms imported before any listing
149 // is attached) still previews; orderby count so the biggest categories lead.
150 $terms = get_terms(
151 array(
152 'taxonomy' => $taxonomy,
153 'hide_empty' => false,
154 'orderby' => 'count',
155 'order' => 'DESC',
156 'number' => $limit,
157 )
158 );
159 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
160 return mlsimport_category_term_vms( $terms );
161 }
162 }
163 return array();
164 }
165
166 /**
167 * Build term view-models (id, name, url, count, image_url) from WP_Term objects.
168 * The image is the term featured image (Mlsimport_Term_Meta::IMAGE_KEY).
169 *
170 * @param array $terms WP_Term list from get_terms().
171 * @return array<int,array<string,mixed>>
172 */
173 function mlsimport_category_term_vms( array $terms ): array {
174 $out = array();
175 foreach ( $terms as $term ) {
176 if ( ! is_object( $term ) || ! isset( $term->term_id ) ) {
177 continue;
178 }
179 $image_id = (int) get_term_meta( $term->term_id, Mlsimport_Term_Meta::IMAGE_KEY, true );
180 $image_url = $image_id > 0 ? (string) wp_get_attachment_image_url( $image_id, 'large' ) : '';
181 $link = get_term_link( $term );
182 $out[] = array(
183 'id' => (int) $term->term_id,
184 'name' => (string) $term->name,
185 'url' => is_wp_error( $link ) ? '' : (string) $link,
186 'count' => (int) $term->count,
187 'image_url' => $image_url,
188 );
189 }
190 return $out;
191 }
192
193 /**
194 * The terms of a taxonomy as an id => name option map, for the Category widgets'
195 * per-taxonomy term pickers. Called ONLY by the block-editor / Elementor adapters
196 * when they build the inspector controls — never on the front end, which reads the
197 * saved picks and never needs the option list.
198 *
199 * @param string $taxonomy Taxonomy slug.
200 * @return array<string,string> term id => term name (ordered by name).
201 */
202 function mlsimport_category_term_options( string $taxonomy ): array {
203 if ( ! function_exists( 'get_terms' ) || ! taxonomy_exists( $taxonomy ) ) {
204 return array();
205 }
206 $terms = get_terms(
207 array(
208 'taxonomy' => $taxonomy,
209 'hide_empty' => false,
210 'orderby' => 'name',
211 'order' => 'ASC',
212 )
213 );
214 if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
215 return array();
216 }
217 $options = array();
218 foreach ( $terms as $term ) {
219 $options[ (string) $term->term_id ] = (string) $term->name;
220 }
221 return $options;
222 }
223
224 /**
225 * Render one term tile — the single tile markup shared by both Category widgets.
226 * The term image is the featured image; the title (and optional listing count)
227 * overlay it; the whole tile links to the term archive.
228 *
229 * @param array $vm Term view-model from mlsimport_category_terms().
230 * @param bool $show_count Append a "N listings" tagline.
231 * @return string
232 */
233 function mlsimport_category_tile( array $vm, bool $show_count ): string {
234 $url = '' !== $vm['url'] ? $vm['url'] : '#';
235 $name = (string) $vm['name'];
236
237 $img = '' !== $vm['image_url']
238 ? '<span class="mlsimport-cat-tile__img" style="background-image:url(' . esc_url( $vm['image_url'] ) . ');" role="img" aria-label="' . esc_attr( $name ) . '"></span>'
239 : '<span class="mlsimport-cat-tile__img mlsimport-cat-tile__img--empty" aria-hidden="true"></span>';
240
241 $tagline = '';
242 if ( $show_count ) {
243 $n = (int) $vm['count'];
244 /* translators: %s: number of listings in a category. */
245 $label = sprintf( _n( '%s listing', '%s listings', $n, 'mlsimport' ), number_format_i18n( $n ) );
246 $tagline = '<span class="mlsimport-cat-tile__tagline">' . esc_html( $label ) . '</span>';
247 }
248
249 return '<a class="mlsimport-cat-tile" href="' . esc_url( $url ) . '">'
250 . $img
251 . '<span class="mlsimport-cat-tile__overlay"></span>'
252 . '<span class="mlsimport-cat-tile__body">'
253 . '<span class="mlsimport-cat-tile__title">' . esc_html( $name ) . '</span>'
254 . $tagline
255 . '</span>'
256 . '</a>';
257 }
258
259 /**
260 * Shared style attribute for a Category widget wrapper — the tile look controls
261 * (height, gap, radius, padding, title/tagline margins, typography) as CSS custom
262 * properties the tile CSS reads. Only set values are emitted.
263 *
264 * @param array $args Block args.
265 * @param string[] $keys Which look controls apply to this widget.
266 * @return string ' style="…"' or ''.
267 */
268 function mlsimport_category_style_attr( array $args, array $keys ): string {
269 $px = static function ( $v ) {
270 return ( '' === (string) $v || null === $v ) ? '' : (int) $v . 'px';
271 };
272
273 $all = array(
274 '--mlsimport-cat-height' => isset( $args['item_height'] ) ? (string) $args['item_height'] : '',
275 '--mlsimport-cat-gap' => isset( $args['gap'] ) ? $px( $args['gap'] ) : '',
276 '--mlsimport-cat-radius' => isset( $args['border_radius'] ) ? $px( $args['border_radius'] ) : '',
277 '--mlsimport-cat-pad' => isset( $args['text_padding'] ) ? $px( $args['text_padding'] ) : '',
278 '--mlsimport-cat-title-mb' => isset( $args['title_margin'] ) ? $px( $args['title_margin'] ) : '',
279 '--mlsimport-cat-tagline-mb' => isset( $args['tagline_margin'] ) ? $px( $args['tagline_margin'] ) : '',
280 '--mlsimport-cat-title-size' => isset( $args['title_size'] ) ? $px( $args['title_size'] ) : '',
281 '--mlsimport-cat-title-color' => isset( $args['title_color'] ) ? (string) $args['title_color'] : '',
282 '--mlsimport-cat-cols' => isset( $args['per_row'] ) ? (string) (int) $args['per_row'] : '',
283 '--mlsimport-cat-min-width' => isset( $args['min_width'] ) ? $px( $args['min_width'] ) : '',
284 '--mlsimport-cat-border-w' => isset( $args['border_width'] ) ? $px( $args['border_width'] ) : '',
285 '--mlsimport-cat-border-c' => isset( $args['border_color'] ) ? (string) $args['border_color'] : '',
286 );
287
288 $map = array();
289 foreach ( $keys as $key ) {
290 if ( isset( $all[ $key ] ) ) {
291 $map[ $key ] = $all[ $key ];
292 }
293 }
294
295 $style = mlsimport_category_css_vars( $map );
296 return '' !== $style ? ' style="' . esc_attr( $style ) . '"' : '';
297 }
298
299 /**
300 * Category Slider — the selected taxonomy terms as a Splide carousel of image
301 * tiles. Reuses the property slider's Splide assets; data-per-page drives how many
302 * tiles show per view. Backs the [mlsimport_category_slider] shortcode, the
303 * mlsimport/category-slider block and the Elementor "Category Slider" widget.
304 *
305 * @param array $args Block args.
306 * @return string
307 */
308 function mlsimport_page_block_category_slider( array $args ): string {
309 $terms = mlsimport_category_terms( $args );
310 if ( empty( $terms ) ) {
311 return '';
312 }
313
314 $per_row = in_array( (string) ( $args['per_row'] ?? '3' ), array( '1', '2', '3', '4', '6' ), true ) ? (string) $args['per_row'] : '3';
315 $design = in_array( (string) ( $args['design'] ?? '1' ), array( '1', '2', '3' ), true ) ? (string) $args['design'] : '1';
316 $show = ! in_array( (string) ( $args['show_count'] ?? '1' ), array( '', '0', 'no', 'false' ), true );
317 $gap = ( isset( $args['gap'] ) && '' !== (string) $args['gap'] ) ? (int) $args['gap'] . 'px' : '';
318
319 $slides = '';
320 foreach ( $terms as $vm ) {
321 $slides .= '<li class="splide__slide">' . mlsimport_category_tile( $vm, $show ) . '</li>';
322 }
323
324 $gap_attr = '' !== $gap ? ' data-gap="' . esc_attr( $gap ) . '"' : '';
325
326 $style = mlsimport_category_style_attr(
327 $args,
328 array( '--mlsimport-cat-height', '--mlsimport-cat-gap', '--mlsimport-cat-radius', '--mlsimport-cat-pad', '--mlsimport-cat-title-mb', '--mlsimport-cat-tagline-mb', '--mlsimport-cat-title-size', '--mlsimport-cat-title-color' )
329 );
330
331 // The design modifier rides the outer wrapper — an ancestor of every tile — so the
332 // one design ruleset (mlsimport_category_tile markup is identical everywhere) styles
333 // the slider tiles exactly as it does the Display Categories grid tiles.
334 return '<div class="mlsimport-page-block mlsimport-page-block--category-slider mlsimport-cat--design-' . esc_attr( $design ) . '"' . $style . '>'
335 . '<div class="mlsimport-category-slider splide" data-per-page="' . esc_attr( $per_row ) . '"' . $gap_attr . ' role="group" aria-label="' . esc_attr__( 'Categories', 'mlsimport' ) . '">'
336 . '<div class="splide__track"><ul class="splide__list">'
337 . $slides
338 . '</ul></div></div></div>';
339 }
340
341 /**
342 * Display Categories — the selected taxonomy terms as a grid of image tiles, in one
343 * of three design types. display_grid switches from a fixed column count (per_row)
344 * to an auto-fill grid of a minimum unit width. Backs the
345 * [mlsimport_category_list] shortcode, the mlsimport/category-list block and the
346 * Elementor "Display Categories" widget.
347 *
348 * @param array $args Block args.
349 * @return string
350 */
351 function mlsimport_page_block_category_list( array $args ): string {
352 $terms = mlsimport_category_terms( $args );
353 if ( empty( $terms ) ) {
354 return '';
355 }
356
357 $design = in_array( (string) ( $args['design'] ?? '1' ), array( '1', '2', '3' ), true ) ? (string) $args['design'] : '1';
358 $grid = ! in_array( (string) ( $args['display_grid'] ?? '' ), array( '', '0', 'no', 'false' ), true );
359 $show = ! in_array( (string) ( $args['show_count'] ?? '1' ), array( '', '0', 'no', 'false' ), true );
360
361 $tiles = '';
362 foreach ( $terms as $vm ) {
363 $tiles .= mlsimport_category_tile( $vm, $show );
364 }
365
366 $style = mlsimport_category_style_attr(
367 $args,
368 array( '--mlsimport-cat-height', '--mlsimport-cat-gap', '--mlsimport-cat-radius', '--mlsimport-cat-pad', '--mlsimport-cat-title-size', '--mlsimport-cat-title-color', '--mlsimport-cat-cols', '--mlsimport-cat-min-width', '--mlsimport-cat-border-w', '--mlsimport-cat-border-c' )
369 );
370
371 $classes = 'mlsimport-page-block mlsimport-page-block--category-list mlsimport-cat-list mlsimport-cat--design-' . $design . ' mlsimport-cat-list--design-' . $design;
372 if ( $grid ) {
373 $classes .= ' mlsimport-cat-list--autogrid';
374 }
375
376 return '<div class="' . esc_attr( $classes ) . '"' . $style . '>' . $tiles . '</div>';
377 }
378