Files
Jacek Pyziak 5014b9108f feat(media-folder-pro): add virtual folder system for WordPress media library
Custom WordPress plugin that replaces the default flat media library with
a structured folder view. Features: hierarchical folders via custom taxonomy,
sidebar folder tree, drag & drop, modal integration with Elementor/builders,
bulk assign, upload auto-assign, toast notifications.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:08:49 +01:00

230 lines
6.9 KiB
JavaScript

/**
* Media Folder Pro — Modal Integration
* Adds folder dropdown filter to wp.media modal and auto-assigns uploads.
*/
( function () {
'use strict';
if ( ! window.wp || ! wp.media || ! wp.media.view ) return;
const { ajaxUrl, nonce, i18n } = window.mfpData || {};
if ( ! ajaxUrl ) return;
let activeFolderId = 0;
let foldersCache = null;
// ─── AJAX helper ──────────────────────────────────────────
function ajax( action, data ) {
data = data || {};
const body = new URLSearchParams();
body.append( 'action', action );
body.append( 'nonce', nonce );
for ( const [ k, v ] of Object.entries( data ) ) {
body.append( k, v );
}
return fetch( ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
body: body,
} ).then( function ( r ) { return r.json(); } );
}
// ─── Load folders ─────────────────────────────────────────
function loadFolders() {
if ( foldersCache ) {
return Promise.resolve( foldersCache );
}
return ajax( 'mfp_get_folders' ).then( function ( res ) {
if ( res.success ) {
foldersCache = res.data.folders;
return foldersCache;
}
return [];
} );
}
function flattenFolders( folders, depth ) {
depth = depth || 0;
var result = [];
for ( var i = 0; i < folders.length; i++ ) {
var folder = folders[ i ];
var prefix = depth > 0 ? '\u2003'.repeat( depth ) + '\u2014 ' : '';
result.push( {
value: folder.id,
label: prefix + folder.name,
count: folder.count,
} );
if ( folder.children && folder.children.length > 0 ) {
result = result.concat( flattenFolders( folder.children, depth + 1 ) );
}
}
return result;
}
// ─── Build a folder <select> element ──────────────────────
function buildFolderSelect( onChange ) {
var select = document.createElement( 'select' );
select.className = 'mfp-modal-filter attachment-filters';
var defaultOpt = document.createElement( 'option' );
defaultOpt.value = '0';
defaultOpt.textContent = i18n.allMedia;
select.appendChild( defaultOpt );
loadFolders().then( function ( folders ) {
var flat = flattenFolders( folders );
for ( var j = 0; j < flat.length; j++ ) {
var opt = document.createElement( 'option' );
opt.value = String( flat[ j ].value );
opt.textContent = flat[ j ].label + ( flat[ j ].count > 0 ? ' (' + flat[ j ].count + ')' : '' );
select.appendChild( opt );
}
if ( activeFolderId ) {
select.value = String( activeFolderId );
}
} );
select.addEventListener( 'change', function () {
var folderId = parseInt( select.value, 10 );
activeFolderId = folderId || 0;
if ( onChange ) onChange( folderId );
// Sync all folder selects in the modal
syncAllSelects( folderId );
} );
return select;
}
// Keep all folder selects in sync (library toolbar + upload tab)
var allSelects = [];
function syncAllSelects( folderId ) {
for ( var i = 0; i < allSelects.length; i++ ) {
allSelects[ i ].value = String( folderId || 0 );
}
}
// ─── Override AttachmentsBrowser toolbar ───────────────────
var OriginalBrowser = wp.media.view.AttachmentsBrowser;
wp.media.view.AttachmentsBrowser = OriginalBrowser.extend( {
createToolbar: function () {
OriginalBrowser.prototype.createToolbar.call( this );
var toolbar = this.toolbar;
var collection = this.collection;
var select = buildFolderSelect( function ( folderId ) {
if ( folderId > 0 ) {
collection.props.set( { media_folder: folderId } );
} else {
collection.props.set( { media_folder: 0 } );
}
} );
allSelects.push( select );
var FilterView = wp.media.View.extend( {
tagName: 'div',
className: 'mfp-modal-filter-wrap',
render: function () {
this.$el.append( select );
return this;
},
} );
toolbar.set( 'mfpFolderFilter', new FilterView( {
controller: this.controller,
priority: -75,
} ) );
},
} );
// ─── Add folder select to Upload tab ──────────────────────
var OriginalUploaderInline = wp.media.view.UploaderInline;
wp.media.view.UploaderInline = OriginalUploaderInline.extend( {
ready: function () {
OriginalUploaderInline.prototype.ready.apply( this, arguments );
// Don't add twice
if ( this.$el.find( '.mfp-upload-folder-wrap' ).length ) return;
var wrap = document.createElement( 'div' );
wrap.className = 'mfp-upload-folder-wrap';
var label = document.createElement( 'label' );
label.className = 'mfp-upload-folder-label';
label.textContent = ( i18n.moveToFolder || 'Wyślij do folderu' ) + ': ';
var select = buildFolderSelect( function () {
// Just updates activeFolderId via buildFolderSelect's change handler
} );
allSelects.push( select );
wrap.appendChild( label );
wrap.appendChild( select );
// Insert before the upload area
this.$el.prepend( wrap );
},
} );
// ─── Inject folder ID into plupload params ──────────────
if ( wp.Uploader ) {
var origUploaderInit = wp.Uploader.prototype.init;
wp.Uploader.prototype.init = function () {
origUploaderInit.apply( this, arguments );
var uploader = this.uploader;
if ( ! uploader ) return;
uploader.bind( 'BeforeUpload', function () {
if ( activeFolderId > 0 ) {
uploader.settings.multipart_params.mfp_folder_id = activeFolderId;
} else {
delete uploader.settings.multipart_params.mfp_folder_id;
}
} );
uploader.bind( 'FileUploaded', function () {
document.dispatchEvent( new CustomEvent( 'mfp-folder-changed' ) );
// Re-query the library so new file appears.
// Don't use reset() — it clears the grid and shows "no items".
// Instead, toggle the prop to force a fresh AJAX query.
setTimeout( function () {
if ( ! wp.media.frame ) return;
try {
var state = wp.media.frame.state();
var lib = state && state.get( 'library' );
if ( lib && lib.props ) {
var folder = lib.props.get( 'media_folder' ) || 0;
lib.props.unset( 'media_folder', { silent: true } );
lib.props.set( { media_folder: folder } );
}
} catch ( e ) {}
}, 800 );
} );
};
}
// ─── Sync on modal open/close ─────────────────────────────
var origModalOpen = wp.media.view.Modal.prototype.open;
wp.media.view.Modal.prototype.open = function () {
var modal = this;
origModalOpen.apply( this, arguments );
if ( ! modal._mfpCloseHooked ) {
modal._mfpCloseHooked = true;
var origClose = modal.close;
modal.close = function () {
foldersCache = null;
activeFolderId = 0;
allSelects = [];
document.dispatchEvent( new CustomEvent( 'mfp-folder-changed' ) );
return origClose.apply( this, arguments );
};
}
};
} )();