mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-06 01:25:38 -05:00
Hackathon - Better Markdown Editor (#11540)
* Hackathon - better markdown editor - modified Bold to wrap selection in HTML. Split Image button into two new options: embed and link. Made preview container contentEditable. * Removed the new dropdown from Image button -- it is not necessary since we are adding a context panel instead. * Modified preview icons * Set code-component dimensions so it is not visible. It is still being used to pass markdown changes to however. * add turndown and save markdown * update model on UI when source changes * Added conditional that sets element attribute contentEditable when it is in edit mode. * Added textView component that can be used for editing. * update source on MD view not on every keystroke * Added markdown editor buttons that allow user to swap editor, preview views. * Cleaning up implementation * Setting base value of _showPreview to false. * don't allow html edit on split view * Update editor automagically * Add an image picking dialog to notebook toolbar. * Await transformText() * revert pushEditOperations to fix cursor issue * Implemented radio buttons for three view toggles. * Added new, optional properties to radioButton: name, icon class and tooltip. This allows for display as toggleable icon. Updated styles and theme accordingly. * Style tweaks. * Added new ViewAction file where the RadioButton action will reside. * Removed radio button implementation in exchange for native button instantiation. Adjusted CSS and theme accordingly. * Styles, component and template changes to handle view toggle between text, markdownn an splitview. Includes reverting of radioButton as this is no longer used. * WYSIWYG 3 Modes * Ensure one action active at a time * Setting Text View button active by default. Cleaned up styles. Moved toolbar element to prevent code cell layout overflow. * Ensure we respect editMode, add showMarkdown * hiding overflow on code-cell * Empty text container needs 100% width. Eliminates weird selection border too. * Initialize _previewMode * Actions Compatibility * Further toolbar enhancements * Update yarn lock after merge * Slim down changes * Remove commented out code * Added margins around notebook-preview container for more visual space for text * Add turndown to workbench html * Tweak import * Add types/turndown * Remove workbench.html fix * Import cjs modules directly for turndown * Leverage solution from github * browser umd * non browser umd * welp dependency * Modified updatePreview to insert a p tag only when text cell is empty. * add listener for undo * add turndown to remote and web * Fix workbench, check in plugin * PR comment Co-authored-by: maddydev <makoripa@microsoft.com> Co-authored-by: chlafreniere <hichise@gmail.com> Co-authored-by: Cory Rivera <corivera@microsoft.com> Co-authored-by: Lucy Zhang <luczhan@microsoft.com>
This commit is contained in:
@@ -208,7 +208,7 @@ export class CodeComponent extends CellView implements OnInit, OnChanges {
|
||||
|
||||
let untitledEditorModel = await this._editorInput.resolve() as UntitledTextEditorModel;
|
||||
this._editorModel = untitledEditorModel.textEditorModel;
|
||||
|
||||
this.updateModel();
|
||||
let isActive = this.cellModel.id === this._activeCellId;
|
||||
this._editor.toggleEditorSelected(isActive);
|
||||
|
||||
@@ -252,8 +252,11 @@ export class CodeComponent extends CellView implements OnInit, OnChanges {
|
||||
this._register(this.cellModel.onCollapseStateChanged(isCollapsed => {
|
||||
this.onCellCollapse(isCollapsed);
|
||||
}));
|
||||
|
||||
this._register(this.cellModel.onCellPreviewChanged(() => {
|
||||
this._register(this.cellModel.onCellPreviewModeChanged((e) => {
|
||||
if (!e && this._cellModel.cellSourceChanged) {
|
||||
this.updateModel();
|
||||
this._cellModel.cellSourceChanged = false;
|
||||
}
|
||||
this._layoutEmitter.fire();
|
||||
}));
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { localize } from 'vs/nls';
|
||||
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { TransformMarkdownAction, MarkdownButtonType, TogglePreviewAction } from 'sql/workbench/contrib/notebook/browser/markdownToolbarActions';
|
||||
import { TransformMarkdownAction, MarkdownButtonType, ToggleViewAction } from 'sql/workbench/contrib/notebook/browser/markdownToolbarActions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { DropdownMenuActionViewItem } from 'sql/base/browser/ui/buttonMenu/buttonMenu';
|
||||
|
||||
@@ -38,8 +38,15 @@ export class MarkdownToolbarComponent {
|
||||
public optionHeading3 = localize('optionHeading3', "Heading 3");
|
||||
public optionParagraph = localize('optionParagraph', "Paragraph");
|
||||
|
||||
public textViewButton = localize('textViewButton', "View as Text");
|
||||
public splitViewButton = localize('splitViewButton', "View as Split");
|
||||
public markdownButton = localize('markdownButton', "View as Markdown");
|
||||
|
||||
@Input() public cellModel: ICellModel;
|
||||
private _actionBar: Taskbar;
|
||||
_toggleTextViewAction: ToggleViewAction;
|
||||
_toggleSplitViewAction: ToggleViewAction;
|
||||
_toggleMarkdownViewAction: ToggleViewAction;
|
||||
|
||||
constructor(
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@@ -60,7 +67,6 @@ export class MarkdownToolbarComponent {
|
||||
let listButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.listText', '', 'list masked-icon', this.buttonList, this.cellModel, MarkdownButtonType.UNORDERED_LIST);
|
||||
let orderedListButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.orderedText', '', 'ordered-list masked-icon', this.buttonOrderedList, this.cellModel, MarkdownButtonType.ORDERED_LIST);
|
||||
let imageButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.imageText', '', 'insert-image masked-icon', this.buttonImage, this.cellModel, MarkdownButtonType.IMAGE);
|
||||
let togglePreviewAction = this._instantiationService.createInstance(TogglePreviewAction, 'notebook.togglePreview masked-icon', true, this.cellModel.showPreview);
|
||||
|
||||
let headingDropdown = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading', '', 'heading', this.dropdownHeading, this.cellModel, null);
|
||||
let heading1 = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading1', this.optionHeading1, 'heading 1', this.optionHeading1, this.cellModel, MarkdownButtonType.HEADING1);
|
||||
@@ -68,6 +74,10 @@ export class MarkdownToolbarComponent {
|
||||
let heading3 = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading3', this.optionHeading3, 'heading 3', this.optionHeading3, this.cellModel, MarkdownButtonType.HEADING3);
|
||||
let paragraph = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.paragraph', this.optionParagraph, 'paragraph', this.optionParagraph, this.cellModel, MarkdownButtonType.PARAGRAPH);
|
||||
|
||||
this._toggleTextViewAction = this._instantiationService.createInstance(ToggleViewAction, 'notebook.toggleTextView', '', 'masked-icon show-text active', this.textViewButton, true, false);
|
||||
this._toggleSplitViewAction = this._instantiationService.createInstance(ToggleViewAction, 'notebook.toggleSplitView', '', 'masked-icon split-toggle-on', this.splitViewButton, true, true);
|
||||
this._toggleMarkdownViewAction = this._instantiationService.createInstance(ToggleViewAction, 'notebook.toggleMarkdownView', '', 'masked-icon show-markdown', this.markdownButton, false, true);
|
||||
|
||||
let taskbar = <HTMLElement>this.mdtoolbar.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar);
|
||||
this._actionBar.context = this;
|
||||
@@ -99,7 +109,18 @@ export class MarkdownToolbarComponent {
|
||||
{ action: orderedListButton },
|
||||
{ action: imageButton },
|
||||
{ element: buttonDropdownContainer },
|
||||
{ action: togglePreviewAction }
|
||||
{ action: this._toggleTextViewAction },
|
||||
{ action: this._toggleSplitViewAction },
|
||||
{ action: this._toggleMarkdownViewAction }
|
||||
]);
|
||||
}
|
||||
|
||||
public removeActiveClassFromModeActions() {
|
||||
const activeClass = ' active';
|
||||
for (let action of [this._toggleTextViewAction, this._toggleSplitViewAction, this._toggleMarkdownViewAction]) {
|
||||
if (action.class.includes(activeClass)) {
|
||||
action.class = action.class.replace(activeClass, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,65 +20,86 @@
|
||||
padding: 10px 16px 4px 16px;
|
||||
}
|
||||
|
||||
.markdown-toolbar ul {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-toolbar .carbon-taskbar li.action-item {
|
||||
display: inline-block;
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li {
|
||||
margin-right: 14px;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li:nth-child(1) {
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li:nth-child(1) {
|
||||
margin-right: 9px;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li:nth-child(2) {
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li:nth-child(2) {
|
||||
margin-right: 9px;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li.action-item .masked-pseudo-after.dropdown-arrow {
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li:nth-last-child(-n+3) {
|
||||
margin-left: auto;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li:nth-last-child(-n+2) {
|
||||
margin-left: initial;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li:nth-last-child(-n+1) {
|
||||
margin-left: initial;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li.action-item .masked-pseudo-after.dropdown-arrow {
|
||||
background-color: transparent;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li.action-item .masked-pseudo-after.dropdown-arrow:after {
|
||||
.markdown-toolbar .carbon-taskbar ul.actions-container li.action-item .masked-pseudo-after.dropdown-arrow:after {
|
||||
position: relative;
|
||||
right: -6px;
|
||||
top: -3px;
|
||||
width: 26px;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li:last-child {
|
||||
margin-right: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.markdown-toolbar .carbon-taskbar li a {
|
||||
.markdown-toolbar .carbon-taskbar a {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
.markdown-toolbar a.active::after {
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 2px;
|
||||
bottom: -2px;
|
||||
content: '';
|
||||
display: block;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.markdown-toolbar li a.codicon.masked-icon::before {
|
||||
.markdown-toolbar .codicon.masked-icon::before {
|
||||
-webkit-mask-size: auto;
|
||||
mask-size: auto;
|
||||
}
|
||||
|
||||
.markdown-toolbar li a.codicon.code::before {
|
||||
.markdown-toolbar .codicon.code::before {
|
||||
-webkit-mask-size: 88% 100%;
|
||||
mask-size: 88% 100%;
|
||||
}
|
||||
|
||||
.markdown-toolbar li a.codicon.insert-link::before {
|
||||
.markdown-toolbar .codicon.insert-link::before {
|
||||
-webkit-mask-size: 80% 100%;
|
||||
mask-size: 80% 100%;
|
||||
}
|
||||
|
||||
.markdown-toolbar li a.codicon.ordered-list::before {
|
||||
.markdown-toolbar .codicon.ordered-list::before {
|
||||
-webkit-mask-size: 86% 100%;
|
||||
mask-size: 86% 100%;
|
||||
}
|
||||
|
||||
.markdown-toolbar .codicon.show-markdown::before {
|
||||
-webkit-mask-size: 100%;
|
||||
mask-size: 100%;
|
||||
}
|
||||
.markdown-toolbar .codicon.show-text::before {
|
||||
-webkit-mask-size: 100%;
|
||||
mask-size: 100%;
|
||||
}
|
||||
|
||||
text-cell-component .offscreen {
|
||||
height: 1px;
|
||||
margin-top: -1px;
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="width: 100%; height: 100%; display: flex; flex-flow: column" (mouseover)="hover=true" (mouseleave)="hover=false">
|
||||
<markdown-toolbar-component #markdownToolbar *ngIf="previewFeaturesEnabled === true && isEditMode" [cellModel]="cellModel"></markdown-toolbar-component>
|
||||
<div class="notebook-text" [class.edit-mode]="isEditMode" [class.show-preview]="showPreview">
|
||||
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId">
|
||||
<markdown-toolbar-component #markdownToolbar *ngIf="isEditMode" [cellModel]="cellModel"></markdown-toolbar-component>
|
||||
<div class="notebook-text" [class.show-markdown]="markdownMode" [class.show-preview]="previewMode">
|
||||
<code-component *ngIf="markdownMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId">
|
||||
</code-component>
|
||||
<div #preview link-handler *ngIf="showPreview" [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview">
|
||||
<div #preview link-handler *ngIf="previewMode" [isTrusted]="isTrusted" [notebookUri]="notebookUri" contentEditable="{{!markdownMode && previewMode && isEditMode ? 'true' : 'false'}}" class="notebook-preview" (input)="handleHtmlChanged()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,8 @@ import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/
|
||||
import { NotebookRange, ICellEditorProvider, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IColorTheme } from 'vs/platform/theme/common/themeService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import * as turndownPluginGfm from '../turndownPluginGfm';
|
||||
import TurndownService = require('turndown');
|
||||
import * as Mark from 'mark.js';
|
||||
import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput';
|
||||
|
||||
@@ -73,10 +75,16 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
document.execCommand('selectAll');
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.meta.z', ['$event'])
|
||||
onUndo(e) {
|
||||
document.execCommand('undo');
|
||||
}
|
||||
|
||||
private _content: string | string[];
|
||||
private _lastTrustedMode: boolean;
|
||||
private isEditMode: boolean;
|
||||
private showPreview: boolean;
|
||||
private _previewMode: boolean = true;
|
||||
private _markdownMode: boolean;
|
||||
private _sanitizer: ISanitizer;
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
@@ -85,6 +93,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
private markdownRenderer: NotebookMarkdownRenderer;
|
||||
private markdownResult: IMarkdownRenderResult;
|
||||
public previewFeaturesEnabled: boolean = false;
|
||||
private turndownService;
|
||||
public doubleClickEditEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -96,8 +105,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
|
||||
) {
|
||||
super();
|
||||
this.isEditMode = true;
|
||||
this.showPreview = true;
|
||||
this.setTurndownOptions();
|
||||
this.markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
|
||||
this._register(toDisposable(() => {
|
||||
if (this.markdownResult) {
|
||||
@@ -146,6 +154,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
this.setFocusAndScroll();
|
||||
this.cellModel.isEditMode = false;
|
||||
this._register(this.cellModel.onOutputsChanged(e => {
|
||||
this.updatePreview();
|
||||
}));
|
||||
@@ -153,9 +162,15 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
if (mode !== this.isEditMode) {
|
||||
this.toggleEditMode(mode);
|
||||
}
|
||||
this._changeRef.detectChanges();
|
||||
}));
|
||||
this._register(this.cellModel.onCellPreviewChanged(preview => {
|
||||
this._register(this.cellModel.onCellPreviewModeChanged(preview => {
|
||||
this.previewMode = preview;
|
||||
this.focusIfPreviewMode();
|
||||
}));
|
||||
this._register(this.cellModel.onCellMarkdownModeChanged(markdown => {
|
||||
this.markdownMode = markdown;
|
||||
this.focusIfPreviewMode();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -196,7 +211,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
let trustedChanged = this.cellModel && this._lastTrustedMode !== this.cellModel.trustedMode;
|
||||
let cellModelSourceJoined = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source;
|
||||
let contentJoined = Array.isArray(this._content) ? this._content.join('') : this._content;
|
||||
let contentChanged = contentJoined !== cellModelSourceJoined || cellModelSourceJoined.length === 0 || this.showPreview === true;
|
||||
let contentChanged = contentJoined !== cellModelSourceJoined || cellModelSourceJoined.length === 0 || this._previewMode === true;
|
||||
if (trustedChanged || contentChanged) {
|
||||
this._lastTrustedMode = this.cellModel.trustedMode;
|
||||
if ((!cellModelSourceJoined) && !this.isEditMode) {
|
||||
@@ -206,9 +221,8 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
this._content = localize('addContent', "Add content here...");
|
||||
}
|
||||
} else {
|
||||
this._content = this.cellModel.source;
|
||||
this._content = this.cellModel.source[0] === '' ? '<p> </p>' : this.cellModel.source;
|
||||
}
|
||||
|
||||
this.markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
|
||||
this.markdownResult = this.markdownRenderer.render({
|
||||
isTrusted: true,
|
||||
@@ -216,14 +230,22 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
});
|
||||
this.markdownResult.element.innerHTML = this.sanitizeContent(this.markdownResult.element.innerHTML);
|
||||
this.setLoading(false);
|
||||
if (this.showPreview) {
|
||||
if (this._previewMode) {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = this.markdownResult.element.innerHTML;
|
||||
this.cellModel.renderedOutputTextContent = this.getRenderedTextOutput();
|
||||
outputElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateCellSource(): void {
|
||||
let textOutputElement = <HTMLElement>this.output.nativeElement;
|
||||
let newCellSource: string = this.turndownService.turndown(textOutputElement.innerHTML, { gfm: true });
|
||||
this.cellModel.source = newCellSource;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
//Sanitizes the content based on trusted mode of Cell Model
|
||||
private sanitizeContent(content: string): string {
|
||||
if (this.cellModel && !this.cellModel.trustedMode) {
|
||||
@@ -247,23 +269,40 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
public handleHtmlChanged(): void {
|
||||
this.updateCellSource();
|
||||
}
|
||||
|
||||
public toggleEditMode(editMode?: boolean): void {
|
||||
this.isEditMode = editMode !== undefined ? editMode : !this.isEditMode;
|
||||
this.cellModel.isEditMode = this.isEditMode;
|
||||
if (!this.isEditMode) {
|
||||
this.previewMode = true;
|
||||
this.markdownMode = false;
|
||||
}
|
||||
this.updatePreview();
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
public get previewMode(): boolean {
|
||||
return this.showPreview;
|
||||
return this._previewMode;
|
||||
}
|
||||
public set previewMode(value: boolean) {
|
||||
this.showPreview = value;
|
||||
this._changeRef.detectChanges();
|
||||
this.updatePreview();
|
||||
if (this._previewMode !== value) {
|
||||
this._previewMode = value;
|
||||
this.updatePreview();
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
public get markdownMode(): boolean {
|
||||
return this._markdownMode;
|
||||
}
|
||||
public set markdownMode(value: boolean) {
|
||||
if (this._markdownMode !== value) {
|
||||
this._markdownMode = value;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private toggleUserSelect(userSelect: boolean): void {
|
||||
@@ -281,11 +320,21 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
this.toggleEditMode(this.isActive());
|
||||
|
||||
if (this.output && this.output.nativeElement) {
|
||||
(<HTMLElement>this.output.nativeElement).scrollTo({ behavior: 'smooth' });
|
||||
let outputElement = this.output.nativeElement as HTMLElement;
|
||||
outputElement.scrollTo({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
private focusIfPreviewMode(): void {
|
||||
if (this.previewMode && !this.markdownMode) {
|
||||
let outputElement = this.output?.nativeElement as HTMLElement;
|
||||
if (outputElement) {
|
||||
outputElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive(): boolean {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
@@ -330,23 +379,25 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private getHtmlElements(): any[] {
|
||||
let hostElem = this.output.nativeElement;
|
||||
let hostElem = this.output?.nativeElement;
|
||||
let children = [];
|
||||
for (let element of hostElem.children) {
|
||||
if (element.nodeName.toLowerCase() === 'table') {
|
||||
// add table header and table rows.
|
||||
if (element.children.length > 0) {
|
||||
children.push(element.children[0]);
|
||||
if (element.children.length > 1) {
|
||||
for (let trow of element.children[1].children) {
|
||||
children.push(trow);
|
||||
if (hostElem) {
|
||||
for (let element of hostElem.children) {
|
||||
if (element.nodeName.toLowerCase() === 'table') {
|
||||
// add table header and table rows.
|
||||
if (element.children.length > 0) {
|
||||
children.push(element.children[0]);
|
||||
if (element.children.length > 1) {
|
||||
for (let trow of element.children[1].children) {
|
||||
children.push(trow);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element.children.length > 1) {
|
||||
children = children.concat(this.getChildren(element));
|
||||
} else {
|
||||
children.push(element);
|
||||
}
|
||||
} else if (element.children.length > 1) {
|
||||
children = children.concat(this.getChildren(element));
|
||||
} else {
|
||||
children.push(element);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
@@ -377,6 +428,33 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
return textOutput;
|
||||
}
|
||||
|
||||
private setTurndownOptions() {
|
||||
this.turndownService = new TurndownService({ 'emDelimiter': '_', 'bulletListMarker': '-' });
|
||||
this.turndownService.keep(['u', 'mark']);
|
||||
this.turndownService.use(turndownPluginGfm.gfm);
|
||||
this.turndownService.addRule('pre', {
|
||||
filter: 'pre',
|
||||
replacement: function (content, node) {
|
||||
return '\n```\n' + node.textContent + '\n```\n';
|
||||
}
|
||||
});
|
||||
this.turndownService.addRule('span', {
|
||||
filter: function (node, options) {
|
||||
return (
|
||||
node.nodeName === 'MARK' ||
|
||||
(node.nodeName === 'SPAN' &&
|
||||
node.getAttribute('style') === 'background-color: yellow;')
|
||||
);
|
||||
},
|
||||
replacement: function (content, node) {
|
||||
if (node.nodeName === 'SPAN') {
|
||||
return '<mark>' + node.textContent + '</mark>';
|
||||
}
|
||||
return node.textContent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enables edit mode on double clicking active cell
|
||||
private enableActiveCellEditOnDoubleClick() {
|
||||
if (!this.isEditMode && this.doubleClickEditEnabled) {
|
||||
|
||||
@@ -6,41 +6,34 @@
|
||||
text-cell-component {
|
||||
display: block;
|
||||
}
|
||||
|
||||
text-cell-component .notebook-text {
|
||||
display: flex;
|
||||
outline: none;
|
||||
}
|
||||
text-cell-component code-component {
|
||||
flex-direction: column;
|
||||
}
|
||||
text-cell-component .notebook-preview {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
text-cell-component .edit-mode code-component {
|
||||
display: block;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
text-cell-component .show-preview.edit-mode code-component {
|
||||
width: 50%;
|
||||
text-cell-component .show-markdown.show-preview code-component {
|
||||
width: 50%
|
||||
}
|
||||
text-cell-component .edit-mode .notebook-preview {
|
||||
text-cell-component .show-markdown .notebook-preview {
|
||||
border-color: transparent;
|
||||
border-left-width: 1px;
|
||||
border-style: solid;
|
||||
border-top-width: 0;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
width: 50%
|
||||
}
|
||||
|
||||
.notebook-preview.actionselect {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
text-cell-component code-component .monaco-scrollable-element.editor-scrollable.vs {
|
||||
left: 16px!important;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { INotebookEditor, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
@@ -13,9 +12,9 @@ import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ToggleableAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { MarkdownToolbarComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component';
|
||||
|
||||
|
||||
export class TransformMarkdownAction extends Action {
|
||||
@@ -35,19 +34,70 @@ export class TransformMarkdownAction extends Action {
|
||||
public run(context: any): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
let markdownTextTransformer = new MarkdownTextTransformer(this._notebookService, this._cellModel);
|
||||
markdownTextTransformer.transformText(this._type);
|
||||
if (!context?.cellModel?.showMarkdown && context?.cellModel?.showPreview) {
|
||||
this.transformDocumentCommand();
|
||||
} else {
|
||||
let markdownTextTransformer = new MarkdownTextTransformer(this._notebookService, this._cellModel);
|
||||
markdownTextTransformer.transformText(this._type);
|
||||
}
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private transformDocumentCommand() {
|
||||
switch (this._type) {
|
||||
case MarkdownButtonType.BOLD:
|
||||
document.execCommand('bold');
|
||||
break;
|
||||
case MarkdownButtonType.CODE:
|
||||
document.execCommand('formatBlock', false, 'pre');
|
||||
break;
|
||||
case MarkdownButtonType.HEADING1:
|
||||
document.execCommand('formatBlock', false, 'H1');
|
||||
break;
|
||||
case MarkdownButtonType.HEADING2:
|
||||
document.execCommand('formatBlock', false, 'H2');
|
||||
break;
|
||||
case MarkdownButtonType.HEADING3:
|
||||
document.execCommand('formatBlock', false, 'H3');
|
||||
break;
|
||||
case MarkdownButtonType.HIGHLIGHT:
|
||||
document.execCommand('hiliteColor', false, 'Yellow');
|
||||
break;
|
||||
case MarkdownButtonType.IMAGE:
|
||||
// TODO
|
||||
break;
|
||||
case MarkdownButtonType.ITALIC:
|
||||
document.execCommand('italic');
|
||||
break;
|
||||
case MarkdownButtonType.LINK:
|
||||
document.execCommand('createLink', false, window.getSelection()?.focusNode?.textContent);
|
||||
break;
|
||||
case MarkdownButtonType.ORDERED_LIST:
|
||||
document.execCommand('insertOrderedList');
|
||||
break;
|
||||
case MarkdownButtonType.PARAGRAPH:
|
||||
document.execCommand('formatBlock', false, 'p');
|
||||
break;
|
||||
case MarkdownButtonType.UNDERLINE:
|
||||
document.execCommand('underline');
|
||||
break;
|
||||
case MarkdownButtonType.UNORDERED_LIST:
|
||||
document.execCommand('insertUnorderedList');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MarkdownTextTransformer {
|
||||
|
||||
constructor(private _notebookService: INotebookService, private _cellModel: ICellModel, private _notebookEditor?: INotebookEditor) { }
|
||||
constructor(
|
||||
private _notebookService: INotebookService,
|
||||
private _cellModel: ICellModel,
|
||||
private _notebookEditor?: INotebookEditor) { }
|
||||
|
||||
public get notebookEditor(): INotebookEditor {
|
||||
return this._notebookEditor;
|
||||
@@ -473,39 +523,24 @@ function getColumnOffsetForSelection(type: MarkdownButtonType, nothingSelected:
|
||||
}
|
||||
}
|
||||
|
||||
export class TogglePreviewAction extends ToggleableAction {
|
||||
|
||||
private static readonly previewShowLabel = localize('previewShowLabel', "Show Preview");
|
||||
private static readonly previewHideLabel = localize('previewHideLabel', "Hide Preview");
|
||||
private static readonly baseClass = 'codicon';
|
||||
private static readonly previewShowCssClass = 'split-toggle-on';
|
||||
private static readonly previewHideCssClass = 'split-toggle-off';
|
||||
private static readonly maskedIconClass = 'masked-icon';
|
||||
|
||||
export class ToggleViewAction extends Action {
|
||||
constructor(
|
||||
id: string, toggleTooltip: boolean, showPreview: boolean
|
||||
id: string,
|
||||
label: string,
|
||||
cssClass: string,
|
||||
tooltip: string,
|
||||
private showPreview: boolean,
|
||||
private showMarkdown: boolean
|
||||
) {
|
||||
super(id, {
|
||||
baseClass: TogglePreviewAction.baseClass,
|
||||
toggleOffLabel: TogglePreviewAction.previewShowLabel,
|
||||
toggleOffClass: TogglePreviewAction.previewShowCssClass,
|
||||
toggleOnLabel: TogglePreviewAction.previewHideLabel,
|
||||
toggleOnClass: TogglePreviewAction.previewHideCssClass,
|
||||
maskedIconClass: TogglePreviewAction.maskedIconClass,
|
||||
shouldToggleTooltip: toggleTooltip,
|
||||
isOn: showPreview
|
||||
});
|
||||
super(id, label, cssClass);
|
||||
this._tooltip = tooltip;
|
||||
}
|
||||
|
||||
public get previewMode(): boolean {
|
||||
return this.state.isOn;
|
||||
}
|
||||
public set previewMode(value: boolean) {
|
||||
this.toggle(value);
|
||||
}
|
||||
public async run(context: any): Promise<boolean> {
|
||||
this.previewMode = !this.previewMode;
|
||||
context.cellModel.showPreview = this.previewMode;
|
||||
public async run(context: MarkdownToolbarComponent): Promise<boolean> {
|
||||
context.removeActiveClassFromModeActions();
|
||||
this.class += ' active';
|
||||
context.cellModel.showPreview = this.showPreview;
|
||||
context.cellModel.showMarkdown = this.showMarkdown;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,12 +167,13 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf
|
||||
collector.addRule(`.notebookEditor .notebook-button.masked-pseudo-after:after { background-color: ${buttonMenuArrowColor};}`);
|
||||
}
|
||||
|
||||
// Active cell border, cell toolbar border, cell toolbar icons
|
||||
// Active cell border, cell toolbar border, cell toolbar icons, view toggle active button bottom border
|
||||
const cellBorderColor = theme.getColor(cellBorder);
|
||||
if (cellBorderColor) {
|
||||
collector.addRule(`.notebookEditor .notebook-cell.active { border-color: ${cellBorderColor};}`);
|
||||
collector.addRule(`.notebookEditor .notebook-cell.active cell-toolbar-component { border-color: ${cellBorderColor};}`);
|
||||
collector.addRule(`.notebookEditor .notebook-cell.active cell-toolbar-component .codicon:before { background-color: ${cellBorderColor};}`);
|
||||
collector.addRule(`.markdown-toolbar a.active::after { border-bottom-color: ${cellBorderColor};}`);
|
||||
}
|
||||
// Cell toolbar background
|
||||
const notebookToolbarSelectBackgroundColor = theme.getColor(notebookToolbarSelectBackground);
|
||||
@@ -192,7 +193,7 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf
|
||||
}
|
||||
const toolbarIconColor = theme.getColor(toolbarIcon);
|
||||
if (toolbarIconColor) {
|
||||
collector.addRule(`.markdown-toolbar li a:before { background-color: ${toolbarIconColor};}`);
|
||||
collector.addRule(`.markdown-toolbar a::before { background-color: ${toolbarIconColor};}`);
|
||||
}
|
||||
const toolbarBottomBorderColor = theme.getColor(toolbarBottomBorder);
|
||||
if (toolbarBottomBorderColor) {
|
||||
|
||||
179
src/sql/workbench/contrib/notebook/browser/turndownPluginGfm.ts
Normal file
179
src/sql/workbench/contrib/notebook/browser/turndownPluginGfm.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
The Source EULA (MIT)
|
||||
|
||||
Copyright (c) 2017 Dom Christie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
const highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
||||
|
||||
export function highlightedCodeBlock(turndownService) {
|
||||
turndownService.addRule('highlightedCodeBlock', {
|
||||
filter: function (node) {
|
||||
let firstChild = node.firstChild;
|
||||
return (
|
||||
node.nodeName === 'DIV' &&
|
||||
highlightRegExp.test(node.className) &&
|
||||
firstChild &&
|
||||
firstChild.nodeName === 'PRE'
|
||||
);
|
||||
},
|
||||
replacement: function (content, node, options) {
|
||||
let className = node.className || '';
|
||||
let language = (className.match(highlightRegExp) || [null, ''])[1];
|
||||
|
||||
return (
|
||||
'\n\n' + options.fence + language + '\n' +
|
||||
node.firstChild.textContent +
|
||||
'\n' + options.fence + '\n\n'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function strikethrough(turndownService) {
|
||||
turndownService.addRule('strikethrough', {
|
||||
filter: ['del', 's', 'strike'],
|
||||
replacement: function (content) {
|
||||
return '~' + content + '~';
|
||||
}
|
||||
});
|
||||
}
|
||||
let rules = {};
|
||||
|
||||
rules['tableCell'] = {
|
||||
filter: ['th', 'td'],
|
||||
replacement: function (content, node) {
|
||||
return cell(content, node);
|
||||
}
|
||||
};
|
||||
|
||||
rules['tableRow'] = {
|
||||
filter: 'tr',
|
||||
replacement: function (content, node) {
|
||||
let borderCells = '';
|
||||
let alignMap = { left: ':--', right: '--:', center: ':-:' };
|
||||
|
||||
if (isHeadingRow(node)) {
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
let border = '---';
|
||||
let align = (
|
||||
node.childNodes[i].getAttribute('align') || ''
|
||||
).toLowerCase();
|
||||
|
||||
if (align) {
|
||||
border = alignMap[align] || border;
|
||||
}
|
||||
|
||||
borderCells += cell(border, node.childNodes[i]);
|
||||
}
|
||||
}
|
||||
return '\n' + content + (borderCells ? '\n' + borderCells : '');
|
||||
}
|
||||
};
|
||||
|
||||
rules['table'] = {
|
||||
// Only convert tables with a heading row.
|
||||
// Tables with no heading row are kept using `keep` (see below).
|
||||
filter: function (node) {
|
||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0]);
|
||||
},
|
||||
|
||||
replacement: function (content) {
|
||||
// Ensure there are no blank lines
|
||||
content = content.replace('\n\n', '\n');
|
||||
return '\n\n' + content + '\n\n';
|
||||
}
|
||||
};
|
||||
|
||||
rules['tableSection'] = {
|
||||
filter: ['thead', 'tbody', 'tfoot'],
|
||||
replacement: function (content) {
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
// A tr is a heading row if:
|
||||
// - the parent is a THEAD
|
||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
||||
// following a blank THEAD)
|
||||
// - and every cell is a TH
|
||||
function isHeadingRow(tr) {
|
||||
let parentNode = tr.parentNode;
|
||||
return (
|
||||
parentNode.nodeName === 'THEAD' ||
|
||||
(
|
||||
parentNode.firstChild === tr &&
|
||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
||||
Array.prototype.every.call(tr.childNodes, function (n) { return n.nodeName === 'TH'; })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isFirstTbody(element) {
|
||||
let previousSibling = element.previousSibling;
|
||||
return (
|
||||
element.nodeName === 'TBODY' && (
|
||||
!previousSibling ||
|
||||
(
|
||||
previousSibling.nodeName === 'THEAD' &&
|
||||
/^\s*$/i.test(previousSibling.textContent)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function cell(content, node) {
|
||||
let index = Array.prototype.indexOf.call(node.parentNode.childNodes, node);
|
||||
let prefix = ' ';
|
||||
if (index === 0) {
|
||||
prefix = '| ';
|
||||
}
|
||||
return prefix + content + ' |';
|
||||
}
|
||||
|
||||
export function tables(turndownService) {
|
||||
turndownService.keep(function (node) {
|
||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0]);
|
||||
});
|
||||
for (let key in rules) {
|
||||
turndownService.addRule(key, rules[key]);
|
||||
}
|
||||
}
|
||||
|
||||
export function taskListItems(turndownService) {
|
||||
turndownService.addRule('taskListItems', {
|
||||
filter: function (node) {
|
||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI';
|
||||
},
|
||||
replacement: function (content, node) {
|
||||
return (node.checked ? '[x]' : '[ ]') + ' ';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function gfm(turndownService) {
|
||||
turndownService.use([
|
||||
highlightedCodeBlock,
|
||||
strikethrough,
|
||||
tables,
|
||||
taskListItems
|
||||
]);
|
||||
}
|
||||
@@ -40,12 +40,13 @@ suite('MarkdownTextTransformer', () => {
|
||||
let notebookEditor: NotebookEditorStub;
|
||||
let mockNotebookService: TypeMoq.Mock<INotebookService>;
|
||||
let cellModel: CellModel;
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(() => {
|
||||
const dialogService = new TestDialogService();
|
||||
const notificationService = new TestNotificationService();
|
||||
const undoRedoService = new UndoRedoService(dialogService, notificationService);
|
||||
const instantiationService = new TestInstantiationService();
|
||||
instantiationService = new TestInstantiationService();
|
||||
|
||||
instantiationService.stub(IAccessibilityService, new TestAccessibilityService());
|
||||
instantiationService.stub(IContextKeyService, new MockContextKeyService());
|
||||
|
||||
@@ -61,9 +61,12 @@ export class CellModel extends Disposable implements ICellModel {
|
||||
private _isCollapsed: boolean;
|
||||
private _onCollapseStateChanged = new Emitter<boolean>();
|
||||
private _modelContentChangedEvent: IModelContentChangedEvent;
|
||||
private _showPreview: boolean = true;
|
||||
private _onCellPreviewChanged = new Emitter<boolean>();
|
||||
private _onCellMarkdownChanged = new Emitter<boolean>();
|
||||
private _isCommandExecutionSettingEnabled: boolean = false;
|
||||
private _showPreview: boolean = true;
|
||||
private _showMarkdown: boolean = false;
|
||||
private _cellSourceChanged: boolean = false;
|
||||
private _gridDataConversionComplete: Promise<void>[] = [];
|
||||
|
||||
constructor(cellData: nb.ICellContents,
|
||||
@@ -243,6 +246,7 @@ export class CellModel extends Disposable implements ICellModel {
|
||||
if (this._source !== newSource) {
|
||||
this._source = newSource;
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated);
|
||||
this.cellSourceChanged = true;
|
||||
}
|
||||
this._modelContentChangedEvent = undefined;
|
||||
}
|
||||
@@ -309,16 +313,35 @@ export class CellModel extends Disposable implements ICellModel {
|
||||
}
|
||||
|
||||
public set showPreview(val: boolean) {
|
||||
if (val !== this._showPreview) {
|
||||
this._showPreview = val;
|
||||
this._onCellPreviewChanged.fire(this._showPreview);
|
||||
}
|
||||
this._showPreview = val;
|
||||
this._onCellPreviewChanged.fire(this._showPreview);
|
||||
}
|
||||
|
||||
public get onCellPreviewChanged(): Event<boolean> {
|
||||
public get showMarkdown(): boolean {
|
||||
return this._showMarkdown;
|
||||
}
|
||||
|
||||
public set showMarkdown(val: boolean) {
|
||||
this._showMarkdown = val;
|
||||
this._onCellMarkdownChanged.fire(this._showMarkdown);
|
||||
}
|
||||
|
||||
|
||||
public get cellSourceChanged(): boolean {
|
||||
return this._cellSourceChanged;
|
||||
}
|
||||
public set cellSourceChanged(val: boolean) {
|
||||
this._cellSourceChanged = val;
|
||||
}
|
||||
|
||||
public get onCellPreviewModeChanged(): Event<boolean> {
|
||||
return this._onCellPreviewChanged.event;
|
||||
}
|
||||
|
||||
public get onCellMarkdownModeChanged(): Event<boolean> {
|
||||
return this._onCellMarkdownChanged.event;
|
||||
}
|
||||
|
||||
private notifyExecutionComplete(): void {
|
||||
if (this._notebookService) {
|
||||
this._notebookService.serializeNotebookStateChange(this.notebookModel.notebookUri, NotebookChangeType.CellExecuted, this)
|
||||
|
||||
@@ -482,8 +482,11 @@ export interface ICellModel {
|
||||
modelContentChangedEvent: IModelContentChangedEvent;
|
||||
isEditMode: boolean;
|
||||
showPreview: boolean;
|
||||
readonly onCellPreviewChanged: Event<boolean>;
|
||||
showMarkdown: boolean;
|
||||
readonly onCellPreviewModeChanged: Event<boolean>;
|
||||
readonly onCellMarkdownModeChanged: Event<boolean>;
|
||||
sendChangeToNotebook(change: NotebookChangeType): void;
|
||||
cellSourceChanged: boolean;
|
||||
gridDataConversionComplete: Promise<void>;
|
||||
addGridDataConversionPromise(complete: Promise<void>): void;
|
||||
updateOutputData(batchId: number, id: number, data: any): void;
|
||||
|
||||
Reference in New Issue
Block a user