PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.2
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.2
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 / class-mlsimport-standalone-render.php

class-mlsimport-standalone-render.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.1.2, at includes/standalone/class-mlsimport-standalone-render.php

530 lines 22.3 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) render orchestrator.
4 *
5 * Bridges the listings search to the front-end: runs one flat-table query, then
6 * primes the matching posts with a single WP_Query(post__in, orderby=post__in)
7 * so order is preserved and get_permalink()/thumbnail hit cache. Cards read
8 * scalars from the listings row (returned keyed by post_id) — display-only meta
9 * stays in postmeta. Markup lives in templates that consume this data.
10 *
11 * @package Mlsimport
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 require_once __DIR__ . '/class-mlsimport-standalone-listings-query.php';
19 require_once __DIR__ . '/class-mlsimport-standalone-template.php';
20 require_once __DIR__ . '/class-mlsimport-standalone-settings.php';
21 require_once __DIR__ . '/class-mlsimport-pagination.php';
22 require_once __DIR__ . '/class-mlsimport-map-clusterer.php';
23 require_once __DIR__ . '/listing-card.php';
24
25 /**
26 * Prepares ordered, cache-primed listing data for the front-end templates.
27 */
28 class Mlsimport_Standalone_Render {
29
30 /**
31 * Default page size for the full listings grid UI (the search-form + results
32 * surface). Page blocks that select a fixed set pass their own count.
33 */
34 const DEFAULT_PER_PAGE = 12;
35
36 /**
37 * Resolve the page size for the listings grid: an explicit positive limit wins,
38 * otherwise the (filterable) default. Shared by render_grid and the AJAX repaint
39 * so the first paint and every subsequent page use the same size.
40 *
41 * @param array $args Render args.
42 * @return int
43 */
44 public static function grid_limit( array $args ): int {
45 $limit = isset( $args['limit'] ) ? (int) $args['limit'] : 0;
46 if ( $limit > 0 ) {
47 return $limit;
48 }
49 // Design Settings → General → "No. of Properties per Page" is the site default;
50 // DEFAULT_PER_PAGE only applies when that option is unset or non-positive.
51 $configured = function_exists( 'mlsimport_standalone_option' ) ? (int) mlsimport_standalone_option( 'properties_per_page', 0 ) : 0;
52 $default = $configured > 0 ? $configured : self::DEFAULT_PER_PAGE;
53
54 /** Filter the default listings grid page size. @since 6.4 */
55 return max( 1, (int) apply_filters( 'mlsimport_listings_default_per_page', $default ) );
56 }
57
58 /**
59 * Resolve the grid's cards-per-row: an explicit columns arg (2/3/4) wins,
60 * otherwise 3. Emitted as the --mli-cols custom property on the grid wrapper
61 * (see render_grid). The archive template passes the site's "Cards per row"
62 * setting here; page-builder blocks pass nothing and stay at the default 3.
63 *
64 * @param array $args Render args.
65 * @return int One of 2, 3, 4.
66 */
67 public static function grid_columns( array $args ): int {
68 $columns = isset( $args['columns'] ) ? (int) $args['columns'] : 0;
69 return in_array( $columns, array( 2, 3, 4 ), true ) ? $columns : 3;
70 }
71
72 /**
73 * Options for the results toolbar's Sort select: value => label. Sourced from the
74 * settings registry's "Order by" field so the visitor-facing list and the admin's
75 * default-order list can never drift apart. The registry's 'default' becomes the
76 * empty value, i.e. "no explicit sort" — which lets the site default apply.
77 *
78 * @return array<string,string>
79 */
80 private static function sort_options(): array {
81 $registry = function_exists( 'mlsimport_standalone_field_registry' ) ? mlsimport_standalone_field_registry() : array();
82 $options = isset( $registry['order_by']['options'] ) ? (array) $registry['order_by']['options'] : array();
83
84 $result = array();
85 foreach ( $options as $value => $label ) {
86 $result[ 'default' === $value ? '' : (string) $value ] = (string) $label;
87 }
88
89 return $result;
90 }
91
92 /**
93 * The search form's visible field keys for $args, or null when every field
94 * shows (the default, and backward compatible). The listings block/shortcode
95 * pass search_fields as a comma list (or array) of the field keys to keep — an
96 * unset/empty value means "show all". search-form.php reads this to render only
97 * the enabled fields; it is pure display config and never touches the query.
98 *
99 * @param array $args Render args.
100 * @return string[]|null Enabled field keys, or null for "all".
101 */
102 public static function visible_fields( array $args ): ?array {
103 $result = null;
104 if ( isset( $args['search_fields'] ) && '' !== $args['search_fields'] && array() !== $args['search_fields'] ) {
105 // A non-empty selection lists exactly the fields to keep. Keys that match no
106 // real field (e.g. the "none" marker the block stores when every field is
107 // switched off) drop out, leaving an empty allow-list — i.e. show no fields.
108 $raw = is_array( $args['search_fields'] ) ? $args['search_fields'] : explode( ',', (string) $args['search_fields'] );
109 $result = array_values( array_filter( array_map( 'sanitize_key', array_map( 'strval', $raw ) ), 'strlen' ) );
110 }
111 /** Filter the search form's visible field keys; null shows every field. @since 6.4 */
112 $result = apply_filters( 'mlsimport_listings_visible_fields', $result, $args );
113 return is_array( $result ) ? $result : null;
114 }
115
116 /**
117 * Resolve filter args into the data a card grid renders.
118 *
119 * @param array $args Consumer filter params (see Mlsimport_Standalone_Query).
120 * @return array{posts:WP_Post[],rows:array<int,object>,total:int}
121 */
122 public static function prepare( array $args ): array {
123 /** Filter the inbound render args. @since 6.3 */
124 $args = (array) apply_filters( 'mlsimport_render_args', $args );
125 $result = Mlsimport_Standalone_Listings_Query::search( $args );
126
127 // Collect the matched post IDs (in row order) and index each row by post_id so
128 // the card template can look up its scalars without another query.
129 $ids = array();
130 $rows_by_id = array();
131 foreach ( $result['rows'] as $row ) {
132 $post_id = (int) $row->post_id;
133 $ids[] = $post_id;
134 $rows_by_id[ $post_id ] = $row;
135 }
136
137 // Prime the posts in one query, preserving the flat-table order via post__in.
138 $posts = array();
139 if ( $ids ) {
140 $query = new WP_Query(
141 array(
142 'post_type' => 'mlsimport_property',
143 'post__in' => $ids,
144 'orderby' => 'post__in',
145 'posts_per_page' => count( $ids ),
146 'no_found_rows' => true,
147 )
148 );
149 $posts = $query->posts;
150 }
151
152 $payload = array(
153 'posts' => $posts,
154 'rows' => $rows_by_id,
155 'total' => (int) $result['total'],
156 );
157
158 /** Filter the prepared listings payload. @since 6.3 */
159 return (array) apply_filters( 'mlsimport_prepared_listings', $payload, $args );
160 }
161
162 /**
163 * Render the listings grid HTML for $args by including the (theme-overridable)
164 * card template once per matching post.
165 *
166 * @param array $args Consumer filter params.
167 * @return string Concatenated card HTML, or '' when there are no matches.
168 */
169 public static function render( array $args ): string {
170 return self::render_cards( self::prepare( $args ) );
171 }
172
173 /**
174 * Render the full listings UI: search form + results grid (cards) — the
175 * droppable surface for the shortcode/block/widget. AJAX repaints the grid.
176 *
177 * @param array $args Consumer filter params.
178 * @return string
179 */
180 public static function render_grid( array $args ): string {
181 // Page the grid by default (a listings surface should never dump every row).
182 // The resolved limit is written back into $args so the search-form carries it
183 // (hidden field) and the AJAX repaint keeps the same page size.
184 $args['limit'] = self::grid_limit( $args );
185 $page = isset( $args['page'] ) ? max( 1, (int) $args['page'] ) : 1;
186 $args['page'] = $page;
187 $cols = self::grid_columns( $args );
188
189 $data = self::prepare( $args );
190 $cards = self::render_cards( $data );
191 $search_template = Mlsimport_Standalone_Template::locate( 'search-form.php' );
192 $total = (int) $data['total'];
193
194 // The form is always rendered (it carries the filters for the AJAX repaint);
195 // hide_search_form only adds a modifier the CSS uses to visually hide it, so a
196 // "results only" surface still paginates the same search without a reload.
197 $wrap_class = 'mlsimport-listings' . ( ! empty( $args['hide_search_form'] ) ? ' mlsimport-listings--no-form' : '' );
198
199 ob_start();
200 echo '<div class="' . esc_attr( $wrap_class ) . '">';
201 do_action( 'mlsimport_before_listings', $args, $total );
202 do_action( 'mlsimport_before_search_form', $args );
203 include $search_template; // Uses $args.
204 do_action( 'mlsimport_after_search_form', $args );
205 echo '<div class="mlsimport-results">';
206 // Count + sort share one toolbar row above the grid: count left, sort right.
207 // The Sort control lives HERE, not among the search filters, but still drives the
208 // same AJAX repaint — mlsimport-listings.js reads select[name="orderby"] from this
209 // toolbar (it is outside form.mlsimport-search, so FormData never sees it).
210 echo '<div class="mlsimport-results__toolbar">';
211 echo '<div class="mlsimport-results__meta">' . esc_html( $total . ' ' . _n( 'result', 'results', $total, 'mlsimport' ) ) . '</div>';
212 $visible = self::visible_fields( $args );
213 if ( null === $visible || in_array( 'sort', $visible, true ) ) {
214 // selected() on server render so the DOM value matches the preset; otherwise the
215 // select shows its first option and the next AJAX repaint silently sorts by that.
216 $orderby = isset( $args['orderby'] ) && ! is_array( $args['orderby'] ) ? (string) $args['orderby'] : '';
217 if ( '' === $orderby ) {
218 $orderby = Mlsimport_Standalone_Query::default_sort();
219 }
220 // A DIV, not a LABEL: mlsimport-property-interest.js replaces the select with
221 // a <button>, and a button inside a label swallows its own clicks to the
222 // label's activation behaviour. The label text becomes a plain span instead.
223 //
224 // The .mlsimport-interest wrapper is what that enhancer looks for. It hides
225 // the native select (only once enhanced) and builds a button + listbox from
226 // the options, so Sort gets the same dropdown as the property page's
227 // "I'm interested in" — including a styled option list, which a native
228 // select can never have. The select stays in the DOM and stays the value
229 // mlsimport-listings.js reads, so the AJAX repaint is untouched and a page
230 // with no JS still shows a working control.
231 echo '<div class="mlsimport-results__sort">';
232 echo '<span class="mlsimport-results__sort-label">' . esc_html__( 'Sort by', 'mlsimport' ) . '</span>';
233 echo '<span class="mlsimport-interest mlsimport-results__sort-control">';
234 echo '<select name="orderby">';
235 foreach ( self::sort_options() as $mli_value => $mli_label ) {
236 echo '<option value="' . esc_attr( $mli_value ) . '"' . selected( $orderby, $mli_value, false ) . '>' . esc_html( $mli_label ) . '</option>';
237 }
238 echo '</select>';
239 echo '</span>';
240 echo '</div>';
241 }
242 echo '</div>';
243 do_action( 'mlsimport_before_results', $args, $total );
244 echo '<div class="mlsimport-results__grid" style="--mli-cols:' . esc_attr( (string) $cols ) . ';">';
245 if ( '' !== $cards ) {
246 echo $cards; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- card.php escapes each field at source.
247 } else {
248 $empty = esc_html__( 'No listings match your search.', 'mlsimport' );
249 /** Filter the empty-results message. @since 6.3 */
250 echo wp_kses_post( apply_filters( 'mlsimport_no_results_message', '<p class="mlsimport-results__empty">' . $empty . '</p>', $args ) );
251 }
252 echo '</div>';
253 // Pager container — the AJAX layer repaints its inner <nav> on filter/paginate.
254 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Mlsimport_Pagination::render returns escaped markup.
255 echo '<div class="mlsimport-results__pager">' . Mlsimport_Pagination::render( $total, (int) $args['limit'], $page ) . '</div>';
256 do_action( 'mlsimport_after_results', $args, $total );
257 echo '</div></div>';
258 return (string) ob_get_clean();
259 }
260
261 /** Default cap on individual markers before the map switches to clusters. */
262 const MAP_MARKER_CAP = 400;
263
264 /**
265 * Map marker data for $args: one entry per matching listing that has
266 * coordinates. Honours every filter (incl. the lat/lng bounding box).
267 *
268 * @param array $args Consumer filter params.
269 * @return array<int,array{post_id:int,lat:float,lng:float,price:float|null,title:string,url:string,image:string,beds:string,baths:string,area:string}>
270 */
271 public static function markers( array $args ): array {
272 /** Filter the map marker set. @since 6.3 */
273 return (array) apply_filters( 'mlsimport_map_markers', self::markers_from_data( self::prepare( $args ) ), $args );
274 }
275
276 /**
277 * Map markers for an explicit, ordered set of property post IDs (the page
278 * block's hand-picked "by IDs" selection). Mirrors cards_for_posts() so a
279 * hand-picked map honours the chosen listings instead of the whole feed.
280 *
281 * @param int[] $ids Property post IDs.
282 * @return array
283 */
284 public static function markers_for_posts( array $ids ): array {
285 global $wpdb;
286
287 $ids = array_values( array_filter( array_map( 'intval', $ids ) ) );
288 if ( empty( $ids ) ) {
289 return array();
290 }
291
292 $table = $wpdb->prefix . 'mlsimport_listings';
293 $placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
294 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
295 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE post_id IN ({$placeholders})", $ids ) );
296
297 $rows_by_id = array();
298 foreach ( $rows as $row ) {
299 $rows_by_id[ (int) $row->post_id ] = $row;
300 }
301
302 $query = new WP_Query(
303 array(
304 'post_type' => 'mlsimport_property',
305 'post__in' => $ids,
306 'orderby' => 'post__in',
307 'posts_per_page' => count( $ids ),
308 'no_found_rows' => true,
309 )
310 );
311
312 return self::markers_from_data(
313 array(
314 'posts' => $query->posts,
315 'rows' => $rows_by_id,
316 )
317 );
318 }
319
320 /**
321 * Build the marker array from an already-prepared { posts, rows } set. One
322 * builder so the query, by-IDs and viewport paths all emit identical markers.
323 *
324 * @param array $data { posts: WP_Post[], rows: array<int,object> }.
325 * @return array
326 */
327 private static function markers_from_data( array $data ): array {
328 $markers = array();
329
330 // Spec formatter — mirrors mlsimport_format_amount() so the info-card
331 // beds/baths/area read the same as the single-property map, without a
332 // cross-file dependency on property-sections.php from this class.
333 $fmt = static function ( $value ) {
334 if ( null === $value || '' === $value ) {
335 return '';
336 }
337 $f = (float) $value;
338 return ( $f === (float) (int) $f ) ? number_format( $f ) : number_format( $f, 1 );
339 };
340
341 foreach ( $data['posts'] as $post ) {
342 $row = isset( $data['rows'][ $post->ID ] ) ? $data['rows'][ $post->ID ] : null;
343 if ( null === $row || null === $row->latitude || null === $row->longitude ) {
344 continue;
345 }
346
347 // Clean RESO address for the info-card heading — same source the
348 // listing cards use (mlsimport_card_view). The raw post title is an
349 // importer concatenation ("indian river 1475 ... ,Eugene,Lane,Residential")
350 // and is only the fallback when no UnparsedAddress is stored.
351 $title = trim( (string) get_post_meta( $post->ID, 'mlsimport_UnparsedAddress', true ) );
352 if ( '' === $title ) {
353 $title = get_the_title( $post->ID );
354 }
355
356 $markers[] = array(
357 'post_id' => (int) $post->ID,
358 'lat' => (float) $row->latitude,
359 'lng' => (float) $row->longitude,
360 'price' => null !== $row->price ? (float) $row->price : null,
361 'title' => $title,
362 'url' => (string) get_permalink( $post->ID ),
363 'image' => (string) ( get_the_post_thumbnail_url( $post->ID, 'medium' ) ?: '' ),
364 'beds' => $fmt( isset( $row->bedrooms ) ? $row->bedrooms : null ),
365 'baths' => $fmt( isset( $row->bathrooms ) ? $row->bathrooms : null ),
366 // Whole-number ft² (like the listing card + single-property popup); $fmt
367 // would leak a fractional area (it keeps a decimal for non-integers).
368 'area' => ( isset( $row->living_area ) && null !== $row->living_area && '' !== $row->living_area ) ? number_format_i18n( (float) $row->living_area ) : '',
369 );
370 }
371
372 return $markers;
373 }
374
375 /**
376 * Viewport map payload: given filter args (carrying the current lat/lng bbox)
377 * and the map zoom, grid-cluster every in-view listing into zoom-sized cells.
378 * A cell holding one listing returns a price pin (full card data); a cell
379 * holding several returns a counted bubble. Both arrays travel together, so
380 * dense areas collapse to bubbles while isolated listings stay pins — at every
381 * zoom, for any catalog size. The cell count is bounded by the viewport area,
382 * so the browser never receives more than a screenful of objects.
383 *
384 * @param array $args Filter args incl. lat_min/lat_max/lng_min/lng_max.
385 * @param int $zoom Leaflet zoom level (grid sizing).
386 * @return array{type:string,total:int,markers:array,clusters:array,bounds:?array}
387 */
388 public static function map_payload( array $args, int $zoom ): array {
389 /** Short-circuit the viewport map payload (live mode answers from the MLS here). @since 6.4 */
390 $pre = apply_filters( 'mlsimport_map_payload_pre', null, $args, $zoom );
391 if ( is_array( $pre ) ) {
392 return $pre;
393 }
394
395 // The map shows the whole in-view set (grid-clustered), never the grid's page
396 // size — drop any limit/page so coords and markers can't disagree on the count.
397 unset( $args['limit'], $args['page'] );
398
399 $coords = Mlsimport_Standalone_Listings_Query::coords( $args );
400 $total = count( $coords );
401
402 // The filter's OVERALL bounds (ignoring the viewport bbox), so a client that
403 // just changed the filter can refit the map to the whole result set. Computed
404 // from the args minus the bbox keys — otherwise bounds() would just echo the
405 // current viewport. Cheap (one indexed MIN/MAX) and ignored on plain panning.
406 $nogeo = $args;
407 unset( $nogeo['lat_min'], $nogeo['lat_max'], $nogeo['lng_min'], $nogeo['lng_max'] );
408 $bounds = Mlsimport_Standalone_Listings_Query::bounds( $nogeo );
409
410 // Split the grid cells: single-occupancy cells become pins (fetch full card
411 // data for just those lone listings), multi-occupancy cells become bubbles.
412 // The "Use the Pin Cluster" + "Maximum zoom" design settings decide whether a
413 // dense cell clusters at all — when off (or zoomed past the cut-over) every
414 // cell's listings render as individual price pins.
415 $cluster = mlsimport_standalone_map_cluster_enabled( $zoom );
416 $single = array();
417 $clusters = array();
418 foreach ( Mlsimport_Map_Clusterer::grid( $coords, $zoom ) as $cell ) {
419 // A crowded cell becomes a counted bubble only when clustering is on;
420 // otherwise every listing in it is drawn as its own pin.
421 if ( $cluster && $cell['count'] > 1 ) {
422 $clusters[] = array(
423 'lat' => $cell['lat'],
424 'lng' => $cell['lng'],
425 'count' => $cell['count'],
426 );
427 } else {
428 foreach ( $cell['ids'] as $id ) {
429 $single[] = (int) $id;
430 }
431 }
432 }
433
434 $markers = $single ? self::markers_for_posts( $single ) : array();
435 /** Filter the viewport map's individual (single-cell) markers. @since 6.3 */
436 $markers = (array) apply_filters( 'mlsimport_map_markers', $markers, $args );
437
438 return array(
439 'type' => $clusters ? 'clusters' : 'markers',
440 'total' => $total,
441 'markers' => $markers,
442 'clusters' => $clusters,
443 'bounds' => $bounds,
444 );
445 }
446
447 /**
448 * Render the card grid from an already-prepared data set (one query reused
449 * by both the page render and the AJAX payload).
450 *
451 * @param array $data Output of prepare().
452 * @param string $slide_class When set, each card is wrapped in a <li> with this
453 * class (e.g. 'splide__slide' for the content slider).
454 * @return string Concatenated card HTML, or '' when there are no posts.
455 */
456 public static function render_cards( array $data, string $slide_class = '' ): string {
457 if ( empty( $data['posts'] ) ) {
458 return '';
459 }
460
461 // One resolver picks the card design (v1/v2/v3) from settings, so every
462 // listing grid that reaches render_cards is styled consistently.
463 $card_name = function_exists( 'mlsimport_standalone_card_template' ) ? mlsimport_standalone_card_template() : 'card.php';
464 $template = Mlsimport_Standalone_Template::locate( $card_name );
465
466 $out = '';
467 foreach ( $data['posts'] as $post ) {
468 $row = isset( $data['rows'][ $post->ID ] ) ? $data['rows'][ $post->ID ] : null;
469 // Buffer each card individually so the before/after action slots and
470 // the per-card filter compose cleanly into one returned string.
471 ob_start();
472 do_action( 'mlsimport_before_listing_card', $post, $row );
473 include $template;
474 do_action( 'mlsimport_after_listing_card', $post, $row );
475 $card_html = (string) ob_get_clean();
476 /** Filter one card's HTML. @since 6.3 */
477 $card_html = (string) apply_filters( 'mlsimport_listing_card_html', $card_html, $post, $row );
478 $out .= '' !== $slide_class ? '<li class="' . esc_attr( $slide_class ) . '">' . $card_html . '</li>' : $card_html;
479 }
480 return $out;
481 }
482
483 /**
484 * Render cards for an explicit, ordered set of property post IDs (e.g. an
485 * agent's listings, linked by meta rather than the flat search). Fetches each
486 * post's listings row so the card shows its scalars.
487 *
488 * @param int[] $ids Property post IDs, in display order.
489 * @param string $slide_class When set, each card is wrapped in a <li> with this class.
490 * @return string
491 */
492 public static function cards_for_posts( array $ids, string $slide_class = '' ): string {
493 global $wpdb;
494
495 $ids = array_values( array_filter( array_map( 'intval', $ids ) ) );
496 if ( empty( $ids ) ) {
497 return '';
498 }
499
500 $table = $wpdb->prefix . 'mlsimport_listings';
501 $placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
502 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
503 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE post_id IN ({$placeholders})", $ids ) );
504
505 $rows_by_id = array();
506 foreach ( $rows as $row ) {
507 $rows_by_id[ (int) $row->post_id ] = $row;
508 }
509
510 $query = new WP_Query(
511 array(
512 'post_type' => 'mlsimport_property',
513 'post__in' => $ids,
514 'orderby' => 'post__in',
515 'posts_per_page' => count( $ids ),
516 'no_found_rows' => true,
517 )
518 );
519
520 return self::render_cards(
521 array(
522 'posts' => $query->posts,
523 'rows' => $rows_by_id,
524 'total' => count( $query->posts ),
525 ),
526 $slide_class
527 );
528 }
529 }
530