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>
This commit is contained in:
509
wp-content/plugins/media-folder-pro/assets/js/folder-tree.js
Normal file
509
wp-content/plugins/media-folder-pro/assets/js/folder-tree.js
Normal file
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* Media Folder Pro — Folder Tree UI
|
||||
* Vanilla JS, no jQuery dependency.
|
||||
*/
|
||||
( function () {
|
||||
'use strict';
|
||||
|
||||
const { ajaxUrl, nonce, i18n } = window.mfpData || {};
|
||||
if ( ! ajaxUrl ) return;
|
||||
|
||||
let folders = [];
|
||||
let activeFolder = null;
|
||||
let contextMenu = null;
|
||||
let root = null;
|
||||
|
||||
// ─── AJAX helper ──────────────────────────────────────────
|
||||
function ajax( action, 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,
|
||||
} ).then( ( r ) => r.json() );
|
||||
}
|
||||
|
||||
// ─── Toast notifications ──────────────────────────────────
|
||||
function showToast( message, type ) {
|
||||
type = type || 'success';
|
||||
const toast = document.createElement( 'div' );
|
||||
toast.className = 'mfp-toast mfp-toast--' + type;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild( toast );
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame( function () {
|
||||
toast.classList.add( 'is-visible' );
|
||||
} );
|
||||
|
||||
setTimeout( function () {
|
||||
toast.classList.remove( 'is-visible' );
|
||||
toast.addEventListener( 'transitionend', function () {
|
||||
toast.remove();
|
||||
} );
|
||||
// Fallback removal
|
||||
setTimeout( function () { toast.remove(); }, 500 );
|
||||
}, 3000 );
|
||||
}
|
||||
|
||||
// Expose globally
|
||||
window.mfpToast = showToast;
|
||||
|
||||
// ─── DOM helpers ──────────────────────────────────────────
|
||||
function el( tag, attrs, children ) {
|
||||
attrs = attrs || {};
|
||||
children = children || [];
|
||||
const node = document.createElement( tag );
|
||||
for ( const [ k, v ] of Object.entries( attrs ) ) {
|
||||
if ( k === 'className' ) {
|
||||
node.className = v;
|
||||
} else if ( k === 'textContent' ) {
|
||||
node.textContent = v;
|
||||
} else if ( k.startsWith( 'on' ) ) {
|
||||
node.addEventListener( k.slice( 2 ).toLowerCase(), v );
|
||||
} else {
|
||||
node.setAttribute( k, v );
|
||||
}
|
||||
}
|
||||
for ( const child of children ) {
|
||||
if ( typeof child === 'string' ) {
|
||||
node.appendChild( document.createTextNode( child ) );
|
||||
} else if ( child ) {
|
||||
node.appendChild( child );
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// ─── Context menu ─────────────────────────────────────────
|
||||
function closeContextMenu() {
|
||||
if ( contextMenu ) {
|
||||
contextMenu.remove();
|
||||
contextMenu = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showContextMenu( e, folder ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeContextMenu();
|
||||
|
||||
contextMenu = el( 'div', { className: 'mfp-context-menu' }, [
|
||||
el( 'button', {
|
||||
className: 'mfp-context-menu__item',
|
||||
textContent: i18n.newSubfolder,
|
||||
onClick: function () { closeContextMenu(); createFolder( folder.id ); },
|
||||
} ),
|
||||
el( 'button', {
|
||||
className: 'mfp-context-menu__item',
|
||||
textContent: i18n.rename,
|
||||
onClick: function () { closeContextMenu(); renameFolder( folder ); },
|
||||
} ),
|
||||
el( 'div', { className: 'mfp-context-menu__separator' } ),
|
||||
el( 'button', {
|
||||
className: 'mfp-context-menu__item mfp-context-menu__item--danger',
|
||||
textContent: i18n.delete,
|
||||
onClick: function () { closeContextMenu(); deleteFolder( folder ); },
|
||||
} ),
|
||||
] );
|
||||
|
||||
contextMenu.style.left = e.clientX + 'px';
|
||||
contextMenu.style.top = e.clientY + 'px';
|
||||
document.body.appendChild( contextMenu );
|
||||
|
||||
const rect = contextMenu.getBoundingClientRect();
|
||||
if ( rect.right > window.innerWidth ) {
|
||||
contextMenu.style.left = ( e.clientX - rect.width ) + 'px';
|
||||
}
|
||||
if ( rect.bottom > window.innerHeight ) {
|
||||
contextMenu.style.top = ( e.clientY - rect.height ) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Render ───────────────────────────────────────────────
|
||||
function renderTree( items, container ) {
|
||||
const ul = el( 'ul' );
|
||||
for ( const folder of items ) {
|
||||
ul.appendChild( renderFolder( folder ) );
|
||||
}
|
||||
container.appendChild( ul );
|
||||
}
|
||||
|
||||
function renderFolder( folder ) {
|
||||
const hasChildren = folder.children && folder.children.length > 0;
|
||||
|
||||
const toggle = el( 'span', {
|
||||
className: 'mfp-folder__toggle' + ( hasChildren ? '' : ' is-leaf' ),
|
||||
textContent: '\u25B6',
|
||||
onClick: function ( e ) {
|
||||
e.stopPropagation();
|
||||
if ( hasChildren ) toggleExpand( li );
|
||||
},
|
||||
} );
|
||||
|
||||
const icon = el( 'span', {
|
||||
className: 'mfp-folder__icon',
|
||||
textContent: '\uD83D\uDCC1',
|
||||
} );
|
||||
|
||||
const name = el( 'span', {
|
||||
className: 'mfp-folder__name',
|
||||
textContent: folder.name,
|
||||
} );
|
||||
|
||||
const count = el( 'span', {
|
||||
className: 'mfp-folder__count',
|
||||
textContent: folder.count > 0 ? String( folder.count ) : '',
|
||||
} );
|
||||
|
||||
const row = el( 'div', {
|
||||
className: 'mfp-folder__row',
|
||||
'data-id': String( folder.id ),
|
||||
draggable: 'true',
|
||||
onClick: function () { selectFolder( folder ); },
|
||||
onContextmenu: function ( e ) { showContextMenu( e, folder ); },
|
||||
}, [ toggle, icon, name, count ] );
|
||||
|
||||
// ─── Folder drag (reorder hierarchy) ──────────────────
|
||||
row.addEventListener( 'dragstart', function ( e ) {
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.setData( 'application/mfp-folder', String( folder.id ) );
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
document.body.classList.add( 'mfp-dragging-folder' );
|
||||
} );
|
||||
|
||||
row.addEventListener( 'dragend', function () {
|
||||
document.body.classList.remove( 'mfp-dragging-folder' );
|
||||
root.querySelectorAll( '.mfp-folder-drop-target' ).forEach( function ( el ) {
|
||||
el.classList.remove( 'mfp-folder-drop-target' );
|
||||
} );
|
||||
} );
|
||||
|
||||
// Drop target for folder-on-folder
|
||||
row.addEventListener( 'dragover', function ( e ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Folder drag = yellow, media drag = blue (handled in media-filter.js)
|
||||
if ( e.dataTransfer.types.includes( 'application/mfp-folder' ) ) {
|
||||
row.classList.add( 'mfp-folder-drop-target' );
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
} else {
|
||||
row.classList.add( 'mfp-drop-target' );
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
} );
|
||||
|
||||
row.addEventListener( 'dragleave', function () {
|
||||
row.classList.remove( 'mfp-folder-drop-target' );
|
||||
row.classList.remove( 'mfp-drop-target' );
|
||||
} );
|
||||
|
||||
row.addEventListener( 'drop', function ( e ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
row.classList.remove( 'mfp-folder-drop-target' );
|
||||
row.classList.remove( 'mfp-drop-target' );
|
||||
|
||||
const draggedFolderId = e.dataTransfer.getData( 'application/mfp-folder' );
|
||||
if ( draggedFolderId ) {
|
||||
// Folder → folder move
|
||||
if ( String( draggedFolderId ) === String( folder.id ) ) return;
|
||||
moveFolder( parseInt( draggedFolderId, 10 ), folder.id );
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentId = e.dataTransfer.getData( 'text/plain' );
|
||||
if ( attachmentId ) {
|
||||
// Media → folder assign (delegated to media-filter.js via event)
|
||||
document.dispatchEvent( new CustomEvent( 'mfp-media-dropped', {
|
||||
detail: { attachmentIds: [ attachmentId ], folderId: folder.id },
|
||||
} ) );
|
||||
}
|
||||
} );
|
||||
|
||||
const childrenContainer = el( 'div', {
|
||||
className: 'mfp-folder__children',
|
||||
} );
|
||||
|
||||
if ( hasChildren ) {
|
||||
renderTree( folder.children, childrenContainer );
|
||||
}
|
||||
|
||||
const li = el( 'li', { className: 'mfp-folder' }, [ row, childrenContainer ] );
|
||||
return li;
|
||||
}
|
||||
|
||||
function toggleExpand( li ) {
|
||||
const children = li.querySelector( '.mfp-folder__children' );
|
||||
const toggle = li.querySelector( '.mfp-folder__toggle' );
|
||||
if ( ! children ) return;
|
||||
|
||||
const isOpen = children.classList.contains( 'is-open' );
|
||||
children.classList.toggle( 'is-open', ! isOpen );
|
||||
toggle.classList.toggle( 'is-expanded', ! isOpen );
|
||||
}
|
||||
|
||||
function selectFolder( folder ) {
|
||||
activeFolder = folder.id;
|
||||
clearActiveStates();
|
||||
|
||||
const row = root.querySelector( '.mfp-folder__row[data-id="' + folder.id + '"]' );
|
||||
if ( row ) row.classList.add( 'is-active' );
|
||||
|
||||
document.dispatchEvent( new CustomEvent( 'mfp-folder-selected', {
|
||||
detail: { folderId: folder.id, folderName: folder.name },
|
||||
} ) );
|
||||
}
|
||||
|
||||
function selectAllMedia() {
|
||||
activeFolder = null;
|
||||
clearActiveStates();
|
||||
|
||||
var allBtn = root.querySelector( '.mfp-all-media' );
|
||||
if ( allBtn ) allBtn.classList.add( 'is-active' );
|
||||
|
||||
document.dispatchEvent( new CustomEvent( 'mfp-folder-selected', {
|
||||
detail: { folderId: null, folderName: null },
|
||||
} ) );
|
||||
}
|
||||
|
||||
function selectUncategorized() {
|
||||
activeFolder = -1;
|
||||
clearActiveStates();
|
||||
|
||||
var uncatBtn = root.querySelector( '.mfp-uncategorized' );
|
||||
if ( uncatBtn ) uncatBtn.classList.add( 'is-active' );
|
||||
|
||||
document.dispatchEvent( new CustomEvent( 'mfp-folder-selected', {
|
||||
detail: { folderId: -1, folderName: i18n.uncategorized },
|
||||
} ) );
|
||||
}
|
||||
|
||||
function clearActiveStates() {
|
||||
root.querySelectorAll( '.mfp-folder__row.is-active' ).forEach( function ( el ) {
|
||||
el.classList.remove( 'is-active' );
|
||||
} );
|
||||
root.querySelector( '.mfp-all-media' )?.classList.remove( 'is-active' );
|
||||
root.querySelector( '.mfp-uncategorized' )?.classList.remove( 'is-active' );
|
||||
}
|
||||
|
||||
// ─── CRUD ─────────────────────────────────────────────────
|
||||
function createFolder( parentId ) {
|
||||
parentId = parentId || 0;
|
||||
const name = prompt( i18n.folderName );
|
||||
if ( ! name || ! name.trim() ) return;
|
||||
|
||||
ajax( 'mfp_create_folder', { name: name.trim(), parent_id: parentId } )
|
||||
.then( function ( res ) {
|
||||
if ( res.success ) {
|
||||
refreshTree();
|
||||
showToast( i18n.folderCreated || 'Folder utworzony' );
|
||||
} else {
|
||||
showToast( res.data?.message || i18n.error, 'error' );
|
||||
}
|
||||
} )
|
||||
.catch( function () { showToast( i18n.error, 'error' ); } );
|
||||
}
|
||||
|
||||
function renameFolder( folder ) {
|
||||
const row = root.querySelector( '.mfp-folder__row[data-id="' + folder.id + '"]' );
|
||||
if ( ! row ) return;
|
||||
|
||||
const nameEl = row.querySelector( '.mfp-folder__name' );
|
||||
const oldName = nameEl.textContent;
|
||||
|
||||
const input = el( 'input', {
|
||||
className: 'mfp-folder__rename-input',
|
||||
type: 'text',
|
||||
} );
|
||||
input.value = oldName;
|
||||
|
||||
nameEl.style.display = 'none';
|
||||
row.insertBefore( input, nameEl.nextSibling );
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
function finish() {
|
||||
const newName = input.value.trim();
|
||||
input.remove();
|
||||
nameEl.style.display = '';
|
||||
|
||||
if ( ! newName || newName === oldName ) return;
|
||||
|
||||
ajax( 'mfp_rename_folder', { folder_id: folder.id, name: newName } )
|
||||
.then( function ( res ) {
|
||||
if ( res.success ) {
|
||||
nameEl.textContent = res.data.folder.name;
|
||||
showToast( i18n.folderRenamed || 'Nazwa zmieniona' );
|
||||
} else {
|
||||
showToast( res.data?.message || i18n.error, 'error' );
|
||||
}
|
||||
} )
|
||||
.catch( function () { showToast( i18n.error, 'error' ); } );
|
||||
}
|
||||
|
||||
input.addEventListener( 'blur', finish );
|
||||
input.addEventListener( 'keydown', function ( e ) {
|
||||
if ( e.key === 'Enter' ) {
|
||||
e.preventDefault();
|
||||
input.blur();
|
||||
}
|
||||
if ( e.key === 'Escape' ) {
|
||||
input.value = oldName;
|
||||
input.blur();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function deleteFolder( folder ) {
|
||||
if ( ! confirm( i18n.confirmDelete ) ) return;
|
||||
|
||||
ajax( 'mfp_delete_folder', { folder_id: folder.id } )
|
||||
.then( function ( res ) {
|
||||
if ( res.success ) {
|
||||
if ( activeFolder === folder.id ) selectAllMedia();
|
||||
refreshTree();
|
||||
showToast( i18n.folderDeleted || 'Folder usunięty' );
|
||||
} else {
|
||||
showToast( res.data?.message || i18n.error, 'error' );
|
||||
}
|
||||
} )
|
||||
.catch( function () { showToast( i18n.error, 'error' ); } );
|
||||
}
|
||||
|
||||
function moveFolder( folderId, newParentId ) {
|
||||
ajax( 'mfp_move_folder', { folder_id: folderId, new_parent_id: newParentId } )
|
||||
.then( function ( res ) {
|
||||
if ( res.success ) {
|
||||
refreshTree();
|
||||
showToast( i18n.folderMoved || 'Folder przeniesiony' );
|
||||
} else {
|
||||
showToast( res.data?.message || i18n.error, 'error' );
|
||||
}
|
||||
} )
|
||||
.catch( function () { showToast( i18n.error, 'error' ); } );
|
||||
}
|
||||
|
||||
// ─── Refresh ──────────────────────────────────────────────
|
||||
window.mfpRefreshTree = function () { refreshTree(); };
|
||||
|
||||
function refreshTree() {
|
||||
ajax( 'mfp_get_folders' ).then( function ( res ) {
|
||||
if ( ! res.success ) return;
|
||||
|
||||
folders = res.data.folders;
|
||||
const tree = root.querySelector( '.mfp-tree' );
|
||||
tree.innerHTML = '';
|
||||
|
||||
if ( folders.length === 0 ) {
|
||||
tree.appendChild( el( 'div', { className: 'mfp-empty' }, [
|
||||
el( 'div', { className: 'mfp-empty__icon', textContent: '\uD83D\uDCC2' } ),
|
||||
el( 'div', { className: 'mfp-empty__text', textContent: i18n.emptyState || 'Brak folderów' } ),
|
||||
el( 'button', {
|
||||
className: 'mfp-empty__cta',
|
||||
textContent: i18n.createFirst || 'Utwórz pierwszy folder',
|
||||
onClick: function () { createFolder( 0 ); },
|
||||
} ),
|
||||
] ) );
|
||||
} else {
|
||||
renderTree( folders, tree );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────
|
||||
function init() {
|
||||
root = document.getElementById( 'mfp-folder-root' );
|
||||
if ( ! root ) return;
|
||||
|
||||
// Toolbar
|
||||
const toolbar = el( 'div', { className: 'mfp-toolbar' }, [
|
||||
el( 'span', { className: 'mfp-toolbar__title', textContent: 'Foldery' } ),
|
||||
el( 'button', {
|
||||
className: 'mfp-toolbar__btn',
|
||||
textContent: '+',
|
||||
title: i18n.newFolder,
|
||||
onClick: function () { createFolder( 0 ); },
|
||||
} ),
|
||||
] );
|
||||
|
||||
// All Media
|
||||
const allMedia = el( 'div', {
|
||||
className: 'mfp-all-media is-active',
|
||||
onClick: selectAllMedia,
|
||||
}, [
|
||||
el( 'span', { className: 'mfp-all-media__icon', textContent: '\uD83D\uDDBC\uFE0F' } ),
|
||||
el( 'span', { textContent: i18n.allMedia } ),
|
||||
] );
|
||||
|
||||
// Uncategorized
|
||||
const uncategorized = el( 'div', {
|
||||
className: 'mfp-uncategorized',
|
||||
onClick: selectUncategorized,
|
||||
}, [
|
||||
el( 'span', { className: 'mfp-uncategorized__icon', textContent: '\uD83D\uDCC4' } ),
|
||||
el( 'span', { textContent: i18n.uncategorized || 'Bez folderu' } ),
|
||||
] );
|
||||
|
||||
// Tree
|
||||
const tree = el( 'div', { className: 'mfp-tree' }, [
|
||||
el( 'div', { className: 'mfp-loading' }, [
|
||||
el( 'div', { className: 'mfp-spinner' } ),
|
||||
] ),
|
||||
] );
|
||||
|
||||
root.appendChild( toolbar );
|
||||
root.appendChild( allMedia );
|
||||
root.appendChild( uncategorized );
|
||||
root.appendChild( tree );
|
||||
|
||||
// Drop on root area = move folder to top level
|
||||
tree.addEventListener( 'dragover', function ( e ) {
|
||||
if ( e.target.closest( '.mfp-folder__row' ) ) return;
|
||||
if ( e.dataTransfer.types.includes( 'application/mfp-folder' ) ) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
tree.classList.add( 'mfp-tree-drop-target' );
|
||||
}
|
||||
} );
|
||||
|
||||
tree.addEventListener( 'dragleave', function ( e ) {
|
||||
if ( ! tree.contains( e.relatedTarget ) || e.relatedTarget === tree ) {
|
||||
tree.classList.remove( 'mfp-tree-drop-target' );
|
||||
}
|
||||
} );
|
||||
|
||||
tree.addEventListener( 'drop', function ( e ) {
|
||||
if ( e.target.closest( '.mfp-folder__row' ) ) return;
|
||||
tree.classList.remove( 'mfp-tree-drop-target' );
|
||||
const draggedFolderId = e.dataTransfer.getData( 'application/mfp-folder' );
|
||||
if ( draggedFolderId ) {
|
||||
e.preventDefault();
|
||||
moveFolder( parseInt( draggedFolderId, 10 ), 0 );
|
||||
}
|
||||
} );
|
||||
|
||||
// Close context menu on click outside
|
||||
document.addEventListener( 'click', closeContextMenu );
|
||||
|
||||
// Listen for folder changes from modal
|
||||
document.addEventListener( 'mfp-folder-changed', function () {
|
||||
refreshTree();
|
||||
} );
|
||||
|
||||
// Load folders
|
||||
refreshTree();
|
||||
}
|
||||
|
||||
if ( document.readyState === 'loading' ) {
|
||||
document.addEventListener( 'DOMContentLoaded', init );
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
} )();
|
||||
293
wp-content/plugins/media-folder-pro/assets/js/media-filter.js
Normal file
293
wp-content/plugins/media-folder-pro/assets/js/media-filter.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Media Folder Pro — Media Grid Filter + Drag & Drop + Bulk Assign
|
||||
* Integrates with WP media grid via wp.media backbone props.
|
||||
*/
|
||||
( function () {
|
||||
'use strict';
|
||||
|
||||
const { ajaxUrl, nonce, i18n } = window.mfpData || {};
|
||||
if ( ! ajaxUrl ) return;
|
||||
|
||||
let currentFolderId = null;
|
||||
|
||||
function toast( msg, type ) {
|
||||
if ( window.mfpToast ) window.mfpToast( msg, type );
|
||||
}
|
||||
|
||||
// ─── 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 ) ) {
|
||||
if ( Array.isArray( v ) ) {
|
||||
v.forEach( function ( item ) { body.append( k + '[]', item ); } );
|
||||
} else {
|
||||
body.append( k, v );
|
||||
}
|
||||
}
|
||||
return fetch( ajaxUrl, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: body,
|
||||
} ).then( function ( r ) { return r.json(); } );
|
||||
}
|
||||
|
||||
// ─── Media Grid Filter ───────────────────────────────────
|
||||
function getMediaCollection() {
|
||||
if ( ! window.wp || ! wp.media || ! wp.media.frame ) {
|
||||
return null;
|
||||
}
|
||||
var library = wp.media.frame.state &&
|
||||
wp.media.frame.state() &&
|
||||
wp.media.frame.state().get( 'library' );
|
||||
return library || null;
|
||||
}
|
||||
|
||||
function filterByFolder( folderId ) {
|
||||
currentFolderId = folderId;
|
||||
var library = getMediaCollection();
|
||||
if ( library ) {
|
||||
if ( folderId ) {
|
||||
library.props.set( { media_folder: folderId } );
|
||||
} else {
|
||||
library.props.set( { media_folder: 0 } );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for folder selection from tree
|
||||
document.addEventListener( 'mfp-folder-selected', function ( e ) {
|
||||
filterByFolder( e.detail.folderId );
|
||||
} );
|
||||
|
||||
// Listen for media dropped on folder (from folder-tree.js)
|
||||
document.addEventListener( 'mfp-media-dropped', function ( e ) {
|
||||
assignMedia( e.detail.attachmentIds, e.detail.folderId );
|
||||
} );
|
||||
|
||||
// ─── Drag & Drop (media) ─────────────────────────────────
|
||||
function enableDraggable( attachment ) {
|
||||
if ( attachment.getAttribute( 'data-mfp-draggable' ) ) return;
|
||||
attachment.setAttribute( 'data-mfp-draggable', '1' );
|
||||
attachment.setAttribute( 'draggable', 'true' );
|
||||
|
||||
attachment.addEventListener( 'dragstart', function ( e ) {
|
||||
var id = attachment.getAttribute( 'data-id' );
|
||||
if ( ! id ) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.setData( 'text/plain', id );
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
document.body.classList.add( 'mfp-dragging' );
|
||||
} );
|
||||
|
||||
attachment.addEventListener( 'dragend', function () {
|
||||
document.body.classList.remove( 'mfp-dragging' );
|
||||
} );
|
||||
}
|
||||
|
||||
function scanAttachments() {
|
||||
document.querySelectorAll( '.attachments .attachment' ).forEach( enableDraggable );
|
||||
}
|
||||
|
||||
function observeAttachments() {
|
||||
var container = document.querySelector( '.attachments' );
|
||||
if ( ! container ) return;
|
||||
|
||||
scanAttachments();
|
||||
|
||||
var observer = new MutationObserver( function () {
|
||||
scanAttachments();
|
||||
} );
|
||||
observer.observe( container, { childList: true, subtree: true } );
|
||||
}
|
||||
|
||||
// Drop handlers on folder rows — delegated from tree root
|
||||
function initDropTargets() {
|
||||
var root = document.getElementById( 'mfp-folder-root' );
|
||||
if ( ! root ) return;
|
||||
|
||||
// "All Media" drop = unassign
|
||||
var allMedia = root.querySelector( '.mfp-all-media' );
|
||||
if ( allMedia ) {
|
||||
allMedia.addEventListener( 'dragover', function ( e ) {
|
||||
if ( e.dataTransfer.types.includes( 'application/mfp-folder' ) ) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
allMedia.classList.add( 'mfp-drop-target' );
|
||||
} );
|
||||
allMedia.addEventListener( 'dragleave', function () {
|
||||
allMedia.classList.remove( 'mfp-drop-target' );
|
||||
} );
|
||||
allMedia.addEventListener( 'drop', function ( e ) {
|
||||
e.preventDefault();
|
||||
allMedia.classList.remove( 'mfp-drop-target' );
|
||||
var attachmentId = e.dataTransfer.getData( 'text/plain' );
|
||||
if ( attachmentId ) assignMedia( [ attachmentId ], 0 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
function assignMedia( attachmentIds, folderId ) {
|
||||
ajax( 'mfp_assign_media', {
|
||||
attachment_ids: attachmentIds,
|
||||
folder_id: folderId,
|
||||
} ).then( function ( res ) {
|
||||
if ( res.success ) {
|
||||
if ( window.mfpRefreshTree ) window.mfpRefreshTree();
|
||||
|
||||
var count = res.data.count || attachmentIds.length;
|
||||
toast(
|
||||
( i18n.assignSuccess || 'Media przypisane' ) +
|
||||
( count > 1 ? ' (' + count + ')' : '' )
|
||||
);
|
||||
|
||||
// Refresh grid if viewing a different folder
|
||||
if ( currentFolderId && String( currentFolderId ) !== String( folderId ) ) {
|
||||
filterByFolder( currentFolderId );
|
||||
}
|
||||
} else {
|
||||
toast( ( res.data && res.data.message ) || i18n.assignError || 'Error', 'error' );
|
||||
}
|
||||
} ).catch( function () {
|
||||
toast( i18n.error || 'Error', 'error' );
|
||||
} );
|
||||
}
|
||||
|
||||
// ─── Bulk Assign ─────────────────────────────────────────
|
||||
function initBulkAssign() {
|
||||
// Inject bulk button into WP media toolbar
|
||||
var observer = new MutationObserver( function () {
|
||||
var toolbar = document.querySelector( '.media-toolbar-secondary' );
|
||||
if ( toolbar && ! toolbar.querySelector( '.mfp-bulk-btn' ) ) {
|
||||
createBulkButton( toolbar );
|
||||
}
|
||||
} );
|
||||
observer.observe( document.body, { childList: true, subtree: true } );
|
||||
|
||||
// Also check immediately
|
||||
var toolbar = document.querySelector( '.media-toolbar-secondary' );
|
||||
if ( toolbar && ! toolbar.querySelector( '.mfp-bulk-btn' ) ) {
|
||||
createBulkButton( toolbar );
|
||||
}
|
||||
}
|
||||
|
||||
function createBulkButton( toolbar ) {
|
||||
var btn = document.createElement( 'button' );
|
||||
btn.className = 'button mfp-bulk-btn';
|
||||
btn.textContent = i18n.moveToFolder || 'Przenieś do folderu';
|
||||
btn.type = 'button';
|
||||
|
||||
btn.addEventListener( 'click', function () {
|
||||
showBulkDropdown( btn );
|
||||
} );
|
||||
|
||||
toolbar.appendChild( btn );
|
||||
}
|
||||
|
||||
function showBulkDropdown( anchor ) {
|
||||
// Remove existing dropdown
|
||||
var existing = document.querySelector( '.mfp-bulk-dropdown' );
|
||||
if ( existing ) { existing.remove(); return; }
|
||||
|
||||
// Get selected attachments
|
||||
var selected = document.querySelectorAll( '.attachment.selected, .attachment.details' );
|
||||
if ( selected.length === 0 ) {
|
||||
toast( i18n.noSelection || 'Zaznacz media do przeniesienia', 'info' );
|
||||
return;
|
||||
}
|
||||
|
||||
var ids = [];
|
||||
selected.forEach( function ( el ) {
|
||||
var id = el.getAttribute( 'data-id' );
|
||||
if ( id ) ids.push( id );
|
||||
} );
|
||||
|
||||
if ( ids.length === 0 ) return;
|
||||
|
||||
// Load folders and build dropdown
|
||||
ajax( 'mfp_get_folders' ).then( function ( res ) {
|
||||
if ( ! res.success ) return;
|
||||
|
||||
var dropdown = document.createElement( 'div' );
|
||||
dropdown.className = 'mfp-bulk-dropdown';
|
||||
|
||||
// "Remove from folder" option
|
||||
var removeOpt = document.createElement( 'button' );
|
||||
removeOpt.className = 'mfp-bulk-dropdown__item mfp-bulk-dropdown__item--remove';
|
||||
removeOpt.textContent = i18n.removeFromFolder || 'Usuń z folderu';
|
||||
removeOpt.addEventListener( 'click', function () {
|
||||
dropdown.remove();
|
||||
assignMedia( ids, 0 );
|
||||
} );
|
||||
dropdown.appendChild( removeOpt );
|
||||
|
||||
var sep = document.createElement( 'div' );
|
||||
sep.className = 'mfp-context-menu__separator';
|
||||
dropdown.appendChild( sep );
|
||||
|
||||
// Folder options (flat)
|
||||
flattenAndAddOptions( dropdown, res.data.folders, 0, ids );
|
||||
|
||||
// Position
|
||||
var rect = anchor.getBoundingClientRect();
|
||||
dropdown.style.position = 'fixed';
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.top = ( rect.bottom + 4 ) + 'px';
|
||||
|
||||
document.body.appendChild( dropdown );
|
||||
|
||||
// Close on outside click
|
||||
function closeDropdown( e ) {
|
||||
if ( ! dropdown.contains( e.target ) && e.target !== anchor ) {
|
||||
dropdown.remove();
|
||||
document.removeEventListener( 'click', closeDropdown );
|
||||
}
|
||||
}
|
||||
setTimeout( function () {
|
||||
document.addEventListener( 'click', closeDropdown );
|
||||
}, 0 );
|
||||
} );
|
||||
}
|
||||
|
||||
function flattenAndAddOptions( container, folders, depth, ids ) {
|
||||
for ( var i = 0; i < folders.length; i++ ) {
|
||||
var folder = folders[ i ];
|
||||
var prefix = depth > 0 ? '\u2003'.repeat( depth ) + '\u2014 ' : '';
|
||||
|
||||
var opt = document.createElement( 'button' );
|
||||
opt.className = 'mfp-bulk-dropdown__item';
|
||||
opt.textContent = prefix + folder.name;
|
||||
opt.setAttribute( 'data-folder-id', folder.id );
|
||||
|
||||
( function ( fId ) {
|
||||
opt.addEventListener( 'click', function () {
|
||||
container.remove();
|
||||
assignMedia( ids, fId );
|
||||
} );
|
||||
} )( folder.id );
|
||||
|
||||
container.appendChild( opt );
|
||||
|
||||
if ( folder.children && folder.children.length > 0 ) {
|
||||
flattenAndAddOptions( container, folder.children, depth + 1, ids );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────
|
||||
function init() {
|
||||
observeAttachments();
|
||||
initDropTargets();
|
||||
initBulkAssign();
|
||||
}
|
||||
|
||||
if ( document.readyState === 'loading' ) {
|
||||
document.addEventListener( 'DOMContentLoaded', init );
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
} )();
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* 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 );
|
||||
};
|
||||
}
|
||||
};
|
||||
} )();
|
||||
Reference in New Issue
Block a user