|
@@ -32,6 +32,8 @@ import {
|
|
|
|
|
|
import { JupyterFrontEnd } from './frontend';
|
|
|
|
|
|
+import { isValidFileName } from '@jupyterlab/docmanager';
|
|
|
+
|
|
|
/**
|
|
|
* The class name added to AppShell instances.
|
|
|
*/
|
|
@@ -324,14 +326,14 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
|
|
|
);
|
|
|
|
|
|
// Setup single-document-mode title bar
|
|
|
- const titleWidget = (this._titleWidget = new Widget());
|
|
|
- titleWidget.id = 'jp-title-panel-title';
|
|
|
- titleWidget.node.appendChild(document.createElement('h1'));
|
|
|
- this.add(titleWidget, 'top', { rank: 100 });
|
|
|
+ const titleWidgetHandler = (this._titleWidgetHandler = new Private.TitleWidgetHandler(
|
|
|
+ this
|
|
|
+ ));
|
|
|
+ this.add(titleWidgetHandler.titleWidget, 'top', { rank: 100 });
|
|
|
|
|
|
if (this._dockPanel.mode === 'multiple-document') {
|
|
|
this._topHandler.addWidget(this._menuHandler.panel, 100);
|
|
|
- titleWidget.hide();
|
|
|
+ titleWidgetHandler.hide();
|
|
|
} else {
|
|
|
rootLayout.insertWidget(2, this._menuHandler.panel);
|
|
|
}
|
|
@@ -471,7 +473,7 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
|
|
|
// Adjust menu and title
|
|
|
// this.add(this._menuHandler.panel, 'top', {rank: 100});
|
|
|
(this.layout as BoxLayout).insertWidget(2, this._menuHandler.panel);
|
|
|
- this._titleWidget.show();
|
|
|
+ this._titleWidgetHandler.show();
|
|
|
this._updateTitlePanelTitle();
|
|
|
|
|
|
this._modeChanged.emit(mode);
|
|
@@ -519,7 +521,7 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
|
|
|
// Adjust menu and title
|
|
|
this.add(this._menuHandler.panel, 'top', { rank: 100 });
|
|
|
// this._topHandler.addWidget(this._menuHandler.panel, 100)
|
|
|
- this._titleWidget.hide();
|
|
|
+ this._titleWidgetHandler.hide();
|
|
|
|
|
|
// Emit the mode changed signal
|
|
|
this._modeChanged.emit(mode);
|
|
@@ -695,7 +697,7 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
|
|
|
return;
|
|
|
}
|
|
|
this._layoutDebouncer.dispose();
|
|
|
- this._titleWidget.dispose();
|
|
|
+ this._titleWidgetHandler.dispose();
|
|
|
super.dispose();
|
|
|
}
|
|
|
|
|
@@ -865,9 +867,10 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
|
|
|
*/
|
|
|
private _updateTitlePanelTitle() {
|
|
|
let current = this.currentWidget;
|
|
|
- const h1 = this._titleWidget.node.children[0] as HTMLHeadElement;
|
|
|
- h1.textContent = current ? current.title.label : '';
|
|
|
- h1.title = current ? current.title.caption : '';
|
|
|
+ const inputElement = this._titleWidgetHandler.titleWidget.node
|
|
|
+ .children[0] as HTMLInputElement;
|
|
|
+ inputElement.value = current ? current.title.label : '';
|
|
|
+ inputElement.title = current ? current.title.caption : '';
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -1210,7 +1213,7 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
|
|
|
private _headerPanel: Panel;
|
|
|
private _topHandler: Private.PanelHandler;
|
|
|
private _menuHandler: Private.PanelHandler;
|
|
|
- private _titleWidget: Widget;
|
|
|
+ private _titleWidgetHandler: Private.TitleWidgetHandler;
|
|
|
private _bottomPanel: Panel;
|
|
|
private _mainOptionsCache = new Map<Widget, DocumentRegistry.IOpenOptions>();
|
|
|
private _sideOptionsCache = new Map<Widget, DocumentRegistry.IOpenOptions>();
|
|
@@ -1547,4 +1550,136 @@ namespace Private {
|
|
|
private _stackedPanel: StackedPanel;
|
|
|
private _lastCurrent: Widget | null;
|
|
|
}
|
|
|
+
|
|
|
+ export class TitleWidgetHandler {
|
|
|
+ /**
|
|
|
+ * Construct a new title widget handler.
|
|
|
+ */
|
|
|
+ constructor(shell: ILabShell) {
|
|
|
+ this._shell = shell;
|
|
|
+ const titleWidget = (this._titleWidget = new Widget());
|
|
|
+ titleWidget.id = 'jp-title-panel-title';
|
|
|
+ const titleInput = document.createElement('input');
|
|
|
+ titleInput.type = 'text';
|
|
|
+ titleInput.addEventListener('keyup', this);
|
|
|
+ titleInput.addEventListener('click', this);
|
|
|
+ titleInput.addEventListener('blur', this);
|
|
|
+
|
|
|
+ titleWidget.node.appendChild(titleInput);
|
|
|
+ }
|
|
|
+
|
|
|
+ handleEvent(event: Event): void {
|
|
|
+ switch (event.type) {
|
|
|
+ case 'keyup':
|
|
|
+ void this._evtKeyUp(event as KeyboardEvent);
|
|
|
+ break;
|
|
|
+ case 'click':
|
|
|
+ this._evtClick(event as MouseEvent);
|
|
|
+ break;
|
|
|
+ case 'blur':
|
|
|
+ this._selected = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handle `keyup` events on the handler.
|
|
|
+ */
|
|
|
+ private async _evtKeyUp(event: KeyboardEvent): Promise<void> {
|
|
|
+ if (event.key == 'Enter') {
|
|
|
+ const widget = this._shell.currentWidget;
|
|
|
+ if (widget instanceof DocumentWidget) {
|
|
|
+ const oldName = widget.context.path.split('/').pop()!;
|
|
|
+ const inputElement = this.titleWidget.node
|
|
|
+ .children[0] as HTMLInputElement;
|
|
|
+ const newName = inputElement.value;
|
|
|
+ inputElement.blur();
|
|
|
+ if (newName !== oldName && isValidFileName(newName)) {
|
|
|
+ await widget.context.rename(newName);
|
|
|
+ } else {
|
|
|
+ inputElement.value = oldName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handle `click` events on the handler.
|
|
|
+ */
|
|
|
+ private _evtClick(event: MouseEvent): void {
|
|
|
+ // only handle primary button clicks
|
|
|
+ if (event.button !== 0 || this._selected) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentWidget = this._shell.currentWidget;
|
|
|
+ const inputElement = this.titleWidget.node
|
|
|
+ .children[0] as HTMLInputElement;
|
|
|
+ if (currentWidget == null || !(currentWidget instanceof DocumentWidget)) {
|
|
|
+ inputElement.readOnly = true;
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ inputElement.removeAttribute('readOnly');
|
|
|
+ }
|
|
|
+
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+
|
|
|
+ this._selected = true;
|
|
|
+
|
|
|
+ const selectEnd = inputElement.value.indexOf('.');
|
|
|
+ if (selectEnd === -1) {
|
|
|
+ inputElement.select();
|
|
|
+ } else {
|
|
|
+ inputElement.setSelectionRange(0, selectEnd);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the input element managed by the handler.
|
|
|
+ */
|
|
|
+ get titleWidget(): Widget {
|
|
|
+ return this._titleWidget;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Dispose of the handler and the resources it holds.
|
|
|
+ */
|
|
|
+ dispose(): void {
|
|
|
+ if (this.isDisposed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this._isDisposed = true;
|
|
|
+ this._titleWidget.node.removeEventListener('keyup', this);
|
|
|
+ this._titleWidget.node.removeEventListener('click', this);
|
|
|
+ this._titleWidget.node.removeEventListener('blur', this);
|
|
|
+ this._titleWidget.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Hide the title widget.
|
|
|
+ */
|
|
|
+ hide(): void {
|
|
|
+ this._titleWidget.hide();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Show the title widget.
|
|
|
+ */
|
|
|
+ show(): void {
|
|
|
+ this._titleWidget.show();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test whether the handler has been disposed.
|
|
|
+ */
|
|
|
+ get isDisposed(): boolean {
|
|
|
+ return this._isDisposed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private _titleWidget: Widget;
|
|
|
+ private _shell: ILabShell;
|
|
|
+ private _isDisposed: boolean = false;
|
|
|
+ private _selected: boolean = false;
|
|
|
+ }
|
|
|
}
|