| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import '/strings.m.js'; |
| import './feedback_shared_styles.css.js'; |
| // <if expr="is_chromeos"> |
| import './js/jelly_colors.js'; |
| |
| // </if> |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; |
| import {OpenWindowProxyImpl} from 'chrome://resources/js/open_window_proxy.js'; |
| import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| |
| import {getCss} from './app.css.js'; |
| import {getHtml} from './app.html.js'; |
| import {FeedbackBrowserProxyImpl} from './js/feedback_browser_proxy.js'; |
| import {BT_DEVICE_REGEX, BT_REGEX, CANNOT_CONNECT_REGEX, CELLULAR_REGEX, DISPLAY_REGEX, FAST_PAIR_REGEX, NEARBY_SHARE_REGEX, SMART_LOCK_REGEX, TETHER_REGEX, THUNDERBOLT_REGEX, USB_REGEX, WIFI_REGEX} from './js/feedback_regexes.js'; |
| import {FEEDBACK_LANDING_PAGE, FEEDBACK_LANDING_PAGE_TECHSTOP, FEEDBACK_LEGAL_HELP_URL, FEEDBACK_PRIVACY_POLICY_URL, FEEDBACK_TERM_OF_SERVICE_URL, openUrlInAppWindow} from './js/feedback_util.js'; |
| import {domainQuestions, questionnaireBegin, questionnaireNotification} from './js/questionnaire.js'; |
| import {takeScreenshot} from './js/take_screenshot.js'; |
| |
| const MAX_ATTACH_FILE_SIZE: number = 3 * 1024 * 1024; |
| const MAX_SCREENSHOT_WIDTH: number = 100; |
| |
| export class AppElement extends CrLitElement { |
| static get is() { |
| return 'feedback-app'; |
| } |
| |
| static override get styles() { |
| return getCss(); |
| } |
| |
| override render() { |
| return getHtml.bind(this)(); |
| } |
| |
| private formOpenTime: number = new Date().getTime(); |
| private attachedFileBlob: Blob|null = null; |
| |
| /** |
| * Which questions have been appended to the issue description text area. |
| */ |
| private appendedQuestions: {[key: string]: boolean} = {}; |
| |
| /** |
| * The object will be manipulated by sendReport(). |
| */ |
| private feedbackInfo: chrome.feedbackPrivate.FeedbackInfo = { |
| attachedFile: undefined, |
| attachedFileBlobUuid: undefined, |
| autofillMetadata: '', |
| categoryTag: undefined, |
| description: '...', |
| descriptionPlaceholder: undefined, |
| email: undefined, |
| flow: chrome.feedbackPrivate.FeedbackFlow.REGULAR, |
| fromAutofill: false, |
| includeBluetoothLogs: false, |
| pageUrl: undefined, |
| sendHistograms: undefined, |
| systemInformation: [], |
| useSystemWindowFrame: false, |
| isOffensiveOrUnsafe: undefined, |
| aiMetadata: undefined, |
| }; |
| |
| /** |
| * Initializes our page. |
| * Flow: |
| * .) DOMContent Loaded -> . Request feedbackInfo object |
| * . Setup page event handlers |
| * .) Feedback Object Received -> . take screenshot |
| * . request email |
| * . request System info |
| * . request i18n strings |
| * .) Screenshot taken -> . Show Feedback window. |
| */ |
| override async connectedCallback() { |
| super.connectedCallback(); |
| |
| // Initialize `browserProxy` only after tests had a chance to do setup |
| // steps, one of which is to replace the prod proxy with a test version. |
| // this.browserProxy = FeedbackBrowserProxyImpl.getInstance(); |
| |
| const dialogArgs = |
| FeedbackBrowserProxyImpl.getInstance().getDialogArguments(); |
| if (dialogArgs) { |
| this.feedbackInfo = JSON.parse(dialogArgs); |
| } |
| |
| await this.applyData(this.feedbackInfo); |
| |
| // Setup our event handlers. |
| this.getRequiredElement('#attach-file') |
| .addEventListener('change', (e: Event) => this.onFileSelected(e)); |
| this.getRequiredElement('#attach-file') |
| .addEventListener('click', this.onOpenFileDialog.bind(this)); |
| this.getRequiredElement('#send-report-button').onclick = |
| this.sendReport.bind(this); |
| this.getRequiredElement('#cancel-button').onclick = (e: Event) => |
| this.cancel(e); |
| this.getRequiredElement('#remove-attached-file').onclick = |
| this.clearAttachedFile.bind(this); |
| |
| // Dispatch event used by tests. |
| this.dispatchEvent(new CustomEvent('ready-for-testing')); |
| } |
| |
| /** |
| * Apply updates based on the received `FeedbackInfo` object. |
| * @return A promise signaling that all UI updates have finished. |
| */ |
| private applyData(feedbackInfo: chrome.feedbackPrivate.FeedbackInfo): |
| Promise<void> { |
| if (feedbackInfo.includeBluetoothLogs) { |
| assert( |
| feedbackInfo.flow === |
| chrome.feedbackPrivate.FeedbackFlow.GOOGLE_INTERNAL); |
| this.getRequiredElement('#description-text') |
| .addEventListener( |
| 'input', (e: Event) => this.checkForSendBluetoothLogs(e)); |
| } |
| |
| if (feedbackInfo.showQuestionnaire) { |
| assert( |
| feedbackInfo.flow === |
| chrome.feedbackPrivate.FeedbackFlow.GOOGLE_INTERNAL); |
| this.getRequiredElement('#description-text') |
| .addEventListener( |
| 'input', (e: Event) => this.checkForShowQuestionnaire(e)); |
| } |
| |
| if (this.shadowRoot.querySelector<HTMLElement>( |
| '#autofill-checkbox-container') != null && |
| feedbackInfo.flow === |
| chrome.feedbackPrivate.FeedbackFlow.GOOGLE_INTERNAL && |
| feedbackInfo.fromAutofill) { |
| this.getRequiredElement('#autofill-checkbox-container').hidden = false; |
| } |
| |
| this.getRequiredElement('#description-text').textContent = |
| feedbackInfo.description; |
| if (feedbackInfo.descriptionPlaceholder) { |
| this.getRequiredElement<HTMLTextAreaElement>('#description-text') |
| .placeholder = feedbackInfo.descriptionPlaceholder; |
| } |
| if (feedbackInfo.pageUrl) { |
| this.getRequiredElement<HTMLInputElement>('#page-url-text').value = |
| feedbackInfo.pageUrl; |
| } |
| |
| const isAiFlow: boolean = |
| feedbackInfo.flow === chrome.feedbackPrivate.FeedbackFlow.AI; |
| |
| if (isAiFlow) { |
| this.getRequiredElement('#free-form-text').textContent = |
| loadTimeData.getString('freeFormTextAi'); |
| this.getRequiredElement('#offensive-container').hidden = false; |
| this.getRequiredElement('#log-id-container').hidden = false; |
| } |
| |
| const isSeaPenFlow: boolean|undefined = |
| isAiFlow && feedbackInfo.aiMetadata?.includes('from_sea_pen'); |
| |
| if (isSeaPenFlow) { |
| this.getRequiredElement('#log-id-container').hidden = true; |
| this.getRequiredElement('#screenshot-container').hidden = true; |
| this.getRequiredElement('#sys-info-container').hidden = true; |
| } |
| |
| const whenScreenshotUpdated = takeScreenshot().then((screenshotCanvas) => { |
| // We've taken our screenshot, show the feedback page without any |
| // further delay. |
| window.requestAnimationFrame(this.resizeAppWindow.bind(this)); |
| |
| FeedbackBrowserProxyImpl.getInstance().showDialog(); |
| |
| // Allow feedback to be sent even if the screenshot failed. |
| if (!screenshotCanvas) { |
| const checkbox = |
| this.getRequiredElement<HTMLInputElement>('#screenshot-checkbox'); |
| checkbox.disabled = true; |
| checkbox.checked = false; |
| return Promise.resolve(); |
| } |
| |
| return new Promise<void>((resolve) => { |
| screenshotCanvas.toBlob((blob) => { |
| const image = |
| this.getRequiredElement<HTMLImageElement>('#screenshot-image'); |
| image.src = URL.createObjectURL(blob!); |
| // Only set the alt text when the src url is available, otherwise we'd |
| // get a broken image picture instead. crbug.com/773985. |
| image.alt = 'screenshot'; |
| image.classList.toggle( |
| 'wide-screen', image.width > MAX_SCREENSHOT_WIDTH); |
| feedbackInfo.screenshot = blob!; |
| resolve(); |
| }); |
| }); |
| }); |
| |
| const whenEmailUpdated = isAiFlow ? |
| Promise.resolve() : |
| FeedbackBrowserProxyImpl.getInstance().getUserEmail().then((email) => { |
| // Never add an empty option. |
| if (!email) { |
| return; |
| } |
| const optionElement = document.createElement('option'); |
| optionElement.value = email; |
| optionElement.text = email; |
| optionElement.selected = true; |
| // Make sure the "Report anonymously" option comes last. |
| this.getRequiredElement('#user-email-drop-down') |
| .insertBefore( |
| optionElement, |
| this.getRequiredElement('#anonymous-user-option')); |
| |
| // Now we can unhide the user email section: |
| this.getRequiredElement('#user-email').hidden = false; |
| // Only show email consent checkbox when an email address exists. |
| this.getRequiredElement('#consent-container').hidden = false; |
| }); |
| |
| // An extension called us with an attached file. |
| if (feedbackInfo.attachedFile) { |
| this.getRequiredElement('#attached-filename-text').textContent = |
| feedbackInfo.attachedFile.name; |
| this.attachedFileBlob = feedbackInfo.attachedFile.data!; |
| this.getRequiredElement('#custom-file-container').hidden = false; |
| this.getRequiredElement('#attach-file').hidden = true; |
| } |
| |
| // No URL, file attachment for login screen feedback. |
| if (feedbackInfo.flow === chrome.feedbackPrivate.FeedbackFlow.LOGIN) { |
| this.getRequiredElement('#page-url').hidden = true; |
| this.getRequiredElement('#attach-file-container').hidden = true; |
| this.getRequiredElement('#attach-file-note').hidden = true; |
| } |
| |
| const autofillMetadataUrlElement = |
| this.shadowRoot.querySelector<HTMLElement>('#autofill-metadata-url'); |
| |
| if (autofillMetadataUrlElement) { |
| // Opens a new window showing the full anonymized autofill metadata. |
| autofillMetadataUrlElement.onclick = (e: Event) => { |
| e.preventDefault(); |
| |
| FeedbackBrowserProxyImpl.getInstance().showAutofillMetadataInfo( |
| feedbackInfo.autofillMetadata!); |
| }; |
| |
| autofillMetadataUrlElement.onauxclick = (e: Event) => { |
| e.preventDefault(); |
| }; |
| } |
| |
| const sysInfoUrlElement = |
| this.shadowRoot.querySelector<HTMLElement>('#sys-info-url'); |
| if (sysInfoUrlElement) { |
| // Opens a new window showing the full anonymized system+app |
| // information. |
| sysInfoUrlElement.onclick = (e: Event) => { |
| e.preventDefault(); |
| |
| FeedbackBrowserProxyImpl.getInstance().showSystemInfo(); |
| }; |
| |
| sysInfoUrlElement.onauxclick = (e: Event) => { |
| e.preventDefault(); |
| }; |
| } |
| |
| const histogramUrlElement = |
| this.shadowRoot.querySelector<HTMLElement>('#histograms-url'); |
| if (histogramUrlElement) { |
| histogramUrlElement.onclick = (e: Event) => { |
| e.preventDefault(); |
| |
| FeedbackBrowserProxyImpl.getInstance().showMetrics(); |
| }; |
| |
| histogramUrlElement.onauxclick = (e: Event) => { |
| e.preventDefault(); |
| }; |
| } |
| |
| // The following URLs don't open on login screen, so hide them. |
| // TODO(crbug.com/40144717): Find a solution to display them properly. |
| // Update: the bluetooth and assistant logs links will work on login |
| // screen now. But to limit the scope of this CL, they are still hidden. |
| if (feedbackInfo.flow !== chrome.feedbackPrivate.FeedbackFlow.LOGIN) { |
| const legalHelpPageUrlElement = |
| this.shadowRoot.querySelector<HTMLElement>('#legal-help-page-url'); |
| if (legalHelpPageUrlElement) { |
| this.setupLinkHandlers( |
| legalHelpPageUrlElement, FEEDBACK_LEGAL_HELP_URL, |
| false /* useAppWindow */); |
| } |
| |
| const privacyPolicyUrlElement = |
| this.shadowRoot.querySelector<HTMLElement>('#privacy-policy-url'); |
| if (privacyPolicyUrlElement) { |
| this.setupLinkHandlers( |
| privacyPolicyUrlElement, FEEDBACK_PRIVACY_POLICY_URL, |
| false /* useAppWindow */); |
| } |
| |
| const termsOfServiceUrlElement = |
| this.shadowRoot.querySelector<HTMLElement>('#terms-of-service-url'); |
| if (termsOfServiceUrlElement) { |
| this.setupLinkHandlers( |
| termsOfServiceUrlElement, FEEDBACK_TERM_OF_SERVICE_URL, |
| false /* useAppWindow */); |
| } |
| } |
| |
| // Make sure our focus starts on the description field. |
| this.getRequiredElement('#description-text').focus(); |
| |
| return Promise.all([whenScreenshotUpdated, whenEmailUpdated]) |
| .then(() => {}); |
| } |
| |
| private async sendFeedbackReport(useSystemInfo: boolean) { |
| const ID = Math.round(Date.now() / 1000); |
| const FLOW = this.feedbackInfo.flow; |
| |
| const result = await FeedbackBrowserProxyImpl.getInstance().sendFeedback( |
| this.feedbackInfo, useSystemInfo, this.formOpenTime); |
| |
| if (result.status === chrome.feedbackPrivate.Status.SUCCESS) { |
| if (FLOW !== chrome.feedbackPrivate.FeedbackFlow.LOGIN && |
| result.landingPageType !== |
| chrome.feedbackPrivate.LandingPageType.NO_LANDING_PAGE) { |
| const landingPage = result.landingPageType === |
| chrome.feedbackPrivate.LandingPageType.NORMAL ? |
| FEEDBACK_LANDING_PAGE : |
| FEEDBACK_LANDING_PAGE_TECHSTOP; |
| OpenWindowProxyImpl.getInstance().openUrl(landingPage); |
| } |
| } else { |
| console.warn( |
| 'Feedback: Report for request with ID ' + ID + |
| ' will be sent later.'); |
| } |
| this.scheduleWindowClose(); |
| } |
| |
| /** |
| * Reads the selected file when the user selects a file. |
| * @param fileSelectedEvent The onChanged event for the file input box. |
| */ |
| private onFileSelected(fileSelectedEvent: Event) { |
| // <if expr="is_chromeos"> |
| // This is needed on CrOS. Otherwise, the feedback window will stay behind |
| // the Chrome window. |
| FeedbackBrowserProxyImpl.getInstance().showDialog(); |
| // </if> |
| |
| const file = (fileSelectedEvent.target as HTMLInputElement).files![0]; |
| if (!file) { |
| // User canceled file selection. |
| this.attachedFileBlob = null; |
| return; |
| } |
| |
| if (file.size > MAX_ATTACH_FILE_SIZE) { |
| this.getRequiredElement('#attach-error').hidden = false; |
| |
| // Clear our selected file. |
| this.getRequiredElement<HTMLInputElement>('#attach-file').value = ''; |
| this.attachedFileBlob = null; |
| return; |
| } |
| |
| this.attachedFileBlob = file.slice(); |
| } |
| |
| /** |
| * Called when user opens the file dialog. Hide 'attach-error' before file |
| * dialog is open to prevent a11y bug https://crbug.com/1020047 |
| */ |
| private onOpenFileDialog() { |
| this.getRequiredElement('#attach-error').hidden = true; |
| } |
| |
| /** |
| * Clears the file that was attached to the report with the initial request. |
| * Instead we will now show the attach file button in case the user wants to |
| * attach another file. |
| */ |
| private clearAttachedFile() { |
| this.getRequiredElement('#custom-file-container').hidden = true; |
| this.attachedFileBlob = null; |
| this.feedbackInfo.attachedFile = undefined; |
| this.getRequiredElement('#attach-file').hidden = false; |
| } |
| |
| /** |
| * Sets up the event handlers for the given |anchorElement|. |
| * @param anchorElement The <a> html element. |
| * @param url The destination URL for the link. |
| * @param useAppWindow true if the URL should be opened inside a new App |
| * Window, false if it should be opened in a new tab. |
| */ |
| private setupLinkHandlers( |
| anchorElement: HTMLElement, url: string, useAppWindow: boolean) { |
| anchorElement.onclick = (e: Event) => { |
| e.preventDefault(); |
| if (useAppWindow) { |
| openUrlInAppWindow(url); |
| } else { |
| window.open(url, '_blank'); |
| } |
| }; |
| |
| anchorElement.onauxclick = (e: Event) => { |
| e.preventDefault(); |
| }; |
| } |
| |
| /** |
| * Checks if any keywords related to bluetooth have been typed. If they are, |
| * we show the bluetooth logs option, otherwise hide it. |
| * @param inputEvent The input event for the description textarea. |
| */ |
| private checkForSendBluetoothLogs(inputEvent: Event) { |
| const value = (inputEvent.target as HTMLInputElement).value; |
| const isRelatedToBluetooth = BT_REGEX.test(value) || |
| CANNOT_CONNECT_REGEX.test(value) || TETHER_REGEX.test(value) || |
| SMART_LOCK_REGEX.test(value) || NEARBY_SHARE_REGEX.test(value) || |
| FAST_PAIR_REGEX.test(value) || BT_DEVICE_REGEX.test(value); |
| this.getRequiredElement('#bluetooth-checkbox-container').hidden = |
| !isRelatedToBluetooth; |
| } |
| |
| /** |
| * Checks if any keywords have associated questionnaire in a domain. If so, |
| * we append the questionnaire in |
| * getRequiredElement('description-text'). |
| * @param inputEvent The input event for the description textarea. |
| */ |
| private checkForShowQuestionnaire(inputEvent: Event) { |
| const toAppend = []; |
| |
| // Match user-entered description before the questionnaire to reduce false |
| // positives due to matching the questionnaire questions and answers. |
| const value = (inputEvent.target as HTMLInputElement).value; |
| const questionnaireBeginPos = value.indexOf(questionnaireBegin); |
| const matchedText = questionnaireBeginPos >= 0 ? |
| value.substring(0, questionnaireBeginPos) : |
| value; |
| |
| if (BT_REGEX.test(matchedText)) { |
| toAppend.push(...domainQuestions['bluetooth']); |
| } |
| |
| if (WIFI_REGEX.test(matchedText)) { |
| toAppend.push(...domainQuestions['wifi']); |
| } |
| |
| if (CELLULAR_REGEX.test(matchedText)) { |
| toAppend.push(...domainQuestions['cellular']); |
| } |
| |
| if (DISPLAY_REGEX.test(matchedText)) { |
| toAppend.push(...domainQuestions['display']); |
| } |
| |
| if (THUNDERBOLT_REGEX.test(matchedText)) { |
| toAppend.push(...domainQuestions['thunderbolt']); |
| } else if (USB_REGEX.test(matchedText)) { |
| toAppend.push(...domainQuestions['usb']); |
| } |
| |
| if (toAppend.length === 0) { |
| return; |
| } |
| |
| const textarea = |
| this.getRequiredElement<HTMLTextAreaElement>('#description-text'); |
| const savedCursor = textarea.selectionStart; |
| if (Object.keys(this.appendedQuestions).length === 0) { |
| textarea.value += '\n\n' + questionnaireBegin + '\n'; |
| this.getRequiredElement('#questionnaire-notification').textContent = |
| questionnaireNotification; |
| } |
| |
| for (const question of toAppend) { |
| if (question in this.appendedQuestions) { |
| continue; |
| } |
| |
| textarea.value += '* ' + question + ' \n'; |
| this.appendedQuestions[question] = true; |
| } |
| |
| // After appending text, the web engine automatically moves the cursor to |
| // the end of the appended text, so we need to move the cursor back to where |
| // the user was typing before. |
| textarea.selectionEnd = savedCursor; |
| } |
| |
| /** |
| * Updates the description-text box based on whether it was valid. |
| * If invalid, indicate an error to the user. If valid, remove indication of |
| * the error. |
| */ |
| private updateDescription(wasValid: boolean) { |
| // Set visibility of the alert text for users who don't use a screen |
| // reader. |
| this.getRequiredElement('#description-empty-error').hidden = wasValid; |
| |
| // Change the textarea's aria-labelled by to ensure the screen reader does |
| // (or doesn't) read the error, as appropriate. |
| // If it does read the error, it should do so _before_ it reads the normal |
| // description. |
| const description = |
| this.getRequiredElement<HTMLTextAreaElement>('#description-text'); |
| description.setAttribute( |
| 'aria-labelledby', |
| (wasValid ? '' : 'description-empty-error ') + 'free-form-text'); |
| // Indicate whether input is valid. |
| description.setAttribute('aria-invalid', String(!wasValid)); |
| if (!wasValid) { |
| // Return focus to field so user can correct error. |
| description.focus(); |
| } |
| |
| // We may have added or removed a line of text, so make sure the app window |
| // is the right size. |
| this.resizeAppWindow(); |
| } |
| |
| /** |
| * Sends the report; after the report is sent, we need to be redirected to |
| * the landing page, but we shouldn't be able to navigate back, hence |
| * we open the landing page in a new tab and sendReport closes this tab. |
| * @return Whether the report was sent. |
| */ |
| private sendReport(): boolean { |
| const textarea = |
| this.getRequiredElement<HTMLTextAreaElement>('#description-text'); |
| if (textarea.value.length === 0) { |
| this.updateDescription(false); |
| return false; |
| } |
| // This isn't strictly necessary, since if we get past this point we'll |
| // succeed, but for future-compatibility (and in case we later add more |
| // failure cases after this), re-hide the alert and reset the aria label. |
| this.updateDescription(true); |
| |
| // Prevent double clicking from sending additional reports. |
| this.getRequiredElement<HTMLButtonElement>('#send-report-button').disabled = |
| true; |
| if (!this.feedbackInfo.attachedFile && this.attachedFileBlob) { |
| this.feedbackInfo.attachedFile = { |
| name: this.getRequiredElement<HTMLInputElement>('#attach-file').value, |
| data: this.attachedFileBlob, |
| }; |
| } |
| |
| const consentCheckboxValue: boolean = |
| this.getRequiredElement<HTMLInputElement>('#consent-checkbox').checked; |
| this.feedbackInfo.systemInformation = [ |
| { |
| key: 'feedbackUserCtlConsent', |
| value: String(consentCheckboxValue), |
| }, |
| ]; |
| |
| const isAiFlow: boolean = |
| this.feedbackInfo.flow === chrome.feedbackPrivate.FeedbackFlow.AI; |
| const isSeaPenFlow: boolean|undefined = |
| isAiFlow && this.feedbackInfo.aiMetadata?.includes('from_sea_pen'); |
| if (isAiFlow) { |
| this.feedbackInfo.isOffensiveOrUnsafe = |
| this.getRequiredElement<HTMLInputElement>('#offensive-checkbox') |
| .checked; |
| if (isSeaPenFlow || |
| !this.getRequiredElement<HTMLInputElement>('#log-id-checkbox') |
| .checked) { |
| this.feedbackInfo.aiMetadata = undefined; |
| } |
| } |
| |
| this.feedbackInfo.description = textarea.value; |
| this.feedbackInfo.pageUrl = |
| this.getRequiredElement<HTMLInputElement>('#page-url-text').value; |
| this.feedbackInfo.email = |
| this.getRequiredElement<HTMLSelectElement>('#user-email-drop-down') |
| .value; |
| |
| let useSystemInfo = false; |
| let useHistograms = false; |
| const checkbox = |
| this.shadowRoot.querySelector<HTMLInputElement>('#sys-info-checkbox'); |
| // SeaPen flow doesn't collect system info data. |
| if (checkbox != null && checkbox.checked && !isSeaPenFlow) { |
| // Send histograms along with system info. |
| useHistograms = true; |
| useSystemInfo = true; |
| } |
| |
| const autofillCheckbox = this.shadowRoot.querySelector<HTMLInputElement>( |
| '#autofill-metadata-checkbox'); |
| if (autofillCheckbox != null && autofillCheckbox.checked && |
| !this.getRequiredElement('#autofill-checkbox-container').hidden) { |
| this.feedbackInfo.sendAutofillMetadata = true; |
| } |
| |
| this.feedbackInfo.sendHistograms = useHistograms; |
| |
| if (this.getRequiredElement<HTMLInputElement>('#screenshot-checkbox') |
| .checked) { |
| // The user is okay with sending the screenshot and tab titles. |
| this.feedbackInfo.sendTabTitles = true; |
| } else { |
| // The user doesn't want to send the screenshot, so clear it. |
| this.feedbackInfo.screenshot = undefined; |
| } |
| |
| let productId: number|undefined = |
| parseInt('' + this.feedbackInfo.productId, 10); |
| if (isNaN(productId)) { |
| // For apps that still use a string value as the |productId|, we must |
| // clear that value since the API uses an integer value, and a conflict in |
| // data types will cause the report to fail to be sent. |
| productId = undefined; |
| } |
| this.feedbackInfo.productId = productId; |
| |
| // Request sending the report, show the landing page (if allowed) |
| this.sendFeedbackReport(useSystemInfo); |
| |
| return true; |
| } |
| |
| /** |
| * Click listener for the cancel button. |
| */ |
| private cancel(e: Event) { |
| e.preventDefault(); |
| this.scheduleWindowClose(); |
| } |
| |
| private resizeAppWindow() { |
| // TODO(crbug.com/1167223): The UI is now controlled by a WebDialog delegate |
| // which is set to not resizable for now. If needed, a message handler can |
| // be added to respond to resize request. |
| } |
| |
| /** |
| * Close the window after 100ms delay. |
| */ |
| private scheduleWindowClose() { |
| setTimeout(() => FeedbackBrowserProxyImpl.getInstance().closeDialog(), 100); |
| } |
| |
| /** |
| * TODO(crbug.com/41481648): A helper function in favor of converting feedback |
| * UI from non-web component HTML to PolymerElement. It's better to be |
| * replaced by polymer's $ helper dictionary. |
| */ |
| getRequiredElement<T extends HTMLElement = HTMLElement>(query: string): T { |
| const el = this.shadowRoot.querySelector<T>(query); |
| assert(el); |
| assert(el instanceof HTMLElement); |
| return el; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'feedback-app': AppElement; |
| } |
| } |
| |
| customElements.define(AppElement.is, AppElement); |