You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by zj...@apache.org on 2020/01/12 06:38:19 UTC
[zeppelin] 02/16: [ZEPPELIN-4321] Support shortcuts for the
paragraphs
This is an automated email from the ASF dual-hosted git repository.
zjffdu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
commit d3979c2709d95ff17a10f3f3bbca91150f645a78
Author: Hsuan Lee <hs...@gmail.com>
AuthorDate: Fri Nov 8 17:20:59 2019 +0800
[ZEPPELIN-4321] Support shortcuts for the paragraphs
### What is this PR for?
We are using Angular Latest to refactor Zeppelin's front-end. When implementing the features of the shortcuts, we found that the current shortcuts is somewhat complicated and did not distinguish the Command/Ctrl key between Mac and Windows.
So we compared the following applications:
- [Jupyter](https://github.com/jupyter/jupyter)
- [JupyterLab](https://github.com/jupyterlab/jupyterlab)
- [Google Colaboratory](https://colab.research.google.com/)
They can distinguish between edit-mode and command-mode, which will simplify the shortcuts complexity. Meanwhile, we use the [Monaco editor](https://github.com/microsoft/monaco-editor) in the refactor version, it is the core library of [VSCode](https://github.com/microsoft/vscode). We think it is a good choice to use its shortcuts design.
Therefore, for the above reasons, we proposed to redesign Zeppelin's shortcuts according to the table following.
| Actions | Mode | Mac | Windows / Linux | Old(Mac) |
|-------------------------------------------------------|---------|------------------|-----------------------|------------------|
| Command mode | Edit | ESC | ESC | - |
| Edit mode | Command | Enter | Enter | - |
| Run | - | ⇧ + Enter | ⇧ + Enter | ⇧ + Enter |
| Run all below | - | ⇧ + ⌘ + Enter | ⇧ + Ctrl + Enter | ⇧ + Ctrl + Enter |
| Run all above | - | ⇧ + ⌥ + Enter | ⇧ + Alt + Enter | ⇧ + Ctrl + Enter |
| Cancel | - | ⇧ + ⌘ + C | ⇧ + Ctrl + C | ⌥ + Ctrl + C |
| Switch all line number | - | ⇧ + ⌘ + L | ⇧ + Ctrl + L | - |
| Show / Hide all output | - | ⇧ + ⌘ + O | ⇧ + Ctrl + O | - |
| Show / Hide all title | - | ⇧ + ⌘ + T | ⇧ + Ctrl + T | - |
| Clear output | - | ⌘ + ⌥ + L | Ctrl + Alt + L | ⌥ + Ctrl + L |
| Enable/Disable | - | ⌘ + ⌥ + R | Ctrl + Alt + R | ⌥ + Ctrl + R |
| Reduce width | - | ⌘ + ⌥ + + | Ctrl + Alt + - | ⇧ + Ctrl + - |
| Increase width | - | ⌘ + ⌥ + - | Ctrl + Alt + + | ⇧ + Ctrl + + |
| Delete | Command | ⇧ + Del | ⇧ + Del | ⌥ + Ctrl + D |
| Move to up | Command | ⌘ + K / Up | Ctrl + K / Up | ⌥ + Ctrl + K |
| Move to down | Command | ⌘ + J / Down | Ctrl + J / Down | ⌥ + Ctrl + J |
| Select above | Command | K / Up | K / Up | - |
| Select below | Command | J / Down | J / Down | - |
| Switch line number | Command | L | L | ⌥ + Ctrl + M |
| Show / Hide title | Command | T | T | ⌥ + Ctrl + T |
| Show / Hide output | Command | O | O | ⌥ + Ctrl + O |
| Show / Hide editor | Command | E | E | ⌥ + Ctrl + E |
| Insert above | Command | A | A | ⌥ + Ctrl + A |
| Insert below | Command | B | B | ⌥ + Ctrl + B |
| Search | Edit | ⌘ + F | Ctrl + F | ⌥ + Ctrl + F |
| Increase Indent | Edit | Tab | Tab | - |
| Decrease Indent | Edit | ⇧ + Tab | ⇧ + Tab | - |
| Comment Out / In | Edit | ⌘ + / | Ctrl + / | Ctrl + / |
| Undo | Edit | ⌘ + Z | Ctrl + Z | Ctrl + Z |
| Redo | Edit | ⇧ + ⌘ + Z | Ctrl + Y | - |
| Increase font size | Edit | ⌘ + . | Ctrl + . | - |
| Decrease font size | Edit | ⌘ + , | Ctrl + , | - |
| Decrease Indent | Edit | ⌘ + [ | Ctrl + [ | - |
| Increase Indent | Edit | ⌘ + ] | Ctrl + ] | - |
| Move the line down | Edit | ⌥ + Down | Alt + Down | ⌥ + Down |
| Move the line up | Edit | ⌥ + Up | Alt + Up | ⌥ + Down |
| Replace | Edit | ⌘ + ⌥ + F | Ctrl + F | - |
| Select all | Edit | ⌘ + A | Ctrl + A | ⌘ + A |
| Select downward | Edit | ⇧ + Down | ⇧ + Down | ⇧ + Down |
| Select right | Edit | ⇧ + Right | ⇧ + Right | ⇧ + Right |
| Select left | Edit | ⇧ + Left | ⇧ + Left | ⇧ + Left |
| Select upward | Edit | ⇧ + Up | ⇧ + Up | ⇧ + Up |
| Select to the end | Edit | ⌘ + ⇧ + Right | Alt + ⇧ + Right | ⌘ + ⇧ + Right |
| Select to the start | Edit | ⌘ + ⇧ + Left | Alt + ⇧ + Left | ⌘ + ⇧ + Left |
| Align text right | Edit | ⌥ + Right | Ctrl + ⇧ + Right | ⌥ + Right |
| Align text left | Edit | ⌥ + Left | Ctrl + ⇧ + Left | ⌥ + Left |
| Add multi-cursor above | Edit | ⌘ + ⌥ + Up | Ctrl + Alt + Up | - |
| Add multi-cursor below | Edit | ⌘ + ⌥ + Down | Ctrl + Alt + Down | - |
| Move multi-cursor from current line to the line above | Edit | ⌘ + ⌥ + ⇧ + Up | Ctrl + Alt + ⇧ + Up | - |
| Move multi-cursor from current line to the line below | Edit | ⌘ + ⌥ + ⇧ + Down | Ctrl + Alt + ⇧ + Down | - |
### What type of PR is it?
[Feature]
### Todos
* [ ] - Task
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-4402
https://issues.apache.org/jira/browse/ZEPPELIN-4321
### How should this be tested?
* First time? Setup Travis CI as described on https://zeppelin.apache.org/contribution/contributions.html#continuous-integration
* Strongly recommended: add automated unit tests for any new or changed behavior
* Outline any manual steps to test the PR here.
### Screenshots (if appropriate)
### Questions:
* Does the licenses files need update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Author: Hsuan Lee <hs...@gmail.com>
Closes #3517 from hsuanxyz/feat/hot-key and squashes the following commits:
0dec67601 [Hsuan Lee] feat(paragraph): support shortcut
---
zeppelin-web-angular/package-lock.json | 76 +++++--
zeppelin-web-angular/package.json | 6 +-
.../workspace/notebook/notebook.component.html | 6 +-
.../pages/workspace/notebook/notebook.component.ts | 16 +-
.../paragraph/code-editor/code-editor.component.ts | 13 ++
.../paragraph/control/control.component.ts | 24 +--
.../notebook/paragraph/paragraph.component.html | 9 +-
.../notebook/paragraph/paragraph.component.less | 5 +
.../notebook/paragraph/paragraph.component.ts | 227 ++++++++++++++++++++-
.../notebook/paragraph/result/result.component.ts | 3 +
.../src/app/services/public-api.ts | 1 +
.../src/app/services/shortcut.service.ts | 109 ++++++++++
12 files changed, 436 insertions(+), 59 deletions(-)
diff --git a/zeppelin-web-angular/package-lock.json b/zeppelin-web-angular/package-lock.json
index 46c0a07..85ad2a6 100644
--- a/zeppelin-web-angular/package-lock.json
+++ b/zeppelin-web-angular/package-lock.json
@@ -6852,23 +6852,54 @@
}
},
"husky": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/husky/-/husky-2.7.0.tgz",
- "integrity": "sha512-LIi8zzT6PyFpcYKdvWRCn/8X+6SuG2TgYYMrM6ckEYhlp44UcEduVymZGIZNLiwOUjrEud+78w/AsAiqJA/kRg==",
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz",
+ "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==",
"dev": true,
"requires": {
- "cosmiconfig": "^5.2.0",
+ "chalk": "^2.4.2",
+ "ci-info": "^2.0.0",
+ "cosmiconfig": "^5.2.1",
"execa": "^1.0.0",
- "find-up": "^3.0.0",
"get-stdin": "^7.0.0",
- "is-ci": "^2.0.0",
- "pkg-dir": "^4.1.0",
- "please-upgrade-node": "^3.1.1",
- "read-pkg": "^5.1.1",
+ "opencollective-postinstall": "^2.0.2",
+ "pkg-dir": "^4.2.0",
+ "please-upgrade-node": "^3.2.0",
+ "read-pkg": "^5.2.0",
"run-node": "^1.0.0",
"slash": "^3.0.0"
},
"dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -6900,18 +6931,6 @@
"dev": true,
"requires": {
"find-up": "^4.0.0"
- },
- "dependencies": {
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- }
}
},
"slash": {
@@ -6919,6 +6938,15 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
}
}
},
@@ -10409,6 +10437,12 @@
"is-wsl": "^1.1.0"
}
},
+ "opencollective-postinstall": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
+ "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
+ "dev": true
+ },
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
diff --git a/zeppelin-web-angular/package.json b/zeppelin-web-angular/package.json
index ae96fe7..63b6be1 100644
--- a/zeppelin-web-angular/package.json
+++ b/zeppelin-web-angular/package.json
@@ -44,8 +44,6 @@
"zone.js": "~0.9.1"
},
"devDependencies": {
- "monaco-editor-webpack-plugin": "^1.7.0",
- "ngx-build-plus": "^8.1.5",
"@angular-devkit/build-angular": "^0.803.9",
"@angular-devkit/build-ng-packagr": "~0.803.6",
"@angular/cli": "~8.3.9",
@@ -61,7 +59,7 @@
"codelyzer": "^5.0.0",
"dotenv": "^8.0.0",
"https-proxy-agent": "^2.2.1",
- "husky": "^2.2.0",
+ "husky": "^3.0.9",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
@@ -70,7 +68,9 @@
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"lint-staged": "^8.1.6",
+ "monaco-editor-webpack-plugin": "^1.7.0",
"ng-packagr": "^5.4.0",
+ "ngx-build-plus": "^8.1.5",
"prettier": "^1.17.0",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
index 3bad5ee..c447a59 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
@@ -36,8 +36,10 @@
<div class="paragraph-area">
<div class="paragraph-inner" nz-row>
<zeppelin-notebook-paragraph nz-col
- *ngFor="let p of note.paragraphs;let first = first; let last = last;"
+ *ngFor="let p of note.paragraphs;let first = first; let last = last; index as i"
[nzSpan]="p.config.colWidth * 2"
+ [select]="p.id === selectId"
+ [index]="i"
[paragraph]="p"
[note]="note"
[looknfeel]="note.config.looknfeel"
@@ -47,6 +49,8 @@
[revisionView]="revisionView"
[first]="first"
[last]="last"
+ (selectAtIndex)="onSelectAtIndex($event)"
+ (selected)="onParagraphSelect($event)"
(triggerSaveParagraph)="saveParagraph($event)"
(saveNoteTimer)="startSaveTimer()"></zeppelin-notebook-paragraph>
</div>
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
index a894971..97479a7 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
@@ -21,7 +21,7 @@ import {
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { isNil } from 'lodash';
-import { Subject } from 'rxjs';
+import { Subject} from 'rxjs';
import { distinctUntilKeyChanged, takeUntil } from 'rxjs/operators';
import { MessageListener, MessageListenersManager } from '@zeppelin/core';
@@ -48,6 +48,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
private destroy$ = new Subject();
note: Note['note'];
permissions: Permissions;
+ selectId: string | null = null;
isOwner = true;
noteRevisions: RevisionListItem[] = [];
currentRevision: string;
@@ -216,6 +217,17 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
}, 10000);
}
+ onParagraphSelect(id: string) {
+ this.selectId = id;
+ }
+
+ onSelectAtIndex(index: number) {
+ const scopeIndex = Math.min(this.note.paragraphs.length, Math.max(0, index));
+ if (this.note.paragraphs[scopeIndex]) {
+ this.selectId = this.note.paragraphs[scopeIndex].id;
+ }
+ }
+
saveNote() {
if (this.note && this.note.paragraphs && this.listOfNotebookParagraphComponent) {
this.listOfNotebookParagraphComponent.toArray().forEach(p => {
@@ -276,7 +288,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
private noteVarShareService: NoteVarShareService,
private ticketService: TicketService,
private securityService: SecurityService,
- private router: Router
+ private router: Router,
) {
super(messageService);
}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
index 6dabb4b..0711e81 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
@@ -55,6 +55,7 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
@Input() pid: string;
@Output() readonly textChanged = new EventEmitter<string>();
@Output() readonly editorBlur = new EventEmitter<void>();
+ @Output() readonly editorFocus = new EventEmitter<void>();
private editor: IStandaloneCodeEditor;
private monacoDisposables: IDisposable[] = [];
height = 0;
@@ -76,6 +77,7 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
this.monacoDisposables.push(
editor.onDidFocusEditorText(() => {
this.ngZone.runOutsideAngular(() => {
+ this.editorFocus.emit();
editor.updateOptions({ renderLineHighlight: 'all' });
});
}),
@@ -85,6 +87,7 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
editor.updateOptions({ renderLineHighlight: 'none' });
});
}),
+
editor.onDidChangeModelContent(() => {
this.ngZone.run(() => {
this.text = editor.getModel().getValue();
@@ -123,6 +126,16 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
});
}
+ this.editor.addCommand(
+ monaco.KeyCode.Escape,
+ () => {
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur();
+ }
+ },
+ '!suggestWidgetVisible'
+ );
+
this.updateEditorOptions();
this.setParagraphMode();
this.initEditorListener();
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts
index 085fa26..5b95953 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts
@@ -71,6 +71,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
@Output() readonly runAllAbove = new EventEmitter<void>();
@Output() readonly runAllBelowAndCurrent = new EventEmitter<void>();
@Output() readonly cloneParagraph = new EventEmitter<void>();
+ @Output() readonly removeParagraph = new EventEmitter<void>();
fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
dropdownVisible = false;
isMac = navigator.appVersion.indexOf('Mac') !== -1;
@@ -190,7 +191,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
show: this.paragraphLength > 1,
disabled: this.isEntireNoteRunning,
icon: 'delete',
- trigger: () => this.removeParagraph(),
+ trigger: () => this.onRemoveParagraph(),
shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+D`,
keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_D] : []
}
@@ -258,25 +259,8 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
}
}
- removeParagraph() {
- if (!this.isEntireNoteRunning) {
- if (this.paragraphLength === 1) {
- this.nzModalService.warning({
- nzTitle: `Warning`,
- nzContent: `All the paragraphs can't be deleted`
- });
- } else {
- this.nzModalService.confirm({
- nzTitle: 'Delete Paragraph',
- nzContent: 'Do you want to delete this paragraph?',
- nzOnOk: () => {
- this.messageService.paragraphRemove(this.pid);
- this.cdr.markForCheck();
- // TODO(hsuanxyz) moveFocusToNextParagraph
- }
- });
- }
- }
+ onRemoveParagraph() {
+ this.removeParagraph.emit();
}
trigger(event: EventEmitter<void>) {
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
index 861f955..1c42f87 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
@@ -14,7 +14,10 @@
<zeppelin-notebook-add-paragraph *ngIf="!revisionView && looknfeel !== 'report'"
[disabled]="isEntireNoteRunning"
(addParagraph)="insertParagraph('above')"></zeppelin-notebook-add-paragraph>
- <div class="paragraph" [class.simple]="looknfeel !== 'default'" [class.report]="looknfeel === 'report'">
+ <div class="paragraph"
+ [class.focused]="select"
+ [class.simple]="looknfeel !== 'default'"
+ [class.report]="looknfeel === 'report'">
<zeppelin-elastic-input *ngIf="paragraph.config.title"
[value]="paragraph.title"
[min]="true"
@@ -39,6 +42,7 @@
[(editorHide)]="paragraph.config.editorHide"
[(runOnSelectionChange)]="paragraph.config.runOnSelectionChange"
(tableHideChange)="commitParagraph()"
+ (removeParagraph)="removeParagraph()"
(colWidthChange)="changeColWidth(true)"
(fontSizeChange)="commitParagraph()"
(editorHideChange)="commitParagraph()"
@@ -65,7 +69,8 @@
[lineNumbers]="paragraph.config.lineNumbers"
[readOnly]="isEntireNoteRunning || isParagraphRunning || revisionView"
[language]="paragraph.config.editorSetting?.language"
- (editorBlur)="saveParagraph()"
+ (editorBlur)="onEditorBlur()"
+ (editorFocus)="onEditorFocus()"
(textChanged)="textChanged($event)"></zeppelin-notebook-paragraph-code-editor>
<zeppelin-notebook-paragraph-progress *ngIf="paragraph.status === 'RUNNING'"
[progress]="progress"></zeppelin-notebook-paragraph-progress>
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less
index f24d693..60cc5ac 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less
@@ -18,6 +18,7 @@
}
.themeMixin({
+
.paragraph {
background: @component-background;
border: 1px solid @border-color-split;
@@ -25,6 +26,10 @@
padding: 32px 12px 12px 12px;
position: relative;
+ &.focused {
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.46);
+ }
+
zeppelin-notebook-paragraph-code-editor + zeppelin-notebook-paragraph-dynamic-forms {
margin-top: 24px;
}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
index 21d57bb..1dde62d 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
@@ -13,7 +13,7 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
- Component,
+ Component, ElementRef,
EventEmitter,
Input,
OnChanges,
@@ -21,15 +21,16 @@ import {
OnInit,
Output,
QueryList,
+ SimpleChanges,
ViewChild,
ViewChildren
} from '@angular/core';
-import { Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
+import {merge, Observable, Subject} from 'rxjs';
+import {map, takeUntil} from 'rxjs/operators';
import DiffMatchPatch from 'diff-match-patch';
import { isEmpty, isEqual } from 'lodash';
-import { NzModalService } from 'ng-zorro-antd';
+import { NzModalService } from 'ng-zorro-antd/modal';
import { MessageListener, MessageListenersManager } from '@zeppelin/core';
import {
@@ -52,7 +53,10 @@ import {
NgZService,
NoteStatusService,
NoteVarShareService,
- ParagraphStatus
+ ParagraphActions,
+ ParagraphStatus,
+ ShortcutsMap,
+ ShortcutService
} from '@zeppelin/services';
import { SpellResult } from '@zeppelin/spell/spell-result';
@@ -60,10 +64,16 @@ import { NzResizeEvent } from 'ng-zorro-antd/resizable';
import { NotebookParagraphCodeEditorComponent } from './code-editor/code-editor.component';
import { NotebookParagraphResultComponent } from './result/result.component';
+type Mode = 'edit' | 'command';
+
@Component({
selector: 'zeppelin-notebook-paragraph',
templateUrl: './paragraph.component.html',
styleUrls: ['./paragraph.component.less'],
+ host: {
+ 'tabindex': '-1',
+ '(focusin)': 'onFocus()'
+ },
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotebookParagraphComponent extends MessageListenersManager implements OnInit, OnChanges, OnDestroy {
@@ -76,6 +86,8 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
@Input() note: Note['note'];
@Input() looknfeel: string;
@Input() revisionView: boolean;
+ @Input() select: boolean = false;
+ @Input() index: number = -1;
@Input() viewOnly: boolean;
@Input() last: boolean;
@Input() collaborativeMode = false;
@@ -83,8 +95,12 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
@Input() interpreterBindings: InterpreterBindingItem[] = [];
@Output() readonly saveNoteTimer = new EventEmitter();
@Output() readonly triggerSaveParagraph = new EventEmitter<string>();
+ @Output() readonly selected = new EventEmitter<string>();
+ @Output() readonly selectAtIndex = new EventEmitter<number>();
private destroy$ = new Subject();
+ private mode: Mode = 'command';
+ waitConfirmFromEdit = false;
dirtyText: string;
originalText: string;
isEntireNoteRunning = false;
@@ -185,6 +201,19 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
}
}
+ switchMode(mode: Mode): void {
+ if (mode === this.mode) {
+ return;
+ }
+ this.mode = mode;
+ if (mode === 'edit') {
+ this.focusEditor();
+ } else {
+ this.blurEditor();
+ (this.host.nativeElement as HTMLElement).focus();
+ }
+ }
+
updateParagraph(oldPara: ParagraphItem, newPara: ParagraphItem, updateCallback: () => void) {
// 1. can't update on revision view
if (!this.revisionView) {
@@ -237,6 +266,34 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
this.saveNoteTimer.emit();
}
+ onFocus() {
+ this.selected.emit(this.paragraph.id);
+ }
+
+ focusEditor() {
+ this.paragraph.focus = true;
+ this.saveParagraph();
+ this.cdr.markForCheck();
+ }
+
+ blurEditor() {
+ this.paragraph.focus = false;
+ (this.host.nativeElement as HTMLElement).focus();
+ this.saveParagraph();
+ this.cdr.markForCheck();
+ }
+
+ onEditorFocus() {
+ this.switchMode('edit');
+ }
+
+ onEditorBlur() {
+ // Ignore events triggered by open the confirm box in edit mode
+ if (!this.waitConfirmFromEdit) {
+ this.switchMode('command');
+ }
+ }
+
saveParagraph() {
const dirtyText = this.paragraph.text;
if (dirtyText === undefined || dirtyText === this.originalText) {
@@ -248,6 +305,27 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
this.cdr.markForCheck();
}
+ removeParagraph() {
+ if (!this.isEntireNoteRunning) {
+ if (this.note.paragraphs.length === 1) {
+ this.nzModalService.warning({
+ nzTitle: `Warning`,
+ nzContent: `All the paragraphs can't be deleted`
+ });
+ } else {
+ this.nzModalService.confirm({
+ nzTitle: 'Delete Paragraph',
+ nzContent: 'Do you want to delete this paragraph?',
+ nzOnOk: () => {
+ this.messageService.paragraphRemove(this.paragraph.id);
+ this.cdr.markForCheck();
+ // TODO(hsuanxyz) moveFocusToNextParagraph
+ }
+ });
+ }
+ }
+ }
+
runAllAbove() {
const index = this.note.paragraphs.findIndex(p => p.id === this.paragraph.id);
const toRunParagraphs = this.note.paragraphs.filter((p, i) => i < index);
@@ -298,7 +376,11 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
nzOnOk: () => {
this.messageService.runAllParagraphs(this.note.id, paragraphs);
}
- });
+ }).afterClose
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ this.waitConfirmFromEdit = false;
+ });
// TODO(hsuanxyz): save cursor
}
@@ -486,7 +568,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
setTitle(title: string) {
this.paragraph.title = title;
this.commitParagraph();
- this.cdr.markForCheck();
}
commitParagraph() {
@@ -498,6 +579,7 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
settings: { params }
} = this.paragraph;
this.messageService.commitParagraph(id, title, text, config, params, this.note.id);
+ this.cdr.markForCheck();
}
initializeDefault(config: ParagraphConfig) {
@@ -586,7 +668,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
onConfigChange(configResult: ParagraphConfigResult, index: number) {
this.paragraph.config.results[index] = configResult;
this.commitParagraph();
- this.cdr.markForCheck();
}
setEditorHide(editorHide: boolean) {
@@ -610,12 +691,128 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
private nzModalService: NzModalService,
private noteVarShareService: NoteVarShareService,
private cdr: ChangeDetectorRef,
- private ngZService: NgZService
+ private ngZService: NgZService,
+ private shortcutService: ShortcutService,
+ private host: ElementRef
) {
super(messageService);
}
ngOnInit() {
+ const shortcutService = this.shortcutService.forkByElement(this.host.nativeElement);
+ const observables: Array<Observable<{
+ action: ParagraphActions,
+ event: KeyboardEvent
+ }>> = [];
+ Object.entries(ShortcutsMap).forEach(([action, keys]) => {
+ const keysArr: string[] = Array.isArray(keys) ? keys : [keys];
+ keysArr.forEach(key => {
+ observables.push(
+ shortcutService.bindShortcut({
+ keybindings: key
+ }).pipe(
+ takeUntil(this.destroy$),
+ map(({event}) => {
+ return {
+ event,
+ action: action as ParagraphActions
+ }
+ }))
+ );
+ });
+ });
+
+ merge<{
+ action: ParagraphActions,
+ event: KeyboardEvent
+ }>(...observables)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(({action, event}) => {
+ if (this.mode === 'command') {
+ switch (action) {
+ case ParagraphActions.InsertAbove:
+ this.insertParagraph('above');
+ break;
+ case ParagraphActions.InsertBelow:
+ this.insertParagraph('below');
+ break;
+ case ParagraphActions.SwitchEditorShow:
+ this.setEditorHide(!this.paragraph.config.editorHide);
+ this.commitParagraph();
+ break;
+ case ParagraphActions.SwitchOutputShow:
+ this.setTableHide(!this.paragraph.config.tableHide);
+ this.commitParagraph();
+ break;
+ case ParagraphActions.SwitchTitleShow:
+ this.paragraph.config.title = !this.paragraph.config.title;
+ this.commitParagraph();
+ break;
+ case ParagraphActions.SwitchLineNumber:
+ this.paragraph.config.lineNumbers = !this.paragraph.config.lineNumbers;
+ this.commitParagraph();
+ break;
+ case ParagraphActions.MoveToUp:
+ this.moveUpParagraph();
+ break;
+ case ParagraphActions.MoveToDown:
+ this.moveDownParagraph();
+ break;
+ case ParagraphActions.SwitchEnable:
+ this.paragraph.config.enabled = !this.paragraph.config.enabled;
+ this.commitParagraph();
+ break;
+ case ParagraphActions.ReduceWidth:
+ this.paragraph.config.colWidth = Math.max(1, this.paragraph.config.colWidth - 1);
+ this.cdr.markForCheck();
+ this.changeColWidth(true);
+ break;
+ case ParagraphActions.IncreaseWidth:
+ this.paragraph.config.colWidth = Math.min(12, this.paragraph.config.colWidth + 1);
+ this.cdr.markForCheck();
+ this.changeColWidth(true);
+ break;
+ case ParagraphActions.Delete:
+ this.removeParagraph();
+ break;
+ case ParagraphActions.SelectAbove:
+ event.preventDefault();
+ this.selectAtIndex.emit(this.index - 1);
+ break;
+ case ParagraphActions.SelectBelow:
+ event.preventDefault();
+ this.selectAtIndex.emit(this.index + 1);
+ break;
+ default:
+ break;
+ }
+ }
+ switch (action) {
+ case ParagraphActions.EditMode:
+ if (this.mode === 'command') {
+ event.preventDefault();
+ }
+ if (!this.paragraph.config.editorHide) {
+ this.switchMode('edit');
+ }
+ break;
+ case ParagraphActions.Run:
+ event.preventDefault();
+ this.runParagraph();
+ break;
+ case ParagraphActions.RunBelow:
+ this.waitConfirmFromEdit = true;
+ this.runAllBelowAndCurrent();
+ break;
+ case ParagraphActions.Cancel:
+ event.preventDefault();
+ this.cancelParagraph();
+ break;
+ default:
+ break;
+ }
+ });
+
this.setResults();
this.originalText = this.paragraph.text;
this.isEntireNoteRunning = this.noteStatusService.isEntireNoteRunning(this.note);
@@ -644,7 +841,17 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
});
}
- ngOnChanges(): void {}
+ ngOnChanges(changes: SimpleChanges): void {
+ const { index, select } = changes;
+ if (index && index.currentValue !== index.previousValue && this.select
+ || select && select.currentValue === true && select.previousValue !== true) {
+ if (this.host.nativeElement) {
+ setTimeout(() => {
+ (this.host.nativeElement as HTMLElement).focus();
+ })
+ }
+ }
+ }
ngOnDestroy(): void {
super.ngOnDestroy();
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
index 850bc18..903f72b 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
@@ -254,6 +254,9 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit,
}
setGraphConfig() {
+ if (!this.config || !this.config.graph) {
+ return;
+ }
const visualizationItem = this.visualizations.find(v => v.id === this.config.graph.mode);
if (!visualizationItem || !visualizationItem.instance) {
return;
diff --git a/zeppelin-web-angular/src/app/services/public-api.ts b/zeppelin-web-angular/src/app/services/public-api.ts
index d56ec7e..d6709ac 100644
--- a/zeppelin-web-angular/src/app/services/public-api.ts
+++ b/zeppelin-web-angular/src/app/services/public-api.ts
@@ -26,3 +26,4 @@ export * from './ng-z.service';
export * from './array-ordering.service';
export * from './note-list.service';
export * from './runtime-compiler.service';
+export * from './shortcut.service';
diff --git a/zeppelin-web-angular/src/app/services/shortcut.service.ts b/zeppelin-web-angular/src/app/services/shortcut.service.ts
new file mode 100644
index 0000000..b6d6a3a
--- /dev/null
+++ b/zeppelin-web-angular/src/app/services/shortcut.service.ts
@@ -0,0 +1,109 @@
+import {DOCUMENT} from "@angular/common";
+import {Inject, Injectable} from '@angular/core';
+import {EventManager} from "@angular/platform-browser";
+import {Observable} from "rxjs";
+
+export enum ParagraphActions {
+ EditMode = 'Paragraph:EditMode',
+ CommandMode = 'Paragraph:CommandMode',
+ Run = 'Paragraph:Run',
+ RunBelow = 'Paragraph:RunBelow',
+ Cancel = 'Paragraph:Cancel',
+ Clear = 'Paragraph:Clear',
+ ReduceWidth = 'Paragraph:ReduceWidth',
+ IncreaseWidth = 'Paragraph:IncreaseWidth',
+ Delete = 'Paragraph:Delete',
+ MoveToUp = 'Paragraph:MoveToUp',
+ MoveToDown = 'Paragraph:MoveToDown',
+ SelectAbove = 'Paragraph:SelectAbove',
+ SelectBelow = 'Paragraph:SelectBelow',
+ InsertAbove = 'Paragraph:InsertAbove',
+ InsertBelow = 'Paragraph:InsertBelow',
+ SwitchLineNumber = 'Paragraph:SwitchLineNumber',
+ SwitchTitleShow = 'Paragraph:SwitchTitleShow',
+ SwitchOutputShow = 'Paragraph:SwitchOutputShow',
+ SwitchEditorShow = 'Paragraph:SwitchEditorShow',
+ SwitchEnable = 'Paragraph:SwitchEnable'
+}
+
+export const ShortcutsMap = {
+ [ParagraphActions.EditMode]: 'enter',
+ [ParagraphActions.CommandMode]: 'esc',
+ [ParagraphActions.Run]: 'shift.enter',
+ [ParagraphActions.RunBelow]: 'shift.ctrlCmd.enter',
+ [ParagraphActions.Cancel]: 'shift.ctrlCmd.c',
+ // Need register special character `¬` in MacOS
+ [ParagraphActions.Clear]: ['alt.ctrlCmd.l', 'alt.ctrlCmd.¬'],
+ // Need register special character `®` in MacOS
+ [ParagraphActions.SwitchEnable]: ['alt.ctrlCmd.r', 'alt.ctrlCmd.®'],
+ // Need register special character `–` in MacOS
+ [ParagraphActions.ReduceWidth]: ['alt.ctrlCmd.-', 'alt.ctrlCmd.–'],
+ // Need register special character `≠` in MacOS
+ [ParagraphActions.IncreaseWidth]: ['alt.ctrlCmd.+', 'alt.ctrlCmd.≠'],
+ [ParagraphActions.Delete]: 'shift.delete',
+ [ParagraphActions.MoveToUp]: ['ctrlCmd.k', 'ctrlCmd.arrowup'],
+ [ParagraphActions.MoveToDown]: ['ctrlCmd.j', 'ctrlCmd.arrowdown'],
+ [ParagraphActions.SelectAbove]: ['k', 'arrowup'],
+ [ParagraphActions.SelectBelow]: ['j', 'arrowdown'],
+ [ParagraphActions.SwitchLineNumber]: 'l',
+ [ParagraphActions.SwitchTitleShow]: 't',
+ [ParagraphActions.SwitchOutputShow]: 'o',
+ [ParagraphActions.SwitchEditorShow]: 'e',
+ [ParagraphActions.InsertAbove]: 'a',
+ [ParagraphActions.InsertBelow]: 'b'
+};
+
+export interface ShortcutEvent {
+ event: KeyboardEvent
+ keybindings: string;
+}
+
+export interface ShortcutOption {
+ scope?: HTMLElement,
+ keybindings: string
+}
+
+function isMacOS() {
+ return navigator.platform.indexOf('Mac') > -1
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ShortcutService {
+
+ private element: HTMLElement;
+
+ constructor(private eventManager: EventManager,
+ @Inject(DOCUMENT) _document: any) {
+ this.element = _document;
+ }
+
+ forkByElement(element: HTMLElement) {
+ return new ShortcutService(this.eventManager, element);
+ }
+
+ bindShortcut(option: ShortcutOption): Observable<ShortcutEvent> {
+ const host = option.scope || this.element;
+ // `ctrlCmd` is special symbol, will be replaced `meta` in MacOS, 'control' in Windows/Linux
+ const keybindings = option.keybindings
+ .replace(/ctrlCmd/g, isMacOS() ? 'meta' : 'control');
+ const event = `keydown.${keybindings}`;
+ let dispose: Function;
+ return new Observable<ShortcutEvent>(observer => {
+ const handler = event => {
+ observer.next({
+ event,
+ keybindings: option.keybindings
+ });
+ };
+
+ dispose = this.eventManager.addEventListener(host, event, handler);
+
+ return () => {
+ dispose();
+ };
+ })
+ }
+
+}