Callout Dialog Fixes + WYSIWYG Improvements for Insert Link (#14494)

* wip

* Works in all edit modes

* Default value set

* wip

* preventdefault

* cleanup, add tests

* markup -> markdown

* Ensure selection is persisted for WYSIWYG

* Add simple dialog tests and some PR feedback

* floating promise

* PR comments, formatted markdown refactor

* Change escaping logic + PR comments

* PR feedback
This commit is contained in:
Chris LaFreniere
2021-03-04 12:51:13 -08:00
committed by GitHub
parent 0141db80bc
commit 69a35b38b2
9 changed files with 356 additions and 91 deletions

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as strings from 'vs/base/common/strings';
/**
* Escape string to be used as label in markdown link
* @param unescapedLabel label to escape
*/
export function escapeLabel(unescapedLabel: string): string {
let firstEscape = strings.escape(unescapedLabel);
return firstEscape.replace(/[[]]/g, function (match) {
switch (match) {
case '[': return '\[';
case ']': return '\]';
default: return match;
}
});
}
/**
* Escape string to be used as url in markdown link
* @param unescapedUrl url to escapes
*/
export function escapeUrl(unescapedUrl: string): string {
let firstEscape = strings.escape(unescapedUrl);
return firstEscape.replace(/[()]/g, function (match) {
switch (match) {
case '(': return '%28';
case ')': return '%29';
default: return match;
}
});
}

View File

@@ -5,7 +5,6 @@
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';
@@ -27,10 +26,11 @@ 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';
import { escapeUrl } from 'sql/workbench/contrib/notebook/browser/calloutDialog/common/utils';
export interface IImageCalloutDialogOptions {
insertTitle?: string,
insertMarkup?: string,
insertEscapedMarkdown?: string,
imagePath?: string,
embedImage?: boolean
}
@@ -186,7 +186,7 @@ export class ImageCalloutDialog extends CalloutDialog<IImageCalloutDialogOptions
public insert(): void {
this.hide();
this._selectionComplete.resolve({
insertMarkup: `<img src="${strings.escape(this._imageUrlInputBox.value)}">`,
insertEscapedMarkdown: `![](${escapeUrl(this._imageUrlInputBox.value)})`,
imagePath: this._imageUrlInputBox.value,
embedImage: this._imageEmbedCheckbox.checked
});
@@ -196,7 +196,7 @@ export class ImageCalloutDialog extends CalloutDialog<IImageCalloutDialogOptions
public cancel(): void {
super.cancel();
this._selectionComplete.resolve({
insertMarkup: '',
insertEscapedMarkdown: '',
imagePath: undefined,
embedImage: undefined
});

View File

@@ -5,7 +5,6 @@
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';
@@ -20,12 +19,17 @@ 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';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { escapeLabel, escapeUrl } from 'sql/workbench/contrib/notebook/browser/calloutDialog/common/utils';
const DEFAULT_DIALOG_WIDTH = 452;
export interface ILinkCalloutDialogOptions {
insertTitle?: string,
insertMarkup?: string
insertEscapedMarkdown?: string,
insertUnescapedLinkLabel?: string,
insertUnescapedLinkUrl?: string
}
export class LinkCalloutDialog extends CalloutDialog<ILinkCalloutDialogOptions> {
@@ -34,11 +38,12 @@ export class LinkCalloutDialog extends CalloutDialog<ILinkCalloutDialogOptions>
private _linkTextInputBox: InputBox;
private _linkAddressLabel: HTMLElement;
private _linkUrlInputBox: InputBox;
private _previouslySelectedRange: Range;
constructor(
title: string,
width: DialogWidth,
dialogProperties: IDialogProperties,
private readonly _defaultLabel: string = '',
@IContextViewService private readonly _contextViewService: IContextViewService,
@IThemeService themeService: IThemeService,
@ILayoutService layoutService: ILayoutService,
@@ -50,7 +55,7 @@ export class LinkCalloutDialog extends CalloutDialog<ILinkCalloutDialogOptions>
) {
super(
title,
width,
DEFAULT_DIALOG_WIDTH,
dialogProperties,
themeService,
layoutService,
@@ -60,12 +65,17 @@ export class LinkCalloutDialog extends CalloutDialog<ILinkCalloutDialogOptions>
logService,
textResourcePropertiesService
);
let selection = window.getSelection();
if (selection.rangeCount > 0) {
this._previouslySelectedRange = selection?.getRangeAt(0);
}
}
/**
* Opens the dialog and returns a promise for what options the user chooses.
*/
public open(): Promise<ILinkCalloutDialogOptions> {
this._selectionComplete = new Deferred<ILinkCalloutDialogOptions>();
this.show();
return this._selectionComplete.promise;
}
@@ -97,6 +107,7 @@ export class LinkCalloutDialog extends CalloutDialog<ILinkCalloutDialogOptions>
placeholder: constants.linkTextPlaceholder,
ariaLabel: constants.linkTextLabel
});
this._linkTextInputBox.value = this._defaultLabel;
DOM.append(linkTextRow, linkTextInputContainer);
let linkAddressRow = DOM.$('.row');
@@ -121,18 +132,45 @@ export class LinkCalloutDialog extends CalloutDialog<ILinkCalloutDialogOptions>
this._register(styler.attachInputBoxStyler(this._linkUrlInputBox, this._themeService));
}
protected onAccept(e?: StandardKeyboardEvent) {
// EventHelper.stop() will call preventDefault. Without it, text cell will insert an extra newline when pressing enter on dialog
DOM.EventHelper.stop(e, true);
this.insert();
}
protected onClose(e?: StandardKeyboardEvent) {
DOM.EventHelper.stop(e, true);
this.cancel();
}
public insert(): void {
this.hide();
this._selectionComplete.resolve({
insertMarkup: `<a href="${strings.escape(this._linkUrlInputBox.value)}">${strings.escape(this._linkTextInputBox.value)}</a>`,
});
this.dispose();
let escapedLabel = escapeLabel(this._linkTextInputBox.value);
let escapedUrl = escapeUrl(this._linkUrlInputBox.value);
if (this._previouslySelectedRange) {
// Reset selection to previous state before callout was open
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(this._previouslySelectedRange);
this._selectionComplete.resolve({
insertEscapedMarkdown: `[${escapedLabel}](${escapedUrl})`,
insertUnescapedLinkLabel: this._linkTextInputBox.value,
insertUnescapedLinkUrl: this._linkUrlInputBox.value
});
}
}
public cancel(): void {
super.cancel();
this._selectionComplete.resolve({
insertMarkup: ''
insertEscapedMarkdown: '',
insertUnescapedLinkLabel: escapeLabel(this._linkTextInputBox.value)
});
}
public set url(val: string) {
this._linkUrlInputBox.value = val;
}
}