@@ -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
0 commit comments