UNPKG

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