From 9e02cf86a4a2a63b751bb6ad73ce7418cfe89f0e Mon Sep 17 00:00:00 2001 From: Hale Rankin Date: Thu, 25 Feb 2021 14:44:25 -0800 Subject: [PATCH] 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. --- .../workbench/browser/modal/calloutDialog.ts | 280 +----------------- .../browser/modal/media/calloutDialog.css | 98 ++++++ .../workbench/browser/modal/media/modal.css | 111 ------- .../browser/calloutDialog/common/constants.ts | 25 ++ .../calloutDialog/imageCalloutDialog.ts | 227 ++++++++++++++ .../calloutDialog/linkCalloutDialog.ts | 138 +++++++++ .../media/imageCalloutDialog.css | 19 ++ .../calloutDialog/media/linkCalloutDialog.css | 5 + .../browser/markdownToolbarActions.ts | 34 ++- 9 files changed, 539 insertions(+), 398 deletions(-) create mode 100644 src/sql/workbench/browser/modal/media/calloutDialog.css create mode 100644 src/sql/workbench/contrib/notebook/browser/calloutDialog/common/constants.ts create mode 100644 src/sql/workbench/contrib/notebook/browser/calloutDialog/imageCalloutDialog.ts create mode 100644 src/sql/workbench/contrib/notebook/browser/calloutDialog/linkCalloutDialog.ts create mode 100644 src/sql/workbench/contrib/notebook/browser/calloutDialog/media/imageCalloutDialog.css create mode 100644 src/sql/workbench/contrib/notebook/browser/calloutDialog/media/linkCalloutDialog.css diff --git a/src/sql/workbench/browser/modal/calloutDialog.ts b/src/sql/workbench/browser/modal/calloutDialog.ts index 822ca01d98..2d4619d3ed 100644 --- a/src/sql/workbench/browser/modal/calloutDialog.ts +++ b/src/sql/workbench/browser/modal/calloutDialog.ts @@ -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; - // 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 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(); - this._calloutType = calloutType; } - /** - * Opens the dialog and returns a promise for what options the user chooses. - */ - public open(): Promise { - this.show(); - return this._selectionComplete.promise; - } + protected abstract renderBody(container: HTMLElement): void; - public render() { - super.render(); + public abstract open(): Promise; - 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: ``, - imagePath: this._imageUrlInputBox.value, - embedImage: this._imageEmbedCheckbox.checked - }); - } - if (this._calloutType === 'LINK') { - this._selectionComplete.resolve({ - insertMarkup: `${strings.escape(this._linkTextInputBox.value)}`, - }); - } - this.dispose(); - } - - public cancel() { - this.hide(); - this._selectionComplete.resolve({ - insertMarkup: '', - imagePath: undefined, - embedImage: undefined - }); - this.dispose(); - } - - private async getUserHome(): Promise { - const userHomeUri = await this._pathService.userHome(); - return userHomeUri.path; - } - - private async handleBrowse(): Promise { - 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; - } - } } diff --git a/src/sql/workbench/browser/modal/media/calloutDialog.css b/src/sql/workbench/browser/modal/media/calloutDialog.css new file mode 100644 index 0000000000..66498918fa --- /dev/null +++ b/src/sql/workbench/browser/modal/media/calloutDialog.css @@ -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; +} diff --git a/src/sql/workbench/browser/modal/media/modal.css b/src/sql/workbench/browser/modal/media/modal.css index 107c68e499..f6a0646676 100644 --- a/src/sql/workbench/browser/modal/media/modal.css +++ b/src/sql/workbench/browser/modal/media/modal.css @@ -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; diff --git a/src/sql/workbench/contrib/notebook/browser/calloutDialog/common/constants.ts b/src/sql/workbench/contrib/notebook/browser/calloutDialog/common/constants.ts new file mode 100644 index 0000000000..d1366b7040 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/calloutDialog/common/constants.ts @@ -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"); diff --git a/src/sql/workbench/contrib/notebook/browser/calloutDialog/imageCalloutDialog.ts b/src/sql/workbench/contrib/notebook/browser/calloutDialog/imageCalloutDialog.ts new file mode 100644 index 0000000000..09aca869d3 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/calloutDialog/imageCalloutDialog.ts @@ -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 { + private _selectionComplete: Deferred = new Deferred(); + 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 { + 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: ``, + 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 { + const userHomeUri = await this._pathService.userHome(); + return userHomeUri.path; + } + + private async handleBrowse(): Promise { + 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; + } + } + +} diff --git a/src/sql/workbench/contrib/notebook/browser/calloutDialog/linkCalloutDialog.ts b/src/sql/workbench/contrib/notebook/browser/calloutDialog/linkCalloutDialog.ts new file mode 100644 index 0000000000..d583d2d73a --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/calloutDialog/linkCalloutDialog.ts @@ -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 { + private _selectionComplete: Deferred = new Deferred(); + 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 { + 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: `${strings.escape(this._linkTextInputBox.value)}`, + }); + this.dispose(); + } + + public cancel(): void { + super.cancel(); + this._selectionComplete.resolve({ + insertMarkup: '' + }); + } +} diff --git a/src/sql/workbench/contrib/notebook/browser/calloutDialog/media/imageCalloutDialog.css b/src/sql/workbench/contrib/notebook/browser/calloutDialog/media/imageCalloutDialog.css new file mode 100644 index 0000000000..b9d19e81a3 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/calloutDialog/media/imageCalloutDialog.css @@ -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; +} + diff --git a/src/sql/workbench/contrib/notebook/browser/calloutDialog/media/linkCalloutDialog.css b/src/sql/workbench/contrib/notebook/browser/calloutDialog/media/linkCalloutDialog.css new file mode 100644 index 0000000000..e99b7c43cc --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/calloutDialog/media/linkCalloutDialog.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + diff --git a/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts b/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts index 9457b3f424..a1dd9dcdb7 100644 --- a/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts @@ -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; }