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

@@ -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;
}