UNPKG

@jupyterlab/filebrowser

Version:
480 lines 17.7 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { DOMUtils, showErrorMessage } from '@jupyterlab/apputils'; import { PageConfig, PathExt } from '@jupyterlab/coreutils'; import { renameFile } from '@jupyterlab/docmanager'; import { nullTranslator } from '@jupyterlab/translation'; import { ellipsesIcon, homeIcon as preferredIcon, folderIcon as rootIcon } from '@jupyterlab/ui-components'; import { JSONExt } from '@lumino/coreutils'; import { Widget } from '@lumino/widgets'; /** * The class name added to the breadcrumb node. */ const BREADCRUMB_CLASS = 'jp-BreadCrumbs'; /** * The class name for the breadcrumbs home node */ const BREADCRUMB_ROOT_CLASS = 'jp-BreadCrumbs-home'; /** * The class name for the breadcrumbs preferred node */ const BREADCRUMB_PREFERRED_CLASS = 'jp-BreadCrumbs-preferred'; /** * The class name added to the breadcrumb node. */ const BREADCRUMB_ITEM_CLASS = 'jp-BreadCrumbs-item'; /** * The class name for the breadcrumbs ellipsis node */ const BREADCRUMB_ELLIPSIS_CLASS = 'jp-BreadCrumbs-ellipsis'; /** * The mime type for a contents drag object. */ const CONTENTS_MIME = 'application/x-jupyter-icontents'; /** * The class name added to drop targets. */ const DROP_TARGET_CLASS = 'jp-mod-dropTarget'; /** * A class which hosts folder breadcrumbs. */ export class BreadCrumbs extends Widget { /** * Construct a new file browser crumb widget. * * @param options Constructor options. */ constructor(options) { var _a, _b; super(); this._previousState = null; this.translator = options.translator || nullTranslator; this._trans = this.translator.load('jupyterlab'); this._model = options.model; this._fullPath = options.fullPath || false; this._minimumLeftItems = (_a = options.minimumLeftItems) !== null && _a !== void 0 ? _a : 0; this._minimumRightItems = (_b = options.minimumRightItems) !== null && _b !== void 0 ? _b : 2; this.addClass(BREADCRUMB_CLASS); this._crumbs = Private.createCrumbs(); const hasPreferred = PageConfig.getOption('preferredPath'); this._hasPreferred = hasPreferred && hasPreferred !== '/' ? true : false; if (this._hasPreferred) { this.node.appendChild(this._crumbs[Private.Crumb.Preferred]); } this.node.appendChild(this._crumbs[Private.Crumb.Home]); this._model.refreshed.connect(this.update, this); } /** * Handle the DOM events for the bread crumbs. * * @param event - The DOM event sent to the widget. * * #### Notes * This method implements the DOM `EventListener` interface and is * called in response to events on the panel's DOM node. It should * not be called directly by user code. */ handleEvent(event) { switch (event.type) { case 'click': this._evtClick(event); break; case 'lm-dragenter': this._evtDragEnter(event); break; case 'lm-dragleave': this._evtDragLeave(event); break; case 'lm-dragover': this._evtDragOver(event); break; case 'lm-drop': this._evtDrop(event); break; default: return; } } /** * Whether to show the full path in the breadcrumbs */ get fullPath() { return this._fullPath; } set fullPath(value) { this._fullPath = value; } /** * Number of items to show on left of ellipsis */ get minimumLeftItems() { return this._minimumLeftItems; } set minimumLeftItems(value) { this._minimumLeftItems = value; } /** * Number of items to show on right of ellipsis */ get minimumRightItems() { return this._minimumRightItems; } set minimumRightItems(value) { this._minimumRightItems = value; } /** * A message handler invoked on an `'after-attach'` message. */ onAfterAttach(msg) { super.onAfterAttach(msg); this.update(); const node = this.node; node.addEventListener('click', this); node.addEventListener('lm-dragenter', this); node.addEventListener('lm-dragleave', this); node.addEventListener('lm-dragover', this); node.addEventListener('lm-drop', this); } /** * A message handler invoked on a `'before-detach'` message. */ onBeforeDetach(msg) { super.onBeforeDetach(msg); const node = this.node; node.removeEventListener('click', this); node.removeEventListener('lm-dragenter', this); node.removeEventListener('lm-dragleave', this); node.removeEventListener('lm-dragover', this); node.removeEventListener('lm-drop', this); } /** * A handler invoked on an `'update-request'` message. */ onUpdateRequest(msg) { // Update the breadcrumb list. const contents = this._model.manager.services.contents; const localPath = contents.localPath(this._model.path); const state = { path: localPath, hasPreferred: this._hasPreferred, fullPath: this._fullPath, minimumLeftItems: this._minimumLeftItems, minimumRightItems: this._minimumRightItems }; if (this._previousState && JSONExt.deepEqual(state, this._previousState)) { return; } this._previousState = state; Private.updateCrumbs(this._crumbs, state); } /** * Handle the `'click'` event for the widget. */ _evtClick(event) { // Do nothing if it's not a left mouse press. if (event.button !== 0) { return; } // Find a valid click target. let node = event.target; while (node && node !== this.node) { if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS)) { const preferredPath = PageConfig.getOption('preferredPath'); const path = preferredPath ? '/' + preferredPath : preferredPath; this._model .cd(path) .catch(error => showErrorMessage(this._trans.__('Open Error'), error)); // Stop the event propagation. event.preventDefault(); event.stopPropagation(); return; } if (node.classList.contains(BREADCRUMB_ITEM_CLASS) || node.classList.contains(BREADCRUMB_ROOT_CLASS)) { let destination; if (node.classList.contains(BREADCRUMB_ROOT_CLASS)) { destination = '/'; } else { destination = `/${node.dataset.path}`; } if (destination) { this._model .cd(destination) .catch(error => showErrorMessage(this._trans.__('Open Error'), error)); } // Stop the event propagation. event.preventDefault(); event.stopPropagation(); return; } node = node.parentElement; } } /** * Handle the `'lm-dragenter'` event for the widget. */ _evtDragEnter(event) { if (event.mimeData.hasData(CONTENTS_MIME)) { const breadcrumbElements = this._getBreadcrumbElements(); let index = -1; let target = event.target; while (target && target !== this.node) { index = breadcrumbElements.indexOf(target); if (index !== -1) { break; } target = target.parentElement; } if (index !== -1) { const hitElement = breadcrumbElements[index]; // Don't allow dropping on the current path const currentPath = this._model.manager.services.contents.localPath(this._model.path); if (hitElement.dataset.path !== currentPath) { hitElement.classList.add(DROP_TARGET_CLASS); event.preventDefault(); event.stopPropagation(); } } } } /** * Handle the `'lm-dragleave'` event for the widget. */ _evtDragLeave(event) { event.preventDefault(); event.stopPropagation(); const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS); if (dropTarget) { dropTarget.classList.remove(DROP_TARGET_CLASS); } } /** * Handle the `'lm-dragover'` event for the widget. */ _evtDragOver(event) { event.preventDefault(); event.stopPropagation(); event.dropAction = event.proposedAction; const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS); if (dropTarget) { dropTarget.classList.remove(DROP_TARGET_CLASS); } const breadcrumbElements = this._getBreadcrumbElements(); let index = -1; let target = event.target; while (target && target !== this.node) { index = breadcrumbElements.indexOf(target); if (index !== -1) { break; } target = target.parentElement; } if (index !== -1) { breadcrumbElements[index].classList.add(DROP_TARGET_CLASS); } } /** * Handle the `'lm-drop'` event for the widget. */ _evtDrop(event) { event.preventDefault(); event.stopPropagation(); if (event.proposedAction === 'none') { event.dropAction = 'none'; return; } if (!event.mimeData.hasData(CONTENTS_MIME)) { return; } event.dropAction = event.proposedAction; let target = event.target; while (target && target.parentElement) { if (target.classList.contains(DROP_TARGET_CLASS)) { target.classList.remove(DROP_TARGET_CLASS); break; } target = target.parentElement; } let destinationPath = null; if (target.classList.contains(BREADCRUMB_ROOT_CLASS)) { destinationPath = '/'; } else if (target.classList.contains(BREADCRUMB_PREFERRED_CLASS)) { const preferredPath = PageConfig.getOption('preferredPath'); destinationPath = preferredPath ? '/' + preferredPath : '/'; } else if (target.dataset.path) { destinationPath = target.dataset.path; } if (!destinationPath) { return; } const model = this._model; const manager = model.manager; // Move all of the items. const promises = []; const oldPaths = event.mimeData.getData(CONTENTS_MIME); for (const oldPath of oldPaths) { const name = PathExt.basename(oldPath); const newPath = PathExt.join(destinationPath, name); promises.push(renameFile(manager, oldPath, newPath)); } void Promise.all(promises).catch(err => { return showErrorMessage(this._trans.__('Move Error'), err); }); } /** * Get all breadcrumb elements that can be drop targets. */ _getBreadcrumbElements() { const elements = []; const children = this.node.children; for (let i = 0; i < children.length; i++) { const child = children[i]; if ((child.classList.contains(BREADCRUMB_ITEM_CLASS) || child.classList.contains(BREADCRUMB_ROOT_CLASS) || child.classList.contains(BREADCRUMB_PREFERRED_CLASS)) && !child.classList.contains(BREADCRUMB_ELLIPSIS_CLASS)) { elements.push(child); } } return elements; } } /** * The namespace for the crumbs private data. */ var Private; (function (Private) { /** * Breadcrumb item list enum. */ let Crumb; (function (Crumb) { Crumb[Crumb["Home"] = 0] = "Home"; Crumb[Crumb["Ellipsis"] = 1] = "Ellipsis"; Crumb[Crumb["Preferred"] = 2] = "Preferred"; })(Crumb = Private.Crumb || (Private.Crumb = {})); /** * Populate the breadcrumb node. */ function updateCrumbs(breadcrumbs, state) { const node = breadcrumbs[0].parentNode; // Remove all but the home or preferred node. const firstChild = node.firstChild; while (firstChild && firstChild.nextSibling) { node.removeChild(firstChild.nextSibling); } if (state.hasPreferred) { node.appendChild(breadcrumbs[Crumb.Home]); node.appendChild(createCrumbSeparator()); } else { node.appendChild(createCrumbSeparator()); } const parts = state.path.split('/').filter(part => part !== ''); if (!state.fullPath && parts.length > 0) { const minimumLeftItems = state.minimumLeftItems; const minimumRightItems = state.minimumRightItems; // Check if we need ellipsis if (parts.length > minimumLeftItems + minimumRightItems) { // Add left items for (let i = 0; i < minimumLeftItems; i++) { const elemPath = parts.slice(0, i + 1).join('/'); const elem = createBreadcrumbElement(parts[i], elemPath); node.appendChild(elem); node.appendChild(createCrumbSeparator()); } // Add ellipsis node.appendChild(breadcrumbs[Crumb.Ellipsis]); const hiddenStartIndex = minimumLeftItems; const hiddenEndIndex = parts.length - minimumRightItems; const hiddenParts = parts.slice(hiddenStartIndex, hiddenEndIndex); const hiddenFolders = hiddenParts.join('/'); const hiddenPath = hiddenParts.length > 0 ? parts.slice(0, hiddenEndIndex).join('/') : parts.slice(0, minimumLeftItems).join('/'); breadcrumbs[Crumb.Ellipsis].title = hiddenFolders; breadcrumbs[Crumb.Ellipsis].dataset.path = hiddenPath; node.appendChild(createCrumbSeparator()); // Add right items const rightStartIndex = parts.length - minimumRightItems; for (let i = rightStartIndex; i < parts.length; i++) { const elemPath = parts.slice(0, i + 1).join('/'); const elem = createBreadcrumbElement(parts[i], elemPath); node.appendChild(elem); node.appendChild(createCrumbSeparator()); } } else { for (let i = 0; i < parts.length; i++) { const elemPath = parts.slice(0, i + 1).join('/'); const elem = createBreadcrumbElement(parts[i], elemPath); node.appendChild(elem); node.appendChild(createCrumbSeparator()); } } } else if (state.fullPath && parts.length > 0) { for (let i = 0; i < parts.length; i++) { const elemPath = parts.slice(0, i + 1).join('/'); const elem = createBreadcrumbElement(parts[i], elemPath); node.appendChild(elem); const separator = document.createElement('span'); separator.textContent = '/'; node.appendChild(separator); } } } Private.updateCrumbs = updateCrumbs; /** * Create a breadcrumb element for a path part. */ function createBreadcrumbElement(pathPart, fullPath) { const elem = document.createElement('span'); elem.className = BREADCRUMB_ITEM_CLASS; elem.textContent = pathPart; elem.title = fullPath; elem.dataset.path = fullPath; return elem; } /** * Create the breadcrumb nodes. */ function createCrumbs() { const home = rootIcon.element({ className: BREADCRUMB_ROOT_CLASS, tag: 'span', title: PageConfig.getOption('serverRoot') || 'Jupyter Server Root', stylesheet: 'breadCrumb' }); home.dataset.path = '/'; const ellipsis = ellipsesIcon.element({ className: `${BREADCRUMB_ITEM_CLASS} ${BREADCRUMB_ELLIPSIS_CLASS}`, tag: 'span', stylesheet: 'breadCrumb' }); const preferredPath = PageConfig.getOption('preferredPath'); const path = preferredPath ? '/' + preferredPath : preferredPath; const preferred = preferredIcon.element({ className: BREADCRUMB_PREFERRED_CLASS, tag: 'span', title: path || 'Jupyter Preferred Path', stylesheet: 'breadCrumb' }); preferred.dataset.path = path || '/'; return [home, ellipsis, preferred]; } Private.createCrumbs = createCrumbs; /** * Create the breadcrumb separator nodes. */ function createCrumbSeparator() { const item = document.createElement('span'); item.textContent = '/'; return item; } Private.createCrumbSeparator = createCrumbSeparator; })(Private || (Private = {})); //# sourceMappingURL=crumbs.js.map