summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/content
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sessionstore/content')
-rw-r--r--browser/components/sessionstore/content/aboutSessionRestore.js450
-rw-r--r--browser/components/sessionstore/content/aboutSessionRestore.xhtml69
-rw-r--r--browser/components/sessionstore/content/content-sessionStore.js13
3 files changed, 532 insertions, 0 deletions
diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js
new file mode 100644
index 0000000000..4cbf8b7bdd
--- /dev/null
+++ b/browser/components/sessionstore/content/aboutSessionRestore.js
@@ -0,0 +1,450 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+var gStateObject;
+var gTreeData;
+var gTreeInitialized = false;
+
+// Page initialization
+
+window.onload = function () {
+ let toggleTabs = document.getElementById("tabsToggle");
+ if (toggleTabs) {
+ let tabList = document.getElementById("tabList");
+
+ let toggleHiddenTabs = () => {
+ toggleTabs.classList.toggle("tabs-hidden");
+ tabList.hidden = toggleTabs.classList.contains("tabs-hidden");
+ initTreeView();
+ };
+ toggleTabs.onclick = toggleHiddenTabs;
+ }
+
+ // wire up click handlers for the radio buttons if they exist.
+ for (let radioId of ["radioRestoreAll", "radioRestoreChoose"]) {
+ let button = document.getElementById(radioId);
+ if (button) {
+ button.addEventListener("click", updateTabListVisibility);
+ }
+ }
+
+ var tabListTree = document.getElementById("tabList");
+ tabListTree.addEventListener("click", onListClick);
+ tabListTree.addEventListener("keydown", onListKeyDown);
+
+ var errorCancelButton = document.getElementById("errorCancel");
+ // aboutSessionRestore.js is included aboutSessionRestore.xhtml
+ // and aboutWelcomeBack.xhtml, but the latter does not have an
+ // errorCancel button.
+ if (errorCancelButton) {
+ errorCancelButton.addEventListener("command", startNewSession);
+ }
+
+ var errorTryAgainButton = document.getElementById("errorTryAgain");
+ errorTryAgainButton.addEventListener("command", restoreSession);
+
+ // the crashed session state is kept inside a textbox so that SessionStore picks it up
+ // (for when the tab is closed or the session crashes right again)
+ var sessionData = document.getElementById("sessionData");
+ if (!sessionData.value) {
+ errorTryAgainButton.disabled = true;
+ return;
+ }
+
+ gStateObject = JSON.parse(sessionData.value);
+
+ // make sure the data is tracked to be restored in case of a subsequent crash
+ var event = document.createEvent("UIEvents");
+ event.initUIEvent("input", true, true, window, 0);
+ sessionData.dispatchEvent(event);
+
+ initTreeView();
+
+ errorTryAgainButton.focus({ focusVisible: false });
+};
+
+function isTreeViewVisible() {
+ return !document.getElementById("tabList").hidden;
+}
+
+async function initTreeView() {
+ if (gTreeInitialized || !isTreeViewVisible()) {
+ return;
+ }
+
+ var tabList = document.getElementById("tabList");
+ let l10nIds = [];
+ for (
+ let labelIndex = 0;
+ labelIndex < gStateObject.windows.length;
+ labelIndex++
+ ) {
+ l10nIds.push({
+ id: "restore-page-window-label",
+ args: { windowNumber: labelIndex + 1 },
+ });
+ }
+ let winLabels = await document.l10n.formatValues(l10nIds);
+ gTreeData = [];
+ gStateObject.windows.forEach(function (aWinData, aIx) {
+ var winState = {
+ label: winLabels[aIx],
+ open: true,
+ checked: true,
+ ix: aIx,
+ };
+ winState.tabs = aWinData.tabs.map(function (aTabData) {
+ var entry = aTabData.entries[aTabData.index - 1] || {
+ url: "about:blank",
+ };
+ var iconURL = aTabData.image || null;
+ // don't initiate a connection just to fetch a favicon (see bug 462863)
+ if (/^https?:/.test(iconURL)) {
+ iconURL = "moz-anno:favicon:" + iconURL;
+ }
+ return {
+ label: entry.title || entry.url,
+ checked: true,
+ src: iconURL,
+ parent: winState,
+ };
+ });
+ gTreeData.push(winState);
+ for (let tab of winState.tabs) {
+ gTreeData.push(tab);
+ }
+ }, this);
+
+ tabList.view = treeView;
+ tabList.view.selection.select(0);
+ gTreeInitialized = true;
+}
+
+// User actions
+function updateTabListVisibility() {
+ document.getElementById("tabList").hidden =
+ !document.getElementById("radioRestoreChoose").checked;
+ initTreeView();
+}
+
+function restoreSession() {
+ Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore");
+ document.getElementById("errorTryAgain").disabled = true;
+
+ if (isTreeViewVisible()) {
+ if (!gTreeData.some(aItem => aItem.checked)) {
+ // This should only be possible when we have no "cancel" button, and thus
+ // the "Restore session" button always remains enabled. In that case and
+ // when nothing is selected, we just want a new session.
+ startNewSession();
+ return;
+ }
+
+ // remove all unselected tabs from the state before restoring it
+ var ix = gStateObject.windows.length - 1;
+ for (var t = gTreeData.length - 1; t >= 0; t--) {
+ if (treeView.isContainer(t)) {
+ if (gTreeData[t].checked === 0) {
+ // this window will be restored partially
+ gStateObject.windows[ix].tabs = gStateObject.windows[ix].tabs.filter(
+ (aTabData, aIx) => gTreeData[t].tabs[aIx].checked
+ );
+ } else if (!gTreeData[t].checked) {
+ // this window won't be restored at all
+ gStateObject.windows.splice(ix, 1);
+ }
+ ix--;
+ }
+ }
+ }
+ var stateString = JSON.stringify(gStateObject);
+
+ var top = getBrowserWindow();
+
+ // if there's only this page open, reuse the window for restoring the session
+ if (top.gBrowser.tabs.length == 1) {
+ SessionStore.setWindowState(top, stateString, true);
+ return;
+ }
+
+ // restore the session into a new window and close the current tab
+ var newWindow = top.openDialog(
+ top.location,
+ "_blank",
+ "chrome,dialog=no,all"
+ );
+
+ Services.obs.addObserver(function observe(win, topic) {
+ if (win != newWindow) {
+ return;
+ }
+
+ Services.obs.removeObserver(observe, topic);
+ SessionStore.setWindowState(newWindow, stateString, true);
+
+ let tabbrowser = top.gBrowser;
+ let browser = window.docShell.chromeEventHandler;
+ let tab = tabbrowser.getTabForBrowser(browser);
+ tabbrowser.removeTab(tab);
+ }, "browser-delayed-startup-finished");
+}
+
+function startNewSession() {
+ if (Services.prefs.getIntPref("browser.startup.page") == 0) {
+ getBrowserWindow().gBrowser.loadURI(Services.io.newURI("about:blank"), {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ });
+ } else {
+ getBrowserWindow().BrowserHome();
+ }
+}
+
+function onListClick(aEvent) {
+ // don't react to right-clicks
+ if (aEvent.button == 2) {
+ return;
+ }
+
+ var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.col) {
+ // Restore this specific tab in the same window for middle/double/accel clicking
+ // on a tab's title.
+ let accelKey =
+ AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
+ if (
+ (aEvent.button == 1 ||
+ (aEvent.button == 0 && aEvent.detail == 2) ||
+ accelKey) &&
+ cell.col.id == "title" &&
+ !treeView.isContainer(cell.row)
+ ) {
+ restoreSingleTab(cell.row, aEvent.shiftKey);
+ aEvent.stopPropagation();
+ } else if (cell.col.id == "restore") {
+ toggleRowChecked(cell.row);
+ }
+ }
+}
+
+function onListKeyDown(aEvent) {
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_SPACE:
+ toggleRowChecked(document.getElementById("tabList").currentIndex);
+ // Prevent page from scrolling on the space key.
+ aEvent.preventDefault();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ var ix = document.getElementById("tabList").currentIndex;
+ if (aEvent.ctrlKey && !treeView.isContainer(ix)) {
+ restoreSingleTab(ix, aEvent.shiftKey);
+ }
+ break;
+ }
+}
+
+// Helper functions
+
+function getBrowserWindow() {
+ return window.browsingContext.topChromeWindow;
+}
+
+function toggleRowChecked(aIx) {
+ function isChecked(aItem) {
+ return aItem.checked;
+ }
+
+ var item = gTreeData[aIx];
+ item.checked = !item.checked;
+ treeView.treeBox.invalidateRow(aIx);
+
+ if (treeView.isContainer(aIx)) {
+ // (un)check all tabs of this window as well
+ for (let tab of item.tabs) {
+ tab.checked = item.checked;
+ treeView.treeBox.invalidateRow(gTreeData.indexOf(tab));
+ }
+ } else {
+ // Update the window's checkmark as well (0 means "partially checked").
+ let state = false;
+ if (item.parent.tabs.every(isChecked)) {
+ state = true;
+ } else if (item.parent.tabs.some(isChecked)) {
+ state = 0;
+ }
+ item.parent.checked = state;
+
+ treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent));
+ }
+
+ // we only disable the button when there's no cancel button.
+ if (document.getElementById("errorCancel")) {
+ document.getElementById("errorTryAgain").disabled =
+ !gTreeData.some(isChecked);
+ }
+}
+
+function restoreSingleTab(aIx, aShifted) {
+ var tabbrowser = getBrowserWindow().gBrowser;
+ var newTab = tabbrowser.addWebTab();
+ var item = gTreeData[aIx];
+
+ var tabState =
+ gStateObject.windows[item.parent.ix].tabs[
+ aIx - gTreeData.indexOf(item.parent) - 1
+ ];
+ // ensure tab would be visible on the tabstrip.
+ tabState.hidden = false;
+ SessionStore.setTabState(newTab, JSON.stringify(tabState));
+
+ // respect the preference as to whether to select the tab (the Shift key inverses)
+ if (
+ Services.prefs.getBoolPref("browser.tabs.loadInBackground") != !aShifted
+ ) {
+ tabbrowser.selectedTab = newTab;
+ }
+}
+
+// Tree controller
+
+var treeView = {
+ treeBox: null,
+ selection: null,
+
+ get rowCount() {
+ return gTreeData.length;
+ },
+ setTree(treeBox) {
+ this.treeBox = treeBox;
+ },
+ getCellText(idx, column) {
+ return gTreeData[idx].label;
+ },
+ isContainer(idx) {
+ return "open" in gTreeData[idx];
+ },
+ getCellValue(idx, column) {
+ return gTreeData[idx].checked;
+ },
+ isContainerOpen(idx) {
+ return gTreeData[idx].open;
+ },
+ isContainerEmpty(idx) {
+ return false;
+ },
+ isSeparator(idx) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isEditable(idx, column) {
+ return false;
+ },
+ canDrop(idx, orientation, dt) {
+ return false;
+ },
+ getLevel(idx) {
+ return this.isContainer(idx) ? 0 : 1;
+ },
+
+ getParentIndex(idx) {
+ if (!this.isContainer(idx)) {
+ for (var t = idx - 1; t >= 0; t--) {
+ if (this.isContainer(t)) {
+ return t;
+ }
+ }
+ }
+ return -1;
+ },
+
+ hasNextSibling(idx, after) {
+ var thisLevel = this.getLevel(idx);
+ for (var t = after + 1; t < gTreeData.length; t++) {
+ if (this.getLevel(t) <= thisLevel) {
+ return this.getLevel(t) == thisLevel;
+ }
+ }
+ return false;
+ },
+
+ toggleOpenState(idx) {
+ if (!this.isContainer(idx)) {
+ return;
+ }
+ var item = gTreeData[idx];
+ if (item.open) {
+ // remove this window's tab rows from the view
+ var thisLevel = this.getLevel(idx);
+ /* eslint-disable no-empty */
+ for (
+ var t = idx + 1;
+ t < gTreeData.length && this.getLevel(t) > thisLevel;
+ t++
+ ) {}
+ /* eslint-disable no-empty */
+ var deletecount = t - idx - 1;
+ gTreeData.splice(idx + 1, deletecount);
+ this.treeBox.rowCountChanged(idx + 1, -deletecount);
+ } else {
+ // add this window's tab rows to the view
+ var toinsert = gTreeData[idx].tabs;
+ for (var i = 0; i < toinsert.length; i++) {
+ gTreeData.splice(idx + i + 1, 0, toinsert[i]);
+ }
+ this.treeBox.rowCountChanged(idx + 1, toinsert.length);
+ }
+ item.open = !item.open;
+ this.treeBox.invalidateRow(idx);
+ },
+
+ getCellProperties(idx, column) {
+ if (
+ column.id == "restore" &&
+ this.isContainer(idx) &&
+ gTreeData[idx].checked === 0
+ ) {
+ return "partial";
+ }
+ if (column.id == "title") {
+ return this.getImageSrc(idx, column) ? "icon" : "noicon";
+ }
+
+ return "";
+ },
+
+ getRowProperties(idx) {
+ var winState = gTreeData[idx].parent || gTreeData[idx];
+ if (winState.ix % 2 != 0) {
+ return "alternate";
+ }
+
+ return "";
+ },
+
+ getImageSrc(idx, column) {
+ if (column.id == "title") {
+ return gTreeData[idx].src || null;
+ }
+ return null;
+ },
+
+ cycleHeader(column) {},
+ cycleCell(idx, column) {},
+ selectionChanged() {},
+ getColumnProperties(column) {
+ return "";
+ },
+};
diff --git a/browser/components/sessionstore/content/aboutSessionRestore.xhtml b/browser/components/sessionstore/content/aboutSessionRestore.xhtml
new file mode 100644
index 0000000000..05538be5d9
--- /dev/null
+++ b/browser/components/sessionstore/content/aboutSessionRestore.xhtml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome: resource:; img-src chrome: resource: data:; object-src 'none'" />
+ <meta name="color-scheme" content="light dark"/>
+ <title data-l10n-id="restore-page-tab-title"></title>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css" media="all"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutSessionRestore.css" type="text/css" media="all"/>
+ <link rel="icon" href="chrome://global/skin/icons/info.svg"/>
+ <link rel="localization" href="browser/aboutSessionRestore.ftl"/>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <script src="chrome://browser/content/aboutSessionRestore.js"/>
+ </head>
+
+ <body>
+
+ <div class="container tab-list-tree-container">
+ <div class="description-wrapper">
+ <div class="title">
+ <h1 class="title-text" data-l10n-id="restore-page-error-title"></h1>
+ </div>
+ <div class="description">
+ <p data-l10n-id="restore-page-problem-desc"></p>
+ <p data-l10n-id="restore-page-try-this"></p>
+ </div>
+ <button id="tabsToggle" class="tabs-hidden">
+ <span id="showTabs" data-l10n-id="restore-page-show-tabs"></span>
+ <span id="hideTabs" data-l10n-id="restore-page-hide-tabs"></span>
+ </button>
+ </div>
+ <xul:tree id="tabList" seltype="single" hidecolumnpicker="true" hidden="true">
+ <xul:treecols>
+ <xul:treecol cycler="true" id="restore" type="checkbox" data-l10n-id="restore-page-restore-header"/>
+ <xul:splitter class="tree-splitter"/>
+ <xul:treecol primary="true" id="title" data-l10n-id="restore-page-list-header" flex="1"/>
+ </xul:treecols>
+ <xul:treechildren flex="1"/>
+ </xul:tree>
+ <div class="button-container">
+#ifdef XP_UNIX
+ <xul:button id="errorCancel"
+ data-l10n-id="restore-page-close-button"/>
+ <xul:button class="primary"
+ id="errorTryAgain"
+ data-l10n-id="restore-page-try-again-button"/>
+#else
+ <xul:button class="primary"
+ id="errorTryAgain"
+ data-l10n-id="restore-page-try-again-button"/>
+ <xul:button id="errorCancel"
+ data-l10n-id="restore-page-close-button"/>
+#endif
+ </div>
+ <!-- holds the session data for when the tab is closed -->
+ <input type="text" id="sessionData" hidden="true"/>
+ </div>
+
+ </body>
+</html>
diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js
new file mode 100644
index 0000000000..a4bdea0bdc
--- /dev/null
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/frame-script */
+
+"use strict";
+
+const { ContentSessionStore } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/ContentSessionStore.sys.mjs"
+);
+
+void new ContentSessionStore(this);