Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions modules/vfs-toolkit/vfs-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Common features:
- **Save as:** download a file to the local filesystem (right-click → "Save as…")
- Provider dropdown in the location bar when multiple providers are available
- File type filter dropdown and filename filter input in the toolbar
- Toolbar action buttons can be hidden without removing the filters
- Context menus can be hidden independently of keyboard shortcuts
- Storage usage shown in the footer
- Toolbar buttons and context menu items disabled automatically based on provider capabilities
- Progress bar with file counter for batch operations; **✕ cancel button** to abort mid-operation
Expand Down Expand Up @@ -160,6 +162,8 @@ The picker always supports multi-selecting files for management (copy, move, del
| `startIn` | `string` | `null` | Absolute path to open in initially. Ignored when `id` has saved state. |
| `multiple` | `boolean` | `false` | Allow confirming multiple files at once. |
| `opfsStorageName` | `string` | - | Display name for the `OPFS` backend. |
| `showToolbarActions` | `boolean` | `true` | Show the built-in and custom toolbar action buttons. When `false`, the filters remain visible; context-menu actions and keyboard shortcuts remain available. |
| `showContextMenu` | `boolean` | `true` | Show the picker context menu for files, folders, and the list background. Keyboard shortcuts remain available when set to `false`. |
| `width` | `number` | `800` | Popup width in pixels |
| `height` | `number` | `600` | Popup height in pixels |

Expand Down Expand Up @@ -198,6 +202,8 @@ Opens a save file picker popup. The user can navigate to a folder and type (or e
| `startIn` | `string` | `null` | Absolute path to open in initially. Ignored when `id` has saved state. |
| `suggestedName` | `string` | - | Pre-filled filename in the save input |
| `opfsStorageName` | `string` | - | Display name for the `OPFS` backend. |
| `showToolbarActions` | `boolean` | `true` | Show the built-in and custom toolbar action buttons. When `false`, the filters remain visible; context-menu actions and keyboard shortcuts remain available. |
| `showContextMenu` | `boolean` | `true` | Show the picker context menu for files, folders, and the list background. Keyboard shortcuts remain available when set to `false`. |
| `width` | `number` | `800` | Popup width in pixels |
| `height` | `number` | `600` | Popup height in pixels |

Expand All @@ -221,6 +227,8 @@ Opens a directory picker popup. The user can navigate to and select a folder. Re
| `id` | `string` | `null` | Picker context ID (remembers last-used directory and connection) |
| `startIn` | `string` | `null` | Absolute path to open in initially |
| `opfsStorageName` | `string` | - | Display name for the `OPFS` backend. |
| `showToolbarActions` | `boolean` | `true` | Show the built-in and custom toolbar action buttons. When `false`, the filters remain visible; context-menu actions and keyboard shortcuts remain available. |
| `showContextMenu` | `boolean` | `true` | Show the picker context menu for files, folders, and the list background. Keyboard shortcuts remain available when set to `false`. |
| `width` | `number` | `800` | Popup width in pixels |
| `height` | `number` | `600` | Popup height in pixels |

Expand Down
6 changes: 5 additions & 1 deletion modules/vfs-toolkit/vfs-client/picker.css
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ body {
display: contents;
}

html.toolbar-actions-hidden #vfs-toolbar .vfs-toolbar-action {
display: none;
}

/* Filter input */
#vfs-type-select {
height: 26px;
Expand Down Expand Up @@ -1070,4 +1074,4 @@ body {
display: flex;
flex-direction: column;
overflow: hidden;
}
}
22 changes: 11 additions & 11 deletions modules/vfs-toolkit/vfs-client/picker.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,46 @@

<!-- Toolbar -->
<div id="vfs-toolbar">
<button id="vfs-btn-new-folder" data-i18n-title="titleNewFolder" title="Create new folder">
<button id="vfs-btn-new-folder" class="vfs-toolbar-action" data-i18n-title="titleNewFolder" title="Create new folder">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path
d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2z" />
</svg>
<span data-i18n-content="btnFolder">Folder</span>
</button>

<button id="vfs-btn-store" data-i18n-title="titleAdd" title="Add files from local filesystem">
<button id="vfs-btn-store" class="vfs-toolbar-action" data-i18n-title="titleAdd" title="Add files from local filesystem">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path
d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2z" />
</svg>
<span data-i18n-content="btnFile">File</span>
</button>

<div class="toolbar-sep"></div>
<div class="toolbar-sep vfs-toolbar-action"></div>

<button id="vfs-btn-rename" data-i18n-title="titleRename" title="Rename selected item (F2)">
<button id="vfs-btn-rename" class="vfs-toolbar-action" data-i18n-title="titleRename" title="Rename selected item (F2)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path
d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z" />
</svg>
</button>

<button id="vfs-btn-cut" data-i18n-title="titleCut" title="Cut selected item (Ctrl+X)">
<button id="vfs-btn-cut" class="vfs-toolbar-action" data-i18n-title="titleCut" title="Cut selected item (Ctrl+X)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path
d="M3.5 3.5c-.614-.884-.074-1.962.858-2.5L8 7.226 11.642 1c.932.538 1.472 1.616.858 2.5L8.81 8.61l1.556 2.661a2.5 2.5 0 1 1-.794.637L8 9.73l-1.572 2.177a2.5 2.5 0 1 1-.794-.637L7.19 8.61 3.5 3.5zm2.5 10a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0zm7 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0z" />
</svg>
</button>

<button id="vfs-btn-copy" data-i18n-title="titleCopy" title="Copy selected items (Ctrl+C)">
<button id="vfs-btn-copy" class="vfs-toolbar-action" data-i18n-title="titleCopy" title="Copy selected items (Ctrl+C)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M1 4h9v9H1V4zM2 5h7v7H2V5z" />
<path fill-rule="evenodd" d="M6 1h9v9H6V1zM7 2h7v7H7V2z" />
</svg>
</button>

<button id="vfs-btn-paste" data-i18n-title="titlePaste" title="Paste (Ctrl+V)">
<button id="vfs-btn-paste" class="vfs-toolbar-action" data-i18n-title="titlePaste" title="Paste (Ctrl+V)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path
d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
Expand All @@ -65,9 +65,9 @@
</svg>
</button>

<div class="toolbar-sep"></div>
<div class="toolbar-sep vfs-toolbar-action"></div>

<button id="vfs-btn-delete" class="btn-danger" data-i18n-title="titleDelete" title="Delete selected item (Del)">
<button id="vfs-btn-delete" class="btn-danger vfs-toolbar-action" data-i18n-title="titleDelete" title="Delete selected item (Del)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
Expand All @@ -77,7 +77,7 @@
</button>

<!-- Custom buttons injected by picker.mjs from the 'buttons' URL param -->
<div id="vfs-custom-buttons"></div>
<div id="vfs-custom-buttons" class="vfs-toolbar-action"></div>

<div class="toolbar-spacer"></div>

Expand Down Expand Up @@ -149,4 +149,4 @@

</body>

</html>
</html>
9 changes: 8 additions & 1 deletion modules/vfs-toolkit/vfs-client/picker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

import * as vfs from './vfs-client.mjs';

const params = new URLSearchParams(location.search);
const SHOW_TOOLBAR_ACTIONS = params.get('showToolbarActions') !== '0';
// Apply the reduced toolbar before locale loading can delay picker initialization.
document.documentElement.classList.toggle('toolbar-actions-hidden', !SHOW_TOOLBAR_ACTIONS);


// ── Locale resolution ─────────────────────────────────────────────────────────

Expand Down Expand Up @@ -86,7 +91,6 @@ function localizeDocument(doc = document) {

// ── Communication mode detection ─────────────────────────────────────────────

const params = new URLSearchParams(location.search);
const SESSION_ID = params.get('session');
const MULTIPLE = params.get('multiple') === '1';
const TYPES = params.get('types') ? JSON.parse(params.get('types')) : null;
Expand All @@ -100,6 +104,7 @@ const SUGGESTED_NAME = params.get('suggestedName') ?? null;
// the client extension background via browser.runtime.sendMessage so the background
// can react (e.g. open a test tab) without any provider involvement.
const BUTTONS = params.get('buttons') ? JSON.parse(params.get('buttons')) : [];
const SHOW_CONTEXT_MENU = params.get('showContextMenu') !== '0';
// 'strict' | 'soft' | null. When set, the picker is locked to LOCKED_REF:
// 'strict' hides the provider selector entirely; 'soft' disables the confirm
// button while the user browses a different connection.
Expand Down Expand Up @@ -424,6 +429,7 @@ function buildRow(entry) {

// Context menu
row.addEventListener('contextmenu', e => {
if (!SHOW_CONTEXT_MENU) return;
e.preventDefault();
if (!state.selected.has(entry.name)) {
selectEntry(entry.name, isDir ? null : entryPath);
Expand Down Expand Up @@ -2156,6 +2162,7 @@ async function init() {
if (!contextMenu.contains(e.target)) hideContextMenu();
});
listArea.addEventListener('contextmenu', e => {
if (!SHOW_CONTEXT_MENU) return;
if (!e.target.closest('.vfs-row')) {
e.preventDefault();
e.stopPropagation();
Expand Down
33 changes: 29 additions & 4 deletions modules/vfs-toolkit/vfs-client/vfs-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,10 @@ export async function getStorageUsage(storageRef = null) {
* @param {string} [options.opfsStorageName]
* Display name for the built-in OPFS (local storage) option in the provider dropdown.
* Has no effect on external providers, which use their own reported name.
* @param {boolean} [options.showToolbarActions=true]
* Show the toolbar action buttons. The filters remain visible when set to false.
* @param {boolean} [options.showContextMenu=true]
* Show the picker context menu for files, folders, and the list background.
* @param {boolean} [options.multiple=false] - Allow selecting multiple files. When false
* (default), the returned array always contains exactly one entry.
* @returns {Promise<Entry[]|null>} Array of `Entry` objects (each with `path` and `storageRef`), or null if cancelled.
Expand All @@ -880,7 +884,7 @@ export function showSelectFilePicker(options = {}) {
// mode=open is the default, no extra param needed
return new Promise((resolve, reject) => {
const sessionId = crypto.randomUUID();
const { types = null, excludeAcceptAllOption = false, width = 800, height = 600, storageRef = null, lockStorage = null, multiple = false, id = null, startIn = null, opfsStorageName = null, buttons = null } = options;
const { types = null, excludeAcceptAllOption = false, width = 800, height = 600, storageRef = null, lockStorage = null, multiple = false, id = null, startIn = null, opfsStorageName = null, buttons = null, showToolbarActions = true, showContextMenu = true } = options;

pendingPickers.set(sessionId, { resolve, reject, defaultValue: [] });

Expand All @@ -896,6 +900,8 @@ export function showSelectFilePicker(options = {}) {
if (startIn) pickerParams.set('startIn', startIn);
if (opfsStorageName) pickerParams.set('opfsStorageName', opfsStorageName);
if (buttons?.length) pickerParams.set('buttons', JSON.stringify(buttons));
if (!showToolbarActions) pickerParams.set('showToolbarActions', '0');
if (!showContextMenu) pickerParams.set('showContextMenu', '0');

_openPopupWindow(sessionId, pickerParams, width, height).catch(reject);
});
Expand Down Expand Up @@ -930,12 +936,16 @@ export function showSelectFilePicker(options = {}) {
* confirm-button effect in browse mode since browse has no confirm button.
* @param {string} [options.opfsStorageName]
* Display name for the built-in OPFS (local storage) option in the provider dropdown.
* @param {boolean} [options.showToolbarActions=true]
* Show the toolbar action buttons. The filters remain visible when set to false.
* @param {boolean} [options.showContextMenu=true]
* Show the picker context menu for files, folders, and the list background.
* @returns {Promise<null>} Always resolves to `null`.
*/
export function showBrowseFilePicker(options = {}) {
return new Promise((resolve, reject) => {
const sessionId = crypto.randomUUID();
const { width = 800, height = 600, storageRef = null, lockStorage = null, id = null, startIn = null, opfsStorageName = null, buttons = null } = options;
const { width = 800, height = 600, storageRef = null, lockStorage = null, id = null, startIn = null, opfsStorageName = null, buttons = null, showToolbarActions = true, showContextMenu = true } = options;

pendingPickers.set(sessionId, { resolve, reject, defaultValue: null });

Expand All @@ -949,6 +959,8 @@ export function showBrowseFilePicker(options = {}) {
if (startIn) pickerParams.set('startIn', startIn);
if (opfsStorageName) pickerParams.set('opfsStorageName', opfsStorageName);
if (buttons?.length) pickerParams.set('buttons', JSON.stringify(buttons));
if (!showToolbarActions) pickerParams.set('showToolbarActions', '0');
if (!showContextMenu) pickerParams.set('showContextMenu', '0');

_openPopupWindow(sessionId, pickerParams, width, height).catch(reject);
});
Expand All @@ -971,6 +983,10 @@ export function showBrowseFilePicker(options = {}) {
* Restrict the picker to the connection given by `storageRef`. See
* `showSelectFilePicker` for semantics.
* @param {string} [options.opfsStorageName]
* @param {boolean} [options.showToolbarActions=true]
* Show the toolbar action buttons. The filters remain visible when set to false.
* @param {boolean} [options.showContextMenu=true]
* Show the picker context menu for files, folders, and the list background.
* @param {number} [options.width=800]
* @param {number} [options.height=600]
* @returns {Promise<Entry|null>}
Expand All @@ -980,7 +996,7 @@ export function showSaveFilePicker(options = {}) {
const sessionId = crypto.randomUUID();
const { types = null, excludeAcceptAllOption = false, width = 800, height = 600,
storageRef = null, lockStorage = null, id = null, startIn = null, opfsStorageName = null,
suggestedName = null, buttons = null } = options;
suggestedName = null, buttons = null, showToolbarActions = true, showContextMenu = true } = options;

pendingPickers.set(sessionId, { resolve, reject, defaultValue: null });

Expand All @@ -997,6 +1013,8 @@ export function showSaveFilePicker(options = {}) {
if (opfsStorageName) pickerParams.set('opfsStorageName', opfsStorageName);
if (suggestedName) pickerParams.set('suggestedName', suggestedName);
if (buttons?.length) pickerParams.set('buttons', JSON.stringify(buttons));
if (!showToolbarActions) pickerParams.set('showToolbarActions', '0');
if (!showContextMenu) pickerParams.set('showContextMenu', '0');

_openPopupWindow(sessionId, pickerParams, width, height).catch(reject);
});
Expand All @@ -1015,6 +1033,10 @@ export function showSaveFilePicker(options = {}) {
* Restrict the picker to the connection given by `storageRef`. See
* `showSelectFilePicker` for semantics.
* @param {string} [options.opfsStorageName]
* @param {boolean} [options.showToolbarActions=true]
* Show the toolbar action buttons. The filters remain visible when set to false.
* @param {boolean} [options.showContextMenu=true]
* Show the picker context menu for files, folders, and the list background.
* @param {number} [options.width=800]
* @param {number} [options.height=600]
* @returns {Promise<Entry|null>}
Expand All @@ -1023,7 +1045,8 @@ export function showDirectoryPicker(options = {}) {
return new Promise((resolve, reject) => {
const sessionId = crypto.randomUUID();
const { width = 800, height = 600, storageRef = null, lockStorage = null, id = null,
startIn = null, opfsStorageName = null, buttons = null } = options;
startIn = null, opfsStorageName = null, buttons = null, showToolbarActions = true,
showContextMenu = true } = options;

pendingPickers.set(sessionId, { resolve, reject, defaultValue: null });

Expand All @@ -1037,6 +1060,8 @@ export function showDirectoryPicker(options = {}) {
if (startIn) pickerParams.set('startIn', startIn);
if (opfsStorageName) pickerParams.set('opfsStorageName', opfsStorageName);
if (buttons?.length) pickerParams.set('buttons', JSON.stringify(buttons));
if (!showToolbarActions) pickerParams.set('showToolbarActions', '0');
if (!showContextMenu) pickerParams.set('showContextMenu', '0');

_openPopupWindow(sessionId, pickerParams, width, height).catch(reject);
});
Expand Down