This is an automated email from the ASF dual-hosted git repository. zjffdu pushed a commit to branch web_angular in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/web_angular by this push: new c513681 [ZEPPELIN-4321] Support shortcuts for the paragraphs c513681 is described below commit c5136819fc9d8c1f13b684c87e0388ad9fd6cb31 Author: Hsuan Lee <hsua...@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 <hsua...@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(); + }; + }) + } + +}