import * as React from 'react'; import { IDocumentManager } from '@jupyterlab/docmanager'; import { Contents } from '@jupyterlab/services'; import { ReactWidget, Toolbar } from '@jupyterlab/ui-components'; import { Signal, ISignal } from '@lumino/signaling'; import { PanelLayout, Widget } from '@lumino/widgets'; import { FileBrowserComponent } from './fileBrowser'; import { xtralabFileBrowserIcon } from './icons'; export const FILE_BROWSER_ID = 'xtralab:file-browser'; /** * The CSS class added to the xtralab file browser widget. The selectors that * bind application context menu items hang off this class, so it must remain * specific enough not to clash with other browsers (such as the default * `jp-FileBrowser`). */ const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser'; const TOOLBAR_CSS_CLASS = 'jp-xtralab-FileBrowser-toolbar'; const CONTENT_CSS_CLASS = 'jp-xtralab-FileBrowser-content'; interface IXtralabFileBrowserOptions { contentsManager: Contents.IManager; docManager: IDocumentManager; onOpenFile?: (serverPath: string) => void; } /** * Public API of the xtralab file browser used by command handlers and other * collaborators. Kept narrow on purpose so the React component stays free to * evolve the underlying tree integration. */ export interface IXtralabFileBrowser { /** * The contents manager backing the tree. Exposed so command handlers can * issue contents operations (rename, delete, copy, …) against the same * drive the tree was loaded from. */ readonly contentsManager: Contents.IManager; /** * The toolbar shown above the tree. Plugin code populates it with command * buttons after the widget is constructed. */ readonly toolbar: Toolbar; /** * Canonical paths of the items currently selected in the tree. Folder * paths carry a trailing slash; file paths do not. */ readonly selectedPaths: readonly string[]; /** * Emits when the tree's selection changes. Commands that have an * `isVisible` / `isEnabled` predicate should listen so they re-evaluate. */ readonly selectionChanged: ISignal; /** * Emits when {@link refresh} is called. The React component listens and * re-fetches the directories that were previously loaded so the tree * matches the contents on disk again. */ readonly refreshRequested: ISignal; /** * Emits when the widget is asked to surface a newly-created path * (for example, after the toolbar's "new folder" command). The React * component listens and inserts the path into the model so the user * sees the new item without a full refresh. */ readonly pathAdded: ISignal; /** * Emits when an external caller asks the tree to scroll to and select * the given canonical path. The React component listens, lazily loads * any unloaded ancestor directories, expands them, and selects the * target. Used by the editor breadcrumbs to jump back to a file or * folder shown in the breadcrumb trail. */ readonly revealRequested: ISignal; /** * Emits when an external caller asks the tree to return to the * workspace root: clear any current selection and scroll back to the * first row. Distinct from {@link revealRequested} because the * workspace root has no tree row of its own — it cannot be reached * by passing a path. */ readonly rootRequested: ISignal; /** * Emits when an external caller asks the tree to collapse every * expanded folder. The React component listens and walks the loaded * directories, calling `.collapse()` on each expanded one. */ readonly collapseAllRequested: ISignal; /** * Whether the filter box above the tree is shown. Mirrors the default * file browser's `showFileFilter`: hidden on startup and flipped by the * toolbar toggle. Also forced to `true` when the tree opens a search * session on its own — typing a letter while the tree has focus. */ readonly fileFilterVisible: boolean; /** * Emits when {@link fileFilterVisible} changes. The React component * listens to show or hide the filter box; the toggle command listens * to refresh its toggled state. */ readonly fileFilterVisibleChanged: ISignal; /** * Trigger a refresh of every loaded directory in the tree. */ refresh(): void; /** * Notify the React component that a new path was created and should * appear in the tree. `canonicalPath` follows the `@pierre/trees` * convention: directories carry a trailing slash, files do not. */ notifyPathAdded(canonicalPath: string): void; /** * Ask the tree to reveal {@link canonicalPath}: load and expand any * unloaded ancestor directories, then select and scroll the target * into view. `canonicalPath` follows the `@pierre/trees` convention * (directories carry a trailing slash, files do not). Must be a * non-empty path — use {@link scrollToRoot} for the root gesture. */ reveal(canonicalPath: string): void; /** * Ask the tree to return to the workspace root: clear any current * selection and scroll back to the top of the tree. */ scrollToRoot(): void; /** * Ask the tree to collapse every currently expanded folder. */ collapseAll(): void; /** * Show or hide the filter box above the tree. */ setFileFilterVisible(visible: boolean): void; /** * Toggle the filter box above the tree. */ toggleFileFilter(): void; } /** * Lumino widget that hosts the React-based `@pierre/trees` file browser. * The widget is laid out with a JupyterLab `Toolbar` on top and the tree * filling the remaining space. */ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser { constructor(options: IXtralabFileBrowserOptions) { super(); const layout = (this.layout = new PanelLayout()); this._contentsManager = options.contentsManager; this._docManager = options.docManager; this._onOpenFile = options.onOpenFile; this.id = FILE_BROWSER_ID; this.title.icon = xtralabFileBrowserIcon; this.title.caption = 'xtralab File Browser'; this.addClass(FILE_BROWSER_CSS_CLASS); this._toolbar = new Toolbar(); this._toolbar.addClass(TOOLBAR_CSS_CLASS); this._toolbar.node.setAttribute('aria-label', 'file browser'); layout.addWidget(this._toolbar); this._content = new XtralabFileTreeContent({ contentsManager: this._contentsManager, docManager: this._docManager, onOpenFile: this._onOpenFile, browser: this }); this._content.addClass(CONTENT_CSS_CLASS); layout.addWidget(this._content); } get contentsManager(): Contents.IManager { return this._contentsManager; } get toolbar(): Toolbar { return this._toolbar; } get selectedPaths(): readonly string[] { return this._selectedPaths; } get selectionChanged(): ISignal { return this._selectionChanged; } get refreshRequested(): ISignal { return this._refreshRequested; } get pathAdded(): ISignal { return this._pathAdded; } get revealRequested(): ISignal { return this._revealRequested; } get rootRequested(): ISignal { return this._rootRequested; } get collapseAllRequested(): ISignal { return this._collapseAllRequested; } get fileFilterVisible(): boolean { return this._fileFilterVisible; } get fileFilterVisibleChanged(): ISignal { return this._fileFilterVisibleChanged; } /** * Update the cached selection. Called from the React tree when the * underlying `@pierre/trees` model emits a selection change. */ updateSelection(paths: readonly string[]): void { this._selectedPaths = paths; this._selectionChanged.emit(paths); } refresh(): void { this._refreshRequested.emit(); } notifyPathAdded(canonicalPath: string): void { this._pathAdded.emit(canonicalPath); } reveal(canonicalPath: string): void { if (canonicalPath.length === 0) { throw new Error( 'XtralabFileBrowser.reveal requires a non-empty canonical path; use scrollToRoot for the workspace root.' ); } this._revealRequested.emit(canonicalPath); } scrollToRoot(): void { this._rootRequested.emit(); } collapseAll(): void { this._collapseAllRequested.emit(); } setFileFilterVisible(visible: boolean): void { if (this._fileFilterVisible === visible) { return; } this._fileFilterVisible = visible; this._fileFilterVisibleChanged.emit(visible); } toggleFileFilter(): void { this.setFileFilterVisible(!this._fileFilterVisible); } private _contentsManager: Contents.IManager; private _docManager: IDocumentManager; private _onOpenFile: ((serverPath: string) => void) | undefined; private _selectedPaths: readonly string[] = []; private _selectionChanged = new Signal(this); private _refreshRequested = new Signal(this); private _pathAdded = new Signal(this); private _revealRequested = new Signal(this); private _rootRequested = new Signal(this); private _collapseAllRequested = new Signal(this); private _fileFilterVisible = false; private _fileFilterVisibleChanged = new Signal(this); private _toolbar: Toolbar; private _content: XtralabFileTreeContent; } interface IFileTreeContentOptions { contentsManager: Contents.IManager; docManager: IDocumentManager; onOpenFile?: (serverPath: string) => void; browser: XtralabFileBrowser; } /** * The React-rendered region of the file browser. Lives inside the host * widget below the toolbar and renders the actual tree. */ class XtralabFileTreeContent extends ReactWidget { constructor(options: IFileTreeContentOptions) { super(); this._options = options; } protected render(): React.ReactElement { return ( ); } private _options: IFileTreeContentOptions; }