UNPKG

14.5 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { DOMUtils, showErrorMessage } from '@jupyterlab/apputils';
4import { PageConfig, PathExt } from '@jupyterlab/coreutils';
5import { renameFile } from '@jupyterlab/docmanager';
6import { nullTranslator } from '@jupyterlab/translation';
7import { ellipsesIcon, homeIcon as preferredIcon, folderIcon as rootIcon } from '@jupyterlab/ui-components';
8import { ArrayExt } from '@lumino/algorithm';
9import { JSONExt } from '@lumino/coreutils';
10import { ElementExt } from '@lumino/domutils';
11import { Widget } from '@lumino/widgets';
12/**
13 * The class name added to the breadcrumb node.
14 */
15const BREADCRUMB_CLASS = 'jp-BreadCrumbs';
16/**
17 * The class name for the breadcrumbs home node
18 */
19const BREADCRUMB_ROOT_CLASS = 'jp-BreadCrumbs-home';
20/**
21 * The class name for the breadcrumbs preferred node
22 */
23const BREADCRUMB_PREFERRED_CLASS = 'jp-BreadCrumbs-preferred';
24/**
25 * The class name added to the breadcrumb node.
26 */
27const BREADCRUMB_ITEM_CLASS = 'jp-BreadCrumbs-item';
28/**
29 * Bread crumb paths.
30 */
31const BREAD_CRUMB_PATHS = ['/', '../../', '../', ''];
32/**
33 * The mime type for a contents drag object.
34 */
35const CONTENTS_MIME = 'application/x-jupyter-icontents';
36/**
37 * The class name added to drop targets.
38 */
39const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
40/**
41 * A class which hosts folder breadcrumbs.
42 */
43export class BreadCrumbs extends Widget {
44 /**
45 * Construct a new file browser crumb widget.
46 *
47 * @param model - The file browser view model.
48 */
49 constructor(options) {
50 super();
51 this._previousState = null;
52 this.translator = options.translator || nullTranslator;
53 this._trans = this.translator.load('jupyterlab');
54 this._model = options.model;
55 this._fullPath = options.fullPath || false;
56 this.addClass(BREADCRUMB_CLASS);
57 this._crumbs = Private.createCrumbs();
58 this._crumbSeps = Private.createCrumbSeparators();
59 const hasPreferred = PageConfig.getOption('preferredPath');
60 this._hasPreferred = hasPreferred && hasPreferred !== '/' ? true : false;
61 if (this._hasPreferred) {
62 this.node.appendChild(this._crumbs[Private.Crumb.Preferred]);
63 }
64 this.node.appendChild(this._crumbs[Private.Crumb.Home]);
65 this._model.refreshed.connect(this.update, this);
66 }
67 /**
68 * Handle the DOM events for the bread crumbs.
69 *
70 * @param event - The DOM event sent to the widget.
71 *
72 * #### Notes
73 * This method implements the DOM `EventListener` interface and is
74 * called in response to events on the panel's DOM node. It should
75 * not be called directly by user code.
76 */
77 handleEvent(event) {
78 switch (event.type) {
79 case 'click':
80 this._evtClick(event);
81 break;
82 case 'lm-dragenter':
83 this._evtDragEnter(event);
84 break;
85 case 'lm-dragleave':
86 this._evtDragLeave(event);
87 break;
88 case 'lm-dragover':
89 this._evtDragOver(event);
90 break;
91 case 'lm-drop':
92 this._evtDrop(event);
93 break;
94 default:
95 return;
96 }
97 }
98 /**
99 * Whether to show the full path in the breadcrumbs
100 */
101 get fullPath() {
102 return this._fullPath;
103 }
104 set fullPath(value) {
105 this._fullPath = value;
106 }
107 /**
108 * A message handler invoked on an `'after-attach'` message.
109 */
110 onAfterAttach(msg) {
111 super.onAfterAttach(msg);
112 this.update();
113 const node = this.node;
114 node.addEventListener('click', this);
115 node.addEventListener('lm-dragenter', this);
116 node.addEventListener('lm-dragleave', this);
117 node.addEventListener('lm-dragover', this);
118 node.addEventListener('lm-drop', this);
119 }
120 /**
121 * A message handler invoked on a `'before-detach'` message.
122 */
123 onBeforeDetach(msg) {
124 super.onBeforeDetach(msg);
125 const node = this.node;
126 node.removeEventListener('click', this);
127 node.removeEventListener('lm-dragenter', this);
128 node.removeEventListener('lm-dragleave', this);
129 node.removeEventListener('lm-dragover', this);
130 node.removeEventListener('lm-drop', this);
131 }
132 /**
133 * A handler invoked on an `'update-request'` message.
134 */
135 onUpdateRequest(msg) {
136 // Update the breadcrumb list.
137 const contents = this._model.manager.services.contents;
138 const localPath = contents.localPath(this._model.path);
139 const state = {
140 path: localPath,
141 hasPreferred: this._hasPreferred,
142 fullPath: this._fullPath
143 };
144 if (this._previousState && JSONExt.deepEqual(state, this._previousState)) {
145 return;
146 }
147 this._previousState = state;
148 Private.updateCrumbs(this._crumbs, this._crumbSeps, state);
149 }
150 /**
151 * Handle the `'click'` event for the widget.
152 */
153 _evtClick(event) {
154 // Do nothing if it's not a left mouse press.
155 if (event.button !== 0) {
156 return;
157 }
158 // Find a valid click target.
159 let node = event.target;
160 while (node && node !== this.node) {
161 if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS)) {
162 this._model
163 .cd(PageConfig.getOption('preferredPath'))
164 .catch(error => showErrorMessage(this._trans.__('Open Error'), error));
165 // Stop the event propagation.
166 event.preventDefault();
167 event.stopPropagation();
168 return;
169 }
170 if (node.classList.contains(BREADCRUMB_ITEM_CLASS) ||
171 node.classList.contains(BREADCRUMB_ROOT_CLASS)) {
172 let index = ArrayExt.findFirstIndex(this._crumbs, value => value === node);
173 let destination = BREAD_CRUMB_PATHS[index];
174 if (this._fullPath &&
175 index < 0 &&
176 !node.classList.contains(BREADCRUMB_ROOT_CLASS)) {
177 destination = node.title;
178 }
179 this._model
180 .cd(destination)
181 .catch(error => showErrorMessage(this._trans.__('Open Error'), error));
182 // Stop the event propagation.
183 event.preventDefault();
184 event.stopPropagation();
185 return;
186 }
187 node = node.parentElement;
188 }
189 }
190 /**
191 * Handle the `'lm-dragenter'` event for the widget.
192 */
193 _evtDragEnter(event) {
194 if (event.mimeData.hasData(CONTENTS_MIME)) {
195 const index = ArrayExt.findFirstIndex(this._crumbs, node => ElementExt.hitTest(node, event.clientX, event.clientY));
196 if (index !== -1) {
197 if (index !== Private.Crumb.Current) {
198 this._crumbs[index].classList.add(DROP_TARGET_CLASS);
199 event.preventDefault();
200 event.stopPropagation();
201 }
202 }
203 }
204 }
205 /**
206 * Handle the `'lm-dragleave'` event for the widget.
207 */
208 _evtDragLeave(event) {
209 event.preventDefault();
210 event.stopPropagation();
211 const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
212 if (dropTarget) {
213 dropTarget.classList.remove(DROP_TARGET_CLASS);
214 }
215 }
216 /**
217 * Handle the `'lm-dragover'` event for the widget.
218 */
219 _evtDragOver(event) {
220 event.preventDefault();
221 event.stopPropagation();
222 event.dropAction = event.proposedAction;
223 const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
224 if (dropTarget) {
225 dropTarget.classList.remove(DROP_TARGET_CLASS);
226 }
227 const index = ArrayExt.findFirstIndex(this._crumbs, node => ElementExt.hitTest(node, event.clientX, event.clientY));
228 if (index !== -1) {
229 this._crumbs[index].classList.add(DROP_TARGET_CLASS);
230 }
231 }
232 /**
233 * Handle the `'lm-drop'` event for the widget.
234 */
235 _evtDrop(event) {
236 event.preventDefault();
237 event.stopPropagation();
238 if (event.proposedAction === 'none') {
239 event.dropAction = 'none';
240 return;
241 }
242 if (!event.mimeData.hasData(CONTENTS_MIME)) {
243 return;
244 }
245 event.dropAction = event.proposedAction;
246 let target = event.target;
247 while (target && target.parentElement) {
248 if (target.classList.contains(DROP_TARGET_CLASS)) {
249 target.classList.remove(DROP_TARGET_CLASS);
250 break;
251 }
252 target = target.parentElement;
253 }
254 // Get the path based on the target node.
255 const index = ArrayExt.findFirstIndex(this._crumbs, node => node === target);
256 if (index === -1) {
257 return;
258 }
259 const model = this._model;
260 const path = PathExt.resolve(model.path, BREAD_CRUMB_PATHS[index]);
261 const manager = model.manager;
262 // Move all of the items.
263 const promises = [];
264 const oldPaths = event.mimeData.getData(CONTENTS_MIME);
265 for (const oldPath of oldPaths) {
266 const localOldPath = manager.services.contents.localPath(oldPath);
267 const name = PathExt.basename(localOldPath);
268 const newPath = PathExt.join(path, name);
269 promises.push(renameFile(manager, oldPath, newPath));
270 }
271 void Promise.all(promises).catch(err => {
272 return showErrorMessage(this._trans.__('Move Error'), err);
273 });
274 }
275}
276/**
277 * The namespace for the crumbs private data.
278 */
279var Private;
280(function (Private) {
281 /**
282 * Breadcrumb item list enum.
283 */
284 let Crumb;
285 (function (Crumb) {
286 Crumb[Crumb["Home"] = 0] = "Home";
287 Crumb[Crumb["Ellipsis"] = 1] = "Ellipsis";
288 Crumb[Crumb["Parent"] = 2] = "Parent";
289 Crumb[Crumb["Current"] = 3] = "Current";
290 Crumb[Crumb["Preferred"] = 4] = "Preferred";
291 })(Crumb = Private.Crumb || (Private.Crumb = {}));
292 /**
293 * Populate the breadcrumb node.
294 */
295 function updateCrumbs(breadcrumbs, separators, state) {
296 const node = breadcrumbs[0].parentNode;
297 // Remove all but the home or preferred node.
298 const firstChild = node.firstChild;
299 while (firstChild && firstChild.nextSibling) {
300 node.removeChild(firstChild.nextSibling);
301 }
302 if (state.hasPreferred) {
303 node.appendChild(breadcrumbs[Crumb.Home]);
304 node.appendChild(separators[0]);
305 }
306 else {
307 node.appendChild(separators[0]);
308 }
309 const parts = state.path.split('/');
310 if (!state.fullPath && parts.length > 2) {
311 node.appendChild(breadcrumbs[Crumb.Ellipsis]);
312 const grandParent = parts.slice(0, parts.length - 2).join('/');
313 breadcrumbs[Crumb.Ellipsis].title = grandParent;
314 node.appendChild(separators[1]);
315 }
316 if (state.path) {
317 if (!state.fullPath) {
318 if (parts.length >= 2) {
319 breadcrumbs[Crumb.Parent].textContent = parts[parts.length - 2];
320 node.appendChild(breadcrumbs[Crumb.Parent]);
321 const parent = parts.slice(0, parts.length - 1).join('/');
322 breadcrumbs[Crumb.Parent].title = parent;
323 node.appendChild(separators[2]);
324 }
325 breadcrumbs[Crumb.Current].textContent = parts[parts.length - 1];
326 node.appendChild(breadcrumbs[Crumb.Current]);
327 breadcrumbs[Crumb.Current].title = state.path;
328 node.appendChild(separators[3]);
329 }
330 else {
331 for (let i = 0; i < parts.length; i++) {
332 const elem = document.createElement('span');
333 elem.className = BREADCRUMB_ITEM_CLASS;
334 elem.textContent = parts[i];
335 const elemPath = `/${parts.slice(0, i + 1).join('/')}`;
336 elem.title = elemPath;
337 node.appendChild(elem);
338 const separator = document.createElement('span');
339 separator.textContent = '/';
340 node.appendChild(separator);
341 }
342 }
343 }
344 }
345 Private.updateCrumbs = updateCrumbs;
346 /**
347 * Create the breadcrumb nodes.
348 */
349 function createCrumbs() {
350 const home = rootIcon.element({
351 className: BREADCRUMB_ROOT_CLASS,
352 tag: 'span',
353 title: PageConfig.getOption('serverRoot') || 'Jupyter Server Root',
354 stylesheet: 'breadCrumb'
355 });
356 const ellipsis = ellipsesIcon.element({
357 className: BREADCRUMB_ITEM_CLASS,
358 tag: 'span',
359 stylesheet: 'breadCrumb'
360 });
361 const parent = document.createElement('span');
362 parent.className = BREADCRUMB_ITEM_CLASS;
363 const current = document.createElement('span');
364 current.className = BREADCRUMB_ITEM_CLASS;
365 const preferred = preferredIcon.element({
366 className: BREADCRUMB_PREFERRED_CLASS,
367 tag: 'span',
368 title: PageConfig.getOption('preferredPath') || 'Jupyter Preferred Path',
369 stylesheet: 'breadCrumb'
370 });
371 return [home, ellipsis, parent, current, preferred];
372 }
373 Private.createCrumbs = createCrumbs;
374 /**
375 * Create the breadcrumb separator nodes.
376 */
377 function createCrumbSeparators() {
378 const items = [];
379 // The maximum number of directories that will be shown in the crumbs
380 const MAX_DIRECTORIES = 2;
381 // Make separators for after each directory, one at the beginning, and one
382 // after a possible ellipsis.
383 for (let i = 0; i < MAX_DIRECTORIES + 2; i++) {
384 const item = document.createElement('span');
385 item.textContent = '/';
386 items.push(item);
387 }
388 return items;
389 }
390 Private.createCrumbSeparators = createCrumbSeparators;
391})(Private || (Private = {}));
392//# sourceMappingURL=crumbs.js.map
\No newline at end of file