feedback.ts 12.2 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 14
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';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
15
import product from 'vs/platform/node/product';
B
Benjamin Pasero 已提交
16
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
17
import { ICommandService } from 'vs/platform/commands/common/commands';
B
Benjamin Pasero 已提交
18
import * as errors from 'vs/base/common/errors';
J
Johannes Rieken 已提交
19
import { IIntegrityService } from 'vs/platform/integrity/common/integrity';
20 21 22
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
import { editorWidgetBackground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, buttonBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
S
Sofian Hnaide 已提交
23 24 25 26 27 28 29

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

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

export interface IFeedbackDropdownOptions {
	contextViewProvider: IContextViewService;
	feedbackService?: IFeedbackService;
}

enum FormEvent {
	SENDING,
	SENT,
	SEND_ERROR
B
Benjamin Pasero 已提交
43
}
S
Sofian Hnaide 已提交
44 45

export class FeedbackDropdown extends Dropdown {
46
	protected maxFeedbackCharacters: number;
S
Sofian Hnaide 已提交
47 48 49 50 51 52 53 54 55 56 57 58 59 60

	protected feedback: string;
	protected sentiment: number;
	protected aliasEnabled: boolean;
	protected isSendingFeedback: boolean;
	protected autoHideTimeout: number;

	protected feedbackService: IFeedbackService;

	protected feedbackForm: HTMLFormElement;
	protected feedbackDescriptionInput: HTMLTextAreaElement;
	protected smileyInput: Builder;
	protected frownyInput: Builder;
	protected sendButton: Builder;
61
	protected remainingCharacterCount: Builder;
S
Sofian Hnaide 已提交
62

S
Sofian Hnaide 已提交
63 64 65
	protected requestFeatureLink: string;
	protected reportIssueLink: string;

66 67
	private _isPure: boolean;

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

S
Sofian Hnaide 已提交
81 82 83 84
				return null;
			}
		});

85 86 87 88 89 90 91
		this._isPure = true;
		this.integrityService.isPure().then(result => {
			if (!result.isPure) {
				this._isPure = false;
			}
		});

92 93
		this.element.addClass('send-feedback');
		this.element.title(nls.localize('sendFeedback', "Tweet Feedback"));
S
Sofian Hnaide 已提交
94 95 96 97 98

		this.feedbackService = options.feedbackService;

		this.feedback = '';
		this.sentiment = 1;
99
		this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment);
S
Sofian Hnaide 已提交
100 101 102 103 104 105 106 107

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

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

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

109 110
		this.reportIssueLink = product.reportIssueUrl;
		this.requestFeatureLink = product.requestFeatureUrl;
S
Sofian Hnaide 已提交
111 112
	}

113
	protected renderContents(container: HTMLElement): IDisposable {
B
Benjamin Pasero 已提交
114
		const $form = $('form.feedback-form').attr({
S
Sofian Hnaide 已提交
115 116 117 118 119 120 121 122
			action: 'javascript:void(0);',
			tabIndex: '-1'
		}).appendTo(container);

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

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

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

125
		this.invoke($('div.cancel').attr('tabindex', '0'), () => {
S
Sofian Hnaide 已提交
126 127 128
			this.hide();
		}).appendTo($form);

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

B
Benjamin Pasero 已提交
131
		const $sentimentContainer = $('div').appendTo($content);
132 133 134 135 136 137
		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 已提交
138
		$('span').text(nls.localize("sentiment", "How was your experience?")).appendTo($sentimentContainer);
S
Sofian Hnaide 已提交
139

B
Benjamin Pasero 已提交
140
		const $feedbackSentiment = $('div.feedback-sentiment').appendTo($sentimentContainer);
S
Sofian Hnaide 已提交
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164

		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 已提交
165
		const $contactUs = $('div.contactus').appendTo($content);
S
Sofian Hnaide 已提交
166 167 168

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

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

B
Benjamin Pasero 已提交
171 172 173 174 175
		$('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);
				this.commandService.executeCommand('workbench.action.reportIssues').done(null, errors.onUnexpectedError);
			})
S
Sofian Hnaide 已提交
176 177
			.appendTo($contactUsContainer);

178
		$('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 已提交
179 180
			.appendTo($contactUsContainer);

181
		this.remainingCharacterCount = $('span.char-counter').text(this.getCharCountText(0));
S
Sofian Hnaide 已提交
182 183

		$('h3').text(nls.localize("tell us why?", "Tell us why?"))
184
			.append(this.remainingCharacterCount)
S
Sofian Hnaide 已提交
185
			.appendTo($form);
S
Sofian Hnaide 已提交
186 187

		this.feedbackDescriptionInput = <HTMLTextAreaElement>$('textarea.feedback-description').attr({
S
Sofian Hnaide 已提交
188
			rows: 3,
189
			maxlength: this.maxFeedbackCharacters,
S
Sofian Hnaide 已提交
190 191 192 193
			'aria-label': nls.localize("commentsHeader", "Comments")
		})
			.text(this.feedback).attr('required', 'required')
			.on('keyup', () => {
194
				this.updateCharCountText();
S
Sofian Hnaide 已提交
195 196 197
			})
			.appendTo($form).domFocus().getHTMLElement();

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

200
		this.sendButton = this.invoke($('input.send').type('submit').attr('disabled', '').value(nls.localize('tweet', "Tweet")).appendTo($buttons), () => {
S
Sofian Hnaide 已提交
201 202 203
			if (this.isSendingFeedback) {
				return;
			}
S
Sofian Hnaide 已提交
204
			this.onSubmit();
S
Sofian Hnaide 已提交
205 206
		});

B
Benjamin Pasero 已提交
207
		this.toDispose.push(attachStylerCallback(this.themeService, { widgetShadow, editorWidgetBackground, inputBackground, inputForeground, inputBorder, editorBackground, contrastBorder }, colors => {
208 209 210 211 212 213 214 215
			$form.style('background-color', colors.editorWidgetBackground);
			$form.style('box-shadow', colors.widgetShadow ? `0 2px 8px ${colors.widgetShadow}` : null);

			this.feedbackDescriptionInput.style.backgroundColor = colors.inputBackground;
			this.feedbackDescriptionInput.style.color = colors.inputForeground;
			this.feedbackDescriptionInput.style.border = `1px solid ${colors.inputBorder || 'transparent'}`;

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

S
Sofian Hnaide 已提交
219 220 221 222 223 224 225 226 227 228
		return {
			dispose: () => {
				this.feedbackForm = null;
				this.feedbackDescriptionInput = null;
				this.smileyInput = null;
				this.frownyInput = null;
			}
		};
	}

229
	private getCharCountText(charCount: number): string {
B
Benjamin Pasero 已提交
230 231
		const remaining = this.maxFeedbackCharacters - charCount;
		const text = (remaining === 1)
232 233 234 235 236 237
			? nls.localize("character left", "character left")
			: nls.localize("characters left", "characters left");

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

238 239 240 241 242
	private updateCharCountText(): void {
		this.remainingCharacterCount.text(this.getCharCountText(this.feedbackDescriptionInput.value.length));
		this.feedbackDescriptionInput.value ? this.sendButton.removeAttribute('disabled') : this.sendButton.attr('disabled', '');
	}

S
Sofian Hnaide 已提交
243 244 245 246 247 248 249 250 251 252 253 254 255
	protected setSentiment(smile: boolean): void {
		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');
		}
		this.sentiment = smile ? 1 : 0;
256 257
		this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment);
		this.updateCharCountText();
J
Joao Moreno 已提交
258
		$(this.feedbackDescriptionInput).attr({ maxlength: this.maxFeedbackCharacters });
S
Sofian Hnaide 已提交
259 260 261 262 263 264
	}

	protected invoke(element: Builder, callback: () => void): Builder {
		element.on('click', callback);
		element.on('keypress', (e) => {
			if (e instanceof KeyboardEvent) {
B
Benjamin Pasero 已提交
265
				const keyboardEvent = <KeyboardEvent>e;
S
Sofian Hnaide 已提交
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
				if (keyboardEvent.keyCode === 13 || keyboardEvent.keyCode === 32) { // Enter or Spacebar
					callback();
				}
			}
		});
		return element;
	}

	public hide(): void {
		if (this.feedbackDescriptionInput) {
			this.feedback = this.feedbackDescriptionInput.value;
		}

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

		super.hide();
	}

	public onEvent(e: Event, activeElement: HTMLElement): void {
		if (e instanceof KeyboardEvent) {
B
Benjamin Pasero 已提交
289
			const keyboardEvent = <KeyboardEvent>e;
S
Sofian Hnaide 已提交
290 291 292 293 294 295
			if (keyboardEvent.keyCode === 27) { // Escape
				this.hide();
			}
		}
	}

S
Sofian Hnaide 已提交
296
	protected onSubmit(): void {
S
Sofian Hnaide 已提交
297
		if ((this.feedbackForm.checkValidity && !this.feedbackForm.checkValidity())) {
S
Sofian Hnaide 已提交
298
			return;
S
Sofian Hnaide 已提交
299 300 301 302
		}

		this.changeFormStatus(FormEvent.SENDING);

S
Sofian Hnaide 已提交
303
		this.feedbackService.submitFeedback({
S
Sofian Hnaide 已提交
304 305 306
			feedback: this.feedbackDescriptionInput.value,
			sentiment: this.sentiment
		});
S
Sofian Hnaide 已提交
307 308

		this.changeFormStatus(FormEvent.SENT);
S
Sofian Hnaide 已提交
309 310 311 312 313 314 315 316
	}


	private changeFormStatus(event: FormEvent): void {
		switch (event) {
			case FormEvent.SENDING:
				this.isSendingFeedback = true;
				this.sendButton.setClass('send in-progress');
317
				this.sendButton.value(nls.localize('feedbackSending', "Sending"));
S
Sofian Hnaide 已提交
318 319 320
				break;
			case FormEvent.SENT:
				this.isSendingFeedback = false;
321
				this.sendButton.setClass('send success').value(nls.localize('feedbackSent', "Thanks"));
S
Sofian Hnaide 已提交
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
				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;
		}
	}

	protected resetForm(): void {
B
Benjamin Pasero 已提交
340 341 342
		if (this.feedbackDescriptionInput) {
			this.feedbackDescriptionInput.value = '';
		}
S
Sofian Hnaide 已提交
343
		this.sentiment = 1;
344
		this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment);
S
Sofian Hnaide 已提交
345 346 347
		this.aliasEnabled = false;
	}
}
348 349 350 351 352 353 354 355

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 已提交
356 357 358 359 360 361

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