mirror of
https://github.com/OctopusSuite/octopus-file-browser.git
synced 2026-10-08 22:05:20 +00:00
Show one directory at a time and let the app load each directory
The browser no longer takes a nested tree. The app sets path and the current directory's entries; selecting a directory or .. emits octopus:navigate with the requested path for the app to load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,25 +23,22 @@ npm install octopus-file-browser
|
|||||||
import 'octopus-file-browser';
|
import 'octopus-file-browser';
|
||||||
|
|
||||||
const browser = document.querySelector('octopus-file-browser');
|
const browser = document.querySelector('octopus-file-browser');
|
||||||
browser.entries = [
|
|
||||||
{
|
|
||||||
name: 'src',
|
|
||||||
type: 'directory',
|
|
||||||
modified: '2026-09-23T17:00:00Z',
|
|
||||||
children: [
|
|
||||||
{ name: 'app.js', type: 'file', size: 1842, modified: '2026-09-23T17:00:00Z' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ name: 'package.json', type: 'file', size: 986, modified: '2026-09-22T09:30:00Z' },
|
|
||||||
];
|
|
||||||
|
|
||||||
browser.addEventListener('octopus:open', ({ detail }) => {
|
async function open(path) {
|
||||||
console.log(detail.path, detail.entry);
|
const response = await fetch(`/api/ls?path=${encodeURIComponent('/' + path.join('/'))}`);
|
||||||
});
|
const entries = await response.json();
|
||||||
|
browser.path = path;
|
||||||
|
browser.entries = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.addEventListener('octopus:navigate', ({ detail }) => open(detail.path));
|
||||||
|
browser.addEventListener('octopus:open', ({ detail }) => console.log(detail.path, detail.entry));
|
||||||
|
|
||||||
|
open([]);
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
Importing the package registers `<octopus-file-browser>`. Directories navigate automatically; selecting a file emits `octopus:open`.
|
Importing the package registers `<octopus-file-browser>`. The browser displays one directory at a time: your app sets `path` and that directory's `entries`. Selecting a directory or `..` emits `octopus:navigate` with the requested path, and your app loads that directory and sets both again. Selecting a file emits `octopus:open`.
|
||||||
|
|
||||||
## Entry data
|
## Entry data
|
||||||
|
|
||||||
@@ -51,7 +48,6 @@ Importing the package registers `<octopus-file-browser>`. Directories navigate a
|
|||||||
| `type` | `"file" \| "directory"` | Entry kind. |
|
| `type` | `"file" \| "directory"` | Entry kind. |
|
||||||
| `size` | `number` | File size in bytes. |
|
| `size` | `number` | File size in bytes. |
|
||||||
| `modified` | `Date \| string \| number` | Modified time. |
|
| `modified` | `Date \| string \| number` | Modified time. |
|
||||||
| `children` | `OctopusEntry[]` | Directory contents. |
|
|
||||||
| `icon` | `string` | Optional custom icon URL. |
|
| `icon` | `string` | Optional custom icon URL. |
|
||||||
|
|
||||||
Directories are sorted before files. Names use natural, case-insensitive sorting.
|
Directories are sorted before files. Names use natural, case-insensitive sorting.
|
||||||
@@ -60,12 +56,11 @@ Directories are sorted before files. Names use natural, case-insensitive sorting
|
|||||||
|
|
||||||
| API | Description |
|
| API | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `browser.entries` | Gets or replaces the root entry array and returns to root. |
|
| `browser.entries` | Gets or replaces the current directory's entries. |
|
||||||
| `browser.path` | Current directory path as a new array. |
|
| `browser.path` | Gets or sets the current directory path, for example `['home', 'ubuntu']`. `..` is shown when it is not empty. |
|
||||||
| `browser.navigate(path)` | Navigates to a directory path, for example `['src', 'assets']`. |
|
|
||||||
| `empty-label` | Attribute controlling the empty-directory message. |
|
| `empty-label` | Attribute controlling the empty-directory message. |
|
||||||
| `octopus:open` | Event with `{ entry, path }` when a file is selected. |
|
| `octopus:open` | Event with `{ entry, path }` when a file is selected. |
|
||||||
| `octopus:navigate` | Event with `{ entries, path }` after navigation. |
|
| `octopus:navigate` | Event with `{ path }` when a directory or `..` is selected. The browser does not change until your app sets `path` and `entries`. |
|
||||||
|
|
||||||
The package also exports `OctopusFileBrowser`, `createOctopusFileBrowser`, `sortEntries`, `formatFileSize`, `formatRelativeDate`, `isRecent`, and `fileKindLabel`. Type declarations are included.
|
The package also exports `OctopusFileBrowser`, `createOctopusFileBrowser`, `sortEntries`, `formatFileSize`, `formatRelativeDate`, `isRecent`, and `fileKindLabel`. Type declarations are included.
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
<meta name="color-scheme" content="light">
|
<meta name="color-scheme" content="light">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
<title>octopus-file-browser</title>
|
<title>octopus-file-browser</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DqA94Wnd.js"></script>
|
<script type="module" crossorigin src="/assets/index-CRttpLjx.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-rQJBLWWV.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-rQJBLWWV.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Vendored
+2
-5
@@ -6,7 +6,6 @@ export interface OctopusEntry {
|
|||||||
size?: number;
|
size?: number;
|
||||||
modified?: OctopusDate;
|
modified?: OctopusDate;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
children?: OctopusEntry[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OctopusOpenDetail {
|
export interface OctopusOpenDetail {
|
||||||
@@ -15,7 +14,6 @@ export interface OctopusOpenDetail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface OctopusNavigateDetail {
|
export interface OctopusNavigateDetail {
|
||||||
entries: OctopusEntry[];
|
|
||||||
path: string[];
|
path: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +24,7 @@ export interface OctopusFileBrowserEventMap {
|
|||||||
|
|
||||||
export class OctopusFileBrowser extends HTMLElement {
|
export class OctopusFileBrowser extends HTMLElement {
|
||||||
entries: OctopusEntry[];
|
entries: OctopusEntry[];
|
||||||
readonly path: string[];
|
path: string[];
|
||||||
navigate(path?: string[]): void;
|
|
||||||
addEventListener<K extends keyof OctopusFileBrowserEventMap>(
|
addEventListener<K extends keyof OctopusFileBrowserEventMap>(
|
||||||
type: K,
|
type: K,
|
||||||
listener: (this: OctopusFileBrowser, event: OctopusFileBrowserEventMap[K]) => void,
|
listener: (this: OctopusFileBrowser, event: OctopusFileBrowserEventMap[K]) => void,
|
||||||
@@ -42,7 +39,7 @@ export class OctopusFileBrowser extends HTMLElement {
|
|||||||
|
|
||||||
export function createOctopusFileBrowser(
|
export function createOctopusFileBrowser(
|
||||||
target: Element,
|
target: Element,
|
||||||
options?: { entries?: OctopusEntry[]; emptyLabel?: string },
|
options?: { entries?: OctopusEntry[]; path?: string[]; emptyLabel?: string },
|
||||||
): OctopusFileBrowser;
|
): OctopusFileBrowser;
|
||||||
|
|
||||||
export function fileKindLabel(name?: string): string;
|
export function fileKindLabel(name?: string): string;
|
||||||
|
|||||||
+15
-2
@@ -1,6 +1,19 @@
|
|||||||
import './demo.css';
|
import './demo.css';
|
||||||
import './octopus-file-browser.js';
|
import './octopus-file-browser.js';
|
||||||
import entries from 'virtual:octopus-repo-tree';
|
import tree from 'virtual:octopus-repo-tree';
|
||||||
|
|
||||||
const browser = document.querySelector('octopus-file-browser');
|
const browser = document.querySelector('octopus-file-browser');
|
||||||
browser.entries = entries;
|
|
||||||
|
// Stands in for an app backend: returns one directory listing per request.
|
||||||
|
function list(path) {
|
||||||
|
return path.reduce((entries, name) =>
|
||||||
|
entries.find((entry) => entry.type === 'directory' && entry.name === name)?.children || [], tree)
|
||||||
|
.map(({ children, ...entry }) => entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.addEventListener('octopus:navigate', ({ detail }) => {
|
||||||
|
browser.path = detail.path;
|
||||||
|
browser.entries = list(detail.path);
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.entries = list([]);
|
||||||
|
|||||||
@@ -27,40 +27,25 @@ export class OctopusFileBrowser extends HTMLElementBase {
|
|||||||
|
|
||||||
set entries(value) {
|
set entries(value) {
|
||||||
this.#entries = Array.isArray(value) ? value : [];
|
this.#entries = Array.isArray(value) ? value : [];
|
||||||
this.#path = [];
|
|
||||||
this.#render();
|
this.#render();
|
||||||
}
|
}
|
||||||
|
|
||||||
get entries() { return this.#entries; }
|
get entries() { return this.#entries; }
|
||||||
get path() { return [...this.#path]; }
|
|
||||||
|
|
||||||
navigate(path = []) {
|
set path(value) {
|
||||||
let entries = this.#entries;
|
this.#path = Array.isArray(value) ? [...value] : [];
|
||||||
const valid = [];
|
|
||||||
for (const part of path) {
|
|
||||||
const directory = entries.find((entry) => entry.type === 'directory' && entry.name === part);
|
|
||||||
if (!directory) break;
|
|
||||||
valid.push(part);
|
|
||||||
entries = directory.children || [];
|
|
||||||
}
|
|
||||||
this.#path = valid;
|
|
||||||
this.#render();
|
this.#render();
|
||||||
this.#emit('octopus:navigate', { path: this.path, entries: this.#currentEntries() });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#currentEntries() {
|
get path() { return [...this.#path]; }
|
||||||
return this.#path.reduce((entries, part) =>
|
|
||||||
entries.find((entry) => entry.type === 'directory' && entry.name === part)?.children || [],
|
|
||||||
this.#entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
#emit(name, detail) {
|
#emit(name, detail) {
|
||||||
this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
|
this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#open(entry) {
|
#open(entry) {
|
||||||
if (entry.parent) return this.navigate(this.#path.slice(0, -1));
|
if (entry.parent) return this.#emit('octopus:navigate', { path: this.#path.slice(0, -1) });
|
||||||
if (entry.type === 'directory') return this.navigate([...this.#path, entry.name]);
|
if (entry.type === 'directory') return this.#emit('octopus:navigate', { path: [...this.#path, entry.name] });
|
||||||
this.#emit('octopus:open', { entry, path: [...this.#path, entry.name] });
|
this.#emit('octopus:open', { entry, path: [...this.#path, entry.name] });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +81,7 @@ export class OctopusFileBrowser extends HTMLElementBase {
|
|||||||
style.textContent = styles;
|
style.textContent = styles;
|
||||||
const list = element('ul', 'list');
|
const list = element('ul', 'list');
|
||||||
list.setAttribute('aria-label', 'Files');
|
list.setAttribute('aria-label', 'Files');
|
||||||
const entries = sortEntries(this.#currentEntries());
|
const entries = sortEntries(this.#entries);
|
||||||
if (this.#path.length) list.append(this.#renderEntry({ name: '..', type: 'directory', parent: true }));
|
if (this.#path.length) list.append(this.#renderEntry({ name: '..', type: 'directory', parent: true }));
|
||||||
if (entries.length) entries.forEach((entry) => list.append(this.#renderEntry(entry)));
|
if (entries.length) entries.forEach((entry) => list.append(this.#renderEntry(entry)));
|
||||||
else list.append(element('li', 'empty', this.getAttribute('empty-label') || 'This folder is empty.'));
|
else list.append(element('li', 'empty', this.getAttribute('empty-label') || 'This folder is empty.'));
|
||||||
@@ -110,6 +95,7 @@ if (globalThis.customElements && !customElements.get('octopus-file-browser'))
|
|||||||
export function createOctopusFileBrowser(target, options = {}) {
|
export function createOctopusFileBrowser(target, options = {}) {
|
||||||
const browser = document.createElement('octopus-file-browser');
|
const browser = document.createElement('octopus-file-browser');
|
||||||
if (options.emptyLabel) browser.setAttribute('empty-label', options.emptyLabel);
|
if (options.emptyLabel) browser.setAttribute('empty-label', options.emptyLabel);
|
||||||
|
browser.path = options.path || [];
|
||||||
browser.entries = options.entries || [];
|
browser.entries = options.entries || [];
|
||||||
target.append(browser);
|
target.append(browser);
|
||||||
return browser;
|
return browser;
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import { defineConfig } from 'vite';
|
|||||||
|
|
||||||
const VIRTUAL_ID = 'virtual:octopus-repo-tree';
|
const VIRTUAL_ID = 'virtual:octopus-repo-tree';
|
||||||
const RESOLVED_ID = `\0${VIRTUAL_ID}`;
|
const RESOLVED_ID = `\0${VIRTUAL_ID}`;
|
||||||
const ignored = new Set(['.git', 'demo', 'dist', 'node_modules']);
|
const ignored = new Set(['.claude', '.git', '.npmrc', 'demo', 'dist', 'node_modules']);
|
||||||
|
|
||||||
function scan(directory) {
|
function scan(directory) {
|
||||||
return readdirSync(directory, { withFileTypes: true })
|
return readdirSync(directory, { withFileTypes: true })
|
||||||
|
|||||||
Reference in New Issue
Block a user