Compare commits

...
Author SHA1 Message Date
Nariman Jelveh 589fbf26aa fix(explorer): close a folder's window when the folder is deleted, however it was opened
Windows opened by path, such as with Ctrl-click on a breadcrumb or sidebar
entry, never received the marker used to find a deleted folder's windows,
so they stayed open on a folder that no longer existed.
2026-11-10 11:19:06 -07:00
Nariman Jelveh 8d5eed1cb7 fix(explorer): open an item's context menu where it was requested
The menu is built after permission and app lookups, and it opened wherever
the pointer had moved to by the time they finished.
2026-11-10 11:19:06 -07:00
Nariman Jelveh a80c748c15 fix(explorer): refuse uploads, copies and shortcuts into the Shared view
Dropping files on Shared in the sidebar, or Ctrl/Alt-dropping items onto a
window showing it, sent requests against a path that is not a directory and
ended in an error or a stuck progress window.
2026-11-10 11:19:06 -07:00
Nariman Jelveh d60526fa72 fix(explorer): remove the stray tooltip from the loading spinner
The spinner's SVG carried an internal title that showed as a tooltip and
was read out by screen readers.
2026-11-10 11:19:06 -07:00
Nariman Jelveh 704649f0c3 fix(explorer): give the layout button a tooltip and a pointer cursor
It was the only navbar control without either, so it did not read as
clickable.
2026-11-10 11:19:06 -07:00
Nariman Jelveh 87c1013986 fix(explorer): align the details header with its rows and mark it clickable
The header ended 10px before the rows and the selection highlight, and its
sortable column titles kept the default cursor.
2026-11-10 11:19:06 -07:00
Nariman Jelveh c0abf69aad fix(explorer): grey out the whole selected row in an unfocused details view
Only the name lost its highlight when the window lost focus, leaving a grey
pill inside a blue row.
2026-11-10 11:19:06 -07:00
Nariman Jelveh 2c819cc158 fix(explorer): keep the current folder visible in a long breadcrumb
A path too long for the bar was cut at its end, hiding the folder being
viewed and its nearest parents. It is now cut at the root end.
2026-11-10 11:19:06 -07:00
Nariman Jelveh 96f3c7d058 fix(explorer): show the slow-load message only for a load that is slow
Once any folder took over a second to load, the spinner text switched to
the slow-load message three seconds later even after the load finished, and
every later load in that window opened with it. The text is also translated
now.
2026-11-10 11:19:06 -07:00
Nariman Jelveh 3809f38e5f fix(explorer): enable the Open dialog's button only for a selected file
The button stayed enabled after navigating to another folder, and a
rubber-band selection of folders alone enabled it, although clicking it then
did nothing.
2026-11-10 11:19:06 -07:00
Nariman Jelveh 89124bb27d fix(explorer): keep sidebar labels translated and titled after reordering
A reordered sidebar rendered the labels saved at the time of the drag, so
they stayed in the old language, and every item's tooltip was empty.
2026-11-10 11:19:06 -07:00
Nariman Jelveh eaff5c5929 fix(explorer): label the root and Shared in the Back/Forward menus
The long-press history menus showed an empty row for the root folder and a
lowercase path segment for Shared, and the Forward menu's Home entry was not
translated.
2026-11-10 11:19:05 -07:00
Nariman Jelveh f452d8d8fa fix(explorer): empty Trash from its window in one request
The Trash window's own Empty Trash sent one delete request per item and
reported Trash as empty before any of them finished. It now runs the same
single operation as the taskbar and desktop.
2026-11-10 11:19:05 -07:00
Nariman Jelveh 23e56f2ce2 fix(explorer): keep desktop rearranging to the desktop itself
Dropping an item from a window showing the Desktop folder onto the desktop
background positioned the window's row absolutely and saved that position.
2026-11-10 11:19:05 -07:00
Nariman Jelveh 095c18fdb0 fix(explorer): don't highlight a window as a drop target for its own items
The check meant to skip the folder an item is already in compared the wrong
path, so dragging inside a window outlined it although dropping there does
nothing.
2026-11-10 11:19:05 -07:00
Nariman Jelveh 8c2b834fd3 fix(explorer): handle a drop onto a folder that is part of the dragged selection
Dragging a selection onto one of its own selected folders also tried to move
that folder into itself. On the desktop the drop also cleared the saved
position of the target folder instead of the items that left the desktop.
2026-11-10 11:19:05 -07:00
Nariman Jelveh 2217513589 fix(explorer): make New > JPEG Image create an actual JPEG
The blank image was encoded with the canvas default, so the .jpg file held
transparent PNG data.
2026-11-10 11:19:05 -07:00
Nariman Jelveh 6379260042 fix(explorer): give a folder named *.app a folder icon
The app-shortcut branch ran before the directory branch, so such a folder
was read as a file on every listing, failed, and showed the app icon.
2026-11-10 11:19:05 -07:00
Nariman Jelveh 9fb31abad4 fix(explorer): offer Cut only where the item can be moved
The item menu on the desktop and in folder windows offered Cut on trashed
items and on shared items the user may not move, unlike the same menu built
for the dashboard.
2026-11-10 11:19:05 -07:00
Nariman Jelveh c369b55841 fix(explorer): use an item's current path and name in its context menu
Cut, Copy, Download, Create Shortcut and the rename editor read the path and
name captured when the row was created. After a rename made from another
window or tab they acted on the old path, and closing the editor repainted
the old name.
2026-11-10 11:19:05 -07:00
Nariman Jelveh 01da03437a fix(explorer): offer a download when no app can open a file
The prompt called a translation import that is not a function, so opening
a file with no suitable app threw and did nothing.
2026-11-10 11:19:05 -07:00
Nariman Jelveh f67a04f67e fix(explorer): let Delete work on a file after its rename was cancelled
Cancelling a rename with Escape, or leaving the editor without a change,
left the item marked as being renamed, and the Delete shortcut skips such
items.
2026-11-10 11:05:01 -07:00
Nariman Jelveh 908dcbbd22 fix(explorer): release each row's document click handler when the row is removed
Every file row bound a click handler on document that was never unbound, so
browsing folders grew the number of handlers run on every click and kept the
removed rows alive.
2026-11-10 11:03:38 -07:00
Nariman Jelveh 1002f628a7 fix(explorer): accept drops into a window that covers a desktop folder
A desktop folder hidden under a window still reacted to the drag and
disabled the window as a drop target, so the drop was ignored. A drop that
bailed out early also left every folder view disabled as a target until the
next hover; targets are now restored when the drag ends.
2026-11-10 11:01:58 -07:00
Nariman Jelveh 01135fcee4 fix(explorer): don't flash the empty-folder message while a folder refreshes
Clearing the rows for a refresh showed the message until the listing came
back, even for a folder full of items.
2026-11-10 10:59:49 -07:00
Nariman Jelveh e7f2f55b79 fix(explorer): show file names once-escaped in move alerts
The name was escaped before being passed to an alert that escapes its whole
message, so names with &, quotes or angle brackets showed entity text.
2026-11-10 10:59:49 -07:00
Nariman Jelveh d98f16f191 fix(explorer): keep one progress window per copy or move
The progress timer is re-armed after every conflict dialog, and it opened a
second window when one was already showing, leaving the first on screen for
good.
2026-11-10 10:58:48 -07:00
Nariman Jelveh bdfcbebd31 fix(explorer): follow renamed and moved folders in Back/Forward and on undo
A window's navigation history kept the old path after a folder was renamed
or moved, so Back led to a load error. Undoing a folder rename also left
windows and items inside it on the renamed path, because the undo retargeted
from the path it was restoring.
2026-11-10 10:58:48 -07:00
Nariman Jelveh af01727f0f fix(explorer): end the extract operation when an archive is corrupt or has no files
A corrupt zip threw before its error could be shown, and an archive with
nothing to write or a failed upload never closed the progress window. A
progress update arriving before the window existed also threw.
2026-11-10 10:57:32 -07:00
Nariman Jelveh a0b1f82b85 fix(explorer): allow moving into folders whose name starts with Trash
The deleted-folder check matched any sibling of Trash sharing its prefix,
so moving into a folder such as Trash-old was refused.
2026-11-10 10:56:40 -07:00
Nariman Jelveh 225eeba6ec fix(explorer): download files whose names contain &, #, % or +
The path went into the download URL unencoded, so the server received a
truncated path and the download failed.
2026-11-10 10:56:39 -07:00
Nariman Jelveh fcf8bc53d1 fix(explorer): refresh size and date when a file is overwritten elsewhere
An overwrite arrives as an update event, which only applied the name, icon
and path, so the size and modified columns and sorting kept old values.
2026-11-10 10:55:59 -07:00
Nariman Jelveh 8b3746cdca fix(explorer): remove rows deleted elsewhere instead of hiding them
A deletion from another tab or an app only faded the row out, so it still
counted in the footer, in Select All and in keyboard actions. The row is now
removed, and windows showing the deleted folder close even when its own row
is not displayed anywhere.
2026-11-10 10:55:59 -07:00
Nariman Jelveh 737fdd2148 fix(explorer): keep the Trash window's states when Trash is emptied elsewhere
Emptying Trash from another tab cleared the whole container, including its
empty, error and loading elements, and left the item count stale.
2026-11-10 10:55:59 -07:00
Nariman Jelveh fb6571e914 fix(explorer): sort names the way the dashboard does, with a stable tie-break
Folder windows and the desktop compared raw lowercase strings, so file10
came before file2 and accented names landed after z, and equal sizes, dates
or types kept whatever order they arrived in. They now use the dashboard's
comparator, so the same folder reads the same in both.
2026-11-10 10:55:59 -07:00
Nariman Jelveh 3512093143 fix(explorer): show a moved item once in its destination
The tab that moves an item renders the row and also receives the move
event, so the destination showed the item twice whenever the event arrived
first. Both paths now replace any row the other already rendered.
2026-11-10 10:52:48 -07:00
Nariman Jelveh 0b48fff418 fix(explorer): paste cut items after their folder is no longer displayed
Paste resolved the cut paths to rows on screen, so cutting a file and then
navigating the same window to the destination moved nothing and silently
emptied the clipboard. Items without a row are now looked up and moved the
same way.
2026-11-10 10:52:48 -07:00
Nariman Jelveh 8d1656dff8 fix(explorer): count only visible items in the footer and drop hidden files from the selection
The item count included dotfiles that were not shown, and hiding hidden
files left them selected, so a later Delete or Cut acted on files the user
could not see.
2026-11-10 10:48:34 -07:00
Nariman Jelveh ed21d14385 fix(explorer): keep selection shortcuts working after navigating, and out of text fields
Select All, Copy, Cut and Delete looked up the folder from the element last
pressed, which is a removed item or a sidebar entry after navigating, so the
shortcuts did nothing. Select All also skips hidden files, and Delete keys
typed in a text field are left to the field.
2026-11-10 10:47:35 -07:00
Nariman Jelveh b2b9db0205 fix(explorer): paste once per Ctrl+V press
The shortcut ran on both keydown and keyup, so releasing V before Ctrl
pasted the clipboard twice.
2026-11-10 10:45:54 -07:00
Nariman Jelveh 804e9c35e9 fix(explorer): make the keyboard permanent-delete confirmation work in every language
The confirmation compared the dialog's answer with the English button label,
so in any other language confirming did nothing.
2026-11-10 10:45:54 -07:00
Nariman Jelveh f14fa619bf fix(explorer): ignore non-character keys in type-to-select
Shift, arrows and Enter were appended to the search term by name, so typing
a capital letter or typing right after navigating matched nothing. Hidden
files are no longer matched, and the footer count follows the container the
selection changed in.
2026-11-10 10:45:54 -07:00
Nariman Jelveh d689861fa2 fix(explorer): keep arrow-key navigation on visible items and a valid anchor
Arrow keys could land on a hidden dotfile and then stop moving, threw when
the remembered anchor belonged to another window, and left the footer's
selected count stale.
2026-11-10 10:45:53 -07:00
Nariman Jelveh 3984ec724c fix(explorer): anchor a Ctrl/Cmd rubber-band selection where the drag started
The rectangle's origin was only recorded for unmodified drags, so adding to
a selection drew it from the previous drag's start point or the top-left
corner.
2026-11-10 10:44:09 -07:00
Nariman Jelveh 95634784a0 fix(explorer): release a window's selection listeners and history when it closes
Each folder window left its rubber-band selection instance, a document
mouseup handler and its navigation history behind after closing, so every
later mouse press ran once per window ever opened. Windows that are not
folders no longer bind the handler at all.
2026-11-10 10:42:49 -07:00
Nariman Jelveh a028f2ce08 fix(explorer): drop the previous folder's identity when a folder fails to load
A rejected stat was never handled, so the window kept the previous folder's
uid, title and item count while showing the load error, and layout or sort
changes were saved to the wrong folder. The window now clears them, and the
failed request no longer surfaces as an unhandled rejection.
2026-11-10 10:42:03 -07:00
Nariman Jelveh dceda8aa2a fix(explorer): keep the address-bar input in step with the Shared view and the root
The path input was only updated after a successful stat, so in views that
are never statted it kept the previous folder's path and Enter navigated
back there.
2026-11-10 10:42:03 -07:00
Nariman Jelveh 63f6ed7c38 fix(explorer): record typed address-bar paths in navigation history and canonicalize them
Pressing Enter in the path input now pushes the destination onto the
window's Back/Forward history like every other navigation, and resolves
the entry first (trim, ~ and home-relative expansion, trailing or doubled
slashes) so the window's path matches the form the rest of the GUI compares
against.
2026-11-10 10:40:48 -07:00
16 changed files with 521 additions and 260 deletions

No files matched your search

+32 -10
View File
@@ -34,7 +34,7 @@ import UIWindowProgress from './UIWindowProgress.js';
import UITaskbar from './UITaskbar.js';
import new_context_menu_item from '../helpers/newContextMenuItem.js';
import refresh_item_container from '../helpers/refreshItemContainer.js';
import { atPath, retargetPaths, underPath } from '../helpers/pathElements.js';
import { atPath, retargetNavHistory, retargetPaths, underPath } from '../helpers/pathElements.js';
import changeLanguage from '../i18n/i18nChangeLanguage.js';
import UIWindowTaskManager from './UIWindowTaskManager.js';
import truncate_filename from '../helpers/truncateFilename.js';
@@ -233,7 +233,12 @@ async function UIDesktop (options) {
// empty trash windows if needed
if ( msg.is_empty )
{
$(`.window[data-path="${html_encode(window.trash_path)}" i]`).find('.item-container').empty();
// only the rows: the container also holds its empty, error and loading states
$(`.window[data-path="${html_encode(window.trash_path)}" i]`).each(function () {
$(this).find('.item-container .item').removeItems();
window.update_explorer_footer_item_count(this);
window.update_explorer_footer_selected_items_count(this);
});
}
});
@@ -416,14 +421,17 @@ async function UIDesktop (options) {
return;
}
// hide all UIItems with matching uids
// remove all UIItems at the removed path
atPath('.item', item.path).fadeOut(150, function () {
// close all windows with matching uids
// $('.window-' + item.uid).close();
// close all windows that belong to a descendant of this item
// todo this has to be case-insensitive
underPath('.window', item.path).close();
const $parent_window = $(this).closest('.window');
$(this).removeItems();
window.update_explorer_footer_item_count($parent_window);
window.update_explorer_footer_selected_items_count($parent_window);
});
// close the windows showing the removed folder or anything inside it,
// whether or not its own row is displayed anywhere
// todo this has to be case-insensitive
atPath('.window[data-app="explorer"]', item.path).add(underPath('.window', item.path)).close();
});
window.socket.on('item.updated', async (item) => {
@@ -455,6 +463,14 @@ async function UIDesktop (options) {
$(`.item[data-uid='${item.uid}'] .item-name-editor`).val(item.name);
$(`.item[data-uid='${item.uid}'] .item-name`).attr('title', item.name);
// An overwrite arrives as an update, with a new size and date
if ( item.size !== undefined && item.modified !== undefined ) {
const $updated_items = $(`.item[data-uid='${item.uid}']`);
$updated_items.attr({ 'data-size': item.size ?? '', 'data-modified': item.modified ?? '' });
$updated_items.find('.item-attr--size > span').text(item.size ? window.byte_format(item.size) : '-');
$updated_items.find('.item-attr--modified > span').text(item.modified === 0 ? '-' : timeago.format(item.modified * 1000));
}
// Set new data-path
const new_path = item.path;
$(`.item[data-uid='${item.uid}']`).attr('data-path', new_path);
@@ -523,6 +539,7 @@ async function UIDesktop (options) {
underPath('.window', resp.old_path).add(atPath('.window', resp.old_path)).each(function () {
window.update_window_path(this, fsentry.path + $(this).attr('data-path').slice(resp.old_path.length));
});
retargetNavHistory(resp.old_path, fsentry.path);
}
if ( dest_path === window.trash_path ) {
@@ -546,13 +563,18 @@ async function UIDesktop (options) {
// Trashing renames the entry to its uid; the real name is in metadata.
fsentry.name = metadata.original_name ?? fsentry.name;
const moved_item_icon = await item_icon(fsentry);
const $dest_containers = atPath('.item-container', dest_path);
// the tab that made this move renders the row itself, in either order
$dest_containers.find(`.item[data-uid='${fsentry.uid}']`).removeItems();
// create new item on matching containers
UIItem({
appendTo: atPath('.item-container', dest_path),
appendTo: $dest_containers,
immutable: fsentry.immutable || fsentry.writable === false,
uid: fsentry.uid,
path: fsentry.path,
icon: await item_icon(fsentry),
icon: moved_item_icon,
name: fsentry.name,
is_dir: fsentry.is_dir,
size: fsentry.size,
+38 -16
View File
@@ -396,7 +396,7 @@ async function UIItem (options) {
// do not highlight if the target is the same as the item being moved
$(el_item).attr('data-path') !== $(window.mouseover_item_container).attr('data-path') &&
// do not highlight if item is being moved to where it already is
$(el_item).attr('data-path') !== $(window.mouseover_item_container).attr('data-path') ) {
path.dirname($(el_item).attr('data-path')) !== $(window.mouseover_item_container).attr('data-path') ) {
// highlight item container
$(window.mouseover_item_container).addClass('item-container-active');
@@ -416,7 +416,8 @@ async function UIItem (options) {
},
stop: function (event, ui) {
// Allow rearranging only if item is on desktop, not trash container, auto arrange is disabled and item is not dropped into another item
if ( $(el_item).closest('.item-container').attr('data-path') === window.desktop_path &&
// the desktop itself, not a window showing the Desktop folder
if ( $(el_item).closest('.item-container').hasClass('desktop') &&
!window.is_auto_arrange_enabled && $(el_item).attr('data-path') !== window.trash_path && !ui.helper.data('dropped') &&
// Item must be dropped on the Desktop and not on the taskbar
window.mouseover_window === undefined && ui.position.top <= window.desktop_height - window.taskbar_height - 15 ) {
@@ -434,7 +435,9 @@ async function UIItem (options) {
// re-enable all droppable UIItems that are not a dir
$('.item[data-is_dir=\'0\']:not(.item-selected)').droppable('enable');
// remove active item-container border highlights
$('.item-container').removeClass('item-container-active');
$('.item-container').removeClass('item-container-active item-container-transparent-border');
// a drop that bailed out early never re-enables the containers an item's hover disabled
$('.item-container.ui-droppable').droppable('enable');
// reset longer hover timeout and last window dragged over
clearTimeout(longer_hover_timeout);
last_window_dragged_over = null;
@@ -548,11 +551,14 @@ async function UIItem (options) {
}
// Otherwise, move items
else if ( options.is_dir ) {
// a selected folder can be the drop target of its own selection; it stays where it is
const items_to_drop = items_to_move.filter(item => $(item).attr('data-uid') !== $(el_item).attr('data-uid'));
if ( $(el_item).closest('.item-container').attr('data-path') === window.desktop_path ) {
delete window.desktop_item_positions[$(el_item).attr('data-uid')];
// the items leaving the desktop lose their saved positions, not the folder they land in
items_to_drop.forEach(item => delete window.desktop_item_positions[$(item).attr('data-uid')]);
window.save_desktop_item_positions();
}
window.move_items(items_to_move, $(el_item).attr('data-shortcut_to_path') !== '' ? $(el_item).attr('data-shortcut_to_path') : $(el_item).attr('data-path'));
window.move_items(items_to_drop, $(el_item).attr('data-shortcut_to_path') !== '' ? $(el_item).attr('data-shortcut_to_path') : $(el_item).attr('data-path'));
}
}
@@ -563,8 +569,9 @@ async function UIItem (options) {
},
over: function (event, ui) {
// Check hovering over an item that is VISIBILE
// A desktop item has no window, so it only counts while the pointer is over the desktop itself
const $event_parent_win = $(event.target).closest('.window');
if ( $event_parent_win.length > 0 && $event_parent_win.attr('data-id') !== $(window.mouseover_window).attr('data-id') )
if ( $event_parent_win.attr('data-id') !== $(window.mouseover_window).attr('data-id') )
{
return;
}
@@ -722,7 +729,7 @@ async function UIItem (options) {
}
});
$(document).on('click', function (e) {
$(document).on(`click.item-${item_id}`, function (e) {
if ( !$(e.target).hasClass('item') && !$(e.target).hasClass('item-name') && !$(e.target).hasClass('item-icon') ) {
skip_a_rename_click = true;
}
@@ -731,6 +738,10 @@ async function UIItem (options) {
skip_a_rename_click = true;
}
});
// a document handler outlives the row unless it is released with it
$(el_item).on('remove', function () {
$(document).off(`click.item-${item_id}`);
});
// --------------------------------------------------------
// Rename
@@ -738,6 +749,7 @@ async function UIItem (options) {
function rename () {
if ( rename_cancelled ) {
rename_cancelled = false;
$(el_item_name_editor).removeClass('item-name-editor-active');
return;
}
@@ -757,10 +769,12 @@ async function UIItem (options) {
UIAlert('The name ".." is not allowed, because it is a reserved name. Please choose another name.');
}
$(el_item_name).html(html_encode(truncate_filename(options.name)));
$(el_item_name).html(html_encode(truncate_filename($(el_item).attr('data-name'))));
$(el_item_name).show();
$(el_item_name_editor).val($(el_item).attr('data-name'));
$(el_item_name_editor).hide();
// the Delete shortcut skips an item whose editor is marked active
$(el_item_name_editor).removeClass('item-name-editor-active');
return;
}
// deactivate item name editable
@@ -806,8 +820,9 @@ async function UIItem (options) {
e.stopPropagation();
e.preventDefault();
rename_cancelled = true;
$(el_item_name_editor).removeClass('item-name-editor-active');
$(el_item_name_editor).hide();
$(el_item_name_editor).val(options.name);
$(el_item_name_editor).val($(el_item).attr('data-name'));
$(el_item_name).show();
}
});
@@ -866,6 +881,11 @@ async function UIItem (options) {
return;
}
// The menu is built after lookups that take time, so it opens where the pointer was when it was asked for.
const menu_position = (isMobile.phone || isMobile.tablet)
? { left: window.last_touch_x, top: window.last_touch_y }
: { left: window.mouseX, top: window.mouseY };
// if editing item name, preserve native context menu
if ( event.target === el_item_name_editor )
{
@@ -1137,7 +1157,7 @@ async function UIItem (options) {
// try to find suitable apps
const suitable_apps = await window.suggest_apps_for_fsentry({
uid: options.uid,
path: options.path,
path: $(el_item).attr('data-path'),
});
if ( suitable_apps && suitable_apps.length > 0 ) {
options.suggested_apps = suitable_apps;
@@ -1275,7 +1295,7 @@ async function UIItem (options) {
file_path: $(el_item).attr('data-path'),
file_uid: $(el_item).attr('data-uid'),
params: {
source_path: options.path,
source_path: $(el_item).attr('data-path'),
},
});
},
@@ -1322,7 +1342,7 @@ async function UIItem (options) {
}
else
{
window.trigger_download([options.path]);
window.trigger_download([$(el_item).attr('data-path')]);
}
},
});
@@ -1437,12 +1457,13 @@ async function UIItem (options) {
// -------------------------------------------
// Cut
// -------------------------------------------
if ( $(el_item).attr('data-immutable') === '0' ) {
// Trashed items leave the Trash through Restore; moved any other way, they'd keep their uid as their name.
if ( $(el_item).attr('data-immutable') === '0' && !is_trashed && may_restructure ) {
menu_items.push({
html: i18n('cut'),
onClick: function () {
window.clipboard_op = 'move';
window.clipboard = [options.path];
window.clipboard = [$(el_item).attr('data-path')];
},
});
}
@@ -1454,7 +1475,7 @@ async function UIItem (options) {
html: i18n('copy'),
onClick: function () {
window.clipboard_op = 'copy';
window.clipboard = [{ path: options.path }];
window.clipboard = [{ path: $(el_item).attr('data-path') }];
},
});
}
@@ -1502,7 +1523,7 @@ async function UIItem (options) {
base_dir,
options.appendTo,
options.shortcut_to === '' ? options.uid : options.shortcut_to,
options.shortcut_to_path === '' ? options.path : options.shortcut_to_path,
options.shortcut_to_path === '' ? $(el_item).attr('data-path') : options.shortcut_to_path,
);
},
});
@@ -1631,6 +1652,7 @@ async function UIItem (options) {
// Create ContextMenu
UIContextMenu({
parent_element: ($(options.appendTo).hasClass('desktop') ? undefined : options.appendTo),
position: menu_position,
items: menu_items,
});
+139 -122
View File
@@ -26,6 +26,7 @@ import UIWindowItemProperties from './UIWindowItemProperties.js';
import new_context_menu_item from '../helpers/newContextMenuItem.js';
import refresh_item_container from '../helpers/refreshItemContainer.js';
import { atPath } from '../helpers/pathElements.js';
import { resolve_typed_path } from '../helpers/expandHomePath.js';
import UIWindowAppFeedback from './UIWindowAppFeedback.js';
import launch_app from '../helpers/launchApp.js';
import publish_as_website from '../helpers/publishAsWebsite.js';
@@ -347,7 +348,18 @@ async function UIWindow (options) {
if ( ! items.some(item => item.path === window.home_path) ) {
items.unshift({ path: window.home_path, name: i18n('home') });
}
// A saved order stores the label it was rendered with; system folders follow the current language instead.
const system_folder_labels = {
[window.home_path]: i18n('home'),
[window.docs_path]: i18n('documents'),
[window.public_path]: i18n('public'),
[window.pictures_path]: i18n('pictures'),
[window.desktop_path]: i18n('desktop'),
[window.videos_path]: i18n('videos'),
[window.shared_path]: i18n('shared'),
};
for ( let item of items ) {
const label = system_folder_labels[item.path] ?? item.name;
let icon;
if ( item.path === window.home_path )
{
@@ -381,7 +393,7 @@ async function UIWindow (options) {
{
icon = window.icons['sidebar-folder.svg'];
}
h += `<div title="${html_encode(item.label)}" class="window-sidebar-item disable-user-select ${options.path === item.path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(item.path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(icon)}">${html_encode(item.name)}</div>`;
h += `<div title="${html_encode(label)}" class="window-sidebar-item disable-user-select ${options.path === item.path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(item.path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(icon)}">${html_encode(label)}</div>`;
}
}
h += '</div>';
@@ -406,7 +418,7 @@ async function UIWindow (options) {
// Path editor
h += `<input class="window-navbar-path-input" data-path="${html_encode(options.path)}" value="${html_encode(options.path)}" spellcheck="false"/>`;
// Layout settings
h += `<img class="window-navbar-layout-settings" src="${html_encode(options.layout === 'icons' ? window.icons['layout-icons.svg'] : window.icons['layout-list.svg'])}" draggable="false">`;
h += `<img class="window-navbar-layout-settings" src="${html_encode(options.layout === 'icons' ? window.icons['layout-icons.svg'] : window.icons['layout-list.svg'])}" draggable="false" title="${i18n('window_click_to_change_layout')}">`;
h += '</div>';
}
@@ -466,7 +478,7 @@ async function UIWindow (options) {
// Loading spinner
h += '<div class="explorer-loading-spinner">';
h += '<svg style="display:block; margin: 0 auto; " xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>';
h += '<svg style="display:block; margin: 0 auto; " xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>';
h += `<p class="explorer-loading-spinner-msg">${i18n('loading')}...</p>`;
h += '</div>';
}
@@ -1114,7 +1126,7 @@ async function UIWindow (options) {
// build item for context menu
items.push({
html: `<span>${history_item === window.home_path ? i18n('home') : html_encode(path.basename(history_item))}</span>`,
html: `<span>${html_encode(nav_history_label(history_item))}</span>`,
val: index,
onClick: async function (e) {
let history_index = e.value;
@@ -1187,7 +1199,7 @@ async function UIWindow (options) {
// build item for context menu
items.push({
html: `<span>${history_item === window.home_path ? 'Home' : html_encode(path.basename(history_item))}</span>`,
html: `<span>${html_encode(nav_history_label(history_item))}</span>`,
val: index,
onClick: async function (e) {
let history_index = e.value;
@@ -1333,6 +1345,8 @@ async function UIWindow (options) {
$(el_window).on('remove', function (e) {
// if on_close callback is set, call it
options.on_close?.();
delete window.window_nav_history[win_id];
delete window.window_nav_history_current_position[win_id];
});
// --------------------------------------------------------
@@ -1426,18 +1440,18 @@ async function UIWindow (options) {
.on('beforedrag', evt => {
})
.on('start', ({ store, event }) => {
if ( !event.ctrlKey && !event.metaKey ) {
// Get the scroll position of the window body
const scrollLeft = $(el_window_body).scrollLeft();
const scrollTop = $(el_window_body).scrollTop();
const scrollLeft = $(el_window_body).scrollLeft();
const scrollTop = $(el_window_body).scrollTop();
// Get the window body's bounding rect relative to the viewport
const windowBodyRect = el_window_body.getBoundingClientRect();
// Get the window body's bounding rect relative to the viewport
const windowBodyRect = el_window_body.getBoundingClientRect();
// Calculate position relative to the window body (accounting for scroll)
selection_area_start_x = window.mouseX - windowBodyRect.left + scrollLeft;
selection_area_start_y = window.mouseY - windowBodyRect.top + scrollTop;
// Calculate position relative to the window body (accounting for scroll)
selection_area_start_x = window.mouseX - windowBodyRect.left + scrollLeft;
selection_area_start_y = window.mouseY - windowBodyRect.top + scrollTop;
if ( !event.ctrlKey && !event.metaKey ) {
for ( const el of store.stored ) {
el.classList.remove('item-selected');
}
@@ -1512,7 +1526,7 @@ async function UIWindow (options) {
window.update_explorer_footer_selected_items_count(el_window);
// If this is openFileDialog, enable/disable the Open button accordingly
if ( options.is_openFileDialog && $(el_window).find('.item-selected').length )
if ( options.is_openFileDialog && $(el_window).find('.item-selected[data-is_dir="0"]').length )
{
$(el_openfiledialog_open_btn).removeClass('disabled');
}
@@ -1523,7 +1537,7 @@ async function UIWindow (options) {
})
.on('stop', ({ store, event }) => {
// If this is openFileDialog, enable/disable the Open button accordingly
if ( options.is_openFileDialog && $(el_window).find('.item-selected').length )
if ( options.is_openFileDialog && $(el_window).find('.item-selected[data-is_dir="0"]').length )
{
$(el_openfiledialog_open_btn).removeClass('disabled');
}
@@ -1532,6 +1546,20 @@ async function UIWindow (options) {
$(el_openfiledialog_open_btn).addClass('disabled');
}
});
$(document).on(`mouseup.window-selection-${win_id}`, function () {
if ( selection_area ) {
$(selection_area).hide();
$(selection_area).remove();
selection_area = null;
}
});
// viselect and the handler above listen on document, so they outlive the window unless released
$(el_window).on('remove', function () {
$(document).off(`mouseup.window-selection-${win_id}`);
selection.destroy();
});
}
// --------------------------------------------------------
@@ -2679,37 +2707,8 @@ async function UIWindow (options) {
{
html: i18n('empty_trash'),
disabled: false,
onClick: async function () {
// TODO: Merge this with window.empty_trash()
const alert_resp = await UIAlert({
message: i18n('empty_trash_confirmation'),
buttons: [
{
label: i18n('yes'),
value: 'yes',
type: 'primary',
},
{
label: i18n('no'),
value: 'no',
},
],
});
if ( alert_resp === 'no' )
{
return;
}
// todo this has to be case-insensitive but the `i` selector doesn't work on ^=
$(`.item[data-path^="${html_encode(window.trash_path)}/"]`).each(function () {
window.delete_item(this);
});
// update other clients
if ( window.socket ) {
window.socket.emit('trash.is_empty', { is_empty: true });
}
// use the 'empty trash' icon
window.update_trash_icons(true);
onClick: function () {
window.empty_trash();
},
},
],
@@ -2845,14 +2844,6 @@ async function UIWindow (options) {
});
}
$(document).on('mouseup', function (e) {
if ( selection_area ) {
$(selection_area).hide();
$(selection_area).remove();
selection_area = null;
}
});
//set styles
$(el_window_body).css(options.body_css);
@@ -3072,7 +3063,21 @@ $(document).on('blur', '.window-navbar-path-input', function (e) {
$(document).on('keyup', '.window-navbar-path-input', function (e) {
if ( e.key === 'Enter' || e.keyCode === 13 ) {
window.update_window_path($(e.target).closest('.window'), $(e.target).val());
const $el_window = $(e.target).closest('.window');
const win_id = $el_window.attr('data-id');
const current_path = $el_window.attr('data-path');
// Other code matches containers by exact path, so navigate to the canonical form.
const target_path = resolve_typed_path($(e.target).val(), {
current_path,
home_path: window.home_path,
shared_path: window.shared_path,
});
if ( target_path !== current_path && window.window_nav_history[win_id] ) {
window.window_nav_history[win_id] = window.window_nav_history[win_id].slice(0, window.window_nav_history_current_position[win_id] + 1);
window.window_nav_history[win_id].push(target_path);
window.window_nav_history_current_position[win_id] = window.window_nav_history[win_id].length - 1;
}
window.update_window_path($el_window, target_path);
$(e.target).hide();
$(e.target).siblings('.window-navbar-path').show().select();
}
@@ -3336,6 +3341,14 @@ window.navbar_path = (abs_path) => {
return str;
};
// The root and the Shared view have no basename to show in the Back/Forward menus.
const nav_history_label = (history_path) => {
if ( history_path === window.home_path ) return i18n('home');
if ( history_path === '/' ) return window.root_dirname;
if ( history_path === window.shared_path ) return i18n('shared');
return path.basename(history_path);
};
window.update_window_path = async function (el_window, target_path) {
const win_id = $(el_window).attr('data-id');
const el_window_navbar_forward_btn = $(el_window).find('.window-navbar-btn-forward');
@@ -3393,6 +3406,8 @@ window.update_window_path = async function (el_window, target_path) {
// empty body to be filled with the results of /readdir
$(el_window_body).find('.item').removeItems();
// nothing is selected in the folder being opened
$(el_window).find('.openfiledialog-open-btn').addClass('disabled');
// add the 'Detail View' table header
if ( $(el_window).find('.explore-table-headers').length === 0 )
@@ -3435,6 +3450,8 @@ window.update_window_path = async function (el_window, target_path) {
$(el_window).attr('data-path', target_path);
$(el_window).attr('data-name', path.basename(target_path));
$(el_window_navbar_path_input).val(target_path);
$(el_window_navbar_path_input).attr('data-path', target_path);
// /stat
if ( target_path === window.shared_path ) {
@@ -3445,72 +3462,72 @@ window.update_window_path = async function (el_window, target_path) {
$(el_window).find('.window-head-icon').attr('src', window.icons['shared.svg']);
}
else if ( target_path !== '/' ) {
try {
puter.fs.stat({ path: target_path, consistency: 'eventual' }).then(fsentry => {
// the window may have moved on while this stat was in flight
if ( $(el_window).attr('data-path') !== target_path ) {
return;
}
$(el_window).removeClass(`window-${ $(el_window).attr('data-uid')}`);
$(el_window).addClass(`window-${ fsentry.id}`);
$(el_window).attr('data-uid', fsentry.id);
$(el_window).attr('data-sort_by', fsentry.sort_by ?? 'name');
$(el_window).attr('data-sort_order', fsentry.sort_order ?? 'asc');
$(el_window).attr('data-layout', fsentry.layout ?? 'icons');
$(el_window_item_container).attr('data-uid', fsentry.id);
// title - use i18n for system directories
if ( target_path === window.home_path )
{
$(el_window).find('.window-head-title').text(i18n('home'));
}
else if ( target_path === window.desktop_path )
{
$(el_window).find('.window-head-title').text(i18n('desktop'));
}
else if ( target_path === window.docs_path || target_path === window.documents_path )
{
$(el_window).find('.window-head-title').text(i18n('documents'));
}
else if ( target_path === window.pictures_path )
{
$(el_window).find('.window-head-title').text(i18n('pictures'));
}
else if ( target_path === window.videos_path )
{
$(el_window).find('.window-head-title').text(i18n('videos'));
}
else if ( target_path === window.public_path )
{
$(el_window).find('.window-head-title').text(i18n('public'));
}
else if ( target_path === window.trash_path )
{
$(el_window).find('.window-head-title').text(i18n('trash'));
}
else
{
$(el_window).find('.window-head-title').text(fsentry.name);
}
// data-name
$(el_window).attr('data-name', fsentry.name);
// data-path
$(el_window).attr('data-path', target_path);
$(el_window_navbar_path_input).val(target_path);
$(el_window_navbar_path_input).attr('data-path', target_path);
// update layout
window.update_window_layout(el_window, fsentry.layout);
// update explore header if in details view
if ( fsentry.layout === 'details' ) {
window.update_details_layout_sort_visuals(el_window, fsentry.sort_by, fsentry.sort_order);
}
});
} catch ( err ) {
UIAlert(err.responseText);
// todo optim: this is dumb because updating the window should only happen if this /readdir request is successful,
// in that case there is no need for using update_window_path on error!!
window.update_window_path(el_window, old_path);
}
puter.fs.stat({ path: target_path, consistency: 'eventual' }).then(fsentry => {
// the window may have moved on while this stat was in flight
if ( $(el_window).attr('data-path') !== target_path ) {
return;
}
$(el_window).removeClass(`window-${ $(el_window).attr('data-uid')}`);
$(el_window).addClass(`window-${ fsentry.id}`);
$(el_window).attr('data-uid', fsentry.id);
$(el_window).attr('data-sort_by', fsentry.sort_by ?? 'name');
$(el_window).attr('data-sort_order', fsentry.sort_order ?? 'asc');
$(el_window).attr('data-layout', fsentry.layout ?? 'icons');
$(el_window_item_container).attr('data-uid', fsentry.id);
// title - use i18n for system directories
if ( target_path === window.home_path )
{
$(el_window).find('.window-head-title').text(i18n('home'));
}
else if ( target_path === window.desktop_path )
{
$(el_window).find('.window-head-title').text(i18n('desktop'));
}
else if ( target_path === window.docs_path || target_path === window.documents_path )
{
$(el_window).find('.window-head-title').text(i18n('documents'));
}
else if ( target_path === window.pictures_path )
{
$(el_window).find('.window-head-title').text(i18n('pictures'));
}
else if ( target_path === window.videos_path )
{
$(el_window).find('.window-head-title').text(i18n('videos'));
}
else if ( target_path === window.public_path )
{
$(el_window).find('.window-head-title').text(i18n('public'));
}
else if ( target_path === window.trash_path )
{
$(el_window).find('.window-head-title').text(i18n('trash'));
}
else
{
$(el_window).find('.window-head-title').text(fsentry.name);
}
// data-name
$(el_window).attr('data-name', fsentry.name);
// data-path
$(el_window).attr('data-path', target_path);
$(el_window_navbar_path_input).val(target_path);
$(el_window_navbar_path_input).attr('data-path', target_path);
// update layout
window.update_window_layout(el_window, fsentry.layout);
// update explore header if in details view
if ( fsentry.layout === 'details' ) {
window.update_details_layout_sort_visuals(el_window, fsentry.sort_by, fsentry.sort_order);
}
}).catch(() => {
if ( $(el_window).attr('data-path') !== target_path ) {
return;
}
// The folder couldn't be read, so the window must stop identifying as the previous one.
$(el_window).removeClass(`window-${ $(el_window).attr('data-uid')}`);
$(el_window).attr('data-uid', 'null');
$(el_window).find('.window-head-title').text(path.basename(target_path));
});
}
// path is '/' (global root)
else {
@@ -3866,7 +3883,7 @@ window.scale_window = (el_window) => {
window.update_explorer_footer_item_count = function (el_window) {
//update dir count in explorer footer
let item_count = $(el_window).find('.item').length;
let item_count = $(el_window).find('.item:not(.item-hidden)').length;
$(el_window).find('.explorer-footer .explorer-footer-item-count').html(`${item_count } ${i18n('item')}${ item_count == 0 || item_count > 1 ? `${i18n('plural_suffix')}` : ''}`);
};
+29 -2
View File
@@ -500,8 +500,9 @@ input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, sel
.explore-table-headers {
display: none;
width: 100%;
min-width: 795px;
/* the negative margin below shifts it left by the body's padding */
width: calc(100% + 10px);
min-width: 805px;
height: 25px;
border-bottom: 1px solid rgb(226, 226, 226);
background-color: #fff;
@@ -534,6 +535,7 @@ span.header-sort-icon img {
margin-left: 15px;
color: #555c61;
display: inline-block;
cursor: pointer;
}
.explore-table-headers-th-active {
@@ -842,6 +844,14 @@ span.header-sort-icon img {
color: white;
}
.device-desktop .item-container-details .item-selected.item-blurred {
background-color: #dbdada;
}
.device-desktop .item-container-details .item-selected.item-blurred .item-attr {
color: rgb(73, 73, 73);
}
.item-container-details .item-attr--modified {
left: 350px;
}
@@ -1199,6 +1209,22 @@ span.header-sort-icon img {
cursor: default;
}
/* A path too long for the bar is cut at the root end, so the current folder stays visible. */
.window-navbar-path {
display: flex;
justify-content: flex-end;
align-items: baseline;
min-height: 37px;
}
.window-navbar-path > * {
flex: none;
}
.window-navbar-path > :last-child {
margin-right: auto;
}
.device-phone .window-navbar-path {
display: none;
}
@@ -1208,6 +1234,7 @@ span.header-sort-icon img {
height: 30px;
margin-left: 10px;
margin-top: 3px;
cursor: pointer;
}
.device-phone .window-navbar-layout-settings {
+105 -72
View File
@@ -23,7 +23,7 @@ import { archiveEntryPath } from './helpers/archiveEntryPath.js';
import get_html_element_from_options from './helpers/getHtmlElementFromOptions.js';
import globToRegExp from './helpers/globToRegExp.js';
import item_icon from './helpers/itemIcon.js';
import { atPath, retargetPaths, underPath } from './helpers/pathElements.js';
import { atPath, retargetNavHistory, retargetPaths, underPath } from './helpers/pathElements.js';
import { is_owned_by_me, trash_path_for } from './helpers/pathOwner.js';
import { invalidate_shared_roots } from './helpers/sharedAccess.js';
import truncate_filename from './helpers/truncateFilename.js';
@@ -38,6 +38,7 @@ import mime from './lib/mime.js';
import path from './lib/path.js';
import UIAlert from './UI/UIAlert.js';
import UIItem from './UI/UIItem.js';
import { compareEntries, compareNames } from './UI/Dashboard/sortEntries.js';
import UIAuthLogin from './UI/Auth/UIAuthLogin.js';
import UIWindowProgress from './UI/UIWindowProgress.js';
import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js';
@@ -1120,48 +1121,19 @@ window.sort_items = (item_container, sort_by, sort_order) => {
sort_order = 'asc';
}
const direction = sort_order === 'asc' ? 1 : -1;
$(item_container).find('.item[data-sortable="true"]').detach().sort(function (a, b) {
// Name
if ( !sort_by || sort_by === 'name' ) {
if ( a.dataset.name.toLowerCase() < b.dataset.name.toLowerCase() ) {
return (sort_order === 'asc' ? -1 : 1);
}
if ( a.dataset.name.toLowerCase() > b.dataset.name.toLowerCase() ) {
return (sort_order === 'asc' ? 1 : -1);
}
return 0;
const name_a = a.dataset.name;
const name_b = b.dataset.name;
if ( sort_by === 'type' ) {
const by_extension = compareNames(path.extname(name_a).toLowerCase(), path.extname(name_b).toLowerCase());
return direction * (by_extension || compareNames(name_a, name_b));
}
// Size
else if ( sort_by === 'size' ) {
if ( parseInt(a.dataset.size) < parseInt(b.dataset.size) ) {
return (sort_order === 'asc' ? -1 : 1);
}
if ( parseInt(a.dataset.size) > parseInt(b.dataset.size) ) {
return (sort_order === 'asc' ? 1 : -1);
}
return 0;
}
// Modified
else if ( sort_by === 'modified' ) {
if ( parseInt(a.dataset.modified) < parseInt(b.dataset.modified) ) {
return (sort_order === 'asc' ? -1 : 1);
}
if ( parseInt(a.dataset.modified) > parseInt(b.dataset.modified) ) {
return (sort_order === 'asc' ? 1 : -1);
}
return 0;
}
// Type
else if ( sort_by === 'type' ) {
if ( path.extname(a.dataset.name.toLowerCase()) < path.extname(b.dataset.name.toLowerCase()) ) {
return (sort_order === 'asc' ? -1 : 1);
}
if ( path.extname(a.dataset.name.toLowerCase()) > path.extname(b.dataset.name.toLowerCase()) ) {
return (sort_order === 'asc' ? 1 : -1);
}
return 0;
}
return direction * compareEntries(
{ name: name_a, size: parseInt(a.dataset.size), modified: parseInt(a.dataset.modified) },
{ name: name_b, size: parseInt(b.dataset.size), modified: parseInt(b.dataset.modified) },
sort_by,
);
}).appendTo(item_container);
};
@@ -1169,10 +1141,18 @@ window.show_or_hide_files = (item_containers) => {
const show_hidden_files = window.user_preferences.show_hidden_files;
const class_to_add = show_hidden_files ? 'item-revealed' : 'item-hidden';
const class_to_remove = show_hidden_files ? 'item-hidden' : 'item-revealed';
$(item_containers)
const $dotfiles = $(item_containers)
.find('.item')
.filter((_, item) => item.dataset.name.startsWith('.'))
.removeClass(class_to_remove).addClass(class_to_add);
// an item the user can no longer see must not stay in the selection
if ( ! show_hidden_files ) {
$dotfiles.removeClass('item-selected');
}
$(item_containers).closest('.window').each(function () {
window.update_explorer_footer_item_count(this);
window.update_explorer_footer_selected_items_count(this);
});
};
window.create_folder = async (basedir, appendto_element) => {
@@ -1275,6 +1255,9 @@ window.available_templates = () => {
};
window.create_shortcut = async (filename, is_dir, basedir, appendto_element, shortcut_to, shortcut_to_path) => {
// The Shared view is a query, not a directory — nothing can be created in it.
if ( basedir === window.shared_path ) return;
const extname = path.extname(filename);
const basename = `${path.basename(filename, extname) } - Shortcut`;
filename = basename + extname;
@@ -1309,6 +1292,8 @@ window.copy_clipboard_items = async function (dest_path, dest_container_element)
let progwin;
let latest_status;
const arm_progwin = () => setTimeout(async () => {
// re-armed after a dialog while the window from before it is still open
if ( progwin ) return;
progwin = await UIWindowProgress({
operation_id: copy_op_id,
on_cancel: () => {
@@ -1442,6 +1427,9 @@ window.copy_clipboard_items = async function (dest_path, dest_container_element)
* @param {string} dest_path - Destination path to copy items to
*/
window.copy_items = function (el_items, dest_path) {
// The Shared view is a query, not a directory — nothing can be copied into it.
if ( dest_path === window.shared_path ) return;
let copy_op_id = window.operation_id++;
let overwrite_all = false;
(async () => {
@@ -1451,6 +1439,8 @@ window.copy_items = function (el_items, dest_path) {
let progwin;
let latest_status;
const arm_progwin = () => setTimeout(async () => {
// re-armed after a dialog while the window from before it is still open
if ( progwin ) return;
progwin = await UIWindowProgress({
operation_id: copy_op_id,
on_cancel: () => {
@@ -1625,20 +1615,37 @@ window.delete_item = async function (el_item, descendants_only = false) {
}
};
window.move_clipboard_items = function (el_target_container, target_path) {
window.move_clipboard_items = async function (el_target_container, target_path) {
let dest_path = target_path === undefined ? $(el_target_container).attr('data-path') : target_path;
let el_items = [];
if ( window.clipboard.length > 0 ) {
for ( let i = 0; i < window.clipboard.length; i++ ) {
el_items.push(atPath('.item', window.clipboard[i]));
}
if ( el_items.length > 0 )
{
window.move_items(el_items, dest_path);
}
}
const cut_paths = window.clipboard;
window.clipboard = [];
// A cut item's row is gone once its folder is no longer displayed, so
// move_items gets a stand-in carrying what it reads off a row.
const el_items = (await Promise.all(cut_paths.map(async (cut_path) => {
const $el_item = atPath('.item', cut_path);
if ( $el_item.length > 0 ) {
return $el_item;
}
try {
const fsentry = await puter.fs.stat({ path: cut_path, consistency: 'eventual' });
return $('<div>').attr({
'data-uid': fsentry.uid,
'data-path': cut_path,
'data-name': fsentry.name,
'data-is_dir': fsentry.is_dir ? '1' : '0',
'data-immutable': (fsentry.immutable || fsentry.writable === false) ? '1' : '0',
'data-metadata': fsentry.metadata ?? '',
});
} catch ( e ) {
// the item no longer exists, so there is nothing to move
return null;
}
}))).filter(Boolean);
if ( el_items.length > 0 ) {
window.move_items(el_items, dest_path);
}
};
function downloadFile (url, postData = {}) {
@@ -1696,7 +1703,7 @@ window.trigger_download = (paths) => {
let urls = [];
for ( let index = 0; index < paths.length; index++ ) {
urls.push({
download: `${window.origin }/down?path=${ paths[index]}`,
download: `${window.origin }/down?path=${ encodeURIComponent(paths[index])}`,
filename: path.basename(paths[index]),
});
}
@@ -1802,6 +1809,8 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
let progwin;
let latest_status;
const arm_progwin = () => setTimeout(async () => {
// re-armed after a dialog while the window from before it is still open
if ( progwin ) return;
progwin = await UIWindowProgress({
operation_id: move_op_id,
on_cancel: () => {
@@ -1849,7 +1858,8 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
if ( path.dirname($(el_item).attr('data-path')) === item_dest_path ) {
// pause the progress-window timer while waiting for the user
clearTimeout(progwin_timeout);
await UIAlert(`<p>Moving <strong>${html_encode($(el_item).attr('data-name'))}</strong></p>Cannot move item to its current location.`);
// UIAlert escapes the message itself
await UIAlert(`<p>Moving <strong>${$(el_item).attr('data-name')}</strong></p>Cannot move item to its current location.`);
progwin_timeout = arm_progwin();
continue;
@@ -1923,7 +1933,7 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
}
// moving an item into a trashed directory? deny.
else if ( dest_path.startsWith(window.trash_path) ) {
else if ( dest_path === window.trash_path || dest_path.startsWith(`${window.trash_path}/`) ) {
// the pending timer would otherwise open an orphan
// progress window after the operation already bailed
clearTimeout(progwin_timeout);
@@ -2008,6 +2018,7 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
underPath('.window', moved_from_path).add(atPath('.window', moved_from_path)).each(function () {
window.update_window_path(this, path.join(item_dest_path, fsentry.name) + $(this).attr('data-path').slice(moved_from_path.length));
});
retargetNavHistory(moved_from_path, fsentry.path);
}
// The dashboard's Back/Forward follows a moved folder, and
@@ -2035,14 +2046,19 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
// if this is trash, get original name from item metadata
fsentry.name = metadata?.original_name || fsentry.name;
const moved_item_icon = await item_icon(fsentry);
const $dest_containers = atPath('.item-container', item_dest_path);
// the item.moved echo of this move can render the row first
$dest_containers.find(`.item[data-uid='${fsentry.uid}']`).removeItems();
// create new item on matching containers
const options = {
appendTo: atPath('.item-container', item_dest_path),
appendTo: $dest_containers,
immutable: fsentry.immutable || (fsentry.writable === false),
associated_app_name: fsentry.associated_app?.name,
uid: fsentry.uid,
path: fsentry.path,
icon: await item_icon(fsentry),
icon: moved_item_icon,
name: is_trashing ? $(el_item).attr('data-name') : fsentry.name,
is_dir: fsentry.is_dir,
size: fsentry.size,
@@ -2137,7 +2153,7 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
item_with_same_name_already_exists = false;
// error message after source item has reappeared
$(el_item).show(0, function () {
UIAlert(`<p>Moving <strong>${html_encode($(el_item).attr('data-name'))}</strong></p>${err.message ?? ''}`);
UIAlert(`<p>Moving <strong>${$(el_item).attr('data-name')}</strong></p>${err.message ?? ''}`);
});
break;
@@ -2400,6 +2416,9 @@ window.upload_items = async function (items, dest_path) {
let upload_progress_window;
let opid;
// The Shared view is a query, not a directory — nothing can be uploaded into it.
if ( dest_path === window.shared_path ) return;
if ( dest_path == window.trash_path ) {
UIAlert('Uploading to trash is not allowed!');
return;
@@ -2879,13 +2898,15 @@ window.unzipItem = async function (itemPath) {
terminateOp = fflate.unzip(file, async (err, unzipped) => {
currentProgress += window.zippingProgressConfig.ZIPPING;
progwin?.set_progress(currentProgress.toPrecision(2));
if ( err ) {
UIAlert(e.message);
// close progress window
const close_progwin = (hide_delay) => {
clearTimeout(progwin_timeout);
setTimeout(() => {
progwin?.close();
}, Math.max(0, window.copy_progress_hide_delay - (Date.now() - start_ts)));
}, Math.max(0, hide_delay - (Date.now() - start_ts)));
};
if ( err ) {
UIAlert(err.message);
close_progwin(window.copy_progress_hide_delay);
} else {
const rootdir = await puter.fs.mkdir(`${path.dirname(filePath) }/${ path.basename(filePath, '.zip')}`, { dedupeName: true });
let perItemProgress = window.zippingProgressConfig.WRITING / Object.keys(unzipped).length;
@@ -2903,6 +2924,10 @@ window.unzipItem = async function (itemPath) {
UIAlert(e.message);
}
});
// an archive with no files leaves nothing to upload, so nothing else would end the operation
if ( queuedFileWrites.length === 0 ) {
close_progwin(window.unzip_progress_hide_delay);
}
queuedFileWrites.length && puter.fs.upload(
// what to upload
queuedFileWrites,
@@ -2914,7 +2939,7 @@ window.unzipItem = async function (itemPath) {
generateThumbnails: true,
thumbnailGenerator: createUploadThumbnailGenerator(),
progress: async function (operation_id, op_progress) {
progwin.set_progress(op_progress);
progwin?.set_progress(op_progress);
// update title if window is not visible
if ( document.visibilityState !== 'visible' ) {
update_title_based_on_uploads();
@@ -2922,11 +2947,10 @@ window.unzipItem = async function (itemPath) {
},
success: async function (items) {
progwin?.set_progress(window.zippingProgressConfig.TOTAL.toPrecision(2));
// close progress window
clearTimeout(progwin_timeout);
setTimeout(() => {
progwin?.close();
}, Math.max(0, window.unzip_progress_hide_delay - (Date.now() - start_ts)));
close_progwin(window.unzip_progress_hide_delay);
},
error: function () {
close_progwin(window.unzip_progress_hide_delay);
},
},
);
@@ -3158,6 +3182,13 @@ window.untarItem = async function (itemPath) {
}
}
// an archive with no files leaves nothing to upload, so nothing else would end the operation
if ( queuedFileWrites.length === 0 ) {
clearTimeout(progwin_timeout);
setTimeout(() => {
progwin?.close();
}, Math.max(0, window.unzip_progress_hide_delay - (Date.now() - start_ts)));
}
queuedFileWrites.length && puter.fs.upload(
queuedFileWrites,
`${rootdir.path }/`,
@@ -3166,7 +3197,7 @@ window.untarItem = async function (itemPath) {
generateThumbnails: true,
thumbnailGenerator: createUploadThumbnailGenerator(),
progress: async function (operation_id, op_progress) {
progwin.set_progress(op_progress);
progwin?.set_progress(op_progress);
if ( document.visibilityState !== 'visible' ) {
update_title_based_on_uploads();
}
@@ -3385,7 +3416,9 @@ window.undo_last_action = async () => {
delete: () => window.undo_delete(data),
rename: () => {
const { options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, onDone } = data;
window.rename_file(options, old_name, new_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, true, onDone);
// the item now sits at its renamed path, which is what this rename moves away from
const renamed_path = path.join(path.dirname(old_path), new_name);
window.rename_file(options, old_name, new_name, renamed_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, true, onDone);
},
};
+23 -1
View File
@@ -37,7 +37,7 @@ globalThis.html_encode = value => jQuery('<div>').text(value).html();
await import('./helpers.js');
const { default: apply_item_added_to_containers } = await import('./helpers/applyItemAddedToContainers.js');
const { default: UIItem } = await import('./UI/UIItem.js');
const { atPath, retargetPaths, underPath } = await import('./helpers/pathElements.js');
const { atPath, retargetNavHistory, retargetPaths, underPath } = await import('./helpers/pathElements.js');
// A plain name, then names holding characters a selector or HTML treats specially.
const NAMES = ['Reports 2024', 'R&D', "Dan's files", 'the "big" one', 'a<b>c', 'back\\slash'];
@@ -146,3 +146,25 @@ describe.each([undefined, null, ''])('a lookup for the path %s', (missing) => {
expect(underPath('.window', missing)).toHaveLength(0);
});
});
describe('retargetNavHistory', () => {
it('rewrites entries at or inside the old path and leaves the rest', () => {
window.window_nav_history = {
1: ['/alice', '/alice/A', '/alice/A/sub', '/alice/AB', 'puter://shared'],
2: ['/alice/Documents'],
};
retargetNavHistory('/alice/A', '/alice/B');
expect(window.window_nav_history[1]).toEqual(['/alice', '/alice/B', '/alice/B/sub', '/alice/AB', 'puter://shared']);
expect(window.window_nav_history[2]).toEqual(['/alice/Documents']);
});
it('is applied by retargetPaths', () => {
window.window_nav_history = { 1: ['/alice/A/sub'] };
retargetPaths('/alice/A', '/alice/B', 'B');
expect(window.window_nav_history[1]).toEqual(['/alice/B/sub']);
});
});
+19
View File
@@ -17,6 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import path from '../lib/path.js';
/**
* Expands a user-facing path into the absolute path the filesystem API takes:
* `~` and `~/x` resolve under the user's home, a bare relative path is read as
@@ -39,3 +41,20 @@ export const expand_home_path = (fspath, home_path) => {
if ( fspath.startsWith('/') ) return fspath;
return `${home}/${fspath}`;
};
/**
* Turns what a user typed into an address bar into the canonical absolute
* path: trimmed, home-expanded, with `.`/`..`, doubled and trailing slashes
* resolved. An empty entry keeps the current path.
*
* @param {string} typed_path - the raw text the user entered
* @param {{ current_path: string, home_path: string, shared_path: string }} context
* @returns {string} the path to navigate to
*/
export const resolve_typed_path = (typed_path, { current_path, home_path, shared_path }) => {
const trimmed = typeof typed_path === 'string' ? typed_path.trim() : '';
if ( trimmed === '' ) return current_path;
// The Shared view is a pseudo-path, not a filesystem one.
if ( trimmed === shared_path ) return shared_path;
return path.resolve('/', expand_home_path(trimmed, home_path));
};
+26 -1
View File
@@ -18,7 +18,7 @@
*/
import { describe, it, expect } from 'vitest';
import { expand_home_path } from './expandHomePath.js';
import { expand_home_path, resolve_typed_path } from './expandHomePath.js';
describe('expand_home_path', () => {
it('resolves a tilde path under the home path', () => {
@@ -57,3 +57,28 @@ describe('expand_home_path', () => {
expect(expand_home_path(null, '/alice')).toBe(null);
});
});
describe('resolve_typed_path', () => {
const context = { current_path: '/alice/Videos', home_path: '/alice', shared_path: 'puter://shared' };
it('canonicalizes trailing, doubled and dot segments', () => {
expect(resolve_typed_path('/alice/Documents/', context)).toBe('/alice/Documents');
expect(resolve_typed_path('/alice//Documents', context)).toBe('/alice/Documents');
expect(resolve_typed_path('/alice/Documents/../Pictures', context)).toBe('/alice/Pictures');
expect(resolve_typed_path('/', context)).toBe('/');
});
it('trims whitespace and expands home-relative entries', () => {
expect(resolve_typed_path(' ~/Documents ', context)).toBe('/alice/Documents');
expect(resolve_typed_path('Documents', context)).toBe('/alice/Documents');
});
it('keeps the current path for an empty entry', () => {
expect(resolve_typed_path('', context)).toBe('/alice/Videos');
expect(resolve_typed_path(' ', context)).toBe('/alice/Videos');
});
it('passes the Shared view through untouched', () => {
expect(resolve_typed_path('puter://shared', context)).toBe('puter://shared');
});
});
+1 -1
View File
@@ -100,7 +100,7 @@ const item_icon = async (fsentry) => {
// --------------------------------------------------
// .app files
// --------------------------------------------------
else if ( fsentry.name && fsentry.name.toLowerCase().endsWith('.app') ) {
else if ( !fsentry.is_dir && fsentry.name && fsentry.name.toLowerCase().endsWith('.app') ) {
try {
const content = await puter.fs.read({ path: fsentry.path });
const text = typeof content === 'string' ? content : await content.text();
+6 -1
View File
@@ -145,9 +145,14 @@ const new_context_menu_item = function (dirname, append_to_element) {
canvas.width = 800;
canvas.height = 600;
// JPEG has no transparency, so the blank image needs a background
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
window.create_file({ dirname: dirname, append_to_element: append_to_element, name: 'New Image.jpg', content: blob });
});
}, 'image/jpeg');
},
},
// Worker
+2 -1
View File
@@ -19,8 +19,9 @@
import UIWindow from '../UI/UIWindow.js';
import UIAlert from '../UI/UIAlert.js';
import i18n from '../i18n/i18n.js';
import launch_app from './launchApp.js';
// installs the global i18n()
import '../i18n/i18n.js';
import path from '../lib/path.js';
import item_icon from './itemIcon.js';
import { resolve_default_app } from './defaultApp.js';
+15 -1
View File
@@ -23,7 +23,6 @@ import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
vi.mock('../UI/UIWindow.js', () => ({ default: vi.fn() }));
vi.mock('../UI/UIAlert.js', () => ({ default: vi.fn(async () => undefined) }));
vi.mock('../i18n/i18n.js', () => ({ default: key => key }));
vi.mock('./launchApp.js', () => ({ default: vi.fn(async () => ({ launchResult: { launched: true } })) }));
vi.mock('./itemIcon.js', () => ({ default: vi.fn() }));
vi.mock('./defaultApp.js', () => ({ resolve_default_app: vi.fn(async () => null) }));
@@ -113,3 +112,18 @@ describe('open_item with a new tab', () => {
expect(tab.close).not.toHaveBeenCalled();
});
});
describe('open_item with no app for the file', () => {
it('offers to download it', async () => {
window.is_dashboard_mode = false;
window.trigger_download = vi.fn();
UIAlert.mockResolvedValueOnce('download_file');
await open_item({ item: row({ name: 'data.bin', path: '/user/data.bin' }) });
expect(UIAlert).toHaveBeenCalledTimes(1);
expect(UIAlert.mock.calls[0][1].map(button => button.value ?? button.label)).toEqual(['download_file', 'Cancel']);
expect(window.trigger_download).toHaveBeenCalledWith(['/user/data.bin']);
});
});
+19
View File
@@ -70,4 +70,23 @@ export const retargetPaths = (oldPath, newPath, newName) => {
$inside.each(function () {
$(this).attr('data-path', newPath + $(this).attr('data-path').slice(oldPath.length));
});
retargetNavHistory(oldPath, newPath);
};
/**
* Points every window's Back/Forward entries at `oldPath`, or inside it, to
* `newPath`, so they still lead somewhere after a rename or move.
*
* @param {string} oldPath
* @param {string} newPath
*/
export const retargetNavHistory = (oldPath, newPath) => {
if ( ! oldPath ) return;
const prefix = `${oldPath}/`;
for ( const history of Object.values(window.window_nav_history ?? {}) ) {
history.forEach((entry, index) => {
if ( entry === oldPath ) history[index] = newPath;
else if ( entry.startsWith(prefix) ) history[index] = newPath + entry.slice(oldPath.length);
});
}
};
+16 -1
View File
@@ -72,9 +72,13 @@ const refresh_item_container = function (el_item_container, options) {
}
// show the loading spinner
$(loading_spinner).find('.explorer-loading-spinner-msg').text(`${i18n('loading')}...`);
$(loading_spinner).show();
setTimeout(function () {
$(loading_spinner).find('.explorer-loading-spinner-msg').html('Taking a little longer than usual. Please wait...');
// only a load that is still running has taken long
if ( is_current() && $(loading_spinner).is(':visible') ) {
$(loading_spinner).find('.explorer-loading-spinner-msg').text(i18n('taking_longer_than_usual'));
}
}, 3000);
}, 1000);
@@ -98,6 +102,8 @@ const refresh_item_container = function (el_item_container, options) {
return;
}
if ( el_window ) {
// the class is how a deleted folder's windows are found, and a window opened by path starts without it
$(el_window).removeClass(`window-${$(el_window).attr('data-uid')}`).addClass(`window-${fsentry.id}`);
$(el_window).attr('data-uid', fsentry.id);
$(el_window).attr('data-sort_by', fsentry.sort_by ?? 'name');
$(el_window).attr('data-sort_order', fsentry.sort_order ?? 'asc');
@@ -120,6 +126,8 @@ const refresh_item_container = function (el_item_container, options) {
if ( fsentry.layout === 'details' ) {
window.update_details_layout_sort_visuals(el_window, fsentry.sort_by, fsentry.sort_order);
}
}).catch(() => {
// The listing request below reports the failure to the user.
});
// is_directoryPicker
@@ -135,6 +143,8 @@ const refresh_item_container = function (el_item_container, options) {
// remove all existing items
$(el_item_container).find('.item').removeItems();
// removing the last row shows the empty message, which is wrong while the folder is loading
$(empty_message).hide();
// get items with subdomains/workers included to avoid per-item stat calls
const entries_promise = is_shared_view
@@ -352,6 +362,11 @@ const refresh_item_container = function (el_item_container, options) {
$(error_message).html(`Failed to load directory${ html_encode((e && e.message ? `: ${ e.message}` : ''))}`);
$(error_message).show();
// the previous folder's items are gone, so its count must not linger
if ( el_window ) {
window.update_explorer_footer_item_count(el_window);
}
// Call onComplete callback even in error case, since the "loading" is technically complete
if ( options.onComplete && typeof options.onComplete === 'function' ) {
options.onComplete();
+1
View File
@@ -1067,6 +1067,7 @@ const en = {
'window_click_to_go_back': 'Click to go back.',
'window_click_to_go_forward': 'Click to go forward.',
'window_click_to_go_up': 'Click to go one directory up.',
'window_click_to_change_layout': 'Click to change the layout.',
'window_title_public': 'Public',
'window_title_videos': 'Videos',
'window_title_pictures': 'Pictures',
+50 -31
View File
@@ -23,6 +23,25 @@ import launch_app from './helpers/launchApp.js';
import open_item from './helpers/openItem.js';
import determine_active_container_parent from './helpers/determineActiveContainerParent.js';
// The item container the user is working in: around or inside the element they
// last pressed, or elsewhere in its window. A pressed item that the window has
// since navigated away from leaves only the container it was in.
const selection_container = () => {
const $active = $(window.active_element);
let $container = $active.closest('.item-container');
if ( $container.length === 0 ) {
$container = $active.find('.item-container');
}
if ( $container.length === 0 ) {
$container = $active.closest('.window').find('.item-container');
}
if ( $container.length === 0 && window.active_element && !document.contains(window.active_element)
&& document.contains(window.active_item_container) ) {
$container = $(window.active_item_container);
}
return $container;
};
$(document).bind('keydown', async function (e) {
const focused_el = document.activeElement;
//-----------------------------------------------------------------------------
@@ -290,13 +309,14 @@ $(document).bind('keydown', async function (e) {
if ( selected.length === 1 ) {
return selected.get(0);
}
if ( selected.length > 1 && window.latest_selected_item ) {
// only an item that is still selected in this container can anchor the move
if ( selected.is(window.latest_selected_item) ) {
return window.latest_selected_item;
}
if ( window.active_element && $(window.active_element).hasClass('item') ) {
if ( selected.is(window.active_element) ) {
return window.active_element;
}
return $(window.active_item_container).find('.item').get(0);
return selected.get(-1);
}
function findNeighbor (current, direction) {
@@ -363,18 +383,19 @@ $(document).bind('keydown', async function (e) {
// select first item if none are already selected
const selected = $(window.active_item_container).find('.item-selected');
if ( selected.length === 0 ) {
const first = $(window.active_item_container).find('.item').get(0);
const first = $(window.active_item_container).find('.item:not(.item-hidden)').get(0);
if ( first ) {
$(first).addClass('item-selected');
window.active_element = first;
window.latest_selected_item = first;
first.scrollIntoView({ block: 'nearest', inline: 'nearest' });
window.update_explorer_footer_selected_items_count($(window.active_item_container).closest('.window'));
}
return;
}
// virtual grid layout to determine item layout and next items
const items = Array.from($(window.active_item_container).find('.item'));
const items = Array.from($(window.active_item_container).find('.item:not(.item-hidden)'));
const grid = items.map(item => {
const rect = item.getBoundingClientRect();
return {
@@ -393,6 +414,7 @@ $(document).bind('keydown', async function (e) {
const currentEl = getActiveItem();
const current = grid.find(i => i.el === currentEl);
if ( ! current ) return;
const next = findNeighbor(current, dir);
// apply new selection(s)
@@ -409,6 +431,7 @@ $(document).bind('keydown', async function (e) {
$(next.el).addClass('item-selected');
}
next.el.scrollIntoView({ block: 'nearest', inline: 'nearest' });
window.update_explorer_footer_selected_items_count($(window.active_item_container).closest('.window'));
}
}
// ----------------------------------------------
@@ -500,14 +523,15 @@ $(document).bind('keydown', async function (e) {
// Shift+Delete (win)/ option+command+delete (Mac) key pressed
// Permanent delete bypassing trash after alert
//-----------------------------------------------------------------------
if ( (e.keyCode === 46 && e.shiftKey) || (e.altKey && e.metaKey && e.keyCode === 8) ) {
let $selected_items = $(window.active_element).closest('.item-container').find('.item-selected');
if ( ((e.keyCode === 46 && e.shiftKey) || (e.altKey && e.metaKey && e.keyCode === 8)) && !$(focused_el).is('input, textarea') ) {
let $selected_items = selection_container().find('.item-selected');
if ( $selected_items.length > 0 ) {
const alert_resp = await UIAlert({
message: i18n('confirm_delete_multiple_items'),
buttons: [
{
label: i18n('delete'),
value: 'delete',
type: 'primary',
},
{
@@ -515,7 +539,7 @@ $(document).bind('keydown', async function (e) {
},
],
});
if ( (alert_resp) === 'Delete' ) {
if ( (alert_resp) === 'delete' ) {
for ( let index = 0; index < $selected_items.length; index++ ) {
const element = $selected_items[index];
await window.delete_item(element);
@@ -528,15 +552,16 @@ $(document).bind('keydown', async function (e) {
// Delete (win)/ ctrl+delete (Mac) / cmd+delete (Mac) key pressed
// Permanent delete from trash after alert or move to trash
//-----------------------------------------------------------------------
if ( e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey)) ) {
if ( (e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey))) && !$(focused_el).is('input, textarea') ) {
// permanent delete?
let $selected_items = $(window.active_element).closest('.item-container').find(`.item-selected[data-path^="${`${window.trash_path }/`}"]`);
let $selected_items = selection_container().find(`.item-selected[data-path^="${`${window.trash_path }/`}"]`);
if ( $selected_items.length > 0 ) {
const alert_resp = await UIAlert({
message: i18n('confirm_delete_multiple_items'),
buttons: [
{
label: i18n('delete'),
value: 'delete',
type: 'primary',
},
{
@@ -544,7 +569,7 @@ $(document).bind('keydown', async function (e) {
},
],
});
if ( (alert_resp) === 'Delete' ) {
if ( (alert_resp) === 'delete' ) {
for ( let index = 0; index < $selected_items.length; index++ ) {
const element = $selected_items[index];
await window.delete_item(element);
@@ -554,7 +579,7 @@ $(document).bind('keydown', async function (e) {
}
// regular delete?
else {
$selected_items = $(window.active_element).closest('.item-container').find('.item-selected');
$selected_items = selection_container().find('.item-selected');
if ( $selected_items.length > 0 ) {
// Only delete the items if we're not renaming one.
if ( $selected_items.children('.item-name-editor-active').length === 0 ) {
@@ -569,6 +594,11 @@ $(document).bind('keydown', async function (e) {
// A letter or number is pressed and there is no context menu open: search items by name
//-----------------------------------------------------------------------
if ( !e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length === 0 ) {
// Shift, arrows, Enter and other named keys are not part of a name
if ( e.key?.length !== 1 ) {
return false;
}
if ( window.keypress_item_seach_term !== '' )
{
clearTimeout(window.keypress_item_seach_buffer_timeout);
@@ -589,7 +619,7 @@ $(document).bind('keydown', async function (e) {
}
// search for matches
let haystack = $(window.active_item_container).find('.item').not('.item-disabled');
let haystack = $(window.active_item_container).find('.item').not('.item-disabled, .item-hidden');
for ( let j = 0; j < haystack.length; j++ ) {
if ( $(haystack[j]).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term) ) {
matches.push(haystack[j]);
@@ -613,7 +643,7 @@ $(document).bind('keydown', async function (e) {
// select matching item
$(matches[0]).not('.item-disabled').addClass('item-selected');
matches[0].scrollIntoView(false);
window.update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
window.update_explorer_footer_selected_items_count($(window.active_item_container).closest('.window'));
}
return false;
@@ -687,11 +717,7 @@ $(document).bind('keyup keydown', async function (e) {
// ctrl/command + a, will select all items on desktop and windows
//-----------------------------------------------------------------------------
if ( (e.ctrlKey || e.metaKey) && e.which === 65 && !$(focused_el).is('input') && !$(focused_el).is('textarea') ) {
let $parent_container = $(window.active_element).closest('.item-container');
if ( $parent_container.length === 0 )
{
$parent_container = $(window.active_element).find('.item-container');
}
const $parent_container = selection_container();
if ( $parent_container.attr('data-multiselectable') === 'false' )
{
@@ -699,7 +725,7 @@ $(document).bind('keyup keydown', async function (e) {
}
if ( $parent_container ) {
$($parent_container).find('.item').not('.item-disabled').addClass('item-selected');
$($parent_container).find('.item').not('.item-disabled, .item-hidden').addClass('item-selected');
window.update_explorer_footer_selected_items_count($parent_container.closest('.window'));
}
@@ -732,11 +758,7 @@ $(document).bind('keyup keydown', async function (e) {
!$(focused_el).is('textarea') ) {
let $selected_items;
let parent_container = $(window.active_element).closest('.item-container');
if ( parent_container.length === 0 )
{
parent_container = $(window.active_element).find('.item-container');
}
const parent_container = selection_container();
if ( parent_container !== null ) {
$selected_items = $(parent_container).find('.item-selected');
@@ -761,11 +783,7 @@ $(document).bind('keyup keydown', async function (e) {
//-----------------------------------------------------------------------------
if ( (e.ctrlKey || e.metaKey) && e.which === 88 && !$(focused_el).is('input') && !$(focused_el).is('textarea') ) {
let $selected_items;
let parent_container = $(window.active_element).closest('.item-container');
if ( parent_container.length === 0 )
{
parent_container = $(window.active_element).find('.item-container');
}
const parent_container = selection_container();
if ( parent_container !== null ) {
$selected_items = $(parent_container).find('.item-selected');
@@ -863,7 +881,8 @@ $(document).bind('keyup keydown', async function (e) {
// Paste
// ctrl/command + v, will paste items from the clipboard to the active element
//----------------------------------------------
if ( (e.ctrlKey || e.metaKey) && e.which === 86 && !$(focused_el).is('input') && !$(focused_el).is('textarea') ) {
// Once per press: releasing V with Ctrl still held would paste a second time.
if ( (e.ctrlKey || e.metaKey) && e.which === 86 && e.type === 'keydown' && !$(focused_el).is('input') && !$(focused_el).is('textarea') ) {
let target_path, target_el;
// continue only if there is something in the clipboard