Skip to content

Commit 71c3fcd

Browse files
committed
feat: show folder suggestions when typing a folder name in the dialog
1 parent 10a7d76 commit 71c3fcd

File tree

3 files changed

+232
-13
lines changed

3 files changed

+232
-13
lines changed

src/LiveDevelopment/LivePreviewEdit.js

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ define(function (require, exports, module) {
3131
const ProjectManager = require("project/ProjectManager");
3232
const FileSystem = require("filesystem/FileSystem");
3333
const PathUtils = require("thirdparty/path-utils/path-utils");
34+
const StringMatch = require("utils/StringMatch");
3435
const Dialogs = require("widgets/Dialogs");
36+
3537
const ImageFolderDialogTemplate = require("text!htmlContent/image-folder-dialog.html");
3638

3739
/**
@@ -783,6 +785,120 @@ define(function (require, exports, module) {
783785
});
784786
}
785787

788+
// these folders are generally very large, and we don't scan them otherwise it might freeze the UI
789+
const EXCLUDED_FOLDERS = ['node_modules', 'bower_components', '.git', '.npm', '.yarn'];
790+
791+
/**
792+
* this function scans all the directories recursively
793+
* and then add the relative paths of the directories to the folderList array
794+
*
795+
* @param {Directory} directory - The parent directory to scan
796+
* @param {string} relativePath - The relative path from project root
797+
* @param {Array<string>} folderList - Array to store all discovered folder paths
798+
* @return {Promise} Resolves when scanning is complete
799+
*/
800+
function _scanDirectories(directory, relativePath, folderList) {
801+
return new Promise((resolve) => {
802+
directory.getContents((err, contents) => {
803+
if (err) {
804+
resolve();
805+
return;
806+
}
807+
808+
const directories = contents.filter(entry => entry.isDirectory);
809+
const scanPromises = [];
810+
811+
directories.forEach(dir => {
812+
// if its an excluded folder we ignore it
813+
if (EXCLUDED_FOLDERS.includes(dir.name)) {
814+
return;
815+
}
816+
817+
const dirRelativePath = relativePath ? `${relativePath}${dir.name}/` : `${dir.name}/`;
818+
folderList.push(dirRelativePath);
819+
820+
// also check subdirectories for this dir
821+
scanPromises.push(_scanDirectories(dir, dirRelativePath, folderList));
822+
});
823+
824+
Promise.all(scanPromises).then(() => resolve());
825+
});
826+
});
827+
}
828+
829+
/**
830+
* Renders folder suggestions as a dropdown in the UI with fuzzy match highlighting
831+
*
832+
* @param {Array<string|Object>} matches - Array of folder paths (strings) or fuzzy match objects with stringRanges
833+
* @param {JQuery} $suggestions - jQuery element for the suggestions container
834+
* @param {JQuery} $input - jQuery element for the input field
835+
*/
836+
function _renderFolderSuggestions(matches, $suggestions, $input) {
837+
if (matches.length === 0) {
838+
$suggestions.empty();
839+
return;
840+
}
841+
842+
let html = '<ul class="folder-suggestions-list">';
843+
matches.forEach((match) => {
844+
let displayHTML = '';
845+
let folderPath = '';
846+
847+
// Check if match is a string or an object
848+
if (typeof match === 'string') {
849+
// Simple string (from empty query showing folders)
850+
displayHTML = match;
851+
folderPath = match;
852+
} else if (match && match.stringRanges) {
853+
// fuzzy match, highlight matched chars
854+
match.stringRanges.forEach(range => {
855+
if (range.matched) {
856+
displayHTML += `<span class="folder-match-highlight">${range.text}</span>`;
857+
} else {
858+
displayHTML += range.text;
859+
}
860+
});
861+
folderPath = match.label || '';
862+
}
863+
864+
html += `<li class="folder-suggestion-item" data-path="${folderPath}">${displayHTML}</li>`;
865+
});
866+
html += '</ul>';
867+
868+
$suggestions.html(html);
869+
870+
// when a suggestion is clicked we add the folder path in the input box
871+
$suggestions.find('.folder-suggestion-item').on('click', function() {
872+
const folderPath = $(this).data('path');
873+
$input.val(folderPath);
874+
$suggestions.empty();
875+
});
876+
}
877+
878+
/**
879+
* This function is responsible to update the folder suggestion everytime a new char is inserted in the input field
880+
*
881+
* @param {string} query - The search query from the input field
882+
* @param {Array<string>} folderList - List of all available folder paths
883+
* @param {StringMatch.StringMatcher} stringMatcher - StringMatcher instance for fuzzy matching
884+
* @param {JQuery} $suggestions - jQuery element for the suggestions container
885+
* @param {JQuery} $input - jQuery element for the input field
886+
*/
887+
function _updateFolderSuggestions(query, folderList, stringMatcher, $suggestions, $input) {
888+
if (!query || query.trim() === '') {
889+
return;
890+
}
891+
892+
// filter folders using fuzzy matching
893+
const matches = folderList
894+
.map(folder => stringMatcher.match(folder, query))
895+
.filter(result => result !== null && result !== undefined)
896+
.sort((a, b) => b.matchGoodness - a.matchGoodness)
897+
.slice(0, 5);
898+
899+
_renderFolderSuggestions(matches, $suggestions, $input);
900+
}
901+
786902
/**
787903
* This function is called when 'use this image' button is clicked in the image ribbon gallery
788904
* or user loads an image file from the computer
@@ -792,33 +908,43 @@ define(function (require, exports, module) {
792908
* @param {Object} message - the message object which stores all the required data for this operation
793909
*/
794910
function _handleUseThisImage(message) {
911+
const projectRoot = ProjectManager.getProjectRoot();
912+
if (!projectRoot) { return; }
913+
795914
// show the dialog with a text box to select a folder
796915
// dialog html is written in 'image-folder-dialog.html'
797916
const dialog = Dialogs.showModalDialogUsingTemplate(ImageFolderDialogTemplate, false);
798917
const $dlg = dialog.getElement();
799918
const $input = $dlg.find("#folder-path-input");
919+
const $suggestions = $dlg.find("#folder-suggestions");
920+
921+
let folderList = [];
922+
let stringMatcher = null;
923+
924+
// Scan project directories and setup event handlers
925+
_scanDirectories(projectRoot, '', folderList).then(() => {
926+
stringMatcher = new StringMatch.StringMatcher({ segmentedSearch: true });
927+
928+
// input event handler
929+
$input.on('input', function() {
930+
_updateFolderSuggestions($input.val(), folderList, stringMatcher, $suggestions, $input);
931+
});
932+
});
800933

801934
// focus the input box
802935
setTimeout(function() {
803936
$input.focus();
804937
}, 100);
805938

806939
// handle dialog button clicks
940+
// so the logic is either its an ok button click or cancel button click, so if its ok click
941+
// then we download image in that folder and close the dialog, in close btn click we directly close the dialog
807942
$dlg.one("buttonClick", function(e, buttonId) {
808943
if (buttonId === Dialogs.DIALOG_BTN_OK) {
809944
const folderPath = $input.val().trim();
810-
dialog.close();
811-
// if folder path is specified we download in that folder
812-
// else we download in the project root
813-
if (folderPath) {
814-
_downloadToFolder(message, folderPath);
815-
} else {
816-
_downloadToFolder(message, '');
817-
}
818-
} else if (buttonId === Dialogs.DIALOG_BTN_CANCEL) {
819-
// if cancel is clicked, we abort the download
820-
dialog.close();
945+
_downloadToFolder(message, folderPath);
821946
}
947+
dialog.close();
822948
});
823949
}
824950

src/htmlContent/image-folder-dialog.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ <h1 class="dialog-title">Select Folder to Save Image</h1>
99
placeholder="Type folder path (e.g., assets/images/)"
1010
value=""
1111
autocomplete="off"
12-
spellcheck="false"
13-
style="width: 100%; height: 30px; padding: 5px; box-sizing: border-box;">
12+
spellcheck="false">
13+
14+
<!-- the folder suggestions will come here dynamically -->
15+
<div id="folder-suggestions"></div>
16+
17+
<p class="folder-help-text">
18+
💡 Tip: Type to filter folders or create a new path
19+
</p>
1420
</div>
1521
<div class="modal-footer">
1622
<button class="dialog-button btn" data-button-id="cancel">Cancel</button>

src/styles/brackets_patterns_override.less

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2518,3 +2518,90 @@ code {
25182518
}
25192519
}
25202520
}
2521+
2522+
// image folder selection dialog
2523+
.image-folder-dialog {
2524+
#folder-path-input {
2525+
width: 100%;
2526+
height: 30px;
2527+
padding: 5px;
2528+
box-sizing: border-box;
2529+
margin-bottom: 8px;
2530+
}
2531+
2532+
#folder-suggestions {
2533+
max-height: 180px;
2534+
overflow-y: auto;
2535+
overflow-x: hidden;
2536+
border: 1px solid @bc-btn-border;
2537+
border-radius: @bc-border-radius;
2538+
background-color: @bc-panel-bg-alt;
2539+
2540+
.dark & {
2541+
border: 1px solid @dark-bc-btn-border;
2542+
background-color: @dark-bc-panel-bg-alt;
2543+
}
2544+
2545+
&:empty {
2546+
display: none;
2547+
}
2548+
2549+
.folder-suggestions-list {
2550+
margin: 0;
2551+
padding: 0;
2552+
list-style: none;
2553+
}
2554+
2555+
.folder-suggestion-item {
2556+
padding: 6px 10px;
2557+
cursor: pointer;
2558+
font-size: 12px;
2559+
color: @bc-text;
2560+
transition: background-color 0.1s;
2561+
border-left: 3px solid transparent;
2562+
2563+
.dark & {
2564+
color: @dark-bc-text;
2565+
}
2566+
2567+
&:hover {
2568+
background-color: @bc-panel-bg-hover-alt;
2569+
2570+
.dark & {
2571+
background-color: @dark-bc-panel-bg-hover-alt;
2572+
}
2573+
}
2574+
2575+
&.highlight {
2576+
background-color: @bc-bg-highlight;
2577+
border-left-color: @bc-primary-btn-bg;
2578+
2579+
.dark & {
2580+
background-color: @dark-bc-bg-highlight;
2581+
border-left-color: @dark-bc-primary-btn-bg;
2582+
}
2583+
}
2584+
}
2585+
2586+
.folder-match-highlight {
2587+
font-weight: @font-weight-semibold;
2588+
color: @bc-primary-btn-bg;
2589+
2590+
.dark & {
2591+
color: @dark-bc-primary-btn-bg;
2592+
}
2593+
}
2594+
}
2595+
2596+
.folder-help-text {
2597+
margin-top: 8px;
2598+
margin-bottom: 0;
2599+
font-size: 11px;
2600+
color: @bc-text-quiet;
2601+
user-select: none;
2602+
2603+
.dark & {
2604+
color: @dark-bc-text-quiet;
2605+
}
2606+
}
2607+
}

0 commit comments

Comments
 (0)