@jupyterlab/filebrowser 4.6.0-alpha.4 → 4.6.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser.d.ts +14 -0
- package/lib/browser.js +48 -1
- package/lib/browser.js.map +1 -1
- package/lib/crumbs.d.ts +76 -0
- package/lib/crumbs.js +362 -81
- package/lib/crumbs.js.map +1 -1
- package/lib/listing.d.ts +84 -11
- package/lib/listing.js +276 -99
- package/lib/listing.js.map +1 -1
- package/lib/model.d.ts +25 -0
- package/lib/model.js +48 -2
- package/lib/model.js.map +1 -1
- package/lib/opendialog.d.ts +7 -0
- package/lib/opendialog.js +5 -4
- package/lib/opendialog.js.map +1 -1
- package/lib/pathnavigator.d.ts +108 -0
- package/lib/pathnavigator.js +389 -0
- package/lib/pathnavigator.js.map +1 -0
- package/package.json +11 -11
- package/src/browser.ts +50 -1
- package/src/crumbs.ts +432 -87
- package/src/listing.ts +418 -119
- package/src/model.ts +63 -2
- package/src/opendialog.ts +13 -0
- package/src/pathnavigator.ts +456 -0
- package/style/base.css +64 -35
- package/style/pathnavigator.css +72 -0
package/lib/crumbs.js
CHANGED
|
@@ -5,6 +5,7 @@ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
|
|
|
5
5
|
import { renameFile } from '@jupyterlab/docmanager';
|
|
6
6
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
7
7
|
import { ellipsesIcon, homeIcon as preferredIcon, folderIcon as rootIcon } from '@jupyterlab/ui-components';
|
|
8
|
+
import { PathNavigator } from './pathnavigator';
|
|
8
9
|
import { JSONExt } from '@lumino/coreutils';
|
|
9
10
|
import { Throttler } from '@lumino/polling';
|
|
10
11
|
import { Widget } from '@lumino/widgets';
|
|
@@ -40,6 +41,18 @@ const CONTENTS_MIME = 'application/x-jupyter-icontents';
|
|
|
40
41
|
* The class name added to drop targets.
|
|
41
42
|
*/
|
|
42
43
|
const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
|
|
44
|
+
/**
|
|
45
|
+
* The class name for the container span that holds all breadcrumb items.
|
|
46
|
+
*/
|
|
47
|
+
const BREADCRUMB_CONTAINER_CLASS = 'jp-BreadCrumbs-container';
|
|
48
|
+
/**
|
|
49
|
+
* The class name for the inner wrapper that hugs breadcrumb content.
|
|
50
|
+
*/
|
|
51
|
+
const BREADCRUMB_CONTENT_CLASS = 'jp-BreadCrumbs-content';
|
|
52
|
+
/**
|
|
53
|
+
* The class name added to the breadcrumb node when in edit mode.
|
|
54
|
+
*/
|
|
55
|
+
const BREADCRUMB_EDIT_MODE_CLASS = 'jp-mod-editMode';
|
|
43
56
|
/**
|
|
44
57
|
* A class which hosts folder breadcrumbs.
|
|
45
58
|
*/
|
|
@@ -55,21 +68,31 @@ export class BreadCrumbs extends Widget {
|
|
|
55
68
|
this._previousState = null;
|
|
56
69
|
this._cachedWidths = null;
|
|
57
70
|
this._lastRenderedWidth = 0;
|
|
71
|
+
this._isEditMode = false;
|
|
72
|
+
this._lastPath = '';
|
|
73
|
+
/**
|
|
74
|
+
* After `cd()` rebuilds the trail, restore focus to the current-directory segment.
|
|
75
|
+
*/
|
|
76
|
+
this._restoreBreadcrumbFocusAfterUpdate = false;
|
|
58
77
|
this.translator = options.translator || nullTranslator;
|
|
59
78
|
this._trans = this.translator.load('jupyterlab');
|
|
60
79
|
this._model = options.model;
|
|
61
80
|
this._fullPath = options.fullPath || false;
|
|
62
81
|
this._minimumLeftItems = (_a = options.minimumLeftItems) !== null && _a !== void 0 ? _a : 0;
|
|
63
82
|
this._minimumRightItems = (_b = options.minimumRightItems) !== null && _b !== void 0 ? _b : 2;
|
|
83
|
+
this._onPathEdited = options.onPathEdited;
|
|
84
|
+
this._onPathActivated = options.onPathActivated;
|
|
64
85
|
this.addClass(BREADCRUMB_CLASS);
|
|
65
86
|
this._crumbs = Private.createCrumbs();
|
|
66
87
|
const hasPreferred = PageConfig.getOption('preferredPath');
|
|
67
88
|
this._hasPreferred = hasPreferred && hasPreferred !== '/' ? true : false;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
89
|
+
this._crumbContainer = document.createElement('span');
|
|
90
|
+
this._crumbContainer.className = BREADCRUMB_CONTAINER_CLASS;
|
|
91
|
+
this._crumbContent = document.createElement('span');
|
|
92
|
+
this._crumbContent.className = BREADCRUMB_CONTENT_CLASS;
|
|
93
|
+
this._crumbContainer.appendChild(this._crumbContent);
|
|
94
|
+
this.node.appendChild(this._crumbContainer);
|
|
95
|
+
this._model.refreshed.connect(this._onModelRefreshed, this);
|
|
73
96
|
this._resizeThrottler = new Throttler(() => this._onResize(), 50);
|
|
74
97
|
this._resizeObserver = new ResizeObserver(entries => {
|
|
75
98
|
const entry = entries[0];
|
|
@@ -84,6 +107,11 @@ export class BreadCrumbs extends Widget {
|
|
|
84
107
|
void this._resizeThrottler.invoke();
|
|
85
108
|
}
|
|
86
109
|
});
|
|
110
|
+
this._pathNavigator = new PathNavigator({
|
|
111
|
+
model: this._model,
|
|
112
|
+
translator: options.translator
|
|
113
|
+
});
|
|
114
|
+
this._pathNavigator.closed.connect(this._exitEditMode, this);
|
|
87
115
|
}
|
|
88
116
|
/**
|
|
89
117
|
* Handle the DOM events for the bread crumbs.
|
|
@@ -97,6 +125,9 @@ export class BreadCrumbs extends Widget {
|
|
|
97
125
|
*/
|
|
98
126
|
handleEvent(event) {
|
|
99
127
|
switch (event.type) {
|
|
128
|
+
case 'keydown':
|
|
129
|
+
this._evtKeyDown(event);
|
|
130
|
+
break;
|
|
100
131
|
case 'click':
|
|
101
132
|
this._evtClick(event);
|
|
102
133
|
break;
|
|
@@ -150,6 +181,9 @@ export class BreadCrumbs extends Widget {
|
|
|
150
181
|
if (this.isDisposed) {
|
|
151
182
|
return;
|
|
152
183
|
}
|
|
184
|
+
this._model.refreshed.disconnect(this._onModelRefreshed, this);
|
|
185
|
+
this._pathNavigator.closed.disconnect(this._exitEditMode, this);
|
|
186
|
+
this._pathNavigator.dispose();
|
|
153
187
|
this._resizeObserver.disconnect();
|
|
154
188
|
this._resizeThrottler.dispose();
|
|
155
189
|
super.dispose();
|
|
@@ -161,19 +195,23 @@ export class BreadCrumbs extends Widget {
|
|
|
161
195
|
super.onAfterAttach(msg);
|
|
162
196
|
this.update();
|
|
163
197
|
const node = this.node;
|
|
198
|
+
node.addEventListener('keydown', this);
|
|
164
199
|
node.addEventListener('click', this);
|
|
165
200
|
node.addEventListener('lm-dragenter', this);
|
|
166
201
|
node.addEventListener('lm-dragleave', this);
|
|
167
202
|
node.addEventListener('lm-dragover', this);
|
|
168
203
|
this._resizeObserver.observe(node);
|
|
169
204
|
node.addEventListener('lm-drop', this);
|
|
205
|
+
Widget.attach(this._pathNavigator, this.node);
|
|
170
206
|
}
|
|
171
207
|
/**
|
|
172
208
|
* A message handler invoked on a `'before-detach'` message.
|
|
173
209
|
*/
|
|
174
210
|
onBeforeDetach(msg) {
|
|
211
|
+
Widget.detach(this._pathNavigator);
|
|
175
212
|
super.onBeforeDetach(msg);
|
|
176
213
|
const node = this.node;
|
|
214
|
+
node.removeEventListener('keydown', this);
|
|
177
215
|
node.removeEventListener('click', this);
|
|
178
216
|
node.removeEventListener('lm-dragenter', this);
|
|
179
217
|
node.removeEventListener('lm-dragleave', this);
|
|
@@ -185,9 +223,18 @@ export class BreadCrumbs extends Widget {
|
|
|
185
223
|
* A handler invoked on an `'update-request'` message.
|
|
186
224
|
*/
|
|
187
225
|
onUpdateRequest(msg) {
|
|
226
|
+
if (this._isEditMode) {
|
|
227
|
+
// In edit mode the CSS class hides breadcrumb content,
|
|
228
|
+
// we do not need to refresh, as only the pathnavigator is visible.
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
188
231
|
// Update the breadcrumb list.
|
|
189
232
|
const contents = this._model.manager.services.contents;
|
|
190
233
|
const localPath = contents.localPath(this._model.path);
|
|
234
|
+
// We track the path on which we are;
|
|
235
|
+
// this is to make sure we don't exit edit mode on model refresh if
|
|
236
|
+
// the path has not changed.
|
|
237
|
+
this._lastPath = localPath;
|
|
191
238
|
// Invalidate cached widths if the path changed
|
|
192
239
|
if (this._previousState && this._previousState.path !== localPath) {
|
|
193
240
|
this._cachedWidths = null;
|
|
@@ -196,16 +243,191 @@ export class BreadCrumbs extends Widget {
|
|
|
196
243
|
const adaptiveItems = this._calculateAdaptiveItems(localPath);
|
|
197
244
|
const state = {
|
|
198
245
|
path: localPath,
|
|
246
|
+
root: this._model.root,
|
|
199
247
|
hasPreferred: this._hasPreferred,
|
|
200
248
|
fullPath: this._fullPath,
|
|
201
249
|
minimumLeftItems: adaptiveItems.left,
|
|
202
250
|
minimumRightItems: adaptiveItems.right
|
|
203
251
|
};
|
|
204
|
-
|
|
252
|
+
const stateUnchanged = this._previousState !== null &&
|
|
253
|
+
JSONExt.deepEqual(state, this._previousState);
|
|
254
|
+
if (!stateUnchanged) {
|
|
255
|
+
this._previousState = state;
|
|
256
|
+
Private.updateCrumbs(this._crumbContent, this._crumbs, state);
|
|
257
|
+
this._syncCrumbTabIndices();
|
|
258
|
+
}
|
|
259
|
+
this._runPostActivationFocus();
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Restore crumb focus after breadcrumb activation and invoke activation callback.
|
|
263
|
+
*/
|
|
264
|
+
_runPostActivationFocus() {
|
|
265
|
+
if (!this._restoreBreadcrumbFocusAfterUpdate) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this._restoreBreadcrumbFocusAfterUpdate = false;
|
|
269
|
+
requestAnimationFrame(() => {
|
|
270
|
+
var _a;
|
|
271
|
+
if (this.isDisposed || this._isEditMode) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
(_a = this._onPathActivated) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Return breadcrumb segments in DOM order that participate in arrow-key focus.
|
|
279
|
+
*/
|
|
280
|
+
_getFocusableCrumbElements() {
|
|
281
|
+
const result = [];
|
|
282
|
+
const children = this._crumbContent.children;
|
|
283
|
+
for (let i = 0; i < children.length; i++) {
|
|
284
|
+
const el = children[i];
|
|
285
|
+
if (el.classList.contains(BREADCRUMB_PREFERRED_CLASS) ||
|
|
286
|
+
el.classList.contains(BREADCRUMB_ROOT_CLASS) ||
|
|
287
|
+
el.classList.contains(BREADCRUMB_ITEM_CLASS)) {
|
|
288
|
+
result.push(el);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Roving tabindex: one segment is in tab order (`tabIndex` 0), the rest -1.
|
|
295
|
+
*/
|
|
296
|
+
_syncCrumbTabIndices() {
|
|
297
|
+
const items = this._getFocusableCrumbElements();
|
|
298
|
+
for (let i = 0; i < items.length; i++) {
|
|
299
|
+
items[i].tabIndex = i === 0 ? 0 : -1;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Focus a crumb segment and make it the sole tab stop within the trail.
|
|
304
|
+
*/
|
|
305
|
+
_focusCrumb(crumb) {
|
|
306
|
+
const items = this._getFocusableCrumbElements();
|
|
307
|
+
const idx = items.indexOf(crumb);
|
|
308
|
+
if (idx === -1) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
for (let i = 0; i < items.length; i++) {
|
|
312
|
+
items[i].tabIndex = i === idx ? 0 : -1;
|
|
313
|
+
}
|
|
314
|
+
crumb.focus();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Focus the last segment in the trail (the current directory), for keyboard UX after navigation.
|
|
318
|
+
*/
|
|
319
|
+
_focusTrailingCrumb() {
|
|
320
|
+
const items = this._getFocusableCrumbElements();
|
|
321
|
+
if (items.length === 0) {
|
|
322
|
+
// If there is no focusable crumb segment (e.g. root-restricted path),
|
|
323
|
+
// keep focus inside the breadcrumb widget itself.
|
|
324
|
+
this.node.tabIndex = -1;
|
|
325
|
+
this.node.focus();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this._focusCrumb(items[items.length - 1]);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Walk from an event target to the nearest breadcrumb segment host, if any.
|
|
332
|
+
*/
|
|
333
|
+
_resolveCrumbFromEventTarget(target) {
|
|
334
|
+
let node = target;
|
|
335
|
+
while (node && node !== this.node) {
|
|
336
|
+
if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS) ||
|
|
337
|
+
node.classList.contains(BREADCRUMB_ROOT_CLASS) ||
|
|
338
|
+
node.classList.contains(BREADCRUMB_ITEM_CLASS)) {
|
|
339
|
+
return node;
|
|
340
|
+
}
|
|
341
|
+
node = node.parentElement;
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Whether a crumb corresponds to the current directory segment.
|
|
347
|
+
*/
|
|
348
|
+
_isCurrentDirectoryCrumb(crumb) {
|
|
349
|
+
if (!crumb.classList.contains(BREADCRUMB_ITEM_CLASS) ||
|
|
350
|
+
crumb.classList.contains(BREADCRUMB_ELLIPSIS_CLASS)) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const contents = this._model.manager.services.contents;
|
|
354
|
+
const localPath = contents.localPath(this._model.path);
|
|
355
|
+
return crumb.dataset.path === localPath;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Navigate to the directory represented by a breadcrumb segment.
|
|
359
|
+
*/
|
|
360
|
+
_activateCrumbSegment(crumb) {
|
|
361
|
+
this._focusCrumb(crumb);
|
|
362
|
+
const destination = this._destinationForCrumb(crumb);
|
|
363
|
+
if (!destination) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this._restoreBreadcrumbFocusAfterUpdate = true;
|
|
367
|
+
this._model.cd(destination).catch(error => {
|
|
368
|
+
this._restoreBreadcrumbFocusAfterUpdate = false;
|
|
369
|
+
void showErrorMessage(this._trans.__('Open Error'), error);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Resolve the destination path for an activated breadcrumb segment.
|
|
374
|
+
*/
|
|
375
|
+
_destinationForCrumb(crumb) {
|
|
376
|
+
if (crumb.classList.contains(BREADCRUMB_PREFERRED_CLASS)) {
|
|
377
|
+
const preferredPath = PageConfig.getOption('preferredPath');
|
|
378
|
+
return preferredPath ? '/' + preferredPath : '/';
|
|
379
|
+
}
|
|
380
|
+
if (crumb.classList.contains(BREADCRUMB_ROOT_CLASS)) {
|
|
381
|
+
return '/';
|
|
382
|
+
}
|
|
383
|
+
if (crumb.classList.contains(BREADCRUMB_ITEM_CLASS)) {
|
|
384
|
+
return `/${crumb.dataset.path}`;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Handle the `'keydown'` event for the widget.
|
|
390
|
+
*/
|
|
391
|
+
_evtKeyDown(event) {
|
|
392
|
+
if (this._isEditMode) {
|
|
205
393
|
return;
|
|
206
394
|
}
|
|
207
|
-
|
|
208
|
-
|
|
395
|
+
const isActivateKey = event.key === 'Enter' || event.key === ' ';
|
|
396
|
+
const crumb = this._resolveCrumbFromEventTarget(event.target);
|
|
397
|
+
const localPath = this._model.manager.services.contents.localPath(this._model.path);
|
|
398
|
+
if (isActivateKey && crumb && this._crumbContent.contains(crumb)) {
|
|
399
|
+
const enterEditModeFromRoot = event.key === ' ' &&
|
|
400
|
+
crumb.classList.contains(BREADCRUMB_ROOT_CLASS) &&
|
|
401
|
+
localPath === '';
|
|
402
|
+
if (this._isCurrentDirectoryCrumb(crumb) || enterEditModeFromRoot) {
|
|
403
|
+
this.enterEditMode();
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
this._activateCrumbSegment(crumb);
|
|
407
|
+
}
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
event.stopPropagation();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!crumb || !this._crumbContent.contains(crumb)) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const items = this._getFocusableCrumbElements();
|
|
419
|
+
const idx = items.indexOf(crumb);
|
|
420
|
+
if (idx === -1) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const delta = event.key === 'ArrowLeft' ? -1 : 1;
|
|
424
|
+
const nextIdx = idx + delta;
|
|
425
|
+
if (nextIdx < 0 || nextIdx >= items.length) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
this._focusCrumb(items[nextIdx]);
|
|
429
|
+
event.preventDefault();
|
|
430
|
+
event.stopPropagation();
|
|
209
431
|
}
|
|
210
432
|
/**
|
|
211
433
|
* Handle the `'click'` event for the widget.
|
|
@@ -215,34 +437,17 @@ export class BreadCrumbs extends Widget {
|
|
|
215
437
|
if (event.button !== 0) {
|
|
216
438
|
return;
|
|
217
439
|
}
|
|
440
|
+
// In edit mode the PathNavigator owns all interaction.
|
|
441
|
+
if (this._isEditMode) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
218
444
|
// Find a valid click target.
|
|
219
445
|
let node = event.target;
|
|
220
446
|
while (node && node !== this.node) {
|
|
221
|
-
if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS)
|
|
222
|
-
|
|
223
|
-
const path = preferredPath ? '/' + preferredPath : preferredPath;
|
|
224
|
-
this._model
|
|
225
|
-
.cd(path)
|
|
226
|
-
.catch(error => showErrorMessage(this._trans.__('Open Error'), error));
|
|
227
|
-
// Stop the event propagation.
|
|
228
|
-
event.preventDefault();
|
|
229
|
-
event.stopPropagation();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (node.classList.contains(BREADCRUMB_ITEM_CLASS) ||
|
|
447
|
+
if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS) ||
|
|
448
|
+
node.classList.contains(BREADCRUMB_ITEM_CLASS) ||
|
|
233
449
|
node.classList.contains(BREADCRUMB_ROOT_CLASS)) {
|
|
234
|
-
|
|
235
|
-
if (node.classList.contains(BREADCRUMB_ROOT_CLASS)) {
|
|
236
|
-
destination = '/';
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
destination = `/${node.dataset.path}`;
|
|
240
|
-
}
|
|
241
|
-
if (destination) {
|
|
242
|
-
this._model
|
|
243
|
-
.cd(destination)
|
|
244
|
-
.catch(error => showErrorMessage(this._trans.__('Open Error'), error));
|
|
245
|
-
}
|
|
450
|
+
this._activateCrumbSegment(node);
|
|
246
451
|
// Stop the event propagation.
|
|
247
452
|
event.preventDefault();
|
|
248
453
|
event.stopPropagation();
|
|
@@ -250,6 +455,10 @@ export class BreadCrumbs extends Widget {
|
|
|
250
455
|
}
|
|
251
456
|
node = node.parentElement;
|
|
252
457
|
}
|
|
458
|
+
// Click landed on the breadcrumb background (including separators
|
|
459
|
+
// between crumb items) — enter edit mode. This is intentional: the
|
|
460
|
+
// entire breadcrumb bar acts as a click target for the path editor.
|
|
461
|
+
this.enterEditMode();
|
|
253
462
|
}
|
|
254
463
|
/**
|
|
255
464
|
* Handle the `'lm-dragenter'` event for the widget.
|
|
@@ -369,7 +578,7 @@ export class BreadCrumbs extends Widget {
|
|
|
369
578
|
*/
|
|
370
579
|
_getBreadcrumbElements() {
|
|
371
580
|
const elements = [];
|
|
372
|
-
const children = this.
|
|
581
|
+
const children = this._crumbContent.children;
|
|
373
582
|
for (let i = 0; i < children.length; i++) {
|
|
374
583
|
const child = children[i];
|
|
375
584
|
if ((child.classList.contains(BREADCRUMB_ITEM_CLASS) ||
|
|
@@ -398,7 +607,7 @@ export class BreadCrumbs extends Widget {
|
|
|
398
607
|
* including those currently hidden behind the ellipsis.
|
|
399
608
|
*/
|
|
400
609
|
_measureAllItemWidths(parts) {
|
|
401
|
-
const node = this.
|
|
610
|
+
const node = this._crumbContent;
|
|
402
611
|
// Measure fixed elements that are already in the DOM
|
|
403
612
|
const home = this._crumbs[Private.Crumb.Home];
|
|
404
613
|
const ellipsis = this._crumbs[Private.Crumb.Ellipsis];
|
|
@@ -472,7 +681,7 @@ export class BreadCrumbs extends Widget {
|
|
|
472
681
|
// Calculate available space for items
|
|
473
682
|
let fixedOverhead = homeWidth + separatorWidth;
|
|
474
683
|
if (this._hasPreferred) {
|
|
475
|
-
fixedOverhead += preferredWidth
|
|
684
|
+
fixedOverhead += preferredWidth;
|
|
476
685
|
}
|
|
477
686
|
const availableForItems = containerWidth - fixedOverhead;
|
|
478
687
|
// Check if all parts can fit without ellipsis
|
|
@@ -524,6 +733,63 @@ export class BreadCrumbs extends Widget {
|
|
|
524
733
|
right: finalRight
|
|
525
734
|
};
|
|
526
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Enter edit mode: show the path input and hide the breadcrumb content.
|
|
738
|
+
*/
|
|
739
|
+
enterEditMode() {
|
|
740
|
+
this._isEditMode = true;
|
|
741
|
+
// Snapshot the current path so _onModelRefreshed can reliably detect
|
|
742
|
+
// whether the path actually changed while in edit mode.
|
|
743
|
+
const contents = this._model.manager.services.contents;
|
|
744
|
+
this._lastPath = contents.localPath(this._model.path);
|
|
745
|
+
this.node.classList.add(BREADCRUMB_EDIT_MODE_CLASS);
|
|
746
|
+
// Clear cached state so that when we exit edit mode, onUpdateRequest
|
|
747
|
+
// will unconditionally re-render the breadcrumbs (the path may have
|
|
748
|
+
// changed while we were editing). _exitEditMode also clears this for
|
|
749
|
+
// the same reason — the double-null is intentional even if technically
|
|
750
|
+
// unnecessary.
|
|
751
|
+
this._previousState = null;
|
|
752
|
+
this._pathNavigator.open();
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Move focus to the trailing breadcrumb segment.
|
|
756
|
+
*/
|
|
757
|
+
focusLastCrumb() {
|
|
758
|
+
// Defer to ensure any pending breadcrumb DOM updates are applied.
|
|
759
|
+
requestAnimationFrame(() => {
|
|
760
|
+
this._focusTrailingCrumb();
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Exit edit mode and restore the breadcrumb display.
|
|
765
|
+
*/
|
|
766
|
+
_exitEditMode() {
|
|
767
|
+
if (!this._isEditMode) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
this._isEditMode = false;
|
|
771
|
+
this.node.classList.remove(BREADCRUMB_EDIT_MODE_CLASS);
|
|
772
|
+
this._previousState = null;
|
|
773
|
+
this.update();
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Handle the model's `refreshed` signal.
|
|
777
|
+
* If we are in edit mode, dismiss it (the model path may have changed).
|
|
778
|
+
*/
|
|
779
|
+
_onModelRefreshed() {
|
|
780
|
+
var _a;
|
|
781
|
+
const contents = this._model.manager.services.contents;
|
|
782
|
+
const localPath = contents.localPath(this._model.path);
|
|
783
|
+
if (this._isEditMode) {
|
|
784
|
+
if (this._pathNavigator.matchesSubmittedPath(localPath) ||
|
|
785
|
+
localPath !== this._lastPath) {
|
|
786
|
+
this._exitEditMode();
|
|
787
|
+
(_a = this._onPathEdited) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
this.update();
|
|
792
|
+
}
|
|
527
793
|
}
|
|
528
794
|
/**
|
|
529
795
|
* The namespace for the crumbs private data.
|
|
@@ -540,73 +806,84 @@ var Private;
|
|
|
540
806
|
Crumb[Crumb["Preferred"] = 2] = "Preferred";
|
|
541
807
|
})(Crumb = Private.Crumb || (Private.Crumb = {}));
|
|
542
808
|
/**
|
|
543
|
-
* Populate the breadcrumb node.
|
|
809
|
+
* Populate the breadcrumb container node.
|
|
810
|
+
*
|
|
811
|
+
* @param container - The container element that holds breadcrumb items.
|
|
812
|
+
* @param breadcrumbs - The reusable breadcrumb elements (Home, Ellipsis, Preferred).
|
|
813
|
+
* @param state - The current breadcrumb state.
|
|
544
814
|
*/
|
|
545
|
-
function updateCrumbs(breadcrumbs, state) {
|
|
546
|
-
const
|
|
547
|
-
//
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if
|
|
553
|
-
|
|
554
|
-
|
|
815
|
+
function updateCrumbs(container, breadcrumbs, state) {
|
|
816
|
+
const nodes = [];
|
|
817
|
+
// Calculate the starting index for breadcrumbs based on root restriction
|
|
818
|
+
const rootParts = state.root
|
|
819
|
+
? state.root.split('/').filter(part => part !== '')
|
|
820
|
+
: [];
|
|
821
|
+
const rootDepth = rootParts.length;
|
|
822
|
+
// Only show home/preferred if there's no root restriction
|
|
823
|
+
if (!state.root) {
|
|
824
|
+
if (state.hasPreferred) {
|
|
825
|
+
nodes.push(breadcrumbs[Private.Crumb.Preferred]);
|
|
826
|
+
}
|
|
827
|
+
nodes.push(breadcrumbs[Crumb.Home], createCrumbSeparator());
|
|
555
828
|
}
|
|
556
829
|
else {
|
|
557
|
-
|
|
830
|
+
// With root restriction, just add an initial separator
|
|
831
|
+
nodes.push(createCrumbSeparator());
|
|
558
832
|
}
|
|
559
833
|
const parts = state.path.split('/').filter(part => part !== '');
|
|
560
|
-
|
|
834
|
+
// Filter parts to only show those at or below the root
|
|
835
|
+
const visibleParts = state.root ? parts.slice(rootDepth) : parts;
|
|
836
|
+
const visibleStartIndex = rootDepth;
|
|
837
|
+
if (!state.fullPath && visibleParts.length > 0) {
|
|
561
838
|
const minimumLeftItems = state.minimumLeftItems;
|
|
562
839
|
const minimumRightItems = state.minimumRightItems;
|
|
563
|
-
// Check if we need ellipsis
|
|
564
|
-
if (
|
|
840
|
+
// Check if we need ellipsis (based on visible parts)
|
|
841
|
+
if (visibleParts.length > minimumLeftItems + minimumRightItems) {
|
|
565
842
|
// Add left items
|
|
566
843
|
for (let i = 0; i < minimumLeftItems; i++) {
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
844
|
+
const fullIndex = visibleStartIndex + i;
|
|
845
|
+
const elemPath = parts.slice(0, fullIndex + 1).join('/');
|
|
846
|
+
nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
|
|
847
|
+
nodes.push(createCrumbSeparator());
|
|
571
848
|
}
|
|
572
849
|
// Add ellipsis
|
|
573
|
-
node.appendChild(breadcrumbs[Crumb.Ellipsis]);
|
|
574
850
|
const hiddenStartIndex = minimumLeftItems;
|
|
575
|
-
const hiddenEndIndex =
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
851
|
+
const hiddenEndIndex = visibleParts.length - minimumRightItems;
|
|
852
|
+
const hiddenVisibleParts = visibleParts.slice(hiddenStartIndex, hiddenEndIndex);
|
|
853
|
+
const ellipsis = breadcrumbs[Crumb.Ellipsis];
|
|
854
|
+
ellipsis.title = hiddenVisibleParts.join('/');
|
|
855
|
+
ellipsis.dataset.path =
|
|
856
|
+
hiddenVisibleParts.length > 0
|
|
857
|
+
? parts.slice(0, visibleStartIndex + hiddenEndIndex).join('/')
|
|
858
|
+
: parts.slice(0, visibleStartIndex + minimumLeftItems).join('/');
|
|
859
|
+
nodes.push(ellipsis, createCrumbSeparator());
|
|
584
860
|
// Add right items
|
|
585
|
-
const rightStartIndex =
|
|
586
|
-
for (let i = rightStartIndex; i <
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
861
|
+
const rightStartIndex = visibleParts.length - minimumRightItems;
|
|
862
|
+
for (let i = rightStartIndex; i < visibleParts.length; i++) {
|
|
863
|
+
const fullIndex = visibleStartIndex + i;
|
|
864
|
+
const elemPath = parts.slice(0, fullIndex + 1).join('/');
|
|
865
|
+
nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
|
|
866
|
+
nodes.push(createCrumbSeparator());
|
|
591
867
|
}
|
|
592
868
|
}
|
|
593
869
|
else {
|
|
594
|
-
for (let i = 0; i <
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
870
|
+
for (let i = 0; i < visibleParts.length; i++) {
|
|
871
|
+
const fullIndex = visibleStartIndex + i;
|
|
872
|
+
const elemPath = parts.slice(0, fullIndex + 1).join('/');
|
|
873
|
+
nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
|
|
874
|
+
nodes.push(createCrumbSeparator());
|
|
599
875
|
}
|
|
600
876
|
}
|
|
601
877
|
}
|
|
602
|
-
else if (state.fullPath &&
|
|
603
|
-
for (let i = 0; i <
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
878
|
+
else if (state.fullPath && visibleParts.length > 0) {
|
|
879
|
+
for (let i = 0; i < visibleParts.length; i++) {
|
|
880
|
+
const fullIndex = visibleStartIndex + i;
|
|
881
|
+
const elemPath = parts.slice(0, fullIndex + 1).join('/');
|
|
882
|
+
nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
|
|
883
|
+
nodes.push(createCrumbSeparator());
|
|
608
884
|
}
|
|
609
885
|
}
|
|
886
|
+
container.replaceChildren(...nodes);
|
|
610
887
|
}
|
|
611
888
|
Private.updateCrumbs = updateCrumbs;
|
|
612
889
|
/**
|
|
@@ -618,6 +895,7 @@ var Private;
|
|
|
618
895
|
elem.textContent = pathPart;
|
|
619
896
|
elem.title = fullPath;
|
|
620
897
|
elem.dataset.path = fullPath;
|
|
898
|
+
elem.tabIndex = -1;
|
|
621
899
|
return elem;
|
|
622
900
|
}
|
|
623
901
|
/**
|
|
@@ -631,11 +909,13 @@ var Private;
|
|
|
631
909
|
stylesheet: 'breadCrumb'
|
|
632
910
|
});
|
|
633
911
|
home.dataset.path = '/';
|
|
912
|
+
home.tabIndex = -1;
|
|
634
913
|
const ellipsis = ellipsesIcon.element({
|
|
635
914
|
className: `${BREADCRUMB_ITEM_CLASS} ${BREADCRUMB_ELLIPSIS_CLASS}`,
|
|
636
915
|
tag: 'span',
|
|
637
916
|
stylesheet: 'breadCrumb'
|
|
638
917
|
});
|
|
918
|
+
ellipsis.tabIndex = -1;
|
|
639
919
|
const preferredPath = PageConfig.getOption('preferredPath');
|
|
640
920
|
const path = preferredPath ? '/' + preferredPath : preferredPath;
|
|
641
921
|
const preferred = preferredIcon.element({
|
|
@@ -645,6 +925,7 @@ var Private;
|
|
|
645
925
|
stylesheet: 'breadCrumb'
|
|
646
926
|
});
|
|
647
927
|
preferred.dataset.path = path || '/';
|
|
928
|
+
preferred.tabIndex = -1;
|
|
648
929
|
return [home, ellipsis, preferred];
|
|
649
930
|
}
|
|
650
931
|
Private.createCrumbs = createCrumbs;
|