| 1 |
/** |
| 2 |
* Field Configuration browser controller. |
| 3 |
* |
| 4 |
* One controller owns the list view and one ordered mutation queue. Every save |
| 5 |
* posts four fixed variables (action, nonce, revision, JSON command), applies |
| 6 |
* the authoritative server result, and advances the revision before the next |
| 7 |
* command starts. Network interruptions receive three automatic retries. Any |
| 8 |
* persistent or rejected save pauses later work and leaves the administrator's |
| 9 |
* visible edits intact until Retry or Reload is chosen. |
| 10 |
* |
| 11 |
* No state is published on window and no unload warning is registered. |
| 12 |
*/ |
| 13 |
(function ($) { |
| 14 |
'use strict'; |
| 15 |
|
| 16 |
/** |
| 17 |
* Start one isolated controller for every rendered configuration list. |
| 18 |
* |
| 19 |
* Settings and onboarding may render the same component on different pages. |
| 20 |
* Each container receives private revision and queue state, with nothing |
| 21 |
* shared through window globals. |
| 22 |
*/ |
| 23 |
$(function () { |
| 24 |
$('.mlsimport-field-selector-container').each(function () { |
| 25 |
createController($(this)); |
| 26 |
}); |
| 27 |
}); |
| 28 |
|
| 29 |
/** |
| 30 |
* Create the UI state, event bindings, sortable behavior, and save queue. |
| 31 |
* |
| 32 |
* @param {jQuery} $container One rendered Field Configuration interface. |
| 33 |
*/ |
| 34 |
function createController($container) { |
| 35 |
var state = { |
| 36 |
revision: parseInt($container.attr('data-revision'), 10) || 0, |
| 37 |
queue: [], |
| 38 |
saving: false, |
| 39 |
paused: false, |
| 40 |
retryTimer: null |
| 41 |
}; |
| 42 |
var $body = $container.find('#mlsimport-fields-table-body'); |
| 43 |
var $status = $container.find('.mlsimport-field-save-status'); |
| 44 |
var $search = $container.find('#mlsimport-field-search'); |
| 45 |
var $filter = $container.find('#mlsimport-import-filter'); |
| 46 |
var $sort = $container.find('#mlsimport-field-sort'); |
| 47 |
|
| 48 |
$filter.val($container.attr('data-initial-filter') || 'all'); |
| 49 |
|
| 50 |
/** |
| 51 |
* Return all current-metadata rows, including locally hidden rows. |
| 52 |
* |
| 53 |
* Search and filtering toggle visibility without removing elements, so |
| 54 |
* this collection remains the complete active ordering boundary. |
| 55 |
* |
| 56 |
* @return {jQuery} Active field row collection. |
| 57 |
*/ |
| 58 |
function rows() { |
| 59 |
return $body.children('.mlsimport-field-row'); |
| 60 |
} |
| 61 |
|
| 62 |
/** |
| 63 |
* Find one row without interpolating an MLS key into a CSS selector. |
| 64 |
* |
| 65 |
* Comparing data values handles punctuation safely and avoids escaping |
| 66 |
* provider-specific field names before selector construction. |
| 67 |
* |
| 68 |
* @param {string} fieldKey RESO field identifier. |
| 69 |
* @return {jQuery} Matching row, or an empty collection. |
| 70 |
*/ |
| 71 |
function rowFor(fieldKey) { |
| 72 |
return rows().filter(function () { |
| 73 |
return String($(this).attr('data-field-key')) === String(fieldKey); |
| 74 |
}).first(); |
| 75 |
} |
| 76 |
|
| 77 |
/** |
| 78 |
* Recalculate active and selected counts from the current controls. |
| 79 |
* |
| 80 |
* Controls may contain authoritative saved values or newer queued edits; |
| 81 |
* counts intentionally describe what the administrator currently sees. |
| 82 |
*/ |
| 83 |
function updateStats() { |
| 84 |
var total = rows().length; |
| 85 |
var selected = rows().find('.mlsimport-import-checkbox:checked').length; |
| 86 |
$container.find('.mlsimport-total-count').text(total + ' fields total'); |
| 87 |
$container.find('.mlsimport-selected-count').text(selected + ' marked for import'); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Apply search/filter/sort locally and enable ordering only in the full |
| 92 |
* custom-order view, where every active row and position is visible. |
| 93 |
*/ |
| 94 |
function applyView() { |
| 95 |
var search = $.trim($search.val() || '').toLowerCase(); |
| 96 |
var filter = $filter.val() || 'all'; |
| 97 |
var sort = $sort.val() || 'custom_order'; |
| 98 |
var $rows = rows(); |
| 99 |
|
| 100 |
$rows.each(function () { |
| 101 |
var $row = $(this); |
| 102 |
// Match the MLS field key, never the whole row text: every row |
| 103 |
// contains a taxonomy dropdown whose option labels (e.g. |
| 104 |
// "Property City") would otherwise match terms like "city". |
| 105 |
var fieldKey = String($row.attr('data-field-key') || '').toLowerCase(); |
| 106 |
var matchesSearch = !search || fieldKey.indexOf(search) !== -1; |
| 107 |
var checked = $row.find('.mlsimport-import-checkbox').prop('checked'); |
| 108 |
var matchesFilter = filter === 'all' || (filter === 'selected' && checked) || (filter === 'not_selected' && !checked); |
| 109 |
$row.toggle(matchesSearch && matchesFilter); |
| 110 |
}); |
| 111 |
|
| 112 |
var sorted = $rows.get().sort(function (left, right) { |
| 113 |
var $left = $(left); |
| 114 |
var $right = $(right); |
| 115 |
var leftValue; |
| 116 |
var rightValue; |
| 117 |
|
| 118 |
if (sort === 'custom_order') { |
| 119 |
return (parseInt($left.attr('data-field-order'), 10) || 0) - (parseInt($right.attr('data-field-order'), 10) || 0); |
| 120 |
} |
| 121 |
if (sort.indexOf('label_') === 0) { |
| 122 |
leftValue = $left.find('.mlsimport-label-input').val() || ''; |
| 123 |
rightValue = $right.find('.mlsimport-label-input').val() || ''; |
| 124 |
} else if (sort.indexOf('postmeta_') === 0) { |
| 125 |
leftValue = $left.find('.mlsimport-postmeta-input').val() || ''; |
| 126 |
rightValue = $right.find('.mlsimport-postmeta-input').val() || ''; |
| 127 |
} else if (sort.indexOf('category_') === 0) { |
| 128 |
leftValue = $left.find('.mlsimport-taxonomy-select').val() || ''; |
| 129 |
rightValue = $right.find('.mlsimport-taxonomy-select').val() || ''; |
| 130 |
} else { |
| 131 |
leftValue = $left.attr('data-field-key') || ''; |
| 132 |
rightValue = $right.attr('data-field-key') || ''; |
| 133 |
} |
| 134 |
|
| 135 |
leftValue = String(leftValue).toLowerCase(); |
| 136 |
rightValue = String(rightValue).toLowerCase(); |
| 137 |
return leftValue.localeCompare(rightValue) * (sort.slice(-5) === '_desc' ? -1 : 1); |
| 138 |
}); |
| 139 |
$.each(sorted, function (_, row) { |
| 140 |
$body.append(row); |
| 141 |
}); |
| 142 |
|
| 143 |
// Reordering (buttons and drag-and-drop) stays available in every |
| 144 |
// view: moves are relative commands anchored to the nearest visible |
| 145 |
// row, which is well-defined within the full stored order no matter |
| 146 |
// how the list is currently searched, filtered, or sorted. |
| 147 |
} |
| 148 |
|
| 149 |
/** |
| 150 |
* Render the paused/error state and the correct recovery action. |
| 151 |
* |
| 152 |
* Routine saving/saved feedback is shown per-control by the inline save |
| 153 |
* indicators; this status line only carries failures. Stale revisions |
| 154 |
* require reload because another writer owns newer state. Other |
| 155 |
* failures keep the head command queued and expose Retry. |
| 156 |
* |
| 157 |
* @param {string} kind Visual state: error. |
| 158 |
* @param {string} message Administrator-facing status text. |
| 159 |
* @param {string} errorCode Stable module failure code. |
| 160 |
*/ |
| 161 |
function showStatus(kind, message, errorCode) { |
| 162 |
$status.removeClass('is-saving is-saved is-error').empty().addClass('is-' + kind).text(message || ''); |
| 163 |
if (kind !== 'error') { |
| 164 |
return; |
| 165 |
} |
| 166 |
|
| 167 |
var $button = $('<button type="button" class="button mlsimport-field-save-recovery"></button>'); |
| 168 |
if (errorCode === 'stale_revision') { |
| 169 |
$button.text(mlsimport_params.messages.reload).on('click', function () { |
| 170 |
window.location.reload(); |
| 171 |
}); |
| 172 |
} else { |
| 173 |
$button.text(mlsimport_params.messages.retry).on('click', function () { |
| 174 |
state.paused = false; |
| 175 |
processQueue(); |
| 176 |
}); |
| 177 |
} |
| 178 |
$status.append(' ').append($button); |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Map one queued command to the row cells that carry its save |
| 183 |
* indicator: the edited column for set/bulk commands and the |
| 184 |
* field-name cell for move commands. |
| 185 |
* |
| 186 |
* @param {Object} command Compact set, bulk, or move command. |
| 187 |
* @return {jQuery} Cells to annotate. |
| 188 |
*/ |
| 189 |
function indicatorCells(command) { |
| 190 |
var cellByProperty = { |
| 191 |
'import': '.mlsimport-field-import', |
| 192 |
admin: '.mlsimport-field-admin', |
| 193 |
label: '.mlsimport-field-label', |
| 194 |
postmeta: '.mlsimport-field-postmeta', |
| 195 |
taxonomy: '.mlsimport-field-taxonomy' |
| 196 |
}; |
| 197 |
var selector = command.type === 'move' ? '.mlsimport-field-name' : cellByProperty[command.property]; |
| 198 |
var fieldKeys = command.type === 'bulk' ? command.fields || [] : [command.field]; |
| 199 |
var $cells = $(); |
| 200 |
$.each(fieldKeys, function (_, fieldKey) { |
| 201 |
$cells = $cells.add(rowFor(fieldKey).find(selector)); |
| 202 |
}); |
| 203 |
return $cells; |
| 204 |
} |
| 205 |
|
| 206 |
/** |
| 207 |
* Show a spinning save indicator inside each given cell, replacing any |
| 208 |
* indicator left over from an earlier save of the same control. |
| 209 |
* |
| 210 |
* @param {jQuery} $cells Cells returned by indicatorCells(). |
| 211 |
*/ |
| 212 |
function addSaveIndicator($cells) { |
| 213 |
$cells.each(function () { |
| 214 |
$(this).find('.mlsimport-save-indicator').remove(); |
| 215 |
$(this).append('<span class="mlsimport-save-indicator is-spinning"></span>'); |
| 216 |
}); |
| 217 |
} |
| 218 |
|
| 219 |
/** |
| 220 |
* Swap each cell's spinner for a fading success tick or an error cross. |
| 221 |
* |
| 222 |
* @param {jQuery} $cells Annotated cells. |
| 223 |
* @param {boolean} success Whether the save was accepted. |
| 224 |
*/ |
| 225 |
function resolveSaveIndicators($cells, success) { |
| 226 |
var $indicators = $cells.find('.mlsimport-save-indicator'); |
| 227 |
$indicators.removeClass('is-spinning'); |
| 228 |
if (success) { |
| 229 |
$indicators.addClass('is-success').html('✓'); |
| 230 |
window.setTimeout(function () { |
| 231 |
$indicators.fadeOut(500, function () { |
| 232 |
$(this).remove(); |
| 233 |
}); |
| 234 |
}, 1000); |
| 235 |
return; |
| 236 |
} |
| 237 |
$indicators.addClass('is-error').html('✕'); |
| 238 |
} |
| 239 |
|
| 240 |
/** |
| 241 |
* Add one administrator intent to the tail of the ordered queue. |
| 242 |
* |
| 243 |
* A fresh network-attempt count belongs to each command. Processing can |
| 244 |
* start immediately, but never overtakes a currently saving head item. |
| 245 |
* |
| 246 |
* @param {Object} command Compact set, bulk, or move command. |
| 247 |
*/ |
| 248 |
function enqueue(command) { |
| 249 |
state.queue.push({ command: command, networkAttempts: 0 }); |
| 250 |
processQueue(); |
| 251 |
} |
| 252 |
|
| 253 |
/** |
| 254 |
* Send only the head command. Later changes wait for its authoritative |
| 255 |
* result, ensuring each subsequent request uses the returned revision. |
| 256 |
*/ |
| 257 |
function processQueue() { |
| 258 |
if (state.saving || state.paused || state.queue.length === 0) { |
| 259 |
return; |
| 260 |
} |
| 261 |
|
| 262 |
state.saving = true; |
| 263 |
$status.removeClass('is-saving is-saved is-error').empty(); |
| 264 |
var item = state.queue[0]; |
| 265 |
var $indicated = indicatorCells(item.command); |
| 266 |
addSaveIndicator($indicated); |
| 267 |
$.ajax({ |
| 268 |
url: mlsimport_params.ajax_url, |
| 269 |
type: 'POST', |
| 270 |
dataType: 'json', |
| 271 |
timeout: 20000, |
| 272 |
data: { |
| 273 |
action: mlsimport_params.action, |
| 274 |
security: mlsimport_params.nonce, |
| 275 |
revision: state.revision, |
| 276 |
command: JSON.stringify(item.command) |
| 277 |
} |
| 278 |
}).done(function (response) { |
| 279 |
if (!response || !response.success || !response.data || !response.data.success) { |
| 280 |
resolveSaveIndicators($indicated, false); |
| 281 |
pauseQueue(response && response.data ? response.data : null, 'The server rejected this Field Configuration change.'); |
| 282 |
return; |
| 283 |
} |
| 284 |
|
| 285 |
state.queue.shift(); |
| 286 |
state.revision = parseInt(response.data.revision, 10) || state.revision; |
| 287 |
$container.attr('data-revision', state.revision); |
| 288 |
applyResult(response.data); |
| 289 |
restoreQueuedView(); |
| 290 |
state.saving = false; |
| 291 |
resolveSaveIndicators($indicated, true); |
| 292 |
processQueue(); |
| 293 |
}).fail(function (xhr, textStatus) { |
| 294 |
var isNetworkFailure = xhr.status === 0 || textStatus === 'timeout'; |
| 295 |
if (isNetworkFailure && item.networkAttempts < 3) { |
| 296 |
item.networkAttempts += 1; |
| 297 |
state.saving = false; |
| 298 |
state.retryTimer = window.setTimeout(processQueue, item.networkAttempts * 500); |
| 299 |
return; |
| 300 |
} |
| 301 |
|
| 302 |
var payload = xhr.responseJSON && xhr.responseJSON.data ? xhr.responseJSON.data : null; |
| 303 |
resolveSaveIndicators($indicated, false); |
| 304 |
pauseQueue(payload, isNetworkFailure ? 'The network did not recover. Your change is still unsaved.' : 'Field Configuration could not be saved.'); |
| 305 |
}); |
| 306 |
} |
| 307 |
|
| 308 |
/** |
| 309 |
* Pause later changes while preserving the current optimistic controls. |
| 310 |
* |
| 311 |
* The failed head remains in the queue. Only stale results advance the |
| 312 |
* locally known revision, and their recovery action is Reload rather than |
| 313 |
* retrying a command based on superseded state. |
| 314 |
* |
| 315 |
* @param {Object|null} payload Failed Field Configuration Result. |
| 316 |
* @param {string} fallbackMessage Message used for malformed responses. |
| 317 |
*/ |
| 318 |
function pauseQueue(payload, fallbackMessage) { |
| 319 |
var error = payload && payload.error ? payload.error : {}; |
| 320 |
state.saving = false; |
| 321 |
state.paused = true; |
| 322 |
if (payload && payload.revision !== undefined && error.code === 'stale_revision') { |
| 323 |
state.revision = parseInt(payload.revision, 10) || state.revision; |
| 324 |
} |
| 325 |
showStatus('error', error.message || fallbackMessage, error.code || 'persistence_failed'); |
| 326 |
} |
| 327 |
|
| 328 |
/** |
| 329 |
* Apply cleaned fields and computed order returned by the PHP module. |
| 330 |
* |
| 331 |
* This replaces values from the command that just completed. A separate |
| 332 |
* overlay immediately reapplies later queued intent so an earlier server |
| 333 |
* response cannot make unsaved work disappear from the controls. |
| 334 |
* |
| 335 |
* @param {Object} result Successful authoritative result. |
| 336 |
*/ |
| 337 |
function applyResult(result) { |
| 338 |
$.each(result.fields || {}, function (fieldKey, field) { |
| 339 |
var $row = rowFor(fieldKey); |
| 340 |
$row.find('.mlsimport-import-checkbox').prop('checked', Number(field.import) === 1); |
| 341 |
$row.find('.mlsimport-admin-checkbox').prop('checked', Number(field.admin) === 1); |
| 342 |
$row.find('.mlsimport-label-input').val(field.label || ''); |
| 343 |
$row.find('.mlsimport-postmeta-input').val(field.postmeta || ''); |
| 344 |
$row.find('.mlsimport-taxonomy-select').val(field.taxonomy || ''); |
| 345 |
}); |
| 346 |
$.each(result.order || [], function (_, fieldKey) { |
| 347 |
$body.append(rowFor(fieldKey)); |
| 348 |
}); |
| 349 |
refreshPositions(); |
| 350 |
updateStats(); |
| 351 |
applyView(); |
| 352 |
} |
| 353 |
|
| 354 |
/** |
| 355 |
* Overlay one pending command on the authoritative DOM without saving. |
| 356 |
* |
| 357 |
* Set and bulk commands update their controls, including mutually |
| 358 |
* exclusive mappings. Move commands recreate the pending relative order. |
| 359 |
* This function never enqueues, sends, or changes the revision. |
| 360 |
* |
| 361 |
* @param {Object} command Pending compact command. |
| 362 |
*/ |
| 363 |
function applyPendingCommand(command) { |
| 364 |
var $targets; |
| 365 |
var $row; |
| 366 |
var $anchor; |
| 367 |
|
| 368 |
if (command.type === 'move') { |
| 369 |
$row = rowFor(command.field); |
| 370 |
$anchor = rowFor(command.anchor); |
| 371 |
if ($row.length && $anchor.length && !$row.is($anchor)) { |
| 372 |
command.position === 'before' ? $row.insertBefore($anchor) : $row.insertAfter($anchor); |
| 373 |
} |
| 374 |
return; |
| 375 |
} |
| 376 |
|
| 377 |
$targets = command.type === 'bulk' ? command.fields || [] : [command.field]; |
| 378 |
$.each($targets, function (_, fieldKey) { |
| 379 |
$row = rowFor(fieldKey); |
| 380 |
if (command.property === 'import') { |
| 381 |
$row.find('.mlsimport-import-checkbox').prop('checked', Number(command.value) === 1); |
| 382 |
} else if (command.property === 'admin') { |
| 383 |
$row.find('.mlsimport-admin-checkbox').prop('checked', Number(command.value) === 1); |
| 384 |
} else if (command.property === 'label') { |
| 385 |
$row.find('.mlsimport-label-input').val(command.value || ''); |
| 386 |
} else if (command.property === 'postmeta') { |
| 387 |
$row.find('.mlsimport-postmeta-input').val(command.value || ''); |
| 388 |
if ($.trim(command.value || '') !== '') { |
| 389 |
$row.find('.mlsimport-taxonomy-select').val(''); |
| 390 |
} |
| 391 |
} else if (command.property === 'taxonomy') { |
| 392 |
$row.find('.mlsimport-taxonomy-select').val(command.value || ''); |
| 393 |
if ((command.value || '') !== '') { |
| 394 |
$row.find('.mlsimport-postmeta-input').val(''); |
| 395 |
} |
| 396 |
} |
| 397 |
}); |
| 398 |
} |
| 399 |
|
| 400 |
/** |
| 401 |
* Restore all later unsaved intent after an authoritative result lands. |
| 402 |
* |
| 403 |
* Commands are overlaid in their original queue order so the last edit to |
| 404 |
* a control remains visible. Positions, counts, and filters are refreshed |
| 405 |
* once after the complete overlay rather than once per pending command. |
| 406 |
*/ |
| 407 |
function restoreQueuedView() { |
| 408 |
$.each(state.queue, function (_, item) { |
| 409 |
applyPendingCommand(item.command); |
| 410 |
}); |
| 411 |
refreshPositions(); |
| 412 |
updateStats(); |
| 413 |
applyView(); |
| 414 |
} |
| 415 |
|
| 416 |
/** |
| 417 |
* Re-index the optimistic DOM after a permitted drag or relative move. |
| 418 |
* |
| 419 |
* These positions are presentation state until the server returns its |
| 420 |
* authoritative order; no complete order array is placed on the wire. |
| 421 |
*/ |
| 422 |
function refreshPositions() { |
| 423 |
rows().each(function (index) { |
| 424 |
$(this).attr('data-field-order', index).find('.field-position').text((index + 1) + '. '); |
| 425 |
}); |
| 426 |
} |
| 427 |
|
| 428 |
/** |
| 429 |
* Translate one changed control into a compact one-field command. |
| 430 |
* |
| 431 |
* Mapping exclusivity is reflected optimistically before enqueueing. The |
| 432 |
* authoritative response later replaces normalized text and destinations. |
| 433 |
*/ |
| 434 |
$container.on('change', '.mlsimport-import-checkbox, .mlsimport-admin-checkbox, .mlsimport-label-input, .mlsimport-postmeta-input, .mlsimport-taxonomy-select', function () { |
| 435 |
var $control = $(this); |
| 436 |
var $row = $control.closest('.mlsimport-field-row'); |
| 437 |
var property; |
| 438 |
var value; |
| 439 |
|
| 440 |
if ($control.hasClass('mlsimport-import-checkbox')) { |
| 441 |
property = 'import'; |
| 442 |
value = $control.prop('checked') ? 1 : 0; |
| 443 |
} else if ($control.hasClass('mlsimport-admin-checkbox')) { |
| 444 |
property = 'admin'; |
| 445 |
value = $control.prop('checked') ? 1 : 0; |
| 446 |
} else if ($control.hasClass('mlsimport-label-input')) { |
| 447 |
property = 'label'; |
| 448 |
value = $control.val(); |
| 449 |
} else if ($control.hasClass('mlsimport-postmeta-input')) { |
| 450 |
property = 'postmeta'; |
| 451 |
value = $control.val(); |
| 452 |
if ($.trim(value) !== '') { |
| 453 |
$row.find('.mlsimport-taxonomy-select').val(''); |
| 454 |
} |
| 455 |
} else { |
| 456 |
property = 'taxonomy'; |
| 457 |
value = $control.val(); |
| 458 |
if (value !== '') { |
| 459 |
$row.find('.mlsimport-postmeta-input').val(''); |
| 460 |
} |
| 461 |
} |
| 462 |
|
| 463 |
enqueue({ type: 'set', field: $row.attr('data-field-key'), property: property, value: value }); |
| 464 |
updateStats(); |
| 465 |
applyView(); |
| 466 |
}); |
| 467 |
|
| 468 |
/** |
| 469 |
* Change and send only active rows currently visible to the administrator. |
| 470 |
* |
| 471 |
* Sending explicit field keys makes the server-side bulk transition |
| 472 |
* independent of search text, filters, pagination, or POST variable count. |
| 473 |
*/ |
| 474 |
$container.on('click', '#mlsimport-select-all-import, #mlsimport-select-none-import, #mlsimport-select-all-admin, #mlsimport-select-none-admin', function () { |
| 475 |
var id = this.id; |
| 476 |
var property = id.indexOf('admin') !== -1 ? 'admin' : 'import'; |
| 477 |
var value = id.indexOf('none') === -1 ? 1 : 0; |
| 478 |
var selector = property === 'admin' ? '.mlsimport-admin-checkbox' : '.mlsimport-import-checkbox'; |
| 479 |
var $visibleRows = rows().filter(':visible'); |
| 480 |
var fieldKeys = $visibleRows.map(function () { return $(this).attr('data-field-key'); }).get(); |
| 481 |
|
| 482 |
if (fieldKeys.length === 0) { |
| 483 |
return; |
| 484 |
} |
| 485 |
$visibleRows.find(selector).prop('checked', value === 1); |
| 486 |
enqueue({ type: 'bulk', property: property, fields: fieldKeys, value: value }); |
| 487 |
updateStats(); |
| 488 |
applyView(); |
| 489 |
}); |
| 490 |
|
| 491 |
/** |
| 492 |
* Convert move buttons into the same relative command as drag-and-drop. |
| 493 |
* |
| 494 |
* Anchors are the nearest VISIBLE rows so that, in a filtered view, one |
| 495 |
* click jumps past hidden neighbors instead of swapping with them |
| 496 |
* invisibly. The DOM moves immediately, then the queue sends only the |
| 497 |
* moving field, anchor field, and before/after position. |
| 498 |
*/ |
| 499 |
$container.on('click', '.mlsimport-move-btn', function () { |
| 500 |
var $row = $(this).closest('.mlsimport-field-row'); |
| 501 |
var $anchor; |
| 502 |
var position; |
| 503 |
|
| 504 |
if ($(this).hasClass('mlsimport-move-up')) { |
| 505 |
$anchor = $row.prevAll('.mlsimport-field-row:visible').first(); |
| 506 |
position = 'before'; |
| 507 |
} else if ($(this).hasClass('mlsimport-move-down')) { |
| 508 |
$anchor = $row.nextAll('.mlsimport-field-row:visible').first(); |
| 509 |
position = 'after'; |
| 510 |
} else if ($(this).hasClass('mlsimport-move-top')) { |
| 511 |
$anchor = rows().filter(':visible').first(); |
| 512 |
position = 'before'; |
| 513 |
} else { |
| 514 |
$anchor = rows().filter(':visible').last(); |
| 515 |
position = 'after'; |
| 516 |
} |
| 517 |
|
| 518 |
if (!$anchor.length || $anchor.is($row)) { |
| 519 |
return; |
| 520 |
} |
| 521 |
// Keep the clicked button under the cursor: measure its position |
| 522 |
// before the move, then scroll by its displacement so repeated |
| 523 |
// clicks walk the row without chasing the button down the page. |
| 524 |
var buttonTop = $(this).offset().top; |
| 525 |
position === 'before' ? $row.insertBefore($anchor) : $row.insertAfter($anchor); |
| 526 |
refreshPositions(); |
| 527 |
window.scrollBy(0, $(this).offset().top - buttonTop); |
| 528 |
enqueue({ type: 'move', field: $row.attr('data-field-key'), anchor: $anchor.attr('data-field-key'), position: position }); |
| 529 |
}); |
| 530 |
|
| 531 |
/** |
| 532 |
* Derive one moving/anchor command from a completed sortable interaction. |
| 533 |
* |
| 534 |
* Reordering is enabled only for the unfiltered custom-order view, so the |
| 535 |
* adjacent anchor always belongs to the complete active configuration. |
| 536 |
*/ |
| 537 |
if ($.fn.sortable) { |
| 538 |
$body.sortable({ |
| 539 |
items: '> .mlsimport-field-row', |
| 540 |
handle: '.mlsimport-field-name', |
| 541 |
update: function (_, ui) { |
| 542 |
var $row = ui.item; |
| 543 |
var $anchor = $row.prev('.mlsimport-field-row'); |
| 544 |
var position = 'after'; |
| 545 |
if (!$anchor.length) { |
| 546 |
$anchor = $row.next('.mlsimport-field-row'); |
| 547 |
position = 'before'; |
| 548 |
} |
| 549 |
refreshPositions(); |
| 550 |
if ($anchor.length) { |
| 551 |
enqueue({ type: 'move', field: $row.attr('data-field-key'), anchor: $anchor.attr('data-field-key'), position: position }); |
| 552 |
} |
| 553 |
} |
| 554 |
}); |
| 555 |
} |
| 556 |
|
| 557 |
$search.on('input', applyView); |
| 558 |
$filter.add($sort).on('change', applyView); |
| 559 |
updateStats(); |
| 560 |
applyView(); |
| 561 |
} |
| 562 |
})(jQuery); |
| 563 |
|