@jupyterlab/filebrowser
Version:
JupyterLab - FileBrowser Widget
480 lines • 17.7 kB
JavaScript
// 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