@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/src/model.ts CHANGED
@@ -61,6 +61,7 @@ export class FileBrowserModel implements IDisposable {
61
61
  this.translator = options.translator || nullTranslator;
62
62
  this._trans = this.translator.load('jupyterlab');
63
63
  this._driveName = options.driveName || '';
64
+ this._root = options.root || '';
64
65
  this._allowFileUploads = options.allowFileUploads ?? true;
65
66
  this._model = {
66
67
  path: this.rootPath,
@@ -149,6 +150,15 @@ export class FileBrowserModel implements IDisposable {
149
150
  return this._driveName ? this._driveName + ':' : '';
150
151
  }
151
152
 
153
+ /**
154
+ * Get the navigation root path.
155
+ *
156
+ * When set, navigation is restricted to this path and its subdirectories.
157
+ */
158
+ get root(): string {
159
+ return this._root;
160
+ }
161
+
152
162
  /**
153
163
  * A signal emitted when the path changes.
154
164
  */
@@ -241,11 +251,21 @@ export class FileBrowserModel implements IDisposable {
241
251
  * @returns A promise with the contents of the directory.
242
252
  */
243
253
  async cd(path = '.'): Promise<void> {
254
+ const isRefresh = path === '.';
244
255
  if (path !== '.') {
245
256
  path = this.manager.services.contents.resolvePath(this._model.path, path);
246
257
  } else {
247
258
  path = this._pendingPath || this._model.path;
248
259
  }
260
+ // Check if navigation is restricted and the path is outside the root
261
+ if (this._root && !this._isPathWithinRoot(path)) {
262
+ if (isRefresh) {
263
+ // During refresh, if current path is outside root, navigate to root
264
+ path = this._root;
265
+ } else {
266
+ return;
267
+ }
268
+ }
249
269
  if (this._pending) {
250
270
  // Collapse requests to the same directory.
251
271
  if (path === this._pendingPath) {
@@ -289,14 +309,19 @@ export class FileBrowserModel implements IDisposable {
289
309
  .catch(error => {
290
310
  this._pendingPath = null;
291
311
  this._pending = null;
292
- if (error.response && error.response.status === 404 && path !== '/') {
312
+ const fallbackPath = this._root || '/';
313
+ if (
314
+ error.response &&
315
+ error.response.status === 404 &&
316
+ path !== fallbackPath
317
+ ) {
293
318
  error.message = this._trans.__(
294
319
  'Directory not found: "%1"',
295
320
  this._model.path
296
321
  );
297
322
  console.error(error);
298
323
  this._connectionFailure.emit(error);
299
- return this.cd('/');
324
+ return this.cd(fallbackPath);
300
325
  } else {
301
326
  this._connectionFailure.emit(error);
302
327
  }
@@ -648,6 +673,26 @@ export class FileBrowserModel implements IDisposable {
648
673
  }
649
674
  }
650
675
 
676
+ /**
677
+ * Check if a path is within the navigation root.
678
+ *
679
+ * @param path - The path to check.
680
+ * @returns Whether the path is within the root boundary.
681
+ */
682
+ private _isPathWithinRoot(path: string): boolean {
683
+ if (!this._root) {
684
+ return true;
685
+ }
686
+ // Normalize paths for comparison (remove trailing slashes)
687
+ const normalizedPath = PathExt.removeSlash(path);
688
+ const normalizedRoot = PathExt.removeSlash(this._root);
689
+ // Path is valid if it equals the root or is a subdirectory of root
690
+ return (
691
+ normalizedPath === normalizedRoot ||
692
+ normalizedPath.startsWith(normalizedRoot + '/')
693
+ );
694
+ }
695
+
651
696
  /**
652
697
  * Populate the model's sessions collection.
653
698
  */
@@ -675,6 +720,7 @@ export class FileBrowserModel implements IDisposable {
675
720
  private _sessions: Session.IModel[] = [];
676
721
  private _state: IStateDB | null = null;
677
722
  private _driveName: string;
723
+ private _root: string;
678
724
  private _allowFileUploads: boolean;
679
725
  private _isDisposed = false;
680
726
  private _restored = new PromiseDelegate<void>();
@@ -712,6 +758,14 @@ export namespace FileBrowserModel {
712
758
  */
713
759
  manager: IDocumentManager;
714
760
 
761
+ /**
762
+ * The root path for navigation restriction.
763
+ *
764
+ * When set, navigation will be restricted to this path and its
765
+ * subdirectories. Users will not be able to navigate above this path.
766
+ */
767
+ root?: string;
768
+
715
769
  /**
716
770
  * The time interval for browser refreshing, in ms.
717
771
  */
@@ -760,6 +814,13 @@ export class TogglableHiddenFileBrowserModel extends FileBrowserModel {
760
814
  : filter(super.items(), value => !value.name.startsWith('.'));
761
815
  }
762
816
 
817
+ /**
818
+ * Whether hidden files are currently included.
819
+ */
820
+ get includeHiddenFiles(): boolean {
821
+ return this._includeHiddenFiles;
822
+ }
823
+
763
824
  /**
764
825
  * Set the inclusion of hidden files. Triggers a model refresh.
765
826
  */
package/src/opendialog.ts CHANGED
@@ -56,6 +56,14 @@ export namespace FileDialog {
56
56
  */
57
57
  defaultPath?: string;
58
58
 
59
+ /**
60
+ * The root path for navigation.
61
+ *
62
+ * When set, navigation will be restricted to this path and its
63
+ * subdirectories. Users will not be able to navigate above this path.
64
+ */
65
+ root?: string;
66
+
59
67
  /**
60
68
  * Text to display above the file browser.
61
69
  */
@@ -132,6 +140,7 @@ class OpenDialog extends Dialog<Contents.IModel[]> {
132
140
  options.filter,
133
141
  translator,
134
142
  options.defaultPath,
143
+ options.root,
135
144
  options.label,
136
145
  true,
137
146
  handleOpenFile
@@ -165,6 +174,7 @@ class OpenDialogBody
165
174
  filter?: (value: Contents.IModel) => Partial<IScore> | null,
166
175
  translator?: ITranslator,
167
176
  defaultPath?: string,
177
+ root?: string,
168
178
  label?: string,
169
179
  filterDirectories?: boolean,
170
180
  handleOpenFile?: (path: string) => void
@@ -181,6 +191,7 @@ class OpenDialogBody
181
191
  {},
182
192
  translator,
183
193
  defaultPath,
194
+ root,
184
195
  filterDirectories,
185
196
  handleOpenFile
186
197
  )
@@ -318,6 +329,7 @@ namespace Private {
318
329
  options: IFileBrowserFactory.IOptions = {},
319
330
  translator?: ITranslator,
320
331
  defaultPath?: string,
332
+ root?: string,
321
333
  filterDirectories?: boolean,
322
334
  handleOpenFile?: (path: string) => void
323
335
  ): Promise<FileBrowser> => {
@@ -328,6 +340,7 @@ namespace Private {
328
340
  translator,
329
341
  driveName: options.driveName,
330
342
  refreshInterval: options.refreshInterval,
343
+ root,
331
344
  filterDirectories
332
345
  });
333
346
 
@@ -0,0 +1,456 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { showErrorMessage } from '@jupyterlab/apputils';
5
+ import type { ITranslator, TranslationBundle } from '@jupyterlab/translation';
6
+ import { nullTranslator } from '@jupyterlab/translation';
7
+ import type { Message } from '@lumino/messaging';
8
+ import type { ISignal } from '@lumino/signaling';
9
+ import { Signal } from '@lumino/signaling';
10
+ import { Widget } from '@lumino/widgets';
11
+ import type {
12
+ FileBrowserModel,
13
+ TogglableHiddenFileBrowserModel
14
+ } from './model';
15
+
16
+ /**
17
+ * We cache per directory; in case the filesystem changes below us, we refresh
18
+ * every 5000 ms just in case; Note that on model update it should refresh as
19
+ * well; so this is extra precautions.
20
+ */
21
+ const SUGGESTION_CACHE_TTL_MS = 5000;
22
+
23
+ const PATHNAVIGATOR_CLASS = 'jp-PathNavigator';
24
+ const PATHNAVIGATOR_SUGGESTIONS_CLASS = 'jp-PathNavigator-suggestions';
25
+
26
+ /**
27
+ * A widget that renders a path text input with directory autocomplete.
28
+ * It owns only the input field and the suggestions dropdown.
29
+ * The trigger button and edit-mode state are managed by the parent widget.
30
+ */
31
+ export class PathNavigator extends Widget {
32
+ constructor(options: PathNavigator.IOptions) {
33
+ super({ node: document.createElement('span') });
34
+ this.addClass(PATHNAVIGATOR_CLASS);
35
+
36
+ this._model = options.model;
37
+ this._trans = (options.translator ?? nullTranslator).load('jupyterlab');
38
+
39
+ this._inputNode = document.createElement('input');
40
+ this._inputNode.type = 'text';
41
+ this._inputNode.placeholder = this._trans.__('Type a path…');
42
+
43
+ this._suggestionsNode = document.createElement('ul');
44
+ this._suggestionsNode.className = PATHNAVIGATOR_SUGGESTIONS_CLASS;
45
+ this._suggestionsNode.style.display = 'none';
46
+
47
+ this.node.appendChild(this._inputNode);
48
+ this.node.appendChild(this._suggestionsNode);
49
+
50
+ this._model.refreshed.connect(this._onModelRefreshed, this);
51
+ }
52
+
53
+ /**
54
+ * A signal emitted when the navigator closes (Escape, blur, or after
55
+ * navigation is committed). The parent widget should use this to exit
56
+ * edit mode.
57
+ */
58
+ get closed(): ISignal<this, void> {
59
+ return this._closed;
60
+ }
61
+
62
+ /**
63
+ * Dispose of the resources held by the widget.
64
+ */
65
+ dispose(): void {
66
+ if (this.isDisposed) {
67
+ return;
68
+ }
69
+ this._model.refreshed.disconnect(this._onModelRefreshed, this);
70
+ Signal.clearData(this);
71
+ super.dispose();
72
+ }
73
+
74
+ /**
75
+ * Open the path input: prefill with the model's current path, focus,
76
+ * and load suggestions.
77
+ */
78
+ open(): void {
79
+ this._isOpen = true;
80
+ const contents = this._model.manager.services.contents;
81
+ const currentPath = contents.localPath(this._model.path);
82
+ const prefill = currentPath ? currentPath + '/' : '';
83
+ this._inputNode.value = prefill;
84
+ // Defer focus so that callers (e.g. command palette) can finish their
85
+ // own focus cleanup before we grab focus. Without this, the palette's
86
+ // closing logic can steal focus back, triggering the blur→close handler
87
+ // and immediately exiting edit mode.
88
+ requestAnimationFrame(() => {
89
+ if (!this._isOpen || this.isDisposed) {
90
+ return;
91
+ }
92
+ this._inputNode.focus();
93
+ this._inputNode.setSelectionRange(prefill.length, prefill.length);
94
+ });
95
+ void this._updateSuggestions(prefill);
96
+ }
97
+
98
+ /**
99
+ * A message handler invoked on an `'after-attach'` message.
100
+ */
101
+ protected onAfterAttach(msg: Message): void {
102
+ super.onAfterAttach(msg);
103
+ this._inputNode.addEventListener('input', this);
104
+ this._inputNode.addEventListener('keydown', this);
105
+ this._inputNode.addEventListener('blur', this);
106
+ // Use mousedown (not click) so we can preventDefault() before blur fires.
107
+ this._suggestionsNode.addEventListener('mousedown', this);
108
+ }
109
+
110
+ /**
111
+ * A message handler invoked on a `'before-detach'` message.
112
+ */
113
+ protected onBeforeDetach(msg: Message): void {
114
+ this._inputNode.removeEventListener('input', this);
115
+ this._inputNode.removeEventListener('keydown', this);
116
+ this._inputNode.removeEventListener('blur', this);
117
+ this._suggestionsNode.removeEventListener('mousedown', this);
118
+ super.onBeforeDetach(msg);
119
+ }
120
+
121
+ handleEvent(event: Event): void {
122
+ switch (event.type) {
123
+ case 'input':
124
+ void this._updateSuggestions(this._inputNode.value);
125
+ break;
126
+ case 'keydown':
127
+ this._evtKeydown(event as KeyboardEvent);
128
+ break;
129
+ case 'blur':
130
+ this._close();
131
+ break;
132
+ case 'mousedown':
133
+ this._evtSuggestionMousedown(event as MouseEvent);
134
+ break;
135
+ default:
136
+ break;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Close the input and notify the parent via the `closed` signal.
142
+ */
143
+ private _close(): void {
144
+ if (!this._isOpen || this.isDisposed) {
145
+ return;
146
+ }
147
+ this._isOpen = false;
148
+ this._submittedLocalPath = null;
149
+ this._suggestionsNode.style.display = 'none';
150
+ this._closed.emit();
151
+ }
152
+
153
+ /**
154
+ * Navigate to the given path, then close the input.
155
+ */
156
+ private _commitNavigation(path: string): void {
157
+ // Strip trailing slash (except bare root), then ensure a leading slash so
158
+ // model.cd() → resolvePath() treats this as absolute rather than relative
159
+ // to the current directory.
160
+ let normalized =
161
+ path.endsWith('/') && path.length > 1 ? path.slice(0, -1) : path;
162
+ if (!normalized.startsWith('/')) {
163
+ normalized = '/' + normalized;
164
+ }
165
+ // Collapse any double slashes that may result from the above transforms.
166
+ normalized = normalized.replace(/\/\/+/g, '/');
167
+ this._submittedLocalPath = this._model.manager.services.contents.localPath(
168
+ normalized || '/'
169
+ );
170
+ // Hide suggestions immediately so the input looks committed.
171
+ this._suggestionsNode.style.display = 'none';
172
+ this._model
173
+ .cd(normalized || '/')
174
+ .then(() => this._close())
175
+ .catch(error => {
176
+ this._submittedLocalPath = null;
177
+ void showErrorMessage(this._trans.__('Open Error'), error);
178
+ this._close();
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Whether the refreshed path corresponds to the last submitted path.
184
+ */
185
+ matchesSubmittedPath(localPath: string): boolean {
186
+ return this._submittedLocalPath === localPath;
187
+ }
188
+
189
+ /**
190
+ * Fetch and display directory suggestions for the given input value.
191
+ */
192
+ private async _updateSuggestions(inputValue: string): Promise<void> {
193
+ if (!this._isOpen) {
194
+ return;
195
+ }
196
+
197
+ const lastSlash = inputValue.lastIndexOf('/');
198
+ const rawDirPart = lastSlash >= 0 ? inputValue.slice(0, lastSlash) : '';
199
+ // Normalize to a path relative to the Jupyter server root by stripping
200
+ // any leading slash.
201
+ const dirPart = rawDirPart.startsWith('/')
202
+ ? rawDirPart.slice(1)
203
+ : rawDirPart;
204
+ const searchPart =
205
+ lastSlash >= 0 ? inputValue.slice(lastSlash + 1) : inputValue;
206
+
207
+ // Re-fetch when the directory changes or the cache has gone stale.
208
+ const cacheStale =
209
+ Date.now() - this._suggestionFetchTime > SUGGESTION_CACHE_TTL_MS;
210
+ if (dirPart !== this._suggestionDirPath || cacheStale) {
211
+ const fetchId = ++this._fetchId;
212
+ try {
213
+ const contents = this._model.manager.services.contents;
214
+ const result = await contents.get(dirPart || '', { content: true });
215
+ // Discard result if a newer fetch has started or the widget was closed.
216
+ if (fetchId !== this._fetchId || !this._isOpen) {
217
+ return;
218
+ }
219
+ this._suggestionDirPath = dirPart;
220
+ this._suggestionFetchTime = Date.now();
221
+ const items =
222
+ result.type === 'directory'
223
+ ? (result.content as Array<{ name: string; type: string }>)
224
+ : [];
225
+ this._suggestions = items
226
+ .filter(item => item.type === 'directory')
227
+ .map(item => (dirPart ? `${dirPart}/${item.name}` : item.name));
228
+ } catch {
229
+ this._suggestionDirPath = dirPart;
230
+ this._suggestionFetchTime = Date.now();
231
+ this._suggestions = [];
232
+ }
233
+ }
234
+
235
+ const lower = searchPart.toLowerCase();
236
+ const showHidden =
237
+ searchPart.startsWith('.') ||
238
+ ('includeHiddenFiles' in this._model &&
239
+ (this._model as TogglableHiddenFileBrowserModel).includeHiddenFiles);
240
+ const filtered = this._suggestions.filter(s => {
241
+ const base = s.slice(s.lastIndexOf('/') + 1);
242
+ if (!showHidden && base.startsWith('.')) {
243
+ return false;
244
+ }
245
+ return base.toLowerCase().startsWith(lower);
246
+ });
247
+
248
+ this._activeSuggestionIndex = -1;
249
+ this._renderSuggestions(
250
+ filtered.slice().sort((a, b) => {
251
+ const nameA = a.slice(a.lastIndexOf('/') + 1);
252
+ const nameB = b.slice(b.lastIndexOf('/') + 1);
253
+ return nameA.localeCompare(nameB);
254
+ })
255
+ );
256
+ }
257
+
258
+ /**
259
+ * Re-render the suggestions list from the given paths.
260
+ */
261
+ private _renderSuggestions(suggestions: string[]): void {
262
+ this._suggestionsNode.replaceChildren();
263
+ this._currentFilteredSuggestions = suggestions;
264
+ if (suggestions.length === 0) {
265
+ this._suggestionsNode.style.display = 'none';
266
+ return;
267
+ }
268
+ for (const path of suggestions) {
269
+ const li = document.createElement('li');
270
+ const name = path.slice(path.lastIndexOf('/') + 1);
271
+ li.textContent = name;
272
+ li.dataset.path = path;
273
+ if (name.startsWith('.')) {
274
+ li.dataset.isDot = '';
275
+ }
276
+ this._suggestionsNode.appendChild(li);
277
+ }
278
+ this._suggestionsNode.style.display = '';
279
+ }
280
+
281
+ /**
282
+ * Handle keyboard navigation and confirmation inside the input.
283
+ */
284
+ private _evtKeydown(event: KeyboardEvent): void {
285
+ switch (event.key) {
286
+ case 'Enter':
287
+ this._commitNavigation(this._inputNode.value);
288
+ break;
289
+ case 'Escape':
290
+ this._close();
291
+ break;
292
+ case 'Tab':
293
+ // Only prevent default Tab behavior when there is a suggestion to accept.
294
+ if (this._currentFilteredSuggestions?.length) {
295
+ event.preventDefault();
296
+ this._acceptSuggestion();
297
+ }
298
+ break;
299
+ case 'ArrowDown':
300
+ event.preventDefault();
301
+ this._navigateSuggestions(1);
302
+ break;
303
+ case 'ArrowUp':
304
+ event.preventDefault();
305
+ this._navigateSuggestions(-1);
306
+ break;
307
+ // `/` should be use to commit navigation to a new folder while the user
308
+ // types. It just happens for current implementation to not need anything.
309
+ // case '/'
310
+ // break
311
+ default:
312
+ break;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Handle mousedown on a suggestion item.
318
+ *
319
+ * Using mousedown (before blur) and calling preventDefault() keeps focus on
320
+ * the input, so we can navigate without the blur handler firing first.
321
+ */
322
+ private _evtSuggestionMousedown(event: MouseEvent): void {
323
+ // Prevent the input from losing focus before we process the selection.
324
+ event.preventDefault();
325
+ let target = event.target as HTMLElement;
326
+ while (target && target !== this._suggestionsNode) {
327
+ if (target.tagName === 'LI') {
328
+ const path = target.dataset.path;
329
+ if (path) {
330
+ this._commitNavigation(path);
331
+ }
332
+ return;
333
+ }
334
+ target = target.parentElement as HTMLElement;
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Move the active suggestion up or down by `direction` steps.
340
+ */
341
+ private _navigateSuggestions(direction: 1 | -1): void {
342
+ const items = Array.from(this._suggestionsNode.children) as HTMLElement[];
343
+ if (items.length === 0) {
344
+ return;
345
+ }
346
+
347
+ if (this._activeSuggestionIndex >= 0) {
348
+ items[this._activeSuggestionIndex].classList.remove('jp-mod-active');
349
+ }
350
+
351
+ this._activeSuggestionIndex += direction;
352
+ if (this._activeSuggestionIndex < 0) {
353
+ this._activeSuggestionIndex = items.length - 1;
354
+ } else if (this._activeSuggestionIndex >= items.length) {
355
+ this._activeSuggestionIndex = 0;
356
+ }
357
+
358
+ const activeItem = items[this._activeSuggestionIndex];
359
+ activeItem.classList.add('jp-mod-active');
360
+ activeItem.scrollIntoView({ block: 'nearest' });
361
+
362
+ const path = activeItem.dataset.path;
363
+ if (path) {
364
+ // It is tempting to append / here, though not appending it allows us to
365
+ // use this key (`/`) as committing navigation in the pathnavigator and
366
+ // keep typing, and showing completion while Enter/Return validate the
367
+ // breadcrumb level. Appending / here feels awkward when used in
368
+ // practice.
369
+ // Note that (`/`) is not handled specifically in `_evtKeydown`, as the
370
+ // codepath is the same as default;
371
+ this._inputNode.value = path;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Accept the highlighted suggestion (Tab key).
377
+ * If none is highlighted, complete to the sole match or longest common prefix.
378
+ */
379
+ private _acceptSuggestion(): void {
380
+ const items = Array.from(this._suggestionsNode.children) as HTMLElement[];
381
+
382
+ if (
383
+ this._activeSuggestionIndex >= 0 &&
384
+ items[this._activeSuggestionIndex]
385
+ ) {
386
+ const path = items[this._activeSuggestionIndex].dataset.path;
387
+ if (path) {
388
+ this._inputNode.value = path + '/';
389
+ void this._updateSuggestions(this._inputNode.value);
390
+ }
391
+ } else if (this._currentFilteredSuggestions.length === 1) {
392
+ this._inputNode.value = this._currentFilteredSuggestions[0] + '/';
393
+ void this._updateSuggestions(this._inputNode.value);
394
+ } else if (this._currentFilteredSuggestions.length > 1) {
395
+ // Complete to the longest common prefix of all matching names.
396
+ const names = this._currentFilteredSuggestions.map(s =>
397
+ s.slice(s.lastIndexOf('/') + 1)
398
+ );
399
+ let prefix = names[0];
400
+ for (const name of names.slice(1)) {
401
+ let i = 0;
402
+ while (i < prefix.length && i < name.length && prefix[i] === name[i]) {
403
+ i++;
404
+ }
405
+ prefix = prefix.slice(0, i);
406
+ }
407
+ if (prefix) {
408
+ const lastSlash = this._inputNode.value.lastIndexOf('/');
409
+ const dirPart =
410
+ lastSlash >= 0 ? this._inputNode.value.slice(0, lastSlash + 1) : '';
411
+ this._inputNode.value = dirPart + prefix;
412
+ void this._updateSuggestions(this._inputNode.value);
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Handle the model's `refreshed` signal.
419
+ * Invalidate the suggestion cache so the next lookup fetches fresh data.
420
+ * If the input is currently open, proactively re-fetch suggestions.
421
+ */
422
+ private _onModelRefreshed(): void {
423
+ this._suggestionFetchTime = 0;
424
+ if (this._isOpen) {
425
+ void this._updateSuggestions(this._inputNode.value);
426
+ }
427
+ }
428
+
429
+ private _model: FileBrowserModel;
430
+ private _trans: TranslationBundle;
431
+ private _inputNode: HTMLInputElement;
432
+ private _suggestionsNode: HTMLElement;
433
+ private _closed = new Signal<this, void>(this);
434
+ private _isOpen = false;
435
+ private _suggestions: string[] = [];
436
+ private _currentFilteredSuggestions: string[] = [];
437
+ private _activeSuggestionIndex = -1;
438
+ private _suggestionDirPath = '';
439
+ private _suggestionFetchTime = 0;
440
+ private _fetchId = 0;
441
+ private _submittedLocalPath: string | null = null;
442
+ }
443
+
444
+ export namespace PathNavigator {
445
+ export interface IOptions {
446
+ /**
447
+ * The file browser model.
448
+ */
449
+ model: FileBrowserModel;
450
+
451
+ /**
452
+ * The application language translator.
453
+ */
454
+ translator?: ITranslator;
455
+ }
456
+ }