register_control_type( 'Mlsimport_Customize_Sections_Control' ); } // The root "MLSImport Design" panel that holds the schema-driven sections. $wp_customize->add_panel( MLSIMPORT_CUSTOMIZER_PANEL, array( 'title' => __( 'MLSImport Design', 'mlsimport' ), 'description' => __( 'Appearance of your listings, cards, search, maps, property and agent pages. These are the same settings as the MLS Import Design Settings screen.', 'mlsimport' ), 'priority' => 130, ) ); // A field's Property Page sub-tab (pp_general, pp_layout, …) lives in the // settings-page presentation map. A section whose fields carry sub-tabs (the // Property Page) becomes its OWN panel with one section per sub-tab, so the // Customizer drills down — click "Property Page" → its sub-tabs → a sub-tab's // settings, with the back arrow — exactly like the settings page's nested menu. // (The Customizer can't nest a panel inside a panel, so the Property Page panel // sits at the root next to "MLSImport Design"; a flat "one section per sub-tab" // would instead dump every field into one screen.) // Presentation map (per-field sub-tab) and the sub-tab id => title list. $ui = function_exists( 'mlsimport_standalone_settings_ui' ) ? mlsimport_standalone_settings_ui() : array(); $subtabs = function_exists( 'mlsimport_standalone_settings_subtabs' ) ? mlsimport_standalone_settings_subtabs() : array(); // Helper: add one Customizer section under $panel and its per-field controls. $add_section = function ( $section_id, $title, $fields, $panel ) use ( $wp_customize ) { $wp_customize->add_section( $section_id, array( 'title' => $title, 'panel' => $panel, ) ); // One control per field in the section. foreach ( $fields as $field ) { mlsimport_customizer_add_field( $wp_customize, $section_id, $field ); } }; $priority = 131; // Property Page panel sits right after "MLSImport Design". // Walk the schema, turning each section into a Customizer section or drill-down panel. foreach ( mlsimport_standalone_settings_schema() as $section ) { // Bucket the section's fields by sub-tab (empty string = no sub-tab). $by_subtab = array(); $has_subtabs = false; foreach ( $section['fields'] as $field ) { // This field's sub-tab id from the presentation map ('' when none). $sub = isset( $ui[ $field['key'] ]['subtab'] ) ? (string) $ui[ $field['key'] ]['subtab'] : ''; // Any sub-tab present flips this section into drill-down mode. if ( '' !== $sub ) { $has_subtabs = true; } $by_subtab[ $sub ][] = $field; } // Flat section: no sub-tabs, so all fields live in one Customizer section. if ( ! $has_subtabs ) { $add_section( MLSIMPORT_CUSTOMIZER_PANEL . '_' . $section['id'], $section['title'], $section['fields'], MLSIMPORT_CUSTOMIZER_PANEL ); continue; } // This section drills down: its own panel, one section per sub-tab. $sub_panel = MLSIMPORT_CUSTOMIZER_PANEL . '_' . $section['id']; $wp_customize->add_panel( $sub_panel, array( 'title' => $section['title'], 'priority' => $priority++, ) ); // One section per sub-tab that actually has fields, in sub-tab order. foreach ( $subtabs as $sub_id => $sub_title ) { // Skip sub-tabs with no fields in this section. if ( empty( $by_subtab[ $sub_id ] ) ) { continue; } $add_section( $sub_panel . '_' . $sub_id, $sub_title, $by_subtab[ $sub_id ], $sub_panel ); } // Any field without a sub-tab still gets a home inside the sub-panel. if ( ! empty( $by_subtab[''] ) ) { $add_section( $sub_panel . '_misc', $section['title'], $by_subtab[''], $sub_panel ); } } } /** * Register the setting + control for one schema field. The setting is bound to the * multidimensional option key mlsimport_standalone_options[], so the * Customizer writes the very same option the settings page and front end read — * one source of truth, no divergence. * * @param WP_Customize_Manager $wp_customize The Customizer manager. * @param string $section_id The Customizer section id. * @param array $field A schema field. * @return void */ function mlsimport_customizer_add_field( $wp_customize, string $section_id, array $field ): void { // Bind the setting to the multidimensional option key options[]. $key = $field['key']; $setting_id = MLSIMPORT_STANDALONE_OPTION . '[' . $key . ']'; $defaults = mlsimport_standalone_option_defaults(); // Register the setting: option storage, default, cap, transport, per-field sanitizer. $wp_customize->add_setting( $setting_id, array( 'type' => 'option', 'default' => isset( $defaults[ $key ] ) ? $defaults[ $key ] : '', 'capability' => 'manage_options', // Colours preview live; everything else refreshes the preview. 'transport' => 'postMessage' === $field['transport'] ? 'postMessage' : 'refresh', 'sanitize_callback' => function ( $value ) use ( $key ) { // Reuse the exact save-path sanitizer, per field. $clean = mlsimport_sanitize_standalone_options( array( $key => $value ) ); return $clean[ $key ]; }, ) ); // Shared control args; each type below adds its own specifics. $control_args = array( 'label' => $field['label'], 'section' => $section_id, 'settings' => $setting_id, ); // Pick the control class/type from the field's declared type. switch ( $field['type'] ) { case 'color': // Native colour picker. $wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, $setting_id, $control_args ) ); break; case 'int': // mls_logo_id — an attachment id chosen through the media modal. $wp_customize->add_control( new WP_Customize_Media_Control( $wp_customize, $setting_id, $control_args + array( 'mime_type' => 'image' ) ) ); break; case 'select': // Dropdown built from the field's options map. $wp_customize->add_control( $setting_id, $control_args + array( 'type' => 'select', 'choices' => is_array( $field['options'] ) ? $field['options'] : array(), ) ); break; case 'textarea': case 'html': // Both render as a plain textarea control. $wp_customize->add_control( $setting_id, $control_args + array( 'type' => 'textarea' ) ); break; case 'number': // Numeric input. $wp_customize->add_control( $setting_id, $control_args + array( 'type' => 'number' ) ); break; case 'email': // Email input. $wp_customize->add_control( $setting_id, $control_args + array( 'type' => 'email' ) ); break; case 'sections': // Resolve the choice catalog from the field's callable, if any. $catalog = array(); if ( ! empty( $field['catalog'] ) && is_callable( $field['catalog'] ) ) { $catalog = (array) call_user_func( $field['catalog'] ); } // Custom reorder/enable-disable arranger control. It also learns which // slugs the field's default keeps disabled, so a catalog entry the saved // value never mentions is shown where the sanitizer will put it on save. $wp_customize->add_control( new Mlsimport_Customize_Sections_Control( $wp_customize, $setting_id, $control_args + array( 'catalog' => $catalog, 'default_inactive' => isset( $field['default']['inactive'] ) ? array_values( (array) $field['default']['inactive'] ) : array(), ) ) ); break; default: // text. // Fallback single-line text input. $wp_customize->add_control( $setting_id, $control_args + array( 'type' => 'text' ) ); break; } } /** * Enqueue the custom "arrange sections" control JS + CSS in the Customizer * controls pane (the left panel, not the preview). Standalone mode only. * * @return void */ function mlsimport_customizer_enqueue_controls(): void { // Standalone (990) only. if ( ! function_exists( 'mlsimport_is_standalone_mode' ) || ! mlsimport_is_standalone_mode() ) { return; } // Cache-busting version and plugin base URL. $ver = defined( 'MLSIMPORT_VERSION' ) ? MLSIMPORT_VERSION : '1'; $url = defined( 'MLSIMPORT_PLUGIN_URL' ) ? MLSIMPORT_PLUGIN_URL : plugin_dir_url( dirname( __DIR__ ) . '/mlsimport.php' ); // Controls-pane script + stylesheet for the arranger control. wp_enqueue_script( 'mlsimport-customizer-controls', $url . 'admin/js/mlsimport-customizer-controls.js', array( 'jquery', 'customize-controls', 'wp-api-fetch' ), $ver, true ); wp_enqueue_style( 'mlsimport-customizer-controls', $url . 'admin/css/mlsimport-customizer-controls.css', array(), $ver ); // Land the preview on the listings archive so the accent colour and card style // are visible the moment the panel opens (see the preview-landing controls JS). // Pass the listings archive URL to JS so the preview lands there on open. $archive = function_exists( 'get_post_type_archive_link' ) ? get_post_type_archive_link( 'mlsimport_property' ) : ''; wp_add_inline_script( 'mlsimport-customizer-controls', 'window.mlsimportCustomizer = ' . wp_json_encode( array( 'archiveUrl' => $archive ? $archive : '' ) ) . ';', 'before' ); } /** * Enqueue the accent live-preview script INSIDE the preview iframe. Standalone * mode only. * * @return void */ function mlsimport_customizer_enqueue_preview(): void { // Standalone (990) only. if ( ! function_exists( 'mlsimport_is_standalone_mode' ) || ! mlsimport_is_standalone_mode() ) { return; } // Cache-busting version and plugin base URL. $ver = defined( 'MLSIMPORT_VERSION' ) ? MLSIMPORT_VERSION : '1'; $url = defined( 'MLSIMPORT_PLUGIN_URL' ) ? MLSIMPORT_PLUGIN_URL : plugin_dir_url( dirname( __DIR__ ) . '/mlsimport.php' ); // Accent live-preview script, loaded inside the preview iframe. wp_enqueue_script( 'mlsimport-customizer-preview', $url . 'admin/js/mlsimport-customizer-preview.js', array( 'customize-preview' ), $ver, true ); } // Wire the panel registration and the two enqueue passes into the Customizer. if ( function_exists( 'add_action' ) ) { add_action( 'customize_register', 'mlsimport_customizer_register' ); add_action( 'customize_controls_enqueue_scripts', 'mlsimport_customizer_enqueue_controls' ); add_action( 'customize_preview_init', 'mlsimport_customizer_enqueue_preview' ); }