UIItem.js 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547
  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 UIWindowPublishWebsite from './UIWindowPublishWebsite.js';
  20. import UIWindowItemProperties from './UIWindowItemProperties.js';
  21. import UIWindowGetCopyLink from './UIWindowGetCopyLink.js';
  22. import UIWindowSaveAccount from './UIWindowSaveAccount.js';
  23. import UIPopover from './UIPopover.js';
  24. import UIWindowEmailConfirmationRequired from './UIWindowEmailConfirmationRequired.js';
  25. import UIContextMenu from './UIContextMenu.js'
  26. import UIAlert from './UIAlert.js'
  27. import path from "../lib/path.js"
  28. function UIItem(options){
  29. const matching_appendto_count = $(options.appendTo).length;
  30. if(matching_appendto_count > 1){
  31. $(options.appendTo).each(function(){
  32. const opts = options;
  33. opts.appendTo = this;
  34. UIItem(opts);
  35. })
  36. return;
  37. }else if(matching_appendto_count === 0){
  38. return;
  39. }
  40. const item_id = global_element_id++;
  41. let last_mousedown_ts = 999999999999999;
  42. let rename_cancelled = false;
  43. // set options defaults
  44. options.disabled = options.disabled ?? false;
  45. options.visible = options.visible ?? 'visible'; // one of 'visible', 'revealed', 'hidden'
  46. options.is_dir = options.is_dir ?? false;
  47. options.is_selected = options.is_selected ?? false;
  48. options.is_shared = options.is_shared ?? false;
  49. options.is_shortcut = options.is_shortcut ?? 0;
  50. options.is_trash = options.is_trash ?? false;
  51. options.metadata = options.metadata ?? '';
  52. options.multiselectable = options.multiselectable ?? true;
  53. options.shortcut_to = options.shortcut_to ?? '';
  54. options.shortcut_to_path = options.shortcut_to_path ?? '';
  55. options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1);
  56. options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false);
  57. const is_shared_with_me = (options.path !== '/'+window.user.username && !options.path.startsWith('/'+window.user.username+'/'));
  58. let website_url = determine_website_url(options.path);
  59. // do a quick check to see if the target parent has any file type restrictions
  60. const appendto_allowed_file_types = $(options.appendTo).attr('data-allowed_file_types')
  61. if(!window.check_fsentry_against_allowed_file_types_string({is_dir: options.is_dir, name:options.name, type:options.type}, appendto_allowed_file_types))
  62. options.disabled = true;
  63. // --------------------------------------------------------
  64. // HTML for Item
  65. // --------------------------------------------------------
  66. let h = '';
  67. h += `<div id="item-${item_id}"
  68. class="item${options.is_selected ? ' item-selected':''} ${options.disabled ? 'item-disabled':''} item-${options.visible}"
  69. data-id="${item_id}"
  70. data-name="${html_encode(options.name)}"
  71. data-metadata="${html_encode(options.metadata)}"
  72. data-uid="${options.uid}"
  73. data-is_dir="${options.is_dir ? 1 : 0}"
  74. data-is_trash="${options.is_trash ? 1 : 0}"
  75. data-has_website="${options.has_website ? 1 : 0 }"
  76. data-website_url = "${website_url ? html_encode(website_url) : ''}"
  77. data-immutable="${options.immutable}"
  78. data-is_shortcut = "${options.is_shortcut}"
  79. data-shortcut_to = "${html_encode(options.shortcut_to)}"
  80. data-shortcut_to_path = "${html_encode(options.shortcut_to_path)}"
  81. data-sortable = "${options.sortable ?? 'true'}"
  82. data-sort_by = "${html_encode(options.sort_by) ?? 'name'}"
  83. data-size = "${options.size ?? ''}"
  84. data-type = "${html_encode(options.type) ?? ''}"
  85. data-modified = "${options.modified ?? ''}"
  86. data-associated_app_name = "${html_encode(options.associated_app_name) ?? ''}"
  87. data-path="${html_encode(options.path)}">`;
  88. // spinner
  89. h += `<div class="item-spinner">`;
  90. h += `</div>`;
  91. // modified
  92. h += `<div class="item-attr item-attr--modified">`;
  93. h += `<span>${options.modified === 0 ? '-' : timeago.format(options.modified*1000)}</span>`;
  94. h += `</div>`;
  95. // size
  96. h += `<div class="item-attr item-attr--size">`;
  97. h += `<span>${options.size ? byte_format(options.size) : '-'}</span>`;
  98. h += `</div>`;
  99. // type
  100. h += `<div class="item-attr item-attr--type">`;
  101. if(options.is_dir)
  102. h += `<span>Folder</span>`;
  103. else
  104. h += `<span>${options.type ? html_encode(options.type) : '-'}</span>`;
  105. h += `</div>`;
  106. // icon
  107. h += `<div class="item-icon">`;
  108. h += `<img src="${html_encode(options.icon.image)}" class="item-icon-${options.icon.type}" data-item-id="${item_id}">`;
  109. h += `</div>`;
  110. // badges
  111. h += `<div class="item-badges">`;
  112. // website badge
  113. h += `<img class="item-badge item-has-website-badge long-hover"
  114. style="${options.has_website ? 'display:block;' : ''}"
  115. src="${html_encode(window.icons['world.svg'])}"
  116. data-item-id="${item_id}"
  117. >`;
  118. // link badge
  119. h += `<img class="item-badge item-has-website-url-badge"
  120. style="${website_url ? 'display:block;' : ''}"
  121. src="${html_encode(window.icons['link.svg'])}"
  122. data-item-id="${item_id}"
  123. >`;
  124. // shared badge
  125. h += `<img class="item-badge item-badge-has-permission"
  126. style="display: ${ is_shared_with_me ? 'block' : 'none'};
  127. background-color: #ffffff;
  128. padding: 2px;" src="${html_encode(window.icons['shared.svg'])}"
  129. data-item-id="${item_id}"
  130. title="A user has shared this item with you.">`;
  131. // owner-shared badge
  132. h += `<img class="item-badge item-is-shared"
  133. style="background-color: #ffffff; padding: 2px; ${!is_shared_with_me && options.is_shared ? 'display:block;' : ''}"
  134. src="${html_encode(window.icons['owner-shared.svg'])}"
  135. data-item-id="${item_id}"
  136. data-item-uid="${options.uid}"
  137. data-item-path="${html_encode(options.path)}"
  138. title="You have shared this item with at least one other user."
  139. >`;
  140. // shortcut badge
  141. h += `<img class="item-badge item-shortcut"
  142. style="background-color: #ffffff; padding: 2px; ${options.is_shortcut !== 0 ? 'display:block;' : ''}"
  143. src="${html_encode(window.icons['shortcut.svg'])}"
  144. data-item-id="${item_id}"
  145. title="Shortcut"
  146. >`;
  147. h += `</div>`;
  148. // name
  149. h += `<span class="item-name" data-item-id="${item_id}" title="${html_encode(options.name)}">${html_encode(truncate_filename(options.name, TRUNCATE_LENGTH)).replaceAll(' ', '&nbsp;')}</span>`
  150. // name editor
  151. h += `<textarea class="item-name-editor hide-scrollbar" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" data-gramm_editor="false">${html_encode(options.name)}</textarea>`
  152. h += `</div>`;
  153. // append to options.appendTo
  154. $(options.appendTo).append(h);
  155. // updte item_container
  156. const item_container = $(options.appendTo).closest('.item-container');
  157. show_or_hide_empty_folder_message(item_container);
  158. // get all the elements needed
  159. const el_item = document.getElementById(`item-${item_id}`);
  160. const el_item_name = document.querySelector(`#item-${item_id} > .item-name`);
  161. const el_item_icon = document.querySelector(`#item-${item_id} .item-icon`);
  162. const el_item_name_editor = document.querySelector(`#item-${item_id} > .item-name-editor`);
  163. const is_trashed = $(el_item).attr('data-path').startsWith(trash_path + '/');
  164. // update parent window's explorer item count if applicable
  165. if(options.appendTo !== undefined){
  166. let el_window = options.appendTo;
  167. if(!$(el_window).hasClass('.window'))
  168. el_window = $(el_window).closest('.window');
  169. update_explorer_footer_item_count(el_window);
  170. }
  171. // position
  172. if(!is_auto_arrange_enabled && options.position && $(el_item).closest('.item-container').attr('data-path') === window.desktop_path){
  173. el_item.style.position = 'absolute';
  174. el_item.style.left = options.position.left + 'px';
  175. el_item.style.top = options.position.top + 'px';
  176. }
  177. // --------------------------------------------------------
  178. // Dragster
  179. // allow dragging of local files on this window, if it's is_dir
  180. // --------------------------------------------------------
  181. if(options.is_dir){
  182. $(el_item).dragster({
  183. enter: function () {
  184. $(el_item).not('.item-disabled').addClass('item-selected');
  185. },
  186. leave: function () {
  187. $(el_item).removeClass('item-selected');
  188. },
  189. drop: function (dragsterEvent, event) {
  190. const e = event.originalEvent;
  191. $(el_item).removeClass('item-selected');
  192. // if files were dropped...
  193. if(e.dataTransfer?.items?.length > 0){
  194. upload_items( e.dataTransfer.items, $(el_item).attr('data-path'))
  195. }
  196. e.stopPropagation();
  197. e.preventDefault();
  198. return false;
  199. }
  200. });
  201. }
  202. // --------------------------------------------------------
  203. // Draggable
  204. // --------------------------------------------------------
  205. let longer_hover_timeout;
  206. let last_window_dragged_over;
  207. $(el_item).draggable({
  208. appendTo: "body",
  209. helper: "clone",
  210. revert: "invalid",
  211. //containment: "document",
  212. zIndex: 10000,
  213. scroll:false,
  214. distance: 5,
  215. revertDuration: 100,
  216. start: function(event, ui) {
  217. // select this item and its helper
  218. $(el_item).addClass('item-selected');
  219. $('.ui-draggable-dragging').addClass('item-selected');
  220. //clone other selected items
  221. $(el_item)
  222. .siblings('.item-selected')
  223. .clone()
  224. .addClass('item-selected-clone')
  225. .css('position', 'absolute')
  226. .appendTo('body')
  227. .hide();
  228. // Bring item and clones to front
  229. $('.item-selected-clone, .ui-draggable-dragging').css('z-index', 99999);
  230. // count badge
  231. const item_count = $('.item-selected-clone').length;
  232. if(item_count > 0){
  233. $('body').append(`<span class="draggable-count-badge">${item_count + 1}</span>`);
  234. }
  235. // Disable all droppable UIItems that are not a dir/app to avoid accidental cancellation
  236. // on Items that are not droppables. In general if an item is dropped on another, if the
  237. // target is not a dir, the source needs to be dropped on the target's container.
  238. $(`.item[data-is_dir="0"][data-associated_app_name=""]:not(.item-selected)`).droppable('disable');
  239. // Disable pointer events on all app iframes. This is needed because as soon as
  240. // a dragging event enters the iframe the event is delegated to iframe which makes the item
  241. // stuck at the edge of the iframe not allowing us to move items freely across the screen
  242. $('.window-app-iframe').css('pointer-events', 'none')
  243. // reset longer hover timeout and last window dragged over
  244. longer_hover_timeout = null;
  245. last_window_dragged_over = null;
  246. },
  247. drag: function(event, ui) {
  248. // Only show drag helpers if the item has been moved more than 5px
  249. if( Math.abs(ui.originalPosition.top - ui.offset.top) > 5
  250. ||
  251. Math.abs(ui.originalPosition.left - ui.offset.left) > 5 ){
  252. $('.ui-draggable-dragging').show();
  253. $('.item-selected-clone').show();
  254. $('.draggable-count-badge').show();
  255. }
  256. const other_selected_items = $('.item-selected-clone');
  257. const item_count = other_selected_items.length + 1;
  258. // Move count badge with mouse
  259. $('.draggable-count-badge').css({
  260. top: event.pageY,
  261. left: event.pageX + 10,
  262. })
  263. // Move other selected items
  264. for(let i=0; i < item_count - 1; i++){
  265. $(other_selected_items[i]).css({
  266. 'left': ui.position.left + 3 * (i+1),
  267. 'top': ui.position.top + 3 * (i+1),
  268. 'z-index': 999 - (i),
  269. 'opacity': 0.5 - i*0.1,
  270. })
  271. }
  272. // remove all item-container active borders
  273. $('.item-container').removeClass('item-container-active');
  274. // if item has changed container, remove timeout for window focus and reset last target
  275. if(longer_hover_timeout && last_window_dragged_over !== window.mouseover_window){
  276. clearTimeout(longer_hover_timeout);
  277. longer_hover_timeout = null;
  278. last_window_dragged_over = window.mouseover_window;
  279. }
  280. // if item hover for more than 1.2s, focus the window
  281. if(!longer_hover_timeout){
  282. longer_hover_timeout = setTimeout(() => {
  283. $(last_window_dragged_over).focusWindow();
  284. }, 1200);
  285. }
  286. // Highlight item container to help user see more clearly where the item is going to be dropped
  287. if($(window.mouseover_item_container).closest('.window').is(window.mouseover_window) &&
  288. // do not highlight if the target is the same as the item being moved
  289. $(el_item).attr('data-path') !== $(window.mouseover_item_container).attr('data-path') &&
  290. // do not highlight if item is being moved to where it already is
  291. $(el_item).attr('data-path') !== $(window.mouseover_item_container).attr('data-path')){
  292. // highlight item container
  293. $(window.mouseover_item_container).addClass('item-container-active');
  294. }
  295. // send drag event to iframe if mouse is inside iframe
  296. if(mouseover_window){
  297. const $app_iframe = $(mouseover_window).find('.window-app-iframe');
  298. if(!$(mouseover_window).hasClass('window-disabled') && $app_iframe.length > 0){
  299. var rect = $app_iframe.get(0).getBoundingClientRect();
  300. // if mouse is inside iframe, send drag message to iframe
  301. if(mouseX > rect.left && mouseX < rect.right && mouseY > rect.top && mouseY < rect.bottom){
  302. $app_iframe.get(0).contentWindow.postMessage({msg: "drag", x: (mouseX - rect.left), y: (mouseY - rect.top)}, '*');
  303. }
  304. }
  305. }
  306. },
  307. stop: function(event, ui){
  308. // Allow rearranging only if item is on desktop, not trash container, auto arrange is disabled and item is not dropped into another item
  309. if($(el_item).closest('.item-container').attr('data-path') === window.desktop_path &&
  310. !is_auto_arrange_enabled && $(el_item).attr('data-path') !== trash_path && !ui.helper.data('dropped')){
  311. el_item.style.position = 'absolute';
  312. el_item.style.left = ui.position.left + 'px';
  313. el_item.style.top = ui.position.top + 'px';
  314. $('.ui-draggable-dragging').remove();
  315. desktop_item_positions[$(el_item).attr('data-uid')] = ui.position;
  316. save_desktop_item_positions()
  317. }
  318. $('.item-selected-clone').remove();
  319. $('.draggable-count-badge').remove();
  320. // re-enable all droppable UIItems that are not a dir
  321. $(`.item[data-is_dir='0']:not(.item-selected)`).droppable('enable');
  322. // remove active item-container border highlights
  323. $('.item-container').removeClass('item-container-active');
  324. // reset longer hover timeout and last window dragged over
  325. clearTimeout(longer_hover_timeout);
  326. last_window_dragged_over = null;
  327. }
  328. });
  329. // --------------------------------------------------------
  330. // Droppable
  331. // --------------------------------------------------------
  332. $(el_item).droppable({
  333. accept: '.item',
  334. // 'pointer' is very important because of active window tracking is based on the position of cursor.
  335. tolerance: 'pointer',
  336. drop: async function( event, ui ) {
  337. // Check if hovering over an item that is VISIBILE
  338. if($(event.target).closest('.window').attr('data-id') !== $(mouseover_window).attr('data-id'))
  339. return;
  340. // If ctrl is pressed and source is Trashed, cancel whole operation
  341. if(event.ctrlKey && path.dirname($(ui.draggable).attr('data-path')) === window.trash_path)
  342. return;
  343. // Adding a flag to know whether item is rearraged or dropped
  344. ui.helper.data('dropped', true);
  345. const items_to_move = []
  346. // First item
  347. items_to_move.push(ui.draggable);
  348. // All subsequent items
  349. const cloned_items = document.getElementsByClassName('item-selected-clone');
  350. for(let i =0; i<cloned_items.length; i++){
  351. const source_item = document.getElementById('item-' + $(cloned_items[i]).attr('data-id'));
  352. if(source_item !== null)
  353. items_to_move.push(source_item);
  354. }
  355. // --------------------------------------------------------
  356. // If dropped on an app, open the app with the dropped
  357. // items as argument
  358. //--------------------------------------------------------
  359. if(options.associated_app_name){
  360. // an array that hold the items to sign
  361. const items_to_open = [];
  362. // prepare items to sign
  363. for(let i=0; i < items_to_move.length; i++){
  364. items_to_open.push({
  365. name: $(items_to_move[i]).attr('data-name'),
  366. uid: $(items_to_move[i]).attr('data-uid'),
  367. action: 'write',
  368. path: $(items_to_move[i]).attr('data-path')
  369. });
  370. }
  371. // open each item
  372. for (let i = 0; i < items_to_open.length; i++) {
  373. const item = items_to_open[i];
  374. launch_app({
  375. name: options.associated_app_name,
  376. file_path: item.path,
  377. // app_obj: open_item_meta.suggested_apps[0],
  378. window_title: item.name,
  379. file_uid: item.uid,
  380. file_signature: item,
  381. });
  382. }
  383. // deselect dragged item
  384. for(let i=0; i < items_to_move.length; i++)
  385. $(items_to_move[i]).removeClass('item-selected');
  386. }
  387. //--------------------------------------------------------
  388. // If dropped on a directory, move items to that directory
  389. //--------------------------------------------------------
  390. else{
  391. // If ctrl key is down, copy items. Except if target or source is Trash
  392. if(event.ctrlKey){
  393. if(options.is_dir && $(el_item).attr('data-path') !== window.trash_path )
  394. copy_items(items_to_move, $(el_item).attr('data-path'))
  395. else if(!options.is_dir)
  396. copy_items(items_to_move, path.dirname($(el_item).attr('data-path')));
  397. }
  398. // If alt key is down, create shortcut items
  399. else if(event.altKey && window.feature_flags.create_shortcut){
  400. items_to_move.forEach((item_to_move) => {
  401. create_shortcut(
  402. path.basename($(item_to_move).attr('data-path')),
  403. $(item_to_move).attr('data-is_dir') === '1',
  404. options.is_dir ? $(el_item).attr('data-path') : path.dirname($(el_item).attr('data-path')),
  405. null,
  406. $(item_to_move).attr('data-shortcut_to') === '' ? $(item_to_move).attr('data-uid') : $(item_to_move).attr('data-shortcut_to'),
  407. $(item_to_move).attr('data-shortcut_to_path') === '' ? $(item_to_move).attr('data-path') : $(item_to_move).attr('data-shortcut_to_path'),
  408. );
  409. });
  410. }
  411. // Otherwise, move items
  412. else if(options.is_dir){
  413. if($(el_item).closest('.item-container').attr('data-path') === window.desktop_path){
  414. delete desktop_item_positions[$(el_item).attr('data-uid')];
  415. save_desktop_item_positions()
  416. }
  417. move_items(items_to_move, $(el_item).attr('data-shortcut_to_path') !== '' ? $(el_item).attr('data-shortcut_to_path') : $(el_item).attr('data-path'));
  418. }
  419. }
  420. // Re-enable droppable on all 'item-container's
  421. $('.item-container').droppable('enable')
  422. return false;
  423. },
  424. over: function(event, ui){
  425. // Check hovering over an item that is VISIBILE
  426. const $event_parent_win = $(event.target).closest('.window')
  427. if( $event_parent_win.length > 0 && $event_parent_win.attr('data-id') !== $(mouseover_window).attr('data-id'))
  428. return;
  429. // Don't do anything if the dragged item is NOT a UIItem
  430. if(!$(ui.draggable).hasClass('item'))
  431. return;
  432. // If this is a directory or an app, and an item was dragged over it, highlight it.
  433. if(options.is_dir || options.associated_app_name){
  434. $(el_item).addClass('item-selected');
  435. $('.ui-draggable-dragging .item-name, .item-selected-clone .item-name').css('opacity', 0.1)
  436. // remove all item-container active borders
  437. $('.item-container').addClass('item-container-transparent-border')
  438. }
  439. // Disable all window bodies
  440. $('.item-container').droppable( 'disable' )
  441. },
  442. out: function(event, ui){
  443. // Don't do anything if the dragged item is NOT a UIItem
  444. if(!$(ui.draggable).hasClass('item'))
  445. return;
  446. // Unselect directory/app if item is dragged out
  447. if(options.is_dir || options.associated_app_name){
  448. $(el_item).removeClass('item-selected');
  449. $('.ui-draggable-dragging .item-name, .item-selected-clone .item-name').css('opacity', 'initial')
  450. $('.item-container').removeClass('item-container-transparent-border')
  451. }
  452. $('.item-container').droppable( 'enable' )
  453. }
  454. });
  455. // --------------------------------------------------------
  456. // Double Click/Single Tap on Item
  457. // --------------------------------------------------------
  458. if(isMobile.phone || isMobile.tablet){
  459. $(el_item).on('click', async function (e) {
  460. // if item is disabled, do not allow any action
  461. if($(el_item).hasClass('item-disabled'))
  462. return false;
  463. if($(e.target).hasClass('item-name-editor'))
  464. return false;
  465. open_item({
  466. item: el_item,
  467. maximized: true,
  468. });
  469. });
  470. }else{
  471. $(el_item).on('dblclick', async function (e) {
  472. // if item is disabled, do not allow any action
  473. if($(el_item).hasClass('item-disabled'))
  474. return false;
  475. if($(e.target).hasClass('item-name-editor'))
  476. return false;
  477. open_item({
  478. item: el_item,
  479. new_window: e.metaKey || e.ctrlKey,
  480. });
  481. });
  482. }
  483. // --------------------------------------------------------
  484. // Mousedown
  485. // --------------------------------------------------------
  486. $(el_item).on('mousedown', function (e) {
  487. // if item is disabled, do not allow any action
  488. if($(el_item).hasClass('item-disabled'))
  489. return false;
  490. // if link badge is clicked, don't continue
  491. if($(e.target).hasClass('item-has-website-url-badge'))
  492. return false;
  493. const $el_parent_window = $(el_item).closest('.window');
  494. // first see if this is a ContextMenu call on multiple items
  495. if(e.which === 3 && $(el_item).hasClass('item-selected') && $(el_item).siblings('.item-selected').length > 0){
  496. $(".context-menu").remove();
  497. return false;
  498. }
  499. // unselect other items if neither CTRL nor Command key are held
  500. // or
  501. // if parent is not multiselectable
  502. if((!e.ctrlKey && !e.metaKey && !$(this).hasClass('item-selected')) || ($el_parent_window.length>0 && $el_parent_window.attr('data-multiselectable') !== 'true')){
  503. $(this).closest('.item-container').find('.item-selected').removeClass('item-selected');
  504. }
  505. if((e.ctrlKey || e.metaKey) && $(this).hasClass('item-selected')){
  506. $(this).removeClass('item-selected')
  507. }
  508. else{
  509. $(this).addClass('item-selected')
  510. }
  511. update_explorer_footer_selected_items_count($el_parent_window)
  512. });
  513. // --------------------------------------------------------
  514. // Click
  515. // --------------------------------------------------------
  516. $(el_item).on('click', function (e) {
  517. // if item is disabled, do not allow any action
  518. if($(el_item).hasClass('item-disabled'))
  519. return false;
  520. skip_a_rename_click = false;
  521. const $el_parent_window = $(el_item).closest('.window');
  522. // do not unselect other items if:
  523. // CTRL/Command key is pressed or clicking an item that is already selected
  524. if(!e.ctrlKey && !e.metaKey){
  525. $(this).closest('.item-container').find('.item-selected').not(this).removeClass('item-selected');
  526. update_explorer_footer_selected_items_count($el_parent_window)
  527. }
  528. //----------------------------------------------------------------
  529. // On an OpenFileDialog?
  530. //----------------------------------------------------------------
  531. if($el_parent_window.attr('data-is_openFileDialog') === 'true'){
  532. if(!options.is_dir)
  533. $el_parent_window.find('.openfiledialog-open-btn').removeClass('disabled');
  534. else
  535. $el_parent_window.find('.openfiledialog-open-btn').addClass('disabled');
  536. }
  537. //----------------------------------------------------------------
  538. // On a SaveFileDialog?
  539. //----------------------------------------------------------------
  540. if($el_parent_window.attr('data-is_saveFileDialog') === 'true' && !options.is_dir){
  541. $el_parent_window.find('.savefiledialog-filename').val($(el_item).attr('data-name'));
  542. $el_parent_window.find('.savefiledialog-save-btn').removeClass('disabled');
  543. }
  544. });
  545. $(document).on('click', function(e){
  546. if(!$(e.target).hasClass('item') && !$(e.target).hasClass('item-name') && !$(e.target).hasClass('item-icon')){
  547. skip_a_rename_click = true;
  548. }
  549. if($(e.target).parents('.item').data('id') !== item_id){
  550. skip_a_rename_click = true;
  551. }
  552. })
  553. // --------------------------------------------------------
  554. // Rename
  555. // --------------------------------------------------------
  556. function rename(){
  557. if(rename_cancelled){
  558. rename_cancelled = false;
  559. return;
  560. }
  561. const old_name = $(el_item).attr('data-name');
  562. const old_path = $(el_item).attr('data-path');
  563. const new_name = $(el_item_name_editor).val();
  564. // Don't send a rename request if:
  565. // the new name is the same as the old one,
  566. // or it's empty,
  567. // or editable was not even active at all
  568. if(old_name === new_name || !new_name || new_name === '.' || new_name === '..' || !$(el_item_name_editor).hasClass('item-name-editor-active')){
  569. if(new_name === '.'){
  570. UIAlert(`The name "." is not allowed, because it is a reserved name. Please choose another name.`);
  571. }
  572. else if(new_name === '..'){
  573. UIAlert(`The name ".." is not allowed, because it is a reserved name. Please choose another name.`)
  574. }
  575. $(el_item_name).html(truncate_filename(options.name, TRUNCATE_LENGTH).replaceAll(' ', '&nbsp;'));
  576. $(el_item_name).show();
  577. $(el_item_name_editor).val($(el_item).attr('data-name'));
  578. $(el_item_name_editor).hide();
  579. return;
  580. }
  581. // deactivate item name editable
  582. $(el_item_name_editor).removeClass('item-name-editor-active');
  583. // Perform rename request
  584. rename_file(options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url);
  585. }
  586. // --------------------------------------------------------
  587. // Rename if enter pressed on Item Name Editor
  588. // --------------------------------------------------------
  589. $(el_item_name_editor).on('keypress',function(e) {
  590. // If name editor is not active don't continue
  591. if(!$(el_item_name_editor).is(":visible"))
  592. return;
  593. // Enter key = rename
  594. if(e.which === 13) {
  595. e.stopPropagation();
  596. e.preventDefault();
  597. $(el_item_name_editor).blur();
  598. $(el_item).addClass('item-selected');
  599. last_enter_pressed_to_rename_ts = Date.now();
  600. update_explorer_footer_selected_items_count($(el_item).closest('.item-container'));
  601. return false;
  602. }
  603. })
  604. // --------------------------------------------------------
  605. // Cancel and undo if escape pressed on Item Name Editor
  606. // --------------------------------------------------------
  607. $(el_item_name_editor).on('keyup',function(e) {
  608. if(!$(el_item_name_editor).is(":visible"))
  609. return;
  610. // Escape = undo rename
  611. else if(e.which === 27){
  612. e.stopPropagation();
  613. e.preventDefault();
  614. rename_cancelled = true;
  615. $(el_item_name_editor).hide();
  616. $(el_item_name_editor).val(options.name);
  617. $(el_item_name).show();
  618. }
  619. });
  620. $(el_item_name_editor).on('focusout',function(e) {
  621. e.stopPropagation();
  622. e.preventDefault();
  623. rename();
  624. });
  625. /************************************************
  626. * Takes care of 'click to edit item name'
  627. ************************************************/
  628. let skip_a_rename_click = true;
  629. $(el_item_name).on('click', function(e){
  630. if( !skip_a_rename_click && e.which !== 3 && $(el_item_name).parent('.item-selected').length > 0){
  631. last_mousedown_ts = Date.now();
  632. setTimeout(() => {
  633. if(!skip_a_rename_click && (Date.now() - last_mousedown_ts) > 400){
  634. if (!e.ctrlKey && !e.metaKey)
  635. activate_item_name_editor(el_item)
  636. last_mousedown_ts = 0
  637. }else{
  638. last_mousedown_ts = Date.now() + 500;
  639. skip_a_rename_click= false;
  640. }
  641. }, 500);
  642. }
  643. skip_a_rename_click = false;
  644. })
  645. $(el_item_name).on('dblclick', function(e){
  646. skip_a_rename_click = true;
  647. })
  648. // --------------------------------------------------------
  649. // ContextMenu
  650. // --------------------------------------------------------
  651. $(el_item).bind("contextmenu taphold", async function (event) {
  652. // if item is disabled, do not allow any action
  653. if($(el_item).hasClass('item-disabled'))
  654. return false;
  655. // if on website link badge, don't continue
  656. if($(event.target).hasClass('item-has-website-url-badge'))
  657. return false;
  658. // dimiss taphold on regular devices
  659. if(event.type==='taphold' && !isMobile.phone && !isMobile.tablet)
  660. return;
  661. // if editing item name, preserve native context menu
  662. if(event.target === el_item_name_editor)
  663. return;
  664. // if ctrl is pressed don't open ctxmenu, ctrl is for drag and copy
  665. if(event.ctrlKey)
  666. return false;
  667. event.preventDefault();
  668. let menu_items;
  669. const $selected_items = $(el_item).closest('.item-container').find('.item-selected').not(el_item).addBack();
  670. // -------------------------------------------------------
  671. // Multiple items selected
  672. // -------------------------------------------------------
  673. if($selected_items.length > 1){
  674. const are_trashed = $selected_items.attr('data-path').startsWith(trash_path + '/');
  675. menu_items = []
  676. // -------------------------------------------
  677. // Restore
  678. // -------------------------------------------
  679. if(are_trashed){
  680. menu_items.push({
  681. html: i18n('restore'),
  682. onClick: function(){
  683. $selected_items.each(function() {
  684. const ell = this;
  685. let metadata = $(ell).attr('data-metadata') === '' ? {} : JSON.parse($(ell).attr('data-metadata'))
  686. move_items([ell], path.dirname(metadata.original_path));
  687. })
  688. }
  689. });
  690. // -------------------------------------------
  691. // -
  692. // -------------------------------------------
  693. menu_items.push('-');
  694. }
  695. if(!are_trashed){
  696. // -------------------------------------------
  697. // Donwload
  698. // -------------------------------------------
  699. menu_items.push({
  700. html: i18n('Download'),
  701. onClick: async function(){
  702. let items = [];
  703. for (let index = 0; index < $selected_items.length; index++) {
  704. items.push($selected_items[index]);
  705. }
  706. zipItems(items, path.dirname($(el_item).attr('data-path')), true);
  707. }
  708. });
  709. // -------------------------------------------
  710. // Zip
  711. // -------------------------------------------
  712. menu_items.push({
  713. html: i18n('zip'),
  714. onClick: async function(){
  715. let items = [];
  716. for (let index = 0; index < $selected_items.length; index++) {
  717. items.push($selected_items[index]);
  718. }
  719. zipItems(items, path.dirname($(el_item).attr('data-path')), false);
  720. }
  721. });
  722. // -------------------------------------------
  723. // -
  724. // -------------------------------------------
  725. menu_items.push('-');
  726. }
  727. // -------------------------------------------
  728. // Cut
  729. // -------------------------------------------
  730. menu_items.push({
  731. html: i18n('cut'),
  732. onClick: function(){
  733. window.clipboard_op= 'move';
  734. window.clipboard = [];
  735. $selected_items.each(function() {
  736. const ell = this;
  737. window.clipboard.push($(ell).attr('data-path'));
  738. })
  739. }
  740. });
  741. // -------------------------------------------
  742. // Copy
  743. // -------------------------------------------
  744. if(!are_trashed){
  745. menu_items.push({
  746. html: i18n('copy'),
  747. onClick: function(){
  748. window.clipboard_op= 'copy';
  749. window.clipboard = [];
  750. $selected_items.each(function() {
  751. const ell = this;
  752. window.clipboard.push({path: $(ell).attr('data-path')});
  753. })
  754. }
  755. });
  756. }
  757. // -------------------------------------------
  758. // -
  759. // -------------------------------------------
  760. menu_items.push('-');
  761. // -------------------------------------------
  762. // Delete Permanently
  763. // -------------------------------------------
  764. if(are_trashed){
  765. menu_items.push({
  766. html: i18n('delete_permanently'),
  767. onClick: async function(){
  768. const alert_resp = await UIAlert({
  769. message: `Are you sure you want to permanently delete these items?`,
  770. buttons:[
  771. {
  772. label: 'Delete',
  773. type: 'primary',
  774. },
  775. {
  776. label: 'Cancel'
  777. },
  778. ]
  779. })
  780. if((alert_resp) === 'Delete'){
  781. for (let index = 0; index < $selected_items.length; index++) {
  782. const element = $selected_items[index];
  783. await delete_item(element);
  784. }
  785. const trash = await puter.fs.stat(trash_path);
  786. // update other clients
  787. if(window.socket){
  788. window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
  789. }
  790. if(trash.is_empty){
  791. $(`.item[data-path="${html_encode(trash_path)}" i], .item[data-shortcut_to_path="${trash_path}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
  792. $(`.window[data-path="${html_encode(trash_path)}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
  793. }
  794. }
  795. }
  796. });
  797. }
  798. // -------------------------------------------
  799. // Create Shortcut
  800. // -------------------------------------------
  801. if(!are_trashed && window.feature_flags.create_shortcut){
  802. menu_items.push({
  803. html: i18n('create_shortcut'),
  804. onClick: async function(){
  805. $selected_items.each(function() {
  806. let base_dir = path.dirname($(this).attr('data-path'));
  807. // Trash on Desktop is a special case
  808. if($(this).attr('data-path') && $(this).closest('.item-container').attr('data-path') === window.desktop_path){
  809. base_dir = window.desktop_path;
  810. }
  811. // create shortcut
  812. create_shortcut(
  813. path.basename($(this).attr('data-path')),
  814. $(this).attr('data-is_dir') === '1',
  815. base_dir,
  816. $(this).closest('.item-container'),
  817. $(this).attr('data-shortcut_to') === '' ? $(this).attr('data-uid') : $(this).attr('data-shortcut_to'),
  818. $(this).attr('data-shortcut_to_path') === '' ? $(this).attr('data-path') : $(this).attr('data-shortcut_to_path'),
  819. );
  820. })
  821. }
  822. });
  823. }
  824. // -------------------------------------------
  825. // Delete
  826. // -------------------------------------------
  827. if(!are_trashed){
  828. menu_items.push({
  829. html: i18n('delete'),
  830. onClick: async function(){
  831. move_items($selected_items, trash_path);
  832. }
  833. });
  834. }
  835. }
  836. // -------------------------------------------------------
  837. // One item selected
  838. // -------------------------------------------------------
  839. else{
  840. const is_trash = $(el_item).attr('data-path') === trash_path || $(el_item).attr('data-shortcut_to_path') === trash_path;
  841. menu_items = [];
  842. // -------------------------------------------
  843. // Open
  844. // -------------------------------------------
  845. if(!is_trashed){
  846. menu_items.push({
  847. html: i18n('open'),
  848. onClick: function(){
  849. open_item({item: el_item});
  850. }
  851. });
  852. // -------------------------------------------
  853. // -
  854. // -------------------------------------------
  855. if(options.associated_app_name || is_trash)
  856. menu_items.push('-');
  857. }
  858. // -------------------------------------------
  859. // Open With
  860. // -------------------------------------------
  861. if(!is_trashed && !is_trash && (options.associated_app_name === null || options.associated_app_name === undefined)){
  862. let items = [];
  863. if(!options.suggested_apps || options.suggested_apps.length === 0){
  864. // try to find suitable apps
  865. const suitable_apps = await suggest_apps_for_fsentry({
  866. uid: options.uid,
  867. path: options.path,
  868. });
  869. if(suitable_apps && suitable_apps.length > 0){
  870. options.suggested_apps = suitable_apps;
  871. }
  872. }
  873. if(options.suggested_apps && options.suggested_apps.length > 0){
  874. for (let index = 0; index < options.suggested_apps.length; index++) {
  875. const suggested_app = options.suggested_apps[index];
  876. if ( ! suggested_app ) {
  877. console.warn(`suggested_app is null`, options.suggested_apps, index);
  878. continue;
  879. }
  880. items.push({
  881. html: suggested_app.title,
  882. icon: `<img src="${html_encode(suggested_app.icon ?? window.icons['app.svg'])}" style="width:16px; height: 16px; margin-bottom: -4px;">`,
  883. onClick: async function(){
  884. launch_app({
  885. name: suggested_app.name,
  886. file_path: $(el_item).attr('data-path'),
  887. window_title: $(el_item).attr('data-name'),
  888. file_uid: $(el_item).attr('data-uid'),
  889. });
  890. }
  891. })
  892. }
  893. }else{
  894. items.push({
  895. html: 'No suitable apps found',
  896. disabled: true,
  897. });
  898. }
  899. // add all suitable apps
  900. menu_items.push({
  901. html: i18n('open_with'),
  902. items: items,
  903. });
  904. // -------------------------------------------
  905. // -- separator --
  906. // -------------------------------------------
  907. menu_items.push('-');
  908. }
  909. // -------------------------------------------
  910. // Open in New Window
  911. // (only if the item is on a window)
  912. // -------------------------------------------
  913. if($(el_item).closest('.window-body').length > 0 && options.is_dir){
  914. menu_items.push({
  915. html: i18n('open_in_new_window'),
  916. onClick: function(){
  917. if(options.is_dir){
  918. open_item({item: el_item, new_window: true})
  919. }
  920. }
  921. });
  922. // -------------------------------------------
  923. // -- separator --
  924. // -------------------------------------------
  925. if(!is_trash && !is_trashed && options.is_dir)
  926. menu_items.push('-');
  927. }
  928. // -------------------------------------------
  929. // Publish As Website
  930. // -------------------------------------------
  931. if(!is_trashed && !is_trash && options.is_dir){
  932. menu_items.push({
  933. html: i18n('publish_as_website'),
  934. disabled: !options.is_dir,
  935. onClick: async function () {
  936. if(window.require_email_verification_to_publish_website){
  937. if(window.user.is_temp &&
  938. !await UIWindowSaveAccount({
  939. send_confirmation_code: true,
  940. message: 'Please create an account to proceed.',
  941. window_options: {
  942. backdrop: true,
  943. close_on_backdrop_click: false,
  944. }
  945. }))
  946. return;
  947. else if(!window.user.email_confirmed && !await UIWindowEmailConfirmationRequired())
  948. return;
  949. }
  950. UIWindowPublishWebsite(options.uid, $(el_item).attr('data-name'), $(el_item).attr('data-path'));
  951. }
  952. });
  953. }
  954. // -------------------------------------------
  955. // Deploy As App
  956. // -------------------------------------------
  957. if(!is_trashed && !is_trash && options.is_dir){
  958. menu_items.push({
  959. html: i18n('deploy_as_app'),
  960. disabled: !options.is_dir,
  961. onClick: async function () {
  962. launch_app({
  963. name: 'dev-center',
  964. file_path: $(el_item).attr('data-path'),
  965. file_uid: $(el_item).attr('data-uid'),
  966. params: {
  967. source_path: options.path,
  968. }
  969. })
  970. }
  971. });
  972. menu_items.push('-');
  973. }
  974. // -------------------------------------------
  975. // Empty Trash
  976. // -------------------------------------------
  977. if(is_trash){
  978. menu_items.push({
  979. html: i18n('empty_trash'),
  980. onClick: async function(){
  981. empty_trash();
  982. }
  983. });
  984. }
  985. // -------------------------------------------
  986. // Donwload
  987. // -------------------------------------------
  988. if(!is_trash && !is_trashed && (options.associated_app_name === null || options.associated_app_name === undefined)){
  989. menu_items.push({
  990. html: i18n('Download'),
  991. disabled: options.is_dir && !window.feature_flags.download_directory,
  992. onClick: async function(){
  993. if(options.is_dir)
  994. zipItems(el_item, path.dirname($(el_item).attr('data-path')), true);
  995. else
  996. trigger_download([options.path]);
  997. }
  998. });
  999. }
  1000. // -------------------------------------------
  1001. // Get Copy Link
  1002. // -------------------------------------------
  1003. if(!is_trashed && !is_trash && (options.associated_app_name === null || options.associated_app_name === undefined)){
  1004. menu_items.push({
  1005. html: i18n('get_copy_link'),
  1006. onClick: async function(){
  1007. if(window.user.is_temp &&
  1008. !await UIWindowSaveAccount({
  1009. message: i18n('save_account_to_get_copy_link'),
  1010. send_confirmation_code: true,
  1011. window_options: {
  1012. backdrop: true,
  1013. close_on_backdrop_click: false,
  1014. }
  1015. }))
  1016. return;
  1017. else if(!window.user.email_confirmed && !await UIWindowEmailConfirmationRequired())
  1018. return;
  1019. UIWindowGetCopyLink({
  1020. name: $(el_item).attr('data-name'),
  1021. uid: $(el_item).attr('data-uid'),
  1022. path: $(el_item).attr('data-path'),
  1023. is_dir: options.is_dir,
  1024. });
  1025. }
  1026. });
  1027. }
  1028. // -------------------------------------------
  1029. // Zip
  1030. // -------------------------------------------
  1031. if(!is_trash && !is_trashed && !$(el_item).attr('data-path').endsWith('.zip')){
  1032. menu_items.push({
  1033. html: i18n('zip'),
  1034. onClick: function(){
  1035. zipItems(el_item, path.dirname($(el_item).attr('data-path')), false);
  1036. }
  1037. })
  1038. }
  1039. // -------------------------------------------
  1040. // Unzip
  1041. // -------------------------------------------
  1042. if(!is_trash && !is_trashed && $(el_item).attr('data-path').endsWith('.zip')){
  1043. menu_items.push({
  1044. html: i18n('unzip'),
  1045. onClick: async function(){
  1046. const zip = new JSZip();
  1047. let filPath = $(el_item).attr('data-path');
  1048. let file = puter.fs.read($(el_item).attr('data-path'));
  1049. zip.loadAsync(file).then(async function (zip) {
  1050. const rootdir = await puter.fs.mkdir(path.dirname(filPath) + '/' + path.basename(filPath, '.zip'), {dedupeName: true});
  1051. Object.keys(zip.files).forEach(async function (filename) {
  1052. if(filename.endsWith('/'))
  1053. await puter.fs.mkdir(rootdir.path +'/' + filename, {createMissingParents: true});
  1054. zip.files[filename].async('blob').then(async function (fileData) {
  1055. await puter.fs.write(rootdir.path +'/' + filename, fileData);
  1056. }).catch(function (e) {
  1057. // UIAlert(e.message);
  1058. })
  1059. })
  1060. }).catch(function (e) {
  1061. // UIAlert(e.message);
  1062. })
  1063. }
  1064. })
  1065. }
  1066. // -------------------------------------------
  1067. // Restore
  1068. // -------------------------------------------
  1069. if(is_trashed){
  1070. menu_items.push({
  1071. html: i18n('restore'),
  1072. onClick: async function(){
  1073. let metadata = $(el_item).attr('data-metadata') === '' ? {} : JSON.parse($(el_item).attr('data-metadata'))
  1074. move_items([el_item], path.dirname(metadata.original_path));
  1075. }
  1076. });
  1077. }
  1078. // -------------------------------------------
  1079. // -
  1080. // -------------------------------------------
  1081. if(!is_trash && (options.associated_app_name === null || options.associated_app_name === undefined))
  1082. menu_items.push('-');
  1083. // -------------------------------------------
  1084. // Cut
  1085. // -------------------------------------------
  1086. if($(el_item).attr('data-immutable') === '0'){
  1087. menu_items.push({
  1088. html: i18n('cut'),
  1089. onClick: function(){
  1090. window.clipboard_op= 'move';
  1091. window.clipboard= [options.path];
  1092. }
  1093. });
  1094. }
  1095. // -------------------------------------------
  1096. // Copy
  1097. // -------------------------------------------
  1098. if(!is_trashed && !is_trash){
  1099. menu_items.push({
  1100. html: i18n('copy'),
  1101. onClick: function(){
  1102. window.clipboard_op= 'copy';
  1103. window.clipboard= [{path: options.path}];
  1104. }
  1105. });
  1106. }
  1107. // -------------------------------------------
  1108. // Paste Into Folder
  1109. // -------------------------------------------
  1110. if($(el_item).attr('data-is_dir') === '1' && !is_trashed && !is_trash){
  1111. menu_items.push({
  1112. html: i18n('paste_into_folder'),
  1113. disabled: clipboard.length > 0 ? false : true,
  1114. onClick: function(){
  1115. if(clipboard_op === 'copy')
  1116. copy_clipboard_items($(el_item).attr('data-path'), null);
  1117. else if(clipboard_op === 'move')
  1118. move_clipboard_items(null, $(el_item).attr('data-path'))
  1119. }
  1120. })
  1121. }
  1122. // -------------------------------------------
  1123. // -
  1124. // -------------------------------------------
  1125. if($(el_item).attr('data-immutable') === '0' && !is_trash){
  1126. menu_items.push('-')
  1127. }
  1128. // -------------------------------------------
  1129. // Create Shortcut
  1130. // -------------------------------------------
  1131. if(!is_trashed && window.feature_flags.create_shortcut){
  1132. menu_items.push({
  1133. html: i18n('create_shortcut'),
  1134. onClick: async function(){
  1135. let base_dir = path.dirname($(el_item).attr('data-path'));
  1136. // Trash on Desktop is a special case
  1137. if($(el_item).attr('data-path') && $(el_item).closest('.item-container').attr('data-path') === window.desktop_path){
  1138. base_dir = window.desktop_path;
  1139. }
  1140. create_shortcut(
  1141. path.basename($(el_item).attr('data-path')),
  1142. options.is_dir,
  1143. base_dir,
  1144. options.appendTo,
  1145. options.shortcut_to === '' ? options.uid : options.shortcut_to,
  1146. options.shortcut_to_path === '' ? options.path : options.shortcut_to_path,
  1147. );
  1148. }
  1149. });
  1150. }
  1151. // -------------------------------------------
  1152. // Delete
  1153. // -------------------------------------------
  1154. if($(el_item).attr('data-immutable') === '0' && !is_trashed){
  1155. menu_items.push({
  1156. html: i18n('delete'),
  1157. onClick: async function(){
  1158. move_items([el_item], trash_path);
  1159. }
  1160. });
  1161. }
  1162. // -------------------------------------------
  1163. // Delete Permanently
  1164. // -------------------------------------------
  1165. if(is_trashed){
  1166. menu_items.push({
  1167. html: i18n('delete_permanently'),
  1168. onClick: async function(){
  1169. const alert_resp = await UIAlert({
  1170. message: `Are you sure you want to permanently delete this item?`,
  1171. buttons:[
  1172. {
  1173. label: 'Delete',
  1174. type: 'primary',
  1175. },
  1176. {
  1177. label: 'Cancel'
  1178. },
  1179. ]
  1180. })
  1181. if((alert_resp) === 'Delete'){
  1182. await delete_item(el_item);
  1183. // check if trash is empty
  1184. const trash = await puter.fs.stat(trash_path);
  1185. // update other clients
  1186. if(window.socket){
  1187. window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
  1188. }
  1189. // update this client
  1190. if(trash.is_empty){
  1191. $(`.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']);
  1192. $(`.window[data-path="${trash_path}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
  1193. }
  1194. }
  1195. }
  1196. });
  1197. }
  1198. // -------------------------------------------
  1199. // Rename
  1200. // -------------------------------------------
  1201. if($(el_item).attr('data-immutable') === '0' && !is_trashed && !is_trash){
  1202. menu_items.push({
  1203. html: i18n('rename'),
  1204. onClick: function(){
  1205. activate_item_name_editor(el_item)
  1206. }
  1207. });
  1208. }
  1209. // -------------------------------------------
  1210. // -
  1211. // -------------------------------------------
  1212. menu_items.push('-');
  1213. // -------------------------------------------
  1214. // Properties
  1215. // -------------------------------------------
  1216. menu_items.push({
  1217. html: i18n('properties'),
  1218. onClick: function(){
  1219. let window_height = 500;
  1220. let window_width = 450;
  1221. let left = $(el_item).position().left + $(el_item).width();
  1222. left = left > (window.innerWidth - window_width)? (window.innerWidth - window_width) : left;
  1223. let top = $(el_item).position().top + $(el_item).height();
  1224. top = top > (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height))? (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) : top;
  1225. UIWindowItemProperties(
  1226. $(el_item).attr('data-name'),
  1227. $(el_item).attr('data-path'),
  1228. $(el_item).attr('data-uid'),
  1229. left,
  1230. top,
  1231. window_width,
  1232. window_height,
  1233. );
  1234. }
  1235. });
  1236. }
  1237. // Create ContextMenu
  1238. UIContextMenu({
  1239. parent_element: ($(options.appendTo).hasClass('desktop') ? undefined : options.appendTo),
  1240. items: menu_items
  1241. });
  1242. return false
  1243. })
  1244. // --------------------------------------------------------
  1245. // Resize Item Name Editor on every keystroke
  1246. // --------------------------------------------------------
  1247. $(el_item_name_editor).on('input keypress focus', function(){
  1248. const val = $(el_item_name_editor).val();
  1249. $('.item-name-shadow').html(html_encode(val).replaceAll(' ', '&nbsp;'));
  1250. if(val !== ''){
  1251. const w = $('.item-name-shadow').width();
  1252. const h = $('.item-name-shadow').height();
  1253. $(el_item_name_editor).width(w + 4)
  1254. $(el_item_name_editor).height(h + 2)
  1255. }
  1256. })
  1257. if(options.sort_container_after_append){
  1258. sort_items(options.appendTo, $(el_item).closest('.item-container').attr('data-sort_by'), $(el_item).closest('.item-container').attr('data-sort_order'));
  1259. }
  1260. if(options.editable){
  1261. activate_item_name_editor(el_item)
  1262. }
  1263. }
  1264. // Create item-name-shadow
  1265. // This element has the exact styling as item name editor and allows us
  1266. // to measure the width and height of the item name editor and automatically
  1267. // resize it to fit the text.
  1268. $('body').append(`<span class="item-name-shadow"></span>`);
  1269. $(document).on('click', '.item-has-website-url-badge', async function(e){
  1270. e.stopPropagation();
  1271. e.preventDefault();
  1272. const website_url = $(this).closest('.item').attr('data-website_url');
  1273. if(website_url){
  1274. window.open(website_url, '_blank');
  1275. }
  1276. return false;
  1277. })
  1278. $(document).on('mousedown', '.item-has-website-url-badge', async function(e){
  1279. console.log('mousedown')
  1280. e.stopPropagation();
  1281. e.preventDefault();
  1282. return false;
  1283. })
  1284. $(document).on('contextmenu', '.item-has-website-url-badge', async function(e){
  1285. e.stopPropagation();
  1286. e.preventDefault();
  1287. // close other context menus
  1288. const $ctxmenus = $(".context-menu");
  1289. $ctxmenus.fadeOut(200, function(){
  1290. $ctxmenus.remove();
  1291. });
  1292. UIContextMenu({
  1293. parent_element: this,
  1294. items: [
  1295. // Open
  1296. {
  1297. html: `${i18n('open_in_new_tab')} <img src="${window.icons['launch.svg']}" style="width:10px; height:10px; margin-left: 5px;">` ,
  1298. html_active: `${i18n('open_in_new_tab')} <img src="${window.icons['launch-white.svg']}" style="width:10px; height:10px; margin-left: 5px;">` ,
  1299. onClick: function(){
  1300. const website_url = $(e.target).closest('.item').attr('data-website_url');
  1301. if(website_url){
  1302. window.open(website_url, '_blank');
  1303. }
  1304. }
  1305. },
  1306. // Copy Link
  1307. {
  1308. html: i18n('copy_link'),
  1309. onClick: async function(){
  1310. const website_url = $(e.target).closest('.item').attr('data-website_url');
  1311. if(website_url){
  1312. await copy_to_clipboard(website_url);
  1313. }
  1314. }
  1315. },
  1316. ]
  1317. });
  1318. return false;
  1319. })
  1320. $(document).on('click', '.item-has-website-badge', async function(e){
  1321. puter.fs.stat({
  1322. uid: $(this).closest('.item').attr('data-uid'),
  1323. returnSubdomains: true,
  1324. returnPermissions: false,
  1325. returnVersions: false,
  1326. success: function (fsentry){
  1327. if(fsentry.subdomains)
  1328. window.open(fsentry.subdomains[0].address, '_blank');
  1329. }
  1330. })
  1331. })
  1332. $(document).on('long-hover', '.item-has-website-badge', function(e){
  1333. puter.fs.stat({
  1334. uid: $(this).closest('.item').attr('data-uid'),
  1335. returnSubdomains: true,
  1336. returnPermissions: false,
  1337. returnVersions: false,
  1338. success: function (fsentry){
  1339. var box = e.target.getBoundingClientRect();
  1340. var body = document.body;
  1341. var docEl = document.documentElement;
  1342. var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
  1343. var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
  1344. var clientTop = docEl.clientTop || body.clientTop || 0;
  1345. var clientLeft = docEl.clientLeft || body.clientLeft || 0;
  1346. var top = box.top + scrollTop - clientTop;
  1347. var left = box.left + scrollLeft - clientLeft;
  1348. if(fsentry.subdomains){
  1349. let h = `<div class="allow-user-select website-badge-popover-content">`;
  1350. h += `<div class="website-badge-popover-title">Associated website${ fsentry.subdomains.length > 1 ? 's':''}</div>`;
  1351. fsentry.subdomains.forEach(subdomain => {
  1352. h += `
  1353. <a class="website-badge-popover-link" href="${subdomain.address}" style="font-size:13px;" target="_blank">${subdomain.address.replace('https://', '')}</a>
  1354. <br>`;
  1355. });
  1356. h += `</div>`;
  1357. // close other website popovers
  1358. $('.website-badge-popover-content').closest('.popover').remove();
  1359. // show a UIPopover with the website
  1360. UIPopover({
  1361. target: e.target,
  1362. content:h,
  1363. snapToElement: e.target,
  1364. parent_element: e.target,
  1365. top: top - 30,
  1366. left: left + 20,
  1367. })
  1368. }
  1369. }
  1370. })
  1371. })
  1372. $(document).on('click', '.website-badge-popover-link', function(e){
  1373. // remove the parent popover
  1374. $(e.target).closest('.popover').remove();
  1375. })
  1376. // removes item(s)
  1377. $.fn.removeItems = async function(options) {
  1378. options = options || {};
  1379. $(this).each(async function() {
  1380. const parent_container = $(this).closest('.item-container');
  1381. $(this).remove();
  1382. show_or_hide_empty_folder_message(parent_container);
  1383. });
  1384. return this;
  1385. }
  1386. window.activate_item_name_editor= function(el_item){
  1387. // files in trash cannot be renamed, the user should be notified with an Alert.
  1388. if($(el_item).attr('data-immutable') !== '0'){
  1389. return;
  1390. }
  1391. // files in trash cannot be renamed, user should be notified with an Alert.
  1392. else if(path.dirname($(el_item).attr('data-path')) === window.trash_path){
  1393. UIAlert(i18n('items_in_trash_cannot_be_renamed'));
  1394. return;
  1395. }
  1396. const el_item_name = $(el_item).find('.item-name');
  1397. const el_item_name_editor = $(el_item).find('.item-name-editor').get(0);
  1398. $(el_item_name).hide();
  1399. $(el_item_name_editor).show();
  1400. $(el_item_name_editor).focus();
  1401. $(el_item_name_editor).addClass('item-name-editor-active');
  1402. // select all text before extension
  1403. const item_name = $(el_item).attr('data-name');
  1404. const is_dir = parseInt($(el_item).attr('data-is_dir'));
  1405. const extname = path.extname('/'+item_name);
  1406. if(extname !== '' && !is_dir)
  1407. el_item_name_editor.setSelectionRange(0, item_name.length - extname.length)
  1408. else
  1409. $(el_item_name_editor).select();
  1410. }
  1411. export default UIItem;