| 1 |
<?php |
| 2 |
/** |
| 3 |
* Render the form-free Field Configuration interface. |
| 4 |
* |
| 5 |
* PHP renders the current active metadata in authoritative custom order. The |
| 6 |
* browser may search, filter, sort, bulk-select visible rows, and enqueue compact |
| 7 |
* commands, but it does not submit a mirrored WordPress settings form. Dormant |
| 8 |
* fields remain in storage through the domain module and are intentionally not |
| 9 |
* rendered until their MLS metadata returns. |
| 10 |
* |
| 11 |
* @package MLSImport |
| 12 |
* @subpackage MLSImport/includes |
| 13 |
*/ |
| 14 |
|
| 15 |
if ( ! defined( 'ABSPATH' ) ) { |
| 16 |
exit; |
| 17 |
} |
| 18 |
|
| 19 |
/** |
| 20 |
* Order current metadata by the saved custom sequence. |
| 21 |
* |
| 22 |
* Only keys present in current metadata are returned. Any metadata field absent |
| 23 |
* from the normalized order is appended alphabetically as a safe read-only |
| 24 |
* fallback; metadata reconciliation normally added it before this render. |
| 25 |
* |
| 26 |
* @param array $fields Current MLS metadata keyed by field name. |
| 27 |
* @param array $options Normalized Field Configuration. |
| 28 |
* @param array $params Render parameters retained for the public signature. |
| 29 |
* @return array Ordered active metadata. |
| 30 |
*/ |
| 31 |
function prepare_mls_fields_for_display( $fields, $options, $params = array() ) { |
| 32 |
$fields = is_array( $fields ) ? $fields : array(); |
| 33 |
$options = is_array( $options ) ? $options : array(); |
| 34 |
$order = isset( $options['field_order'] ) && is_array( $options['field_order'] ) ? $options['field_order'] : array(); |
| 35 |
asort( $order, SORT_NUMERIC ); |
| 36 |
|
| 37 |
$ordered = array(); |
| 38 |
foreach ( array_keys( $order ) as $field_key ) { |
| 39 |
if ( array_key_exists( $field_key, $fields ) ) { |
| 40 |
$ordered[ $field_key ] = $fields[ $field_key ]; |
| 41 |
} |
| 42 |
} |
| 43 |
|
| 44 |
$remaining = array_diff_key( $fields, $ordered ); |
| 45 |
ksort( $remaining, SORT_STRING ); |
| 46 |
|
| 47 |
return array_merge( $ordered, $remaining ); |
| 48 |
} |
| 49 |
|
| 50 |
/** |
| 51 |
* Render the complete active Field Configuration list. |
| 52 |
* |
| 53 |
* @param array $fields Current MLS metadata. |
| 54 |
* @param array $options Normalized Field Configuration. |
| 55 |
* @param array $render_params Initial filter and feature controls. |
| 56 |
* @param array $theme_schema Theme defaults retained for compatible callers. |
| 57 |
* @return string Complete interface HTML. |
| 58 |
*/ |
| 59 |
function render_mls_field_selection_interface( $fields, $options, $render_params = array(), $theme_schema = array() ) { |
| 60 |
$params = wp_parse_args( |
| 61 |
$render_params, |
| 62 |
array( |
| 63 |
'import_filter' => 'all', |
| 64 |
'show_filters' => true, |
| 65 |
'show_stats' => true, |
| 66 |
'enable_drag_drop' => true, |
| 67 |
'plugin_name' => 'mlsimport', |
| 68 |
) |
| 69 |
); |
| 70 |
$display_fields = prepare_mls_fields_for_display( $fields, $options, $params ); |
| 71 |
$revision = max( 0, (int) ( $options[ Mlsimport_Field_Configuration::REVISION_KEY ] ?? 0 ) ); |
| 72 |
$taxonomies = mlsimport_field_configuration_taxonomies(); |
| 73 |
|
| 74 |
ob_start(); |
| 75 |
echo '<div class="mlsimport-field-selector-container" data-revision="' . esc_attr( $revision ) . '" data-initial-filter="' . esc_attr( $params['import_filter'] ) . '">'; |
| 76 |
echo '<div class="mlsimport-field-save-status" role="status" aria-live="polite"></div>'; |
| 77 |
|
| 78 |
if ( $params['show_stats'] ) { |
| 79 |
mlsimport_render_field_stats( $display_fields, $options ); |
| 80 |
} |
| 81 |
if ( $params['show_filters'] ) { |
| 82 |
mlsimport_render_field_filters( $params ); |
| 83 |
} |
| 84 |
|
| 85 |
echo '<table class="mlsimport-fields-table" id="mlsimport-fields-table">'; |
| 86 |
mlsimport_render_field_table_header(); |
| 87 |
echo '<tbody id="mlsimport-fields-table-body">'; |
| 88 |
foreach ( $display_fields as $field_key => $description ) { |
| 89 |
mlsimport_render_field_table_row( $field_key, $description, $options, $taxonomies ); |
| 90 |
} |
| 91 |
if ( empty( $display_fields ) ) { |
| 92 |
echo '<tr class="mlsimport-no-fields"><td colspan="7">' . esc_html__( 'No MLS fields are available.', 'mlsimport' ) . '</td></tr>'; |
| 93 |
} |
| 94 |
echo '</tbody></table></div>'; |
| 95 |
|
| 96 |
return ob_get_clean(); |
| 97 |
} |
| 98 |
|
| 99 |
/** |
| 100 |
* Render active-field statistics and visible-only bulk controls. |
| 101 |
* |
| 102 |
* Counts cover only current MLS metadata because dormant fields are retained in |
| 103 |
* storage but absent from this interface. Bulk buttons describe their visible |
| 104 |
* scope explicitly; the controller sends the exact visible field keys. |
| 105 |
* |
| 106 |
* @param array $fields Ordered active metadata. |
| 107 |
* @param array $options Normalized Field Configuration. |
| 108 |
* @return void |
| 109 |
*/ |
| 110 |
function mlsimport_render_field_stats( array $fields, array $options ) { |
| 111 |
$selected = 0; |
| 112 |
foreach ( array_keys( $fields ) as $field_key ) { |
| 113 |
$selected += ! empty( $options['mls-fields'][ $field_key ] ) ? 1 : 0; |
| 114 |
} |
| 115 |
|
| 116 |
echo '<div class="mlsimport-field-stats"><ul>'; |
| 117 |
echo '<li class="mlsimport-total-count">' . esc_html( sprintf( __( '%d fields total', 'mlsimport' ), count( $fields ) ) ) . '</li>'; |
| 118 |
echo '<li class="mlsimport-selected-count">' . esc_html( sprintf( __( '%d marked for import', 'mlsimport' ), $selected ) ) . '</li>'; |
| 119 |
echo '</ul><div class="mlsimport-button-action-wrapper">'; |
| 120 |
echo '<button type="button" id="mlsimport-select-all-import" class="button mlsimport_button">' . esc_html__( 'Import - Select all visible', 'mlsimport' ) . '</button>'; |
| 121 |
echo '<button type="button" id="mlsimport-select-none-import" class="button mlsimport_button">' . esc_html__( 'Import - Select none visible', 'mlsimport' ) . '</button>'; |
| 122 |
echo '<button type="button" id="mlsimport-select-all-admin" class="button mlsimport_button secondary">' . esc_html__( 'Hidden from Public - Select all visible', 'mlsimport' ) . '</button>'; |
| 123 |
echo '<button type="button" id="mlsimport-select-none-admin" class="button mlsimport_button secondary">' . esc_html__( 'Hidden from Public - Select none visible', 'mlsimport' ) . '</button>'; |
| 124 |
echo '</div></div>'; |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Render client-side search, import filter, and sort controls. |
| 129 |
* |
| 130 |
* These controls change only the local view. Custom order remains the sole view |
| 131 |
* where reordering is enabled, avoiding position commands derived from a |
| 132 |
* filtered or alphabetically sorted subset. |
| 133 |
* |
| 134 |
* @param array $params Parsed renderer parameters including initial filter. |
| 135 |
* @return void |
| 136 |
*/ |
| 137 |
function mlsimport_render_field_filters( array $params ) { |
| 138 |
echo '<div class="mlsimport-field-filters">'; |
| 139 |
echo '<select id="mlsimport-field-sort" class="mlsimport-field-filter mlsimport-2025-select" style="width:200px">'; |
| 140 |
echo '<option value="custom_order">' . esc_html__( 'Custom Order', 'mlsimport' ) . '</option>'; |
| 141 |
echo '<option value="field_asc">' . esc_html__( 'Field Name (A-Z)', 'mlsimport' ) . '</option>'; |
| 142 |
echo '<option value="field_desc">' . esc_html__( 'Field Name (Z-A)', 'mlsimport' ) . '</option>'; |
| 143 |
echo '<option value="label_asc">' . esc_html__( 'Label (A-Z)', 'mlsimport' ) . '</option>'; |
| 144 |
echo '<option value="label_desc">' . esc_html__( 'Label (Z-A)', 'mlsimport' ) . '</option>'; |
| 145 |
echo '<option value="postmeta_asc">' . esc_html__( 'Property Detail Field (A-Z)', 'mlsimport' ) . '</option>'; |
| 146 |
echo '<option value="postmeta_desc">' . esc_html__( 'Property Detail Field (Z-A)', 'mlsimport' ) . '</option>'; |
| 147 |
echo '<option value="category_asc">' . esc_html__( 'Category (A-Z)', 'mlsimport' ) . '</option>'; |
| 148 |
echo '<option value="category_desc">' . esc_html__( 'Category (Z-A)', 'mlsimport' ) . '</option>'; |
| 149 |
echo '</select>'; |
| 150 |
|
| 151 |
echo '<select id="mlsimport-import-filter" class="mlsimport-field-filter mlsimport-2025-select" style="width:200px">'; |
| 152 |
echo '<option value="all"' . selected( $params['import_filter'], 'all', false ) . '>' . esc_html__( 'All Fields', 'mlsimport' ) . '</option>'; |
| 153 |
echo '<option value="selected"' . selected( $params['import_filter'], 'selected', false ) . '>' . esc_html__( 'Selected Only', 'mlsimport' ) . '</option>'; |
| 154 |
echo '<option value="not_selected"' . selected( $params['import_filter'], 'not_selected', false ) . '>' . esc_html__( 'Not Selected', 'mlsimport' ) . '</option>'; |
| 155 |
echo '</select>'; |
| 156 |
echo '<input type="text" id="mlsimport-field-search" class="mlsimport-field-filter mlsimport-input mlsimport-2025-input" style="width:300px" placeholder="' . esc_attr__( 'Search...', 'mlsimport' ) . '">'; |
| 157 |
echo '</div>'; |
| 158 |
} |
| 159 |
|
| 160 |
/** |
| 161 |
* Render the seven stable table columns consumed by the browser controller. |
| 162 |
* |
| 163 |
* No form names are emitted because changes travel as compact JSON commands. |
| 164 |
* The actions column holds relative move controls that remain usable when the |
| 165 |
* jQuery sortable interaction is unavailable. |
| 166 |
* |
| 167 |
* @return void |
| 168 |
*/ |
| 169 |
function mlsimport_render_field_table_header() { |
| 170 |
echo '<thead><tr>'; |
| 171 |
echo '<th>' . esc_html__( 'Field', 'mlsimport' ) . '</th>'; |
| 172 |
echo '<th style="width:75px">' . esc_html__( 'Import it?', 'mlsimport' ) . '</th>'; |
| 173 |
echo '<th>' . esc_html__( 'Front End Label', 'mlsimport' ) . '</th>'; |
| 174 |
echo '<th style="width:75px">' . esc_html__( 'Hidden from Public?', 'mlsimport' ) . '</th>'; |
| 175 |
echo '<th>' . esc_html__( 'Property Detail Field (post meta)', 'mlsimport' ) . '</th>'; |
| 176 |
echo '<th>' . esc_html__( 'Category', 'mlsimport' ) . '</th>'; |
| 177 |
echo '<th style="width:140px">' . esc_html__( 'Actions', 'mlsimport' ) . '</th>'; |
| 178 |
echo '</tr></thead>'; |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Render one active field without form names or mandatory-field branches. |
| 183 |
* |
| 184 |
* Values come exclusively from the normalized configuration. The row exposes |
| 185 |
* the RESO key and custom-order position as data attributes, while editable |
| 186 |
* controls expose only classes interpreted by the compact command controller. |
| 187 |
* |
| 188 |
* @param string $field_key RESO field identifier. |
| 189 |
* @param mixed $description Metadata description displayed below the key. |
| 190 |
* @param array $options Normalized Field Configuration. |
| 191 |
* @param array $taxonomies Allowed taxonomy destinations. |
| 192 |
* @return void |
| 193 |
*/ |
| 194 |
function mlsimport_render_field_table_row( $field_key, $description, array $options, array $taxonomies ) { |
| 195 |
$order = (int) ( $options['field_order'][ $field_key ] ?? 0 ); |
| 196 |
$is_import = ! empty( $options['mls-fields'][ $field_key ] ); |
| 197 |
$is_admin = ! empty( $options['mls-fields-admin'][ $field_key ] ); |
| 198 |
$label = (string) ( $options['mls-fields-label'][ $field_key ] ?? '' ); |
| 199 |
$postmeta = (string) ( $options['mls-fields-map-postmeta'][ $field_key ] ?? '' ); |
| 200 |
$taxonomy = (string) ( $options['mls-fields-map-taxonomy'][ $field_key ] ?? '' ); |
| 201 |
$description = is_scalar( $description ) && ! is_bool( $description ) ? (string) $description : ''; |
| 202 |
|
| 203 |
echo '<tr class="mlsimport-field-row" data-field-order="' . esc_attr( $order ) . '" data-field-key="' . esc_attr( $field_key ) . '">'; |
| 204 |
echo '<td class="mlsimport-field-name mlsimport-field-name_title_desc"><span class="field-name"><span class="field-position">' . esc_html( $order + 1 ) . '. </span>' . esc_html( $field_key ) . '</span>'; |
| 205 |
if ( '' !== $description && '1' !== $description && '0' !== $description ) { |
| 206 |
echo '<div class="field-explanation">' . esc_html( $description ) . '</div>'; |
| 207 |
} |
| 208 |
echo '</td>'; |
| 209 |
echo '<td class="mlsimport-field-import"><input type="checkbox" class="mlsimport-import-checkbox" value="1" ' . checked( $is_import, true, false ) . '></td>'; |
| 210 |
echo '<td class="mlsimport-field-label"><input type="text" class="mlsimport-label-input mlsimport-input mlsimport-2025-input" value="' . esc_attr( $label ) . '" placeholder="' . esc_attr__( 'enter label...', 'mlsimport' ) . '"></td>'; |
| 211 |
echo '<td class="mlsimport-field-admin"><input type="checkbox" class="mlsimport-admin-checkbox" value="1" ' . checked( $is_admin, true, false ) . '></td>'; |
| 212 |
echo '<td class="mlsimport-field-postmeta"><input type="text" class="mlsimport-postmeta-input mlsimport-input mlsimport-2025-input" value="' . esc_attr( $postmeta ) . '"></td>'; |
| 213 |
echo '<td class="mlsimport-field-taxonomy">' . mlsimport_field_taxonomy_dropdown( $taxonomy, $taxonomies ) . '</td>'; |
| 214 |
echo '<td class="mlsimport-field-actions"><div class="mlsimport-row-actions">'; |
| 215 |
echo '<button type="button" class="mlsimport-move-btn mlsimport-move-up" title="' . esc_attr__( 'Move field up', 'mlsimport' ) . '">↑</button>'; |
| 216 |
echo '<button type="button" class="mlsimport-move-btn mlsimport-move-down" title="' . esc_attr__( 'Move field down', 'mlsimport' ) . '">↓</button>'; |
| 217 |
echo '<button type="button" class="mlsimport-move-btn mlsimport-move-top" title="' . esc_attr__( 'Move field to top', 'mlsimport' ) . '">⇈</button>'; |
| 218 |
echo '<button type="button" class="mlsimport-move-btn mlsimport-move-bottom" title="' . esc_attr__( 'Move field to bottom', 'mlsimport' ) . '">⇊</button>'; |
| 219 |
echo '</div></td></tr>'; |
| 220 |
} |
| 221 |
|
| 222 |
/** |
| 223 |
* Build the active property type's allowed taxonomy destination control. |
| 224 |
* |
| 225 |
* An explicit empty option represents no taxonomy mapping. The selected value |
| 226 |
* is read from normalized state, and every value and label is escaped before |
| 227 |
* the complete select element is returned to the row renderer. |
| 228 |
* |
| 229 |
* @param string $selected_taxonomy Currently mapped taxonomy slug. |
| 230 |
* @param array $taxonomies Allowed slug-to-label map. |
| 231 |
* @return string Escaped select markup. |
| 232 |
*/ |
| 233 |
function mlsimport_field_taxonomy_dropdown( $selected_taxonomy, array $taxonomies ) { |
| 234 |
$html = '<select class="mlsimport-taxonomy-select mlsimport-2025-select">'; |
| 235 |
$html .= '<option value="">' . esc_html__( 'None', 'mlsimport' ) . '</option>'; |
| 236 |
foreach ( $taxonomies as $taxonomy => $label ) { |
| 237 |
$html .= '<option value="' . esc_attr( $taxonomy ) . '" ' . selected( $selected_taxonomy, $taxonomy, false ) . '>' . esc_html( $label ) . '</option>'; |
| 238 |
} |
| 239 |
$html .= '</select>'; |
| 240 |
|
| 241 |
return $html; |
| 242 |
} |
| 243 |
|