class-ecs-loop-custom-layout-module.php
518 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Module: Loop Custom Layout |
| 4 | * |
| 5 | * Replaces the default Loop Grid widget output with an ECS Custom Layout |
| 6 | * template. Each queried loop item is rendered in post context, then |
| 7 | * distributed into ECS Container Placeholder widgets inside the template — |
| 8 | * cycling when items > placeholders, leaving placeholders empty when |
| 9 | * items < placeholders. |
| 10 | * |
| 11 | * Requirements: |
| 12 | * - Elementor Pro ≥ 3.8 (Loop Builder module — provides the Loop Grid widget) |
| 13 | * - ECS Container Layout module (registers the ecs_custom_layout document type |
| 14 | * and the ECS Container Placeholder widget) |
| 15 | */ |
| 16 | |
| 17 | if ( ! defined( 'ABSPATH' ) ) { |
| 18 | exit; |
| 19 | } |
| 20 | |
| 21 | class ECS_Loop_Custom_Layout_Module extends ECS_Module_Base { |
| 22 | |
| 23 | /** |
| 24 | * Collected loop-grid widgets (widget_id => template_id) that have |
| 25 | * ecs_use_custom_layout enabled. Populated before each document renders, |
| 26 | * consumed in the elementor/frontend/the_content filter. |
| 27 | * |
| 28 | * @var array<string, int> |
| 29 | */ |
| 30 | private array $pending_widgets = []; |
| 31 | |
| 32 | // ── Identity ────────────────────────────────────────────────────────────── |
| 33 | |
| 34 | public function get_id(): string { |
| 35 | return 'loop_custom_layout'; |
| 36 | } |
| 37 | |
| 38 | public function get_title(): string { |
| 39 | return __( 'Loop Custom Layout', 'ele-custom-skin' ); |
| 40 | } |
| 41 | |
| 42 | public function get_description(): string { |
| 43 | return __( 'Arrange Loop Grid items inside an ECS Custom Layout template via placeholder widgets — the same injection system as Container Content Layout but powered by the Loop Grid query. Requires Elementor Pro and the Container Layout module.', 'ele-custom-skin' ); |
| 44 | } |
| 45 | |
| 46 | // ── Boot ────────────────────────────────────────────────────────────────── |
| 47 | |
| 48 | public function boot(): void { |
| 49 | // Requires Elementor Pro Loop Builder (Loop Grid widget). |
| 50 | if ( ! class_exists( '\ElementorPro\Modules\LoopBuilder\Module' ) ) { |
| 51 | return; |
| 52 | } |
| 53 | |
| 54 | // Inject the toggle + template selector at the bottom of the Layout section. |
| 55 | add_action( |
| 56 | 'elementor/element/loop-grid/section_layout/before_section_end', |
| 57 | [ $this, 'add_loop_grid_controls' ], |
| 58 | 10, 2 |
| 59 | ); |
| 60 | |
| 61 | // Before each document renders: collect loop-grid widgets with our setting. |
| 62 | // (edit-mode check is deferred to inside the callbacks to avoid calling |
| 63 | // is_edit_mode() here — doing so would trigger documents->get(), firing |
| 64 | // elementor/documents/register before EPro registers its loop-item type.) |
| 65 | add_action( 'elementor/frontend/before_get_builder_content', [ $this, 'collect_widgets' ], 10, 1 ); |
| 66 | add_filter( 'elementor/frontend/the_content', [ $this, 'inject_custom_layouts' ], 10, 1 ); |
| 67 | |
| 68 | // AJAX endpoint: render template with loop items for the editor preview. |
| 69 | add_action( 'wp_ajax_ecs_loop_custom_layout_preview', [ $this, 'ajax_loop_custom_layout_preview' ] ); |
| 70 | } |
| 71 | |
| 72 | // ── Controls ────────────────────────────────────────────────────────────── |
| 73 | |
| 74 | /** |
| 75 | * Inject a switcher + template selector at the bottom of the Layout section. |
| 76 | */ |
| 77 | public function add_loop_grid_controls( $element, $args ): void { |
| 78 | $element->add_control( 'ecs_use_custom_layout', [ |
| 79 | 'label' => esc_html__( 'Use Custom Layout', 'ele-custom-skin' ), |
| 80 | 'type' => \Elementor\Controls_Manager::SWITCHER, |
| 81 | 'return_value' => 'yes', |
| 82 | 'default' => '', |
| 83 | 'separator' => 'before', |
| 84 | ] ); |
| 85 | |
| 86 | $element->add_control( 'ecs_loop_layout_id', [ |
| 87 | 'label' => esc_html__( 'Custom Layout Template', 'ele-custom-skin' ), |
| 88 | 'type' => \Elementor\Controls_Manager::SELECT2, |
| 89 | 'options' => $this->get_custom_layout_templates(), |
| 90 | 'label_block' => true, |
| 91 | 'description' => esc_html__( 'Placeholder widgets inside the template receive loop items in order, cycling when items exceed placeholders.', 'ele-custom-skin' ), |
| 92 | 'condition' => [ 'ecs_use_custom_layout' => 'yes' ], |
| 93 | ] ); |
| 94 | } |
| 95 | |
| 96 | // ── Document-level interception ─────────────────────────────────────────── |
| 97 | |
| 98 | /** |
| 99 | * Fired before Elementor renders a document. |
| 100 | * Walks the document element tree and registers any loop-grid widgets that |
| 101 | * have ecs_use_custom_layout enabled. |
| 102 | * |
| 103 | * @param \Elementor\Core\Base\Document $document |
| 104 | */ |
| 105 | public function collect_widgets( $document ): void { |
| 106 | // Skip in the editor panel but NOT in the editor's preview iframe — |
| 107 | // is_edit_mode()=true covers both; is_preview_mode()=true only for the iframe. |
| 108 | if ( \Elementor\Plugin::$instance->editor->is_edit_mode() |
| 109 | && ! \Elementor\Plugin::$instance->preview->is_preview_mode() ) { |
| 110 | return; |
| 111 | } |
| 112 | |
| 113 | $elements_data = $document->get_elements_data(); |
| 114 | if ( empty( $elements_data ) ) { |
| 115 | return; |
| 116 | } |
| 117 | |
| 118 | $this->walk_elements( $elements_data ); |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Fired after Elementor renders a document (receives its full HTML). |
| 123 | * For each collected loop-grid widget whose ID class appears in the HTML, |
| 124 | * extract the rendered loop items and replace the widget output with the |
| 125 | * ECS Custom Layout template. |
| 126 | */ |
| 127 | public function inject_custom_layouts( string $content ): string { |
| 128 | if ( empty( $this->pending_widgets ) ) { |
| 129 | return $content; |
| 130 | } |
| 131 | |
| 132 | foreach ( $this->pending_widgets as $widget_id => $template_id ) { |
| 133 | // Quick check before the expensive extraction. |
| 134 | if ( false === strpos( $content, 'elementor-element-' . $widget_id ) ) { |
| 135 | continue; |
| 136 | } |
| 137 | |
| 138 | $widget_html = $this->extract_element_by_id( $content, $widget_id ); |
| 139 | if ( ! $widget_html ) { |
| 140 | continue; |
| 141 | } |
| 142 | |
| 143 | $items = $this->extract_loop_items( $widget_html ); |
| 144 | if ( empty( $items ) ) { |
| 145 | continue; // No loop items rendered — leave original output. |
| 146 | } |
| 147 | |
| 148 | ob_start(); |
| 149 | $this->render_with_custom_layout( $template_id, $items ); |
| 150 | $replacement = ob_get_clean(); |
| 151 | |
| 152 | $pos = strpos( $content, $widget_html ); |
| 153 | if ( false !== $pos ) { |
| 154 | $content = substr( $content, 0, $pos ) |
| 155 | . $replacement |
| 156 | . substr( $content, $pos + strlen( $widget_html ) ); |
| 157 | } |
| 158 | |
| 159 | unset( $this->pending_widgets[ $widget_id ] ); |
| 160 | } |
| 161 | |
| 162 | return $content; |
| 163 | } |
| 164 | |
| 165 | // ── Injection ───────────────────────────────────────────────────────────── |
| 166 | |
| 167 | /** |
| 168 | * Render the ECS Custom Layout template with loop items distributed into |
| 169 | * placeholder widgets. Mirrors the logic in ECS_Container_Layout_Module. |
| 170 | * |
| 171 | * @param int $template_id Published ECS Custom Layout post ID. |
| 172 | * @param list<string> $items Rendered loop item HTML strings. |
| 173 | */ |
| 174 | private function render_with_custom_layout( int $template_id, array $items ): void { |
| 175 | if ( ! class_exists( 'ECS_Container_Placeholder_Widget' ) ) { |
| 176 | // Container Layout module inactive — output items without a template. |
| 177 | echo '<div class="elementor-widget-container">' . implode( '', $items ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 178 | return; |
| 179 | } |
| 180 | |
| 181 | // Inline template CSS — same technique as Container Content Layout. |
| 182 | $css_path = WP_CONTENT_DIR . '/uploads/elementor/css/post-' . $template_id . '.css'; |
| 183 | if ( file_exists( $css_path ) ) { |
| 184 | echo '<style id="elementor-post-' . esc_attr( $template_id ) . '-css">' |
| 185 | . file_get_contents( $css_path ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents |
| 186 | . '</style>'; |
| 187 | } |
| 188 | |
| 189 | // The shortcode-rendering filter breaks placeholder output during |
| 190 | // Elementor's element-cache build pass — temporarily remove it. |
| 191 | $filter_was_active = has_filter( 'elementor/element/should_render_shortcode', '__return_true' ); |
| 192 | if ( $filter_was_active ) { |
| 193 | remove_filter( 'elementor/element/should_render_shortcode', '__return_true' ); |
| 194 | } |
| 195 | |
| 196 | echo '<div class="elementor-widget-container"><div class="ecs-loop-custom-layout-wrap">'; |
| 197 | |
| 198 | $batch = $items; |
| 199 | $first_pass = true; |
| 200 | |
| 201 | do { |
| 202 | ECS_Container_Placeholder_Widget::set_pending_children( $batch ); |
| 203 | |
| 204 | $output = \Elementor\Plugin::$instance->frontend->get_builder_content_for_display( $template_id, true ); |
| 205 | echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 206 | |
| 207 | if ( $first_pass && ! ECS_Container_Placeholder_Widget::any_consumed() && ! empty( $items ) ) { |
| 208 | // Template has no placeholder widgets — show fallback. |
| 209 | ECS_Container_Placeholder_Widget::reset_pending_children(); |
| 210 | echo '<div class="ecs-missing-placeholder">'; |
| 211 | foreach ( $items as $item ) { |
| 212 | echo $item; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 213 | } |
| 214 | echo '</div>'; |
| 215 | break; |
| 216 | } |
| 217 | |
| 218 | $batch = ECS_Container_Placeholder_Widget::get_overflow_children(); |
| 219 | $first_pass = false; |
| 220 | ECS_Container_Placeholder_Widget::reset_pending_children(); |
| 221 | |
| 222 | } while ( ! empty( $batch ) ); |
| 223 | |
| 224 | echo '</div></div>'; // .ecs-loop-custom-layout-wrap + .elementor-widget-container |
| 225 | |
| 226 | if ( $filter_was_active ) { |
| 227 | add_filter( 'elementor/element/should_render_shortcode', '__return_true' ); |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | // ── Helpers ─────────────────────────────────────────────────────────────── |
| 232 | |
| 233 | /** |
| 234 | * Recursively walk the element tree and add qualifying loop-grid widgets |
| 235 | * to $this->pending_widgets. |
| 236 | */ |
| 237 | private function walk_elements( array $elements ): void { |
| 238 | foreach ( $elements as $element ) { |
| 239 | if ( |
| 240 | ( $element['elType'] ?? '' ) === 'widget' |
| 241 | && ( $element['widgetType'] ?? '' ) === 'loop-grid' |
| 242 | && ( $element['settings']['ecs_use_custom_layout'] ?? '' ) === 'yes' |
| 243 | ) { |
| 244 | $template_id = absint( $element['settings']['ecs_loop_layout_id'] ?? 0 ); |
| 245 | if ( $template_id && 'publish' === get_post_status( $template_id ) ) { |
| 246 | $this->pending_widgets[ $element['id'] ] = $template_id; |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | if ( ! empty( $element['elements'] ) ) { |
| 251 | $this->walk_elements( $element['elements'] ); |
| 252 | } |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * Find and return the outermost element HTML that carries the class |
| 258 | * elementor-element-{$element_id}. Uses tag-depth tracking to find the |
| 259 | * matching closing tag, handling arbitrary nesting. |
| 260 | */ |
| 261 | private function extract_element_by_id( string $html, string $element_id ): string { |
| 262 | if ( ! preg_match( |
| 263 | '/<([\w-]+)(\s[^>]*?\bclass="[^"]*\belementor-element-' . preg_quote( $element_id, '/' ) . '\b[^"]*"[^>]*)>/i', |
| 264 | $html, |
| 265 | $m, |
| 266 | PREG_OFFSET_CAPTURE |
| 267 | ) ) { |
| 268 | return ''; |
| 269 | } |
| 270 | |
| 271 | $tag = $m[1][0]; |
| 272 | $start = $m[0][1]; |
| 273 | $after_open = $start + strlen( $m[0][0] ); |
| 274 | $len = strlen( $html ); |
| 275 | $depth = 1; |
| 276 | $cursor = $after_open; |
| 277 | $end = $after_open; |
| 278 | |
| 279 | while ( $depth > 0 && $cursor < $len ) { |
| 280 | $next_open = strpos( $html, '<' . $tag, $cursor ); |
| 281 | $next_close = strpos( $html, '</' . $tag . '>', $cursor ); |
| 282 | |
| 283 | if ( false === $next_close ) { |
| 284 | break; |
| 285 | } |
| 286 | if ( false === $next_open ) { |
| 287 | $next_open = $len; |
| 288 | } |
| 289 | |
| 290 | if ( $next_open < $next_close ) { |
| 291 | $depth++; |
| 292 | $cursor = $next_open + 1; |
| 293 | } else { |
| 294 | $depth--; |
| 295 | $cursor = $next_close + 1; |
| 296 | if ( 0 === $depth ) { |
| 297 | $end = $next_close + strlen( '</' . $tag . '>' ); |
| 298 | } |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | return ( 0 === $depth ) ? substr( $html, $start, $end - $start ) : ''; |
| 303 | } |
| 304 | |
| 305 | /** |
| 306 | * Extract individual loop item HTML strings from the buffered Loop Grid output. |
| 307 | * |
| 308 | * Elementor Pro marks each rendered loop item with class `e-loop-item` on |
| 309 | * its outermost element. We use tag-depth tracking to find the matching |
| 310 | * closing tag for each item, handling arbitrary nesting correctly. |
| 311 | * |
| 312 | * @return list<string> |
| 313 | */ |
| 314 | private function extract_loop_items( string $html ): array { |
| 315 | $items = []; |
| 316 | $pos = 0; |
| 317 | $len = strlen( $html ); |
| 318 | |
| 319 | while ( $pos < $len ) { |
| 320 | // Find the next opening tag that carries the e-loop-item class. |
| 321 | if ( ! preg_match( |
| 322 | '/<(\w[\w-]*)(\s[^>]*?\bclass="[^"]*\be-loop-item\b[^"]*"[^>]*|[^>]*\bclass=\'[^\']*\be-loop-item\b[^\']*\'[^>]*)>/i', |
| 323 | $html, |
| 324 | $m, |
| 325 | PREG_OFFSET_CAPTURE, |
| 326 | $pos |
| 327 | ) ) { |
| 328 | break; |
| 329 | } |
| 330 | |
| 331 | $tag = $m[1][0]; |
| 332 | $start = $m[0][1]; |
| 333 | $after_open = $start + strlen( $m[0][0] ); |
| 334 | |
| 335 | // Walk forward tracking open/close depth to find the matching close tag. |
| 336 | $depth = 1; |
| 337 | $cursor = $after_open; |
| 338 | $end = $after_open; |
| 339 | |
| 340 | while ( $depth > 0 && $cursor < $len ) { |
| 341 | $next_open = strpos( $html, '<' . $tag, $cursor ); |
| 342 | $next_close = strpos( $html, '</' . $tag . '>', $cursor ); |
| 343 | |
| 344 | if ( false === $next_close ) { |
| 345 | break; // Malformed HTML — stop. |
| 346 | } |
| 347 | if ( false === $next_open ) { |
| 348 | $next_open = $len; |
| 349 | } |
| 350 | |
| 351 | if ( $next_open < $next_close ) { |
| 352 | $depth++; |
| 353 | $cursor = $next_open + 1; |
| 354 | } else { |
| 355 | $depth--; |
| 356 | $cursor = $next_close + 1; |
| 357 | if ( 0 === $depth ) { |
| 358 | $end = $next_close + strlen( '</' . $tag . '>' ); |
| 359 | } |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | if ( 0 === $depth ) { |
| 364 | $items[] = substr( $html, $start, $end - $start ); |
| 365 | $pos = $end; |
| 366 | } else { |
| 367 | break; // Could not close the tag — stop parsing. |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | return $items; |
| 372 | } |
| 373 | |
| 374 | // ── Editor assets ───────────────────────────────────────────────────────── |
| 375 | |
| 376 | /** |
| 377 | * Enqueue the editor preview script for the Loop Custom Layout feature. |
| 378 | */ |
| 379 | public function enqueue_editor_assets(): void { |
| 380 | wp_enqueue_script( |
| 381 | 'ecs-loop-custom-layout-editor', |
| 382 | $this->module_asset_url( 'assets/js/ecs-loop-custom-layout-editor.js' ), |
| 383 | [ 'elementor-editor' ], |
| 384 | ECS_VERSION, |
| 385 | true |
| 386 | ); |
| 387 | |
| 388 | wp_localize_script( |
| 389 | 'ecs-loop-custom-layout-editor', |
| 390 | 'ecsLoopPreview', |
| 391 | [ |
| 392 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ), |
| 393 | 'nonce' => wp_create_nonce( 'ecs-loop-custom-layout-preview' ), |
| 394 | ] |
| 395 | ); |
| 396 | } |
| 397 | |
| 398 | // ── AJAX ────────────────────────────────────────────────────────────────── |
| 399 | |
| 400 | /** |
| 401 | * AJAX handler: render a custom layout template with caller-supplied loop |
| 402 | * item HTML strings so the Elementor editor canvas can preview the result. |
| 403 | * |
| 404 | * Security: nonce + capability check (wp_ajax_ requires authentication). |
| 405 | */ |
| 406 | public function ajax_loop_custom_layout_preview(): void { |
| 407 | check_ajax_referer( 'ecs-loop-custom-layout-preview', 'nonce' ); |
| 408 | |
| 409 | if ( ! current_user_can( 'edit_posts' ) ) { |
| 410 | wp_send_json_error( [ 'message' => 'Unauthorized' ] ); |
| 411 | return; |
| 412 | } |
| 413 | |
| 414 | $template_id = absint( $_POST['template_id'] ?? 0 ); |
| 415 | $items = isset( $_POST['items'] ) && is_array( $_POST['items'] ) |
| 416 | ? array_values( wp_unslash( $_POST['items'] ) ) |
| 417 | : []; |
| 418 | |
| 419 | if ( ! $template_id || 'publish' !== get_post_status( $template_id ) ) { |
| 420 | wp_send_json_error( [ 'message' => 'Invalid template' ] ); |
| 421 | return; |
| 422 | } |
| 423 | |
| 424 | // Ensure the placeholder widget class is loaded before static calls. |
| 425 | if ( ! class_exists( 'ECS_Container_Placeholder_Widget', false ) ) { |
| 426 | $widget_file = ECS_PATH . 'modules/container-layout/widgets/class-ecs-container-placeholder-widget.php'; |
| 427 | if ( file_exists( $widget_file ) ) { |
| 428 | require_once $widget_file; |
| 429 | } |
| 430 | } |
| 431 | |
| 432 | if ( ! class_exists( 'ECS_Container_Placeholder_Widget', false ) ) { |
| 433 | wp_send_json_error( [ 'message' => 'Container Layout module not active' ] ); |
| 434 | return; |
| 435 | } |
| 436 | |
| 437 | ob_start(); |
| 438 | // Render only what goes inside .elementor-widget-container — |
| 439 | // i.e. the ecs-loop-custom-layout-wrap div and its template instances. |
| 440 | $this->render_loop_items_with_template( $template_id, $items ); |
| 441 | $html = ob_get_clean(); |
| 442 | |
| 443 | wp_send_json_success( [ 'html' => $html ] ); |
| 444 | } |
| 445 | |
| 446 | /** |
| 447 | * Like render_with_custom_layout() but emits only the inner content — |
| 448 | * the ecs-loop-custom-layout-wrap div — without the outer |
| 449 | * .elementor-widget-container wrapper (the editor injects into that wrapper). |
| 450 | * |
| 451 | * @param int $template_id Published ECS Custom Layout post ID. |
| 452 | * @param list<string> $items Rendered loop item HTML strings. |
| 453 | */ |
| 454 | private function render_loop_items_with_template( int $template_id, array $items ): void { |
| 455 | $filter_was_active = has_filter( 'elementor/element/should_render_shortcode', '__return_true' ); |
| 456 | if ( $filter_was_active ) { |
| 457 | remove_filter( 'elementor/element/should_render_shortcode', '__return_true' ); |
| 458 | } |
| 459 | |
| 460 | echo '<div class="ecs-loop-custom-layout-wrap">'; |
| 461 | |
| 462 | $batch = $items; |
| 463 | $first_pass = true; |
| 464 | |
| 465 | do { |
| 466 | ECS_Container_Placeholder_Widget::set_pending_children( $batch ); |
| 467 | |
| 468 | $output = \Elementor\Plugin::$instance->frontend->get_builder_content_for_display( $template_id, true ); |
| 469 | echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 470 | |
| 471 | if ( $first_pass && ! ECS_Container_Placeholder_Widget::any_consumed() && ! empty( $items ) ) { |
| 472 | ECS_Container_Placeholder_Widget::reset_pending_children(); |
| 473 | echo '<div class="ecs-missing-placeholder">'; |
| 474 | foreach ( $items as $item ) { |
| 475 | echo $item; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 476 | } |
| 477 | echo '</div>'; |
| 478 | break; |
| 479 | } |
| 480 | |
| 481 | $batch = ECS_Container_Placeholder_Widget::get_overflow_children(); |
| 482 | $first_pass = false; |
| 483 | ECS_Container_Placeholder_Widget::reset_pending_children(); |
| 484 | |
| 485 | } while ( ! empty( $batch ) ); |
| 486 | |
| 487 | echo '</div>'; // .ecs-loop-custom-layout-wrap |
| 488 | |
| 489 | if ( $filter_was_active ) { |
| 490 | add_filter( 'elementor/element/should_render_shortcode', '__return_true' ); |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | /** |
| 495 | * Return an id → title map of all published ECS Custom Layout templates. |
| 496 | */ |
| 497 | private function get_custom_layout_templates(): array { |
| 498 | $posts = get_posts( [ |
| 499 | 'post_type' => 'elementor_library', |
| 500 | 'post_status' => 'publish', |
| 501 | 'posts_per_page' => -1, |
| 502 | 'meta_query' => [ |
| 503 | [ |
| 504 | 'key' => '_elementor_template_type', |
| 505 | 'value' => 'ecs_custom_layout', |
| 506 | ], |
| 507 | ], |
| 508 | ] ); |
| 509 | |
| 510 | $options = [ '' => esc_html__( '— Select Template —', 'ele-custom-skin' ) ]; |
| 511 | foreach ( $posts as $post ) { |
| 512 | $options[ $post->ID ] = $post->post_title; |
| 513 | } |
| 514 | |
| 515 | return $options; |
| 516 | } |
| 517 | } |
| 518 |