| 1 |
/** |
| 2 |
* MLS Listing metabox — tab switching, plus the Media tab gallery. |
| 3 |
* |
| 4 |
* Vanilla JS (no build, no jQuery dependency). Clicking a tab shows its pane and |
| 5 |
* remembers the choice so the same tab is active after the post is saved. The |
| 6 |
* Media tab additionally previews the Photos Count cap as it is typed and deletes |
| 7 |
* individual images over AJAX. |
| 8 |
*/ |
| 9 |
( function () { |
| 10 |
'use strict'; |
| 11 |
|
| 12 |
// Cookie name used to remember which metabox tab was last active |
| 13 |
var COOKIE = 'mlsimport_mb_tab'; |
| 14 |
|
| 15 |
/** |
| 16 |
* Persist the active tab slug in a site-wide cookie. |
| 17 |
* |
| 18 |
* @param {string} value - Tab slug to store. |
| 19 |
*/ |
| 20 |
function setCookie( value ) { |
| 21 |
document.cookie = COOKIE + '=' + encodeURIComponent( value ) + '; path=/'; |
| 22 |
} |
| 23 |
|
| 24 |
/** |
| 25 |
* Read the remembered tab slug from the cookie. |
| 26 |
* |
| 27 |
* @return {string} Stored slug, or '' when absent. |
| 28 |
*/ |
| 29 |
function getCookie() { |
| 30 |
var match = document.cookie.match( new RegExp( '(?:^|; )' + COOKIE + '=([^;]*)' ) ); |
| 31 |
return match ? decodeURIComponent( match[ 1 ] ) : ''; |
| 32 |
} |
| 33 |
|
| 34 |
/** |
| 35 |
* Activate the tab (and matching pane) identified by slug within a metabox. |
| 36 |
* |
| 37 |
* @param {Element} box - The metabox root element. |
| 38 |
* @param {string} slug - Tab slug to activate. |
| 39 |
* @return {boolean} True if a matching tab was found and activated. |
| 40 |
*/ |
| 41 |
function activate( box, slug ) { |
| 42 |
// Collect the tab buttons and content panes in this box |
| 43 |
var tabs = box.querySelectorAll( '.mlsimport-mb__tab' ); |
| 44 |
var panes = box.querySelectorAll( '.mlsimport-mb__pane' ); |
| 45 |
var found = false; |
| 46 |
|
| 47 |
// Toggle each tab's active state; track whether the slug matched any tab |
| 48 |
tabs.forEach( function ( tab ) { |
| 49 |
var on = tab.getAttribute( 'data-tab' ) === slug; |
| 50 |
tab.classList.toggle( 'is-active', on ); |
| 51 |
found = found || on; |
| 52 |
} ); |
| 53 |
// No tab matched: leave panes untouched and report failure |
| 54 |
if ( ! found ) { |
| 55 |
return false; |
| 56 |
} |
| 57 |
// Show the pane whose data-pane matches the slug, hide the rest |
| 58 |
panes.forEach( function ( pane ) { |
| 59 |
pane.classList.toggle( 'is-active', pane.getAttribute( 'data-pane' ) === slug ); |
| 60 |
} ); |
| 61 |
return true; |
| 62 |
} |
| 63 |
|
| 64 |
// On DOM ready, wire tab clicks and restore the remembered tab |
| 65 |
document.addEventListener( 'DOMContentLoaded', function () { |
| 66 |
// Locate the metabox; nothing to do if it isn't on this screen |
| 67 |
var box = document.querySelector( '.mlsimport-mb' ); |
| 68 |
if ( ! box ) { |
| 69 |
return; |
| 70 |
} |
| 71 |
|
| 72 |
// Delegate clicks so any tab within the box is handled |
| 73 |
box.addEventListener( 'click', function ( e ) { |
| 74 |
// Ignore clicks that didn't land on (or inside) a tab |
| 75 |
var tab = e.target.closest( '.mlsimport-mb__tab' ); |
| 76 |
if ( ! tab ) { |
| 77 |
return; |
| 78 |
} |
| 79 |
// Activate the clicked tab and, if successful, remember it |
| 80 |
var slug = tab.getAttribute( 'data-tab' ); |
| 81 |
if ( activate( box, slug ) ) { |
| 82 |
setCookie( slug ); |
| 83 |
} |
| 84 |
} ); |
| 85 |
|
| 86 |
// Restore the previously active tab from the cookie, if any |
| 87 |
var stored = getCookie(); |
| 88 |
if ( stored ) { |
| 89 |
activate( box, stored ); |
| 90 |
} |
| 91 |
|
| 92 |
wireGallery( box ); |
| 93 |
} ); |
| 94 |
|
| 95 |
/** |
| 96 |
* Apply the Photos Count cap to the tiles: the first N stay visible, the rest |
| 97 |
* are hidden. A blank or zero count means no cap. Mirrors the PHP rule in |
| 98 |
* mlsimport_property_gallery_ids() so the preview matches what gets published. |
| 99 |
* |
| 100 |
* @param {Element} box - The metabox root element. |
| 101 |
*/ |
| 102 |
function applyCount( box ) { |
| 103 |
var input = box.querySelector( '#mlsimport_PhotosCount' ); |
| 104 |
var tiles = box.querySelectorAll( '.mlsimport-mb__thumb' ); |
| 105 |
// A missing input (or no gallery) leaves every tile as the server rendered it |
| 106 |
if ( ! input || ! tiles.length ) { |
| 107 |
return; |
| 108 |
} |
| 109 |
|
| 110 |
// Anything not a positive number publishes the whole gallery |
| 111 |
var limit = parseInt( input.value, 10 ); |
| 112 |
if ( isNaN( limit ) || limit < 1 ) { |
| 113 |
limit = tiles.length; |
| 114 |
} |
| 115 |
|
| 116 |
// Hide every tile past the limit; show the ones within it |
| 117 |
tiles.forEach( function ( tile, index ) { |
| 118 |
tile.classList.toggle( 'is-beyond-count', index >= limit ); |
| 119 |
} ); |
| 120 |
|
| 121 |
// Keep the heading honest about how many of the stored images are published |
| 122 |
var title = box.querySelector( '.mlsimport-mb__gallery-title' ); |
| 123 |
if ( title && title.getAttribute( 'data-template' ) ) { |
| 124 |
var shown = Math.min( limit, tiles.length ); |
| 125 |
title.textContent = title |
| 126 |
.getAttribute( 'data-template' ) |
| 127 |
.replace( '%1$d', shown ) |
| 128 |
.replace( '%2$d', tiles.length ); |
| 129 |
} |
| 130 |
} |
| 131 |
|
| 132 |
/** |
| 133 |
* Wire the Media tab gallery: live Photos Count preview and per-image delete. |
| 134 |
* |
| 135 |
* @param {Element} box - The metabox root element. |
| 136 |
*/ |
| 137 |
function wireGallery( box ) { |
| 138 |
var gallery = box.querySelector( '.mlsimport-mb__gallery' ); |
| 139 |
// Listings with no imported images render no gallery at all |
| 140 |
if ( ! gallery ) { |
| 141 |
return; |
| 142 |
} |
| 143 |
|
| 144 |
// Re-apply the cap on every keystroke / spinner click |
| 145 |
var count = box.querySelector( '#mlsimport_PhotosCount' ); |
| 146 |
if ( count ) { |
| 147 |
count.addEventListener( 'input', function () { |
| 148 |
applyCount( box ); |
| 149 |
} ); |
| 150 |
} |
| 151 |
|
| 152 |
// Delegate delete clicks so tiles removed from the DOM need no cleanup |
| 153 |
gallery.addEventListener( 'click', function ( e ) { |
| 154 |
var btn = e.target.closest( '.mlsimport-mb__thumb-del' ); |
| 155 |
// Ignore clicks that landed on the thumbnail link or the grid itself |
| 156 |
if ( ! btn ) { |
| 157 |
return; |
| 158 |
} |
| 159 |
e.preventDefault(); |
| 160 |
deleteImage( box, gallery, btn ); |
| 161 |
} ); |
| 162 |
} |
| 163 |
|
| 164 |
/** |
| 165 |
* Delete one image over AJAX, then drop its tile and re-apply the cap. |
| 166 |
* |
| 167 |
* @param {Element} box - The metabox root element. |
| 168 |
* @param {Element} gallery - The gallery wrapper (carries data-post). |
| 169 |
* @param {Element} btn - The clicked delete button (carries data-id). |
| 170 |
*/ |
| 171 |
function deleteImage( box, gallery, btn ) { |
| 172 |
var cfg = window.mlsimportMetabox; |
| 173 |
// Without the localized config there is no nonce, so no request to make |
| 174 |
if ( ! cfg ) { |
| 175 |
return; |
| 176 |
} |
| 177 |
// Deleting is permanent — make the editor say yes first |
| 178 |
if ( ! window.confirm( cfg.confirm ) ) { |
| 179 |
return; |
| 180 |
} |
| 181 |
|
| 182 |
var tile = btn.closest( '.mlsimport-mb__thumb' ); |
| 183 |
// Disable while in flight so a double-click can't fire two deletes |
| 184 |
btn.disabled = true; |
| 185 |
|
| 186 |
var body = new URLSearchParams(); |
| 187 |
body.append( 'action', cfg.action ); |
| 188 |
body.append( 'nonce', cfg.nonce ); |
| 189 |
body.append( 'post_id', gallery.getAttribute( 'data-post' ) ); |
| 190 |
body.append( 'attach_id', btn.getAttribute( 'data-id' ) ); |
| 191 |
|
| 192 |
window |
| 193 |
.fetch( cfg.ajaxUrl, { |
| 194 |
method: 'POST', |
| 195 |
credentials: 'same-origin', |
| 196 |
body: body, |
| 197 |
} ) |
| 198 |
.then( function ( r ) { |
| 199 |
return r.json(); |
| 200 |
} ) |
| 201 |
.then( function ( res ) { |
| 202 |
// Server refused (bad nonce, no capability, not in this gallery) |
| 203 |
if ( ! res || ! res.success ) { |
| 204 |
throw new Error( 'refused' ); |
| 205 |
} |
| 206 |
// Gone for good: drop the tile, then re-cap the survivors |
| 207 |
tile.remove(); |
| 208 |
// The server may have promoted a new featured image — reflect it |
| 209 |
markFeatured( box, res.data ? res.data.featured : 0 ); |
| 210 |
applyCount( box ); |
| 211 |
} ) |
| 212 |
.catch( function () { |
| 213 |
btn.disabled = false; |
| 214 |
window.alert( cfg.failed ); |
| 215 |
} ); |
| 216 |
} |
| 217 |
|
| 218 |
/** |
| 219 |
* Move the "Featured" badge to the tile the server now reports as featured. |
| 220 |
* |
| 221 |
* @param {Element} box - The metabox root element. |
| 222 |
* @param {number} id - Attachment id of the new featured image (0 = none). |
| 223 |
*/ |
| 224 |
function markFeatured( box, id ) { |
| 225 |
var tiles = box.querySelectorAll( '.mlsimport-mb__thumb' ); |
| 226 |
tiles.forEach( function ( tile ) { |
| 227 |
var isFeatured = id && tile.getAttribute( 'data-id' ) === String( id ); |
| 228 |
var badge = tile.querySelector( '.mlsimport-mb__thumb-badge' ); |
| 229 |
|
| 230 |
tile.classList.toggle( 'is-featured', !! isFeatured ); |
| 231 |
|
| 232 |
// Add the badge when this tile just became featured |
| 233 |
if ( isFeatured && ! badge ) { |
| 234 |
badge = document.createElement( 'span' ); |
| 235 |
badge.className = 'mlsimport-mb__thumb-badge'; |
| 236 |
badge.textContent = window.mlsimportMetabox ? window.mlsimportMetabox.featured : 'Featured'; |
| 237 |
tile.appendChild( badge ); |
| 238 |
// Remove a stale badge from a tile that no longer holds the role |
| 239 |
} else if ( ! isFeatured && badge ) { |
| 240 |
badge.remove(); |
| 241 |
} |
| 242 |
} ); |
| 243 |
} |
| 244 |
} )(); |
| 245 |
|