calloutDialog refactor - new superclasses for insert image and insert link (#14385)

* calloutDialog refactor - split code specific to image and link into their own super classes. Moved callout styles into a new stylesheet.

* Image and Link inserts working.

* Stylesheets cleanup. Refactor cleanup.

* Removed CSS comment. Added missing image callout style. Revised generic open and cancel classes. Moved all remaining localized strings into shared constants file.
This commit is contained in:
Hale Rankin
2021-02-25 14:44:25 -08:00
committed by GitHub
parent 4053666bef
commit 9e02cf86a4
9 changed files with 539 additions and 398 deletions

View File

@@ -3,86 +3,27 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/calloutDialog';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import * as DOM from 'vs/base/browser/dom';
import * as styler from 'vs/platform/theme/common/styler';
import * as strings from 'vs/base/common/strings';
import { IDialogProperties, Modal, DialogWidth } from 'sql/workbench/browser/modal/modal';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { localize } from 'vs/nls';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IFileDialogService, IOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ILogService } from 'vs/platform/log/common/log';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
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 { IPathService } from 'vs/workbench/services/path/common/pathService';
export type CalloutType = 'IMAGE' | 'LINK';
export interface ICalloutDialogOptions {
insertTitle?: string,
calloutType?: CalloutType,
insertMarkup?: string,
imagePath?: string,
embedImage?: boolean
}
export class CalloutDialog extends Modal {
private _calloutType: CalloutType;
private _selectionComplete: Deferred<ICalloutDialogOptions>;
// Link
private _linkTextLabel: HTMLElement;
private _linkTextInputBox: InputBox;
private _linkAddressLabel: HTMLElement;
private _linkUrlInputBox: InputBox;
// Image
private _imageLocationLabel: HTMLElement;
private _imageLocalRadioButton: RadioButton;
private _editorImageLocationGroup: string = 'editorImageLocationGroup';
private _imageRemoteRadioButton: RadioButton;
private _imageUrlLabel: HTMLElement;
private _imageUrlInputBox: InputBox;
private _imageBrowseButton: HTMLAnchorElement;
private _imageEmbedLabel: HTMLElement;
private _imageEmbedCheckbox: Checkbox;
private readonly insertButtonText = localize('callout.insertButton', "Insert");
private readonly cancelButtonText = localize('callout.cancelButton', "Cancel");
// Link
private readonly linkTextLabel = localize('callout.linkTextLabel', "Text to display");
private readonly linkTextPlaceholder = localize('callout.linkTextPlaceholder', "Text to display");
private readonly linkAddressLabel = localize('callout.linkAddressLabel', "Address");
private readonly linkAddressPlaceholder = localize('callout.linkAddressPlaceholder', "Link to an existing file or web page");
// Image
private readonly locationLabel = localize('callout.locationLabel', "Image location");
private readonly localImageLabel = localize('callout.localImageLabel', "This computer");
private readonly remoteImageLabel = localize('callout.remoteImageLabel', "Online");
private readonly pathInputLabel = localize('callout.pathInputLabel', "Image URL");
private readonly pathPlaceholder = localize('callout.pathPlaceholder', "Enter image path");
private readonly urlPlaceholder = localize('callout.urlPlaceholder', "Enter image URL");
private readonly browseAltText = localize('callout.browseAltText', "Browse");
private readonly embedImageLabel = localize('callout.embedImageLabel', "Attach image to notebook");
export abstract class CalloutDialog<T> extends Modal {
constructor(
calloutType: CalloutType,
title: string,
width: DialogWidth,
dialogProperties: IDialogProperties,
@IPathService private readonly _pathService: IPathService,
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
@IThemeService themeService: IThemeService,
@ILayoutService layoutService: ILayoutService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextViewService private _contextViewService: IContextViewService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
@@ -103,224 +44,17 @@ export class CalloutDialog extends Modal {
dialogProperties: dialogProperties,
width: width
});
this._selectionComplete = new Deferred<ICalloutDialogOptions>();
this._calloutType = calloutType;
}
/**
* Opens the dialog and returns a promise for what options the user chooses.
*/
public open(): Promise<ICalloutDialogOptions> {
this.show();
return this._selectionComplete.promise;
}
protected abstract renderBody(container: HTMLElement): void;
public render() {
super.render();
public abstract open(): Promise<T>;
attachModalDialogStyler(this, this._themeService);
this.addFooterButton(this.insertButtonText, () => this.insert());
this.addFooterButton(this.cancelButtonText, () => this.cancel(), undefined, true);
this.registerListeners();
}
protected renderBody(container: HTMLElement) {
if (this._calloutType === 'IMAGE') {
this.buildInsertImageCallout(container);
}
if (this._calloutType === 'LINK') {
this.buildInsertLinkCallout(container);
}
}
private buildInsertImageCallout(container: HTMLElement): void {
let imageContentColumn = DOM.$('.column.insert-image');
DOM.append(container, imageContentColumn);
let locationRow = DOM.$('.row');
DOM.append(imageContentColumn, locationRow);
this._imageLocationLabel = DOM.$('p');
this._imageLocationLabel.innerText = this.locationLabel;
DOM.append(locationRow, this._imageLocationLabel);
let radioButtonGroup = DOM.$('.radio-group');
this._imageLocalRadioButton = new RadioButton(radioButtonGroup, {
label: this.localImageLabel,
enabled: true,
checked: true
});
this._imageRemoteRadioButton = new RadioButton(radioButtonGroup, {
label: this.remoteImageLabel,
enabled: true,
checked: false
});
this._imageLocalRadioButton.value = localize('local', "Local");
this._imageLocalRadioButton.name = this._editorImageLocationGroup;
this._imageRemoteRadioButton.value = localize('remote', "Remote");
this._imageRemoteRadioButton.name = this._editorImageLocationGroup;
DOM.append(locationRow, radioButtonGroup);
let pathRow = DOM.$('.row');
DOM.append(imageContentColumn, pathRow);
this._imageUrlLabel = DOM.$('p');
if (this._imageLocalRadioButton.checked === true) {
this._imageUrlLabel.innerText = this.pathPlaceholder;
} else {
this._imageUrlLabel.innerText = this.urlPlaceholder;
}
DOM.append(pathRow, this._imageUrlLabel);
let inputContainer = DOM.$('.flex-container');
this._imageUrlInputBox = new InputBox(
inputContainer,
this._contextViewService,
{
placeholder: this.pathPlaceholder,
ariaLabel: this.pathInputLabel
});
let browseButtonContainer = DOM.$('.button-icon');
this._imageBrowseButton = DOM.$('a.codicon.masked-icon.browse-local');
this._imageBrowseButton.title = this.browseAltText;
DOM.append(inputContainer, browseButtonContainer);
DOM.append(browseButtonContainer, this._imageBrowseButton);
this._register(DOM.addDisposableListener(this._imageBrowseButton, DOM.EventType.CLICK, async () => {
let selectedUri = await this.handleBrowse();
if (selectedUri) {
this._imageUrlInputBox.value = selectedUri.fsPath;
}
}, true));
this._register(this._imageRemoteRadioButton.onClicked(e => {
this._imageBrowseButton.style.display = 'none';
this._imageUrlLabel.innerText = this.urlPlaceholder;
this._imageUrlInputBox.setPlaceHolder(this.urlPlaceholder);
}));
this._register(this._imageLocalRadioButton.onClicked(e => {
this._imageBrowseButton.style.display = 'block';
this._imageUrlLabel.innerText = this.pathPlaceholder;
this._imageUrlInputBox.setPlaceHolder(this.pathPlaceholder);
}));
DOM.append(pathRow, inputContainer);
let embedRow = DOM.$('.row');
DOM.append(imageContentColumn, embedRow);
this._imageEmbedLabel = DOM.append(embedRow, DOM.$('.checkbox'));
this._imageEmbedCheckbox = new Checkbox(
this._imageEmbedLabel,
{
label: this.embedImageLabel,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: this.embedImageLabel
});
DOM.append(embedRow, this._imageEmbedLabel);
}
private buildInsertLinkCallout(container: HTMLElement): void {
let linkContentColumn = DOM.$('.column.insert-link');
DOM.append(container, linkContentColumn);
let linkTextRow = DOM.$('.row');
DOM.append(linkContentColumn, linkTextRow);
this._linkTextLabel = DOM.$('p');
this._linkTextLabel.innerText = this.linkTextLabel;
DOM.append(linkTextRow, this._linkTextLabel);
const linkTextInputContainer = DOM.$('.input-field');
this._linkTextInputBox = new InputBox(
linkTextInputContainer,
this._contextViewService,
{
placeholder: this.linkTextPlaceholder,
ariaLabel: this.linkTextLabel
});
DOM.append(linkTextRow, linkTextInputContainer);
let linkAddressRow = DOM.$('.row');
DOM.append(linkContentColumn, linkAddressRow);
this._linkAddressLabel = DOM.$('p');
this._linkAddressLabel.innerText = this.linkAddressLabel;
DOM.append(linkAddressRow, this._linkAddressLabel);
const linkAddressInputContainer = DOM.$('.input-field');
this._linkUrlInputBox = new InputBox(
linkAddressInputContainer,
this._contextViewService,
{
placeholder: this.linkAddressPlaceholder,
ariaLabel: this.linkAddressLabel
});
DOM.append(linkAddressRow, linkAddressInputContainer);
}
private registerListeners(): void {
// Theme styler
if (this._calloutType === 'IMAGE') {
this._register(styler.attachInputBoxStyler(this._imageUrlInputBox, this._themeService));
this._register(styler.attachCheckboxStyler(this._imageEmbedCheckbox, this._themeService));
}
if (this._calloutType === 'LINK') {
this._register(styler.attachInputBoxStyler(this._linkTextInputBox, this._themeService));
this._register(styler.attachInputBoxStyler(this._linkUrlInputBox, this._themeService));
}
public cancel(): void {
this.hide();
this.dispose();
}
protected layout(height?: number): void {
}
public insert() {
this.hide();
if (this._calloutType === 'IMAGE') {
this._selectionComplete.resolve({
insertMarkup: `<img src="${strings.escape(this._imageUrlInputBox.value)}">`,
imagePath: this._imageUrlInputBox.value,
embedImage: this._imageEmbedCheckbox.checked
});
}
if (this._calloutType === 'LINK') {
this._selectionComplete.resolve({
insertMarkup: `<a href="${strings.escape(this._linkUrlInputBox.value)}">${strings.escape(this._linkTextInputBox.value)}</a>`,
});
}
this.dispose();
}
public cancel() {
this.hide();
this._selectionComplete.resolve({
insertMarkup: '',
imagePath: undefined,
embedImage: undefined
});
this.dispose();
}
private async getUserHome(): Promise<string> {
const userHomeUri = await this._pathService.userHome();
return userHomeUri.path;
}
private async handleBrowse(): Promise<URI | undefined> {
let options: IOpenDialogOptions = {
openLabel: undefined,
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: URI.file(await this.getUserHome()),
title: undefined
};
let imageUri: URI[] = await this._fileDialogService.showOpenDialog(options);
if (imageUri.length > 0) {
return imageUri[0];
} else {
return undefined;
}
}
}

View File

@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.modal.callout-dialog {
background-color: transparent;
}
.modal.callout-dialog .modal-dialog {
border-radius: 2px;
box-shadow: 0px 3px 8px rgba(var(--foreground));
max-height: 300px;
position: absolute;
}
.modal.callout-dialog .modal-content p {
margin: 0;
}
.modal.callout-dialog .modal-content .button-icon {
cursor: pointer;
margin-left: 10px;
}
.modal.callout-dialog .modal-content .row {
margin-bottom: 16px;
}
.hc-black .modal.callout-dialog .modal-dialog {
box-shadow: none;
}
.callout-arrow:before {
border-width: 1px;
border-style: solid;
border-color:
transparent
transparent
var(--bodybackground)
var(--bodybackground);
box-shadow: -3px 3px 3px 0 rgba(var(--foreground));
content: '';
display: block;
height: 0;
position: absolute;
width: 0;
}
.callout-arrow.from-below:before {
border-width: 0.5em;
left: 2em;
top: -0.2em;
transform: rotate(135deg);
}
.callout-arrow.from-left:before {
background-color: var(--bodybackground);
height: 26px;
right: -13px;
top: 26px;
transform: rotate(-135deg);
width: 26px;
}
.hc-black .callout-arrow:before {
background-color: var(--bodybackground);
border-color:
transparent
transparent
var(--border)
var(--border);
border-width: 0.1em;
box-shadow: none;
height: 0.8em;
width: 0.8em;
}
.hc-black .callout-arrow.from-below:before {
top: -0.4em;
}
.hc-black .callout-arrow.from-left:before {
height: 2em;
right: -1.2em;
width: 2em;
}
.modal.callout-dialog .modal-header {
padding: 18px 24px 8px 24px;
}
.modal.callout-dialog .modal-footer {
padding: 15px 24px 15px 24px;
}
.modal.callout-dialog .modal-body {
padding: 8px 24px;
}
.modal.callout-dialog.compact .modal-header {
padding: 16px 24px 4px 24px;
}
.modal.callout-dialog.compact .modal-body {
padding: 4px 24px 16px 24px;
}

View File

@@ -24,113 +24,13 @@
height: 480px;
}
.modal.callout-dialog {
background-color: transparent;
}
.modal.callout-dialog .modal-dialog {
border-radius: 2px;
box-shadow: 0px 3px 8px rgba(var(--foreground));
max-height: 300px;
position: absolute;
}
.modal.callout-dialog .modal-content .insert-image .flex-container {
display: flex;
}
.modal.callout-dialog .modal-content .insert-image .flex-container > div {
flex: 1;
}
.modal.callout-dialog .modal-content p {
margin: 0;
}
.modal.callout-dialog .modal-content .button-icon {
cursor: pointer;
margin-left: 10px;
}
.modal.callout-dialog .modal-content .insert-image .monaco-inputbox {
min-width: 380px;
}
.modal.callout-dialog .modal-content .row {
margin-bottom: 16px;
}
.modal.callout-dialog .modal-content .radio-group input {
margin-right: 8px;
}
.modal.callout-dialog .modal-content .radio-group span {
margin-right: 15px;
}
.hc-black .modal.callout-dialog .modal-dialog {
box-shadow: none;
}
/* Correct the arrow appearance for HC theme */
.callout-arrow:before {
border-width: 1px;
border-style: solid;
border-color:
transparent
transparent
var(--bodybackground)
var(--bodybackground);
box-shadow: -3px 3px 3px 0 rgba(var(--foreground));
content: '';
display: block;
height: 0;
position: absolute;
width: 0;
}
.callout-arrow.from-below:before {
border-width: 0.5em;
left: 2em;
top: -0.2em;
transform: rotate(135deg);
}
.callout-arrow.from-left:before {
background-color: var(--bodybackground);
height: 26px;
right: -13px;
top: 26px;
transform: rotate(-135deg);
width: 26px;
}
.hc-black .callout-arrow:before {
background-color: var(--bodybackground);
border-color:
transparent
transparent
var(--border)
var(--border);
border-width: 0.1em;
box-shadow: none;
height: 0.8em;
width: 0.8em;
}
.hc-black .callout-arrow.from-below:before {
top: -0.4em;
}
.hc-black .callout-arrow.from-left:before {
height: 2em;
right: -1.2em;
width: 2em;
}
.modal .modal-header {
padding: 15px;
}
.modal.callout-dialog .modal-header {
padding: 18px 24px 8px 24px;
}
.modal .modal-footer {
padding: 15px;
}
.modal.callout-dialog .modal-footer {
padding: 15px 24px 15px 24px;
}
.modal .codicon.in-progress {
width: 25px;
@@ -189,17 +89,6 @@
overflow: hidden;
}
.modal.callout-dialog .modal-body {
padding: 8px 24px;
}
.modal.callout-dialog.compact .modal-header {
padding: 16px 24px 4px 24px;
}
.modal.callout-dialog.compact .modal-body {
padding: 4px 24px 16px 24px;
}
/* modl body content style(excluding dialogErrorMessage section) for angulr component dialog */
.angular-modal-body-content {
overflow-x: hidden;

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
// Localized texts
export const insertButtonText = localize('callout.insertButton', "Insert");
export const cancelButtonText = localize('callout.cancelButton', "Cancel");
// Insert Image
export const locationLabel = localize('imageCallout.locationLabel', "Image location");
export const localImageLabel = localize('imageCallout.localImageLabel', "This computer");
export const remoteImageLabel = localize('imageCallout.remoteImageLabel', "Online");
export const pathInputLabel = localize('imageCallout.pathInputLabel', "Image URL");
export const pathPlaceholder = localize('imageCallout.pathPlaceholder', "Enter image path");
export const urlPlaceholder = localize('imageCallout.urlPlaceholder', "Enter image URL");
export const browseAltText = localize('imageCallout.browseAltText', "Browse");
export const embedImageLabel = localize('imageCallout.embedImageLabel', "Attach image to notebook");
export const locationLocal = localize('imageCallout.local', "Local");
export const locationRemote = localize('imageCallout.remote', "Remote");
// Insert Link
export const linkTextLabel = localize('linkCallout.linkTextLabel', "Text to display");
export const linkTextPlaceholder = localize('linkCallout.linkTextPlaceholder', "Text to display");
export const linkAddressLabel = localize('linkCallout.linkAddressLabel', "Address");
export const linkAddressPlaceholder = localize('linkCallout.linkAddressPlaceholder', "Link to an existing file or web page");

View File

@@ -0,0 +1,227 @@
/*---------------------------------------------------------------------------------------------
* 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!./media/imageCalloutDialog';
import * as DOM from 'vs/base/browser/dom';
import * as strings from 'vs/base/common/strings';
import * as styler from 'vs/platform/theme/common/styler';
import { URI } from 'vs/base/common/uri';
import * as constants from 'sql/workbench/contrib/notebook/browser/calloutDialog/common/constants';
import { CalloutDialog } from 'sql/workbench/browser/modal/calloutDialog';
import { IFileDialogService, IOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IDialogProperties } from 'sql/workbench/browser/modal/modal';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ILogService } from 'vs/platform/log/common/log';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
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 { DialogWidth } from 'sql/workbench/api/common/sqlExtHostTypes';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
export interface IImageCalloutDialogOptions {
insertTitle?: string,
insertMarkup?: string,
imagePath?: string,
embedImage?: boolean
}
export class ImageCalloutDialog extends CalloutDialog<IImageCalloutDialogOptions> {
private _selectionComplete: Deferred<IImageCalloutDialogOptions> = new Deferred<IImageCalloutDialogOptions>();
private _imageLocationLabel: HTMLElement;
private _imageLocalRadioButton: RadioButton;
private _editorImageLocationGroup: string = 'editorImageLocationGroup';
private _imageRemoteRadioButton: RadioButton;
private _imageUrlLabel: HTMLElement;
private _imageUrlInputBox: InputBox;
private _imageBrowseButton: HTMLAnchorElement;
private _imageEmbedLabel: HTMLElement;
private _imageEmbedCheckbox: Checkbox;
constructor(
title: string,
width: DialogWidth,
dialogProperties: IDialogProperties,
@IPathService private readonly _pathService: IPathService,
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IThemeService themeService: IThemeService,
@ILayoutService layoutService: ILayoutService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
) {
super(
title,
width,
dialogProperties,
themeService,
layoutService,
telemetryService,
contextKeyService,
clipboardService,
logService,
textResourcePropertiesService
);
}
/**
* Opens the dialog and returns a promise for what options the user chooses.
*/
public open(): Promise<IImageCalloutDialogOptions> {
this.show();
return this._selectionComplete.promise;
}
public render(): void {
super.render();
attachModalDialogStyler(this, this._themeService);
this.addFooterButton(constants.insertButtonText, () => this.insert());
this.addFooterButton(constants.cancelButtonText, () => this.cancel(), undefined, true);
this.registerListeners();
}
protected renderBody(container: HTMLElement) {
let imageContentColumn = DOM.$('.column.insert-image');
DOM.append(container, imageContentColumn);
let locationRow = DOM.$('.row');
DOM.append(imageContentColumn, locationRow);
this._imageLocationLabel = DOM.$('p');
this._imageLocationLabel.innerText = constants.locationLabel;
DOM.append(locationRow, this._imageLocationLabel);
let radioButtonGroup = DOM.$('.radio-group');
this._imageLocalRadioButton = new RadioButton(radioButtonGroup, {
label: constants.localImageLabel,
enabled: true,
checked: true
});
this._imageRemoteRadioButton = new RadioButton(radioButtonGroup, {
label: constants.remoteImageLabel,
enabled: true,
checked: false
});
this._imageLocalRadioButton.name = this._editorImageLocationGroup;
this._imageLocalRadioButton.value = constants.locationLocal;
this._imageRemoteRadioButton.name = this._editorImageLocationGroup;
this._imageRemoteRadioButton.value = constants.locationRemote;
DOM.append(locationRow, radioButtonGroup);
let pathRow = DOM.$('.row');
DOM.append(imageContentColumn, pathRow);
this._imageUrlLabel = DOM.$('p');
if (this._imageLocalRadioButton.checked === true) {
this._imageUrlLabel.innerText = constants.pathPlaceholder;
} else {
this._imageUrlLabel.innerText = constants.urlPlaceholder;
}
DOM.append(pathRow, this._imageUrlLabel);
let inputContainer = DOM.$('.flex-container');
this._imageUrlInputBox = new InputBox(
inputContainer,
this._contextViewService,
{
placeholder: constants.pathPlaceholder,
ariaLabel: constants.pathInputLabel
});
let browseButtonContainer = DOM.$('.button-icon');
this._imageBrowseButton = DOM.$('a.codicon.masked-icon.browse-local');
this._imageBrowseButton.title = constants.browseAltText;
DOM.append(inputContainer, browseButtonContainer);
DOM.append(browseButtonContainer, this._imageBrowseButton);
this._register(DOM.addDisposableListener(this._imageBrowseButton, DOM.EventType.CLICK, async () => {
let selectedUri = await this.handleBrowse();
if (selectedUri) {
this._imageUrlInputBox.value = selectedUri.fsPath;
}
}, 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');
DOM.append(imageContentColumn, embedRow);
this._imageEmbedLabel = DOM.append(embedRow, DOM.$('.checkbox'));
this._imageEmbedCheckbox = new Checkbox(
this._imageEmbedLabel,
{
label: constants.embedImageLabel,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: constants.embedImageLabel
});
DOM.append(embedRow, this._imageEmbedLabel);
}
private registerListeners(): void {
this._register(styler.attachInputBoxStyler(this._imageUrlInputBox, this._themeService));
this._register(styler.attachCheckboxStyler(this._imageEmbedCheckbox, this._themeService));
}
public insert(): void {
this.hide();
this._selectionComplete.resolve({
insertMarkup: `<img src="${strings.escape(this._imageUrlInputBox.value)}">`,
imagePath: this._imageUrlInputBox.value,
embedImage: this._imageEmbedCheckbox.checked
});
this.dispose();
}
public cancel(): void {
super.cancel();
this._selectionComplete.resolve({
insertMarkup: '',
imagePath: undefined,
embedImage: undefined
});
}
private async getUserHome(): Promise<string> {
const userHomeUri = await this._pathService.userHome();
return userHomeUri.path;
}
private async handleBrowse(): Promise<URI | undefined> {
let options: IOpenDialogOptions = {
openLabel: undefined,
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: URI.file(await this.getUserHome()),
title: undefined
};
let imageUri: URI[] = await this._fileDialogService.showOpenDialog(options);
if (imageUri.length > 0) {
return imageUri[0];
} else {
return undefined;
}
}
}

View File

@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* 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!./media/linkCalloutDialog';
import * as DOM from 'vs/base/browser/dom';
import * as strings from 'vs/base/common/strings';
import * as styler from 'vs/platform/theme/common/styler';
import * as constants from 'sql/workbench/contrib/notebook/browser/calloutDialog/common/constants';
import { CalloutDialog } from 'sql/workbench/browser/modal/calloutDialog';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IDialogProperties } from 'sql/workbench/browser/modal/modal';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ILogService } from 'vs/platform/log/common/log';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { Deferred } from 'sql/base/common/promise';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { DialogWidth } from 'sql/workbench/api/common/sqlExtHostTypes';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
export interface ILinkCalloutDialogOptions {
insertTitle?: string,
insertMarkup?: string
}
export class LinkCalloutDialog extends CalloutDialog<ILinkCalloutDialogOptions> {
private _selectionComplete: Deferred<ILinkCalloutDialogOptions> = new Deferred<ILinkCalloutDialogOptions>();
private _linkTextLabel: HTMLElement;
private _linkTextInputBox: InputBox;
private _linkAddressLabel: HTMLElement;
private _linkUrlInputBox: InputBox;
constructor(
title: string,
width: DialogWidth,
dialogProperties: IDialogProperties,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IThemeService themeService: IThemeService,
@ILayoutService layoutService: ILayoutService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
) {
super(
title,
width,
dialogProperties,
themeService,
layoutService,
telemetryService,
contextKeyService,
clipboardService,
logService,
textResourcePropertiesService
);
}
/**
* Opens the dialog and returns a promise for what options the user chooses.
*/
public open(): Promise<ILinkCalloutDialogOptions> {
this.show();
return this._selectionComplete.promise;
}
public render(): void {
super.render();
attachModalDialogStyler(this, this._themeService);
this.addFooterButton(constants.insertButtonText, () => this.insert());
this.addFooterButton(constants.cancelButtonText, () => this.cancel(), undefined, true);
this.registerListeners();
}
protected renderBody(container: HTMLElement) {
let linkContentColumn = DOM.$('.column.insert-link');
DOM.append(container, linkContentColumn);
let linkTextRow = DOM.$('.row');
DOM.append(linkContentColumn, linkTextRow);
this._linkTextLabel = DOM.$('p');
this._linkTextLabel.innerText = constants.linkTextLabel;
DOM.append(linkTextRow, this._linkTextLabel);
const linkTextInputContainer = DOM.$('.input-field');
this._linkTextInputBox = new InputBox(
linkTextInputContainer,
this._contextViewService,
{
placeholder: constants.linkTextPlaceholder,
ariaLabel: constants.linkTextLabel
});
DOM.append(linkTextRow, linkTextInputContainer);
let linkAddressRow = DOM.$('.row');
DOM.append(linkContentColumn, linkAddressRow);
this._linkAddressLabel = DOM.$('p');
this._linkAddressLabel.innerText = constants.linkAddressLabel;
DOM.append(linkAddressRow, this._linkAddressLabel);
const linkAddressInputContainer = DOM.$('.input-field');
this._linkUrlInputBox = new InputBox(
linkAddressInputContainer,
this._contextViewService,
{
placeholder: constants.linkAddressPlaceholder,
ariaLabel: constants.linkAddressLabel
});
DOM.append(linkAddressRow, linkAddressInputContainer);
}
private registerListeners(): void {
this._register(styler.attachInputBoxStyler(this._linkTextInputBox, this._themeService));
this._register(styler.attachInputBoxStyler(this._linkUrlInputBox, this._themeService));
}
public insert(): void {
this.hide();
this._selectionComplete.resolve({
insertMarkup: `<a href="${strings.escape(this._linkUrlInputBox.value)}">${strings.escape(this._linkTextInputBox.value)}</a>`,
});
this.dispose();
}
public cancel(): void {
super.cancel();
this._selectionComplete.resolve({
insertMarkup: ''
});
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.modal.callout-dialog .modal-content .insert-image .flex-container {
display: flex;
}
.modal.callout-dialog .modal-content .insert-image .flex-container > div {
flex: 1;
min-width: 380px;
}
.modal.callout-dialog .modal-content .radio-group input {
margin-right: 8px;
}
.modal.callout-dialog .modal-content .radio-group span {
margin-right: 15px;
}

View File

@@ -0,0 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View File

@@ -16,7 +16,8 @@ import { Selection } from 'vs/editor/common/core/selection';
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';
import { CalloutDialog, CalloutType } from 'sql/workbench/browser/modal/calloutDialog';
import { ImageCalloutDialog } from 'sql/workbench/contrib/notebook/browser/calloutDialog/imageCalloutDialog';
import { LinkCalloutDialog } from 'sql/workbench/contrib/notebook/browser/calloutDialog/linkCalloutDialog';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DialogWidth } from 'sql/workbench/api/common/sqlExtHostTypes';
@@ -130,7 +131,8 @@ export class TransformMarkdownAction extends Action {
}
export class MarkdownTextTransformer {
private _callout: CalloutDialog;
private _imageCallout: ImageCalloutDialog;
private _linkCallout: LinkCalloutDialog;
private readonly insertLinkHeading = localize('callout.insertLinkHeading', "Insert link");
private readonly insertImageHeading = localize('callout.insertImageHeading', "Insert image");
@@ -208,24 +210,28 @@ export class MarkdownTextTransformer {
const triggerPosY = triggerElement.getBoundingClientRect().top;
const triggerHeight = triggerElement.offsetHeight;
const triggerWidth = triggerElement.offsetWidth;
const dialogProperties = { xPos: triggerPosX, yPos: triggerPosY, width: triggerWidth, height: triggerHeight };
let calloutOptions;
/**
* Width value here reflects designs for Notebook callouts.
*/
const width: DialogWidth = 452;
const calloutType: CalloutType = type === MarkdownButtonType.IMAGE_PREVIEW ? 'IMAGE' : 'LINK';
let title = type === MarkdownButtonType.IMAGE_PREVIEW ? this.insertImageHeading : this.insertLinkHeading;
if (!this._callout) {
const dialogProperties = { xPos: triggerPosX, yPos: triggerPosY, width: triggerWidth, height: triggerHeight };
this._callout = this._instantiationService.createInstance(CalloutDialog, calloutType, title, width, dialogProperties);
this._callout.render();
if (type === MarkdownButtonType.IMAGE_PREVIEW) {
if (!this._imageCallout) {
this._imageCallout = this._instantiationService.createInstance(ImageCalloutDialog, this.insertImageHeading, width, dialogProperties);
this._imageCallout.render();
calloutOptions = await this._imageCallout.open();
calloutOptions.insertTitle = this.insertImageHeading;
}
} else {
if (!this._linkCallout) {
this._linkCallout = this._instantiationService.createInstance(LinkCalloutDialog, this.insertLinkHeading, width, dialogProperties);
this._linkCallout.render();
calloutOptions = await this._linkCallout.open();
calloutOptions.insertTitle = this.insertLinkHeading;
}
}
let calloutOptions = await this._callout.open();
calloutOptions.insertTitle = title;
calloutOptions.calloutType = calloutType;
return calloutOptions.insertMarkup;
}