UIWindow.js 150 KB

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