mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 09:35:38 -05:00
Insert local/online images using image call out (#15238)
* changes from Chris's branch and cell model updates * get base64 value * handle spaces in image names * add comments * add tests for imageCallOut dialog * format document for hygiene errors * address comments * check base64 validity using regex * replace space with regex * add parameter and return type * split into two functions * move functions to fileUtilities * correct import * fix for layering issue * revert file function changes
This commit is contained in:
@@ -9,7 +9,7 @@ import * as styler from 'vs/platform/theme/common/styler';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import * as constants from 'sql/workbench/contrib/notebook/browser/calloutDialog/common/constants';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Modal, IDialogProperties } from 'sql/workbench/browser/modal/modal';
|
||||
import { Modal, IDialogProperties, DialogPosition, DialogWidth } from 'sql/workbench/browser/modal/modal';
|
||||
import { IFileDialogService, IOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
@@ -24,9 +24,8 @@ import { Deferred } from 'sql/base/common/promise';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
import { RadioButton } from 'sql/base/browser/ui/radioButton/radioButton';
|
||||
import { DialogPosition, DialogWidth } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { attachCalloutDialogStyler } from 'sql/workbench/common/styler';
|
||||
import { escapeUrl } from 'sql/workbench/contrib/notebook/browser/calloutDialog/common/utils';
|
||||
import * as path from 'vs/base/common/path';
|
||||
|
||||
export interface IImageCalloutDialogOptions {
|
||||
insertTitle?: string,
|
||||
@@ -37,6 +36,7 @@ export interface IImageCalloutDialogOptions {
|
||||
|
||||
const DEFAULT_DIALOG_WIDTH: DialogWidth = 452;
|
||||
|
||||
const IMAGE_Extensions: string[] = ['jpg', 'jpeg', 'png', 'gif'];
|
||||
export class ImageCalloutDialog extends Modal {
|
||||
private _selectionComplete: Deferred<IImageCalloutDialogOptions> = new Deferred<IImageCalloutDialogOptions>();
|
||||
private _imageLocationLabel: HTMLElement;
|
||||
@@ -163,16 +163,6 @@ export class ImageCalloutDialog extends Modal {
|
||||
}
|
||||
}, true));
|
||||
|
||||
this._register(this._imageRemoteRadioButton.onClicked(e => {
|
||||
this._imageBrowseButton.style.display = 'none';
|
||||
this._imageUrlLabel.innerText = constants.urlPlaceholder;
|
||||
this._imageUrlInputBox.setPlaceHolder(constants.urlPlaceholder);
|
||||
}));
|
||||
this._register(this._imageLocalRadioButton.onClicked(e => {
|
||||
this._imageBrowseButton.style.display = 'block';
|
||||
this._imageUrlLabel.innerText = constants.pathPlaceholder;
|
||||
this._imageUrlInputBox.setPlaceHolder(constants.pathPlaceholder);
|
||||
}));
|
||||
DOM.append(pathRow, inputContainer);
|
||||
|
||||
let embedRow = DOM.$('.row');
|
||||
@@ -187,6 +177,19 @@ export class ImageCalloutDialog extends Modal {
|
||||
ariaLabel: constants.embedImageLabel
|
||||
});
|
||||
DOM.append(embedRow, this._imageEmbedLabel);
|
||||
|
||||
this._register(this._imageRemoteRadioButton.onClicked(e => {
|
||||
this._imageBrowseButton.style.display = 'none';
|
||||
this._imageEmbedCheckbox.enabled = false;
|
||||
this._imageUrlLabel.innerText = constants.urlPlaceholder;
|
||||
this._imageUrlInputBox.setPlaceHolder(constants.urlPlaceholder);
|
||||
}));
|
||||
this._register(this._imageLocalRadioButton.onClicked(e => {
|
||||
this._imageBrowseButton.style.display = 'block';
|
||||
this._imageEmbedCheckbox.enabled = true;
|
||||
this._imageUrlLabel.innerText = constants.pathPlaceholder;
|
||||
this._imageUrlInputBox.setPlaceHolder(constants.pathPlaceholder);
|
||||
}));
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
@@ -196,10 +199,14 @@ export class ImageCalloutDialog extends Modal {
|
||||
|
||||
public insert(): void {
|
||||
this.hide('ok');
|
||||
let imgPath = this._imageUrlInputBox.value;
|
||||
let imageName = path.basename(imgPath);
|
||||
this._selectionComplete.resolve({
|
||||
insertEscapedMarkdown: `})`,
|
||||
imagePath: this._imageUrlInputBox.value,
|
||||
embedImage: this._imageEmbedCheckbox.checked
|
||||
embedImage: this._imageEmbedCheckbox.checked,
|
||||
// check for spaces and remove them in imageName.
|
||||
// if spaces in image path replace with   as per https://github.com/microsoft/vscode/issues/11933#issuecomment-249987377
|
||||
insertEscapedMarkdown: this._imageEmbedCheckbox.checked ? `})` : `})`,
|
||||
imagePath: imgPath
|
||||
});
|
||||
this.dispose();
|
||||
}
|
||||
@@ -225,7 +232,8 @@ export class ImageCalloutDialog extends Modal {
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
defaultUri: URI.file(await this.getUserHome()),
|
||||
title: undefined
|
||||
title: undefined,
|
||||
filters: [{ extensions: IMAGE_Extensions, name: 'images' }]
|
||||
};
|
||||
let imageUri: URI[] = await this._fileDialogService.showOpenDialog(options);
|
||||
if (imageUri.length > 0) {
|
||||
@@ -234,4 +242,12 @@ export class ImageCalloutDialog extends Modal {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set imagePath(val: string) {
|
||||
this._imageUrlInputBox.value = val;
|
||||
}
|
||||
|
||||
public set embedImage(val: boolean) {
|
||||
this._imageEmbedCheckbox.checked = val;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { IEditor } from 'vs/editor/common/editorCommon';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { escape } from 'vs/base/common/strings';
|
||||
import { IImageCalloutDialogOptions, ImageCalloutDialog } from 'sql/workbench/contrib/notebook/browser/calloutDialog/imageCalloutDialog';
|
||||
|
||||
export const MARKDOWN_TOOLBAR_SELECTOR: string = 'markdown-toolbar-component';
|
||||
const linksRegex = /\[(?<text>.+)\]\((?<url>[^ ]+)(?: "(?<title>.+)")?\)/;
|
||||
@@ -225,6 +226,7 @@ export class MarkdownToolbarComponent extends AngularDisposable {
|
||||
let triggerElement = event.target as HTMLElement;
|
||||
let needsTransform = true;
|
||||
let linkCalloutResult: ILinkCalloutDialogOptions;
|
||||
let imageCalloutResult: IImageCalloutDialogOptions;
|
||||
|
||||
if (type === MarkdownButtonType.LINK_PREVIEW) {
|
||||
linkCalloutResult = await this.createCallout(type, triggerElement);
|
||||
@@ -251,6 +253,12 @@ export class MarkdownToolbarComponent extends AngularDisposable {
|
||||
document.execCommand('insertHTML', false, `<a href="${escape(linkUrl)}">${escape(linkCalloutResult?.insertUnescapedLinkLabel)}</a>`);
|
||||
return;
|
||||
}
|
||||
} else if (type === MarkdownButtonType.IMAGE_PREVIEW) {
|
||||
imageCalloutResult = await this.createCallout(type, triggerElement);
|
||||
// If cell edit mode isn't WYSIWYG, use result from callout. No need for further transformation.
|
||||
if (this.cellModel.currentMode !== CellEditModes.WYSIWYG) {
|
||||
needsTransform = false;
|
||||
}
|
||||
}
|
||||
|
||||
const transformer = new MarkdownTextTransformer(this._notebookService, this.cellModel);
|
||||
@@ -259,6 +267,14 @@ export class MarkdownToolbarComponent extends AngularDisposable {
|
||||
} else if (!needsTransform) {
|
||||
if (type === MarkdownButtonType.LINK_PREVIEW) {
|
||||
await insertFormattedMarkdown(linkCalloutResult?.insertEscapedMarkdown, this.getCellEditorControl());
|
||||
} else if (type === MarkdownButtonType.IMAGE_PREVIEW) {
|
||||
if (imageCalloutResult.embedImage) {
|
||||
let base64String = await this.getFileContentBase64(URI.file(imageCalloutResult.imagePath));
|
||||
let mimeType = await this.getFileMimeType(URI.file(imageCalloutResult.imagePath));
|
||||
this.cellModel.addAttachment(mimeType, base64String, path.basename(imageCalloutResult.imagePath).replace(' ', ''));
|
||||
await insertFormattedMarkdown(imageCalloutResult.insertEscapedMarkdown, this.getCellEditorControl());
|
||||
}
|
||||
await insertFormattedMarkdown(imageCalloutResult.insertEscapedMarkdown, this.getCellEditorControl());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,6 +320,10 @@ export class MarkdownToolbarComponent extends AngularDisposable {
|
||||
this._linkCallout = this._instantiationService.createInstance(LinkCalloutDialog, this.insertLinkHeading, dialogPosition, dialogProperties, defaultLabel, defaultLinkUrl);
|
||||
this._linkCallout.render();
|
||||
calloutOptions = await this._linkCallout.open();
|
||||
} else if (type === MarkdownButtonType.IMAGE_PREVIEW) {
|
||||
const imageCallout = this._instantiationService.createInstance(ImageCalloutDialog, this.insertImageHeading, dialogPosition, dialogProperties);
|
||||
imageCallout.render();
|
||||
calloutOptions = await imageCallout.open();
|
||||
}
|
||||
return calloutOptions;
|
||||
}
|
||||
@@ -354,4 +374,26 @@ export class MarkdownToolbarComponent extends AngularDisposable {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async getFileContentBase64(fileUri: URI): Promise<string> {
|
||||
return new Promise<string>(async resolve => {
|
||||
let response = await fetch(fileUri.toString());
|
||||
let blob = await response.blob();
|
||||
|
||||
let file = new File([blob], fileUri.toString());
|
||||
let reader = new FileReader();
|
||||
// Read file content on file loaded event
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result.toString());
|
||||
};
|
||||
// Convert data to base64
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
public async getFileMimeType(fileUri: URI): Promise<string> {
|
||||
let response = await fetch(fileUri.toString());
|
||||
let blob = await response.blob();
|
||||
return blob.type;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user