UNPKG

29.4 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Dialog, SessionContext, sessionContextDialogs, showDialog, showErrorMessage } from '@jupyterlab/apputils';
4import { PathExt } from '@jupyterlab/coreutils';
5import { ProviderMock } from '@jupyterlab/docprovider';
6import { RenderMimeRegistry } from '@jupyterlab/rendermime';
7import { nullTranslator } from '@jupyterlab/translation';
8import { PromiseDelegate } from '@lumino/coreutils';
9import { DisposableDelegate } from '@lumino/disposable';
10import { Signal } from '@lumino/signaling';
11import { Widget } from '@lumino/widgets';
12/**
13 * An implementation of a document context.
14 *
15 * This class is typically instantiated by the document manager.
16 */
17export class Context {
18 /**
19 * Construct a new document context.
20 */
21 constructor(options) {
22 this._path = '';
23 this._lineEnding = null;
24 this._contentsModel = null;
25 this._populatedPromise = new PromiseDelegate();
26 this._isPopulated = false;
27 this._isReady = false;
28 this._isDisposed = false;
29 this._pathChanged = new Signal(this);
30 this._fileChanged = new Signal(this);
31 this._saveState = new Signal(this);
32 this._disposed = new Signal(this);
33 this._lastModifiedCheckMargin = 500;
34 this._timeConflictModalIsOpen = false;
35 const manager = (this._manager = options.manager);
36 this.translator = options.translator || nullTranslator;
37 this._trans = this.translator.load('jupyterlab');
38 this._factory = options.factory;
39 this._dialogs = options.sessionDialogs || sessionContextDialogs;
40 this._opener = options.opener || Private.noOp;
41 this._path = this._manager.contents.normalize(options.path);
42 this._lastModifiedCheckMargin = options.lastModifiedCheckMargin || 500;
43 const localPath = this._manager.contents.localPath(this._path);
44 const lang = this._factory.preferredLanguage(PathExt.basename(localPath));
45 const dbFactory = options.modelDBFactory;
46 if (dbFactory) {
47 const localPath = manager.contents.localPath(this._path);
48 this._modelDB = dbFactory.createNew(localPath);
49 this._model = this._factory.createNew(lang, this._modelDB, false);
50 }
51 else {
52 this._model = this._factory.createNew(lang, undefined, false);
53 }
54 const ymodel = this._model.sharedModel; // translate to the concrete Yjs implementation
55 const ydoc = ymodel.ydoc;
56 this._ydoc = ydoc;
57 this._ycontext = ydoc.getMap('context');
58 const docProviderFactory = options.docProviderFactory;
59 this._provider = docProviderFactory
60 ? docProviderFactory({
61 path: this._path,
62 contentType: this._factory.contentType,
63 ymodel
64 })
65 : new ProviderMock();
66 this._readyPromise = manager.ready.then(() => {
67 return this._populatedPromise.promise;
68 });
69 const ext = PathExt.extname(this._path);
70 this.sessionContext = new SessionContext({
71 sessionManager: manager.sessions,
72 specsManager: manager.kernelspecs,
73 path: this._path,
74 type: ext === '.ipynb' ? 'notebook' : 'file',
75 name: PathExt.basename(localPath),
76 kernelPreference: options.kernelPreference || { shouldStart: false },
77 setBusy: options.setBusy
78 });
79 this.sessionContext.propertyChanged.connect(this._onSessionChanged, this);
80 manager.contents.fileChanged.connect(this._onFileChanged, this);
81 const urlResolver = (this.urlResolver = new RenderMimeRegistry.UrlResolver({
82 path: this._path,
83 contents: manager.contents
84 }));
85 this._ycontext.set('path', this._path);
86 this._ycontext.observe(event => {
87 var _a;
88 const pathChanged = event.changes.keys.get('path');
89 if (pathChanged) {
90 const newPath = this._ycontext.get('path');
91 if (newPath && newPath !== pathChanged.oldValue) {
92 urlResolver.path = newPath;
93 this._path = newPath;
94 this._provider.setPath(newPath);
95 this._pathChanged.emit(this.path);
96 (_a = this.sessionContext.session) === null || _a === void 0 ? void 0 : _a.setPath(newPath);
97 }
98 }
99 });
100 }
101 /**
102 * A signal emitted when the path changes.
103 */
104 get pathChanged() {
105 return this._pathChanged;
106 }
107 /**
108 * A signal emitted when the model is saved or reverted.
109 */
110 get fileChanged() {
111 return this._fileChanged;
112 }
113 /**
114 * A signal emitted on the start and end of a saving operation.
115 */
116 get saveState() {
117 return this._saveState;
118 }
119 /**
120 * A signal emitted when the context is disposed.
121 */
122 get disposed() {
123 return this._disposed;
124 }
125 /**
126 * Configurable margin used to detect document modification conflicts, in milliseconds
127 */
128 get lastModifiedCheckMargin() {
129 return this._lastModifiedCheckMargin;
130 }
131 set lastModifiedCheckMargin(value) {
132 this._lastModifiedCheckMargin = value;
133 }
134 /**
135 * Get the model associated with the document.
136 */
137 get model() {
138 return this._model;
139 }
140 /**
141 * The current path associated with the document.
142 */
143 get path() {
144 return this._path;
145 }
146 /**
147 * The current local path associated with the document.
148 * If the document is in the default notebook file browser,
149 * this is the same as the path.
150 */
151 get localPath() {
152 return this._manager.contents.localPath(this._path);
153 }
154 /**
155 * The current contents model associated with the document.
156 *
157 * #### Notes
158 * The contents model will be null until the context is populated.
159 * It will have an empty `contents` field.
160 */
161 get contentsModel() {
162 return this._contentsModel;
163 }
164 /**
165 * Get the model factory name.
166 *
167 * #### Notes
168 * This is not part of the `IContext` API.
169 */
170 get factoryName() {
171 return this.isDisposed ? '' : this._factory.name;
172 }
173 /**
174 * Test whether the context is disposed.
175 */
176 get isDisposed() {
177 return this._isDisposed;
178 }
179 /**
180 * Dispose of the resources held by the context.
181 */
182 dispose() {
183 if (this.isDisposed) {
184 return;
185 }
186 this._isDisposed = true;
187 this.sessionContext.dispose();
188 if (this._modelDB) {
189 this._modelDB.dispose();
190 }
191 this._model.dispose();
192 this._provider.destroy();
193 this._model.sharedModel.dispose();
194 this._ydoc.destroy();
195 this._disposed.emit(void 0);
196 Signal.clearData(this);
197 }
198 /**
199 * Whether the context is ready.
200 */
201 get isReady() {
202 return this._isReady;
203 }
204 /**
205 * A promise that is fulfilled when the context is ready.
206 */
207 get ready() {
208 return this._readyPromise;
209 }
210 /**
211 * Initialize the context.
212 *
213 * @param isNew - Whether it is a new file.
214 *
215 * @returns a promise that resolves upon initialization.
216 */
217 async initialize(isNew) {
218 const lock = await this._provider.acquireLock();
219 const contentIsInitialized = await this._provider.requestInitialContent();
220 let promise;
221 if (isNew || contentIsInitialized) {
222 promise = this._save();
223 }
224 else {
225 promise = this._revert();
226 }
227 // make sure that the lock is released after the above operations are completed.
228 const finally_ = () => {
229 this._provider.releaseLock(lock);
230 };
231 // if save/revert completed successfully, we set the initialized content in the rtc server.
232 promise
233 .then(() => {
234 this._provider.putInitializedState();
235 this._model.initialize();
236 })
237 .then(finally_, finally_);
238 return promise;
239 }
240 /**
241 * Rename the document.
242 *
243 * @param newName - the new name for the document.
244 */
245 rename(newName) {
246 return this.ready.then(() => {
247 return this._manager.ready.then(() => {
248 return this._rename(newName);
249 });
250 });
251 }
252 /**
253 * Save the document contents to disk.
254 */
255 async save() {
256 const [lock] = await Promise.all([
257 this._provider.acquireLock(),
258 this.ready
259 ]);
260 let promise;
261 promise = this._save();
262 // if save completed successfully, we set the initialized content in the rtc server.
263 promise = promise.then(() => {
264 this._provider.putInitializedState();
265 });
266 const finally_ = () => {
267 this._provider.releaseLock(lock);
268 };
269 promise.then(finally_, finally_);
270 return await promise;
271 }
272 /**
273 * Save the document to a different path chosen by the user.
274 */
275 saveAs() {
276 return this.ready
277 .then(() => {
278 return Private.getSavePath(this._path);
279 })
280 .then(newPath => {
281 if (this.isDisposed || !newPath) {
282 return;
283 }
284 if (newPath === this._path) {
285 return this.save();
286 }
287 // Make sure the path does not exist.
288 return this._manager.ready
289 .then(() => {
290 return this._manager.contents.get(newPath);
291 })
292 .then(() => {
293 return this._maybeOverWrite(newPath);
294 })
295 .catch(err => {
296 if (!err.response || err.response.status !== 404) {
297 throw err;
298 }
299 return this._finishSaveAs(newPath);
300 });
301 });
302 }
303 /**
304 * Download a file.
305 *
306 * @param path - The path of the file to be downloaded.
307 *
308 * @returns A promise which resolves when the file has begun
309 * downloading.
310 */
311 async download() {
312 const url = await this._manager.contents.getDownloadUrl(this._path);
313 const element = document.createElement('a');
314 element.href = url;
315 element.download = '';
316 document.body.appendChild(element);
317 element.click();
318 document.body.removeChild(element);
319 return void 0;
320 }
321 /**
322 * Revert the document contents to disk contents.
323 */
324 async revert() {
325 const [lock] = await Promise.all([
326 this._provider.acquireLock(),
327 this.ready
328 ]);
329 const promise = this._revert();
330 const finally_ = () => {
331 this._provider.releaseLock(lock);
332 };
333 promise.then(finally_, finally_);
334 return await promise;
335 }
336 /**
337 * Create a checkpoint for the file.
338 */
339 createCheckpoint() {
340 const contents = this._manager.contents;
341 return this._manager.ready.then(() => {
342 return contents.createCheckpoint(this._path);
343 });
344 }
345 /**
346 * Delete a checkpoint for the file.
347 */
348 deleteCheckpoint(checkpointId) {
349 const contents = this._manager.contents;
350 return this._manager.ready.then(() => {
351 return contents.deleteCheckpoint(this._path, checkpointId);
352 });
353 }
354 /**
355 * Restore the file to a known checkpoint state.
356 */
357 restoreCheckpoint(checkpointId) {
358 const contents = this._manager.contents;
359 const path = this._path;
360 return this._manager.ready.then(() => {
361 if (checkpointId) {
362 return contents.restoreCheckpoint(path, checkpointId);
363 }
364 return this.listCheckpoints().then(checkpoints => {
365 if (this.isDisposed || !checkpoints.length) {
366 return;
367 }
368 checkpointId = checkpoints[checkpoints.length - 1].id;
369 return contents.restoreCheckpoint(path, checkpointId);
370 });
371 });
372 }
373 /**
374 * List available checkpoints for a file.
375 */
376 listCheckpoints() {
377 const contents = this._manager.contents;
378 return this._manager.ready.then(() => {
379 return contents.listCheckpoints(this._path);
380 });
381 }
382 /**
383 * Add a sibling widget to the document manager.
384 *
385 * @param widget - The widget to add to the document manager.
386 *
387 * @param options - The desired options for adding the sibling.
388 *
389 * @returns A disposable used to remove the sibling if desired.
390 *
391 * #### Notes
392 * It is assumed that the widget has the same model and context
393 * as the original widget.
394 */
395 addSibling(widget, options = {}) {
396 const opener = this._opener;
397 if (opener) {
398 opener(widget, options);
399 }
400 return new DisposableDelegate(() => {
401 widget.close();
402 });
403 }
404 /**
405 * Handle a change on the contents manager.
406 */
407 _onFileChanged(sender, change) {
408 var _a, _b, _c;
409 if (change.type !== 'rename') {
410 return;
411 }
412 let oldPath = change.oldValue && change.oldValue.path;
413 let newPath = change.newValue && change.newValue.path;
414 if (newPath && this._path.indexOf(oldPath || '') === 0) {
415 let changeModel = change.newValue;
416 // When folder name changed, `oldPath` is `foo`, `newPath` is `bar` and `this._path` is `foo/test`,
417 // we should update `foo/test` to `bar/test` as well
418 if (oldPath !== this._path) {
419 newPath = this._path.replace(new RegExp(`^${oldPath}/`), `${newPath}/`);
420 oldPath = this._path;
421 // Update client file model from folder change
422 changeModel = {
423 last_modified: (_a = change.newValue) === null || _a === void 0 ? void 0 : _a.created,
424 path: newPath
425 };
426 }
427 this._path = newPath;
428 void ((_b = this.sessionContext.session) === null || _b === void 0 ? void 0 : _b.setPath(newPath));
429 const updateModel = Object.assign(Object.assign({}, this._contentsModel), changeModel);
430 const localPath = this._manager.contents.localPath(newPath);
431 void ((_c = this.sessionContext.session) === null || _c === void 0 ? void 0 : _c.setName(PathExt.basename(localPath)));
432 this._updateContentsModel(updateModel);
433 this._ycontext.set('path', this._path);
434 }
435 }
436 /**
437 * Handle a change to a session property.
438 */
439 _onSessionChanged(sender, type) {
440 if (type !== 'path') {
441 return;
442 }
443 const path = this.sessionContext.session.path;
444 if (path !== this._path) {
445 this._path = path;
446 this._ycontext.set('path', this._path);
447 }
448 }
449 /**
450 * Update our contents model, without the content.
451 */
452 _updateContentsModel(model) {
453 const newModel = {
454 path: model.path,
455 name: model.name,
456 type: model.type,
457 content: undefined,
458 writable: model.writable,
459 created: model.created,
460 last_modified: model.last_modified,
461 mimetype: model.mimetype,
462 format: model.format
463 };
464 const mod = this._contentsModel ? this._contentsModel.last_modified : null;
465 this._contentsModel = newModel;
466 this._ycontext.set('last_modified', newModel.last_modified);
467 if (!mod || newModel.last_modified !== mod) {
468 this._fileChanged.emit(newModel);
469 }
470 }
471 /**
472 * Handle an initial population.
473 */
474 _populate() {
475 this._isPopulated = true;
476 this._isReady = true;
477 this._populatedPromise.resolve(void 0);
478 // Add a checkpoint if none exists and the file is writable.
479 return this._maybeCheckpoint(false).then(() => {
480 if (this.isDisposed) {
481 return;
482 }
483 // Update the kernel preference.
484 const name = this._model.defaultKernelName ||
485 this.sessionContext.kernelPreference.name;
486 this.sessionContext.kernelPreference = Object.assign(Object.assign({}, this.sessionContext.kernelPreference), { name, language: this._model.defaultKernelLanguage });
487 // Note: we don't wait on the session to initialize
488 // so that the user can be shown the content before
489 // any kernel has started.
490 void this.sessionContext.initialize().then(shouldSelect => {
491 if (shouldSelect) {
492 void this._dialogs.selectKernel(this.sessionContext, this.translator);
493 }
494 });
495 });
496 }
497 /**
498 * Rename the document.
499 *
500 * @param newName - the new name for the document.
501 */
502 async _rename(newName) {
503 var _a, _b;
504 const splitPath = this.path.split('/');
505 splitPath[splitPath.length - 1] = newName;
506 const newPath = splitPath.join('/');
507 await this._manager.contents.rename(this.path, newPath);
508 await ((_a = this.sessionContext.session) === null || _a === void 0 ? void 0 : _a.setPath(newPath));
509 await ((_b = this.sessionContext.session) === null || _b === void 0 ? void 0 : _b.setName(newName));
510 this._path = newPath;
511 this._ycontext.set('path', this._path);
512 }
513 /**
514 * Save the document contents to disk.
515 */
516 async _save() {
517 this._saveState.emit('started');
518 const model = this._model;
519 let content;
520 if (this._factory.fileFormat === 'json') {
521 content = model.toJSON();
522 }
523 else {
524 content = model.toString();
525 if (this._lineEnding) {
526 content = content.replace(/\n/g, this._lineEnding);
527 }
528 }
529 const options = {
530 type: this._factory.contentType,
531 format: this._factory.fileFormat,
532 content
533 };
534 try {
535 let value;
536 await this._manager.ready;
537 if (!model.modelDB.isCollaborative) {
538 value = await this._maybeSave(options);
539 }
540 else {
541 value = await this._manager.contents.save(this._path, options);
542 }
543 if (this.isDisposed) {
544 return;
545 }
546 model.dirty = false;
547 this._updateContentsModel(value);
548 if (!this._isPopulated) {
549 await this._populate();
550 }
551 // Emit completion.
552 this._saveState.emit('completed');
553 }
554 catch (err) {
555 // If the save has been canceled by the user,
556 // throw the error so that whoever called save()
557 // can decide what to do.
558 if (err.message === 'Cancel' ||
559 err.message === 'Modal is already displayed') {
560 throw err;
561 }
562 // Otherwise show an error message and throw the error.
563 const localPath = this._manager.contents.localPath(this._path);
564 const name = PathExt.basename(localPath);
565 void this._handleError(err, this._trans.__('File Save Error for %1', name));
566 // Emit failure.
567 this._saveState.emit('failed');
568 throw err;
569 }
570 }
571 /**
572 * Revert the document contents to disk contents.
573 *
574 * @param initializeModel - call the model's initialization function after
575 * deserializing the content.
576 */
577 _revert(initializeModel = false) {
578 const opts = Object.assign({ type: this._factory.contentType, content: this._factory.fileFormat !== null }, (this._factory.fileFormat !== null
579 ? { format: this._factory.fileFormat }
580 : {}));
581 const path = this._path;
582 const model = this._model;
583 return this._manager.ready
584 .then(() => {
585 return this._manager.contents.get(path, opts);
586 })
587 .then(contents => {
588 if (this.isDisposed) {
589 return;
590 }
591 if (contents.format === 'json') {
592 model.fromJSON(contents.content);
593 if (initializeModel) {
594 model.initialize();
595 }
596 }
597 else {
598 let content = contents.content;
599 // Convert line endings if necessary, marking the file
600 // as dirty.
601 if (content.indexOf('\r\n') !== -1) {
602 this._lineEnding = '\r\n';
603 content = content.replace(/\r\n/g, '\n');
604 }
605 else if (content.indexOf('\r') !== -1) {
606 this._lineEnding = '\r';
607 content = content.replace(/\r/g, '\n');
608 }
609 else {
610 this._lineEnding = null;
611 }
612 model.fromString(content);
613 if (initializeModel) {
614 model.initialize();
615 }
616 }
617 this._updateContentsModel(contents);
618 model.dirty = false;
619 if (!this._isPopulated) {
620 return this._populate();
621 }
622 })
623 .catch(async (err) => {
624 const localPath = this._manager.contents.localPath(this._path);
625 const name = PathExt.basename(localPath);
626 void this._handleError(err, this._trans.__('File Load Error for %1', name));
627 throw err;
628 });
629 }
630 /**
631 * Save a file, dealing with conflicts.
632 */
633 _maybeSave(options) {
634 const path = this._path;
635 // Make sure the file has not changed on disk.
636 const promise = this._manager.contents.get(path, { content: false });
637 return promise.then(model => {
638 var _a;
639 if (this.isDisposed) {
640 return Promise.reject(new Error('Disposed'));
641 }
642 // We want to check last_modified (disk) > last_modified (client)
643 // (our last save)
644 // In some cases the filesystem reports an inconsistent time, so we allow buffer when comparing.
645 const lastModifiedCheckMargin = this._lastModifiedCheckMargin;
646 const ycontextModified = this._ycontext.get('last_modified');
647 // prefer using the timestamp from ycontext because it is more up to date
648 const modified = ycontextModified || ((_a = this.contentsModel) === null || _a === void 0 ? void 0 : _a.last_modified);
649 const tClient = modified ? new Date(modified) : new Date();
650 const tDisk = new Date(model.last_modified);
651 if (modified &&
652 tDisk.getTime() - tClient.getTime() > lastModifiedCheckMargin) {
653 return this._timeConflict(tClient, model, options);
654 }
655 return this._manager.contents.save(path, options);
656 }, err => {
657 if (err.response && err.response.status === 404) {
658 return this._manager.contents.save(path, options);
659 }
660 throw err;
661 });
662 }
663 /**
664 * Handle a save/load error with a dialog.
665 */
666 async _handleError(err, title) {
667 await showErrorMessage(title, err);
668 return;
669 }
670 /**
671 * Add a checkpoint the file is writable.
672 */
673 _maybeCheckpoint(force) {
674 let writable = this._contentsModel && this._contentsModel.writable;
675 let promise = Promise.resolve(void 0);
676 if (!writable) {
677 return promise;
678 }
679 if (force) {
680 promise = this.createCheckpoint().then( /* no-op */);
681 }
682 else {
683 promise = this.listCheckpoints().then(checkpoints => {
684 writable = this._contentsModel && this._contentsModel.writable;
685 if (!this.isDisposed && !checkpoints.length && writable) {
686 return this.createCheckpoint().then( /* no-op */);
687 }
688 });
689 }
690 return promise.catch(err => {
691 // Handle a read-only folder.
692 if (!err.response || err.response.status !== 403) {
693 throw err;
694 }
695 });
696 }
697 /**
698 * Handle a time conflict.
699 */
700 _timeConflict(tClient, model, options) {
701 const tDisk = new Date(model.last_modified);
702 console.warn(`Last saving performed ${tClient} ` +
703 `while the current file seems to have been saved ` +
704 `${tDisk}`);
705 if (this._timeConflictModalIsOpen) {
706 return Promise.reject(new Error('Modal is already displayed'));
707 }
708 const body = this._trans.__(`"%1" has changed on disk since the last time it was opened or saved.
709Do you want to overwrite the file on disk with the version open here,
710or load the version on disk (revert)?`, this.path);
711 const revertBtn = Dialog.okButton({ label: this._trans.__('Revert') });
712 const overwriteBtn = Dialog.warnButton({
713 label: this._trans.__('Overwrite')
714 });
715 this._timeConflictModalIsOpen = true;
716 return showDialog({
717 title: this._trans.__('File Changed'),
718 body,
719 buttons: [Dialog.cancelButton(), revertBtn, overwriteBtn]
720 }).then(result => {
721 this._timeConflictModalIsOpen = false;
722 if (this.isDisposed) {
723 return Promise.reject(new Error('Disposed'));
724 }
725 if (result.button.label === this._trans.__('Overwrite')) {
726 return this._manager.contents.save(this._path, options);
727 }
728 // FIXME-TRANS: Why compare to label?
729 if (result.button.label === this._trans.__('Revert')) {
730 return this.revert().then(() => {
731 return model;
732 });
733 }
734 return Promise.reject(new Error('Cancel')); // Otherwise cancel the save.
735 });
736 }
737 /**
738 * Handle a time conflict.
739 */
740 _maybeOverWrite(path) {
741 const body = this._trans.__('"%1" already exists. Do you want to replace it?', path);
742 const overwriteBtn = Dialog.warnButton({
743 label: this._trans.__('Overwrite')
744 });
745 return showDialog({
746 title: this._trans.__('File Overwrite?'),
747 body,
748 buttons: [Dialog.cancelButton(), overwriteBtn]
749 }).then(result => {
750 if (this.isDisposed) {
751 return Promise.reject(new Error('Disposed'));
752 }
753 // FIXME-TRANS: Why compare to label?
754 if (result.button.label === this._trans.__('Overwrite')) {
755 return this._manager.contents.delete(path).then(() => {
756 return this._finishSaveAs(path);
757 });
758 }
759 });
760 }
761 /**
762 * Finish a saveAs operation given a new path.
763 */
764 async _finishSaveAs(newPath) {
765 var _a, _b;
766 this._path = newPath;
767 await ((_a = this.sessionContext.session) === null || _a === void 0 ? void 0 : _a.setPath(newPath));
768 await ((_b = this.sessionContext.session) === null || _b === void 0 ? void 0 : _b.setName(newPath.split('/').pop()));
769 await this.save();
770 this._ycontext.set('path', this._path);
771 await this._maybeCheckpoint(true);
772 }
773}
774/**
775 * A namespace for private data.
776 */
777var Private;
778(function (Private) {
779 /**
780 * Get a new file path from the user.
781 */
782 function getSavePath(path, translator) {
783 translator = translator || nullTranslator;
784 const trans = translator.load('jupyterlab');
785 const saveBtn = Dialog.okButton({ label: trans.__('Save') });
786 return showDialog({
787 title: trans.__('Save File As..'),
788 body: new SaveWidget(path),
789 buttons: [Dialog.cancelButton(), saveBtn]
790 }).then(result => {
791 var _a;
792 // FIXME-TRANS: Why use the label?
793 if (result.button.label === trans.__('Save')) {
794 return (_a = result.value) !== null && _a !== void 0 ? _a : undefined;
795 }
796 return;
797 });
798 }
799 Private.getSavePath = getSavePath;
800 /**
801 * A no-op function.
802 */
803 function noOp() {
804 /* no-op */
805 }
806 Private.noOp = noOp;
807 /*
808 * A widget that gets a file path from a user.
809 */
810 class SaveWidget extends Widget {
811 /**
812 * Construct a new save widget.
813 */
814 constructor(path) {
815 super({ node: createSaveNode(path) });
816 }
817 /**
818 * Get the value for the widget.
819 */
820 getValue() {
821 return this.node.value;
822 }
823 }
824 /**
825 * Create the node for a save widget.
826 */
827 function createSaveNode(path) {
828 const input = document.createElement('input');
829 input.value = path;
830 return input;
831 }
832})(Private || (Private = {}));
833//# sourceMappingURL=context.js.map
\No newline at end of file