keyboard.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  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 './UI/UIAlert.js';
  20. $(document).bind('keydown', async function(e){
  21. const focused_el = document.activeElement;
  22. //-----------------------------------------------------------------------
  23. // ← ↑ → ↓: an arrow key is pressed
  24. //-----------------------------------------------------------------------
  25. if((e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
  26. // ----------------------------------------------
  27. // Launch menu is open
  28. // ----------------------------------------------
  29. if($('.launch-popover').length > 0){
  30. // If no item is selected and down arrow is pressed, select the first item
  31. if($('.launch-popover .start-app-card.launch-app-selected').length === 0 && (e.which === 40)){
  32. $('.launch-popover .start-app-card:visible').first().addClass('launch-app-selected');
  33. // blur search input
  34. $('.launch-popover .launch-search').blur();
  35. return false;
  36. }
  37. // if search input is focused and left or right arrow is pressed, return false
  38. else if($('.launch-popover .launch-search').is(':focus') && (e.which === 37 || e.which === 39)){
  39. return false;
  40. }
  41. else{
  42. // If an item is already selected, move the selection up, down, left or right
  43. let selected_item = $('.launch-popover .start-app-card.launch-app-selected').get(0);
  44. let selected_item_index = $('.launch-popover .start-app-card:visible').index(selected_item);
  45. let selected_item_row = Math.floor(selected_item_index / 5);
  46. let selected_item_col = selected_item_index % 5;
  47. let selected_item_row_count = Math.ceil($('.launch-popover .start-app-card:visible').length / 5);
  48. let selected_item_col_count = 5;
  49. let new_selected_item_index = selected_item_index;
  50. let new_selected_item_row = selected_item_row;
  51. let new_selected_item_col = selected_item_col;
  52. let new_selected_item;
  53. // if up arrow is pressed
  54. if(e.which === 38){
  55. // if this item is in the first row, up arrow should bring the focus back to the search input
  56. if(selected_item_row === 0){
  57. $('.launch-popover .launch-search').focus();
  58. // unselect all items
  59. $('.launch-popover .start-app-card.launch-app-selected').removeClass('launch-app-selected');
  60. // bring cursor to the end of the search input
  61. $('.launch-popover .launch-search').val($('.launch-popover .launch-search').val());
  62. return false;
  63. }
  64. // if this item is not in the first row, move the selection up
  65. else{
  66. new_selected_item_row = selected_item_row - 1;
  67. if(new_selected_item_row < 0)
  68. new_selected_item_row = selected_item_row_count - 1;
  69. }
  70. }
  71. // if down arrow is pressed
  72. else if(e.which === 40){
  73. new_selected_item_row = selected_item_row + 1;
  74. if(new_selected_item_row >= selected_item_row_count)
  75. new_selected_item_row = 0;
  76. }
  77. // if left arrow is pressed
  78. else if(e.which === 37){
  79. new_selected_item_col = selected_item_col - 1;
  80. if(new_selected_item_col < 0)
  81. new_selected_item_col = selected_item_col_count - 1;
  82. }
  83. // if right arrow is pressed
  84. else if(e.which === 39){
  85. new_selected_item_col = selected_item_col + 1;
  86. if(new_selected_item_col >= selected_item_col_count)
  87. new_selected_item_col = 0;
  88. }
  89. new_selected_item_index = (new_selected_item_row * selected_item_col_count) + new_selected_item_col;
  90. new_selected_item = $('.launch-popover .start-app-card:visible').get(new_selected_item_index);
  91. $(selected_item).removeClass('launch-app-selected');
  92. $(new_selected_item).addClass('launch-app-selected');
  93. // make sure the selected item is visible in the popover by scrolling the popover
  94. let popover = $('.launch-popover').get(0);
  95. let popover_height = $('.launch-popover').height();
  96. let popover_scroll_top = popover.getBoundingClientRect().top;
  97. let popover_scroll_bottom = popover_scroll_top + popover_height;
  98. let selected_item_top = new_selected_item.getBoundingClientRect().top;
  99. let selected_item_bottom = new_selected_item.getBoundingClientRect().bottom;
  100. let isVisible = (selected_item_top >= popover_scroll_top) && (selected_item_bottom <= popover_scroll_top + popover_height);
  101. if ( ! isVisible ) {
  102. const scrollTop = selected_item_top - popover_scroll_top;
  103. const scrollBot = selected_item_bottom - popover_scroll_bottom;
  104. if (Math.abs(scrollTop) < Math.abs(scrollBot)) {
  105. popover.scrollTop += scrollTop;
  106. } else {
  107. popover.scrollTop += scrollBot;
  108. }
  109. }
  110. return false;
  111. }
  112. }
  113. // ----------------------------------------------
  114. // A context menu is open
  115. // ----------------------------------------------
  116. else if($('.context-menu').length > 0){
  117. // if no item is selected and down arrow is pressed, select the first item
  118. if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 40)){
  119. let selected_item = $('.context-menu-active .context-menu-item').get(0);
  120. window.select_ctxmenu_item(selected_item);
  121. return false;
  122. }
  123. // if no item is selected and up arrow is pressed, select the last item
  124. else if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 38)){
  125. let selected_item = $('.context-menu .context-menu-item').get($('.context-menu .context-menu-item').length - 1);
  126. window.select_ctxmenu_item(selected_item);
  127. return false;
  128. }
  129. // if an item is selected and down arrow is pressed, select the next enabled item
  130. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 40)){
  131. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  132. let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
  133. let new_selected_item_index = selected_item_index + 1;
  134. let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  135. while($(new_selected_item).hasClass('context-menu-item-disabled')){
  136. new_selected_item_index = new_selected_item_index + 1;
  137. new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  138. }
  139. window.select_ctxmenu_item(new_selected_item);
  140. return false;
  141. }
  142. // if an item is selected and up arrow is pressed, select the previous enabled item
  143. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 38)){
  144. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  145. let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
  146. let new_selected_item_index = selected_item_index - 1;
  147. let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  148. while($(new_selected_item).hasClass('context-menu-item-disabled')){
  149. new_selected_item_index = new_selected_item_index - 1;
  150. new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
  151. }
  152. window.select_ctxmenu_item(new_selected_item);
  153. return false;
  154. }
  155. // if right arrow is pressed, open the submenu by triggering a mouseover event
  156. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 39)){
  157. const selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  158. $(selected_item).trigger('mouseover');
  159. // if the submenu is open, select the first item in the submenu
  160. if($(selected_item).hasClass('context-menu-item-submenu') === true){
  161. $(selected_item).removeClass('context-menu-item-active');
  162. $(selected_item).addClass('context-menu-item-active-blurred');
  163. window.select_ctxmenu_item($('.context-menu[data-is-submenu="true"] .context-menu-item').get(0));
  164. }
  165. return false;
  166. }
  167. // if left arrow is pressed on a submenu, close the submenu
  168. else if($('.context-menu-active[data-is-submenu="true"]').length > 0 && (e.which === 37)){
  169. // get parent menu
  170. let parent_menu_id = $('.context-menu-active[data-is-submenu="true"]').data('parent-id');
  171. let parent_menu = $('.context-menu[data-element-id="' + parent_menu_id + '"]');
  172. // remove the submenu
  173. $('.context-menu-active[data-is-submenu="true"]').remove();
  174. // activate the parent menu
  175. $(parent_menu).addClass('context-menu-active');
  176. // select the item that opened the submenu
  177. let selected_item = $('.context-menu-active .context-menu-item-active-blurred').get(0);
  178. $(selected_item).removeClass('context-menu-item-active-blurred');
  179. $(selected_item).addClass('context-menu-item-active');
  180. return false;
  181. }
  182. // if enter is pressed, trigger a click event on the selected item
  183. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
  184. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  185. $(selected_item).trigger('click');
  186. return false;
  187. }
  188. }
  189. // ----------------------------------------------
  190. // Navigate items in the active item container
  191. // ----------------------------------------------
  192. else if(!$(focused_el).is('input') && !$(focused_el).is('textarea') && (e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
  193. let item_width = 110, item_height = 110, selected_item;
  194. // select first item in container if none is selected
  195. if($(window.active_item_container).find('.item-selected').length === 0){
  196. selected_item = $(window.active_item_container).find('.item').get(0);
  197. window.active_element = selected_item;
  198. $(window.active_item_container).find('.item-selected').removeClass('item-selected');
  199. $(selected_item).addClass('item-selected');
  200. return false;
  201. }
  202. // if Shift key is pressed and ONE item is already selected, pick that item
  203. else if($(window.active_item_container).find('.item-selected').length === 1 && e.shiftKey){
  204. selected_item = $(window.active_item_container).find('.item-selected').get(0);
  205. }
  206. // if Shift key is pressed and MORE THAN ONE item is selected, pick the latest active item
  207. else if($(window.active_item_container).find('.item-selected').length > 1 && e.shiftKey){
  208. selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
  209. }
  210. // otherwise if an item is selected, pick that item
  211. else if($(window.active_item_container).find('.item-selected').length === 1){
  212. selected_item = $(window.active_item_container).find('.item-selected').get(0);
  213. }
  214. else{
  215. selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
  216. }
  217. // override the default behavior of ctrl/meta key
  218. // in some browsers ctrl/meta key + arrow keys will scroll the page or go back/forward in history
  219. if(e.ctrlKey || e.metaKey){
  220. e.preventDefault();
  221. e.stopPropagation();
  222. }
  223. // get the position of the selected item
  224. let active_el_pos = $(selected_item).hasClass('item') ? selected_item.getBoundingClientRect() : $(selected_item).closest('.item').get(0).getBoundingClientRect();
  225. let xpos = active_el_pos.left + item_width/2;
  226. let ypos = active_el_pos.top + item_height/2;
  227. // these hold next item's position on the grid
  228. let x_nxtpos, y_nxtpos;
  229. // these hold the amount of pixels to scroll the container
  230. let x_scroll = 0, y_scroll = 0;
  231. // determine next item's position on the grid
  232. // left
  233. if(e.which === 37){
  234. x_nxtpos = (xpos - item_width) > 0 ? (xpos - item_width) : 0;
  235. y_nxtpos = (ypos);
  236. x_scroll = (item_width / 2);
  237. }
  238. // up
  239. else if(e.which === 38){
  240. x_nxtpos = (xpos);
  241. y_nxtpos = (ypos - item_height) > 0 ? (ypos - item_height) : 0;
  242. y_scroll = -1 * (item_height / 2);
  243. }
  244. // right
  245. else if(e.which === 39){
  246. x_nxtpos = (xpos + item_width);
  247. y_nxtpos = (ypos);
  248. x_scroll = -1 * (item_width / 2);
  249. }
  250. // down
  251. else if(e.which === 40){
  252. x_nxtpos = (xpos);
  253. y_nxtpos = (ypos + item_height);
  254. y_scroll = (item_height / 2);
  255. }
  256. let elements_at_next_pos = document.elementsFromPoint(x_nxtpos, y_nxtpos);
  257. let next_item;
  258. for (let index = 0; index < elements_at_next_pos.length; index++) {
  259. const elem_at_next_pos = elements_at_next_pos[index];
  260. if($(elem_at_next_pos).hasClass('item') && $(elem_at_next_pos).closest('.item-container').is(window.active_item_container)){
  261. next_item = elem_at_next_pos;
  262. break;
  263. }
  264. }
  265. if(next_item){
  266. selected_item = next_item;
  267. window.active_element = next_item;
  268. // if ctrl or meta key is not pressed, unselect all items
  269. if(!e.shiftKey){
  270. $(window.active_item_container).find('.item').removeClass('item-selected');
  271. }
  272. $(next_item).addClass('item-selected');
  273. window.latest_selected_item = next_item;
  274. // scroll to the selected item only if this was a down or up move
  275. if(e.which === 38 || e.which === 40)
  276. next_item.scrollIntoView(false);
  277. }
  278. }
  279. }
  280. //-----------------------------------------------------------------------
  281. // if the Esc key is pressed on a FileDialog/Alert, close that FileDialog/Alert
  282. //-----------------------------------------------------------------------
  283. else if(
  284. // escape key code
  285. e.which === 27 &&
  286. // active window must be a FileDialog or Alert
  287. ($('.window-active').hasClass('window-filedialog') || $('.window-active').hasClass('window-alert')) &&
  288. // either don't close if an input is focused or if the input is the filename input
  289. ((!$(focused_el).is('input') && !$(focused_el).is('textarea')) || $(focused_el).hasClass('savefiledialog-filename'))
  290. ){
  291. // close the FileDialog
  292. $('.window-active').close();
  293. }
  294. //-----------------------------------------------------------------------
  295. // if the Esc key is pressed on a Window Navbar Editor, deactivate the editor
  296. //-----------------------------------------------------------------------
  297. else if( e.which === 27 && $(focused_el).hasClass('window-navbar-path-input')){
  298. $(focused_el).blur();
  299. $(focused_el).val($(focused_el).closest('.window').attr('data-path'));
  300. $(focused_el).attr('data-path', $(focused_el).closest('.window').attr('data-path'));
  301. }
  302. //-----------------------------------------------------------------------
  303. // Esc key should:
  304. // - always close open context menus
  305. // - close the Launch Popover if it's open
  306. //-----------------------------------------------------------------------
  307. if( e.which === 27){
  308. // close open context menus
  309. $('.context-menu').remove();
  310. // close the Launch Popover if it's open
  311. $(".launch-popover").closest('.popover').fadeOut(200, function(){
  312. $(".launch-popover").closest('.popover').remove();
  313. });
  314. }
  315. })
  316. $(document).bind('keydown', async function(e){
  317. const focused_el = document.activeElement;
  318. //-----------------------------------------------------------------------
  319. // Shift+Delete (win)/ option+command+delete (Mac) key pressed
  320. // Permanent delete bypassing trash after alert
  321. //-----------------------------------------------------------------------
  322. if((e.keyCode === 46 && e.shiftKey) || (e.altKey && e.metaKey && e.keyCode === 8)) {
  323. let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected`);
  324. if($selected_items.length > 0){
  325. const alert_resp = await UIAlert({
  326. message: i18n('confirm_delete_multiple_items'),
  327. buttons:[
  328. {
  329. label: i18n('delete'),
  330. type: 'primary',
  331. },
  332. {
  333. label: i18n('cancel')
  334. },
  335. ]
  336. })
  337. if((alert_resp) === 'Delete'){
  338. for (let index = 0; index < $selected_items.length; index++) {
  339. const element = $selected_items[index];
  340. await window.delete_item(element);
  341. }
  342. }
  343. }
  344. return false;
  345. }
  346. //-----------------------------------------------------------------------
  347. // Delete (win)/ ctrl+delete (Mac) / cmd+delete (Mac) key pressed
  348. // Permanent delete from trash after alert or move to trash
  349. //-----------------------------------------------------------------------
  350. if(e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey))) {
  351. // permanent delete?
  352. let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected[data-path^="${window.trash_path + '/'}"]`);
  353. if($selected_items.length > 0){
  354. const alert_resp = await UIAlert({
  355. message: i18n('confirm_delete_multiple_items'),
  356. buttons:[
  357. {
  358. label: i18n('delete'),
  359. type: 'primary',
  360. },
  361. {
  362. label: i18n('cancel')
  363. },
  364. ]
  365. })
  366. if((alert_resp) === 'Delete'){
  367. for (let index = 0; index < $selected_items.length; index++) {
  368. const element = $selected_items[index];
  369. await window.delete_item(element);
  370. }
  371. const trash = await puter.fs.stat(window.trash_path);
  372. if(window.socket){
  373. window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
  374. }
  375. if(trash.is_empty){
  376. $(`[data-app="trash"]`).find('.taskbar-icon > img').attr('src', window.icons['trash.svg']);
  377. $(`.item[data-path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
  378. $(`.window[data-path="${html_encode(window.trash_path)}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
  379. }
  380. }
  381. }
  382. // regular delete?
  383. else{
  384. $selected_items = $(window.active_element).closest('.item-container').find('.item-selected');
  385. if($selected_items.length > 0){
  386. // Only delete the items if we're not renaming one.
  387. if ($selected_items.children('.item-name-editor-active').length === 0) {
  388. window.move_items($selected_items, window.trash_path);
  389. }
  390. }
  391. }
  392. return false;
  393. }
  394. //-----------------------------------------------------------------------
  395. // A letter or number is pressed and there is no context menu open: search items by name
  396. //-----------------------------------------------------------------------
  397. if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length === 0){
  398. if(window.keypress_item_seach_term !== '')
  399. clearTimeout(window.keypress_item_seach_buffer_timeout);
  400. window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
  401. window.keypress_item_seach_term = '';
  402. }, 700);
  403. window.keypress_item_seach_term += e.key.toLocaleLowerCase();
  404. let matches= [];
  405. const selected_items = $(window.active_item_container).find(`.item-selected`).not('.item-disabled').first();
  406. // if one item is selected and the selected item matches the search term, don't continue search and select this item again
  407. if(selected_items.length === 1 && $(selected_items).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
  408. return false;
  409. }
  410. // search for matches
  411. let haystack = $(window.active_item_container).find(`.item`).not('.item-disabled');
  412. for(let j=0; j < haystack.length; j++){
  413. if($(haystack[j]).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
  414. matches.push(haystack[j])
  415. }
  416. }
  417. if(matches.length > 0){
  418. // if there are multiple matches and an item is already selected, remove all matches before the selected item
  419. if(selected_items.length > 0 && matches.length > 1){
  420. let match_index;
  421. for(let i=0; i < matches.length - 1; i++){
  422. if($(matches[i]).is(selected_items)){
  423. match_index = i;
  424. break;
  425. }
  426. }
  427. matches.splice(0, match_index+1);
  428. }
  429. // deselect all selected sibling items
  430. $(window.active_item_container).find(`.item-selected`).removeClass('item-selected');
  431. // select matching item
  432. $(matches[0]).not('.item-disabled').addClass('item-selected');
  433. matches[0].scrollIntoView(false);
  434. window.update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
  435. }
  436. return false;
  437. }
  438. //-----------------------------------------------------------------------
  439. // A letter or number is pressed and there is a context menu open: search items by name
  440. //-----------------------------------------------------------------------
  441. else if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length > 0){
  442. if(window.keypress_item_seach_term !== '')
  443. clearTimeout(window.keypress_item_seach_buffer_timeout);
  444. window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
  445. window.keypress_item_seach_term = '';
  446. }, 700);
  447. window.keypress_item_seach_term += e.key.toLocaleLowerCase();
  448. let matches= [];
  449. const selected_items = $('.context-menu').find(`.context-menu-item-active`).first();
  450. // if one item is selected and the selected item matches the search term, don't continue search and select this item again
  451. if(selected_items.length === 1 && $(selected_items).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
  452. return false;
  453. }
  454. // search for matches
  455. let haystack = $('.context-menu-active').find(`.context-menu-item`);
  456. for(let j=0; j < haystack.length; j++){
  457. if($(haystack[j]).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
  458. matches.push(haystack[j])
  459. }
  460. }
  461. if(matches.length > 0){
  462. // if there are multiple matches and an item is already selected, remove all matches before the selected item
  463. if(selected_items.length > 0 && matches.length > 1){
  464. let match_index;
  465. for(let i=0; i < matches.length - 1; i++){
  466. if($(matches[i]).is(selected_items)){
  467. match_index = i;
  468. break;
  469. }
  470. }
  471. matches.splice(0, match_index+1);
  472. }
  473. // deselect all selected sibling items
  474. $('.context-menu').find(`.context-menu-item-active`).removeClass('context-menu-item-active');
  475. // select matching item
  476. $(matches[0]).addClass('context-menu-item-active');
  477. // matches[0].scrollIntoView(false);
  478. // update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
  479. }
  480. return false;
  481. }
  482. })
  483. $(document).bind("keyup keydown", async function(e){
  484. const focused_el = document.activeElement;
  485. //-----------------------------------------------------------------------------
  486. // Override ctrl/cmd + s/o
  487. //-----------------------------------------------------------------------------
  488. if((e.ctrlKey || e.metaKey) && (e.which === 83 || e.which === 79)){
  489. e.preventDefault()
  490. return false;
  491. }
  492. //-----------------------------------------------------------------------------
  493. // Select All
  494. // ctrl/command + a, will select all items on desktop and windows
  495. //-----------------------------------------------------------------------------
  496. if((e.ctrlKey || e.metaKey) && e.which === 65 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
  497. let $parent_container = $(window.active_element).closest('.item-container');
  498. if($parent_container.length === 0)
  499. $parent_container = $(window.active_element).find('.item-container');
  500. if($parent_container.attr('data-multiselectable') === 'false')
  501. return false;
  502. if($parent_container){
  503. $($parent_container).find('.item').not('.item-disabled').addClass('item-selected');
  504. window.update_explorer_footer_selected_items_count($parent_container.closest('.window'));
  505. }
  506. return false;
  507. }
  508. //-----------------------------------------------------------------------------
  509. // Close Window
  510. // ctrl + w, will close the active window
  511. //-----------------------------------------------------------------------------
  512. if(e.ctrlKey && e.which === 87){
  513. let $parent_window = $(window.active_element).closest('.window');
  514. if($parent_window.length === 0)
  515. $parent_window = $(window.active_element).find('.window');
  516. if($parent_window !== null){
  517. $($parent_window).close();
  518. }
  519. }
  520. //-----------------------------------------------------------------------------
  521. // Copy
  522. // ctrl/command + c, will copy selected items on the active element to the clipboard
  523. //-----------------------------------------------------------------------------
  524. if((e.ctrlKey || e.metaKey) && e.which === 67 &&
  525. $(window.mouseover_window).attr('data-is_dir') !== 'false' &&
  526. $(window.mouseover_window).attr('data-path') !== window.trash_path &&
  527. !$(focused_el).is('input') &&
  528. !$(focused_el).is('textarea')){
  529. let $selected_items;
  530. let parent_container = $(window.active_element).closest('.item-container');
  531. if(parent_container.length === 0)
  532. parent_container = $(window.active_element).find('.item-container');
  533. if(parent_container !== null){
  534. $selected_items = $(parent_container).find('.item-selected');
  535. if($selected_items.length > 0){
  536. window.clipboard = [];
  537. window.clipboard_op = 'copy';
  538. $selected_items.each(function() {
  539. // error if trash is being copied
  540. if($(this).attr('data-path') === window.trash_path){
  541. return;
  542. }
  543. // add to clipboard
  544. window.clipboard.push({path: $(this).attr('data-path'), uid: $(this).attr('data-uid'), metadata: $(this).attr('data-metadata')});
  545. })
  546. }
  547. }
  548. return false;
  549. }
  550. //-----------------------------------------------------------------------------
  551. // Cut
  552. // ctrl/command + x, will copy selected items on the active element to the clipboard
  553. //-----------------------------------------------------------------------------
  554. if((e.ctrlKey || e.metaKey) && e.which === 88 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
  555. let $selected_items;
  556. let parent_container = $(window.active_element).closest('.item-container');
  557. if(parent_container.length === 0)
  558. parent_container = $(window.active_element).find('.item-container');
  559. if(parent_container !== null){
  560. $selected_items = $(parent_container).find('.item-selected');
  561. if($selected_items.length > 0){
  562. window.clipboard = [];
  563. window.clipboard_op = 'move';
  564. $selected_items.each(function() {
  565. window.clipboard.push($(this).attr('data-path'));
  566. })
  567. }
  568. }
  569. return false;
  570. }
  571. //-----------------------------------------------------------------------
  572. // Open
  573. // Enter key on a selected item will open it
  574. //-----------------------------------------------------------------------
  575. if(e.which === 13 && !$(focused_el).is('input') && !$(focused_el).is('textarea') && (Date.now() - window.last_enter_pressed_to_rename_ts) >200
  576. // prevent firing twice, because this will be fired on both keyup and keydown
  577. && e.type === 'keydown'){
  578. let $selected_items;
  579. e.preventDefault();
  580. e.stopPropagation();
  581. // ---------------------------------------------
  582. // if this is a selected Launch menu item, open it
  583. // ---------------------------------------------
  584. if($('.launch-app-selected').length > 0){
  585. // close launch menu
  586. $(".launch-popover").fadeOut(200, function(){
  587. window.launch_app({
  588. name: $('.launch-app-selected').attr('data-name'),
  589. })
  590. $(".launch-popover").remove();
  591. });
  592. return false;
  593. }
  594. // ---------------------------------------------
  595. // if this is a selected context menu item, open it
  596. // ---------------------------------------------
  597. else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
  598. // let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  599. // $(selected_item).trigger('mouseover');
  600. // $(selected_item).trigger('click');
  601. let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
  602. $(selected_item).removeClass('context-menu-item-active');
  603. $(selected_item).addClass('context-menu-item-active-blurred');
  604. $(selected_item).trigger('mouseover');
  605. $(selected_item).trigger('click');
  606. if($('.context-menu[data-is-submenu="true"]').length > 0){
  607. let selected_item = $('.context-menu[data-is-submenu="true"] .context-menu-item').get(0);
  608. window.select_ctxmenu_item(selected_item);
  609. }
  610. return false;
  611. }
  612. // ---------------------------------------------
  613. // if this is a selected item, open it
  614. // ---------------------------------------------
  615. else if(window.active_item_container){
  616. $selected_items = $(window.active_item_container).find('.item-selected');
  617. if($selected_items.length > 0){
  618. $selected_items.each(function() {
  619. window.open_item({
  620. item: this,
  621. new_window: e.metaKey || e.ctrlKey,
  622. });
  623. })
  624. }
  625. return false;
  626. }
  627. return false;
  628. }
  629. //----------------------------------------------
  630. // Paste
  631. // ctrl/command + v, will paste items from the clipboard to the active element
  632. //----------------------------------------------
  633. if((e.ctrlKey || e.metaKey) && e.which === 86 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
  634. let target_path, target_el;
  635. // continue only if there is something in the clipboard
  636. if(window.clipboard.length === 0)
  637. return;
  638. let parent_container = determine_active_container_parent();
  639. if(parent_container){
  640. target_el = parent_container;
  641. target_path = $(parent_container).attr('data-path');
  642. // don't allow pasting in Trash
  643. if((target_path === window.trash_path || target_path.startsWith(window.trash_path + '/')) && window.clipboard_op !== 'move')
  644. return;
  645. // execute clipboard operation
  646. if(window.clipboard_op === 'copy')
  647. window.copy_clipboard_items(target_path);
  648. else if(window.clipboard_op === 'move')
  649. window.move_clipboard_items(target_el, target_path);
  650. }
  651. return false;
  652. }
  653. //-----------------------------------------------------------------------------
  654. // Undo
  655. // ctrl/command + z, will undo last action
  656. //-----------------------------------------------------------------------------
  657. if((e.ctrlKey || e.metaKey) && e.which === 90){
  658. window.undo_last_action();
  659. return false;
  660. }
  661. });