Notebook views UI (#13914)

* Add notebook editor

Introduce notebook editor component to allow for separate notebook displays in order to accomodate notebook views

* Localize notebook views configuration title

* Refactor view mode and remove the views configuration while it is unused

* Only fire view mode changed event when the value has been changed

* Remove notebook views contribution

* Add metadata capabilities

* Notebook views definitions

* Add notebook views models

* Views test

* Rename type arguments

* Additional tests

* Fix unused import

* Notebook views changes

* Add notebookviews.css

* Update notebookViewModel.ts

* Add cell toolbar styles

* Upgrade gridstack

* Add gridstack styles

* Remove ununsed references

These are part of the next PR and not available yet

* Remove gridstack static file

* Add gridstack as a module in the electron unit tests

* Spacing fixes

* Add copyright notice

* Remove commented code

* Spacing fixes in notebook styles

* Move handle svg to image

* Add typing for gridstack

* Move notebook styles to file

* Rename selector constant

* Rename grid css file

* Add nb-grid-stack class to views grid

* Cell toolbar style adjustments

* Remove unused imports

* Update .eslintrc.json

* Fix outdated instantiation of LabeledMenuItemActionItem

* Address feedback

* Fix from update to main
This commit is contained in:
Daniel Grajeda
2021-05-17 10:16:43 -07:00
committed by GitHub
parent 7e8dccec82
commit 2781279644
18 changed files with 864 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar {
border-width: 1px;
border-style: solid;
position: absolute;
left: 20px;
top: -20px;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .carbon-taskbar .monaco-action-bar.animated {
padding: 0;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated ul.actions-container {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li.action-item {
display: inline-flex;
margin-right: 0;
text-align: center;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li:last-child {
margin-right: 0;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .carbon-taskbar .action-label {
padding: 0;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .monaco-action-bar .action-label {
margin-right: 0;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li a.masked-icon {
display: flex;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li a.masked-icon,
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li a.masked-icon:before {
height: 24px;
width: 29px;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon,
.vs .nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon,
.vs-dark .nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon,
.hc-black .nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon {
background-image: none;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .notebook-button.toolbarIconStop {
margin: 0 2px;
background-size: 20px 25px;
height: 24px;
width: 29px;
background-repeat: no-repeat;
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #viewsToolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center;"></div>
<div #container class="scrollable" style="flex: 1 1 auto; position: relative; outline: none" (click)="unselectActiveCell()" (scroll)="scrollHandler($event)">
<loading-spinner [loading]="isLoading"></loading-spinner>
<notebook-views-grid-component #gridstack [views]="views" [cells]="cells" [model]="model"></notebook-views-grid-component>
<div class="book-nav" #bookNav [style.visibility]="navigationVisibility">
</div>
</div>
</div>

View File

@@ -0,0 +1,278 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef } from '@angular/core';
import { ICellModel, INotebookModel, ISingleNotebookEditOperation } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component';
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
import { ICellEditorProvider, INotebookParams, INotebookService, INotebookEditor, NotebookRange, INotebookSection, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import * as notebookUtils from 'sql/workbench/services/notebook/browser/models/notebookUtils';
import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
import { Action, IActionViewItem } from 'vs/base/common/actions';
import { LabeledMenuItemActionItem } from 'sql/platform/actions/browser/menuEntryActionViewItem';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { MenuItemAction } from 'vs/platform/actions/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { onUnexpectedError } from 'vs/base/common/errors';
import { localize } from 'vs/nls';
import { Deferred } from 'sql/base/common/promise';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CellType, CellTypes } from 'sql/workbench/services/notebook/common/contracts';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { NotebookViewsGridComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component';
export const NOTEBOOKVIEWS_SELECTOR: string = 'notebook-view-component';
@Component({
selector: NOTEBOOKVIEWS_SELECTOR,
templateUrl: decodeURI(require.toUrl('./notebookViews.component.html'))
})
export class NotebookViewComponent extends AngularDisposable implements INotebookEditor {
@Input() model: NotebookModel;
@Input() activeView: INotebookView;
@Input() views: NotebookViewsExtension;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
@ViewChild('viewsToolbar', { read: ElementRef }) private _viewsToolbar: ElementRef;
@ViewChild(NotebookViewsGridComponent) private _gridstack: NotebookViewsGridComponent;
@ViewChildren(CodeCellComponent) private _codeCells: QueryList<CodeCellComponent>;
@ViewChildren(TextCellComponent) private _textCells: QueryList<TextCellComponent>;
protected _actionBar: Taskbar;
public previewFeaturesEnabled: boolean = false;
private _modelReadyDeferred = new Deferred<NotebookModel>();
private _scrollTop: number;
constructor(
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IKeybindingService) private _keybindingService: IKeybindingService,
@Inject(INotificationService) private _notificationService: INotificationService,
@Inject(INotebookService) private _notebookService: INotebookService,
@Inject(IConnectionManagementService) private _connectionManagementService: IConnectionManagementService,
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
@Inject(IEditorService) private _editorService: IEditorService
) {
super();
this._register(this._configurationService.onDidChangeConfiguration(e => {
this.previewFeaturesEnabled = this._configurationService.getValue('workbench.enablePreviewFeatures');
}));
}
public get notebookParams(): INotebookParams {
return this._notebookParams;
}
public get id(): string {
return this.notebookParams.notebookUri.toString();
}
isDirty(): boolean {
return this.notebookParams.input.isDirty();
}
isActive(): boolean {
return this._editorService.activeEditor ? this._editorService.activeEditor.matches(this.notebookParams.input) : false;
}
isVisible(): boolean {
let notebookEditor = this.notebookParams.input;
return this._editorService.visibleEditors.some(e => e.matches(notebookEditor));
}
executeEdits(edits: ISingleNotebookEditOperation[]): boolean {
throw new Error('Method not implemented.');
}
async runCell(cell: ICellModel): Promise<boolean> {
await this.modelReady;
let uriString = cell.cellUri.toString();
if (this.model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) {
this.selectCell(cell);
return cell.runCell(this._notificationService, this._connectionManagementService);
} else {
throw new Error(localize('cellNotFound', "cell with URI {0} was not found in this model", uriString));
}
}
public async runAllCells(startCell?: ICellModel, endCell?: ICellModel): Promise<boolean> {
await this.modelReady;
let codeCells = this.model.cells.filter(cell => cell.cellType === CellTypes.Code);
if (codeCells && codeCells.length) {
// For the run all cells scenario where neither startId not endId are provided, set defaults
let startIndex = 0;
let endIndex = codeCells.length;
if (!isUndefinedOrNull(startCell)) {
startIndex = codeCells.findIndex(c => c.id === startCell.id);
}
if (!isUndefinedOrNull(endCell)) {
endIndex = codeCells.findIndex(c => c.id === endCell.id);
}
for (let i = startIndex; i < endIndex; i++) {
let cellStatus = await this.runCell(codeCells[i]);
if (!cellStatus) {
throw new Error(localize('cellRunFailed', "Run Cells failed - See error in output of the currently selected cell for more information."));
}
}
}
return true;
}
clearOutput(cell: ICellModel): Promise<boolean> {
throw new Error('Method not implemented.');
}
clearAllOutputs(): Promise<boolean> {
throw new Error('Method not implemented.');
}
getSections(): INotebookSection[] {
throw new Error('Method not implemented.');
}
navigateToSection(sectionId: string): void {
throw new Error('Method not implemented.');
}
deltaDecorations(newDecorationRange: NotebookRange, oldDecorationRange: NotebookRange): void {
throw new Error('Method not implemented.');
}
addCell(cellType: CellType, index?: number, event?: UIEvent) {
throw new Error('Method not implemented.');
}
insertCell(cell: ICellModel) {
this._gridstack.onCellChanged({ cell: cell, event: 'insert' });
}
ngOnInit() {
this.initViewsToolbar();
this._notebookService.addNotebookEditor(this);
this._modelReadyDeferred.resolve(this.model);
this.setScrollPosition();
this.doLoad().catch(e => onUnexpectedError(e));
}
ngOnDestroy() {
this.dispose();
}
ngOnChanges() {
this.initViewsToolbar();
}
private async doLoad(): Promise<void> {
await this.awaitNonDefaultProvider();
await this.model.requestModelLoad();
await this.model.onClientSessionReady;
this.detectChanges();
}
private async awaitNonDefaultProvider(): Promise<void> {
// Wait on registration for now. Long-term would be good to cache and refresh
await this._notebookService.registrationComplete;
this.model.standardKernels = this._notebookParams.input.standardKernels;
// Refresh the provider if we had been using default
let providerInfo = await this._notebookParams.providerInfo;
if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) {
let providers = notebookUtils.getProvidersForFileName(this._notebookParams.notebookUri.fsPath, this._notebookService);
let tsqlProvider = providers.find(provider => provider === SQL_NOTEBOOK_PROVIDER);
providerInfo.providerId = tsqlProvider ? SQL_NOTEBOOK_PROVIDER : providers[0];
}
}
public get cells(): ICellModel[] {
return this.model ? this.model.cells : [];
}
public selectCell(cell: ICellModel, event?: Event) {
if (event) {
event.stopPropagation();
}
if (!this.model.activeCell || this.model.activeCell.id !== cell.id) {
this.model.updateActiveCell(cell);
this.detectChanges();
}
}
private setScrollPosition(): void {
if (this._notebookParams && this._notebookParams.input) {
this._register(this._notebookParams.input.layoutChanged(() => {
let containerElement = <HTMLElement>this._container.nativeElement;
containerElement.scrollTop = this._scrollTop;
}));
}
}
/**
* Saves scrollTop value on scroll change
*/
public scrollHandler(event: Event) {
this._scrollTop = (<HTMLElement>event.srcElement).scrollTop;
}
public unselectActiveCell() {
this.model.updateActiveCell(undefined);
this.detectChanges();
}
protected initViewsToolbar() {
let taskbar = <HTMLElement>this._viewsToolbar.nativeElement;
if (!this._actionBar) {
this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) });
this._actionBar.context = this._notebookParams.notebookUri;//this.model;
taskbar.classList.add('in-preview');
}
let titleElement = document.createElement('li');
let titleText = document.createElement('span');
titleText.innerHTML = this.activeView?.name;
titleElement.appendChild(titleText);
titleElement.style.marginRight = '25px';
titleElement.style.minHeight = '25px';
this._actionBar.setContent([
{ element: titleElement },
{ element: Taskbar.createTaskbarSeparator() },
]);
}
private actionItemProvider(action: Action): IActionViewItem {
// Check extensions to create ActionItem; otherwise, return undefined
// This is similar behavior that exists in MenuItemActionItem
if (action instanceof MenuItemAction) {
if (action.item.id.includes('jupyter.cmd') && this.previewFeaturesEnabled) {
action.tooltip = action.label;
action.label = '';
}
return new LabeledMenuItemActionItem(action, this._keybindingService, this._notificationService, 'notebook-button fixed-width');
}
return undefined;
}
private detectChanges(): void {
if (!(this._changeRef['destroyed'])) {
this._changeRef.detectChanges();
}
}
public get modelReady(): Promise<INotebookModel> {
return this._modelReadyDeferred.promise;
}
public get cellEditors(): ICellEditorProvider[] {
let editors: ICellEditorProvider[] = [];
if (this._codeCells) {
this._codeCells.toArray().forEach(cell => editors.push(...cell.cellEditors));
}
if (this._textCells) {
this._textCells.toArray().forEach(cell => editors.push(...cell.cellEditors));
editors.push(...this._textCells.toArray());
}
return editors;
}
}

View File

@@ -0,0 +1,34 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<ng-template #templateRef>
<div
*ngIf="display"
class="grid-stack-item"
attr.data-cell-id="{{cell.cellGuid}}"
attr.data-gs-width="{{width}}"
attr.data-gs-height="{{height}}"
attr.data-gs-x="{{x}}"
attr.data-gs-y="{{y}}"
(click)="selectCell(cell, $event)"
>
<div
#item
class="grid-stack-item-content notebook-cell"
[class.active]="cell.active"
>
<div #actionbar [style.display]="showActionBar ? 'block' : 'none'" class="actionbar"></div>
<div class="grid-stack-content-wrapper">
<div class="grid-stack-content-wrapper-inner">
<views-code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</views-code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</text-cell-component>
</div>
</div>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* 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!./cellToolbar';
import { Component, OnInit, Input, ViewChild, TemplateRef, ElementRef, Inject, Output, EventEmitter, ChangeDetectorRef, forwardRef } from '@angular/core';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { DEFAULT_VIEW_CARD_HEIGHT, DEFAULT_VIEW_CARD_WIDTH } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { CellChangeEventType, INotebookView, INotebookViewCellMetadata } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
@Component({
selector: 'view-card-component',
templateUrl: decodeURI(require.toUrl('./notebookViewsCard.component.html'))
})
export class NotebookViewsCardComponent implements OnInit {
private _metadata: INotebookViewCellMetadata;
private _activeView: INotebookView;
@Input() cell: ICellModel;
@Input() model: NotebookModel;
@Input() views: NotebookViewsExtension;
@Input() ready: boolean;
@Output() onChange: EventEmitter<any> = new EventEmitter();
@ViewChild('templateRef') templateRef: TemplateRef<any>;
@ViewChild('item', { read: ElementRef }) private _item: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
) { }
ngOnInit() { }
ngOnChanges() {
if (this.views) {
this._activeView = this.views.getActiveView();
this._metadata = this.views.getCellMetadata(this.cell);
}
}
ngAfterContentInit() {
if (this.views) {
this._activeView = this.views.getActiveView();
this._metadata = this.views.getCellMetadata(this.cell);
}
}
ngAfterViewInit() {
this.detectChanges();
}
get elementRef(): ElementRef {
return this._item;
}
changed(event: CellChangeEventType) {
this.onChange.emit({ cell: this.cell, event: event });
}
detectChanges() {
this._changeRef.detectChanges();
}
public selectCell(cell: ICellModel, event?: Event) {
event?.stopPropagation();
if (!this.model.activeCell || this.model.activeCell.id !== cell.id) {
this.model.updateActiveCell(cell);
this.changed('active');
}
}
public hide(): void {
this.changed('hide');
}
public get data(): any {
return this._metadata?.views?.find(v => v.guid === this._activeView.guid);
}
public get width(): number {
return this.data?.width ? this.data.width : DEFAULT_VIEW_CARD_WIDTH;
}
public get height(): number {
return this.data.height ? this.data.height : DEFAULT_VIEW_CARD_HEIGHT;
}
public get x(): number {
return this.data?.x;
}
public get y(): number {
return this.data?.y;
}
public get display(): boolean {
if (!this._metadata || !this._activeView) {
return true;
}
return !this.data?.hidden;
}
public get showActionBar(): boolean {
return this.cell.active;
}
}

View File

@@ -0,0 +1,16 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="width: 100%; height: 100%; display: flex; flex-flow: column">
<div style="flex: 0 0 auto; width: 100%; height: 100%; display: block">
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel" [activeCellId]="activeCellId">
</output-area-component>
<div *ngIf="!cellModel.outputs || !cellModel.outputs.length">
{{emptyCellText}}
</div>
<stdin-component *ngIf="isStdInVisible" [onSendInput]="inputDeferred" [stdIn]="stdIn" [cellModel]="cellModel"></stdin-component>
</div>
</div>

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata';
import { ChangeDetectorRef, Component, forwardRef, Inject } from '@angular/core';
import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component';
import { localize } from 'vs/nls';
export const CODE_SELECTOR: string = 'views-code-cell-component';
@Component({
selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./notebookViewsCodeCell.component.html'))
})
export class NotebookViewsCodeCellComponent extends CodeCellComponent {
public readonly emptyCellText: string = localize('viewsCodeCell.emptyCellText', "Please run this cell to view outputs.");
constructor(@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
super(changeRef);
}
get outputs(): nb.ICellOutput[] {
return this.cellModel.outputs.filter((output: nb.IDisplayResult) => output.data && output.data['text/plain'] !== '<IPython.core.display.HTML object>');
}
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="nb-grid-stack grid-stack">
<ng-container *ngFor="let cell of cells">
<view-card-component #wrapper [ready]="loaded" [views]="views" [cell]="cell" [model]="model" (onChange)="onCellChanged($event)"></view-card-component>
<ng-content *ngTemplateOutlet='wrapper.templateRef'></ng-content>
</ng-container>
</div>
<div class="empty-message" *ngIf="empty && loaded">
{{emptyText}}
</div>

View File

@@ -0,0 +1,129 @@
/*---------------------------------------------------------------------------------------------
* 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!./notebookViewsGrid';
import { Component, OnInit, ViewChildren, QueryList, Input, Inject, forwardRef, ChangeDetectorRef } from '@angular/core';
import { NotebookViewsCardComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { GridStack, GridStackEvent, GridStackNode } from 'gridstack';
import 'gridstack/dist/h5/gridstack-dd-native';
import { localize } from 'vs/nls';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { CellChangeEvent, INotebookViewCell } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
@Component({
selector: 'notebook-views-grid-component',
templateUrl: decodeURI(require.toUrl('./notebookViewsGrid.component.html'))
})
export class NotebookViewsGridComponent implements OnInit {
@Input() cells: ICellModel[];
@Input() model: NotebookModel;
@Input() views: NotebookViewsExtension;
@ViewChildren(NotebookViewsCardComponent) private _items: QueryList<NotebookViewsCardComponent>;
protected _grid: GridStack;
public loaded: boolean;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
) {
this.loaded = false;
}
public get empty(): boolean {
return !this._items || !this._items.find(item => item.display);
}
public get hiddenItems(): NotebookViewsCardComponent[] {
return this._items.filter(item => !item.display);
}
public get emptyText(): String {
return localize('emptyText', "This view is empty. Add a cell to this view by clicking the Insert Cells button.");
}
ngOnInit() { }
ngAfterViewInit() {
const self = this;
self._grid = GridStack.init({
alwaysShowResizeHandle: false,
styleInHead: true
});
this.loaded = true;
this.detectChanges();
self._grid.on('added', function (e: Event, items: GridStackNode[]) { self.persist('added', items, self._grid, self._items); });
self._grid.on('removed', function (e: Event, items: GridStackNode[]) { self.persist('removed', items, self._grid, self._items); });
self._grid.on('change', function (e: Event, items: GridStackNode[]) { self.persist('change', items, self._grid, self._items); });
}
private detectChanges(): void {
if (!(this._changeRef['destroyed'])) {
this._changeRef.detectChanges();
}
}
async onCellChanged(e: CellChangeEvent): Promise<void> {
const currentView = this.views.getActiveView();
if (this._grid && currentView) {
const cellElem: HTMLElement = this._grid.el.querySelector(`[data-cell-id='${e.cell.cellGuid}']`);
if (cellElem && e.event === 'hide') {
this._grid.removeWidget(cellElem);
currentView.hideCell(e.cell);
}
if (e.cell && e.event === 'insert') {
const component = this._items.find(x => x.cell.cellGuid === e.cell.cellGuid);
// Prevent an awkward movement on the grid by moving this out of view first
currentView.moveCell(e.cell, 9999, 0);
currentView.insertCell(e.cell);
const el = this._grid.getGridItems().find(x => x.getAttribute('data-cell-id') === e.cell.cellGuid);
this._grid.makeWidget(el);
this._grid.update(el, { x: 0, y: 0 });
this._grid.resizable(el, true);
this._grid.movable(el, true);
component.detectChanges();
}
this.detectChanges();
}
}
/**
* Update the document model with the gridstack data as metadata
*/
persist(action: GridStackEvent, changedItems: GridStackNode[], grid: GridStack, items: QueryList<NotebookViewsCardComponent>): void {
changedItems.forEach((changedItem) => {
const cellId = changedItem.el.getAttribute('data-cell-id');
const item = items.toArray().find(item => item.cell.cellGuid === cellId);
const activeView = this.views.getActiveView();
if (item && activeView) {
const update: INotebookViewCell = {
guid: activeView.guid,
x: changedItem.x,
y: changedItem.y,
width: changedItem.w,
height: changedItem.h
};
if (action === 'added') {
update.hidden = false;
} else if (action === 'removed') {
update.hidden = true;
}
this.views.updateCell(item.cell, activeView, update);
}
});
}
}

View File

@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.nb-grid-stack {
margin: 0 8px;
}
.nb-grid-stack + .empty-message{
text-align: center;
}
.nb-grid-stack > .grid-stack-item {
margin: 0;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content {
display: flex;
cursor: move;
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.13);
border-radius: 2px;
overflow: visible;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content > .grid-stack-content-wrapper {
width: 100%;
display: flex;
flex-direction: column;
flex: 1;
margin-bottom: 20px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-wrapper-inner {
overflow-y: auto;
height: 100%;
cursor: auto;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content ::-webkit-scrollbar {
width: 8px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-header {
flex-direction: row;
justify-content: space-between;
display: flex;
margin-bottom: 3px;
padding: 0 5px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-header .action-label.codicon{
background-size: 16px;
padding: 11px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-item-title {
font-size: 13px;
line-height: 25px;
font-weight: 400;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .actionbar {
top: -14px;
z-index: 999;
}
.nb-grid-stack > .grid-stack-item.notebook-cell.active .actionbar {
z-index: 1;
transform: translate(0px, 7px);
}
.nb-grid-stack > .grid-stack-item.ui-draggable-dragging > .grid-stack-item-content,.nb-grid-stack > .grid-stack-item.ui-resizable-resizing > .grid-stack-item-content{
box-shadow:1px 4px 6px rgba(0,0,0,.2);
opacity:.8
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-se,.nb-grid-stack > .grid-stack-item > .ui-resizable-sw{
background-image:url("../media/light/notebook_views_card_handle.svg");
background-repeat:no-repeat;
background-position:center;
transform: rotate(0deg);
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-nw{
cursor:nw-resize;
width:20px;
height:20px;
left:10px;
top:0
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-n{
cursor:n-resize;
height:10px;
top:0;
left:25px;
right:25px
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-ne{
cursor:ne-resize;
width:20px;
height:20px;
right:10px;
top:0
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-e{
cursor:e-resize;
width:10px;
right:10px;
top:15px;
bottom:15px
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-se{
cursor:se-resize;
width:20px;
height:20px;
right:5px;
bottom:0
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-s{
cursor:s-resize;
height:10px;
left:25px;
bottom:0;
right:25px
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-sw{
cursor:sw-resize;
width:20px;
height:20px;
left:10px;
bottom:0
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-w{
cursor:w-resize;
width:10px;
left:10px;
top:15px;
bottom:15px
}