diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index cb1f94be8d..09b2da5b23 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -57,6 +57,14 @@ function autoscale(value) { return ((Math.round(scale*data)/scale)+' '+unit[base]).replace('.','')+'/s'; } +function preventFileTreeClose() { + // Prevent fileTree dropdown from closing when clicking inside the dialog + // by stopping mousedown events from bubbling to the document handler + $('.ui-dfm').off('mousedown.dfmFileTree').on('mousedown.dfmFileTree', function(e) { + e.stopPropagation(); + }); +} + function folderContextMenu(id, button) { var opts = []; context.settings({button:button}); @@ -248,7 +256,7 @@ function fileEdit(id) { // file editor dialog dfm.window.html($("#dfm_templateEditFile").html().replace('{$0}',source).dfm_build()); dfm.window.dialog({ - classes: {'ui-dialog': 'ui-dfm'}, + classes: {'ui-dialog': 'ui-dfm ui-corner-all'}, autoOpen: true, title: fileName(source), width: 'auto', @@ -291,7 +299,7 @@ function doJobs(title) { dfm.window = $("#dfm_dialogWindow"); dfm.window.html($('#dfm_templateJobs').html().dfm_build()); dfm.window.dialog({ - classes: {'ui-dialog': 'ui-dfm'}, + classes: {'ui-dialog': 'ui-dfm ui-corner-all'}, autoOpen: true, title: title, height: 'auto', @@ -312,7 +320,7 @@ function doJobs(title) { }, "_(Delete)_": function(){ let row = []; - dfm.window.find('i[id^="queue_"]').each(function(){if ($(this).hasClass('fa-check-square-o')) row.push((($(this).prop('id').split('_')[1]-1)*9)+1);}); + dfm.window.find('i[id^="queue_"]').each(function(){if ($(this).hasClass('fa-check-square-o')) row.push($(this).prop('id').split('_')[1]);}); $.post('/webGui/include/Control.php',{mode:'undo',row:row.join(',')},function(queue){ $.post('/webGui/include/Control.php',{mode:'jobs'},function(jobs){ $('#dfm_jobs').html(jobs); @@ -343,6 +351,272 @@ filemonitor.on('message', function(state) { }); setTimeout(function(){filemonitor.start();},3000); +// File tree navigation constants +var FOLDER_EXPAND_DELAY = 300; // Delay per folder expansion in openFolderRecursive (ms) +var NAVIGATION_BUFFER = 500; // Additional buffer time for navigation completion (ms) + +function setupTargetNavigation() { + var $target = dfm.window.find('#dfm_target'); + if (!$target.length) return; + + var navigationTimeout; + var inputElement = $target[0]; + var isNavigatingFromPopular = false; + var isProgrammaticNavigation = false; // Flag for programmatic tree navigation + var savedInputValue = ''; // Save input value during programmatic navigation + var lastNavigatedPath = ''; // Track last navigated path for backward navigation + + // Event handlers for capture phase to prevent fileTree from closing + var preventClose = function(e) { + // Re-open tree if it was closed (must do this BEFORE stopImmediatePropagation) + if (e.type === 'click') { + var $tree = $target.closest('dd').find('.fileTree'); + if ($tree.length && !$tree.is(':visible')) { + // Only show if tree has content + var $content = $tree.find('.jqueryFileTree'); + if ($content.length && $content.children().length > 0) { + try { + $tree.show(); + } catch(err) { + console.error('Error showing tree:', err); + } + } + } + } + + e.stopPropagation(); + e.stopImmediatePropagation(); + }; + + // Attach to input element in capture phase (runs before jquery.filetree.js handler) + if (inputElement) { + inputElement.addEventListener('mousedown', preventClose, true); + inputElement.addEventListener('focus', preventClose, true); + inputElement.addEventListener('click', preventClose, true); + } + + // Handle clicks on ANY tree item to remove popular destinations + var treeClickHandler = function(e) { + var $link = $(e.target).closest('.jqueryFileTree a'); + if ($link.length === 0) return; + + // Remove popular section when clicking on any tree item (except popular items themselves) + if (!$link.closest('.popular-destination').length) { + var $tree = $('.fileTree .jqueryFileTree'); + $tree.find('.popular-header, .popular-destination, .popular-separator').remove(); + } + }; + + // Attach tree click handler in capture phase + document.addEventListener('click', treeClickHandler, true); + + // Handle clicks on popular destinations - use capture phase to run before jQueryFileTree handler + var popularClickHandler = function(e) { + var $link = $(e.target).closest('.popular-destination a'); + if ($link.length === 0) return; + + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + // Get path from data-path attribute + var path = $link.attr('data-path'); + if (!path) return; + + // Remove trailing slash for input field + var cleanPath = path.replace(/\/+$/, ''); + + // Remove popular section immediately (before any navigation) + var $tree = $('.fileTree .jqueryFileTree'); + $tree.find('.popular-header, .popular-destination, .popular-separator').remove(); + + // Set flag to prevent input handler from triggering + isNavigatingFromPopular = true; + + // Set input field value (this triggers 'input' event) + $target.val(cleanPath + '/'); + + // Trigger navigation to expand the path in normal tree + navigateFileTree(cleanPath + '/'); + lastNavigatedPath = cleanPath + '/'; + + // Reset flag after a short delay + setTimeout(function() { + isNavigatingFromPopular = false; + }, 100); + + return false; + }; + + // Attach in capture phase to run before jQueryFileTree handlers + document.addEventListener('click', popularClickHandler, true); + + // Hide/show popular destinations based on input field content + $target.on('input', function() { + // Ignore input events triggered by popular click or programmatic navigation + if (isNavigatingFromPopular || isProgrammaticNavigation) { + return; + } + + var inputValue = this.value.trim(); + var $tree = $('.fileTree .jqueryFileTree'); + + if (inputValue) { + // Input is not empty - remove popular destinations completely + $tree.find('.popular-header, .popular-destination, .popular-separator').remove(); + } else { + // Input is empty - popular destinations will be shown on next tree reload + } + + clearTimeout(navigationTimeout); + + if (!inputValue) { + // Reset tree to initial state when input is cleared + resetFileTree($target); + lastNavigatedPath = ''; + return; + } + if (inputValue[inputValue.length - 1] !== '/') return; + + // Check if we're navigating backward (path got shorter) + if (lastNavigatedPath && inputValue.length < lastNavigatedPath.length && lastNavigatedPath.startsWith(inputValue)) { + // Close all folders between the new path and the old path + var pathToClose = lastNavigatedPath; + while (pathToClose.length > inputValue.length) { + closeFolderPath(pathToClose); + // Remove the last segment to go up one level + pathToClose = pathToClose.replace(/\/+$/, ''); // Remove trailing slashes + var lastSlash = pathToClose.lastIndexOf('/'); + if (lastSlash === -1) break; + pathToClose = pathToClose.substring(0, lastSlash + 1); + } + lastNavigatedPath = inputValue; + return; + } + + navigationTimeout = setTimeout(function() { + navigateFileTree(inputValue); + lastNavigatedPath = inputValue; + }, 200); + }); + + // Restore input value if changed during programmatic navigation + $target.on('change', function() { + var isProgrammaticNav = $target.data('isProgrammaticNavigation'); + var savedValue = $target.data('savedInputValue'); + if (isProgrammaticNav && savedValue) { + this.value = savedValue; + } + }); + + // Cleanup on dialog close + dfm.window.on('dialogclose', function() { + document.removeEventListener('click', treeClickHandler, true); + document.removeEventListener('click', popularClickHandler, true); + if (inputElement) { + inputElement.removeEventListener('mousedown', preventClose, true); + inputElement.removeEventListener('focus', preventClose, true); + inputElement.removeEventListener('click', preventClose, true); + } + }); +} + +function closeFolderPath(path) { + var $tree = $('.jqueryFileTree').first(); + if ($tree.length === 0) return; + + // Remove trailing slash for searching + var cleanPath = path.replace(/\/+$/, ''); + + // Find the folder link by rel attribute + var $folderLink = $tree.find('a[rel="' + cleanPath + '/"]'); + if ($folderLink.length === 0) return; + + var $folderLi = $folderLink.closest('li'); + + // Close the folder: change class from expanded to collapsed + $folderLi.removeClass('expanded').addClass('collapsed'); + + // Remove all child elements (the nested