UIWindow.js 152 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326
  1. /**
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import UIAlert from './UIAlert.js';
  20. import UIContextMenu from './UIContextMenu.js';
  21. import path from '../lib/path.js';
  22. import UITaskbarItem from './UITaskbarItem.js';
  23. import UIWindowLogin from './UIWindowLogin.js';
  24. import UIWindowPublishWebsite from './UIWindowPublishWebsite.js';
  25. import UIWindowItemProperties from './UIWindowItemProperties.js';
  26. import new_context_menu_item from '../helpers/new_context_menu_item.js';
  27. import refresh_item_container from '../helpers/refresh_item_container.js';
  28. const el_body = document.getElementsByTagName('body')[0];
  29. async function UIWindow(options) {
  30. const win_id = global_element_id++;
  31. last_window_zindex++;
  32. // options.dominant places the window in center close to top.
  33. options.dominant = options.dominant ?? false;
  34. // in case of file dialogs, the window is automatically dominant
  35. if(options.is_openFileDialog || options.is_saveFileDialog || options.is_directoryPicker)
  36. options.dominant = true;
  37. // we don't want to increment window_counter for dominant windows
  38. if(!options.dominant)
  39. window.window_counter++;
  40. // add this window's id to the window_stack
  41. window_stack.push(win_id);
  42. // =====================================
  43. // set options defaults
  44. // =====================================
  45. // indicates if sidebar is hidden, only applies to directory windows
  46. let sidebar_hidden = false;
  47. const default_window_top = ('calc(15% + ' + ((window.window_counter-1) % 10 * 20) + 'px)');
  48. // list of file types that are allowed, other types will be disabled but still shown
  49. options.allowed_file_types = options.allowed_file_types ?? '';
  50. options.app = options.app ?? '';
  51. options.allow_context_menu = options.allow_context_menu ?? true;
  52. options.allow_native_ctxmenu = options.allow_native_ctxmenu ?? false;
  53. options.allow_user_select = options.allow_user_select ?? false;
  54. options.backdrop = options.backdrop ?? false;
  55. options.body_css = options.body_css ?? {};
  56. options.border_radius = options.border_radius ?? undefined;
  57. options.draggable_body = options.draggable_body ?? false;
  58. options.element_uuid = options.element_uuid ?? uuidv4();
  59. options.center = options.center ?? false;
  60. options.close_on_backdrop_click = options.close_on_backdrop_click ?? true;
  61. options.disable_parent_window = options.disable_parent_window ?? false;
  62. options.has_head = options.has_head ?? true;
  63. options.height = options.height ?? 380;
  64. options.icon = options.icon ?? null;
  65. options.iframe_msg_uid = options.iframe_msg_uid ?? null;
  66. options.is_droppable = options.is_droppable ?? true;
  67. options.is_draggable = options.is_draggable ?? true;
  68. options.is_dir = options.is_dir ?? false;
  69. options.is_minimized = options.is_minimized ?? false;
  70. options.is_maximized = options.is_maximized ?? false;
  71. options.is_openFileDialog = options.is_openFileDialog ?? false;
  72. options.is_resizable = options.is_resizable ?? true;
  73. // if this is a fullpage window, it won't be resizable
  74. if(options.is_fullpage)
  75. options.is_resizable = false;
  76. // in the embedded/fullpage mode every window is on top since there is no taskbar to switch between windows
  77. // if user has specifically asked for this window to NOT stay on top, honor it.
  78. if((is_embedded || window.is_fullpage_mode) && !options.parent_uuid && options.stay_on_top !== false)
  79. options.stay_on_top = true;
  80. // Keep the window on top of all previously opened windows
  81. options.stay_on_top = options.stay_on_top ?? false;
  82. options.is_saveFileDialog = options.is_saveFileDialog ?? false;
  83. options.show_minimize_button = options.show_minimize_button ?? true;
  84. options.on_close = options.on_close ?? undefined;
  85. options.parent_uuid = options.parent_uuid ?? null;
  86. options.selectable_body = options.selectable_body ?? true;
  87. options.show_in_taskbar = options.show_in_taskbar ?? true;
  88. options.show_maximize_button = options.show_maximize_button ?? true;
  89. options.single_instance = options.single_instance ?? false;
  90. options.sort_by = options.sort_by ?? 'name';
  91. options.sort_order = options.sort_order ?? 'asc';
  92. options.title = options.title ?? null;
  93. options.top = options.top ?? default_window_top;
  94. options.type = options.type ?? null;
  95. options.update_window_url = options.update_window_url ?? false;
  96. options.layout = options.layout ?? 'icons';
  97. options.width = options.width ?? 680;
  98. options.window_css = options.window_css ?? {};
  99. options.window_class = (options.window_class !== undefined ? ' ' + options.window_class : '');
  100. options.is_visible = options.is_visible ?? true;
  101. // if only one instance is allowed, bring focus to the window that is already open
  102. if(options.single_instance && options.app !== ''){
  103. let $already_open_window = $(`.window[data-app="${html_encode(options.app)}"]`);
  104. if($already_open_window.length){
  105. $(`.window[data-app="${html_encode(options.app)}"]`).focusWindow();
  106. return;
  107. }
  108. }
  109. // left
  110. if(!options.dominant && !options.center){
  111. options.left = options.left ?? ((window.innerWidth/2 - options.width/2) +(window.window_counter-1) % 10 * 30) + 'px';
  112. }else if(!options.dominant && options.center){
  113. options.left = options.left ?? ((window.innerWidth/2 - options.width/2)) + 'px';
  114. }
  115. else if(options.dominant){
  116. options.left = (window.innerWidth/2 - options.width/2) + 'px';
  117. }
  118. else
  119. options.left = options.left ?? ((window.innerWidth/2 - options.width/2) + 'px');
  120. // top
  121. if(!options.dominant && !options.center){
  122. options.top = options.top ?? ((window.innerHeight/2 - options.height/2) +(window.window_counter-1) % 10 * 30) + 'px';
  123. }else if(!options.dominant && options.center){
  124. options.top = options.top ?? ((window.innerHeight/2 - options.height/2)) + 'px';
  125. }
  126. else if(options.dominant){
  127. options.top = (window.innerHeight * 0.15);
  128. }
  129. else if(isMobile.phone)
  130. options.top = 100;
  131. if(isMobile.phone){
  132. options.left = 0;
  133. options.top = window.toolbar_height + 'px';
  134. options.width = '100%';
  135. options.height = 'calc(100% - ' + window.toolbar_height + 'px)';
  136. }else{
  137. options.width += 'px'
  138. options.height += 'px'
  139. }
  140. // =====================================
  141. // cover page
  142. // =====================================
  143. if(options.cover_page){
  144. options.left = 0;
  145. options.top = 0;
  146. options.width = '100%';
  147. options.height = '100%';
  148. }
  149. // --------------------------------------------------------
  150. // HTML for Window
  151. // --------------------------------------------------------
  152. let h = '';
  153. // Window
  154. let zindex = options.stay_on_top ? (99999999 + last_window_zindex + 1 + ' !important') : last_window_zindex;
  155. h += `<div class="window window-active
  156. ${options.cover_page ? 'window-cover-page' : ''}
  157. ${options.uid !== undefined ? 'window-'+options.uid : ''}
  158. ${options.window_class}
  159. ${options.allow_user_select ? ' allow-user-select' : ''}
  160. ${options.is_openFileDialog || options.is_saveFileDialog || options.is_directoryPicker ? 'window-filedialog' : ''}"
  161. id="window-${win_id}"
  162. data-allowed_file_types = "${html_encode(options.allowed_file_types)}"
  163. data-app="${html_encode(options.app)}"
  164. data-app_uuid="${html_encode(options.app_uuid ?? '')}"
  165. data-disable_parent_window = "${html_encode(options.disable_parent_window)}"
  166. data-name="${html_encode(options.title)}"
  167. data-path ="${html_encode(options.path)}"
  168. data-uid ="${options.uid}"
  169. data-element_uuid="${options.element_uuid}"
  170. data-parent_uuid="${options.parent_uuid}"
  171. ${options.parent_instance_id ? `data-parent_instance_id="${options.parent_instance_id}"` : ''}
  172. data-id ="${win_id}"
  173. data-iframe_msg_uid ="${options.iframe_msg_uid}"
  174. data-is_dir ="${options.is_dir}"
  175. data-return_to_parent_window = "${options.return_to_parent_window}"
  176. data-initiating_app_uuid = "${options.initiating_app_uuid}"
  177. data-is_openFileDialog ="${options.is_openFileDialog}"
  178. data-is_saveFileDialog ="${options.is_saveFileDialog}"
  179. data-is_directoryPicker ="${options.is_directoryPicker}"
  180. data-is_fullpage ="${options.is_fullpage ? 1 : 0}"
  181. data-is_minimized ="${options.is_minimized ? 1 : 0}"
  182. data-is_maximized ="${options.is_maximized ? 1 : 0}"
  183. data-layout ="${options.layout}"
  184. data-stay_on_top ="${options.stay_on_top}"
  185. data-sort_by ="${options.sort_by ?? 'name'}"
  186. data-sort_order ="${options.sort_order ?? 'asc'}"
  187. data-multiselectable = "${options.selectable_body}"
  188. data-update_window_url = "${options.update_window_url}"
  189. data-initial_zindex = "${zindex}"
  190. style=" z-index: ${zindex};
  191. ${options.width !== undefined ? 'width: ' + html_encode(options.width) +'; ':''}
  192. ${options.height !== undefined ? 'height: ' + html_encode(options.height) +'; ':''}
  193. ${options.border_radius !== undefined ? 'border-radius: ' + html_encode(options.border_radius) +'; ':''}
  194. "
  195. >`;
  196. // window mask
  197. h += `<div class="window-disable-mask">`;
  198. //busy indicator
  199. h += `<div class="busy-indicator">BUSY</div>`;
  200. h += `</div>`;
  201. // Head
  202. if(options.has_head){
  203. h += `<div class="window-head">`;
  204. // draggable handle which also contains icon and title
  205. h+=`<div class="window-head-draggable">`;
  206. // icon
  207. if(options.icon)
  208. h += `<img class="window-head-icon" />`;
  209. // title
  210. h += `<span class="window-head-title" title="${html_encode(options.title)}"></span>`;
  211. h += `</div>`;
  212. // Minimize button, only if window is resizable and not embedded
  213. if(options.is_resizable && options.show_minimize_button && !is_embedded)
  214. h += `<span class="window-action-btn window-minimize-btn" style="margin-left:0;"><img src="${html_encode(window.icons['minimize.svg'])}" draggable="false"></span>`;
  215. // Maximize button
  216. if(options.is_resizable && options.show_maximize_button)
  217. h += `<span class="window-action-btn window-scale-btn"><img src="${html_encode(window.icons['scale.svg'])}" draggable="false"></span>`;
  218. // Close button
  219. h += `<span class="window-action-btn window-close-btn"><img src="${html_encode(window.icons['close.svg'])}" draggable="false"></span>`;
  220. h += `</div>`;
  221. }
  222. // Sidebar
  223. if(options.is_dir && !isMobile.phone){
  224. h += `<div class="window-sidebar disable-user-select hide-scrollbar"
  225. style="${window.window_sidebar_width ? 'width: ' + html_encode(window.window_sidebar_width) + 'px !important;' : ''}"
  226. draggable="false"
  227. >`;
  228. // favorites
  229. h += `<h2 class="window-sidebar-title disable-user-select">Favorites</h2>`;
  230. h += `<div draggable="false" title="Home" class="window-sidebar-item disable-user-select ${options.path === window.home_path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(window.home_path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(window.icons['folder-home.svg'])}">Home</div>`;
  231. h += `<div draggable="false" title="Documents" class="window-sidebar-item disable-user-select ${options.path === window.docs_path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(window.docs_path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(window.icons['folder-documents.svg'])}">Documents</div>`;
  232. h += `<div draggable="false" title="Pictures" class="window-sidebar-item disable-user-select ${options.path === window.pictures_path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(window.pictures_path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(window.icons['folder-pictures.svg'])}">Pictures</div>`;
  233. h += `<div draggable="false" title="Desktop" class="window-sidebar-item disable-user-select ${options.path === window.desktop_path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(window.desktop_path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(window.icons['folder-desktop.svg'])}">Desktop</div>`;
  234. h += `<div draggable="false" title="Videos" class="window-sidebar-item disable-user-select ${options.path === window.videos_path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(window.videos_path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(window.icons['folder-videos.svg'])}">Videos</div>`;
  235. h += `</div>`;
  236. }
  237. // Navbar
  238. if(options.is_dir){
  239. h += `<div class="window-navbar">`;
  240. h += `<div style="float:left; margin-left:5px; margin-right:5px;">`;
  241. // Back
  242. h += `<img draggable="false" class="window-navbar-btn window-navbar-btn-back window-navbar-btn-disabled" src="${html_encode(window.icons['arrow-left.svg'])}" title="Click to go back.">`;
  243. // Forward
  244. h += `<img draggable="false" class="window-navbar-btn window-navbar-btn-forward window-navbar-btn-disabled" src="${html_encode(window.icons['arrow-right.svg'])}" title="Click to go forward.">`;
  245. // Up
  246. h += `<img draggable="false" class="window-navbar-btn window-navbar-btn-up ${options.path === '/' ? 'window-navbar-btn-disabled' : ''}" src="${html_encode(window.icons['arrow-up.svg'])}" title="Click to go one directory up.">`;
  247. h += `</div>`;
  248. // Path
  249. h += `<div class="window-navbar-path">${navbar_path(options.path, window.user.username)}</div>`;
  250. // Path editor
  251. h += `<input class="window-navbar-path-input" data-path="${html_encode(options.path)}" value="${html_encode(options.path)}" spellcheck="false"/>`;
  252. // Layout settings
  253. h += `<img class="window-navbar-layout-settings" src="${html_encode(options.layout === 'icons' ? window.icons['layout-icons.svg'] : window.icons['layout-list.svg'])}" draggable="false">`;
  254. h += `</div>`;
  255. }
  256. // Body
  257. h += `<div
  258. class="window-body${options.is_dir ? ' item-container' : ''}${options.iframe_url !== undefined || options.iframe_srcdoc !== undefined ? ' window-body-app' : ''}${options.is_saveFileDialog || options.is_openFileDialog || options.is_directoryPicker ? ' window-body-filedialog' : ''}"
  259. data-allowed_file_types="${html_encode(options.allowed_file_types)}"
  260. data-path="${html_encode(options.path)}"
  261. data-multiselectable = "${options.selectable_body}"
  262. data-sort_by ="${options.sort_by ?? 'name'}"
  263. data-sort_order ="${options.sort_order ?? 'asc'}"
  264. data-uid ="${options.uid}"
  265. id="window-body-${win_id}"
  266. style="${!options.has_head ? ' height: 100%;' : ''}">`;
  267. // iframe, for apps
  268. if(options.iframe_url || options.iframe_srcdoc){
  269. // iframe
  270. h += `<iframe tabindex="-1"
  271. data-app="${html_encode(options.app)}"
  272. class="window-app-iframe"
  273. allowtransparency="true" allowpaymentrequest="true" allowfullscreen="true"
  274. frameborder="0" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"
  275. ${options.iframe_url ? 'src="'+ html_encode(options.iframe_url)+'"' : ''}
  276. ${options.iframe_srcdoc ? 'srcdoc="'+ html_encode(options.iframe_srcdoc) +'"' : ''}
  277. allow = "accelerometer; camera; encrypted-media; gamepad; display-capture; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write; web-share; fullscreen;"
  278. sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-top-navigation-by-user-activation allow-downloads allow-presentation allow-storage-access-by-user-activation"></iframe>`;
  279. }
  280. // custom body
  281. else if(options.body_content !== undefined){
  282. h += options.body_content;
  283. }
  284. // Directory
  285. if(options.is_dir){
  286. // Detail layout header
  287. h += window.explore_table_headers();
  288. // Add 'This folder is empty' message by default
  289. h += `<div class="explorer-empty-message">This folder is empty</div>`;
  290. h += `<div class="explorer-error-message">Error message is missing</div>`;
  291. // Loading spinner
  292. h += `<div class="explorer-loading-spinner">`;
  293. h +=`<svg style="display:block; margin: 0 auto; " xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>`;
  294. h += `<p class="explorer-loading-spinner-msg">${i18n('loading')}...</p>`;
  295. h += `</div>`;
  296. }
  297. h += `</div>`;
  298. // Explorer footer
  299. if(options.is_dir && !options.is_saveFileDialog && !options.is_openFileDialog && !options.is_directoryPicker){
  300. h += `<div class="explorer-footer">`
  301. h += `<span class="explorer-footer-item-count"></span>`;
  302. h += `<span class="explorer-footer-seperator">|</span>`;
  303. h += `<span class="explorer-footer-selected-items-count"></span>`;
  304. h += `</div>`;
  305. }
  306. // is_saveFileDialog
  307. if(options.is_saveFileDialog){
  308. h += `<div class="window-filedialog-prompt">`;
  309. h += `<div style="display:flex;">`;
  310. h += `<input type="text" class="savefiledialog-filename" autocorrect="off" spellcheck="false" value="${html_encode(options.saveFileDialog_default_filename) ?? ''}">`;
  311. h += `<button class="button button-small filedialog-cancel-btn">Cancel</button>`;
  312. h += `<button class="button `;
  313. if(options.saveFileDialog_default_filename === undefined || options.saveFileDialog_default_filename === '')
  314. h+= `disabled `;
  315. h += `button-small button-primary savefiledialog-save-btn">Save</button>`;
  316. h += `</div>`;
  317. h += `</div>`;
  318. }
  319. // is_openFileDialog
  320. else if(options.is_openFileDialog){
  321. h += `<div class="window-filedialog-prompt">`;
  322. h += `<div style="text-align:right;">`;
  323. h += `<button class="button button-small filedialog-cancel-btn">Cancel</button>`;
  324. h += `<button class="button disabled button-small button-primary openfiledialog-open-btn">Open</button>`;
  325. h += `</div>`;
  326. h += `</div>`;
  327. }
  328. // is_directoryPicker
  329. else if(options.is_directoryPicker){
  330. h += `<div class="window-filedialog-prompt">`;
  331. h += `<div style="text-align:right;">`;
  332. h += `<button class="button button-small filedialog-cancel-btn">Cancel</button>`;
  333. h += `<button class="button button-small button-primary directorypicker-select-btn" style="margin-left:10px;">Select</button>`;
  334. h += `</div>`;
  335. h += `</div>`;
  336. }
  337. h += `</div>`;
  338. // backdrop
  339. if(options.backdrop){
  340. let backdrop_zindex;
  341. // backdrop should also cover over taskbar
  342. let taskbar_zindex = $('.taskbar').css('z-index');
  343. if(taskbar_zindex === null || taskbar_zindex === undefined)
  344. backdrop_zindex = zindex;
  345. else{
  346. taskbar_zindex = parseInt(taskbar_zindex);
  347. backdrop_zindex = taskbar_zindex > zindex ? taskbar_zindex : zindex;
  348. }
  349. h = `<div class="window-backdrop" style="z-index:${backdrop_zindex};">` + h + `</div>`;
  350. }
  351. // Append
  352. $(el_body).append(h);
  353. // disable_parent_window
  354. if(options.disable_parent_window && options.parent_uuid !== null){
  355. const $el_parent_window = $(`.window[data-element_uuid="${options.parent_uuid}"]`);
  356. const $el_parent_disable_mask = $el_parent_window.find('.window-disable-mask');
  357. //disable parent window
  358. $el_parent_window.addClass('window-disabled')
  359. $el_parent_disable_mask.show();
  360. $el_parent_disable_mask.css('z-index', parseInt($el_parent_window.css('z-index')) + 1);
  361. $el_parent_window.find('iframe').blur();
  362. }
  363. // Add Taskbar Item
  364. if(!options.is_openFileDialog && !options.is_saveFileDialog && !options.is_directoryPicker && options.show_in_taskbar){
  365. // add icon if there is no similar app already open
  366. if($(`.taskbar-item[data-app="${options.app}"]`).length === 0){
  367. UITaskbarItem({
  368. icon: options.icon,
  369. name: options.title,
  370. app: options.app,
  371. open_windows_count: 1,
  372. onClick: function(){
  373. let open_window_count = parseInt($(`.taskbar-item[data-app="${options.app}"]`).attr('data-open-windows'));
  374. if(open_window_count === 0){
  375. launch_app({
  376. name: options.app,
  377. })
  378. }else{
  379. return false;
  380. }
  381. }
  382. });
  383. if(options.app)
  384. $(`.taskbar-item[data-app="${options.app}"] .active-taskbar-indicator`).show();
  385. }else{
  386. if(options.app){
  387. $(`.taskbar-item[data-app="${options.app}"]`).attr('data-open-windows', parseInt($(`.taskbar-item[data-app="${options.app}"]`).attr('data-open-windows')) + 1);
  388. $(`.taskbar-item[data-app="${options.app}"] .active-taskbar-indicator`).show();
  389. }
  390. }
  391. }
  392. // if directory, set window_nav_history and window_nav_history_current_position
  393. if(options.is_dir){
  394. window_nav_history[win_id] = [options.path];
  395. window_nav_history_current_position[win_id] = 0;
  396. }
  397. // get all the elements needed
  398. const el_window = document.querySelector(`#window-${win_id}`);
  399. const el_window_head = document.querySelector(`#window-${win_id} > .window-head`);
  400. const el_window_sidebar = document.querySelector(`#window-${win_id} > .window-sidebar`);
  401. const el_window_head_title = document.querySelector(`#window-${win_id} > .window-head .window-head-title`);
  402. const el_window_head_icon = document.querySelector(`#window-${win_id} > .window-head .window-head-icon`);
  403. const el_window_head_scale_btn = document.querySelector(`#window-${win_id} > .window-head > .window-scale-btn`);
  404. const el_window_navbar_back_btn = document.querySelector(`#window-${win_id} .window-navbar-btn-back`);
  405. const el_window_navbar_forward_btn = document.querySelector(`#window-${win_id} .window-navbar-btn-forward`);
  406. const el_window_navbar_up_btn = document.querySelector(`#window-${win_id} .window-navbar-btn-up`);
  407. const el_window_body = document.querySelector(`#window-${win_id} > .window-body`);
  408. const el_window_app_iframe = document.querySelector(`#window-${win_id} > .window-body > .window-app-iframe`);
  409. const el_savefiledialog_filename = document.querySelector(`#window-${win_id} .savefiledialog-filename`);
  410. const el_savefiledialog_save_btn = document.querySelector(`#window-${win_id} .savefiledialog-save-btn`);
  411. const el_filedialog_cancel_btn = document.querySelector(`#window-${win_id} .filedialog-cancel-btn`);
  412. const el_openfiledialog_open_btn = document.querySelector(`#window-${win_id} .openfiledialog-open-btn`);
  413. const el_directorypicker_select_btn = document.querySelector(`#window-${win_id} .directorypicker-select-btn`);
  414. if(options.is_maximized){
  415. // save original size and position
  416. $(el_window).attr({
  417. 'data-left-before-maxim': ((window.innerWidth/2 - 680/2) +(window.window_counter-1) % 10 * 30) + 'px',
  418. 'data-top-before-maxim': default_window_top,
  419. 'data-width-before-maxim': '680px',
  420. 'data-height-before-maxim': '350px',
  421. 'data-is_maximized': '1',
  422. });
  423. // shrink icon
  424. $(el_window).find('.window-scale-btn>img').attr('src', window.icons['scale-down-3.svg']);
  425. // set new size and position
  426. $(el_window).css({
  427. 'top': window.toolbar_height + 'px',
  428. 'left': '0',
  429. 'width': '100%',
  430. 'height': `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)`,
  431. 'transform': 'none',
  432. });
  433. }
  434. // when a window is created, focus is brought to it and
  435. // therefore it is the current active element
  436. window.active_element = el_window;
  437. // set name
  438. $(el_window_head_title).html(html_encode(options.title));
  439. // set icon
  440. if(options.icon)
  441. $(el_window_head_icon).attr('src', options.icon.image ?? options.icon);
  442. // root folder of a shared user?
  443. if(options.is_dir && (options.path.split('/').length - 1) === 1 && options.path !== '/'+window.user.username){
  444. $(el_window_head_icon).attr('src', window.icons['shared.svg']);
  445. }
  446. // focus on this window and deactivate other windows
  447. if ( options.is_visible ) {
  448. $(el_window).focusWindow();
  449. }
  450. if (animate_window_opening) {
  451. // animate window opening
  452. $(el_window).css({
  453. 'opacity': '0',
  454. 'transition': 'opacity 70ms ease-in-out',
  455. });
  456. // Use requestAnimationFrame to schedule a function to run at the next repaint of the browser window
  457. requestAnimationFrame(() => {
  458. // Change the window's opacity to 1 and scale to 1 to create an opening effect
  459. $(el_window).css({
  460. 'opacity': '1',
  461. })
  462. // Set a timeout to run after the transition duration (100ms)
  463. setTimeout(function () {
  464. // Remove the transition property, so future CSS changes won't be animated
  465. $(el_window).css({
  466. 'transition': 'none',
  467. })
  468. }, 70);
  469. });
  470. }
  471. // =====================================
  472. // Center relative to parent window
  473. // =====================================
  474. if(options.parent_center && options.parent_uuid){
  475. const $parent_window = $(`.window[data-element_uuid="${options.parent_uuid}"]`);
  476. const parent_window_width = $parent_window.width();
  477. const parent_window_height = $parent_window.height();
  478. const parent_window_left = $parent_window.offset().left;
  479. const parent_window_top = $parent_window.offset().top;
  480. const window_height = $(el_window).height();
  481. const window_width = $(el_window).width();
  482. options.left = parent_window_left + parent_window_width/2 - window_width/2;
  483. options.top = parent_window_top + parent_window_height/2 - window_height/2;
  484. $(el_window).css({
  485. 'left': options.left + 'px',
  486. 'top': options.top + 'px',
  487. });
  488. }
  489. // onAppend() - using show() is a hack to make sure window is visible AND onAppend is called when
  490. // window is actually appended and usable.
  491. // NOTE: there is another is_visible condition below
  492. if ( options.is_visible ) {
  493. $(el_window).show(0, function(e){
  494. // if SaveFileDialog, bring focus to the el_savefiledialog_filename and select all
  495. if(options.is_saveFileDialog){
  496. let item_name = el_savefiledialog_filename.value;
  497. const extname = path.extname('/' + item_name);
  498. if(extname !== '')
  499. el_savefiledialog_filename.setSelectionRange(0, item_name.length - extname.length)
  500. else
  501. $(el_savefiledialog_filename).select();
  502. $(el_savefiledialog_filename).get(0).focus({preventScroll:true});
  503. }
  504. //set custom window css
  505. $(el_window).css(options.window_css);
  506. // onAppend()
  507. if(options.onAppend && typeof options.onAppend === 'function'){
  508. options.onAppend(el_window);
  509. }
  510. });
  511. }
  512. if(options.is_saveFileDialog){
  513. //------------------------------------------------
  514. // SaveFileDialog > Save button
  515. //------------------------------------------------
  516. $(el_savefiledialog_save_btn).on('click', function(e){
  517. const filename = $(el_savefiledialog_filename).val();
  518. try{
  519. validate_fsentry_name(filename)
  520. }catch(err){
  521. UIAlert(err.message, 'error', 'OK')
  522. return;
  523. }
  524. const target_path = path.join($(el_window).attr('data-path'), filename);
  525. if(options.onSaveFileDialogSave && typeof options.onSaveFileDialogSave === 'function')
  526. options.onSaveFileDialogSave(target_path, el_window)
  527. })
  528. //------------------------------------------------
  529. // SaveFileDialog > Enter
  530. //------------------------------------------------
  531. $(el_savefiledialog_filename).on('keypress', function(event) {
  532. if(event.which === 13){
  533. $(el_savefiledialog_save_btn).trigger('click');
  534. }
  535. })
  536. //------------------------------------------------
  537. // Enable/disable Save button based on input
  538. //------------------------------------------------
  539. $(el_savefiledialog_filename).bind('keydown change input paste', function(){
  540. if($(this).val() !== '')
  541. $(el_savefiledialog_save_btn).removeClass('disabled');
  542. else
  543. $(el_savefiledialog_save_btn).addClass('disabled');
  544. })
  545. $(el_savefiledialog_filename).get(0).focus({preventScroll:true});
  546. }
  547. if(options.is_openFileDialog){
  548. //------------------------------------------------
  549. // OpenFileDialog > Open button
  550. //------------------------------------------------
  551. $(el_openfiledialog_open_btn).on('click', async function(e){
  552. const selected_els = $(el_window).find('.item-selected[data-is_dir="0"]');
  553. let selected_files;
  554. // No item selected
  555. if(selected_els.length === 0)
  556. return;
  557. // ------------------------------------------------
  558. // Item(s) selected
  559. // ------------------------------------------------
  560. else{
  561. selected_files = []
  562. // an array that hold the items to sign
  563. const items_to_sign = [];
  564. // prepare items to sign
  565. for(let i=0; i<selected_els.length; i++)
  566. items_to_sign.push({uid: $(selected_els[i]).attr('data-uid'), action: 'write', path: $(selected_els[i]).attr('data-path')});
  567. // sign items
  568. selected_files = await puter.fs.sign(options.initiating_app_uuid, items_to_sign);
  569. selected_files = selected_files.items;
  570. selected_files = Array.isArray(selected_files) ? selected_files : [selected_files];
  571. // change path of each item to preserve privacy
  572. for(let i=0; i<selected_files.length; i++)
  573. selected_files[i].path = `~/` + selected_files[i].path.split('/').slice(2).join('/');
  574. }
  575. const ifram_msg_uid = $(el_window).attr('data-iframe_msg_uid');
  576. if(options.return_to_parent_window){
  577. window.opener.postMessage({
  578. msg: "fileOpenPicked",
  579. original_msg_id: ifram_msg_uid,
  580. items: Array.isArray(selected_files) ? [...selected_files] : [selected_files],
  581. // LEGACY SUPPORT, remove this in the future when Polotno uses the new SDK
  582. // this is literally put in here to support Polotno's legacy code
  583. ...(selected_files.length === 1 && selected_files[0])
  584. }, '*');
  585. window.close();
  586. window.open('','_self').close();
  587. }
  588. else if(options.parent_uuid){
  589. // send event to iframe
  590. const target_iframe = $(`.window[data-element_uuid="${options.parent_uuid}"]`).find('.window-app-iframe').get(0);
  591. if(target_iframe){
  592. target_iframe.contentWindow.postMessage({
  593. msg: "fileOpenPicked",
  594. original_msg_id: ifram_msg_uid,
  595. items: Array.isArray(selected_files) ? [...selected_files] : [selected_files],
  596. // LEGACY SUPPORT, remove this in the future when Polotno uses the new SDK
  597. // this is literally put in here to support Polotno's legacy code
  598. ...(selected_files.length === 1 && selected_files[0])
  599. }, '*');
  600. }
  601. // focus on iframe
  602. $(target_iframe).get(0)?.focus({preventScroll:true});
  603. // send file_opened event
  604. const file_opened_event = new CustomEvent('file_opened', {detail: Array.isArray(selected_files) ? [...selected_files] : [selected_files]});
  605. // dispatch event to parent window
  606. $(`.window[data-element_uuid="${options.parent_uuid}"]`).get(0)?.dispatchEvent(file_opened_event);
  607. $(el_window).close();
  608. }
  609. })
  610. }
  611. else if(options.is_directoryPicker){
  612. //------------------------------------------------
  613. // DirectoryPicker > Select button
  614. //------------------------------------------------
  615. $(el_directorypicker_select_btn).on('click', async function(e){
  616. const selected_els = $(el_window).find('.item-selected[data-is_dir="1"]');
  617. let selected_dirs;
  618. // ------------------------------------------------
  619. // No item selected, return current directory
  620. // ------------------------------------------------
  621. if(selected_els.length === 0){
  622. selected_dirs = await puter.fs.sign(options.initiating_app_uuid, {uid: $(el_window).attr('data-uid'), action: 'write'})
  623. selected_dirs = selected_dirs.items;
  624. }
  625. // ------------------------------------------------
  626. // directorie(s) selected
  627. // ------------------------------------------------
  628. else{
  629. selected_dirs = []
  630. // an array that hold the items to sign
  631. const items_to_sign = [];
  632. // prepare items to sign
  633. for(let i=0; i<selected_els.length; i++)
  634. items_to_sign.push({uid: $(selected_els[i]).attr('data-uid'), action: 'write', path: $(selected_els[i]).attr('data-path')});
  635. // sign items
  636. selected_dirs = await puter.fs.sign(options.initiating_app_uuid, items_to_sign);
  637. selected_dirs = selected_dirs.items;
  638. selected_dirs = Array.isArray(selected_dirs) ? selected_dirs : [selected_dirs];
  639. // change path of each item to preserve privacy
  640. for(let i=0; i<selected_dirs.length; i++)
  641. selected_dirs[i].path = `~/` + selected_dirs[i].path.split('/').slice(2).join('/');
  642. }
  643. const ifram_msg_uid = $(el_window).attr('data-iframe_msg_uid');
  644. if(options.return_to_parent_window){
  645. window.opener.postMessage({
  646. msg: "directoryPicked",
  647. original_msg_id: ifram_msg_uid,
  648. items: Array.isArray(selected_dirs) ? [...selected_dirs] : [selected_dirs],
  649. // LEGACY SUPPORT, remove this in the future when Polotno uses the new SDK
  650. // this is literally put in here to support Polotno's legacy code
  651. ...(selected_dirs.length === 1 && selected_dirs[0])
  652. }, '*');
  653. window.close();
  654. window.open('','_self').close();
  655. }
  656. if(options.parent_uuid){
  657. // Send directoryPicked event to iframe
  658. const target_iframe = $(`.window[data-element_uuid="${options.parent_uuid}"]`).find('.window-app-iframe').get(0);
  659. if(target_iframe){
  660. target_iframe.contentWindow.postMessage({
  661. msg: "directoryPicked",
  662. original_msg_id: ifram_msg_uid,
  663. items: Array.isArray(selected_dirs) ? [...selected_dirs] : [selected_dirs],
  664. }, '*');
  665. }
  666. $(target_iframe).get(0).focus({preventScroll:true});
  667. $(el_window).close();
  668. }
  669. })
  670. }
  671. if(options.is_saveFileDialog || options.is_openFileDialog || options.is_directoryPicker){
  672. //------------------------------------------------
  673. // FileDialog > Cancel button
  674. //------------------------------------------------
  675. $(el_filedialog_cancel_btn).on('click', function(e){
  676. if(options.return_to_parent_window){
  677. window.close();
  678. window.open('','_self').close();
  679. }
  680. $(el_window).hide(0, ()=>{
  681. // re-anable parent window
  682. $(`.window[data-element_uuid="${options.parent_uuid}"]`).removeClass('window-disabled');
  683. $(`.window[data-element_uuid="${options.parent_uuid}"]`).find('.window-disable-mask').hide();
  684. $(el_window).close();
  685. })
  686. })
  687. }
  688. if(options.is_dir){
  689. navbar_path_droppable(el_window);
  690. sidebar_item_droppable(el_window);
  691. // --------------------------------------------------------
  692. // Back button
  693. // --------------------------------------------------------
  694. $(el_window_navbar_back_btn).on('click', function(e){
  695. // if history menu is open don't continue
  696. if($(el_window_navbar_back_btn).hasClass('has-open-contextmenu'))
  697. return;
  698. // if ctrl/cmd are pressed, open in new window
  699. if(e.ctrlKey || e.metaKey){
  700. const dirpath = window_nav_history[win_id].at(window_nav_history_current_position[win_id] - 1);
  701. UIWindow({
  702. path: dirpath,
  703. title: dirpath === '/' ? root_dirname : path.basename(dirpath),
  704. icon: window.icons['folder.svg'],
  705. // uid: $(el_item).attr('data-uid'),
  706. is_dir: true,
  707. });
  708. }
  709. // ... otherwise, open in same window
  710. else{
  711. window_nav_history_current_position[win_id] > 0 && window_nav_history_current_position[win_id]--;
  712. const new_path = window_nav_history[win_id].at(window_nav_history_current_position[win_id]);
  713. // update window path
  714. update_window_path(el_window, new_path);
  715. }
  716. })
  717. // --------------------------------------------------------
  718. // Back button click-hold
  719. // --------------------------------------------------------
  720. $(el_window_navbar_back_btn).on('taphold', function() {
  721. let items = [];
  722. const pos = el_window_navbar_back_btn.getBoundingClientRect();
  723. for(let index = window_nav_history_current_position[win_id] - 1; index >= 0; index--){
  724. const history_item = window_nav_history[win_id].at(index);
  725. // build item for context menu
  726. items.push({
  727. html: `<span>${history_item === window.home_path ? 'Home' : path.basename(history_item)}</span>`,
  728. val: index,
  729. onClick: async function(e){
  730. let history_index = e.value;
  731. window_nav_history_current_position[win_id] = history_index;
  732. const new_path = window_nav_history[win_id].at(window_nav_history_current_position[win_id]);
  733. // if ctrl/cmd are pressed, open in new window
  734. if(e.ctrlKey || e.metaKey && (new_path !== undefined && new_path !== null)){
  735. UIWindow({
  736. path: new_path,
  737. title: new_path === '/' ? root_dirname : path.basename(new_path),
  738. icon: window.icons['folder.svg'],
  739. is_dir: true,
  740. });
  741. }
  742. // update window path
  743. else{
  744. update_window_path(el_window, new_path);
  745. }
  746. }
  747. })
  748. }
  749. // Menu
  750. UIContextMenu({
  751. position: {top: pos.top + pos.height + 3, left: pos.left},
  752. parent_element: el_window_navbar_back_btn,
  753. items: items,
  754. })
  755. })
  756. // --------------------------------------------------------
  757. // Forward button
  758. // --------------------------------------------------------
  759. $(el_window_navbar_forward_btn).on('click', function(e){
  760. // if history menu is open don't continue
  761. if($(el_window_navbar_forward_btn).hasClass('has-open-contextmenu'))
  762. return;
  763. // if ctrl/cmd are pressed, open in new window
  764. if(e.ctrlKey || e.metaKey){
  765. const dirpath = window_nav_history[win_id].at(window_nav_history_current_position[win_id] + 1);
  766. UIWindow({
  767. path: dirpath,
  768. title: dirpath === '/' ? root_dirname : path.basename(dirpath),
  769. icon: window.icons['folder.svg'],
  770. // uid: $(el_item).attr('data-uid'),
  771. is_dir: true,
  772. });
  773. }
  774. // ... otherwise, open in same window
  775. else{
  776. window_nav_history_current_position[win_id]++;
  777. // get last path in history
  778. const target_path = window_nav_history[win_id].at(window_nav_history_current_position[win_id]);
  779. // update window path
  780. if(target_path !== undefined){
  781. update_window_path(el_window, target_path);
  782. }
  783. }
  784. })
  785. // --------------------------------------------------------
  786. // forward button click-hold
  787. // --------------------------------------------------------
  788. $(el_window_navbar_forward_btn).on('taphold', function() {
  789. let items = [];
  790. const pos = el_window_navbar_forward_btn.getBoundingClientRect();
  791. for(let index = window_nav_history_current_position[win_id] + 1; index < window_nav_history[win_id].length; index++){
  792. const history_item = window_nav_history[win_id].at(index);
  793. // build item for context menu
  794. items.push({
  795. html: `<span>${history_item === window.home_path ? 'Home' : path.basename(history_item)}</span>`,
  796. val: index,
  797. onClick: async function(e){
  798. let history_index = e.value;
  799. window_nav_history_current_position[win_id] = history_index;
  800. const new_path = window_nav_history[win_id].at(window_nav_history_current_position[win_id]);
  801. // if ctrl/cmd are pressed, open in new window
  802. if(e.ctrlKey || e.metaKey && (new_path !== undefined && new_path !== null)){
  803. UIWindow({
  804. path: new_path,
  805. title: new_path === '/' ? root_dirname : path.basename(new_path),
  806. icon: window.icons['folder.svg'],
  807. is_dir: true,
  808. });
  809. }
  810. // update window path
  811. else{
  812. update_window_path(el_window, new_path);
  813. }
  814. }
  815. })
  816. }
  817. // Menu
  818. UIContextMenu({
  819. parent_element: el_window_navbar_forward_btn,
  820. position: {top: pos.top + pos.height + 3, left: pos.left},
  821. items: items,
  822. })
  823. })
  824. // --------------------------------------------------------
  825. // Up button
  826. // --------------------------------------------------------
  827. $(el_window_navbar_up_btn).on('click', function(e){
  828. const target_path = path.resolve(path.join($(el_window).attr('data-path'), '..'));
  829. // if ctrl/cmd are pressed, open in new window
  830. if(e.ctrlKey || e.metaKey && (target_path !== undefined && target_path !== null)){
  831. UIWindow({
  832. path: target_path,
  833. title: target_path === '/' ? root_dirname : path.basename(target_path),
  834. icon: window.icons['folder.svg'],
  835. // uid: $(el_item).attr('data-uid'),
  836. is_dir: true,
  837. });
  838. }
  839. // ... otherwise, open in same window
  840. else if(target_path !== undefined && target_path !== null){
  841. // update history
  842. window_nav_history[win_id] = window_nav_history[win_id].slice(0, window_nav_history_current_position[win_id]+1);
  843. window_nav_history[win_id].push(target_path);
  844. window_nav_history_current_position[win_id]++;
  845. // update window path
  846. update_window_path(el_window, target_path);
  847. }
  848. })
  849. const layouts = ['icons', 'list', 'details'];
  850. $(el_window).find('.window-navbar-layout-settings').on('contextmenu taphold', function() {
  851. let cur_layout = $(el_window).attr('data-layout');
  852. let items = [];
  853. for(let i=0; i<layouts.length; i++){
  854. items.push({
  855. html: `<span style="text-transform: capitalize;">${layouts[i]}</span>`,
  856. icon: cur_layout === layouts[i] ? '✓' : '',
  857. onClick: async function(e){
  858. update_window_layout(el_window, layouts[i]);
  859. window.set_layout($(el_window).attr('data-uid'), layouts[i]);
  860. }
  861. })
  862. }
  863. UIContextMenu({
  864. parent_element: this,
  865. items: items,
  866. })
  867. })
  868. $(el_window).find('.window-navbar-layout-settings').on('click', function() {
  869. let cur_layout = $(el_window).attr('data-layout');
  870. for(let i=0; i<layouts.length; i++){
  871. if(cur_layout === layouts[i]){
  872. if(i === layouts.length - 1){
  873. update_window_layout(el_window, layouts[0]);
  874. window.set_layout($(el_window).attr('data-uid'), layouts[0]);
  875. }else{
  876. update_window_layout(el_window, layouts[i+1]);
  877. window.set_layout($(el_window).attr('data-uid'), layouts[i+1]);
  878. }
  879. break;
  880. }
  881. }
  882. })
  883. // --------------------------------------------------------
  884. // directory content
  885. // --------------------------------------------------------
  886. //auth
  887. if(!is_auth() && !(await UIWindowLogin()))
  888. return;
  889. // get directory content
  890. refresh_item_container(el_window_body, options);
  891. }
  892. // set iframe url
  893. if (options.iframe_url){
  894. $(el_window_app_iframe).attr('src', options.iframe_url)
  895. //bring focus to iframe
  896. el_window_app_iframe.contentWindow.focus();
  897. }
  898. // set the position of window
  899. if(!options.is_maximized){
  900. $(el_window).css('top', options.top)
  901. $(el_window).css('left', options.left)
  902. }
  903. if ( options.is_visible ) {
  904. $(el_window).css('display', 'block');
  905. }
  906. // mousedown on the window body will unselect selected items if neither ctrl nor command are pressed
  907. $(el_window_body).on('mousedown', function(e){
  908. if($(e.target).hasClass('window-body') && !e.ctrlKey && !e.metaKey){
  909. $(el_window_body).find('.item-selected').removeClass('item-selected');
  910. update_explorer_footer_selected_items_count(el_window);
  911. // if this is openFileDialog, disable the Open button
  912. if(options.is_openFileDialog)
  913. $(el_openfiledialog_open_btn).addClass('disabled')
  914. }
  915. })
  916. // on_close event
  917. $(el_window).on('remove', function(e){
  918. // if on_close callback is set, call it
  919. options.on_close?.();
  920. })
  921. // --------------------------------------------------------
  922. // Backdrop click
  923. // --------------------------------------------------------
  924. if(options.backdrop && options.close_on_backdrop_click){
  925. $(el_window).closest('.window-backdrop').on('mousedown', function(e){
  926. if($(e.target).hasClass('window-backdrop')){
  927. $(el_window).close();
  928. }
  929. })
  930. }
  931. // --------------------------------------------------------
  932. // Selectable
  933. // only for Desktop screens
  934. // --------------------------------------------------------
  935. if(options.is_dir && options.selectable_body && !isMobile.phone && !isMobile.tablet){
  936. let selected_ctrl_items = [];
  937. // init viselect
  938. const selection = new SelectionArea({
  939. selectionContainerClass: '.selection-area-container',
  940. container: `#window-body-${win_id}`,
  941. selectables: [`#window-body-${win_id} .item`],
  942. startareas: [`#window-body-${win_id}`],
  943. boundaries: [`#window-body-${win_id}`],
  944. behaviour: {
  945. overlap: 'drop',
  946. intersect: 'touch',
  947. startThreshold: 10,
  948. scrolling: {
  949. speedDivider: 10,
  950. manualSpeed: 750,
  951. startScrollMargins: {x: 0, y: 0}
  952. }
  953. },
  954. features: {
  955. touch: true,
  956. range: true,
  957. singleTap: {
  958. allow: true,
  959. intersect: 'native'
  960. }
  961. }
  962. });
  963. selection.on('beforestart', ({store, event}) => {
  964. selected_ctrl_items = [];
  965. return $(event.target).is(`#window-body-${win_id}`)
  966. })
  967. .on('beforedrag', evt => {
  968. })
  969. .on('start', ({store, event}) => {
  970. if (!event.ctrlKey && !event.metaKey) {
  971. for (const el of store.stored) {
  972. el.classList.remove('item-selected');
  973. }
  974. selection.clearSelection();
  975. }
  976. })
  977. .on('move', ({store: {changed: {added, removed}}, event}) => {
  978. for (const el of added) {
  979. // if ctrl or meta key is pressed and the item is already selected, then unselect it
  980. if((event.ctrlKey || event.metaKey) && $(el).hasClass('item-selected')){
  981. el.classList.remove('item-selected');
  982. selected_ctrl_items.push(el);
  983. }
  984. // otherwise select it
  985. else{
  986. el.classList.add('item-selected');
  987. // the latest selected item is the active element
  988. active_element = el;
  989. }
  990. }
  991. for (const el of removed) {
  992. el.classList.remove('item-selected');
  993. // in case this item was selected by ctrl+click before, then reselect it again
  994. if(selected_ctrl_items.includes(el))
  995. $(el).addClass('item-selected');
  996. }
  997. update_explorer_footer_selected_items_count(el_window);
  998. // If this is openFileDialog, enable/disable the Open button accordingly
  999. if(options.is_openFileDialog && $(el_window).find('.item-selected').length)
  1000. $(el_openfiledialog_open_btn).removeClass('disabled')
  1001. else
  1002. $(el_openfiledialog_open_btn).addClass('disabled')
  1003. })
  1004. .on('stop', ({store, event}) => {
  1005. // If this is openFileDialog, enable/disable the Open button accordingly
  1006. if(options.is_openFileDialog && $(el_window).find('.item-selected').length)
  1007. $(el_openfiledialog_open_btn).removeClass('disabled')
  1008. else
  1009. $(el_openfiledialog_open_btn).addClass('disabled')
  1010. });
  1011. }
  1012. // --------------------------------------------------------
  1013. // Droppable
  1014. // --------------------------------------------------------
  1015. $(el_window_body).droppable({
  1016. accept: '.item',
  1017. greedy: true,
  1018. tolerance: "pointer",
  1019. drop: async function( e, ui ) {
  1020. // check if item was actually dropped on this window
  1021. if($(mouseover_window).attr('data-id') !== $(el_window).attr('data-id'))
  1022. return;
  1023. // can't drop anything here but a UIItem
  1024. if(!$(ui.draggable).hasClass('item'))
  1025. return;
  1026. // --------------------------------------------------
  1027. // In case this was dropped on an App window
  1028. // --------------------------------------------------
  1029. if(el_window_app_iframe !== null){
  1030. const items_to_move = []
  1031. // first item
  1032. items_to_move.push(ui.draggable);
  1033. // all subsequent items
  1034. const cloned_items = document.getElementsByClassName('item-selected-clone');
  1035. for(let i =0; i<cloned_items.length; i++){
  1036. const source_item = document.getElementById('item-' + $(cloned_items[i]).attr('data-id'));
  1037. if(source_item !== null)
  1038. items_to_move.push(source_item);
  1039. }
  1040. // sign all items
  1041. const items_to_sign = []
  1042. // prepare items to sign
  1043. for(let i=0; i<items_to_move.length; i++)
  1044. items_to_sign.push({uid: $(items_to_move[i]).attr('data-uid'), action: 'write', path: $(items_to_move[i]).attr('data-path')});
  1045. // sign items
  1046. let signatures = await puter.fs.sign(options.app_uuid, items_to_sign);
  1047. signatures = signatures.items;
  1048. signatures = Array.isArray(signatures) ? signatures : [signatures];
  1049. // prepare items
  1050. let items = [];
  1051. for (let index = 0; index < signatures.length; index++) {
  1052. const item = signatures[index];
  1053. items.push({
  1054. name: item.fsentry_name,
  1055. readURL: item.read_url,
  1056. writeURL: item.write_url,
  1057. metadataURL: item.metadata_url,
  1058. isDirectory: item.fsentry_is_dir,
  1059. path: `~/` + item.path.split('/').slice(2).join('/'),
  1060. uid: item.uid,
  1061. })
  1062. }
  1063. // send to app iframe
  1064. el_window_app_iframe.contentWindow.postMessage({
  1065. msg: "itemsOpened",
  1066. original_msg_id: $(el_window).attr('data-iframe_msg_uid'),
  1067. items: items,
  1068. }, '*');
  1069. // if item is dragged over an app iframe, highlight the iframe
  1070. var rect = el_window_app_iframe.getBoundingClientRect();
  1071. // if mouse is inside iframe, send drag message to iframe
  1072. el_window_app_iframe.contentWindow.postMessage({msg: "drop", x: (mouseX - rect.left), y: (mouseY - rect.top), items: items}, '*');
  1073. // bring focus to this window
  1074. $(el_window).focusWindow();
  1075. }
  1076. // if this window is not a directory, cancel drop.
  1077. // why not simply only launch droppable on directories? this is because
  1078. // if a window is not droppable and an item is dropped on it, the app will think
  1079. // it was dropped on desktop.
  1080. if(!options.is_dir){
  1081. return false;
  1082. }
  1083. // If dropped on the same window, do not proceed
  1084. if($(ui.draggable).closest('.item-container').attr('data-path') === $(mouseover_window).attr('data-path') && !e.ctrlKey){
  1085. return;
  1086. }
  1087. // If ctrl is pressed and source is Trashed, cancel whole operation
  1088. if(e.ctrlKey && path.dirname($(ui.draggable).attr('data-path')) === window.trash_path)
  1089. return;
  1090. // Unselect already selected items
  1091. $(el_window_body).find('.item-selected').removeClass('item-selected')
  1092. const items_to_move = []
  1093. // first item
  1094. items_to_move.push(ui.draggable);
  1095. // all subsequent items
  1096. const cloned_items = document.getElementsByClassName('item-selected-clone');
  1097. for(let i =0; i<cloned_items.length; i++){
  1098. const source_item = document.getElementById('item-' + $(cloned_items[i]).attr('data-id'));
  1099. if(source_item !== null){
  1100. items_to_move.push(source_item);
  1101. }
  1102. }
  1103. // If ctrl key is down, copy items. Except if target is Trash
  1104. if(e.ctrlKey && $(mouseover_window).attr('data-path') !== window.trash_path){
  1105. // Copy items
  1106. copy_items(items_to_move, $(mouseover_window).attr('data-path'))
  1107. }
  1108. // if alt key is down, create shortcut items
  1109. else if(e.altKey){
  1110. items_to_move.forEach((item_to_move) => {
  1111. create_shortcut(
  1112. path.basename($(item_to_move).attr('data-path')),
  1113. $(item_to_move).attr('data-is_dir') === '1',
  1114. $(mouseover_window).attr('data-path'),
  1115. null,
  1116. $(item_to_move).attr('data-shortcut_to') === '' ? $(item_to_move).attr('data-uid') : $(item_to_move).attr('data-shortcut_to'),
  1117. $(item_to_move).attr('data-shortcut_to_path') === '' ? $(item_to_move).attr('data-path') : $(item_to_move).attr('data-shortcut_to_path'),
  1118. );
  1119. });
  1120. }
  1121. // otherwise, move items
  1122. else{
  1123. move_items(items_to_move, $(mouseover_window).attr('data-path'));
  1124. }
  1125. },
  1126. over: function(event, ui){
  1127. // Don't do anything if the dragged item is NOT a UIItem
  1128. if(!$(ui.draggable).hasClass('item'))
  1129. return;
  1130. },
  1131. out: function(event, ui){
  1132. // Don't do anything if the dragged item is NOT a UIItem
  1133. if(!$(ui.draggable).hasClass('item'))
  1134. return;
  1135. }
  1136. });
  1137. // --------------------------------------------------------
  1138. // Double Click on Head
  1139. // double click on a window head will maximize or shrink window
  1140. // only maximize/shrink if window is marked `is_resizable`
  1141. // --------------------------------------------------------
  1142. if(options.is_resizable){
  1143. $(el_window_head).dblclick(function () {
  1144. scale_window(el_window);
  1145. })
  1146. }
  1147. $(el_window_head).mousedown(function () {
  1148. if(window_is_snapped){
  1149. $( el_window ).draggable( "option", "cursorAt", { left: width_before_snap/2 } );
  1150. }
  1151. })
  1152. // --------------------------------------------------------
  1153. // Click On The `Scale` Button
  1154. // (the little rectangle in the window head)
  1155. // --------------------------------------------------------
  1156. if(options.is_resizable){
  1157. $(el_window_head_scale_btn).click(function () {
  1158. scale_window(el_window);
  1159. })
  1160. }
  1161. // --------------------------------------------------------
  1162. // Dragster
  1163. // If a local item is dragged over this window, bring it to front
  1164. // --------------------------------------------------------
  1165. let drag_enter_timeout;
  1166. $(el_window).dragster({
  1167. enter: function (dragsterEvent, event) {
  1168. // make sure to cancel any previous timeouts otherwise the window will be brought to front multiple times
  1169. clearTimeout(drag_enter_timeout);
  1170. // If items are dragged over this window long enough, bring it to front
  1171. drag_enter_timeout = setTimeout(function(){
  1172. // focus window
  1173. $(el_window).focusWindow();
  1174. }, 1400);
  1175. },
  1176. leave: function (dragsterEvent, event) {
  1177. // cancel the timeout for 'bringing window to front'
  1178. clearTimeout(drag_enter_timeout);
  1179. },
  1180. drop: function (dragsterEvent, event) {
  1181. // cancel the timeout for 'bringing window to front'
  1182. clearTimeout(drag_enter_timeout);
  1183. },
  1184. over: function (dragsterEvent, event) {
  1185. // cancel the timeout for 'bringing window to front'
  1186. clearTimeout(drag_enter_timeout);
  1187. }
  1188. });
  1189. // --------------------------------------------------------
  1190. // Dragster
  1191. // Allow dragging of local files onto this window, if it's is_dir
  1192. // --------------------------------------------------------
  1193. $(el_window_body).dragster({
  1194. enter: function (dragsterEvent, event) {
  1195. if(options.is_dir){
  1196. // remove any context menu that might be open
  1197. $('.context-menu').remove();
  1198. // highlight this item container
  1199. $(el_window).find('.item-container').addClass('item-container-active');
  1200. }
  1201. },
  1202. leave: function (dragsterEvent, event) {
  1203. if(options.is_dir){
  1204. $(el_window).find('.item-container').removeClass('item-container-active');
  1205. }
  1206. },
  1207. drop: function (dragsterEvent, event) {
  1208. const e = event.originalEvent;
  1209. if(options.is_dir){
  1210. // if files were dropped...
  1211. if(e.dataTransfer?.items?.length>0){
  1212. upload_items(e.dataTransfer.items, $(el_window).attr('data-path'))
  1213. }
  1214. // de-highlight all windows
  1215. $('.item-container').removeClass('item-container-active');
  1216. }
  1217. e.stopPropagation();
  1218. e.preventDefault();
  1219. return false;
  1220. }
  1221. });
  1222. // --------------------------------------------------------
  1223. // Close button
  1224. // --------------------------------------------------------
  1225. $(`#window-${win_id} > .window-head > .window-close-btn`).click(function () {
  1226. $(el_window).close({
  1227. shrink_to_target: options.on_close_shrink_to_target
  1228. });
  1229. })
  1230. // --------------------------------------------------------
  1231. // Minimize button
  1232. // --------------------------------------------------------
  1233. $(`#window-${win_id} > .window-head > .window-minimize-btn`).click(function () {
  1234. $(el_window).hideWindow();
  1235. })
  1236. // --------------------------------------------------------
  1237. // Draggable
  1238. // --------------------------------------------------------
  1239. let width_before_snap = 0;
  1240. let height_before_snap = 0;
  1241. let window_is_snapped = false;
  1242. let snap_placeholder_active = false;
  1243. let snap_trigger_timeout;
  1244. if(options.is_draggable){
  1245. let window_snap_placeholder = $(
  1246. `<div class="window-snap-placeholder animate__animated animate__zoomIn animate__faster">
  1247. <div class="window-snap-placeholder-inner"></div>
  1248. </div>`
  1249. );
  1250. $(el_window).draggable({
  1251. start: function(e, ui){
  1252. // if window is snapped, unsnap it and reset its position to where it was before snapping
  1253. if(options.is_resizable && window_is_snapped){
  1254. window_is_snapped = false;
  1255. $(el_window).css({
  1256. 'width': width_before_snap,
  1257. 'height': height_before_snap + 'px',
  1258. });
  1259. // if at any point the window's width is "too small", hide the sidebar
  1260. if($(el_window).width() < window_width_threshold_for_sidebar){
  1261. if(width_before_snap >= window_width_threshold_for_sidebar && !sidebar_hidden){
  1262. $(el_window_sidebar).hide();
  1263. }
  1264. sidebar_hidden = true;
  1265. }
  1266. // if at any point the window's width is "big enough", show the sidebar
  1267. else if($(el_window).width() >= window_width_threshold_for_sidebar){
  1268. if(sidebar_hidden){
  1269. $(el_window_sidebar).show();
  1270. }
  1271. sidebar_hidden = false;
  1272. }
  1273. }
  1274. $(el_window).addClass('window-dragging');
  1275. // rm window from original_window_position
  1276. window.original_window_position[$(el_window).attr('id')] = undefined;
  1277. // since jquery draggable sets the z-index automatically we need this to
  1278. // bring windows to the front when they are clicked.
  1279. last_window_zindex = parseInt($(el_window).css('z-index'));
  1280. //transform causes draggable to start inaccurately
  1281. $(el_window).css('transform', 'none');
  1282. },
  1283. drag: function ( e, ui ) {
  1284. $(el_window_app_iframe).css('pointer-events', 'none');
  1285. $('.window').css('pointer-events', 'none');
  1286. // jqueryui changes the z-index automatically, if the stay_on_top flag is set
  1287. // make sure window stays on top
  1288. $(`.window[data-stay_on_top="true"]`).css('z-index', 999999999)
  1289. if($(el_window).attr('data-is_maximized') === '1'){
  1290. $(el_window).attr('data-is_maximized', '0');
  1291. // maximize icon
  1292. $(el_window_head_scale_btn).find('img').attr('src', window.icons['scale.svg']);
  1293. }
  1294. // --------------------------------------------------------
  1295. // Snap to screen edges
  1296. // --------------------------------------------------------
  1297. if(options.is_resizable){
  1298. clearTimeout(snap_trigger_timeout);
  1299. // if window is not snapped, check if it should be snapped
  1300. snap_trigger_timeout = setTimeout(function(){
  1301. // if cursor is not in a snap zone, don't snap
  1302. if(!current_active_snap_zone){
  1303. return;
  1304. }
  1305. // if dragging has stopped by now, don't snap
  1306. if(!$(el_window).hasClass('window-dragging')){
  1307. return;
  1308. }
  1309. // W
  1310. if(!window_is_snapped && current_active_snap_zone === 'w'){
  1311. window_snap_placeholder.css({
  1312. 'display': 'block',
  1313. 'width': '50%',
  1314. 'height': desktop_height,
  1315. 'top': toolbar_height,
  1316. 'left': 0,
  1317. 'z-index': last_window_zindex - 1,
  1318. })
  1319. }
  1320. // NW
  1321. else if(!window_is_snapped && current_active_snap_zone === 'nw'){
  1322. window_snap_placeholder.css({
  1323. 'display': 'block',
  1324. 'width': '50%',
  1325. 'height': desktop_height/2,
  1326. 'top': toolbar_height,
  1327. 'left': 0,
  1328. 'z-index': last_window_zindex - 1,
  1329. })
  1330. }
  1331. // NE
  1332. else if(!window_is_snapped && current_active_snap_zone ==='ne'){
  1333. window_snap_placeholder.css({
  1334. 'display': 'block',
  1335. 'width': '50%',
  1336. 'height': desktop_height/2,
  1337. 'top': toolbar_height,
  1338. 'left': desktop_width/2,
  1339. 'z-index': last_window_zindex - 1,
  1340. })
  1341. }
  1342. // E
  1343. else if(!window_is_snapped && current_active_snap_zone ==='e'){
  1344. window_snap_placeholder.css({
  1345. 'display': 'block',
  1346. 'width': '50%',
  1347. 'height': desktop_height,
  1348. 'top': toolbar_height,
  1349. 'left': 'initial',
  1350. 'right': 0,
  1351. 'z-index': last_window_zindex - 1,
  1352. })
  1353. }
  1354. // N
  1355. else if(!window_is_snapped && current_active_snap_zone ==='n'){
  1356. window_snap_placeholder.css({
  1357. 'display': 'block',
  1358. 'width': desktop_width,
  1359. 'height': desktop_height,
  1360. 'top': toolbar_height,
  1361. 'left': 0,
  1362. 'z-index': last_window_zindex - 1,
  1363. })
  1364. }
  1365. // SW
  1366. else if(!window_is_snapped && current_active_snap_zone ==='sw'){
  1367. window_snap_placeholder.css({
  1368. 'display': 'block',
  1369. 'top': toolbar_height + desktop_height/2,
  1370. 'left': 0,
  1371. 'width': '50%',
  1372. 'height': desktop_height/2,
  1373. 'z-index': last_window_zindex - 1,
  1374. })
  1375. }
  1376. // SE
  1377. else if(!window_is_snapped && current_active_snap_zone ==='se'){
  1378. window_snap_placeholder.css({
  1379. 'display': 'block',
  1380. 'top': toolbar_height + desktop_height/2,
  1381. 'left': desktop_width/2,
  1382. 'width': '50%',
  1383. 'height': desktop_height/2,
  1384. 'z-index': last_window_zindex - 1,
  1385. })
  1386. }
  1387. // If snap placeholder is not active, append it and make it active
  1388. if(!window_is_snapped && !snap_placeholder_active){
  1389. snap_placeholder_active = true;
  1390. $(el_body).append(window_snap_placeholder);
  1391. }
  1392. // save window size before snap
  1393. width_before_snap = $(el_window).width();
  1394. height_before_snap = $(el_window).height();
  1395. }, 500);
  1396. // if mouse is not in a snap zone, hide snap placeholder
  1397. if(snap_placeholder_active && !current_active_snap_zone){
  1398. snap_placeholder_active = false;
  1399. window_snap_placeholder.fadeOut(80);
  1400. }
  1401. }
  1402. },
  1403. stop: function () {
  1404. let window_will_snap = false;
  1405. $( el_window ).draggable( "option", "cursorAt", false );
  1406. $(el_window).removeClass('window-dragging');
  1407. $(el_window).attr({
  1408. 'data-orig-top': $(el_window).position().top,
  1409. 'data-orig-left': $(el_window).position().left,
  1410. })
  1411. $(el_window_app_iframe).css('pointer-events', 'all');
  1412. $('.window').css('pointer-events', 'initial');
  1413. // jqueryui changes the z-index automatically, if the stay_on_top flag is set
  1414. // make sure window stays on top with the initial zindex though
  1415. $(`.window[data-stay_on_top="true"]`).each(function(){
  1416. $(this).css('z-index', $(this).attr('data-initial_zindex'))
  1417. })
  1418. if(options.is_resizable && snap_placeholder_active && !window_is_snapped){
  1419. window_will_snap = true;
  1420. $(window_snap_placeholder).css('padding', 0);
  1421. setTimeout(function(){
  1422. // snap to w
  1423. if(current_active_snap_zone === 'w'){
  1424. $(el_window).css({
  1425. 'top': toolbar_height,
  1426. 'left': 0,
  1427. 'width': '50%',
  1428. 'height': desktop_height,
  1429. })
  1430. }
  1431. // snap to nw
  1432. else if(current_active_snap_zone === 'nw'){
  1433. $(el_window).css({
  1434. 'top': toolbar_height,
  1435. 'left': 0,
  1436. 'width': '50%',
  1437. 'height': desktop_height/2,
  1438. })
  1439. }
  1440. // snap to ne
  1441. else if(current_active_snap_zone === 'ne'){
  1442. $(el_window).css({
  1443. 'top': toolbar_height,
  1444. 'left': '50%',
  1445. 'width': '50%',
  1446. 'height': desktop_height/2,
  1447. })
  1448. }
  1449. // snap to sw
  1450. else if(current_active_snap_zone === 'sw'){
  1451. $(el_window).css({
  1452. 'top': toolbar_height + desktop_height/2,
  1453. 'left': 0,
  1454. 'width': '50%',
  1455. 'height': desktop_height/2,
  1456. })
  1457. }
  1458. // snap to se
  1459. else if(current_active_snap_zone === 'se'){
  1460. $(el_window).css({
  1461. 'top': toolbar_height + desktop_height/2,
  1462. 'left': desktop_width/2,
  1463. 'width': '50%',
  1464. 'height': desktop_height/2,
  1465. })
  1466. }
  1467. // snap to e
  1468. else if(current_active_snap_zone === 'e'){
  1469. $(el_window).css({
  1470. 'top': toolbar_height,
  1471. 'left': '50%',
  1472. 'width': '50%',
  1473. 'height': desktop_height,
  1474. })
  1475. }
  1476. // snap to n
  1477. else if(current_active_snap_zone === 'n'){
  1478. scale_window(el_window);
  1479. }
  1480. // snap placeholder is no longer active
  1481. snap_placeholder_active = false;
  1482. // hide snap placeholder
  1483. window_snap_placeholder.css('display', 'none');
  1484. window_snap_placeholder.css('padding', '10px');
  1485. // mark window as snapped
  1486. window_is_snapped = true;
  1487. // if at any point the window's width is "too small", hide the sidebar
  1488. if($(el_window).width() < window_width_threshold_for_sidebar){
  1489. if(width_before_snap >= window_width_threshold_for_sidebar && !sidebar_hidden){
  1490. $(el_window_sidebar).hide();
  1491. }
  1492. sidebar_hidden = true;
  1493. }
  1494. // if at any point the window's width is "big enough", show the sidebar
  1495. else if($(el_window).width() >= window_width_threshold_for_sidebar){
  1496. if(sidebar_hidden){
  1497. $(el_window_sidebar).show();
  1498. }
  1499. sidebar_hidden = false;
  1500. }
  1501. }, 100);
  1502. }
  1503. // if window is dropped below the taskbar, move it up
  1504. // the lst '- 30' is to account for the window head
  1505. if($(el_window).position().top > window.innerHeight - taskbar_height - 30 && !window_will_snap){
  1506. $(el_window).animate({
  1507. top: window.innerHeight - taskbar_height - 60,
  1508. }, 100);
  1509. }
  1510. // if window is dropped too far to the right, move it left
  1511. if($(el_window).position().left > window.innerWidth - 50 && !window_will_snap){
  1512. $(el_window).animate({
  1513. left: window.innerWidth - 50,
  1514. }, 100);
  1515. }
  1516. // if window is dropped too far to the left, move it right
  1517. if(($(el_window).position().left + $(el_window).width() - 150 )< 0 && !window_will_snap){
  1518. $(el_window).animate({
  1519. left: -1 * ($(el_window).width() - 150),
  1520. }, 100);
  1521. }
  1522. },
  1523. handle: `.window-head-draggable` + (options.draggable_body ? `, .window-body` : ``),
  1524. stack: `.window`,
  1525. scroll: false,
  1526. containment: '.window-container',
  1527. });
  1528. }
  1529. // --------------------------------------------------------
  1530. // Resizable
  1531. // --------------------------------------------------------
  1532. if(options.is_resizable){
  1533. if($(el_window).width() < window_width_threshold_for_sidebar){
  1534. $(el_window_sidebar).hide();
  1535. sidebar_hidden = true;
  1536. }
  1537. $(el_window).resizable({
  1538. handles: "n, ne, nw, e, s, se, sw, w",
  1539. minWidth: 200,
  1540. minHeight: 200,
  1541. start: function(){
  1542. $(el_window_app_iframe).css('pointer-events', 'none');
  1543. $('.window').css('pointer-events', 'none');
  1544. },
  1545. resize: function (e, ui) {
  1546. // if at any point the window's width is "too small", hide the sidebar
  1547. if(ui.size.width < window_width_threshold_for_sidebar){
  1548. if(ui.originalSize.width >= window_width_threshold_for_sidebar && !sidebar_hidden){
  1549. $(el_window_sidebar).hide();
  1550. }
  1551. sidebar_hidden = true;
  1552. }
  1553. // if at any point the window's width is "big enough", show the sidebar
  1554. else if(ui.size.width >= window_width_threshold_for_sidebar){
  1555. if(sidebar_hidden){
  1556. $(el_window_sidebar).show();
  1557. }
  1558. sidebar_hidden = false;
  1559. }
  1560. // when resizing the top of the window, make sure the window head is not hidden behind the toolbar
  1561. if($(el_window).position().top < toolbar_height){
  1562. var difference = toolbar_height - $(el_window).position().top;
  1563. $(el_window).css({
  1564. 'top': toolbar_height,
  1565. 'height': ui.size.height - difference // Reduce the height by the difference
  1566. });
  1567. // don't resize
  1568. return false;
  1569. }
  1570. },
  1571. stop: function () {
  1572. $(el_window_app_iframe).css('pointer-events', 'all');
  1573. $('.window').css('pointer-events', 'initial');
  1574. $(el_window_sidebar).resizable("option", "maxWidth", el_window.getBoundingClientRect().width/2);
  1575. $(el_window).attr({
  1576. 'data-orig-width': $(el_window).width(),
  1577. 'data-orig-height': $(el_window).height(),
  1578. })
  1579. // maximize icon
  1580. $(el_window_head_scale_btn).find('img').attr('src', window.icons['scale.svg']);
  1581. $(el_window).attr('data-is_maximized', '0');
  1582. },
  1583. containment: 'parent',
  1584. })
  1585. }
  1586. let side = $(el_window).find('.window-sidebar')
  1587. side.resizable({
  1588. handles: "e,w",
  1589. minWidth: 100,
  1590. maxWidth: el_window.getBoundingClientRect().width/2,
  1591. start: function(){
  1592. $(el_window_app_iframe).css('pointer-events', 'none');
  1593. $('.window').css('pointer-events', 'none');
  1594. },
  1595. stop: function () {
  1596. $(el_window_app_iframe).css('pointer-events', 'all');
  1597. $('.window').css('pointer-events', 'initial');
  1598. const new_width = $(el_window_sidebar).width();
  1599. // save new width in the cloud, to user's settings
  1600. setItem({key: "window_sidebar_width", value: new_width});
  1601. // save new width locally, to window object
  1602. window.window_sidebar_width = new_width;
  1603. }
  1604. })
  1605. // --------------------------------------------------------
  1606. // Alt/Option + Shift + click on window head will open a prompt to enter iframe url
  1607. // --------------------------------------------------------
  1608. $(el_window_head).on('click', function(e){
  1609. if(e.altKey && e.shiftKey && el_window_app_iframe !== null){
  1610. let url = prompt("Enter URL", options.iframe_url);
  1611. if(url){
  1612. $(el_window_app_iframe).attr('src', url);
  1613. }
  1614. }
  1615. })
  1616. // --------------------------------------------------------
  1617. // Head Context Menu
  1618. // --------------------------------------------------------
  1619. $(el_window_head).bind("contextmenu taphold", function (event) {
  1620. // dimiss taphold on regular devices
  1621. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  1622. return;
  1623. const $target = $(event.target);
  1624. // Cases in which native ctx menu should be preserved
  1625. if(options.allow_native_ctxmenu || $target.hasClass('allow-native-ctxmenu') || $target.is('input') || $target.is('textarea'))
  1626. return true;
  1627. // custom ctxmenu for all other elements
  1628. event.preventDefault();
  1629. // If window has no head, don't show ctxmenu
  1630. if(!options.has_head)
  1631. return;
  1632. let menu_items = [];
  1633. // -------------------------------------------
  1634. // Maximize/Minimize
  1635. // -------------------------------------------
  1636. if(options.is_resizable){
  1637. menu_items.push({
  1638. html: $(el_window).attr('data-is_maximized') === '0' ? 'Maximize' : 'Restore',
  1639. onClick: function(){
  1640. // maximize window
  1641. scale_window(el_window);
  1642. }
  1643. });
  1644. menu_items.push({
  1645. html: 'Minimize',
  1646. onClick: function(){
  1647. $(el_window).hideWindow();
  1648. }
  1649. });
  1650. // -
  1651. menu_items.push('-')
  1652. }
  1653. // -------------------------------------------
  1654. // Close
  1655. // -------------------------------------------
  1656. menu_items.push({
  1657. html: 'Close',
  1658. onClick: function(){
  1659. $(el_window).close();
  1660. }
  1661. });
  1662. UIContextMenu({
  1663. parent_element: el_window_head,
  1664. items: menu_items,
  1665. parent_id: win_id,
  1666. })
  1667. })
  1668. // --------------------------------------------------------
  1669. // Body Context Menu
  1670. // --------------------------------------------------------
  1671. $(el_window_body).bind("contextmenu taphold", function (event) {
  1672. // dimiss taphold on regular devices
  1673. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  1674. return;
  1675. const $target = $(event.target);
  1676. // Cases in which native ctx menu should be preserved
  1677. if(options.allow_native_ctxmenu || $target.hasClass('allow-native-ctxmenu') || $target.is('input') || $target.is('textarea'))
  1678. return true
  1679. // custom ctxmenu for all other elements
  1680. event.preventDefault();
  1681. if(options.allow_context_menu && event.target === el_window_body){
  1682. // Regular directories
  1683. if($(el_window).attr('data-path') !== trash_path){
  1684. UIContextMenu({
  1685. parent_element: el_window_body,
  1686. items: [
  1687. // -------------------------------------------
  1688. // Sort by
  1689. // -------------------------------------------
  1690. {
  1691. html: i18n('sort_by'),
  1692. items: [
  1693. {
  1694. html: i18n('name'),
  1695. icon: $(el_window).attr('data-sort_by') === 'name' ? '✓' : '',
  1696. onClick: async function(){
  1697. sort_items(el_window_body, 'name', $(el_window).attr('data-sort_order'));
  1698. set_sort_by($(el_window).attr('data-uid'), 'name', $(el_window).attr('data-sort_order'))
  1699. }
  1700. },
  1701. {
  1702. html: i18n('date_modified'),
  1703. icon: $(el_window).attr('data-sort_by') === 'modified' ? '✓' : '',
  1704. onClick: async function(){
  1705. sort_items(el_window_body, 'modified', $(el_window).attr('data-sort_order'));
  1706. set_sort_by($(el_window).attr('data-uid'), 'modified', $(el_window).attr('data-sort_order'))
  1707. }
  1708. },
  1709. {
  1710. html: i18n('type'),
  1711. icon: $(el_window).attr('data-sort_by') === 'type' ? '✓' : '',
  1712. onClick: async function(){
  1713. sort_items(el_window_body, 'type', $(el_window).attr('data-sort_order'));
  1714. set_sort_by($(el_window).attr('data-uid'), 'type', $(el_window).attr('data-sort_order'))
  1715. }
  1716. },
  1717. {
  1718. html: i18n('size'),
  1719. icon: $(el_window).attr('data-sort_by') === 'size' ? '✓' : '',
  1720. onClick: async function(){
  1721. sort_items(el_window_body, 'size', $(el_window).attr('data-sort_order'));
  1722. set_sort_by($(el_window).attr('data-uid'), 'size', $(el_window).attr('data-sort_order'))
  1723. }
  1724. },
  1725. // -------------------------------------------
  1726. // -
  1727. // -------------------------------------------
  1728. '-',
  1729. {
  1730. html: i18n('ascending'),
  1731. icon: $(el_window).attr('data-sort_order') === 'asc' ? '✓' : '',
  1732. onClick: async function(){
  1733. const sort_by = $(el_window).attr('data-sort_by')
  1734. sort_items(el_window_body, sort_by, 'asc');
  1735. set_sort_by($(el_window).attr('data-uid'), sort_by, 'asc')
  1736. }
  1737. },
  1738. {
  1739. html: i18n('descending'),
  1740. icon: $(el_window).attr('data-sort_order') === 'desc' ? '✓' : '',
  1741. onClick: async function(){
  1742. const sort_by = $(el_window).attr('data-sort_by')
  1743. sort_items(el_window_body, sort_by, 'desc');
  1744. set_sort_by($(el_window).attr('data-uid'), sort_by, 'desc')
  1745. }
  1746. },
  1747. ]
  1748. },
  1749. // -------------------------------------------
  1750. // Refresh
  1751. // -------------------------------------------
  1752. {
  1753. html: i18n('refresh'),
  1754. onClick: function(){
  1755. refresh_item_container(el_window_body, options);
  1756. }
  1757. },
  1758. // -------------------------------------------
  1759. // Show/Hide hidden files
  1760. // -------------------------------------------
  1761. {
  1762. html: i18n('show_hidden'),
  1763. icon: window.user_preferences.show_hidden_files ? '✓' : '',
  1764. onClick: function(){
  1765. window.mutate_user_preferences({
  1766. show_hidden_files : !window.user_preferences.show_hidden_files,
  1767. });
  1768. window.show_or_hide_files(document.querySelectorAll('.item-container'));
  1769. }
  1770. },
  1771. // -------------------------------------------
  1772. // -
  1773. // -------------------------------------------
  1774. '-',
  1775. // -------------------------------------------
  1776. // New
  1777. // -------------------------------------------
  1778. new_context_menu_item($(el_window).attr('data-path'), el_window_body),
  1779. // -------------------------------------------
  1780. // -
  1781. // -------------------------------------------
  1782. '-',
  1783. // -------------------------------------------
  1784. // Paste
  1785. // -------------------------------------------
  1786. {
  1787. html: i18n('paste'),
  1788. disabled: (clipboard.length === 0 || $(el_window).attr('data-path') === '/') ? true : false,
  1789. onClick: function(){
  1790. if(clipboard_op === 'copy')
  1791. copy_clipboard_items($(el_window).attr('data-path'), el_window_body);
  1792. else if(clipboard_op === 'move')
  1793. move_clipboard_items(el_window_body)
  1794. }
  1795. },
  1796. // -------------------------------------------
  1797. // Undo
  1798. // -------------------------------------------
  1799. {
  1800. html: i18n('undo'),
  1801. disabled: actions_history.length > 0 ? false : true,
  1802. onClick: function(){
  1803. undo_last_action();
  1804. }
  1805. },
  1806. // -------------------------------------------
  1807. // Upload Here
  1808. // -------------------------------------------
  1809. {
  1810. html: i18n('upload_here'),
  1811. disabled: $(el_window).attr('data-path') === '/' ? true : false,
  1812. onClick: function(){
  1813. init_upload_using_dialog(el_window_body, $(el_window).attr('data-path') + '/');
  1814. }
  1815. },
  1816. // -------------------------------------------
  1817. // -
  1818. // -------------------------------------------
  1819. '-',
  1820. // -------------------------------------------
  1821. // Publish As Website
  1822. // -------------------------------------------
  1823. {
  1824. html: i18n('publish_as_website'),
  1825. disabled: !options.is_dir,
  1826. onClick: async function () {
  1827. if (window.require_email_verification_to_publish_website) {
  1828. if (window.user.is_temp &&
  1829. !await UIWindowSaveAccount({
  1830. send_confirmation_code: true,
  1831. message: i18n('save_account_to_publish'),
  1832. window_options: {
  1833. backdrop: true,
  1834. close_on_backdrop_click: false,
  1835. }
  1836. }))
  1837. return;
  1838. else if (!window.user.email_confirmed && !await UIWindowEmailConfirmationRequired())
  1839. return;
  1840. }
  1841. UIWindowPublishWebsite($(el_window).attr('data-uid'), $(el_window).attr('data-name'), $(el_window).attr('data-path'));
  1842. }
  1843. },
  1844. // -------------------------------------------
  1845. // Deploy as App
  1846. // -------------------------------------------
  1847. {
  1848. html: i18n('deploy_as_app'),
  1849. disabled: !options.is_dir,
  1850. onClick: async function () {
  1851. launch_app({
  1852. name: 'dev-center',
  1853. file_path: $(el_window).attr('data-path'),
  1854. file_uid: $(el_window).attr('data-uid'),
  1855. params: {
  1856. source_path: $(el_window).attr('data-path'),
  1857. }
  1858. })
  1859. }
  1860. },
  1861. // -------------------------------------------
  1862. // -
  1863. // -------------------------------------------
  1864. '-',
  1865. // -------------------------------------------
  1866. // Properties
  1867. // -------------------------------------------
  1868. {
  1869. html: i18n('properties'),
  1870. onClick: function(){
  1871. let window_height = 500;
  1872. let window_width = 450;
  1873. let left = mouseX;
  1874. left -= 200;
  1875. left = left > (window.innerWidth - window_width)? (window.innerWidth - window_width) : left;
  1876. let top = mouseY;
  1877. top = top > (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height))? (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) : top;
  1878. UIWindowItemProperties(options.title, options.path, options.uid, left, top, window_width, window_height);
  1879. }
  1880. },
  1881. ]
  1882. });
  1883. }
  1884. // Trash conext menu
  1885. else{
  1886. UIContextMenu({
  1887. parent_element: el_window_body,
  1888. items: [
  1889. // -------------------------------------------
  1890. // Empty Trash
  1891. // -------------------------------------------
  1892. {
  1893. html: i18n('empty_trash'),
  1894. disabled: false,
  1895. onClick: async function(){
  1896. const alert_resp = await UIAlert({
  1897. message: i18n('empty_trash_confirmation'),
  1898. buttons:[
  1899. {
  1900. label: i18n('yes'),
  1901. value: 'yes',
  1902. type: 'primary',
  1903. },
  1904. {
  1905. label: i18n('no'),
  1906. value: 'no',
  1907. },
  1908. ]
  1909. })
  1910. if(alert_resp === 'no')
  1911. return;
  1912. // todo this has to be case-insensitive but the `i` selector doesn't work on ^=
  1913. $(`.item[data-path^="${html_encode(trash_path)}/"]`).each(function(){
  1914. delete_item(this);
  1915. })
  1916. // update other clients
  1917. if(window.socket){
  1918. window.socket.emit('trash.is_empty', {is_empty: true});
  1919. }
  1920. // use the 'empty trash' icon
  1921. $(`.item[data-path="${html_encode(trash_path)}" i], .item[data-shortcut_to_path="${html_encode(trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
  1922. }
  1923. },
  1924. ]
  1925. });
  1926. }
  1927. }
  1928. });
  1929. // --------------------------------------------------------
  1930. // Head Context Menu
  1931. // --------------------------------------------------------
  1932. if(options.has_head){
  1933. $(el_window_head).bind("contextmenu taphold", function (event) {
  1934. event.preventDefault();
  1935. return false;
  1936. })
  1937. }
  1938. // --------------------------------------------------------
  1939. // Droppable sidebar items
  1940. // --------------------------------------------------------
  1941. $(el_window).find('.window-sidebar-item').each(function (index){
  1942. // todo only continue if this item is a dir
  1943. const el_item = this;
  1944. $(el_item).dragster({
  1945. enter: function (dragsterEvent, event) {
  1946. $(el_item).addClass('item-selected');
  1947. },
  1948. leave: function (dragsterEvent, event) {
  1949. $(el_item).removeClass('item-selected');
  1950. },
  1951. drop: function (dragsterEvent, event) {
  1952. const e = event.originalEvent;
  1953. $(el_item).removeClass('item-selected');
  1954. // if files were dropped...
  1955. if(e.dataTransfer?.items?.length > 0){
  1956. upload_items(e.dataTransfer.items, $(el_item).attr('data-path'))
  1957. }
  1958. e.stopPropagation();
  1959. e.preventDefault();
  1960. return false;
  1961. }
  1962. });
  1963. })
  1964. //set styles
  1965. $(el_window_body).css(options.body_css);
  1966. // is fullpage?
  1967. if(options.is_fullpage){
  1968. $(el_window).hide()
  1969. setTimeout(function(){
  1970. enter_fullpage_mode(el_window);
  1971. $(el_window).show()
  1972. }, 5);
  1973. }
  1974. return el_window;
  1975. }
  1976. function delete_window_element (el_window){
  1977. // if this is the active element, set it to null
  1978. if(active_element === el_window){
  1979. active_element = null;
  1980. }
  1981. // remove DOM element
  1982. $(el_window).remove();
  1983. // if no other windows open, reset window_counter
  1984. // resetting window counter is important so that next window opens at the center of the screen
  1985. if($('.window').length === 0)
  1986. window.window_counter = 0;
  1987. }
  1988. $(document).on('click', '.window-sidebar-item', async function(e){
  1989. const el_window = $(this).closest('.window');
  1990. const parent_win_id = $(el_window).attr('data-id');
  1991. const item_path = $(this).attr('data-path');
  1992. // ctrl/cmd + click will open in new window
  1993. if(e.metaKey || e.ctrlKey){
  1994. UIWindow({
  1995. path: item_path,
  1996. title: path.basename(item_path),
  1997. icon: await item_icon({is_dir: true, path: item_path}),
  1998. // todo
  1999. // uid: $(el_item).attr('data-uid'),
  2000. is_dir: true,
  2001. // todo
  2002. // sort_by: $(el_item).attr('data-sort_by'),
  2003. app: 'explorer',
  2004. // top: options.maximized ? 0 : undefined,
  2005. // left: options.maximized ? 0 : undefined,
  2006. // height: options.maximized ? `calc(100% - ${window.taskbar_height + 1}px)` : undefined,
  2007. // width: options.maximized ? `100%` : undefined,
  2008. });
  2009. }
  2010. // update window path only if it's a new path AND no ctrl/cmd key pressed
  2011. else if(item_path !== $(el_window).attr('data-path')){
  2012. window_nav_history[parent_win_id] = window_nav_history[parent_win_id].slice(0, window_nav_history_current_position[parent_win_id] + 1);
  2013. window_nav_history[parent_win_id].push(item_path);
  2014. window_nav_history_current_position[parent_win_id]++;
  2015. update_window_path(el_window, item_path);
  2016. }
  2017. })
  2018. $(document).on('contextmenu', '.window-sidebar', function(e){
  2019. e.preventDefault();
  2020. e.stopPropagation();
  2021. return false;
  2022. })
  2023. $(document).on('contextmenu taphold', '.window-sidebar-item', function(event){
  2024. // dismiss taphold on regular devices
  2025. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  2026. return;
  2027. event.preventDefault();
  2028. event.stopPropagation();
  2029. // todo
  2030. // $(this).addClass('window-sidebar-item-highlighted');
  2031. const item = this;
  2032. UIContextMenu({
  2033. parent_element: $(this),
  2034. items: [
  2035. //--------------------------------------------------
  2036. // Open
  2037. //--------------------------------------------------
  2038. {
  2039. html: "Open",
  2040. onClick: function(){
  2041. $(item).trigger('click');
  2042. }
  2043. },
  2044. //--------------------------------------------------
  2045. // Open in New Window
  2046. //--------------------------------------------------
  2047. {
  2048. html: "Open in New Window",
  2049. onClick: async function(){
  2050. let item_path = $(item).attr('data-path');
  2051. UIWindow({
  2052. path: item_path,
  2053. title: path.basename(item_path),
  2054. icon: await item_icon({is_dir: true, path: item_path}),
  2055. // todo
  2056. // uid: $(el_item).attr('data-uid'),
  2057. is_dir: true,
  2058. // todo
  2059. // sort_by: $(el_item).attr('data-sort_by'),
  2060. app: 'explorer',
  2061. // top: options.maximized ? 0 : undefined,
  2062. // left: options.maximized ? 0 : undefined,
  2063. // height: options.maximized ? `calc(100% - ${window.taskbar_height + 1}px)` : undefined,
  2064. // width: options.maximized ? `100%` : undefined,
  2065. });
  2066. }
  2067. }
  2068. ]
  2069. });
  2070. return false;
  2071. })
  2072. $(document).on('dblclick', '.window .ui-resizable-handle', function(e){
  2073. let el_window = $(this).closest('.window');
  2074. // bottom
  2075. if($(this).hasClass('ui-resizable-s')){
  2076. let height = window.innerHeight - $(el_window).position().top - window.taskbar_height -1;
  2077. $(el_window).height(height);
  2078. }
  2079. // top
  2080. else if($(this).hasClass('ui-resizable-n')){
  2081. let height = $(el_window).height() + $(el_window).position().top - window.toolbar_height;
  2082. $(el_window).css({
  2083. height: height,
  2084. top: window.toolbar_height,
  2085. });
  2086. }
  2087. // right
  2088. else if($(this).hasClass('ui-resizable-e')){
  2089. let width = window.innerWidth - $(el_window).position().left;
  2090. $(el_window).css({
  2091. width: width,
  2092. });
  2093. }
  2094. // left
  2095. else if($(this).hasClass('ui-resizable-w')){
  2096. let width = $(el_window).width() + $(el_window).position().left;
  2097. $(el_window).css({
  2098. width: width,
  2099. left: 0
  2100. });
  2101. }
  2102. // bottom left
  2103. else if($(this).hasClass('ui-resizable-sw')){
  2104. let width = $(el_window).width() + $(el_window).position().left;
  2105. let height = window.innerHeight - $(el_window).position().top - window.taskbar_height -1;
  2106. $(el_window).css({
  2107. width: width,
  2108. height: height,
  2109. left: 0
  2110. });
  2111. }
  2112. // bottom right
  2113. else if($(this).hasClass('ui-resizable-se')){
  2114. let width = window.innerWidth - $(el_window).position().left;
  2115. let height = window.innerHeight - $(el_window).position().top - window.taskbar_height -1;
  2116. $(el_window).css({
  2117. width: width,
  2118. height: height,
  2119. });
  2120. }
  2121. // top right
  2122. else if($(this).hasClass('ui-resizable-ne')){
  2123. let width = window.innerWidth - $(el_window).position().left;
  2124. let height = $(el_window).height() + $(el_window).position().top - window.toolbar_height;
  2125. $(el_window).css({
  2126. width: width,
  2127. height: height,
  2128. top: window.toolbar_height,
  2129. });
  2130. }
  2131. // top left
  2132. else if($(this).hasClass('ui-resizable-nw')){
  2133. let width = $(el_window).width() + $(el_window).position().left;
  2134. let height = $(el_window).height() + $(el_window).position().top - window.toolbar_height;
  2135. $(el_window).css({
  2136. width: width,
  2137. height: height,
  2138. top: window.toolbar_height,
  2139. left:0,
  2140. });
  2141. }
  2142. })
  2143. $(document).on('click', '.window-navbar-path', function(e){
  2144. if(!$(e.target).hasClass('window-navbar-path'))
  2145. return;
  2146. $(e.target).hide();
  2147. $(e.target).siblings('.window-navbar-path-input').show().select();
  2148. })
  2149. $(document).on('blur', '.window-navbar-path-input', function(e){
  2150. $(e.target).hide();
  2151. $(e.target).siblings('.window-navbar-path').show().select();
  2152. })
  2153. $(document).on('keyup', '.window-navbar-path-input', function(e){
  2154. if (e.key === 'Enter' || e.keyCode === 13) {
  2155. update_window_path($(e.target).closest('.window'), $(e.target).val());
  2156. $(e.target).hide();
  2157. $(e.target).siblings('.window-navbar-path').show().select();
  2158. }
  2159. })
  2160. $(document).on('click', '.window-navbar-path-dirname', function(e){
  2161. const $el_parent_window = $(this).closest('.window');
  2162. const parent_win_id = $($el_parent_window).attr('data-id');
  2163. // open in new window
  2164. if(e.metaKey || e.ctrlKey){
  2165. const dirpath = $(this).attr('data-path');
  2166. UIWindow({
  2167. path: dirpath,
  2168. title: dirpath === '/' ? root_dirname : path.basename(dirpath),
  2169. icon: window.icons['folder.svg'],
  2170. // uid: $(el_item).attr('data-uid'),
  2171. is_dir: true,
  2172. app: 'explorer',
  2173. });
  2174. }
  2175. // only change dir if target is not the same as current path
  2176. else if($el_parent_window.attr('data-path') !== $(this).attr('data-path')){
  2177. window_nav_history[parent_win_id] = window_nav_history[parent_win_id].slice(0, window_nav_history_current_position[parent_win_id]+1);
  2178. window_nav_history[parent_win_id].push($(this).attr('data-path'));
  2179. window_nav_history_current_position[parent_win_id] = window_nav_history[parent_win_id].length - 1;
  2180. update_window_path($el_parent_window, $(this).attr('data-path'));
  2181. }
  2182. })
  2183. $(document).on('contextmenu taphold', '.window-navbar', function(event){
  2184. // don't disable system ctxmenu on the address bar input
  2185. if($(event.target).hasClass('window-navbar-path-input'))
  2186. return;
  2187. // dismiss taphold on regular devices
  2188. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  2189. return;
  2190. event.preventDefault();
  2191. event.stopPropagation();
  2192. return false;
  2193. })
  2194. $(document).on('contextmenu taphold', '.window-navbar-path-dirname', function(event){
  2195. // dismiss taphold on regular devices
  2196. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  2197. return;
  2198. event.preventDefault();
  2199. const menu_items = [];
  2200. const el = this;
  2201. // -------------------------------------------
  2202. // Open
  2203. // -------------------------------------------
  2204. menu_items.push({
  2205. html: 'Open',
  2206. onClick: ()=>{
  2207. $(this).trigger('click');
  2208. }
  2209. });
  2210. // -------------------------------------------
  2211. // Open in New Window
  2212. // (only if the item is on a window)
  2213. // -------------------------------------------
  2214. menu_items.push({
  2215. html: 'Open in New Window',
  2216. onClick: function(){
  2217. UIWindow({
  2218. path: $(el).attr('data-path'),
  2219. title: $(el).attr('data-path') === '/' ? root_dirname : path.basename($(el).attr('data-path')),
  2220. icon: window.icons['folder.svg'],
  2221. uid: $(el).attr('data-uid'),
  2222. is_dir: true,
  2223. app: 'explorer',
  2224. });
  2225. }
  2226. });
  2227. // -------------------------------------------
  2228. // -
  2229. // -------------------------------------------
  2230. menu_items.push('-'),
  2231. // -------------------------------------------
  2232. // Paste
  2233. // -------------------------------------------
  2234. menu_items.push({
  2235. html: "Paste",
  2236. disabled: clipboard.length > 0 ? false : true,
  2237. onClick: function(){
  2238. if(clipboard_op === 'copy')
  2239. copy_clipboard_items($(el).attr('data-path'), null);
  2240. else if(clipboard_op === 'move')
  2241. move_clipboard_items(null, $(el).attr('data-path'))
  2242. }
  2243. })
  2244. UIContextMenu({
  2245. parent_element: $(this),
  2246. items: menu_items
  2247. });
  2248. })
  2249. // if the click is on the mask, bring focus to the active child window
  2250. $(document).on('click', '.window-disable-mask', async function(e){
  2251. e.stopPropagation();
  2252. e.preventDefault();
  2253. return false;
  2254. })
  2255. // --------------------------------------------------------
  2256. // Navbar Dir Droppable
  2257. // --------------------------------------------------------
  2258. window.navbar_path_droppable = (el_window)=>{
  2259. $(el_window).find('.window-navbar-path-dirname').droppable({
  2260. accept: '.item',
  2261. tolerance: 'pointer',
  2262. drop: function( event, ui ) {
  2263. // check if item was actually dropped on this navbar path
  2264. if($(mouseover_window).attr('data-id') !== $(el_window).attr('data-id')){
  2265. return;
  2266. }
  2267. const items_to_move = []
  2268. // first item
  2269. items_to_move.push(ui.draggable);
  2270. // all subsequent items
  2271. const cloned_items = document.getElementsByClassName('item-selected-clone');
  2272. for(let i =0; i<cloned_items.length; i++){
  2273. const source_item = document.getElementById('item-' + $(cloned_items[i]).attr('data-id'));
  2274. if(source_item !== null)
  2275. items_to_move.push(source_item);
  2276. }
  2277. // if alt key is down, create shortcut items
  2278. if(event.altKey){
  2279. items_to_move.forEach((item_to_move) => {
  2280. create_shortcut(
  2281. path.basename($(item_to_move).attr('data-path')),
  2282. $(item_to_move).attr('data-is_dir') === '1',
  2283. $(this).attr('data-path'),
  2284. null,
  2285. $(item_to_move).attr('data-shortcut_to') === '' ? $(item_to_move).attr('data-uid') : $(item_to_move).attr('data-shortcut_to'),
  2286. $(item_to_move).attr('data-shortcut_to_path') === '' ? $(item_to_move).attr('data-path') : $(item_to_move).attr('data-shortcut_to_path'),
  2287. );
  2288. });
  2289. }
  2290. // move items
  2291. else{
  2292. move_items(items_to_move, $(this).attr('data-path'));
  2293. }
  2294. $('.item-container').droppable('enable')
  2295. $(this).removeClass('window-navbar-path-dirname-active');
  2296. return false;
  2297. },
  2298. over: function(event, ui){
  2299. // check if item was actually hovered over this window
  2300. if($(mouseover_window).attr('data-id') !== $(el_window).attr('data-id'))
  2301. return;
  2302. // Don't do anything if the dragged item is NOT a UIItem
  2303. if(!$(ui.draggable).hasClass('item'))
  2304. return;
  2305. // highlight this dirname
  2306. $(this).addClass('window-navbar-path-dirname-active');
  2307. $('.ui-draggable-dragging').css('opacity', 0.2)
  2308. $('.item-selected-clone').css('opacity', 0.2)
  2309. // disable all window bodies
  2310. $('.item-container').droppable( 'disable' )
  2311. },
  2312. out: function(event, ui){
  2313. // Don't do anything if the dragged element is NOT a UIItem
  2314. if(!$(ui.draggable).hasClass('item'))
  2315. return;
  2316. // unselect directory if item is dragged out
  2317. $(this).removeClass('window-navbar-path-dirname-active');
  2318. $('.ui-draggable-dragging').css('opacity', 'initial')
  2319. $('.item-selected-clone').css('opacity', 'initial')
  2320. $('.item-container').droppable( 'enable' )
  2321. }
  2322. });
  2323. }
  2324. /**
  2325. * Constructs a XSS-safe string that represents a navigation bar path.
  2326. * The result is a string with HTML span elements for each directory in the path, each accompanied by a separator icon.
  2327. * Each span element has a `data-path` attribute holding the encoded path to that directory, and contains the encoded directory name as text.
  2328. * The root directory name is a constant defined in globals.js, represented as 'root_dirname'.
  2329. *
  2330. * @param {string} abs_path - The absolute path to be displayed in the navigation bar. It should be a string with directories separated by slashes ('/').
  2331. *
  2332. * @returns {string} A string of HTML spans and separators, each span representing a directory in the navigation bar.
  2333. *
  2334. */
  2335. window.navbar_path = (abs_path)=>{
  2336. const dirs = (abs_path === '/' ? [''] : abs_path.split('/'));
  2337. const dirpaths = (abs_path === '/' ? ['/'] : [])
  2338. const path_seperator_html = `<img class="path-seperator" draggable="false" src="${html_encode(window.icons['triangle-right.svg'])}">`;
  2339. if(dirs.length > 1){
  2340. for(let i=0; i<dirs.length; i++){
  2341. dirpaths[i] = '';
  2342. for(let j=1; j<=i; j++){
  2343. dirpaths[i] += '/'+dirs[j];
  2344. }
  2345. }
  2346. }
  2347. let str = `${path_seperator_html}<span class="window-navbar-path-dirname" data-path="${html_encode('/')}">${html_encode(window.root_dirname)}</span>`;
  2348. for(let k=1; k<dirs.length; k++){
  2349. str += `${path_seperator_html}<span class="window-navbar-path-dirname" data-path="${html_encode(dirpaths[k])}">${dirs[k] === 'Trash' ? i18n('trash') : html_encode(dirs[k])}</span>`;
  2350. }
  2351. return str;
  2352. }
  2353. window.update_window_path = async function(el_window, target_path){
  2354. const win_id = $(el_window).attr('data-id');
  2355. const el_window_navbar_forward_btn = $(el_window).find('.window-navbar-btn-forward');
  2356. const el_window_navbar_back_btn = $(el_window).find('.window-navbar-btn-back');
  2357. const el_window_navbar_up_btn = $(el_window).find('.window-navbar-btn-up');
  2358. const el_window_body = $(el_window).find('.window-body');
  2359. const el_window_item_container = $(el_window).find('.item-container');
  2360. const el_window_navbar_path_input = $(el_window).find('.window-navbar-path-input');
  2361. const is_dir = ($(el_window).attr('data-is_dir') === '1' || $(el_window).attr('data-is_dir') === 'true');
  2362. const old_path = $(el_window).attr('data-path');
  2363. // update sidebar items' active status
  2364. $(el_window).find(`.window-sidebar-item`).removeClass('window-sidebar-item-active');
  2365. $(el_window).find(`.window-sidebar-item[data-path="${html_encode(target_path)}"]`).addClass('window-sidebar-item-active');
  2366. // clean
  2367. $(el_window).find('.explore-table-headers-th > .header-sort-icon').html('');
  2368. if(is_dir){
  2369. // if nav history for this window is empty, disable forward btn
  2370. if(window_nav_history[win_id] && window_nav_history[win_id].length - 1 === window_nav_history_current_position[win_id])
  2371. $(el_window_navbar_forward_btn).addClass('window-navbar-btn-disabled');
  2372. // ... else, enable forawrd btn
  2373. else
  2374. $(el_window_navbar_forward_btn).removeClass('window-navbar-btn-disabled');
  2375. // disable back button if path is root
  2376. if(window_nav_history_current_position[win_id] === 0)
  2377. $(el_window_navbar_back_btn).addClass('window-navbar-btn-disabled');
  2378. // ... enable back btn in all other cases
  2379. else
  2380. $(el_window_navbar_back_btn).removeClass('window-navbar-btn-disabled');
  2381. // disabled Up button if this is root
  2382. if(target_path === '/')
  2383. $(el_window_navbar_up_btn).addClass('window-navbar-btn-disabled');
  2384. // ... enable back btn in all other cases
  2385. else
  2386. $(el_window_navbar_up_btn).removeClass('window-navbar-btn-disabled');
  2387. $(el_window_item_container).attr('data-path', target_path);
  2388. $(el_window).find('.window-navbar-path').html(navbar_path(target_path, window.user.username));
  2389. // empty body to be filled with the results of /readdir
  2390. $(el_window_body).find('.item').removeItems()
  2391. // add the 'Detail View' table header
  2392. if($(el_window).find('.explore-table-headers').length === 0)
  2393. $(el_window_body).prepend(window.explore_table_headers());
  2394. // 'Detail View' table header is hidden by default
  2395. $(el_window).find('.explore-table-headers').hide();
  2396. // system directories with custom icons and predefined names
  2397. if(target_path === window.desktop_path){
  2398. $(el_window).find('.window-head-icon').attr('src', window.icons['folder-desktop.svg']);
  2399. $(el_window).find('.window-head-title').text('Desktop')
  2400. }else if (target_path === window.home_path){
  2401. $(el_window).find('.window-head-icon').attr('src', window.icons['folder-home.svg']);
  2402. $(el_window).find('.window-head-title').text('Home')
  2403. }else if (target_path === window.docs_path){
  2404. $(el_window).find('.window-head-icon').attr('src', window.icons['folder-documents.svg']);
  2405. $(el_window).find('.window-head-title').text('Documents')
  2406. }else if (target_path === window.videos_path){
  2407. $(el_window).find('.window-head-icon').attr('src', window.icons['folder-videos.svg']);
  2408. $(el_window).find('.window-head-title').text('Videos')
  2409. }else if (target_path === window.pictures_path){
  2410. $(el_window).find('.window-head-icon').attr('src', window.icons['folder-pictures.svg']);
  2411. $(el_window).find('.window-head-title').text('Pictures')
  2412. }// root folder of a shared user?
  2413. else if((target_path.split('/').length - 1) === 1 && target_path !== '/'+window.user.username)
  2414. $(el_window).find('.window-head-icon').attr('src', window.icons['shared.svg']);
  2415. else
  2416. $(el_window).find('.window-head-icon').attr('src', window.icons['folder.svg']);
  2417. }
  2418. $(el_window).attr('data-path', html_encode(target_path));
  2419. $(el_window).attr('data-name', html_encode(path.basename(target_path)));
  2420. // /stat
  2421. if(target_path !== '/'){
  2422. try{
  2423. puter.fs.stat(target_path, function(fsentry){
  2424. $(el_window).removeClass('window-' + $(el_window).attr('data-uid'));
  2425. $(el_window).addClass('window-' + fsentry.id);
  2426. $(el_window).attr('data-uid', fsentry.id);
  2427. $(el_window).attr('data-sort_by', fsentry.sort_by ?? 'name');
  2428. $(el_window).attr('data-sort_order', fsentry.sort_order ?? 'asc');
  2429. $(el_window).attr('data-layout', fsentry.layout ?? 'icons');
  2430. $(el_window_item_container).attr('data-uid', fsentry.id);
  2431. // title
  2432. if (target_path === window.home_path)
  2433. $(el_window).find('.window-head-title').text('Home')
  2434. else
  2435. $(el_window).find('.window-head-title').text(fsentry.name);
  2436. // data-name
  2437. $(el_window).attr('data-name', html_encode(fsentry.name));
  2438. // data-path
  2439. $(el_window).attr('data-path', html_encode(target_path));
  2440. $(el_window_navbar_path_input).val(target_path);
  2441. $(el_window_navbar_path_input).attr('data-path', target_path);
  2442. // update layout
  2443. update_window_layout(el_window, fsentry.layout);
  2444. // update explore header if in details view
  2445. if(fsentry.layout === 'details'){
  2446. update_details_layout_sort_visuals(el_window, fsentry.sort_by, fsentry.sort_order);
  2447. }
  2448. });
  2449. }catch(err){
  2450. UIAlert(err.responseText)
  2451. // todo optim: this is dumb because updating the window should only happen if this /readdir request is successful,
  2452. // in that case there is no need for using update_window_path on error!!
  2453. update_window_path(el_window, old_path);
  2454. }
  2455. }
  2456. // path is '/' (global root)
  2457. else{
  2458. $(el_window).removeClass('window-' + $(el_window).attr('data-uid'));
  2459. $(el_window).addClass('window-null');
  2460. $(el_window).attr('data-uid', 'null');
  2461. $(el_window).attr('data-name', '');
  2462. $(el_window).find('.window-head-title').text(root_dirname);
  2463. }
  2464. if(is_dir){
  2465. refresh_item_container(el_window_body);
  2466. navbar_path_droppable(el_window)
  2467. }
  2468. update_explorer_footer_selected_items_count(el_window);
  2469. }
  2470. // --------------------------------------------------------
  2471. // Sidebar Item Droppable
  2472. // --------------------------------------------------------
  2473. window.sidebar_item_droppable = (el_window)=>{
  2474. $(el_window).find('.window-sidebar-item').droppable({
  2475. accept: '.item',
  2476. tolerance: 'pointer',
  2477. drop: function( event, ui ) {
  2478. // check if item was actually dropped on this navbar path
  2479. if($(mouseover_window).attr('data-id') !== $(el_window).attr('data-id')){
  2480. return;
  2481. }
  2482. const items_to_move = []
  2483. // first item
  2484. items_to_move.push(ui.draggable);
  2485. // all subsequent items
  2486. const cloned_items = document.getElementsByClassName('item-selected-clone');
  2487. for(let i =0; i<cloned_items.length; i++){
  2488. const source_item = document.getElementById('item-' + $(cloned_items[i]).attr('data-id'));
  2489. if(source_item !== null)
  2490. items_to_move.push(source_item);
  2491. }
  2492. // if alt key is down, create shortcut items
  2493. if(event.altKey){
  2494. items_to_move.forEach((item_to_move) => {
  2495. create_shortcut(
  2496. path.basename($(item_to_move).attr('data-path')),
  2497. $(item_to_move).attr('data-is_dir') === '1',
  2498. $(this).attr('data-path'),
  2499. null,
  2500. $(item_to_move).attr('data-shortcut_to') === '' ? $(item_to_move).attr('data-uid') : $(item_to_move).attr('data-shortcut_to'),
  2501. $(item_to_move).attr('data-shortcut_to_path') === '' ? $(item_to_move).attr('data-path') : $(item_to_move).attr('data-shortcut_to_path'),
  2502. );
  2503. });
  2504. }
  2505. // move items
  2506. else{
  2507. move_items(items_to_move, $(this).attr('data-path'));
  2508. }
  2509. $('.item-container').droppable('enable')
  2510. $(this).removeClass('window-sidebar-item-drag-active');
  2511. return false;
  2512. },
  2513. over: function(event, ui){
  2514. // check if item was actually hovered over this window
  2515. if($(mouseover_window).attr('data-id') !== $(el_window).attr('data-id'))
  2516. return;
  2517. // Don't do anything if the dragged item is NOT a UIItem
  2518. if(!$(ui.draggable).hasClass('item'))
  2519. return;
  2520. // highlight this item
  2521. $(this).addClass('window-sidebar-item-drag-active');
  2522. $('.ui-draggable-dragging').css('opacity', 0.2)
  2523. $('.item-selected-clone').css('opacity', 0.2)
  2524. // disable all window bodies
  2525. $('.item-container').droppable( 'disable' )
  2526. },
  2527. out: function(event, ui){
  2528. // Don't do anything if the dragged element is NOT a UIItem
  2529. if(!$(ui.draggable).hasClass('item'))
  2530. return;
  2531. // unselect item if item is dragged out
  2532. $(this).removeClass('window-sidebar-item-drag-active');
  2533. $('.ui-draggable-dragging').css('opacity', 'initial')
  2534. $('.item-selected-clone').css('opacity', 'initial')
  2535. $('.item-container').droppable( 'enable' )
  2536. }
  2537. });
  2538. }
  2539. // closes a window
  2540. $.fn.close = async function(options) {
  2541. options = options || {};
  2542. console.log(options);
  2543. $(this).each(async function() {
  2544. const el_iframe = $(this).find('.window-app-iframe');
  2545. const app_uses_sdk = el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true';
  2546. // tell child app that this window is about to close, get its response
  2547. if(app_uses_sdk){
  2548. if(!options.bypass_iframe_messaging){
  2549. const resp = await sendWindowWillCloseMsg(el_iframe.get(0));
  2550. if(!resp.msg){
  2551. return false;
  2552. }
  2553. }
  2554. }
  2555. // Process window close if this is a window
  2556. if($(this).hasClass('window')){
  2557. const win_id = parseInt($(this).attr('data-id'));
  2558. let window_uuid = $(this).attr('data-element_uuid');
  2559. // remove all instances of win_id from window_stack
  2560. _.pullAll(window_stack, [win_id]);
  2561. // taskbar update
  2562. let open_window_count = parseInt($(`.taskbar-item[data-app="${$(this).attr('data-app')}"]`).attr('data-open-windows'));
  2563. // update open window count of corresponding taskbar item
  2564. if(open_window_count > 0){
  2565. $(`.taskbar-item[data-app="${$(this).attr('data-app')}"]`).attr('data-open-windows', open_window_count - 1);
  2566. }
  2567. // decide whether to remove taskbar item
  2568. if(open_window_count === 1){
  2569. $(`.taskbar-item[data-app="${$(this).attr('data-app')}"] .active-taskbar-indicator`).hide();
  2570. remove_taskbar_item($(`.taskbar-item[data-app="${$(this).attr('data-app')}"][data-keep-in-taskbar="false"]`));
  2571. }
  2572. // if no more windows of this app are open, remove taskbar item
  2573. if(open_window_count - 1 === 0)
  2574. $(`.taskbar-item[data-app="${$(this).attr('data-app')}"] .active-taskbar-indicator`).hide();
  2575. // if a fullpage window is closed, show desktop and taskbar
  2576. if($(this).attr('data-is_fullpage') === '1'){
  2577. exit_fullpage_mode();
  2578. }
  2579. // FileDialog closed
  2580. if($(this).hasClass('window-filedialog') || $(this).attr('data-disable_parent_window') === 'true'){
  2581. // re-enable this FileDialog's parent window
  2582. $(`.window[data-element_uuid="${$(this).attr('data-parent_uuid')}"]`).addClass('window-active');
  2583. $(`.window[data-element_uuid="${$(this).attr('data-parent_uuid')}"]`).removeClass('window-disabled');
  2584. $(`.window[data-element_uuid="${$(this).attr('data-parent_uuid')}"]`).find('.window-disable-mask').hide();
  2585. // bring focus back to app iframe, if needed
  2586. $(`.window[data-element_uuid="${$(this).attr('data-parent_uuid')}"]`).focusWindow();
  2587. }
  2588. // Other types of windows closed
  2589. else{
  2590. // close any open FileDialogs belonging to this window
  2591. $(`.window-filedialog[data-parent_uuid="${window_uuid}"]`).close();
  2592. // bring focus to the last window in the window-stack (only if not minimized)
  2593. if(!_.isEmpty(window_stack)){
  2594. const $last_window_in_stack = $(`.window[data-id="${window_stack[window_stack.length - 1]}"]`);
  2595. // check if previous window is not minimized
  2596. if($last_window_in_stack !== null && $last_window_in_stack.attr('data-is_minimized') !== '1' && $last_window_in_stack.attr('data-is_minimized') !== 'true'){
  2597. $(`.window[data-id="${window_stack[window_stack.length - 1]}"]`).focusWindow();
  2598. }
  2599. // otherwise, change URL/Title to desktop
  2600. else{
  2601. window.history.replaceState(null, document.title, '/');
  2602. document.title = 'Puter';
  2603. }
  2604. // if it's explore
  2605. if($last_window_in_stack.attr('data-app') && $last_window_in_stack.attr('data-app').toLowerCase() === 'explorer'){
  2606. window.history.replaceState(null, document.title, '/');
  2607. document.title = 'Puter';
  2608. }
  2609. }
  2610. // otherwise, change URL/Title to desktop
  2611. else{
  2612. window.history.replaceState(null, document.title, '/');
  2613. document.title = 'Puter';
  2614. }
  2615. }
  2616. // close child windows
  2617. $(`.window[data-parent_uuid="${window_uuid}"]`).close();
  2618. // notify other apps that we're closing
  2619. if (app_uses_sdk) {
  2620. // notify parent app, if we have one, that we're closing
  2621. const parent_id = this.dataset['parent_instance_id'];
  2622. const parent = $(`.window[data-element_uuid="${parent_id}"] .window-app-iframe`).get(0);
  2623. if (parent) {
  2624. parent.contentWindow.postMessage({
  2625. msg: 'appClosed',
  2626. appInstanceID: window_uuid,
  2627. }, '*');
  2628. }
  2629. // notify child apps, if we have them, that we're closing
  2630. const children = $(`.window[data-parent_instance_id="${window_uuid}"] .window-app-iframe`);
  2631. children.each((_, child) => {
  2632. child.contentWindow.postMessage({
  2633. msg: 'appClosed',
  2634. appInstanceID: window_uuid,
  2635. }, '*');
  2636. });
  2637. // TODO: Once other AppConnections exist, those will need notifying too.
  2638. }
  2639. // remove backdrop
  2640. $(this).closest('.window-backdrop').remove();
  2641. // remove DOM element
  2642. if(options?.shrink_to_target){
  2643. // get target location
  2644. const target_pos = $(options.shrink_to_target).position();
  2645. const target_size = $(options.shrink_to_target).get(0).getBoundingClientRect();
  2646. // animate window to target location
  2647. $(this).animate({
  2648. width: `1`,
  2649. height: `1`,
  2650. top: target_pos.top + target_size.height / 2,
  2651. left: target_pos.left + target_size.width / 2,
  2652. }, 300, () => {
  2653. // remove DOM element
  2654. delete_window_element(this);
  2655. });
  2656. }
  2657. else if(window.animate_window_closing){
  2658. // start shrink animation
  2659. $(this).css({
  2660. 'transition': 'transform 400ms',
  2661. 'transform': 'scale(0)',
  2662. });
  2663. // remove DOM element after fadeout animation
  2664. $(this).fadeOut(80, function(){
  2665. delete_window_element(this);
  2666. })
  2667. }else{
  2668. delete_window_element(this);
  2669. }
  2670. }
  2671. // focus back to desktop?
  2672. if(_.isEmpty(window_stack)){
  2673. // The following is to make sure the iphone keyboard is dismissed when the last window is closed
  2674. if(isMobile.phone || isMobile.tablet){
  2675. document.activeElement.blur();
  2676. $("input").blur();
  2677. }
  2678. // focus back to desktop
  2679. $('.desktop').find('.item-blurred').removeClass('item-blurred');
  2680. active_item_container = $('.desktop.item-container').get(0);
  2681. }
  2682. })
  2683. return this;
  2684. };
  2685. window.scale_window = (el_window)=>{
  2686. //maximize
  2687. if ($(el_window).attr('data-is_maximized') !== '1') {
  2688. // save original size and position
  2689. let el_window_rect = el_window.getBoundingClientRect();
  2690. $(el_window).attr({
  2691. 'data-left-before-maxim': el_window_rect.left + 'px',
  2692. 'data-top-before-maxim': el_window_rect.top + 'px',
  2693. 'data-width-before-maxim': $(el_window).css('width'),
  2694. 'data-height-before-maxim': $(el_window).css('height'),
  2695. 'data-is_maximized': '1',
  2696. });
  2697. // shrink icon
  2698. $(el_window).find('.window-scale-btn>img').attr('src', window.icons['scale-down-3.svg']);
  2699. // set new size and position
  2700. $(el_window).css({
  2701. 'top': toolbar_height+'px',
  2702. 'left': '0',
  2703. 'width': '100%',
  2704. 'height': `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)`,
  2705. 'transform': 'none',
  2706. });
  2707. }
  2708. //shrink
  2709. else {
  2710. // set size and position to original before maximization
  2711. $(el_window).css({
  2712. 'top': $(el_window).attr('data-top-before-maxim'),
  2713. 'left': $(el_window).attr('data-left-before-maxim'),
  2714. 'width': $(el_window).attr('data-width-before-maxim'),
  2715. 'height': $(el_window).attr('data-height-before-maxim'),
  2716. 'transform': 'none',
  2717. });
  2718. // maximize icon
  2719. $(el_window).find('.window-scale-btn>img').attr('src', window.icons['scale.svg']);
  2720. $(el_window).attr({
  2721. 'data-is_maximized': 0,
  2722. });
  2723. }
  2724. // record window size and position before scaling
  2725. $(el_window).attr({
  2726. 'data-orig-width': $(el_window).width(),
  2727. 'data-orig-height': $(el_window).height(),
  2728. 'data-orig-top': $(el_window).position().top,
  2729. 'data-orig-left': $(el_window).position().left,
  2730. 'data-is_minimized': false,
  2731. })
  2732. }
  2733. window.update_explorer_footer_item_count = function(el_window){
  2734. //update dir count in explorer footer
  2735. let item_count = $(el_window).find('.item').length;
  2736. $(el_window).find('.explorer-footer .explorer-footer-item-count').html(item_count + ` ${i18n('item')}` + (item_count == 0 || item_count > 1 ? `${i18n('plural_suffix')}` : ''));
  2737. }
  2738. window.update_explorer_footer_selected_items_count = function(el_window){
  2739. //update dir count in explorer footer
  2740. let item_count = $(el_window).find('.item-selected').length;
  2741. if(item_count > 0){
  2742. $(el_window).find('.explorer-footer-seperator, .explorer-footer-selected-items-count').show();
  2743. $(el_window).find('.explorer-footer .explorer-footer-selected-items-count').html(item_count + ` ${i18n('item')}` + (item_count == 0 || item_count > 1 ? `${i18n('plural_suffix')}` : '') + ` ${i18n('selected')}`);
  2744. }else{
  2745. $(el_window).find('.explorer-footer-seperator, .explorer-footer-selected-items-count').hide();
  2746. }
  2747. }
  2748. window.set_sort_by = function(item_uid, sort_by, sort_order){
  2749. if(sort_order !== 'asc' && sort_order !== 'desc')
  2750. sort_order = 'asc';
  2751. $.ajax({
  2752. url: api_origin + "/set_sort_by",
  2753. type: 'POST',
  2754. data: JSON.stringify({
  2755. sort_by: sort_by,
  2756. item_uid: item_uid,
  2757. sort_order: sort_order,
  2758. }),
  2759. async: true,
  2760. contentType: "application/json",
  2761. headers: {
  2762. "Authorization": "Bearer "+auth_token
  2763. },
  2764. statusCode: {
  2765. 401: function () {
  2766. logout();
  2767. },
  2768. },
  2769. success: function (){
  2770. }
  2771. })
  2772. // update the sort_by & sort_order attr of every matching element
  2773. $(`[data-uid="${item_uid}"]`).attr({
  2774. 'data-sort_by': sort_by,
  2775. 'data-sort_order': sort_order,
  2776. });
  2777. }
  2778. window.explore_table_headers = function(){
  2779. let h = ``;
  2780. h += `<div class="explore-table-headers">`;
  2781. h += `<div class="explore-table-headers-th explore-table-headers-th--name">Name<span class="header-sort-icon"></span></div>`;
  2782. h += `<div class="explore-table-headers-th explore-table-headers-th--modified">Modified<span class="header-sort-icon"></span></div>`;
  2783. h += `<div class="explore-table-headers-th explore-table-headers-th--size">Size<span class="header-sort-icon"></span></div>`;
  2784. h += `<div class="explore-table-headers-th explore-table-headers-th--type">Type<span class="header-sort-icon"></span></div>`;
  2785. h += `</div>`;
  2786. return h;
  2787. }
  2788. window.update_window_layout = function(el_window, layout){
  2789. layout = layout ?? 'icons';
  2790. if(layout === 'icons'){
  2791. $(el_window).find('.explore-table-headers').hide();
  2792. $(el_window).find('.item-container').removeClass('item-container-list');
  2793. $(el_window).find('.item-container').removeClass('item-container-details');
  2794. $(el_window).find('.window-navbar-layout-settings').attr('src', window.icons['layout-icons.svg']);
  2795. $(el_window).attr('data-layout', layout)
  2796. }
  2797. else if(layout === 'list'){
  2798. $(el_window).find('.explore-table-headers').hide();
  2799. $(el_window).find('.item-container').removeClass('item-container-details');
  2800. $(el_window).find('.item-container').addClass('item-container-list');
  2801. $(el_window).find('.window-navbar-layout-settings').attr('src', window.icons['layout-list.svg'])
  2802. $(el_window).attr('data-layout', layout)
  2803. }
  2804. else if(layout === 'details'){
  2805. $(el_window).find('.explore-table-headers').show();
  2806. $(el_window).find('.item-container').removeClass('item-container-list');
  2807. $(el_window).find('.item-container').addClass('item-container-details');
  2808. $(el_window).find('.window-navbar-layout-settings').attr('src', window.icons['layout-details.svg'])
  2809. $(el_window).attr('data-layout', layout)
  2810. }
  2811. }
  2812. $.fn.showWindow = async function(options) {
  2813. $(this).each(async function() {
  2814. if($(this).hasClass('window')){
  2815. // show window
  2816. const el_window = this;
  2817. $(el_window).css({
  2818. 'transition': `top 0.2s, left 0.2s, bottom 0.2s, right 0.2s, width 0.2s, height 0.2s`,
  2819. top: $(el_window).attr('data-orig-top') + 'px',
  2820. left: $(el_window).attr('data-orig-left') + 'px',
  2821. width: $(el_window).attr('data-orig-width') + 'px',
  2822. height: $(el_window).attr('data-orig-height') + 'px',
  2823. });
  2824. $(el_window).css('z-index', ++last_window_zindex);
  2825. setTimeout(() => {
  2826. $(this).focusWindow();
  2827. }, 80);
  2828. // remove `transitions` a good while after setting css to make sure
  2829. // it doesn't interfere with an ongoing animation
  2830. setTimeout(() => {
  2831. $(el_window).css('transition', 'none');
  2832. }, 250);
  2833. }
  2834. })
  2835. return this;
  2836. };
  2837. window.show_or_hide_empty_folder_message = function(el_item_container){
  2838. // if the item container is the desktop, don't show/hide the empty message
  2839. if($(el_item_container).hasClass('desktop'))
  2840. return;
  2841. // if the item container is empty, show the empty message
  2842. if($(el_item_container).has('.item').length === 0){
  2843. $(el_item_container).find('.explorer-empty-message').show();
  2844. }
  2845. // if the item container is not empty, hide the empty message
  2846. else{
  2847. $(el_item_container).find('.explorer-empty-message').hide();
  2848. }
  2849. }
  2850. $.fn.focusWindow = function(event) {
  2851. if(this.hasClass('window')){
  2852. const $app_iframe = $(this).find('.window-app-iframe');
  2853. $('.window').not(this).removeClass('window-active');
  2854. $(this).addClass('window-active');
  2855. // disable pointer events on all other windows' iframes, except for this window's iframe
  2856. $('.window-app-iframe').not($app_iframe).css('pointer-events', 'none');
  2857. // bring this window to front, only if it's not stay_on_top
  2858. if($(this).attr('data-stay_on_top') !== 'true'){
  2859. $(this).css('z-index', ++last_window_zindex);
  2860. }
  2861. // if this window has a parent, bring them to the front too
  2862. if($(this).attr('data-parent_uuid') !== 'null'){
  2863. $(`.window[data-element_uuid="${$(this).attr('data-parent_uuid')}"]`).css('z-index', last_window_zindex);
  2864. }
  2865. // if this window has child windows, bring them to the front too
  2866. if($(this).attr('data-element_uuid') !== 'null'){
  2867. $(`.window[data-parent_uuid="${$(this).attr('data-element_uuid')}"]`).css('z-index', ++last_window_zindex);
  2868. }
  2869. //
  2870. // if this has an iframe, focus on it
  2871. if(!$(this).hasClass('window-disabled') && $app_iframe.length > 0){
  2872. $($app_iframe).css('pointer-events', 'all');
  2873. $app_iframe.get(0)?.focus({preventScroll:true});
  2874. $app_iframe.get(0)?.contentWindow?.focus({preventScroll:true});
  2875. // todo check if iframe is using SDK before sending messages
  2876. $app_iframe.get(0).contentWindow.postMessage({msg: "focus"}, '*');
  2877. var rect = $app_iframe.get(0).getBoundingClientRect();
  2878. // send click event to iframe, if this focus event was triggered by a click or similar mouse event
  2879. if(
  2880. event !== undefined &&
  2881. (event.type === 'click' || event.type === 'dblclick' || event.type === 'contextmenu' || event.type === 'mousedown' || event.type === 'mouseup' || event.type === 'mousemove')
  2882. ){
  2883. $app_iframe.get(0).contentWindow.postMessage({msg: "click", x: (mouseX - rect.left), y: (mouseY - rect.top)}, '*');
  2884. }
  2885. }
  2886. // set active_item_container
  2887. active_item_container = $(this).find('.item-container').get(0);
  2888. // grey out all selected items on other windows/desktop
  2889. $('.item-container').not(active_item_container).find('.item-selected').addClass('item-blurred');
  2890. // update window-stack
  2891. window_stack.push(parseInt($(this).attr('data-id')));
  2892. // remove blurred class from items on this window
  2893. $(active_item_container).find('.item-blurred').removeClass('item-blurred');
  2894. //change window URL
  2895. const update_window_url = $(this).attr('data-update_window_url');
  2896. if(update_window_url === 'true' || update_window_url === null){
  2897. window.history.replaceState({window_id: $(this).attr('data-id')}, '', '/app/'+$(this).attr('data-app'));
  2898. document.title = $(this).attr('data-name');
  2899. }
  2900. $(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).addClass('taskbar-item-active');
  2901. }else{
  2902. $('.window').find('.item-selected').addClass('item-blurred');
  2903. $('.desktop').find('.item-blurred').removeClass('item-blurred');
  2904. }
  2905. return this;
  2906. }
  2907. // hides a window
  2908. $.fn.hideWindow = async function(options) {
  2909. $(this).each(async function() {
  2910. if($(this).hasClass('window')){
  2911. // get taskbar item location
  2912. const taskbar_item_pos = $(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).position();
  2913. $(this).attr({
  2914. 'data-orig-width': $(this).width(),
  2915. 'data-orig-height': $(this).height(),
  2916. 'data-orig-top': $(this).position().top,
  2917. 'data-orig-left': $(this).position().left,
  2918. 'data-is_minimized': true,
  2919. })
  2920. $(this).css({
  2921. 'transition': `top 0.2s, left 0.2s, bottom 0.2s, right 0.2s, width 0.2s, height 0.2s`,
  2922. width: `0`,
  2923. height: `0`,
  2924. top: 'calc(100% - 60px)',
  2925. left: taskbar_item_pos.left + 29,
  2926. });
  2927. // remove transitions a good while after setting css to make sure
  2928. // it doesn't interfere with an ongoing animation
  2929. setTimeout(() => {
  2930. $(this).css({
  2931. 'transition': 'none',
  2932. 'transform': 'none'
  2933. });
  2934. }, 250);
  2935. // update title and window URL
  2936. window.history.replaceState(null, document.title, '/');
  2937. document.title = 'Puter';
  2938. }
  2939. })
  2940. return this;
  2941. };
  2942. $(document).on('click', '.explore-table-headers-th', function(e){
  2943. let sort_by = 'name';
  2944. let sort_icon = `<img src="${window.icons['up-arrow.svg']}">`;
  2945. // current sort order
  2946. let sort_order = $(e.target).closest('.window').attr('data-sort_order') ?? 'asc';
  2947. // flip sort order
  2948. if(sort_order === 'asc'){
  2949. sort_order = 'desc';
  2950. sort_icon = `<img src="${window.icons['down-arrow.svg']}">`;
  2951. }else if(sort_order === 'desc'){
  2952. sort_icon = `<img src="${window.icons['up-arrow.svg']}">`;
  2953. sort_order = 'asc';
  2954. }
  2955. // remove active class from all headers
  2956. $(e.target).closest('.window').find('.explore-table-headers-th').removeClass('explore-table-headers-th-active');
  2957. // remove icons from all headers
  2958. $(e.target).closest('.window').find('.header-sort-icon').html('');
  2959. // add active class to this header
  2960. $(e.target).addClass('explore-table-headers-th-active');
  2961. // set sort icon
  2962. $(e.target).closest('.window').find('.explore-table-headers-th-active > .header-sort-icon').html(sort_icon);
  2963. // set sort_by
  2964. if($(e.target).hasClass('explore-table-headers-th--name')){
  2965. sort_by = 'name';
  2966. }else if($(e.target).hasClass('explore-table-headers-th--modified')){
  2967. sort_by = 'modified';
  2968. }else if($(e.target).hasClass('explore-table-headers-th--size')){
  2969. sort_by = 'size';
  2970. }else if($(e.target).hasClass('explore-table-headers-th--type')){
  2971. sort_by = 'type';
  2972. }
  2973. // sort
  2974. sort_items($(e.target).closest('.window-body'), sort_by, sort_order);
  2975. set_sort_by($(e.target).closest('.window').attr('data-uid'), sort_by, sort_order)
  2976. })
  2977. window.set_layout = function(item_uid, layout){
  2978. $.ajax({
  2979. url: api_origin + "/set_layout",
  2980. type: 'POST',
  2981. data: JSON.stringify({
  2982. item_uid: item_uid,
  2983. layout: layout,
  2984. }),
  2985. async: true,
  2986. contentType: "application/json",
  2987. headers: {
  2988. "Authorization": "Bearer "+auth_token
  2989. },
  2990. statusCode: {
  2991. 401: function () {
  2992. logout();
  2993. },
  2994. },
  2995. success: function (){
  2996. if(layout === 'details'){
  2997. let el_window = $(`.window[data-uid="${item_uid}"]`);
  2998. if(el_window.length > 0){
  2999. let sort_by = el_window.attr('data-sort_by');
  3000. let sort_order = el_window.attr('data-sort_order');
  3001. update_details_layout_sort_visuals(el_window, sort_by, sort_order);
  3002. }
  3003. }
  3004. }
  3005. })
  3006. }
  3007. window.update_details_layout_sort_visuals = function(el_window, sort_by, sort_order){
  3008. let sort_icon = '';
  3009. $(el_window).find('.explore-table-headers-th > .header-sort-icon').html('');
  3010. if(!sort_order || sort_order === 'asc')
  3011. sort_icon = `<img src="${window.icons['up-arrow.svg']}">`;
  3012. else if(sort_order === 'desc')
  3013. sort_icon = `<img src="${window.icons['down-arrow.svg']}">`;
  3014. if(!sort_by || sort_by === 'name'){
  3015. $(el_window).find('.explore-table-headers-th').removeClass('explore-table-headers-th-active');
  3016. $(el_window).find('.explore-table-headers-th--name').addClass('explore-table-headers-th-active');
  3017. $(el_window).find('.explore-table-headers-th--name > .header-sort-icon').html(sort_icon);
  3018. }else if(sort_by === 'size'){
  3019. $(el_window).find('.explore-table-headers-th').removeClass('explore-table-headers-th-active');
  3020. $(el_window).find('.explore-table-headers-th--size').addClass('explore-table-headers-th-active');
  3021. $(el_window).find('.explore-table-headers-th--size > .header-sort-icon').html(sort_icon);
  3022. }else if(sort_by === 'modified'){
  3023. $(el_window).find('.explore-table-headers-th').removeClass('explore-table-headers-th-active');
  3024. $(el_window).find('.explore-table-headers-th--modified').addClass('explore-table-headers-th-active');
  3025. $(el_window).find('.explore-table-headers-th--modified > .header-sort-icon').html(sort_icon);
  3026. }else if(sort_by === 'type'){
  3027. $(el_window).find('.explore-table-headers-th').removeClass('explore-table-headers-th-active');
  3028. $(el_window).find('.explore-table-headers-th--type').addClass('explore-table-headers-th-active');
  3029. $(el_window).find('.explore-table-headers-th--type > .header-sort-icon').html(sort_icon);
  3030. }
  3031. }
  3032. // This is a hack to fix the issue where the window scrolls to the bottom when an app scrolls.
  3033. // this is due to an issue with iframes being able to hijack the scroll event for the parent object.
  3034. // w3c is working on a fix for this, but it's not ready yet.
  3035. // more info here: https://github.com/w3c/webappsec-permissions-policy/issues/171
  3036. document.addEventListener('scroll', function (event) {
  3037. if($(event.target).hasClass('window-app') || $(event.target).hasClass('window-app-iframe') || $(event.target?.activeElement).hasClass('window-app-iframe')){
  3038. setTimeout(function(){
  3039. // scroll window back to top
  3040. $('.window-app').scrollTop(0);
  3041. // some times it's document that scrolls, so we need to check that too
  3042. $(document).scrollTop(0);
  3043. }, 1);
  3044. }
  3045. }, true);
  3046. export default UIWindow;