| 1 |
/** |
| 2 |
* Customizer control JS for the MLSImport "arrange sections" fields. |
| 3 |
* |
| 4 |
* Registers the wp.customize.controlConstructor for the PHP control type |
| 5 |
* 'mlsimport_sections'. Renders an Enabled and a Disabled list; each item can move |
| 6 |
* up/down within its list or flip between lists. Every change writes the same |
| 7 |
* { active:[...], inactive:[...] } object back to the setting, which is the shared |
| 8 |
* mlsimport_standalone_options[...] key. See class-mlsimport-customize-sections-control.php. |
| 9 |
*/ |
| 10 |
( function ( wp, $ ) { |
| 11 |
'use strict'; |
| 12 |
|
| 13 |
// Bail if the Customizer API is not present (script loaded out of context). |
| 14 |
if ( ! wp || ! wp.customize ) { |
| 15 |
return; |
| 16 |
} |
| 17 |
|
| 18 |
// Land the preview on the listings archive so accent + card style are visible |
| 19 |
// on open. Only redirect away from the home page, and only once, so it never |
| 20 |
// fights the user navigating the preview to a property/agent page. |
| 21 |
wp.customize.bind( 'ready', function () { |
| 22 |
// Nothing to do without a configured archive URL. |
| 23 |
var cfg = window.mlsimportCustomizer || {}; |
| 24 |
if ( ! cfg.archiveUrl ) { |
| 25 |
return; |
| 26 |
} |
| 27 |
var previewer = wp.customize.previewer; |
| 28 |
// Current preview URL and the site home URL, trailing slash ignored. |
| 29 |
var current = previewer.previewUrl.get(); |
| 30 |
var home = ( wp.customize.settings && wp.customize.settings.url && wp.customize.settings.url.home ) || ''; |
| 31 |
// Only redirect when the preview is sitting on the home page. |
| 32 |
if ( home && current.replace( /\/$/, '' ) === home.replace( /\/$/, '' ) ) { |
| 33 |
previewer.previewUrl.set( cfg.archiveUrl ); |
| 34 |
} |
| 35 |
} ); |
| 36 |
|
| 37 |
// Custom control constructor for the PHP 'mlsimport_sections' control type. |
| 38 |
wp.customize.controlConstructor.mlsimport_sections = wp.customize.Control.extend( { |
| 39 |
/** |
| 40 |
* Initialise control state from params and render the two lists. |
| 41 |
*/ |
| 42 |
ready: function () { |
| 43 |
var control = this; |
| 44 |
|
| 45 |
// Build a slug -> label lookup from the catalog of available sections. |
| 46 |
control.catalog = control.params.catalog || []; |
| 47 |
control.labelFor = {}; |
| 48 |
control.catalog.forEach( function ( c ) { |
| 49 |
control.labelFor[ c.slug ] = c.label; |
| 50 |
} ); |
| 51 |
|
| 52 |
// Seed working state from the saved value (copied so we never mutate params). |
| 53 |
var value = control.params.mlsValue || { active: [], inactive: [] }; |
| 54 |
control.state = { |
| 55 |
active: ( value.active || [] ).slice(), |
| 56 |
inactive: ( value.inactive || [] ).slice() |
| 57 |
}; |
| 58 |
|
| 59 |
// Surface any catalog choice the saved value never mentions (e.g. a |
| 60 |
// section added after the user last saved) so it is never hidden. |
| 61 |
var known = control.state.active.concat( control.state.inactive ); |
| 62 |
control.catalog.forEach( function ( c ) { |
| 63 |
if ( known.indexOf( c.slug ) === -1 ) { |
| 64 |
control.state.active.push( c.slug ); |
| 65 |
} |
| 66 |
} ); |
| 67 |
|
| 68 |
control.renderLists(); |
| 69 |
}, |
| 70 |
|
| 71 |
/** |
| 72 |
* Re-render both lists from current state and re-bind their buttons. |
| 73 |
*/ |
| 74 |
renderLists: function () { |
| 75 |
var control = this; |
| 76 |
// Empty both list containers before repopulating. |
| 77 |
var $active = control.container.find( '.mlsimport-arranger__active' ).empty(); |
| 78 |
var $inactive = control.container.find( '.mlsimport-arranger__inactive' ).empty(); |
| 79 |
|
| 80 |
// Append one list item per slug in each list, tracking index + length. |
| 81 |
control.state.active.forEach( function ( slug, i ) { |
| 82 |
$active.append( control.itemHtml( slug, 'active', i, control.state.active.length ) ); |
| 83 |
} ); |
| 84 |
control.state.inactive.forEach( function ( slug, i ) { |
| 85 |
$inactive.append( control.itemHtml( slug, 'inactive', i, control.state.inactive.length ) ); |
| 86 |
} ); |
| 87 |
|
| 88 |
control.wire(); |
| 89 |
}, |
| 90 |
|
| 91 |
/** |
| 92 |
* Build the HTML for one arranger row. |
| 93 |
* |
| 94 |
* @param {string} slug Section slug. |
| 95 |
* @param {string} list Which list this row belongs to ('active'/'inactive'). |
| 96 |
* @param {number} i Row index within its list. |
| 97 |
* @param {number} len Total rows in the list (to decide up/down buttons). |
| 98 |
* @return {string} List-item markup. |
| 99 |
*/ |
| 100 |
itemHtml: function ( slug, list, i, len ) { |
| 101 |
// Human label (fall back to the slug), and the enable/disable toggle glyph. |
| 102 |
var label = this.labelFor[ slug ] || slug; |
| 103 |
var moveLabel = 'active' === list ? '✗' : '✓'; // ✗ disable / ✓ enable |
| 104 |
// Up button only when not first; down button only when not last. |
| 105 |
var up = i > 0 ? '<button type="button" class="button-link mlsimport-arranger__up" aria-label="Move up">↑</button>' : ''; |
| 106 |
var down = i < len - 1 ? '<button type="button" class="button-link mlsimport-arranger__down" aria-label="Move down">↓</button>' : ''; |
| 107 |
return '<li class="mlsimport-arranger__item" data-slug="' + slug + '" data-list="' + list + '">' |
| 108 |
+ '<span class="mlsimport-arranger__label">' + label + '</span>' |
| 109 |
+ '<span class="mlsimport-arranger__actions">' + up + down |
| 110 |
+ '<button type="button" class="button-link mlsimport-arranger__move" aria-label="Toggle">' + moveLabel + '</button>' |
| 111 |
+ '</span></li>'; |
| 112 |
}, |
| 113 |
|
| 114 |
/** |
| 115 |
* Bind up/down/toggle click handlers on every rendered row. |
| 116 |
*/ |
| 117 |
wire: function () { |
| 118 |
var control = this; |
| 119 |
control.container.find( '.mlsimport-arranger__item' ).each( function () { |
| 120 |
// Read the row's slug and which list it lives in from data attributes. |
| 121 |
var $item = $( this ); |
| 122 |
var slug = $item.data( 'slug' ); |
| 123 |
var list = $item.data( 'list' ); |
| 124 |
|
| 125 |
// Move up one position within the same list. |
| 126 |
$item.find( '.mlsimport-arranger__up' ).on( 'click', function () { |
| 127 |
control.move( list, slug, -1 ); |
| 128 |
} ); |
| 129 |
// Move down one position within the same list. |
| 130 |
$item.find( '.mlsimport-arranger__down' ).on( 'click', function () { |
| 131 |
control.move( list, slug, 1 ); |
| 132 |
} ); |
| 133 |
// Toggle the row between the active and inactive lists. |
| 134 |
$item.find( '.mlsimport-arranger__move' ).on( 'click', function () { |
| 135 |
control.flip( list, slug ); |
| 136 |
} ); |
| 137 |
} ); |
| 138 |
}, |
| 139 |
|
| 140 |
/** |
| 141 |
* Reorder a slug within its list by delta positions, then sync. |
| 142 |
* |
| 143 |
* @param {string} list List key ('active'/'inactive'). |
| 144 |
* @param {string} slug Slug to move. |
| 145 |
* @param {number} delta -1 to move up, +1 to move down. |
| 146 |
*/ |
| 147 |
move: function ( list, slug, delta ) { |
| 148 |
var arr = this.state[ list ]; |
| 149 |
var idx = arr.indexOf( slug ); |
| 150 |
var to = idx + delta; |
| 151 |
// Ignore if the slug is missing or the target index is out of bounds. |
| 152 |
if ( idx === -1 || to < 0 || to >= arr.length ) { |
| 153 |
return; |
| 154 |
} |
| 155 |
// Remove from the old index and re-insert at the target index. |
| 156 |
arr.splice( idx, 1 ); |
| 157 |
arr.splice( to, 0, slug ); |
| 158 |
this.sync(); |
| 159 |
}, |
| 160 |
|
| 161 |
/** |
| 162 |
* Move a slug from one list to the other (enable <-> disable), then sync. |
| 163 |
* |
| 164 |
* @param {string} from Source list key ('active'/'inactive'). |
| 165 |
* @param {string} slug Slug to flip. |
| 166 |
*/ |
| 167 |
flip: function ( from, slug ) { |
| 168 |
// Destination is the opposite list. |
| 169 |
var to = 'active' === from ? 'inactive' : 'active'; |
| 170 |
var idx = this.state[ from ].indexOf( slug ); |
| 171 |
// Nothing to do if the slug isn't in the source list. |
| 172 |
if ( idx === -1 ) { |
| 173 |
return; |
| 174 |
} |
| 175 |
// Remove from source, append to destination. |
| 176 |
this.state[ from ].splice( idx, 1 ); |
| 177 |
this.state[ to ].push( slug ); |
| 178 |
this.sync(); |
| 179 |
}, |
| 180 |
|
| 181 |
/** |
| 182 |
* Write current state back to the Customizer setting and re-render. |
| 183 |
*/ |
| 184 |
sync: function () { |
| 185 |
// A fresh object so the Customizer sees the value as changed. |
| 186 |
this.setting.set( { |
| 187 |
active: this.state.active.slice(), |
| 188 |
inactive: this.state.inactive.slice() |
| 189 |
} ); |
| 190 |
this.renderLists(); |
| 191 |
} |
| 192 |
} ); |
| 193 |
} )( window.wp, window.jQuery ); |
| 194 |
|