blob: fb4776167283cdcffcdcabf2695a58380ac72ee5 [file] [log] [blame]
// 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 {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import type {LoadTimeDataRaw} from 'chrome://resources/js/load_time_data.js';
import {getRequiredElement} from 'chrome://resources/js/util.js';
import {html, render} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {HIDDEN_CLASS} from './constants.js';
import {Runner} from './dino_game/offline.js';
declare global {
interface Window {
errorPageController?: ErrorPageController;
// `loadTimeDataRaw` is injected to the `window` scope from C++.
loadTimeDataRaw: LoadTimeDataRaw;
initializeEasterEggHighScore: (score: number) => void;
}
}
interface ErrorPageController {
downloadButtonClick(): void;
reloadButtonClick(url?: string): void;
detailsButtonClick(): void;
diagnoseErrorsButtonClick(): void;
portalSigninButtonClick(): void;
savePageForLater(): void;
cancelSavePage(): void;
updateEasterEggHighScore(score: number): void;
resetEasterEggHighScore(): void;
trackEasterEgg(): void;
}
interface TemplateData {
// Properties that exist in both error pages and chrome://dino.
errorCode: string;
heading: {
msg: string,
};
iconClass: string;
// Properties that exist only when there is an actual error and not when
// visiting chrome://dino directly.
details?: string;
hideDetails?: string;
suggestionsDetails?: Array<{header: string, body: string}>;
suggestionsSummaryList?: Array<{summary: string}>;
suggestionsSummaryListHeader?: string;
summary?: {
msg: string,
};
reloadButton?: {
msg: string,
reloadUrl: string,
};
// TODO(crbug.com/378692755): Mark as Android only.
downloadButton?: {
msg: string,
disabledMsg: string,
};
// TODO(crbug.com/378692755): Mark as Android only.
savePageLater?: {
savePageMsg: string,
cancelMsg: string,
};
}
let showingDetails: boolean = false;
let lastData: TemplateData|null = null;
function toggleHelpBox() {
showingDetails = !showingDetails;
assert(lastData);
render(getHtml(lastData, showingDetails), getRequiredElement('content'));
}
function diagnoseErrors() {
if (window.errorPageController) {
window.errorPageController.diagnoseErrorsButtonClick();
}
}
function portalSignin() {
if (window.errorPageController) {
window.errorPageController.portalSigninButtonClick();
}
}
// Subframes use a different layout but the same html file. This is to make it
// easier to support platforms that load the error page via different
// mechanisms (Currently just iOS).
let isSubFrame = false;
if (window.top!.location !== window.location) {
document.documentElement.setAttribute('subframe', '');
isSubFrame = true;
}
// Re-renders the error page using |data| as the dictionary of values.
// Used by NetErrorTabHelper to update DNS error pages with probe results.
function updateForDnsProbe(newData: TemplateData) {
onTemplateDataReceived(newData);
}
function getMainFrameErrorCssClass(showingDetails: boolean): string {
return showingDetails ? 'showing-details' : '';
}
function getMainFrameErrorIconCssClass(data: TemplateData): string {
return isSubFrame ? '' : data.iconClass;
}
function getSubFrameErrorIconCssClass(data: TemplateData): string {
return isSubFrame ? data.iconClass : '';
}
function shouldShowSuggestionsSummaryList(data: TemplateData): boolean {
return !!data.suggestionsSummaryList &&
data.suggestionsSummaryList.length > 0;
}
function getSuggestionsSummaryItemCssClass(data: TemplateData): string {
assert(data.suggestionsSummaryList);
return data.suggestionsSummaryList.length === 1 ? 'single-suggestion' : '';
}
// Implements button clicks. This function is needed during the transition
// between implementing these in trunk chromium and implementing them in iOS.
function reloadButtonClick(e: Event) {
const url = (e.target as HTMLElement).dataset['url'];
if (window.errorPageController) {
// <if expr="is_ios">
window.errorPageController.reloadButtonClick(url);
// </if>
// <if expr="not is_ios">
window.errorPageController.reloadButtonClick();
// </if>
} else {
assert(url);
window.location.href = url;
}
}
interface WithDisabledText {
disabledText: string;
}
function downloadButtonClick() {
if (window.errorPageController) {
window.errorPageController.downloadButtonClick();
const downloadButton =
getRequiredElement<HTMLButtonElement&WithDisabledText>(
'download-button');
downloadButton.disabled = true;
downloadButton.textContent = downloadButton.disabledText;
}
}
function detailsButtonClick() {
if (window.errorPageController) {
window.errorPageController.detailsButtonClick();
}
toggleHelpBox();
}
function setAutoFetchState(scheduled: boolean, canSchedule: boolean) {
getRequiredElement('cancel-save-page-button')
.classList.toggle(HIDDEN_CLASS, !scheduled);
getRequiredElement('save-page-for-later-button')
.classList.toggle(HIDDEN_CLASS, scheduled || !canSchedule);
}
function savePageLaterClick() {
assert(window.errorPageController);
window.errorPageController.savePageForLater();
// savePageForLater will eventually trigger a call to setAutoFetchState() when
// it completes.
}
function cancelSavePageClick() {
assert(window.errorPageController);
window.errorPageController.cancelSavePage();
// setAutoFetchState is not called in response to cancelSavePage(), so do it
// now.
setAutoFetchState(false, true);
}
function shouldShowControlButtons(data: TemplateData): boolean {
const downloadButtonVisible =
!!data.downloadButton && !!data.downloadButton.msg;
const reloadButtonVisible = !!data.reloadButton && !!data.reloadButton.msg;
return reloadButtonVisible || downloadButtonVisible;
}
function shouldShowDetailsButton(data: TemplateData): boolean {
return !!data.suggestionsDetails && data.suggestionsDetails.length > 0;
}
function getDetailsButtonCssClass(data: TemplateData): string {
return shouldShowControlButtons(data) ? '' : 'singular';
}
function getDetailsButtonText(
data: TemplateData, showingDetails: boolean): string {
assert(data.details);
assert(data.hideDetails);
return showingDetails ? data.hideDetails : data.details;
}
// Sets up the proper button layout for the current platform.
function getButtonsCssClass(): string {
let primaryControlOnLeft = true;
// clang-format off
// <if expr="is_macosx or is_ios or is_linux or is_chromeos or is_android">
// clang-format on
primaryControlOnLeft = false;
// </if>
return primaryControlOnLeft ? 'suggested-left' : 'suggested-right';
}
function onDocumentLoad() {
onTemplateDataReceived(window.loadTimeDataRaw as TemplateData);
}
function onTemplateDataReceived(newData: TemplateData) {
lastData = newData;
render(getHtml(lastData, showingDetails), getRequiredElement('content'));
if (!isSubFrame && newData.iconClass === 'icon-offline') {
document.documentElement.classList.add('offline');
// Set loadTimeData.data because it is used by the dino code.
loadTimeData.data = newData;
Runner.initializeInstance('.interstitial-wrapper');
}
}
function getHtml(data: TemplateData, showingDetails: boolean) {
// clang-format off
return html`
<div id="main-frame-error" class="interstitial-wrapper ${getMainFrameErrorCssClass(showingDetails)}">
<div id="main-content">
<div class="icon ${getMainFrameErrorIconCssClass(data)}"></div>
<div id="main-message">
<h1>
<span .innerHTML="${data.heading.msg}"></span>
</h1>
${data.summary ? html`
<p .innerHTML="${data.summary.msg}"></p>
` : ''}
${shouldShowSuggestionsSummaryList(data) ? html`
<div id="suggestions-list">
<p>${data.suggestionsSummaryListHeader}</p>
<ul class="${getSuggestionsSummaryItemCssClass(data)}">
${data.suggestionsSummaryList!.map(item => html`
<li .innerHTML="${item.summary}"></li>
`)}
</ul>
</div>
` : ''}
<div class="error-code">${data.errorCode}</div>
${data.savePageLater ? html`
<div id="save-page-for-later-button">
<a class="link-button" @click="${savePageLaterClick}">
${data.savePageLater.savePageMsg}
</a>
</div>
<div id="cancel-save-page-button" class="hidden"
@click="${cancelSavePageClick}"
.innerHTML="${data.savePageLater.cancelMsg}">
</div>
` : ''}
</div>
</div>
<div id="buttons" class="nav-wrapper ${getButtonsCssClass()}">
<div id="control-buttons" ?hidden="${!shouldShowControlButtons(data)}">
${data.reloadButton ? html`
<button id="reload-button"
class="blue-button text-button"
@click="${reloadButtonClick}"
data-url="${data.reloadButton.reloadUrl}">
${data.reloadButton.msg}
</button>
` : ''}
${data.downloadButton ? html`
<button id="download-button"
class="blue-button text-button"
@click="${downloadButtonClick}"
.disabledText="${data.downloadButton.disabledMsg}">
${data.downloadButton.msg}
</button>
` : ''}
</div>
${shouldShowDetailsButton(data) ? html`
<button id="details-button" class="secondary-button text-button
small-link ${getDetailsButtonCssClass(data)}"
@click="${detailsButtonClick}">
${getDetailsButtonText(data, showingDetails)}
</button>
` : ''}
</div>
${data.suggestionsDetails ? html`
<div id="details">
${data.suggestionsDetails.map(item => html`
<div class="suggestions">
<div class="suggestion-header" .innerHTML="${item.header}"></div>
<div class="suggestion-body" .innerHTML="${item.body}"></div>
</div>
`)}
</div>
` : ''}
</div>
${data.summary ? html`
<div id="sub-frame-error">
<!-- Show details when hovering over the icon, in case the details are
hidden because they're too large. -->
<div class="icon ${getSubFrameErrorIconCssClass(data)}"></div>
<div id="sub-frame-error-details" .innerHTML="${data.summary.msg}">
</div>
</div>
` : ''}
`;
// clang-format on
}
// Expose methods that are triggered either
// - By `onclick=...` handlers in the HTML code, OR
// - By `href="javascript:..."` in localized links.
// - By inected JS code coming from C++
//
// since those need to be available on the 'window' object.
Object.assign(window, {
diagnoseErrors,
portalSignin,
toggleHelpBox,
updateForDnsProbe,
});
document.addEventListener('DOMContentLoaded', onDocumentLoad);