feedback.ts 14.5 KB
Newer Older
S
Sofian Hnaide 已提交
1 2 3 4 5 6 7 8 9
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import 'vs/css!./media/feedback';
import nls = require('vs/nls');
J
Johannes Rieken 已提交
10 11 12 13
import { IDisposable } from 'vs/base/common/lifecycle';
import { Builder, $ } from 'vs/base/browser/builder';
import { Dropdown } from 'vs/base/browser/ui/dropdown/dropdown';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
14
import product from 'vs/platform/node/product';
B
Benjamin Pasero 已提交
15
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
16
import { ICommandService } from 'vs/platform/commands/common/commands';
B
Benjamin Pasero 已提交
17
import * as errors from 'vs/base/common/errors';
J
Johannes Rieken 已提交
18
import { IIntegrityService } from 'vs/platform/integrity/common/integrity';
19 20
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
21
import { editorWidgetBackground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, buttonBackground, contrastBorder, darken } from 'vs/platform/theme/common/colorRegistry';
22
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
23
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
24
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
25 26

export const FEEDBACK_VISIBLE_CONFIG = 'workbench.statusBar.feedback.visible';
S
Sofian Hnaide 已提交
27 28 29 30 31 32 33

export interface IFeedback {
	feedback: string;
	sentiment: number;
}

export interface IFeedbackService {
S
Sofian Hnaide 已提交
34
	submitFeedback(feedback: IFeedback): void;
35
	getCharacterLimit(sentiment: number): number;
S
Sofian Hnaide 已提交
36 37 38 39 40
}

export interface IFeedbackDropdownOptions {
	contextViewProvider: IContextViewService;
	feedbackService?: IFeedbackService;
41
	onFeedbackVisibilityChange?: (visible: boolean) => void;
S
Sofian Hnaide 已提交
42 43 44 45 46 47
}

enum FormEvent {
	SENDING,
	SENT,
	SEND_ERROR
B
Benjamin Pasero 已提交
48
}
S
Sofian Hnaide 已提交
49 50

export class FeedbackDropdown extends Dropdown {
51
	private maxFeedbackCharacters: number;
S
Sofian Hnaide 已提交
52

53 54 55 56
	private feedback: string;
	private sentiment: number;
	private isSendingFeedback: boolean;
	private autoHideTimeout: number;
S
Sofian Hnaide 已提交
57

58
	private feedbackService: IFeedbackService;
S
Sofian Hnaide 已提交
59

60 61 62 63 64 65 66
	private feedbackForm: HTMLFormElement;
	private feedbackDescriptionInput: HTMLTextAreaElement;
	private smileyInput: Builder;
	private frownyInput: Builder;
	private sendButton: Builder;
	private hideButton: HTMLInputElement;
	private remainingCharacterCount: Builder;
S
Sofian Hnaide 已提交
67

68
	private requestFeatureLink: string;
S
Sofian Hnaide 已提交
69

70 71
	private _isPure: boolean;

S
Sofian Hnaide 已提交
72 73
	constructor(
		container: HTMLElement,
74
		private options: IFeedbackDropdownOptions,
B
Benjamin Pasero 已提交
75
		@ICommandService private commandService: ICommandService,
76
		@ITelemetryService private telemetryService: ITelemetryService,
77 78 79
		@IIntegrityService private integrityService: IIntegrityService,
		@IThemeService private themeService: IThemeService,
		@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService
S
Sofian Hnaide 已提交
80 81 82 83
	) {
		super(container, {
			contextViewProvider: options.contextViewProvider,
			labelRenderer: (container: HTMLElement): IDisposable => {
84
				$(container).addClass('send-feedback', 'mask-icon');
85

S
Sofian Hnaide 已提交
86 87 88 89
				return null;
			}
		});

90 91 92 93 94 95 96
		this._isPure = true;
		this.integrityService.isPure().then(result => {
			if (!result.isPure) {
				this._isPure = false;
			}
		});

97 98
		this.element.addClass('send-feedback');
		this.element.title(nls.localize('sendFeedback', "Tweet Feedback"));
S
Sofian Hnaide 已提交
99 100 101 102 103

		this.feedbackService = options.feedbackService;

		this.feedback = '';
		this.sentiment = 1;
104
		this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment);
S
Sofian Hnaide 已提交
105 106 107 108 109 110 111 112

		this.feedbackForm = null;
		this.feedbackDescriptionInput = null;

		this.smileyInput = null;
		this.frownyInput = null;

		this.sendButton = null;
S
Sofian Hnaide 已提交
113

114
		this.requestFeatureLink = product.sendASmile.requestFeatureUrl;
S
Sofian Hnaide 已提交
115 116
	}

117 118 119 120 121 122 123 124 125 126 127
	protected getAnchor(): HTMLElement | IAnchor {
		const res = dom.getDomNodePagePosition(this.element.getHTMLElement());

		return {
			x: res.left,
			y: res.top - 9, /* above the status bar */
			width: res.width,
			height: res.height
		} as IAnchor;
	}

128
	protected renderContents(container: HTMLElement): IDisposable {
B
Benjamin Pasero 已提交
129
		const $form = $('form.feedback-form').attr({
130
			action: 'javascript:void(0);'
S
Sofian Hnaide 已提交
131 132 133 134 135 136
		}).appendTo(container);

		$(container).addClass('monaco-menu-container');

		this.feedbackForm = <HTMLFormElement>$form.getHTMLElement();

137
		$('h2.title').text(nls.localize("label.sendASmile", "Tweet us your feedback.")).appendTo($form);
S
Sofian Hnaide 已提交
138

139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
		const cancelBtn = $('div.cancel').attr('tabindex', '0');
		cancelBtn.on(dom.EventType.MOUSE_OVER, () => {
			const theme = this.themeService.getTheme();
			let darkenFactor: number;
			switch (theme.type) {
				case 'light':
					darkenFactor = 0.1;
					break;
				case 'dark':
					darkenFactor = 0.2;
					break;
			}

			if (darkenFactor) {
				cancelBtn.getHTMLElement().style.backgroundColor = darken(theme.getColor(editorWidgetBackground), darkenFactor)(theme).toString();
			}
		});
		cancelBtn.on(dom.EventType.MOUSE_OUT, () => {
			cancelBtn.getHTMLElement().style.backgroundColor = null;
		});
		this.invoke(cancelBtn, () => {
S
Sofian Hnaide 已提交
160 161 162
			this.hide();
		}).appendTo($form);

B
Benjamin Pasero 已提交
163
		const $content = $('div.content').appendTo($form);
S
Sofian Hnaide 已提交
164

B
Benjamin Pasero 已提交
165
		const $sentimentContainer = $('div').appendTo($content);
166 167 168 169 170 171
		if (!this._isPure) {
			$('span').text(nls.localize("patchedVersion1", "Your installation is corrupt.")).appendTo($sentimentContainer);
			$('br').appendTo($sentimentContainer);
			$('span').text(nls.localize("patchedVersion2", "Please specify this if you submit a bug.")).appendTo($sentimentContainer);
			$('br').appendTo($sentimentContainer);
		}
S
Sofian Hnaide 已提交
172
		$('span').text(nls.localize("sentiment", "How was your experience?")).appendTo($sentimentContainer);
S
Sofian Hnaide 已提交
173

B
Benjamin Pasero 已提交
174
		const $feedbackSentiment = $('div.feedback-sentiment').appendTo($sentimentContainer);
S
Sofian Hnaide 已提交
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198

		this.smileyInput = $('div').addClass('sentiment smile').attr({
			'aria-checked': 'false',
			'aria-label': nls.localize('smileCaption', "Happy"),
			'tabindex': 0,
			'role': 'checkbox'
		});
		this.invoke(this.smileyInput, () => { this.setSentiment(true); }).appendTo($feedbackSentiment);

		this.frownyInput = $('div').addClass('sentiment frown').attr({
			'aria-checked': 'false',
			'aria-label': nls.localize('frownCaption', "Sad"),
			'tabindex': 0,
			'role': 'checkbox'
		});

		this.invoke(this.frownyInput, () => { this.setSentiment(false); }).appendTo($feedbackSentiment);

		if (this.sentiment === 1) {
			this.smileyInput.addClass('checked').attr('aria-checked', 'true');
		} else {
			this.frownyInput.addClass('checked').attr('aria-checked', 'true');
		}

B
Benjamin Pasero 已提交
199
		const $contactUs = $('div.contactus').appendTo($content);
S
Sofian Hnaide 已提交
200 201 202

		$('span').text(nls.localize("other ways to contact us", "Other ways to contact us")).appendTo($contactUs);

B
Benjamin Pasero 已提交
203
		const $contactUsContainer = $('div.channels').appendTo($contactUs);
S
Sofian Hnaide 已提交
204

B
Benjamin Pasero 已提交
205 206 207
		$('div').append($('a').attr('target', '_blank').attr('href', '#').text(nls.localize("submit a bug", "Submit a bug")).attr('tabindex', '0'))
			.on('click', event => {
				dom.EventHelper.stop(event);
208 209 210 211 212 213 214 215 216 217
				const actionId = 'workbench.action.openIssueReporter';
				this.commandService.executeCommand(actionId).done(null, errors.onUnexpectedError);

				/* __GDPR__
					"workbenchActionExecuted" : {
						"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
						"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
				this.telemetryService.publicLog('workbenchActionExecuted', { id: actionId, from: 'feedback' });
B
Benjamin Pasero 已提交
218
			})
S
Sofian Hnaide 已提交
219 220
			.appendTo($contactUsContainer);

221
		$('div').append($('a').attr('target', '_blank').attr('href', this.requestFeatureLink).text(nls.localize("request a missing feature", "Request a missing feature")).attr('tabindex', '0'))
S
Sofian Hnaide 已提交
222 223
			.appendTo($contactUsContainer);

224
		this.remainingCharacterCount = $('span.char-counter').text(this.getCharCountText(0));
S
Sofian Hnaide 已提交
225 226

		$('h3').text(nls.localize("tell us why?", "Tell us why?"))
227
			.append(this.remainingCharacterCount)
S
Sofian Hnaide 已提交
228
			.appendTo($form);
S
Sofian Hnaide 已提交
229 230

		this.feedbackDescriptionInput = <HTMLTextAreaElement>$('textarea.feedback-description').attr({
S
Sofian Hnaide 已提交
231
			rows: 3,
232
			maxlength: this.maxFeedbackCharacters,
S
Sofian Hnaide 已提交
233 234 235 236
			'aria-label': nls.localize("commentsHeader", "Comments")
		})
			.text(this.feedback).attr('required', 'required')
			.on('keyup', () => {
237
				this.updateCharCountText();
S
Sofian Hnaide 已提交
238 239 240
			})
			.appendTo($form).domFocus().getHTMLElement();

B
Benjamin Pasero 已提交
241
		const $buttons = $('div.form-buttons').appendTo($form);
S
Sofian Hnaide 已提交
242

B
Benjamin Pasero 已提交
243
		const $hideButtonContainer = $('div.hide-button-container').appendTo($buttons);
244 245 246 247 248

		this.hideButton = $('input.hide-button').type('checkbox').attr('checked', '').id('hide-button').appendTo($hideButtonContainer).getHTMLElement() as HTMLInputElement;

		$('label').attr('for', 'hide-button').text(nls.localize('showFeedback', "Show Feedback Smiley in Status Bar")).appendTo($hideButtonContainer);

249
		this.sendButton = this.invoke($('input.send').type('submit').attr('disabled', '').value(nls.localize('tweet', "Tweet")).appendTo($buttons), () => {
S
Sofian Hnaide 已提交
250 251 252
			if (this.isSendingFeedback) {
				return;
			}
253

S
Sofian Hnaide 已提交
254
			this.onSubmit();
S
Sofian Hnaide 已提交
255 256
		});

B
Benjamin Pasero 已提交
257
		this.toDispose.push(attachStylerCallback(this.themeService, { widgetShadow, editorWidgetBackground, inputBackground, inputForeground, inputBorder, editorBackground, contrastBorder }, colors => {
258
			$form.style('background-color', colors.editorWidgetBackground);
259
			$form.style('box-shadow', colors.widgetShadow ? `0 0 8px ${colors.widgetShadow}` : null);
260

261 262 263 264 265
			if (this.feedbackDescriptionInput) {
				this.feedbackDescriptionInput.style.backgroundColor = colors.inputBackground;
				this.feedbackDescriptionInput.style.color = colors.inputForeground;
				this.feedbackDescriptionInput.style.border = `1px solid ${colors.inputBorder || 'transparent'}`;
			}
266 267

			$contactUs.style('background-color', colors.editorBackground);
B
Benjamin Pasero 已提交
268
			$contactUs.style('border', `1px solid ${colors.contrastBorder || 'transparent'}`);
269 270
		}));

S
Sofian Hnaide 已提交
271 272 273 274 275 276 277 278 279 280
		return {
			dispose: () => {
				this.feedbackForm = null;
				this.feedbackDescriptionInput = null;
				this.smileyInput = null;
				this.frownyInput = null;
			}
		};
	}

281
	private getCharCountText(charCount: number): string {
B
Benjamin Pasero 已提交
282 283
		const remaining = this.maxFeedbackCharacters - charCount;
		const text = (remaining === 1)
284 285 286 287 288 289
			? nls.localize("character left", "character left")
			: nls.localize("characters left", "characters left");

		return '(' + remaining + ' ' + text + ')';
	}

290 291 292 293 294
	private updateCharCountText(): void {
		this.remainingCharacterCount.text(this.getCharCountText(this.feedbackDescriptionInput.value.length));
		this.feedbackDescriptionInput.value ? this.sendButton.removeAttribute('disabled') : this.sendButton.attr('disabled', '');
	}

295
	private setSentiment(smile: boolean): void {
S
Sofian Hnaide 已提交
296 297 298 299 300 301 302 303 304 305 306
		if (smile) {
			this.smileyInput.addClass('checked');
			this.smileyInput.attr('aria-checked', 'true');
			this.frownyInput.removeClass('checked');
			this.frownyInput.attr('aria-checked', 'false');
		} else {
			this.frownyInput.addClass('checked');
			this.frownyInput.attr('aria-checked', 'true');
			this.smileyInput.removeClass('checked');
			this.smileyInput.attr('aria-checked', 'false');
		}
307

S
Sofian Hnaide 已提交
308
		this.sentiment = smile ? 1 : 0;
309 310
		this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment);
		this.updateCharCountText();
J
Joao Moreno 已提交
311
		$(this.feedbackDescriptionInput).attr({ maxlength: this.maxFeedbackCharacters });
S
Sofian Hnaide 已提交
312 313
	}

314
	private invoke(element: Builder, callback: () => void): Builder {
S
Sofian Hnaide 已提交
315
		element.on('click', callback);
316

S
Sofian Hnaide 已提交
317 318
		element.on('keypress', (e) => {
			if (e instanceof KeyboardEvent) {
B
Benjamin Pasero 已提交
319
				const keyboardEvent = <KeyboardEvent>e;
S
Sofian Hnaide 已提交
320 321 322 323 324
				if (keyboardEvent.keyCode === 13 || keyboardEvent.keyCode === 32) { // Enter or Spacebar
					callback();
				}
			}
		});
325

S
Sofian Hnaide 已提交
326 327 328
		return element;
	}

329 330 331 332 333 334 335 336 337 338 339 340 341 342
	public show(): void {
		super.show();

		if (this.options.onFeedbackVisibilityChange) {
			this.options.onFeedbackVisibilityChange(true);
		}
	}

	protected onHide(): void {
		if (this.options.onFeedbackVisibilityChange) {
			this.options.onFeedbackVisibilityChange(false);
		}
	}

S
Sofian Hnaide 已提交
343 344 345 346 347 348 349 350 351 352
	public hide(): void {
		if (this.feedbackDescriptionInput) {
			this.feedback = this.feedbackDescriptionInput.value;
		}

		if (this.autoHideTimeout) {
			clearTimeout(this.autoHideTimeout);
			this.autoHideTimeout = null;
		}

B
Benjamin Pasero 已提交
353
		if (this.hideButton && !this.hideButton.checked) {
354 355 356
			this.configurationService.updateValue(FEEDBACK_VISIBLE_CONFIG, false).done(null, errors.onUnexpectedError);
		}

S
Sofian Hnaide 已提交
357 358 359 360 361
		super.hide();
	}

	public onEvent(e: Event, activeElement: HTMLElement): void {
		if (e instanceof KeyboardEvent) {
B
Benjamin Pasero 已提交
362
			const keyboardEvent = <KeyboardEvent>e;
S
Sofian Hnaide 已提交
363 364 365 366 367 368
			if (keyboardEvent.keyCode === 27) { // Escape
				this.hide();
			}
		}
	}

369
	private onSubmit(): void {
S
Sofian Hnaide 已提交
370
		if ((this.feedbackForm.checkValidity && !this.feedbackForm.checkValidity())) {
S
Sofian Hnaide 已提交
371
			return;
S
Sofian Hnaide 已提交
372 373 374 375
		}

		this.changeFormStatus(FormEvent.SENDING);

S
Sofian Hnaide 已提交
376
		this.feedbackService.submitFeedback({
S
Sofian Hnaide 已提交
377 378 379
			feedback: this.feedbackDescriptionInput.value,
			sentiment: this.sentiment
		});
S
Sofian Hnaide 已提交
380 381

		this.changeFormStatus(FormEvent.SENT);
S
Sofian Hnaide 已提交
382 383 384 385 386 387 388 389
	}


	private changeFormStatus(event: FormEvent): void {
		switch (event) {
			case FormEvent.SENDING:
				this.isSendingFeedback = true;
				this.sendButton.setClass('send in-progress');
390
				this.sendButton.value(nls.localize('feedbackSending', "Sending"));
S
Sofian Hnaide 已提交
391 392 393
				break;
			case FormEvent.SENT:
				this.isSendingFeedback = false;
394
				this.sendButton.setClass('send success').value(nls.localize('feedbackSent', "Thanks"));
S
Sofian Hnaide 已提交
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
				this.resetForm();
				this.autoHideTimeout = setTimeout(() => {
					this.hide();
				}, 1000);
				this.sendButton.off(['click', 'keypress']);
				this.invoke(this.sendButton, () => {
					this.hide();
					this.sendButton.off(['click', 'keypress']);
				});
				break;
			case FormEvent.SEND_ERROR:
				this.isSendingFeedback = false;
				this.sendButton.setClass('send error').value(nls.localize('feedbackSendingError', "Try again"));
				break;
		}
	}

412
	private resetForm(): void {
B
Benjamin Pasero 已提交
413 414 415
		if (this.feedbackDescriptionInput) {
			this.feedbackDescriptionInput.value = '';
		}
416

S
Sofian Hnaide 已提交
417
		this.sentiment = 1;
418
		this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment);
S
Sofian Hnaide 已提交
419 420
	}
}
421 422 423 424 425 426 427 428

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {

	// Sentiment Buttons
	const inputActiveOptionBorderColor = theme.getColor(inputActiveOptionBorder);
	if (inputActiveOptionBorderColor) {
		collector.addRule(`.monaco-shell .feedback-form .sentiment.checked { border: 1px solid ${inputActiveOptionBorderColor}; }`);
	}
B
Benjamin Pasero 已提交
429 430 431 432 433 434

	// Links
	const linkColor = theme.getColor(buttonBackground) || theme.getColor(contrastBorder);
	if (linkColor) {
		collector.addRule(`.monaco-shell .feedback-form .content .channels a { color: ${linkColor}; }`);
	}
435
});