1 |
|
2 |
|
3 | import { DOMUtils, showErrorMessage } from '@jupyterlab/apputils';
|
4 | import { PageConfig, PathExt } from '@jupyterlab/coreutils';
|
5 | import { renameFile } from '@jupyterlab/docmanager';
|
6 | import { nullTranslator } from '@jupyterlab/translation';
|
7 | import { ellipsesIcon, homeIcon as preferredIcon, folderIcon as rootIcon } from '@jupyterlab/ui-components';
|
8 | import { ArrayExt } from '@lumino/algorithm';
|
9 | import { JSONExt } from '@lumino/coreutils';
|
10 | import { ElementExt } from '@lumino/domutils';
|
11 | import { Widget } from '@lumino/widgets';
|
12 |
|
13 |
|
14 |
|
15 | const BREADCRUMB_CLASS = 'jp-BreadCrumbs';
|
16 |
|
17 |
|
18 |
|
19 | const BREADCRUMB_ROOT_CLASS = 'jp-BreadCrumbs-home';
|
20 |
|
21 |
|
22 |
|
23 | const BREADCRUMB_PREFERRED_CLASS = 'jp-BreadCrumbs-preferred';
|
24 |
|
25 |
|
26 |
|
27 | const BREADCRUMB_ITEM_CLASS = 'jp-BreadCrumbs-item';
|
28 |
|
29 |
|
30 |
|
31 | const BREAD_CRUMB_PATHS = ['/', '../../', '../', ''];
|
32 |
|
33 |
|
34 |
|
35 | const CONTENTS_MIME = 'application/x-jupyter-icontents';
|
36 |
|
37 |
|
38 |
|
39 | const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
|
40 |
|
41 |
|
42 |
|
43 | export class BreadCrumbs extends Widget {
|
44 | |
45 |
|
46 |
|
47 |
|
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 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
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 |
|
100 |
|
101 | get fullPath() {
|
102 | return this._fullPath;
|
103 | }
|
104 | set fullPath(value) {
|
105 | this._fullPath = value;
|
106 | }
|
107 | |
108 |
|
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 |
|
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 |
|
134 |
|
135 | onUpdateRequest(msg) {
|
136 |
|
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 |
|
152 |
|
153 | _evtClick(event) {
|
154 |
|
155 | if (event.button !== 0) {
|
156 | return;
|
157 | }
|
158 |
|
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 |
|
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 |
|
183 | event.preventDefault();
|
184 | event.stopPropagation();
|
185 | return;
|
186 | }
|
187 | node = node.parentElement;
|
188 | }
|
189 | }
|
190 | |
191 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
278 |
|
279 | var Private;
|
280 | (function (Private) {
|
281 | |
282 |
|
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 |
|
294 |
|
295 | function updateCrumbs(breadcrumbs, separators, state) {
|
296 | const node = breadcrumbs[0].parentNode;
|
297 |
|
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 |
|
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 |
|
376 |
|
377 | function createCrumbSeparators() {
|
378 | const items = [];
|
379 |
|
380 | const MAX_DIRECTORIES = 2;
|
381 |
|
382 |
|
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 |
|
\ | No newline at end of file |