| 1 |
var WP_Attachments = (function($) { |
| 2 |
"use strict"; |
| 3 |
|
| 4 |
var attachments = { |
| 5 |
mediaFrame: null, // <-- Add this line |
| 6 |
|
| 7 |
init: function() { |
| 8 |
this.cacheElements(); |
| 9 |
this.detachModal(); |
| 10 |
this.bindEvents(); |
| 11 |
this.initSortable(); |
| 12 |
this.refreshMoveButtons(); |
| 13 |
this.initializeFilePreview(); |
| 14 |
}, |
| 15 |
|
| 16 |
// The block editor renders metaboxes inside a transformed container. |
| 17 |
// A transformed ancestor becomes the containing block for |
| 18 |
// position:fixed, so the dialog would only cover the metabox panel |
| 19 |
// instead of the viewport. Re-parenting it to <body> fixes that. |
| 20 |
detachModal: function() { |
| 21 |
if (this.$previewModal.length && this.$previewModal.parent()[0] !== document.body) { |
| 22 |
this.$previewModal.appendTo(document.body); |
| 23 |
} |
| 24 |
}, |
| 25 |
|
| 26 |
cacheElements: function() { |
| 27 |
this.$container = $('#wpa-attachment-list'); |
| 28 |
this.$attachmentItems = $('.wpa-attachment-item'); |
| 29 |
this.$addMediaButton = $('.wpa_attach_file'); |
| 30 |
this.$previewModal = $('#wpa-preview-modal'); |
| 31 |
this.$previewContent = $('#wpa-preview-file'); |
| 32 |
this.$previewTitle = $('#wpa-preview-title'); |
| 33 |
this.$stats = $('#wpa-attachments-stats'); |
| 34 |
|
| 35 |
// Cache localized variables |
| 36 |
this.editMediaTitle = WP_Attachments_Vars.editMedia || 'Edit Media'; |
| 37 |
this.youSureText = WP_Attachments_Vars.youSure || 'Are you sure you want to do this?'; |
| 38 |
this.confirmDeleteText = WP_Attachments_Vars.confirmDelete || 'Are you sure you want to delete this permanently?'; |
| 39 |
this.postID = WP_Attachments_Vars.postID || 0; |
| 40 |
this.ajaxurl = WP_Attachments_Vars.ajaxurl || ''; |
| 41 |
this.nonce = WP_Attachments_Vars.nonce || ''; |
| 42 |
}, |
| 43 |
|
| 44 |
bindEvents: function() { |
| 45 |
var self = this; |
| 46 |
|
| 47 |
// Add media button click |
| 48 |
$(document).on('click', '.wpa_attach_file', this.handleAddMedia.bind(this)); |
| 49 |
|
| 50 |
// Visible move controls. Dragging alone is mouse-only and the grip |
| 51 |
// on its own was not discoverable. |
| 52 |
$(document).on('click', '.wpa-move-up, .wpa-move-down', function (e) { |
| 53 |
e.preventDefault(); |
| 54 |
self.moveItem( |
| 55 |
$(this).closest('.wpa-attachment-item'), |
| 56 |
$(this).hasClass('wpa-move-up'), |
| 57 |
this |
| 58 |
); |
| 59 |
}); |
| 60 |
|
| 61 |
// Arrow keys on the grip do the same thing. |
| 62 |
$(document).on('keydown', '.wpa-attachment-drag-handle', function (e) { |
| 63 |
var isUp = (e.key === 'ArrowUp' || e.keyCode === 38); |
| 64 |
var isDown = (e.key === 'ArrowDown' || e.keyCode === 40); |
| 65 |
if (!isUp && !isDown) { |
| 66 |
return; |
| 67 |
} |
| 68 |
|
| 69 |
e.preventDefault(); |
| 70 |
self.moveItem($(this).closest('.wpa-attachment-item'), isUp, this); |
| 71 |
}); |
| 72 |
|
| 73 |
// Edit: open the WordPress media modal instead of leaving the page. |
| 74 |
// Core saves the fields through its own save-attachment endpoint, |
| 75 |
// so there is no custom handler behind this. |
| 76 |
$(document).on('click', '.wpa-edit-attachment', function (e) { |
| 77 |
// Let modified clicks fall through to the full edit screen. |
| 78 |
if (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { |
| 79 |
return; |
| 80 |
} |
| 81 |
|
| 82 |
var id = parseInt($(this).closest('.wpa-attachment-item').data('attachmentid'), 10); |
| 83 |
if (!id || typeof wp === 'undefined' || !wp.media) { |
| 84 |
return; // no JS media library: the href still works |
| 85 |
} |
| 86 |
|
| 87 |
e.preventDefault(); |
| 88 |
self.openEditFrame(id); |
| 89 |
}); |
| 90 |
|
| 91 |
// Preview triggers. Delegated, because rows arrive via AJAX too. |
| 92 |
$(document).on('click', '.wpa-preview-trigger', function(e) { |
| 93 |
e.preventDefault(); |
| 94 |
var $trigger = $(this); |
| 95 |
self.previewFile( |
| 96 |
$trigger.data('url'), |
| 97 |
$trigger.data('mime'), |
| 98 |
$trigger.data('title'), |
| 99 |
this |
| 100 |
); |
| 101 |
}); |
| 102 |
|
| 103 |
// Preview modal close buttons |
| 104 |
$(document).on('click', '.wpa-preview-close', this.closePreviewModal.bind(this)); |
| 105 |
|
| 106 |
// Click outside preview modal to close |
| 107 |
$(document).on('click', '.wpa-preview-modal', function(e) { |
| 108 |
if (e.target === this) { |
| 109 |
self.closePreviewModal(); |
| 110 |
} |
| 111 |
}); |
| 112 |
|
| 113 |
// Keyboard events for modals |
| 114 |
$(document).on('keydown', this.handleKeyEvents.bind(this)); |
| 115 |
|
| 116 |
// Row unattach / delete: done in place, so unsaved edits to the |
| 117 |
// post survive. The href stays as the no-JS fallback. |
| 118 |
$(document).on('click', '.wpa-unattach-action, .wpa-delete-action', function(e) { |
| 119 |
// Modified clicks open the link as before. |
| 120 |
if (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { |
| 121 |
return; |
| 122 |
} |
| 123 |
|
| 124 |
var isDelete = $(this).hasClass('wpa-delete-action'); |
| 125 |
e.preventDefault(); |
| 126 |
|
| 127 |
if (!window.confirm(isDelete ? self.confirmDeleteText : self.youSureText)) { |
| 128 |
return; |
| 129 |
} |
| 130 |
|
| 131 |
var id = parseInt($(this).closest('.wpa-attachment-item').data('attachmentid'), 10); |
| 132 |
if (id) { |
| 133 |
self.runAction(isDelete ? 'delete' : 'unattach', id); |
| 134 |
} |
| 135 |
}); |
| 136 |
}, |
| 137 |
|
| 138 |
// Unattach or delete one file through AJAX, then drop its row. |
| 139 |
runAction: function(action, id) { |
| 140 |
var self = this; |
| 141 |
var $row = $('.wpa-attachment-item[data-attachmentid="' + id + '"]'); |
| 142 |
|
| 143 |
$row.addClass('is-busy'); |
| 144 |
|
| 145 |
$.ajax({ |
| 146 |
url: this.ajaxurl, |
| 147 |
type: 'POST', |
| 148 |
data: { |
| 149 |
action: action === 'delete' ? 'wpa_delete_media' : 'wpa_unattach_media', |
| 150 |
attachment_id: id, |
| 151 |
post_id: this.postID, |
| 152 |
nonce: this.nonce |
| 153 |
}, |
| 154 |
success: function(response) { |
| 155 |
if (!response.success || !response.data) { |
| 156 |
$row.removeClass('is-busy'); |
| 157 |
window.alert(WP_Attachments_Vars.actionFailed || 'The file could not be changed.'); |
| 158 |
return; |
| 159 |
} |
| 160 |
|
| 161 |
var data = response.data; |
| 162 |
|
| 163 |
$row.remove(); |
| 164 |
|
| 165 |
if (data.stats) { |
| 166 |
$('#wpa-attachments-stats').html(data.stats); |
| 167 |
} |
| 168 |
if (data.empty && !$('#wpa-attachment-list .wpa-attachment-item').length) { |
| 169 |
$('#wpa-attachment-list').html(data.empty); |
| 170 |
} |
| 171 |
|
| 172 |
self.cacheElements(); |
| 173 |
self.refreshMoveButtons(); |
| 174 |
|
| 175 |
$('#wpa-reorder-status').text(action === 'delete' |
| 176 |
? (WP_Attachments_Vars.fileDeleted || 'File deleted.') |
| 177 |
: (WP_Attachments_Vars.fileUnattached || 'File unattached.')); |
| 178 |
}, |
| 179 |
error: function(xhr) { |
| 180 |
$row.removeClass('is-busy'); |
| 181 |
// A JSON error is a refusal (permissions, file no longer |
| 182 |
// attached); anything else is a network or server problem. |
| 183 |
window.alert(xhr.responseJSON |
| 184 |
? (WP_Attachments_Vars.actionFailed || 'The file could not be changed.') |
| 185 |
: (WP_Attachments_Vars.requestFailed || 'The request failed. Please try again.')); |
| 186 |
} |
| 187 |
}); |
| 188 |
}, |
| 189 |
|
| 190 |
// Media modal scoped to a single attachment, for editing its details. |
| 191 |
openEditFrame: function (attachmentId) { |
| 192 |
var self = this; |
| 193 |
|
| 194 |
this.editFrames = this.editFrames || {}; |
| 195 |
|
| 196 |
if (this.editFrames[attachmentId]) { |
| 197 |
this.editFrames[attachmentId].open(); |
| 198 |
return; |
| 199 |
} |
| 200 |
|
| 201 |
var model = wp.media.attachment(attachmentId); |
| 202 |
|
| 203 |
var frame = wp.media({ |
| 204 |
title: this.editMediaTitle, |
| 205 |
// Restrict the library to this one file so the modal opens |
| 206 |
// straight on its details instead of the whole collection. |
| 207 |
library: { post__in: [attachmentId] }, |
| 208 |
button: { text: WP_Attachments_Vars.doneEditing || 'Done' }, |
| 209 |
multiple: false |
| 210 |
}); |
| 211 |
|
| 212 |
frame.on('open', function () { |
| 213 |
model.fetch(); |
| 214 |
frame.state().get('selection').add(model); |
| 215 |
}); |
| 216 |
|
| 217 |
// Core writes each field as it changes, so mirror it back into the |
| 218 |
// row as the user types rather than waiting for the modal to close. |
| 219 |
model.on('change:title', function (m) { |
| 220 |
self.updateRowTitle(attachmentId, m.get('title')); |
| 221 |
}); |
| 222 |
|
| 223 |
frame.on('select close', function () { |
| 224 |
self.updateRowTitle(attachmentId, model.get('title')); |
| 225 |
}); |
| 226 |
|
| 227 |
this.editFrames[attachmentId] = frame; |
| 228 |
frame.open(); |
| 229 |
}, |
| 230 |
|
| 231 |
// Keep the row in step with a title edited in the media modal. |
| 232 |
updateRowTitle: function (attachmentId, title) { |
| 233 |
if (typeof title !== 'string') { |
| 234 |
return; |
| 235 |
} |
| 236 |
|
| 237 |
var $row = $('.wpa-attachment-item[data-attachmentid="' + attachmentId + '"]'); |
| 238 |
if (!$row.length) { |
| 239 |
return; |
| 240 |
} |
| 241 |
|
| 242 |
var display = title.length ? title : (WP_Attachments_Vars.noTitle || '(no title)'); |
| 243 |
|
| 244 |
// .attr() alone is not enough: jQuery caches data attributes, and |
| 245 |
// the preview handler reads them through .data(). |
| 246 |
$row.attr('data-title', title).data('title', title); |
| 247 |
$row.find('.wpa-attachment-title a').text(display).attr('title', display); |
| 248 |
|
| 249 |
var $trigger = $row.find('.wpa-preview-trigger'); |
| 250 |
if ($trigger.length) { |
| 251 |
$trigger.attr('data-title', title).data('title', title); |
| 252 |
|
| 253 |
var template = WP_Attachments_Vars.previewLabel || 'Preview %s'; |
| 254 |
$trigger.find('.screen-reader-text').text(template.replace('%s', display)); |
| 255 |
} |
| 256 |
}, |
| 257 |
|
| 258 |
// Move one row up or down. Shared by the buttons and the arrow keys. |
| 259 |
moveItem: function ($item, up, focusEl) { |
| 260 |
var $sibling = up |
| 261 |
? $item.prev('.wpa-attachment-item') |
| 262 |
: $item.next('.wpa-attachment-item'); |
| 263 |
|
| 264 |
if (!$sibling.length) { |
| 265 |
return false; |
| 266 |
} |
| 267 |
|
| 268 |
if (up) { |
| 269 |
$item.insertBefore($sibling); |
| 270 |
} else { |
| 271 |
$item.insertAfter($sibling); |
| 272 |
} |
| 273 |
|
| 274 |
this.refreshMoveButtons(); |
| 275 |
|
| 276 |
// Moving the node in the DOM drops focus in some browsers. If the |
| 277 |
// control that was used just became disabled (the row reached an |
| 278 |
// end), fall back to the grip so focus never lands on <body>. |
| 279 |
if (focusEl) { |
| 280 |
if (focusEl.disabled) { |
| 281 |
$item.find('.wpa-attachment-drag-handle').trigger('focus'); |
| 282 |
} else { |
| 283 |
focusEl.focus(); |
| 284 |
} |
| 285 |
} |
| 286 |
|
| 287 |
this.announcePosition($item); |
| 288 |
this.handleReorder(); |
| 289 |
|
| 290 |
return true; |
| 291 |
}, |
| 292 |
|
| 293 |
// Grey out the moves that would do nothing. |
| 294 |
refreshMoveButtons: function () { |
| 295 |
var $items = $('#wpa-attachment-list').find('.wpa-attachment-item'); |
| 296 |
var last = $items.length - 1; |
| 297 |
|
| 298 |
$items.each(function (i) { |
| 299 |
$(this).find('.wpa-move-up').prop('disabled', i === 0); |
| 300 |
$(this).find('.wpa-move-down').prop('disabled', i === last); |
| 301 |
}); |
| 302 |
}, |
| 303 |
|
| 304 |
// Tell screen readers where the row ended up. |
| 305 |
announcePosition: function ($item) { |
| 306 |
var $status = $('#wpa-reorder-status'); |
| 307 |
if (!$status.length) { |
| 308 |
return; |
| 309 |
} |
| 310 |
|
| 311 |
var $all = $('#wpa-attachment-list').find('.wpa-attachment-item'); |
| 312 |
var position = $all.index($item) + 1; |
| 313 |
var template = WP_Attachments_Vars.movedTo || 'Moved to position %1$s of %2$s'; |
| 314 |
|
| 315 |
$status.text( |
| 316 |
template.replace('%1$s', position).replace('%2$s', $all.length) |
| 317 |
); |
| 318 |
}, |
| 319 |
|
| 320 |
initSortable: function() { |
| 321 |
var self = this; |
| 322 |
|
| 323 |
if (!this.$container.length) { |
| 324 |
return; |
| 325 |
} |
| 326 |
|
| 327 |
// Already initialised: just let jQuery UI pick up new items. |
| 328 |
// Re-running .sortable() would stack duplicate instances, and each |
| 329 |
// one would fire its own reorder request. |
| 330 |
if (this.$container.hasClass('ui-sortable')) { |
| 331 |
this.$container.sortable('refresh'); |
| 332 |
return; |
| 333 |
} |
| 334 |
|
| 335 |
this.$container.sortable({ |
| 336 |
items: '.wpa-attachment-item', |
| 337 |
handle: '.wpa-attachment-drag-handle', |
| 338 |
cursor: 'move', |
| 339 |
opacity: 0.7, |
| 340 |
placeholder: 'wpa-attachment-item ui-sortable-placeholder', |
| 341 |
forcePlaceholderSize: true, |
| 342 |
tolerance: 'pointer', |
| 343 |
containment: 'parent', |
| 344 |
// jQuery UI's default cancel selector is |
| 345 |
// "input,textarea,button,select,option". The drag handle is a |
| 346 |
// <button> (so it is keyboard reachable), which that default |
| 347 |
// silently refuses to drag. Drags can only start on `handle` |
| 348 |
// anyway, so dropping "button" here is safe. |
| 349 |
cancel: 'input, textarea, select, option', |
| 350 |
start: function(e, ui) { |
| 351 |
ui.item.addClass('ui-sortable-helper'); |
| 352 |
ui.placeholder.height(ui.item.height()); |
| 353 |
}, |
| 354 |
stop: function(e, ui) { |
| 355 |
ui.item.removeClass('ui-sortable-helper'); |
| 356 |
}, |
| 357 |
// 'update' fires once, and only when the order actually changed. |
| 358 |
update: function() { |
| 359 |
self.refreshMoveButtons(); |
| 360 |
self.handleReorder(); |
| 361 |
}, |
| 362 |
change: function(e, ui) { |
| 363 |
// Visual feedback during drag |
| 364 |
ui.placeholder.addClass('ui-sortable-placeholder-active'); |
| 365 |
} |
| 366 |
}); |
| 367 |
}, |
| 368 |
|
| 369 |
initializeFilePreview: function() { |
| 370 |
// Make preview functionality globally available |
| 371 |
window.wpaPreviewFile = this.previewFile.bind(this); |
| 372 |
window.wpaClosePreviewModal = this.closePreviewModal.bind(this); |
| 373 |
}, |
| 374 |
|
| 375 |
handleAddMedia: function(e) { |
| 376 |
e.preventDefault(); |
| 377 |
|
| 378 |
var self = this; |
| 379 |
|
| 380 |
// Use the object property, not a local variable |
| 381 |
if (this.mediaFrame) { |
| 382 |
this.mediaFrame.open(); |
| 383 |
return; |
| 384 |
} |
| 385 |
|
| 386 |
this.mediaFrame = wp.media({ |
| 387 |
title: WP_Attachments_Vars.mediaFrameTitle || 'Add Media Attachments', |
| 388 |
button: { |
| 389 |
text: WP_Attachments_Vars.mediaFrameButton || 'Attach to Post' |
| 390 |
}, |
| 391 |
multiple: true |
| 392 |
}); |
| 393 |
|
| 394 |
this.mediaFrame.on('select', function() { |
| 395 |
var selection = self.mediaFrame.state().get('selection'); |
| 396 |
var postId = parseInt(self.postID); |
| 397 |
|
| 398 |
if (!postId) { |
| 399 |
window.alert(WP_Attachments_Vars.saveFirst || 'Please save this content before adding attachments.'); |
| 400 |
return; |
| 401 |
} |
| 402 |
|
| 403 |
selection.each(function(attachment) { |
| 404 |
// Try to get parent ID from multiple possible locations |
| 405 |
var attributes = attachment.attributes; |
| 406 |
var currentParent = parseInt(attributes.uploadedTo || attributes.parent || 0); |
| 407 |
var attachmentTitle = attributes.title || 'this file'; |
| 408 |
var proceed = true; |
| 409 |
|
| 410 |
// If file has a parent and it's not the current post |
| 411 |
// Note: currentParent might be a string like "123" or "0" |
| 412 |
if (currentParent > 0 && currentParent !== postId) { |
| 413 |
var parentTitle = attributes.uploadedToTitle || (attributes.parentObj ? attributes.parentObj.post_title : 'another content'); |
| 414 |
|
| 415 |
var warning = WP_Attachments_Vars.reattachWarning || |
| 416 |
'The file "%1$s" is already attached to "%2$s" (ID: %3$s).'; |
| 417 |
|
| 418 |
proceed = window.confirm( |
| 419 |
warning |
| 420 |
.replace('%1$s', attachmentTitle) |
| 421 |
.replace('%2$s', parentTitle) |
| 422 |
.replace('%3$s', currentParent) |
| 423 |
); |
| 424 |
} |
| 425 |
|
| 426 |
if (proceed) { |
| 427 |
self.attachFileToPost(attachment.id, postId); |
| 428 |
} |
| 429 |
}); |
| 430 |
}); |
| 431 |
|
| 432 |
this.mediaFrame.open(); |
| 433 |
}, |
| 434 |
|
| 435 |
attachFileToPost: function(attachmentId, postId) { |
| 436 |
var self = this; |
| 437 |
|
| 438 |
$.ajax({ |
| 439 |
url: this.ajaxurl, |
| 440 |
type: 'POST', |
| 441 |
data: { |
| 442 |
action: 'wpa_attach_media', // renamed from ij_attach_media |
| 443 |
attachment_id: attachmentId, |
| 444 |
post_id: postId, |
| 445 |
nonce: this.nonce |
| 446 |
}, |
| 447 |
success: function(response) { |
| 448 |
if (response.success && response.data && response.data.html) { |
| 449 |
// Re-query: the editor may have re-rendered the metabox |
| 450 |
// since the last cache. |
| 451 |
self.$container = $('#wpa-attachment-list'); |
| 452 |
if (!self.$container.length) { |
| 453 |
return; |
| 454 |
} |
| 455 |
|
| 456 |
// Remove "no attachments" placeholder if present |
| 457 |
$('.wpa-no-attachments').remove(); |
| 458 |
|
| 459 |
// Append new attachment to the list |
| 460 |
var $newItem = $(response.data.html); |
| 461 |
self.$container.append($newItem.hide().fadeIn(400)); |
| 462 |
|
| 463 |
// Keep the counters in sync without a page reload |
| 464 |
if (response.data.stats) { |
| 465 |
$('#wpa-attachments-stats').html(response.data.stats); |
| 466 |
} |
| 467 |
|
| 468 |
// Re-cache elements and let sortable pick up the new row |
| 469 |
self.cacheElements(); |
| 470 |
self.initSortable(); |
| 471 |
self.refreshMoveButtons(); |
| 472 |
// Persist the new order |
| 473 |
self.handleReorder(); |
| 474 |
} else { |
| 475 |
console.error('Failed to attach media:', response.data); |
| 476 |
} |
| 477 |
}, |
| 478 |
error: function(xhr, status, error) { |
| 479 |
console.error('AJAX Error:', error); |
| 480 |
} |
| 481 |
}); |
| 482 |
}, |
| 483 |
|
| 484 |
previewFile: function(url, mimeType, title, trigger) { |
| 485 |
if (!url || !mimeType) return; |
| 486 |
|
| 487 |
// Remember what opened the modal so focus can go back there. |
| 488 |
this.lastTrigger = trigger || null; |
| 489 |
|
| 490 |
this.$previewTitle.text(title || WP_Attachments_Vars.previewFallback || 'File Preview'); |
| 491 |
this.$previewContent.empty(); |
| 492 |
|
| 493 |
var $previewElement; |
| 494 |
|
| 495 |
// Handle different file types |
| 496 |
if (mimeType.startsWith('image/')) { |
| 497 |
$previewElement = $('<img>').attr({ |
| 498 |
src: url, |
| 499 |
alt: title, |
| 500 |
style: 'max-width: 100%; max-height: 70vh; object-fit: contain;' |
| 501 |
}); |
| 502 |
} else if (mimeType.startsWith('video/')) { |
| 503 |
$previewElement = $('<video controls>').attr({ |
| 504 |
src: url, |
| 505 |
style: 'max-width: 100%; max-height: 70vh;' |
| 506 |
}).prop('preload', 'metadata'); |
| 507 |
} else if (mimeType.startsWith('audio/')) { |
| 508 |
$previewElement = $('<audio controls>').attr({ |
| 509 |
src: url, |
| 510 |
style: 'width: 100%;' |
| 511 |
}).prop('preload', 'metadata'); |
| 512 |
} else if (mimeType === 'application/pdf') { |
| 513 |
$previewElement = $('<iframe>').attr({ |
| 514 |
src: url + '#toolbar=1&navpanes=1&scrollbar=1', |
| 515 |
style: 'width: 100%; height: 70vh; border: none;' |
| 516 |
}); |
| 517 |
} else if (mimeType.startsWith('text/') || mimeType.includes('json') || mimeType.includes('xml')) { |
| 518 |
// For text files, try to load and display content |
| 519 |
$previewElement = $('<div>').addClass('text-preview').text('Loading...'); |
| 520 |
|
| 521 |
$.get(url) |
| 522 |
.done(function(data) { |
| 523 |
$previewElement.html('<pre style="background: #f1f1f1; padding: 15px; border-radius: 4px; overflow: auto; max-height: 60vh;">' + |
| 524 |
$('<div>').text(data).html() + '</pre>'); |
| 525 |
}) |
| 526 |
.fail(function() { |
| 527 |
$previewElement.html('<p>Unable to preview this text file. <a href="' + url + '" target="_blank">Download instead</a></p>'); |
| 528 |
}); |
| 529 |
} else { |
| 530 |
// For other file types, offer a download link. |
| 531 |
// Built with jQuery rather than string concatenation so the URL |
| 532 |
// can never break out of the attribute. |
| 533 |
$previewElement = $('<div>').addClass('file-preview-placeholder') |
| 534 |
.css({ textAlign: 'center', padding: '40px 20px' }); |
| 535 |
|
| 536 |
$previewElement.append( |
| 537 |
$('<div>').addClass('dashicons dashicons-media-default') |
| 538 |
.css({ fontSize: '48px', width: '48px', height: '48px', color: '#c3c4c7' }) |
| 539 |
); |
| 540 |
$previewElement.append( |
| 541 |
$('<h3>').text(WP_Attachments_Vars.previewUnavailable || 'Preview not available') |
| 542 |
); |
| 543 |
$previewElement.append( |
| 544 |
$('<a>').addClass('button button-primary') |
| 545 |
.attr({ href: url, target: '_blank', rel: 'noopener noreferrer' }) |
| 546 |
.text(WP_Attachments_Vars.downloadFile || 'Download file') |
| 547 |
); |
| 548 |
} |
| 549 |
|
| 550 |
this.$previewContent.append($previewElement); |
| 551 |
this.openPreviewModal(); |
| 552 |
}, |
| 553 |
|
| 554 |
openPreviewModal: function() { |
| 555 |
this.$previewModal.addClass('is-open').attr('aria-hidden', 'false'); |
| 556 |
$('body').addClass('wpa-modal-open'); |
| 557 |
|
| 558 |
var $close = this.$previewModal.find('.wpa-preview-close'); |
| 559 |
if ($close.length) { |
| 560 |
$close.trigger('focus'); |
| 561 |
} |
| 562 |
}, |
| 563 |
|
| 564 |
closePreviewModal: function() { |
| 565 |
if (!this.$previewModal || !this.$previewModal.hasClass('is-open')) { |
| 566 |
return; |
| 567 |
} |
| 568 |
|
| 569 |
this.$previewModal.removeClass('is-open').attr('aria-hidden', 'true'); |
| 570 |
$('body').removeClass('wpa-modal-open'); |
| 571 |
|
| 572 |
// Stop playback, then drop the nodes so nothing keeps buffering. |
| 573 |
this.$previewContent.find('video, audio').each(function() { |
| 574 |
if (this.pause) this.pause(); |
| 575 |
this.currentTime = 0; |
| 576 |
}); |
| 577 |
this.$previewContent.empty(); |
| 578 |
|
| 579 |
if (this.lastTrigger) { |
| 580 |
$(this.lastTrigger).trigger('focus'); |
| 581 |
this.lastTrigger = null; |
| 582 |
} |
| 583 |
}, |
| 584 |
|
| 585 |
handleKeyEvents: function(e) { |
| 586 |
// Only act while the modal is actually open. |
| 587 |
if (!this.$previewModal || !this.$previewModal.hasClass('is-open')) { |
| 588 |
return; |
| 589 |
} |
| 590 |
|
| 591 |
if (e.key === 'Escape' || e.keyCode === 27) { |
| 592 |
this.closePreviewModal(); |
| 593 |
return; |
| 594 |
} |
| 595 |
|
| 596 |
if (e.key === 'Tab' || e.keyCode === 9) { |
| 597 |
this.trapFocus(e); |
| 598 |
} |
| 599 |
}, |
| 600 |
|
| 601 |
// Keep Tab inside the dialog while it is open. |
| 602 |
trapFocus: function(e) { |
| 603 |
var $focusable = this.$previewModal |
| 604 |
.find('a[href], button:not([disabled]), video[controls], audio[controls], iframe, [tabindex]:not([tabindex="-1"])') |
| 605 |
.filter(':visible'); |
| 606 |
|
| 607 |
if (!$focusable.length) { |
| 608 |
return; |
| 609 |
} |
| 610 |
|
| 611 |
var first = $focusable.first()[0]; |
| 612 |
var last = $focusable.last()[0]; |
| 613 |
|
| 614 |
if (e.shiftKey && document.activeElement === first) { |
| 615 |
e.preventDefault(); |
| 616 |
last.focus(); |
| 617 |
} else if (!e.shiftKey && document.activeElement === last) { |
| 618 |
e.preventDefault(); |
| 619 |
first.focus(); |
| 620 |
} |
| 621 |
}, |
| 622 |
|
| 623 |
handleReorder: function() { |
| 624 |
var attachmentIds = []; |
| 625 |
|
| 626 |
this.$container.find('.wpa-attachment-item').each(function() { |
| 627 |
var id = $(this).data('attachmentid'); |
| 628 |
if (id) { |
| 629 |
attachmentIds.push(id); |
| 630 |
} |
| 631 |
}); |
| 632 |
|
| 633 |
if (attachmentIds.length === 0) return; |
| 634 |
|
| 635 |
// Send new order to server |
| 636 |
$.ajax({ |
| 637 |
url: this.ajaxurl, |
| 638 |
type: 'POST', |
| 639 |
data: { |
| 640 |
action: 'wpa_realign', // renamed from ij_realign |
| 641 |
alignment: attachmentIds, |
| 642 |
nonce: this.nonce |
| 643 |
}, |
| 644 |
success: function(response) { |
| 645 |
if (!response.success) { |
| 646 |
console.error('Failed to reorder attachments:', response.data); |
| 647 |
} |
| 648 |
}, |
| 649 |
error: function(xhr, status, error) { |
| 650 |
console.error('AJAX Error during reorder:', error); |
| 651 |
} |
| 652 |
}); |
| 653 |
} |
| 654 |
}; |
| 655 |
|
| 656 |
// Initialize when document is ready |
| 657 |
$(document).ready(function() { |
| 658 |
if (!window.WP_Attachments_initialized) { |
| 659 |
attachments.init(); |
| 660 |
window.WP_Attachments_initialized = true; |
| 661 |
} |
| 662 |
|
| 663 |
// Re-initialize after AJAX requests that might add new content |
| 664 |
$(document).ajaxComplete(function(event, xhr, settings) { |
| 665 |
if (settings.url && settings.url.indexOf('admin-ajax.php') !== -1) { |
| 666 |
attachments.cacheElements(); |
| 667 |
} |
| 668 |
}); |
| 669 |
}); |
| 670 |
|
| 671 |
// Make the object globally available for debugging |
| 672 |
window.WP_Attachments = attachments; |
| 673 |
|
| 674 |
return attachments; |
| 675 |
|
| 676 |
})(jQuery); |