// GLOBAL ICON array ICON={} ICON["Import"]="import" ICON["Storage"]="db" ICON["Bin"]="trash" // function called when we get another page from inside the files view function getPageFigures(res, viewingIdx) { // add all the figures to files_div drawPageOfFigures() } // grab all selected thumbnails and return a
containing the thumbnails // with extra yr and date attached as attributes so we can set the default // dir name for a move directory - not used in del, but no harm to include them function GetSelnAsDiv() { seln='' $('.highlight').each(function( index ) { seln+='
' + $(this).children().parent().html() + '
' seln+='' } ) return '
'+seln+'
' } // return a list of eid= for each selected thumbnail function GetSelnAsData() { to_del='' $('.highlight').each(function( index ) { to_del+='&eid-'+index+'='+$(this).attr('id') } ) return to_del } // use an ajax POST to force an AI scan on the selected image(s) function RunAIOnSeln(person) { post_data = GetSelnAsData() post_data += '&person='+person.replace('ai-','') $.ajax({ type: 'POST', data: post_data, url: '/run_ai_on', success: function(data){ CheckForJobs() } }) } // code to change the associated image/icon when the move to selection box (import or storage paths) is changed function change_rp_sel() { icon_url = $('option:selected', '#rp_sel').attr('icon_url') $('#move_path_icon').html( '' ) seld_ptype=$('option:selected', '#rp_sel').attr('path_type') $('#move_path_type').val( seld_ptype ) // clear all 'existing' buttons $('.move_Import').addClass('d-none') $('.move_Storage').addClass('d-none') // show just selected path's (relevant) buttons $('.move_'+seld_ptype).removeClass('d-none') } // POST to see if there are any other existing directories named around this date // (if so display them as options for a move) function GetExistingDirsAsDiv( dt, divname, ptype ) { $.ajax({ type: 'POST', data: null, url: '/get_existing_paths/'+dt, success: function(data) { $('#'+divname).html(data) dirs = JSON.parse(data) s='' dirs.forEach( function(item, index) { if( item.ptype != ptype ) vis = 'd-none ' else vis = '' s+= '' } ) if( s == '' ) $('#existing').html('') else $('#move_'+ptype).removeClass('invisible') $('#'+divname).html(s) } } ) } // wrapper to do some clean up before POST to /move_files or /delete_files // used to remove the highlighted item(s) && reset the numbering so highlighting continues to work function MoveOrDelCleanUpUI() { // remove the images being moved (so UI immediately 'sees' the move) $("[name^=eid-]").each( function() { $('#'+$(this).attr('value')).remove() } ) // reorder the images via ecnt again, so future highlighting can work // document.mf_id=0; $('.figure').each( function() { $(this).attr('ecnt', document.mf_id ); document.mf_id++ } ) $('#dbox').modal('hide') } // show the DBox for a move file, includes all thumbnails of selected files to move // and a pre-populated folder to move them into, with text field to add a suffix function MoveDBox(path_details) { $('#dbox-title').html('Move Selected File(s) to new directory in Storage Path') div =`

Moving the following files?

' div+=GetSelnAsDiv() yr=$('.highlight').first().attr('yr') dt=$('.highlight').first().attr('date') div+='
Use Existing Directory (in the chosen path):
' GetExistingDirsAsDiv( dt, "existing", path_details[0].type.name ) div+=`
` // NB: alert-primary here is a hack to get the bg the same color as the alert primary by div+= '' div+= '' div+=`
` $('#dbox-content').html(div) $('#dbox').modal('show') $("#prefix").keypress(function (e) { if (e.which == 13) { $("#move_submit").click(); return false; } } ) $("#suffix").keypress(function (e) { if (e.which == 13) { $("#move_submit").click(); return false; } } ) } // This function is called anytime we have a job that returns amendments // (visually we want to show this entry is being amended by a job) // as we check for a job to end every second, we can call this multiple times // during the runtime of a job, so only redraw/react to a new amendment // NOTE: we update all views, as we might go into one via jscript before the job ends function processAmendments( ams ) { for (const am of ams) { // if we return anything here, we already have this amendment, so continue to next if( document.amendments.filter(obj => obj.eid === am.eid).length > 0 ) continue document.amendments.push(am) if( document.viewing && document.viewing.id == am.eid ) { im.src=im.src + '?t=' + new Date().getTime(); DrawImg() } // find where in the page this image is being viewed idx = pageList.indexOf(am.eid) // createFigureHtml uses matching document.amendments to show thobber, etc html = createFigureHtml( document.entries[idx] ) $('#'+am.eid).replaceWith( html ) } } // function to add data for document.amendment based on id and amt // used when we transform several images in files_*, or single image in viewer // show the DBox for a delete/restore file, includes all thumbnails of selected files // with appropriate coloured button to Delete or Restore files` function DelDBox(del_or_undel) { to_del = GetSelnAsData() $('#dbox-title').html(del_or_undel+' Selected File(s)') div ='

' + del_or_undel + ' the following files?

' div+=GetSelnAsDiv() if( del_or_undel == "Delete" ) { which="delete" col="danger" } else { which="restore" col="sucess" } document.ents_to_del=[] $('.highlight').each(function( cnt ) { document.ents_to_del[cnt]=parseInt($(this).attr('id')) } ) div+=`
` $('#dbox-content').html(div) $('#dbox').modal('show') } // show the DBox for a lame quick version of file details function DetailsDBox() { $('#dbox-title').html('Details of Selected File(s)') var div ='
' $('.highlight').each(function( index ) { div += "
Name:
" + $(this).attr('fname') + "
" div += "
Date:
" + $(this).attr('pretty_date') + "
" dir = $(this).attr('in_dir') if( dir.slice(-1) != "/" ) dir=dir.concat('/') div += "
Dir:
" + dir + "
" div += "
Size:
" + $(this).attr('size') + " MB
" div += "
Hash:
" + $(this).attr('hash') + "
" div += "
Path Type:
" + $(this).attr('path_type') + "
" } ) div += `

` $('#dbox-content').html(div) $('#dbox').modal('show') } // DoSel is called when a click event occurs, and sets the selection via adding // 'highlight' to the class of the appropriate thumbnails // e == event (can see if shift/ctrl held down while left-clicking // el == element the click is on // this allows single-click to select, ctrl-click to (de)select 1 item, and // shift-click to add all elements between highlighted area and clicked el, // whether you click before highlight or after, or inside a gap and then back // or forward to the closest higlighted entry - also, only works on entry class, // so it ignores figures that we take entry off while we transform, etc it function DoSel(e, el) { const id = $(el).attr('id'); const entries = $('.entry'); // Collect currently highlighted entries const currentHighlights = $('.highlight'); const highlighted = new Set(); currentHighlights.each(function() { highlighted.add($(this).attr('id')); }); // Ctrl+click: toggle highlight for the clicked entry if (e.ctrlKey || document.fake_ctrl === 1) { $(el).toggleClass('highlight'); if (highlighted.has(id)) { highlighted.delete(id); } else { highlighted.add(id); } if (document.fake_ctrl === 1) { document.fake_ctrl = 0; } return; } // Shift+click: select a range else if (e.shiftKey || document.fake_shift === 1) { if (currentHighlights.length === 0) { // If no highlights, just highlight the clicked entry $(el).addClass('highlight'); highlighted.add(id); } else { // Find the nearest highlighted entry const clickedIndex = entries.index($(el)); let nearestHighlightIndex = -1; let minDistance = Infinity; currentHighlights.each(function() { const highlightIndex = entries.index($(this)); const distance = Math.abs(highlightIndex - clickedIndex); if (distance < minDistance) { minDistance = distance; nearestHighlightIndex = highlightIndex; } }); // Highlight the range between the nearest highlighted entry and the clicked entry const from = Math.min(clickedIndex, nearestHighlightIndex); const to = Math.max(clickedIndex, nearestHighlightIndex); for (let i = from; i <= to; i++) { const entryId = entries.eq(i).attr('id'); highlighted.add(entryId); entries.eq(i).addClass('highlight'); } } if (document.fake_shift === 1) { document.fake_shift = 0; } return; } // Single click: clear all highlights and highlight the clicked entry else { $('.highlight').removeClass('highlight'); highlighted.clear(); $(el).addClass('highlight'); highlighted.add(id); } } // if a selection exists, enable move & del/restore buttons otherwise disable them function SetButtonState() { var sel=false $('.highlight').each(function( index ) { sel=true } ) if( sel ) { $('#move').attr('disabled', false ) $('#del').attr('disabled', false ) } else { $('#move').attr('disabled', true ) $('#del').attr('disabled', true ) } } // Check if the set of highlights are either only figures, dirs or both // used to work out what options are shown in the file context menu function FiguresOrDirsOrBoth() { var figure=false var dir=false $('.highlight').each(function( index ) { if( $(this).hasClass('figure') ) { figure=true } if( $(this).hasClass('dir') ) { dir=true } } ) if( figure & ! dir ) return "figure" if( ! figure & dir ) return "dir" return "both" } // Check if the set of highlights contain Bin and Not Bin... // if its both, then no del/restore is possible in context menu // otherwise can either set del or restore appropriately function SelContainsBinAndNotBin() { var bin=false var not_bin=false $('.highlight').each(function( index ) { if( $(this).attr('path_type') == "Bin" ) { bin=true } else { not_bin=true } } ) if( bin && not_bin ) return true else return false } // checks to see if there is no selection active function NoSel() { var sel=false $('.highlight').each(function( index ) { sel=true } ) // func looks for No Selection, so if sel is true and we have a sel, return false (i.e. NOT No Sel -> Sel ) if( sel ) return false else return true } // quick wrapper to add a single
to the #figures div function addFigure( obj ) { html=createFigureHtml( obj ) $('#figures').append( html ) } /** * Renders a group header or entry based on the object and options. * obj - The object containing file/directory details. * returns {string} - Generated HTML string. */ function createFigureHtml( obj ) { // if am is null, no amendment for this obj, otherwise we have one var am=null for (const tmp of document.amendments) if( tmp.eid == obj.id ) am=tmp let html = ""; // Image/Video/Unknown entry if (obj.type.name === "Image" || obj.type.name === "Video" || obj.type.name === "Unknown") { const pathType = obj.in_dir.in_path.type.name; const size = obj.file_details.size_mb; const hash = obj.file_details.hash; const inDir = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}`; const fname = obj.name; const yr = obj.file_details.year; const date = `${yr}${String(obj.file_details.month).padStart(2, '0')}${String(obj.file_details.day).padStart(2, '0')}`; const prettyDate = `${obj.file_details.day}/${obj.file_details.month}/${obj.file_details.year}`; const type = obj.type.name; // if amendment for this obj, do not add entry class - prevents highlighting if( am ) { ent="" gs="style='filter: grayscale(100%);'" am_html ='' am_html+='' if( am.type.which == 'icon' ) am_html+=`` else am_html+=`` } else { ent="entry" gs="" am_html="" } html += `
${renderMedia(obj,gs,am_html)} ` } // Directory entry else if (obj.type.name === "Directory" && OPT.folders) { const dirname = obj.dir_details.rel_path.length ? `${obj.dir_details.in_path.path_prefix}/${obj.dir_details.rel_path}` : obj.dir_details.in_path.path_prefix; html += `
${obj.name}
`; html += ``; } // moved the bindings to here as we need to reset them if we recreate this Figure (after a transform job) html += `
` return html } // Helper function to render media (image/video/unknown) function renderMedia(obj,gs,am_html) { const isImageOrUnknown = obj.type.name === "Image" || obj.type.name === "Unknown"; const isVideo = obj.type.name === "Video"; const path = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}/${obj.name}`; const thumb = obj.file_details.thumbnail ? `${obj.name}` : ``; let mediaHtml = `
${thumb}${am_html}`; if (isVideo) { mediaHtml += `
`; } if (OPT.search_term) { mediaHtml += `
`; } mediaHtml += `
`; return mediaHtml; } // Helper: Get location icon (placeholder) function getLocationIcon(obj) { return ICON[obj.in_dir.in_path.type.name] } // POST to get entry ids, and then getPage for a specified directory function getDirEntries(dir_id, back) { data={} data.dir_id=dir_id data.back=back data.noo=OPT.noo $.ajax({ type: 'POST', url: '/get_dir_eids', data: JSON.stringify(data), contentType: 'application/json', dataType: 'json', success: function(res) { if( res.valid === false ) { $('#figures').html( "ERROR! directory has changed since you loaded this view. You have to reload and reset your view (probably someone deleted the directory or its parent since you loaded this page)" ) return } entryList=res.entry_list pageList=entryList.slice(0, OPT.how_many) // now go get actual data/entries getPage(1,getPageFigures) }, error: function(xhr, status, error) { console.error("Error:", error); } }); } // this function draws all the figures from document.entries - called when we // change pages, but also when we change say grouping/other OPTs function drawPageOfFigures() { $('#figures').empty() var last = { printed: null } // something is up, let the user know if( document.alert ) $('#figures').append( document.alert ) if( OPT.folders ) { // it root_eid is 0, then no entries in this path - cant go up if( OPT.root_eid == 0 || (document.entries.length && document.entries[0].in_dir.eid == OPT.root_eid ) ) { gray="_gray" back="" cl="" back_id=0 } else { gray="" back="Back" cl="back" if( document.entries.length > 0 ) back_id = document.entries[0].in_dir.eid else back_id = document.back_id } // back button, if gray/back decide if we see grayed out folder and/or the name of the folder we go back to // with clas "back" this gets a different click handler which flags server to return data by 'going back/up' in dir tree // we give the server the id of the first item on the page so it can work out how to go back html=`
${back}
` $('#figures').append(html) } for (const obj of document.entries) { // Grouping logic if (OPT.grouping === "Day") { if (last.printed !== obj.file_details.day) { $('#figures').append(`
Day: ${obj.file_details.day} of ${obj.file_details.month}/${obj.file_details.year}
` ); last.printed = obj.file_details.day; } } else if (OPT.grouping === "Week") { if (last.printed !== obj.file_details.woy) { $('#figures').append(`
Week #: ${obj.file_details.woy} of ${obj.file_details.year}
` ); last.printed = obj.file_details.woy; } } else if (OPT.grouping === "Month") { if (last.printed !== obj.file_details.month) { $('#figures').append(`
Month: ${obj.file_details.month} of ${obj.file_details.year}
` ); last.printed = obj.file_details.month; } } addFigure( obj ) } $(".back").click( function(e) { getDirEntries(this.id,true) } ) if( document.entries.length == 0 ) if( OPT.search_term ) $('#figures').append( ` No matches for: '${OPT.search_term}'` ) else if( OPT.root_eid == 0 ) $('#figures').append( `No files in Path!` ) } // emtpy out file_list_div, and repopulate it with new page of content function getPageFileList(res, viewingIdx) { $('#file_list_div').empty() // something is up, let the user know if( document.alert ) $('#file_list_div').append( '
' + document.alert + '
' ) if( OPT.root_eid == 0 ) { $('#file_list_div').append( `No files in Path!` ) return } html='' html+='' for (const obj of res) { html+=`` } html+='
NameSize (MB)Path PrefixHash
${obj.name}
${obj.file_details.size_mb} ${obj.in_dir.in_path.path_prefix.replace("static/","")}/${obj.in_dir.rel_path} ${obj.file_details.hash}
' $('#file_list_div').append(html) } // wrapper function as we want to handle real DB query success, but also do the // same when we just use cache function getEntriesByIdSuccessHandler(res,pageNumber,successCallback,viewingIdx) { if( res.length != pageList.length ) document.alert="WARNING: something has changed since viewing this page (likely someone deleted content in another view), strongly suggest a page reload to get the latest data" document.entries=res; // cache this document.page[pageNumber]=res // FIXME: I want to remove successCallback, instead: if viewing, or files_*, or file_list, then call relevant draw routine successCallback(res,viewingIdx) resetNextPrevButtons() // if search, disable folders if( OPT.search_term ) $('#folders').prop('disabled', 'disabled').removeClass('border-info').addClass('border-secondary').removeClass('text-info').addClass('text-secondary'); else if( document.entries.length == 0 ) { html=`No files in Path` $('#file_list_div').append(html) $('#figures').append(html) } } // Function to get the 'page' of entry ids out of entryList function getPage(pageNumber, successCallback, viewingIdx=0) { // before we do anything, disabled left/right arrows on viewer to stop // getting another event before we have the data for the page back $('#la').prop('disabled', true) $('#ra').prop('disabled', true) const startIndex = (pageNumber - 1) * OPT.how_many; const endIndex = startIndex + OPT.how_many; pageList = entryList.slice(startIndex, endIndex); // set up data to send to server to get the entry data for entries in pageList data={} data.ids = pageList // assume nothing wrong, but if the data goes odd, then this will be non-null and displayed later (cant add here, as later code does .empty() of file divs) document.alert=null // see if we can use cache, and dont reload from DB if( !OPT.folders && document.page.length && document.page[pageNumber] ) { getEntriesByIdSuccessHandler( document.page[pageNumber], pageNumber, successCallback, viewingIdx ) return } $.ajax({ type: 'POST', url: '/get_entries_by_ids', data: JSON.stringify(data), contentType: 'application/json', dataType: 'json', success: function(res) { document.amendments=res.amendments; // only called when an amendment is pending & we are viewing a page in files/list view // so check for amendment job(s) ending... for (const tmp of document.amendments) checkForAmendmentJobToComplete(tmp.job_id) getEntriesByIdSuccessHandler( res.entries, pageNumber, successCallback, viewingIdx ) }, error: function(xhr, status, error) { console.error("Error:", error); } }); return } // Quick Function to check if we are on the first page function isFirstPage(pageNumber) { return pageNumber <= 1; } // Function to check if we are on the last page function isLastPage(pageNumber) { const totalPages = Math.ceil(entryList.length / OPT.how_many); return pageNumber >= totalPages; } // given an id in the list, return which page we are on (page 1 is first page) function getPageNumberForId(id) { const idx = entryList.indexOf(id); // should be impossible but jic if (idx === -1) { return -1 } return Math.floor(idx / OPT.how_many) + 1; } // if we are on first page, disable prev, it not ensure next is enabled // if we are on last page, disable next, it not ensure prev is enabled function resetNextPrevButtons() { // no data, so disabled both if( getPageNumberForId(pageList[0]) == -1 ) { $('.prev').prop('disabled', true).addClass('disabled'); $('.next').prop('disabled', true).addClass('disabled'); return } if ( isFirstPage( getPageNumberForId(pageList[0]) ) ) $('.prev').prop('disabled', true).addClass('disabled'); else $('.prev').prop('disabled', false).removeClass('disabled'); if ( isLastPage( getPageNumberForId(pageList[0]) ) ) $('.next').prop('disabled', true).addClass('disabled'); else $('.next').prop('disabled', false).removeClass('disabled'); } // get list of eids for the next page, also make sure next/prev buttons make sense for page we are on function nextPage(successCallback) { // pageList[0] is the first entry on this page const currentPage=getPageNumberForId( pageList[0] ) // should never happen / just return pageList unchanged if ( currentPage === -1 || isLastPage( currentPage ) ) { console.error( "WARNING: seems first on pg=" + pageList[0] + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go next page?" ) return } getPage( currentPage+1, successCallback ) return } // get list of eids for the prev page, also make sure next/prev buttons make sense for page we are on function prevPage(successCallback) { // pageList[0] is the first entry on this page const currentPage=getPageNumberForId( pageList[0] ) // should never happen / just return pageList unchanged if (currentPage === 1 || currentPage === -1 ) { console.error( "WARNING: seems first on pg=" + pageList[0] + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go prev page?" ) return } getPage( currentPage-1, successCallback ) return } // function to see if we are on a phone or tablet (where we dont have ctrl or shift keys - helps to display fake buttons to allow multiselect on mobiles) function isMobile() { try{ document.createEvent("TouchEvent"); return true; } catch(e){ return false; } } // when we change one of the options (noo, how_many, folders) - then update '{how_many} files' str, // tweak noo menu for folders/flat view then reset the page contents based on current OPT values function changeOPT(successCallback) { OPT.how_many=$('#how_many').val() // changes invalidate page cache so clear it out document.page.length=0 new_f=$('#folders').val() new_f=( new_f == 'True' ) // if change to/from folders, also fix the noo menu if( new_f != OPT.folders ) { if( new_f ) { $('#noo option:lt(2)').prop('disabled', true); $('#noo').val(OPT.default_folder_noo) } else { $('#noo option:lt(2)').prop('disabled', false); $('#noo').val(OPT.default_flat_noo) } } OPT.noo=$('#noo').val() OPT.folders=new_f OPT.folders=$('#folders').val() OPT.grouping=$('#grouping').val() OPT.size=$('input[name="size"]:checked').val(); $.ajax({ type: 'POST', url: '/change_file_opts', data: JSON.stringify(OPT), contentType: 'application/json', success: function(resp) { entryList=resp.query_data.entry_list OPT.how_many=parseInt(OPT.how_many) pageList=entryList.slice(0, OPT.how_many) // put data back into booleans, ints, etc OPT.folders=( OPT.folders == 'True' ) $('.how_many_text').html( ` ${OPT.how_many} files ` ) OPT.size=parseInt(OPT.size) getPage(1,successCallback) } }) } // function to change the size of thumbnails when user clicks xs/s/m/l/xl buttons function changeSize() { sz=$('input[name="size"]:checked').val(); OPT.size=sz $('.thumb').attr( {height: sz, style: 'font-size:'+sz+'px' } ) $('#size').val(sz) sz=sz-22 $('.svg').height(sz); $('.svg').width(sz); $('.svg_cap').width(sz); } // when a delete or restore files job has completed successfullly, then get ids // find the page we are on, remove amendments & ids from entryList and re-get page // which will reset pageList and the UI of images for that page function handleDeleteOrRestoreFileJobCompleted(job) { // this grabs the values from the object attributes of eid-0, eid-1, etc. const ids = job.extra.map(item => item.value) // find page number of first element to delete (this is the page we will return too) pnum=getPageNumberForId( ids[0] ) // remove amendment data for (const ent of ids) { id=parseInt(ent) removeAmendment( id ) // remove the item in the entryList index=entryList.indexOf(id); if( index != -1 ) entryList.splice(index, 1); // Remove the element else { return; // have to get out of here, or calling getPage() below will loop forever } } // re-create pageList by reloading the page getPage(pnum,getPageFigures) } // POST to a check URL, that will tell us if the amendment job has completed, // it also calls CheckForJobs() which will fix up the Active Jobs badge, function checkForAmendmentJobToComplete(job_id) { CheckForJobs() $.ajax( { type: 'POST', data: '&job_id='+job_id, url: '/check_amend_job_status', success: function(res) { handleCheckAmendmentJobStatus(res); } } ) } // the status of a Amendment Job has been returned, finished is True/False // if not finished try again in 1 second... If finished then invalidate page // cache and based on job type call code correct func to update the UI appropriately function handleCheckAmendmentJobStatus(data) { if( data.finished ) { // invalidate the cache document.page.length=0 // transforms contain the single transformed entry data for convenience if( data.job.name == 'transform_image' ) handleTransformImageJobCompleted(data.job, data.entry) else if ( data.job.name == 'delete_files' || data.job.name == 'restore_files' ) handleDeleteOrRestoreFileJobCompleted(data.job) } else { setTimeout( function() { checkForAmendmentJobToComplete(data.job.id) }, 1000 ); } } // different context menu on files $.contextMenu({ selector: '.entry', itemClickEvent: "click", build: function($triggerElement, e) { // when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln if( NoSel() || e.ctrlKey || e.shiftKey ) { DoSel(e, e.currentTarget ) SetButtonState(); } if( FiguresOrDirsOrBoth() == "figure" ) { item_list = { details: { name: "Details..." }, view: { name: "View File" }, sep: "---", } if( e.currentTarget.getAttribute('type') == 'Image' ) { item_list['transform'] = { name: "Transform", items: { "r90": { "name" : "Rotate 90 degrees" }, "r180": { "name" : "Rotate 180 degrees" }, "r270": { "name" : "Rotate 270 degrees" }, "fliph": { "name" : "Flip horizontally" }, "flipv": { "name" : "Flip vertically" } } } } item_list['move'] = { name: "Move selected file(s) to new folder" } item_list['sep2'] = { sep: "---" } } else item_list = { move: { name: "Move selection(s) to new folder" } } item_list['ai'] = { name: "Scan file for faces", items: { "ai-all": { name: "all" } } }; // Dynamically add entries for each person in the `people` array people.forEach(person => { item_list['ai'].items[`ai-${person.tag}`] = { name: person.tag }; }); if( SelContainsBinAndNotBin() ) { item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true } } else { if (e.currentTarget.getAttribute('path_type') == 'Bin' ) item_list['undel']= { name: "Restore selected file(s)" } else if( e.currentTarget.getAttribute('type') != 'Directory' ) item_list['del']= { name: "Delete Selected file(s)" } } return { callback: function( key, options) { if( key == "details" ) { DetailsDBox() } if( key == "view" ) { startViewing( $(this).attr('id') ) } if( key == "move" ) { MoveDBox(move_paths) } if( key == "del" ) { DelDBox('Delete') } if( key == "undel") { DelDBox('Restore') } if( key == "r90" ) { Transform(90) } if( key == "r180" ) { Transform(180) } if( key == "r270" ) { Transform(270) } if( key == "fliph" ) { Transform("fliph") } if( key == "flipv" ) { Transform("flipv") } if( key.startsWith("ai")) { RunAIOnSeln(key) } // dont flow this event through the dom e.stopPropagation() }, items: item_list }; } }); // finally, for files_ip/files_sp/files_rbp - set click inside document (NOT an entry) to remove seln $(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() }); document.page=[]