/** * 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(); } } )();