mirror of
https://github.com/OctopusSuite/octopus-file-browser.git
synced 2026-10-17 09:37:55 +00:00
Build reusable octopus file browser
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import styles from './octopus-file-browser.css?inline';
|
||||
import { fileKindLabel, formatFileSize, formatRelativeDate, isRecent, sortEntries } from './file-browser-utils.js';
|
||||
import { resolveIcon } from './icon-resolver.js';
|
||||
|
||||
const HTMLElementBase = globalThis.HTMLElement || class {};
|
||||
|
||||
function element(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
export class OctopusFileBrowser extends HTMLElementBase {
|
||||
#entries = [];
|
||||
#path = [];
|
||||
|
||||
static get observedAttributes() { return ['empty-label']; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() { this.#render(); }
|
||||
attributeChangedCallback() { if (this.isConnected) this.#render(); }
|
||||
|
||||
set entries(value) {
|
||||
this.#entries = Array.isArray(value) ? value : [];
|
||||
this.#path = [];
|
||||
this.#render();
|
||||
}
|
||||
|
||||
get entries() { return this.#entries; }
|
||||
get path() { return [...this.#path]; }
|
||||
|
||||
navigate(path = []) {
|
||||
let entries = this.#entries;
|
||||
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.#emit('octopus:navigate', { path: this.path, entries: this.#currentEntries() });
|
||||
}
|
||||
|
||||
#currentEntries() {
|
||||
return this.#path.reduce((entries, part) =>
|
||||
entries.find((entry) => entry.type === 'directory' && entry.name === part)?.children || [],
|
||||
this.#entries);
|
||||
}
|
||||
|
||||
#emit(name, detail) {
|
||||
this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
|
||||
}
|
||||
|
||||
#open(entry) {
|
||||
if (entry.parent) return this.navigate(this.#path.slice(0, -1));
|
||||
if (entry.type === 'directory') return this.navigate([...this.#path, entry.name]);
|
||||
this.#emit('octopus:open', { entry, path: [...this.#path, entry.name] });
|
||||
}
|
||||
|
||||
#renderEntry(entry) {
|
||||
const item = element('li');
|
||||
const row = element('button', 'entry');
|
||||
row.type = 'button';
|
||||
row.title = entry.parent ? 'Parent folder' : entry.name;
|
||||
row.setAttribute('aria-label', entry.parent
|
||||
? 'Parent folder'
|
||||
: `${entry.name}, ${entry.type === 'directory' ? 'folder' : fileKindLabel(entry.name)}`);
|
||||
row.addEventListener('click', () => this.#open(entry));
|
||||
|
||||
const name = element('span', 'cell name');
|
||||
const icon = element('img', 'icon');
|
||||
icon.alt = '';
|
||||
icon.src = resolveIcon(entry);
|
||||
name.append(icon, element('span', 'label', entry.name));
|
||||
if (entry.type !== 'directory') name.append(element('span', 'size', formatFileSize(entry.size)));
|
||||
const modified = element('span', 'cell modified', entry.modified ? formatRelativeDate(entry.modified) : '');
|
||||
if (entry.modified) {
|
||||
modified.title = new Date(entry.modified).toLocaleString();
|
||||
if (isRecent(entry.modified)) modified.classList.add('recent');
|
||||
}
|
||||
row.append(name, modified);
|
||||
item.append(row);
|
||||
return item;
|
||||
}
|
||||
|
||||
#render() {
|
||||
if (!this.shadowRoot || typeof document === 'undefined') return;
|
||||
const style = element('style');
|
||||
style.textContent = styles;
|
||||
const list = element('ul', 'list');
|
||||
list.setAttribute('aria-label', 'Files');
|
||||
const entries = sortEntries(this.#currentEntries());
|
||||
if (this.#path.length) list.append(this.#renderEntry({ name: '..', type: 'directory', parent: true }));
|
||||
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.'));
|
||||
this.shadowRoot.replaceChildren(style, list);
|
||||
}
|
||||
}
|
||||
|
||||
if (globalThis.customElements && !customElements.get('octopus-file-browser'))
|
||||
customElements.define('octopus-file-browser', OctopusFileBrowser);
|
||||
|
||||
export function createOctopusFileBrowser(target, options = {}) {
|
||||
const browser = document.createElement('octopus-file-browser');
|
||||
if (options.emptyLabel) browser.setAttribute('empty-label', options.emptyLabel);
|
||||
browser.entries = options.entries || [];
|
||||
target.append(browser);
|
||||
return browser;
|
||||
}
|
||||
|
||||
export { fileKindLabel, formatFileSize, formatRelativeDate, isRecent, sortEntries };
|
||||
Reference in New Issue
Block a user