diff --git a/zeppelin-web-angular/package-lock.json b/zeppelin-web-angular/package-lock.json index 46c0a074b8d..85ad2a6251f 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 ae96fe7f87b..63b6be17d43 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 3bad5eec173..c447a59c39d 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 @@
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 a894971b8e9..97479a70938 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 6dabb4b2bbc..0711e816542 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(); @Output() readonly editorBlur = new EventEmitter(); + @Output() readonly editorFocus = new EventEmitter(); 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 085fa266b2e..5b959539d92 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(); @Output() readonly runAllBelowAndCurrent = new EventEmitter(); @Output() readonly cloneParagraph = new EventEmitter(); + @Output() readonly removeParagraph = new EventEmitter(); 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) { 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 861f955e261..1c42f87b371 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 @@ -
+
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 f24d693f1e8..60cc5ac3b19 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 21d57bb6e17..1dde62dae65 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(); + @Output() readonly selected = new EventEmitter(); + @Output() readonly selectAtIndex = new EventEmitter(); 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> = []; + 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 850bc18a394..903f72bccc5 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 d56ec7e5c6f..d6709ac83e8 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 00000000000..b6d6a3aac18 --- /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 { + 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(observer => { + const handler = event => { + observer.next({ + event, + keybindings: option.keybindings + }); + }; + + dispose = this.eventManager.addEventListener(host, event, handler); + + return () => { + dispose(); + }; + }) + } + +}