mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-20 01:25:37 -05:00
* Pressing Escape key in markdown exits edit mode * text + code cells inactive when pressing escape
244 lines
8.8 KiB
TypeScript
244 lines
8.8 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
import 'vs/css!./textCell';
|
|
|
|
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, OnChanges, SimpleChange, HostListener } from '@angular/core';
|
|
import * as path from 'path';
|
|
|
|
import { localize } from 'vs/nls';
|
|
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
|
import * as themeColors from 'vs/workbench/common/theme';
|
|
import { ICommandService } from 'vs/platform/commands/common/commands';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
import { Emitter } from 'vs/base/common/event';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
|
import * as DOM from 'vs/base/browser/dom';
|
|
|
|
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
|
|
import { CellView } from 'sql/workbench/parts/notebook/cellViews/interfaces';
|
|
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
|
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/outputs/sanitizer';
|
|
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
|
|
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/cellToggleMoreActions';
|
|
|
|
export const TEXT_SELECTOR: string = 'text-cell-component';
|
|
const USER_SELECT_CLASS = 'actionselect';
|
|
|
|
@Component({
|
|
selector: TEXT_SELECTOR,
|
|
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
|
|
})
|
|
export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
|
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
|
|
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
|
@Input() cellModel: ICellModel;
|
|
|
|
@Input() set model(value: NotebookModel) {
|
|
this._model = value;
|
|
}
|
|
|
|
@Input() set activeCellId(value: string) {
|
|
this._activeCellId = value;
|
|
}
|
|
|
|
@Input() set hover(value: boolean) {
|
|
this._hover = value;
|
|
if (!this.isActive()) {
|
|
// Only make a change if we're not active, since this has priority
|
|
this.updateMoreActions();
|
|
}
|
|
}
|
|
|
|
@HostListener('document:keydown.escape', ['$event'])
|
|
handleKeyboardEvent() {
|
|
if (this.isEditMode) {
|
|
this.toggleEditMode(false);
|
|
}
|
|
this.cellModel.active = false;
|
|
this._model.activeCell = undefined;
|
|
}
|
|
|
|
private _content: string;
|
|
private isEditMode: boolean;
|
|
private _sanitizer: ISanitizer;
|
|
private _model: NotebookModel;
|
|
private _activeCellId: string;
|
|
private readonly _onDidClickLink = this._register(new Emitter<URI>());
|
|
public readonly onDidClickLink = this._onDidClickLink.event;
|
|
protected isLoading: boolean;
|
|
private _cellToggleMoreActions: CellToggleMoreActions;
|
|
private _hover: boolean;
|
|
|
|
constructor(
|
|
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
|
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
|
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
|
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
|
@Inject(ICommandService) private _commandService: ICommandService,
|
|
@Inject(IOpenerService) private readonly openerService: IOpenerService,
|
|
) {
|
|
super();
|
|
this.isEditMode = true;
|
|
this.isLoading = true;
|
|
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
|
}
|
|
|
|
//Gets sanitizer from ISanitizer interface
|
|
private get sanitizer(): ISanitizer {
|
|
if (this._sanitizer) {
|
|
return this._sanitizer;
|
|
}
|
|
return this._sanitizer = defaultSanitizer;
|
|
}
|
|
|
|
get model(): NotebookModel {
|
|
return this._model;
|
|
}
|
|
|
|
get activeCellId(): string {
|
|
return this._activeCellId;
|
|
}
|
|
|
|
private setLoading(isLoading: boolean): void {
|
|
this.isLoading = isLoading;
|
|
this._changeRef.detectChanges();
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.setLoading(false);
|
|
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
|
this.updateTheme(this.themeService.getColorTheme());
|
|
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
|
|
this.setFocusAndScroll();
|
|
this._register(this.cellModel.onOutputsChanged(e => {
|
|
this.updatePreview();
|
|
}));
|
|
}
|
|
|
|
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
|
for (let propName in changes) {
|
|
if (propName === 'activeCellId') {
|
|
let changedProp = changes[propName];
|
|
this._activeCellId = changedProp.currentValue;
|
|
this.toggleUserSelect(this.isActive());
|
|
// If the activeCellId is undefined (i.e. in an active cell update), don't unnecessarily set editMode to false;
|
|
// it will be set to true in a subsequent call to toggleEditMode()
|
|
if (changedProp.previousValue !== undefined) {
|
|
this.toggleEditMode(false);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the preview of markdown component with latest changes
|
|
* If content is empty and in non-edit mode, default it to 'Double-click to edit'
|
|
* Sanitizes the data to be shown in markdown cell
|
|
*/
|
|
private updatePreview() {
|
|
if (this._content !== this.cellModel.source || this.cellModel.source.length === 0) {
|
|
if (!this.cellModel.source && !this.isEditMode) {
|
|
this._content = localize('doubleClickEdit', 'Double-click to edit');
|
|
} else {
|
|
this._content = this.sanitizeContent(this.cellModel.source);
|
|
}
|
|
|
|
this._commandService.executeCommand<string>('notebook.showPreview', this.cellModel.notebookModel.notebookUri, this._content).then((htmlcontent) => {
|
|
htmlcontent = this.convertVscodeResourceToFileInSubDirectories(htmlcontent);
|
|
let outputElement = <HTMLElement>this.output.nativeElement;
|
|
outputElement.innerHTML = htmlcontent;
|
|
});
|
|
}
|
|
}
|
|
|
|
//Sanitizes the content based on trusted mode of Cell Model
|
|
private sanitizeContent(content: string): string {
|
|
if (this.cellModel && !this.cellModel.trustedMode) {
|
|
content = this.sanitizer.sanitize(content);
|
|
}
|
|
return content;
|
|
}
|
|
|
|
// Only replace vscode-resource with file when in the same (or a sub) directory
|
|
// This matches Jupyter Notebook viewer behavior
|
|
private convertVscodeResourceToFileInSubDirectories(htmlContent: string): string {
|
|
let htmlContentCopy = htmlContent;
|
|
while (htmlContentCopy.search('(?<=img src=\"vscode-resource:)') > 0) {
|
|
let pathStartIndex = htmlContentCopy.search('(?<=img src=\"vscode-resource:)');
|
|
let pathEndIndex = htmlContentCopy.indexOf('\" ', pathStartIndex);
|
|
let filePath = htmlContentCopy.substring(pathStartIndex, pathEndIndex);
|
|
// If the asset is in the same folder or a subfolder, replace 'vscode-resource:' with 'file:', so the image is visible
|
|
if (!path.relative(path.dirname(this.cellModel.notebookModel.notebookUri.fsPath), filePath).includes('..')) {
|
|
// ok to change from vscode-resource: to file:
|
|
htmlContent = htmlContent.replace('vscode-resource:' + filePath, 'file:' + filePath);
|
|
}
|
|
htmlContentCopy = htmlContentCopy.slice(pathEndIndex);
|
|
}
|
|
return htmlContent;
|
|
}
|
|
|
|
|
|
// Todo: implement layout
|
|
public layout() {
|
|
}
|
|
|
|
private updateTheme(theme: IColorTheme): void {
|
|
let outputElement = <HTMLElement>this.output.nativeElement;
|
|
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
|
|
|
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
|
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
|
}
|
|
|
|
public handleContentChanged(): void {
|
|
this.updatePreview();
|
|
}
|
|
|
|
public toggleEditMode(editMode?: boolean): void {
|
|
this.isEditMode = editMode !== undefined ? editMode : !this.isEditMode;
|
|
this.updateMoreActions();
|
|
this.updatePreview();
|
|
this._changeRef.detectChanges();
|
|
}
|
|
|
|
private updateMoreActions(): void {
|
|
if (!this.isEditMode && (this.isActive() || this._hover)) {
|
|
this.toggleMoreActionsButton(true);
|
|
}
|
|
else {
|
|
this.toggleMoreActionsButton(false);
|
|
}
|
|
}
|
|
|
|
private toggleUserSelect(userSelect: boolean): void {
|
|
if (!this.output) {
|
|
return;
|
|
}
|
|
if (userSelect) {
|
|
DOM.addClass(this.output.nativeElement, USER_SELECT_CLASS);
|
|
} else {
|
|
DOM.removeClass(this.output.nativeElement, USER_SELECT_CLASS);
|
|
}
|
|
}
|
|
|
|
private setFocusAndScroll(): void {
|
|
this.toggleEditMode(this.isActive());
|
|
|
|
if (this.output && this.output.nativeElement) {
|
|
(<HTMLElement>this.output.nativeElement).scrollTo();
|
|
}
|
|
}
|
|
|
|
protected isActive() {
|
|
return this.cellModel && this.cellModel.id === this.activeCellId;
|
|
}
|
|
|
|
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
|
|
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
|
|
}
|
|
}
|