feat(sidebar): part 1 - tree vertical tabs v1.1
This commit is contained in:
@@ -929,7 +929,7 @@ set_config("MOZ_SYSTEM_ZLIB", True, when="--with-system-zlib")
|
||||
|
||||
option(
|
||||
env="USE_LIBZ_RS",
|
||||
default=milestone.is_early_beta_or_earlier,
|
||||
default=True,
|
||||
help="Use libz-rs-sys instead of zlib",
|
||||
when=toolkit & ~with_system_zlib_option,
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
TabGrouping: "resource:///modules/TabGrouping.sys.mjs",
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
UICustomizations: "resource:///modules/UICustomizations.sys.mjs",
|
||||
SidebarPreferencesHandler: "resource:///modules/SidebarPreferencesHandler.sys.mjs",
|
||||
});
|
||||
|
||||
const WATERFOX_CUSTOMIZATIONS_PREF =
|
||||
@@ -64,6 +65,9 @@ export const WaterfoxGlue = {
|
||||
this.startupManifest = await this.getChromeManifest("startup");
|
||||
this.privateManifest = await this.getChromeManifest("private");
|
||||
|
||||
// Initialize sidebar prefs handler early so it can observe pane loads
|
||||
lazy.SidebarPreferencesHandler.init();
|
||||
|
||||
// Observe chrome-document-loaded topic to detect window open
|
||||
Services.obs.addObserver(this, "chrome-document-loaded");
|
||||
// Observe main-pane-loaded topic to detect about:preferences open
|
||||
|
||||
98
waterfox/browser/components/genMozBuild.py
Normal file
98
waterfox/browser/components/genMozBuild.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import os
|
||||
import io
|
||||
import argparse
|
||||
import json
|
||||
|
||||
skipFiles = [
|
||||
"manifest.json.template",
|
||||
"test",
|
||||
"moz.build",
|
||||
]
|
||||
|
||||
def getFullFileList(outputLoc, dirName):
|
||||
result = {dirName: []}
|
||||
for entry in os.listdir(outputLoc):
|
||||
if entry in skipFiles:
|
||||
continue
|
||||
if os.path.isdir(os.path.join(outputLoc, entry)):
|
||||
result.update(getFullFileList(os.path.join(outputLoc, entry), os.path.join(dirName, entry)))
|
||||
elif dirName:
|
||||
result[dirName].append(os.path.join(dirName, entry))
|
||||
else:
|
||||
result[dirName].append(entry)
|
||||
|
||||
return result
|
||||
|
||||
def rewriteMozBuild(outputLoc, fileList, extension_id):
|
||||
mozBuildFile = os.path.join(outputLoc, "moz.build")
|
||||
print("Rewriting %s" % mozBuildFile)
|
||||
|
||||
with io.open(mozBuildFile, "w", encoding="UTF-8") as buildFile:
|
||||
insertion_text = ''
|
||||
|
||||
for dir in sorted(fileList.keys()):
|
||||
if not fileList[dir]:
|
||||
continue
|
||||
|
||||
if not dir:
|
||||
mozBuildPathName = ""
|
||||
else:
|
||||
mozBuildPathName = '["' + '"]["'.join(dir.split(os.sep)) + '"]'
|
||||
|
||||
sorted_files = sorted(fileList[dir], key=lambda s: s.lower()) # Sort the files
|
||||
|
||||
insertion_text += \
|
||||
"FINAL_TARGET_FILES.features['%s']%s += [\n" % (extension_id, mozBuildPathName) + \
|
||||
" '" + \
|
||||
"',\n '".join(sorted_files) + "'\n]\n\n" # Use the sorted files
|
||||
|
||||
new_contents = "# AUTOMATIC INSERTION START\n" + insertion_text + "# AUTOMATIC INSERTION END\n"
|
||||
buildFile.write(new_contents)
|
||||
|
||||
def replace_locale_strings_in_manifest(manifest_path, locale_path):
|
||||
# Load the manifest file
|
||||
with open(manifest_path, 'r') as f:
|
||||
manifest_data = json.load(f)
|
||||
|
||||
# Load the messages file
|
||||
with open(locale_path, 'r') as f:
|
||||
messages_data = json.load(f)
|
||||
|
||||
# Recursive function to replace placeholders in nested objects
|
||||
def replace_placeholders(obj):
|
||||
if isinstance(obj, dict):
|
||||
for key in obj:
|
||||
obj[key] = replace_placeholders(obj[key])
|
||||
elif isinstance(obj, str) and obj.startswith('__MSG_') and obj.endswith('__'):
|
||||
# Extract the message key from the placeholder
|
||||
message_key = obj[6:-2]
|
||||
|
||||
# Replace the placeholder with the message from the messages file
|
||||
if message_key in messages_data:
|
||||
return messages_data[message_key]['message']
|
||||
return obj
|
||||
|
||||
# Replace placeholders in the manifest data
|
||||
manifest_data = replace_placeholders(manifest_data)
|
||||
|
||||
# Write the modified manifest data back to the file
|
||||
with open(manifest_path, 'w') as f:
|
||||
json.dump(manifest_data, f, indent=2)
|
||||
|
||||
def main(directory, extension_id):
|
||||
outputLoc = directory
|
||||
fileList = getFullFileList(outputLoc, "")
|
||||
rewriteMozBuild(outputLoc, fileList, extension_id)
|
||||
|
||||
# Call the function with the paths to the manifest.json and messages.json files
|
||||
manifest_path = os.path.join(directory, 'manifest.json')
|
||||
locale_path = os.path.join(directory, '_locales', 'en', 'messages.json')
|
||||
if os.path.exists(manifest_path) and os.path.exists(locale_path):
|
||||
replace_locale_strings_in_manifest(manifest_path, locale_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Generate moz.build file")
|
||||
parser.add_argument("directory", help="The directory to list files for")
|
||||
parser.add_argument("extension_id", help="The ID of the extension")
|
||||
args = parser.parse_args()
|
||||
main(args.directory, args.extension_id)
|
||||
@@ -12,6 +12,7 @@ DIRS += [
|
||||
"preferences",
|
||||
"privatetab",
|
||||
"search",
|
||||
"sidebar",
|
||||
"statusbar",
|
||||
"tabfeatures",
|
||||
"tabgrouping",
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
/* 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/. */
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
||||
});
|
||||
|
||||
const SIDEBAR_ADDON_ID = "sidebar@waterfox.net";
|
||||
const SIDEBAR_ADDON_VERSION = "1.1.0";
|
||||
const SIDEBAR_ADDON_URI = "resource://builtin-addons/sidebar/";
|
||||
|
||||
const SIDEBAR_COMPONENT_PREF = "browser.sidebar.enabled";
|
||||
const CONTAINERS_CONTROLLER_PREF = "privacy.userContext.extension";
|
||||
|
||||
export const SidebarPreferencesHandler = {
|
||||
_initialized: false,
|
||||
_sidebarPrefObserver: null,
|
||||
|
||||
init() {
|
||||
if (this._initialized) {
|
||||
return;
|
||||
}
|
||||
this._initialized = true;
|
||||
|
||||
// Watch for about:preferences being loaded so we can patch the UI.
|
||||
Services.obs.addObserver(this, "main-pane-loaded");
|
||||
|
||||
// Monitor the sidebar component pref and sync the built-in add-on state.
|
||||
this._monitorSidebarPref();
|
||||
},
|
||||
|
||||
uninit() {
|
||||
if (!this._initialized) {
|
||||
return;
|
||||
}
|
||||
this._initialized = false;
|
||||
|
||||
try {
|
||||
Services.obs.removeObserver(this, "main-pane-loaded");
|
||||
} catch (_) {}
|
||||
|
||||
if (this._sidebarPrefObserver) {
|
||||
try {
|
||||
Services.prefs.removeObserver(SIDEBAR_COMPONENT_PREF, this._sidebarPrefObserver);
|
||||
} catch (_) {}
|
||||
this._sidebarPrefObserver = null;
|
||||
}
|
||||
},
|
||||
|
||||
observe(subject, topic, _data) {
|
||||
switch (topic) {
|
||||
case "main-pane-loaded": {
|
||||
// subject is the about:preferences content window
|
||||
this._ensureTreeCategory(subject);
|
||||
this._patchPreferences(subject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async _monitorSidebarPref() {
|
||||
let addon = await lazy.AddonManager.getAddonByID(SIDEBAR_ADDON_ID);
|
||||
|
||||
// Ensure the built-in sidebar add-on is present (first run or after updates).
|
||||
addon =
|
||||
(await lazy.AddonManager.maybeInstallBuiltinAddon(
|
||||
SIDEBAR_ADDON_ID,
|
||||
SIDEBAR_ADDON_VERSION,
|
||||
SIDEBAR_ADDON_URI
|
||||
)) || addon;
|
||||
|
||||
const syncAddonStateWithPref = async () => {
|
||||
try {
|
||||
const disabled = Services.prefs.getBoolPref(SIDEBAR_COMPONENT_PREF, false);
|
||||
if (disabled) {
|
||||
if (addon?.isActive) {
|
||||
await addon.disable({ allowSystemAddons: true });
|
||||
}
|
||||
} else {
|
||||
if (addon && !addon.isActive) {
|
||||
await addon.enable({ allowSystemAddons: true });
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore transient failures.
|
||||
}
|
||||
};
|
||||
|
||||
// Initial sync and observer
|
||||
await syncAddonStateWithPref();
|
||||
this._sidebarPrefObserver = syncAddonStateWithPref;
|
||||
Services.prefs.addObserver(SIDEBAR_COMPONENT_PREF, syncAddonStateWithPref);
|
||||
},
|
||||
|
||||
_ensureTreeCategory(prefsWin) {
|
||||
const doc = prefsWin.document;
|
||||
const categories = doc.getElementById("categories");
|
||||
if (!categories) {
|
||||
return;
|
||||
}
|
||||
// Bail out if already present
|
||||
if (doc.getElementById("category-tree")) {
|
||||
return;
|
||||
}
|
||||
// Insert before search category (like previous overlay did)
|
||||
const before = doc.getElementById("category-search");
|
||||
const item = doc.createXULElement("richlistitem");
|
||||
item.id = "category-tree";
|
||||
item.className = "category";
|
||||
item.setAttribute("data-l10n-id", "category-tree");
|
||||
item.setAttribute("data-l10n-attrs", "tooltiptext");
|
||||
item.setAttribute("value", "paneTree");
|
||||
item.setAttribute("align", "center");
|
||||
if (before && before.parentNode === categories) {
|
||||
categories.insertBefore(item, before);
|
||||
} else {
|
||||
categories.appendChild(item);
|
||||
}
|
||||
const img = doc.createXULElement("image");
|
||||
img.className = "category-icon";
|
||||
const label = doc.createXULElement("label");
|
||||
label.className = "category-name";
|
||||
label.setAttribute("flex", "1");
|
||||
doc.l10n.setAttributes(label, "pane-tree-title");
|
||||
item.appendChild(img);
|
||||
item.appendChild(label);
|
||||
|
||||
// Ensure the preferences framework knows about our pane so gotoPref works.
|
||||
if (!prefsWin.gCategoryModules?.has("paneTree")) {
|
||||
prefsWin.register_module("paneTree", {
|
||||
init() {
|
||||
// No-op: our pane is already injected and bound via data-category="paneTree".
|
||||
},
|
||||
});
|
||||
console.log("[SidebarPrefs] paneTree registered with preferences");
|
||||
}
|
||||
|
||||
// Wire category selection to navigate to our pane
|
||||
categories.addEventListener("select", () => {
|
||||
const sel = categories.selectedItem;
|
||||
if (sel && sel.getAttribute("value") === "paneTree") {
|
||||
prefsWin.gotoPref("paneTree");
|
||||
}
|
||||
}, { capture: true });
|
||||
},
|
||||
|
||||
_patchPreferences(prefsWin) {
|
||||
// Wait until the Containers UI has been constructed in about:preferences.
|
||||
const startedAt = Date.now();
|
||||
const timer = prefsWin.setInterval(() => {
|
||||
if (Date.now() - startedAt > 5000) {
|
||||
prefsWin.clearInterval(timer);
|
||||
// Still attempt to inject pane even if containers box wasn't found
|
||||
this._injectTreePreferences(prefsWin);
|
||||
return;
|
||||
}
|
||||
const doc = prefsWin.document;
|
||||
if (!doc || doc.readyState === "uninitialized") {
|
||||
return;
|
||||
}
|
||||
const containersBox = doc.getElementById("browserContainersbox");
|
||||
if (!containersBox) {
|
||||
return;
|
||||
}
|
||||
prefsWin.clearInterval(timer);
|
||||
this._hideContainersControlledBannerForBuiltin(prefsWin);
|
||||
this._injectTreePreferences(prefsWin);
|
||||
|
||||
// Handle initial navigation: allow about:preferences#tree to go to our pane.
|
||||
const hash = String(doc.location.hash || "").replace(/^#/, "");
|
||||
if (hash === "tree" || hash === "paneTree") {
|
||||
prefsWin.gotoPref("paneTree");
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
_hideContainersControlledBannerForBuiltin(prefsWin) {
|
||||
try {
|
||||
const controller = Services.prefs.getCharPref(CONTAINERS_CONTROLLER_PREF, "");
|
||||
if (controller !== SIDEBAR_ADDON_ID) {
|
||||
// Some other extension controls containers; do not modify UI.
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = prefsWin.document;
|
||||
const banner = doc.getElementById("browserContainersExtensionContent");
|
||||
const checkbox = doc.getElementById("browserContainersCheckbox");
|
||||
|
||||
// Hide the “controlled by extension” info row.
|
||||
if (banner) {
|
||||
banner.hidden = true;
|
||||
}
|
||||
|
||||
// Optional UX: Ensure the checkbox is enabled so it doesn't look disabled
|
||||
// without an explanation banner.
|
||||
if (checkbox) {
|
||||
checkbox.disabled = false;
|
||||
}
|
||||
|
||||
// Keep the state enforced if prefs code tries to flip it back.
|
||||
const target = doc.getElementById("browserContainersbox") || doc;
|
||||
const mo = new prefsWin.MutationObserver(() => {
|
||||
const b = doc.getElementById("browserContainersExtensionContent");
|
||||
const c = doc.getElementById("browserContainersCheckbox");
|
||||
if (b && !b.hidden) {
|
||||
b.hidden = true;
|
||||
}
|
||||
if (c && c.disabled) {
|
||||
c.disabled = false;
|
||||
}
|
||||
});
|
||||
mo.observe(target, { childList: true, subtree: true, attributes: true });
|
||||
} catch (_) {
|
||||
// Ignore unexpected contexts.
|
||||
}
|
||||
},
|
||||
|
||||
_injectTreePreferences(prefsWin) {
|
||||
const doc = prefsWin.document;
|
||||
|
||||
// Avoid duplicating UI if called more than once
|
||||
if (doc.getElementById("treePreferencesInjected")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert our pane elements into the main pane container. We rely on data-category="paneTree"
|
||||
// so the preferences framework shows/hides it appropriately.
|
||||
const mainPane = doc.getElementById("mainPrefPane");
|
||||
if (!mainPane) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to create XUL nodes
|
||||
const xul = (name, attrs = {}) => {
|
||||
const el = doc.createXULElement(name);
|
||||
for (const [k, v] of Object.entries(attrs)) {
|
||||
if (v !== undefined && v !== null) {
|
||||
el.setAttribute(k, v);
|
||||
}
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
// Root container
|
||||
const group = xul("groupbox", {
|
||||
id: "treePreferencesInjected",
|
||||
"data-category": "paneTree",
|
||||
hidden: "true",
|
||||
});
|
||||
|
||||
const label = xul("label");
|
||||
const h2 = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
|
||||
label.appendChild(h2);
|
||||
group.appendChild(label);
|
||||
doc.l10n.setAttributes(h2, "tree-header");
|
||||
|
||||
// A small helper to add checkbox bound to a pref with l10n id.
|
||||
const addCheckbox = (parent, id, pref, l10nId) => {
|
||||
const box = xul("vbox", { id });
|
||||
const cb = xul("checkbox", { id: `${id}-checkbox`, preference: pref });
|
||||
doc.l10n.setAttributes(cb, l10nId);
|
||||
box.appendChild(cb);
|
||||
parent.appendChild(box);
|
||||
};
|
||||
|
||||
// Appearance section
|
||||
const appearance = xul("groupbox", {
|
||||
id: "treeAppearanceGroup",
|
||||
"data-category": "paneTree",
|
||||
hidden: "true",
|
||||
});
|
||||
{
|
||||
const l = xul("label");
|
||||
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
|
||||
l.appendChild(h);
|
||||
doc.l10n.setAttributes(h, "tree-appearance-header");
|
||||
appearance.appendChild(l);
|
||||
addCheckbox(
|
||||
appearance,
|
||||
"tree_faviconizePinnedTabsBox",
|
||||
"browser.sidebar.faviconizePinnedTabs",
|
||||
"tree-faviconize-pinned-tabs"
|
||||
);
|
||||
}
|
||||
group.appendChild(appearance);
|
||||
|
||||
// Auto-sticky section
|
||||
const sticky = xul("groupbox", {
|
||||
id: "treeAutoStickyGroup",
|
||||
"data-category": "paneTree",
|
||||
hidden: "true",
|
||||
});
|
||||
{
|
||||
const l = xul("label");
|
||||
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
|
||||
l.appendChild(h);
|
||||
doc.l10n.setAttributes(h, "tree-auto-sticky-header");
|
||||
sticky.appendChild(l);
|
||||
|
||||
addCheckbox(
|
||||
sticky,
|
||||
"tree_stickyActiveTabBox",
|
||||
"browser.sidebar.stickyActiveTab",
|
||||
"tree-sticky-active-tab"
|
||||
);
|
||||
addCheckbox(
|
||||
sticky,
|
||||
"tree_stickySoundPlayingTabBox",
|
||||
"browser.sidebar.stickySoundPlayingTab",
|
||||
"tree-sticky-sound-playing-tab"
|
||||
);
|
||||
addCheckbox(
|
||||
sticky,
|
||||
"tree_stickySharingTabBox",
|
||||
"browser.sidebar.stickySharingTab",
|
||||
"tree-sticky-sharing-tab"
|
||||
);
|
||||
}
|
||||
group.appendChild(sticky);
|
||||
|
||||
// Behavior section
|
||||
const behavior = xul("groupbox", {
|
||||
id: "treeBehaviorGroup",
|
||||
"data-category": "paneTree",
|
||||
hidden: "true",
|
||||
});
|
||||
{
|
||||
const l = xul("label");
|
||||
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
|
||||
l.appendChild(h);
|
||||
doc.l10n.setAttributes(h, "tree-behavior-header");
|
||||
behavior.appendChild(l);
|
||||
|
||||
addCheckbox(
|
||||
behavior,
|
||||
"tree_autoCollapseExpandSubtreeOnAttachBox",
|
||||
"browser.sidebar.autoCollapseExpandSubtreeOnAttach",
|
||||
"tree-auto-collapse-expand-subtree-on-attach"
|
||||
);
|
||||
addCheckbox(
|
||||
behavior,
|
||||
"tree_autoCollapseExpandSubtreeOnSelecthBox",
|
||||
"browser.sidebar.autoCollapseExpandSubtreeOnSelect",
|
||||
"tree-auto-collapse-expand-subtree-on-select"
|
||||
);
|
||||
|
||||
// Double-click behavior menulist
|
||||
const dbl = xul("hbox", {
|
||||
id: "tree_treeDoubleClickBehaviorBox",
|
||||
align: "center",
|
||||
});
|
||||
const dblCaption = xul("label", {
|
||||
id: "tree_treeDoubleClickBehaviorCaption",
|
||||
control: "tree_treeDoubleClickBehavior",
|
||||
});
|
||||
doc.l10n.setAttributes(dblCaption, "tree-tree-double-click-behavior-caption");
|
||||
dbl.appendChild(dblCaption);
|
||||
|
||||
const dblList = xul("menulist", {
|
||||
id: "tree_treeDoubleClickBehavior",
|
||||
preference: "browser.sidebar.treeDoubleClickBehavior",
|
||||
});
|
||||
const dblPopup = xul("menupopup", { class: "in-menulist" });
|
||||
const dblOptions = [
|
||||
["1", "tree-tree-double-click-behavior-toggle-collapsed"],
|
||||
["4", "tree-tree-double-click-behavior-toggle-sticky"],
|
||||
["3", "tree-tree-double-click-behavior-toggle-close"],
|
||||
["0", "tree-tree-double-click-behavior-toggle-none"],
|
||||
];
|
||||
for (const [value, l10nId] of dblOptions) {
|
||||
const item = xul("menuitem", { value });
|
||||
doc.l10n.setAttributes(item, l10nId);
|
||||
dblPopup.appendChild(item);
|
||||
}
|
||||
dblList.appendChild(dblPopup);
|
||||
dbl.appendChild(dblList);
|
||||
behavior.appendChild(dbl);
|
||||
|
||||
// Successor control menulist
|
||||
const suc = xul("hbox", {
|
||||
id: "tree_successorTabControlLevelBox",
|
||||
align: "center",
|
||||
});
|
||||
const sucCaption = xul("label", {
|
||||
id: "tree_successorTabControlLevelCaption",
|
||||
control: "tree_successorTabControlLevel",
|
||||
});
|
||||
doc.l10n.setAttributes(sucCaption, "tree-successor-tab-control-level-caption");
|
||||
suc.appendChild(sucCaption);
|
||||
|
||||
const sucList = xul("menulist", {
|
||||
id: "tree_successorTabControlLevel",
|
||||
preference: "browser.sidebar.successorTabControlLevel",
|
||||
});
|
||||
const sucPopup = xul("menupopup", { class: "in-menulist" });
|
||||
const sucOptions = [
|
||||
["2", "tree-successor-tab-control-level-in-tree"],
|
||||
["1", "tree-successor-tab-control-level-simulate-default"],
|
||||
["0", "tree-successor-tab-control-level-never"],
|
||||
];
|
||||
for (const [value, l10nId] of sucOptions) {
|
||||
const item = xul("menuitem", { value });
|
||||
doc.l10n.setAttributes(item, l10nId);
|
||||
sucPopup.appendChild(item);
|
||||
}
|
||||
sucList.appendChild(sucPopup);
|
||||
suc.appendChild(sucList);
|
||||
behavior.appendChild(suc);
|
||||
|
||||
// Drop links behavior menulist
|
||||
const drop = xul("hbox", {
|
||||
id: "tree_dropLinksOnTabBehaviorBox",
|
||||
align: "center",
|
||||
});
|
||||
const dropCaption = xul("label", {
|
||||
id: "tree_dropLinksOnTabBehaviorCaption",
|
||||
control: "tree_dropLinksOnTabBehavior",
|
||||
});
|
||||
doc.l10n.setAttributes(dropCaption, "tree-drop-links-on-tab-behavior-caption");
|
||||
drop.appendChild(dropCaption);
|
||||
|
||||
const dropList = xul("menulist", {
|
||||
id: "tree_dropLinksOnTabBehavior",
|
||||
preference: "browser.sidebar.dropLinksOnTabBehavior",
|
||||
});
|
||||
const dropPopup = xul("menupopup", { class: "in-menulist" });
|
||||
const dropOptions = [
|
||||
["0", "tree-drop-links-on-tab-behavior-ask"],
|
||||
["1", "tree-drop-links-on-tab-behavior-load"],
|
||||
["2", "tree-drop-links-on-tab-behavior-newtab"],
|
||||
];
|
||||
for (const [value, l10nId] of dropOptions) {
|
||||
const item = xul("menuitem", { value });
|
||||
doc.l10n.setAttributes(item, l10nId);
|
||||
dropPopup.appendChild(item);
|
||||
}
|
||||
dropList.appendChild(dropPopup);
|
||||
drop.appendChild(dropList);
|
||||
behavior.appendChild(drop);
|
||||
}
|
||||
group.appendChild(behavior);
|
||||
|
||||
// Auto-attach section
|
||||
const autoAttach = xul("groupbox", {
|
||||
id: "treeAutoAttachGroup",
|
||||
"data-category": "paneTree",
|
||||
hidden: "true",
|
||||
});
|
||||
{
|
||||
const l = xul("label");
|
||||
const h = doc.createElementNS("http://www.w3.org/1999/xhtml", "h2");
|
||||
l.appendChild(h);
|
||||
doc.l10n.setAttributes(h, "tree-auto-attach-header");
|
||||
autoAttach.appendChild(l);
|
||||
|
||||
const addMenu = (rootId, captionL10n, pref, options) => {
|
||||
const box = xul("vbox", { id: rootId });
|
||||
const cap = xul("label", { id: `${rootId}Caption`, control: `${rootId}Menu` });
|
||||
doc.l10n.setAttributes(cap, captionL10n);
|
||||
box.appendChild(cap);
|
||||
const hb = xul("hbox", { id: `${rootId}MenulistBox`, align: "center", class: "sub" });
|
||||
const list = xul("menulist", { id: `${rootId.replace(/Box$/, "")}`, preference: pref });
|
||||
const popup = xul("menupopup", { class: "in-menulist" });
|
||||
for (const [value, l10nId] of options) {
|
||||
const item = xul("menuitem", { value });
|
||||
doc.l10n.setAttributes(item, l10nId);
|
||||
popup.appendChild(item);
|
||||
}
|
||||
list.appendChild(popup);
|
||||
hb.appendChild(list);
|
||||
box.appendChild(hb);
|
||||
autoAttach.appendChild(box);
|
||||
};
|
||||
|
||||
addMenu(
|
||||
"tree_autoAttachOnOpenedWithOwnerBox",
|
||||
"tree-auto-attach-on-opened-with-owner-caption",
|
||||
"browser.sidebar.autoAttachOnOpenedWithOwner",
|
||||
[
|
||||
["-1", "tree-auto-attach-no-control"],
|
||||
["0", "tree-auto-attach-independent"],
|
||||
["6", "tree-auto-attach-child-top"],
|
||||
["7", "tree-auto-attach-child-end"],
|
||||
["5", "tree-auto-attach-child-next-to-last-related-tab"],
|
||||
["2", "tree-auto-attach-sibling"],
|
||||
["3", "tree-auto-attach-next-sibling"],
|
||||
]
|
||||
);
|
||||
addMenu(
|
||||
"tree_insertNewTabFromPinnedTabAtBox",
|
||||
"tree-insert-new-tab-from-pinned-tab-at-caption",
|
||||
"browser.sidebar.insertNewTabFromPinnedTabAt",
|
||||
[
|
||||
["-1", "tree-insert-new-tab-from-pinned-tab-at-no-control"],
|
||||
["3", "tree-insert-new-tab-from-pinned-tab-at-next-to-last-related-tab"],
|
||||
["0", "tree-insert-new-tab-from-pinned-tab-at-top"],
|
||||
["1", "tree-insert-new-tab-from-pinned-tab-at-end"],
|
||||
]
|
||||
);
|
||||
addMenu(
|
||||
"tree_autoAttachOnNewTabCommandBox",
|
||||
"tree-auto-attach-on-new-tab-command-caption",
|
||||
"browser.sidebar.autoAttachOnNewTabCommand",
|
||||
[
|
||||
["-1", "tree-auto-attach-no-control"],
|
||||
["0", "tree-auto-attach-independent"],
|
||||
["6", "tree-auto-attach-child-top"],
|
||||
["7", "tree-auto-attach-child-end"],
|
||||
["2", "tree-auto-attach-sibling"],
|
||||
["3", "tree-auto-attach-next-sibling"],
|
||||
]
|
||||
);
|
||||
addMenu(
|
||||
"tree_autoAttachOnNewTabButtonMiddleClickBox",
|
||||
"tree-auto-attach-on-new-tab-button-middle-click-caption",
|
||||
"browser.sidebar.autoAttachOnNewTabButtonMiddleClick",
|
||||
[
|
||||
["-1", "tree-auto-attach-no-control"],
|
||||
["0", "tree-auto-attach-independent"],
|
||||
["6", "tree-auto-attach-child-top"],
|
||||
["7", "tree-auto-attach-child-end"],
|
||||
["2", "tree-auto-attach-sibling"],
|
||||
["3", "tree-auto-attach-next-sibling"],
|
||||
]
|
||||
);
|
||||
addMenu(
|
||||
"tree_autoAttachOnDuplicatedBox",
|
||||
"tree-auto-attach-on-duplicated-caption",
|
||||
"browser.sidebar.autoAttachOnDuplicated",
|
||||
[
|
||||
["-1", "tree-auto-attach-no-control"],
|
||||
["0", "tree-auto-attach-independent"],
|
||||
["6", "tree-auto-attach-child-top"],
|
||||
["7", "tree-auto-attach-child-end"],
|
||||
["2", "tree-auto-attach-sibling"],
|
||||
["3", "tree-auto-attach-next-sibling"],
|
||||
]
|
||||
);
|
||||
addMenu(
|
||||
"tree_autoAttachSameSiteOrphanBox",
|
||||
"tree-auto-attach-same-site-orphan-caption",
|
||||
"browser.sidebar.autoAttachSameSiteOrphan",
|
||||
[
|
||||
["-1", "tree-auto-attach-no-control"],
|
||||
["0", "tree-auto-attach-independent"],
|
||||
["6", "tree-auto-attach-child-top"],
|
||||
["7", "tree-auto-attach-child-end"],
|
||||
["2", "tree-auto-attach-sibling"],
|
||||
["3", "tree-auto-attach-next-sibling"],
|
||||
]
|
||||
);
|
||||
addMenu(
|
||||
"tree_autoAttachOnOpenedFromExternalBox",
|
||||
"tree-auto-attach-on-opened-from-external-caption",
|
||||
"browser.sidebar.autoAttachOnOpenedFromExternal",
|
||||
[
|
||||
["-1", "tree-auto-attach-no-control"],
|
||||
["0", "tree-auto-attach-independent"],
|
||||
["6", "tree-auto-attach-child-top"],
|
||||
["7", "tree-auto-attach-child-end"],
|
||||
["2", "tree-auto-attach-sibling"],
|
||||
["3", "tree-auto-attach-next-sibling"],
|
||||
]
|
||||
);
|
||||
addMenu(
|
||||
"tree_autoAttachOnAnyOtherTriggerBox",
|
||||
"tree-auto-attach-on-any-other-trigger-caption",
|
||||
"browser.sidebar.autoAttachOnAnyOtherTrigger",
|
||||
[
|
||||
["-1", "tree-auto-attach-no-control"],
|
||||
["0", "tree-auto-attach-independent"],
|
||||
["6", "tree-auto-attach-child-top"],
|
||||
["7", "tree-auto-attach-child-end"],
|
||||
["2", "tree-auto-attach-sibling"],
|
||||
["3", "tree-auto-attach-next-sibling"],
|
||||
]
|
||||
);
|
||||
}
|
||||
group.appendChild(autoAttach);
|
||||
|
||||
// Insert into DOM as a top-level pane element
|
||||
mainPane.appendChild(group);
|
||||
|
||||
// Register preferences with the Preferences binding so widgets reflect pref values
|
||||
const toRegister = [
|
||||
["browser.sidebar.faviconizePinnedTabs", "bool"],
|
||||
["browser.sidebar.stickyActiveTab", "bool"],
|
||||
["browser.sidebar.stickySoundPlayingTab", "bool"],
|
||||
["browser.sidebar.stickySharingTab", "bool"],
|
||||
["browser.sidebar.autoCollapseExpandSubtreeOnAttach", "bool"],
|
||||
["browser.sidebar.autoCollapseExpandSubtreeOnSelect", "bool"],
|
||||
["browser.sidebar.treeDoubleClickBehavior", "unichar"],
|
||||
["browser.sidebar.successorTabControlLevel", "unichar"],
|
||||
["browser.sidebar.dropLinksOnTabBehavior", "unichar"],
|
||||
["browser.sidebar.autoAttachOnOpenedWithOwner", "unichar"],
|
||||
["browser.sidebar.insertNewTabFromPinnedTabAt", "unichar"],
|
||||
["browser.sidebar.autoAttachOnNewTabCommand", "unichar"],
|
||||
["browser.sidebar.autoAttachOnNewTabButtonMiddleClick", "unichar"],
|
||||
["browser.sidebar.autoAttachOnDuplicated", "unichar"],
|
||||
["browser.sidebar.autoAttachSameSiteOrphan", "unichar"],
|
||||
["browser.sidebar.autoAttachOnOpenedFromExternal", "unichar"],
|
||||
["browser.sidebar.autoAttachOnAnyOtherTrigger", "unichar"],
|
||||
];
|
||||
for (const [id, type] of toRegister) {
|
||||
prefsWin.Preferences.add({ id, type });
|
||||
}
|
||||
|
||||
// Reveal sections for our dedicated category
|
||||
group.hidden = false;
|
||||
appearance.hidden = false;
|
||||
sticky.hidden = false;
|
||||
behavior.hidden = false;
|
||||
autoAttach.hidden = false;
|
||||
},
|
||||
};
|
||||
1042
waterfox/browser/components/sidebar/_locales/en/messages.json
Normal file
1042
waterfox/browser/components/sidebar/_locales/en/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1036
waterfox/browser/components/sidebar/_locales/ja/messages.json
Normal file
1036
waterfox/browser/components/sidebar/_locales/ja/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
15
waterfox/browser/components/sidebar/addon-jar.mn
Normal file
15
waterfox/browser/components/sidebar/addon-jar.mn
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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/.
|
||||
|
||||
browser.jar:
|
||||
% resource builtin-addons %builtin-addons/ contentaccessible=yes
|
||||
builtin-addons/sidebar/_locales/ (./_locales/**)
|
||||
builtin-addons/sidebar/background/ (./background/**)
|
||||
builtin-addons/sidebar/common/ (./common/**)
|
||||
builtin-addons/sidebar/experiments/ (./experiments/**)
|
||||
builtin-addons/sidebar/extlib/ (./extlib/**)
|
||||
builtin-addons/sidebar/options/ (./options/**)
|
||||
builtin-addons/sidebar/resources/ (./resources/**)
|
||||
builtin-addons/sidebar/sidebar/ (./sidebar/**)
|
||||
builtin-addons/sidebar/manifest.json (./manifest.json)
|
||||
1402
waterfox/browser/components/sidebar/background/api-tabs-listener.js
Normal file
1402
waterfox/browser/components/sidebar/background/api-tabs-listener.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
configs,
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
configs.$addObserver(key => {
|
||||
switch (key) {
|
||||
case 'stickyActiveTab':
|
||||
updateAutoStickyActive();
|
||||
break;
|
||||
|
||||
case 'stickySoundPlayingTab':
|
||||
updateAutoStickySoundPlaying();
|
||||
break;
|
||||
|
||||
case 'stickySharingTab':
|
||||
updateAutoStickySharing();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
configs.$loaded.then(() => {
|
||||
updateAutoStickyActive();
|
||||
updateAutoStickySoundPlaying();
|
||||
updateAutoStickySharing();
|
||||
});
|
||||
|
||||
function updateAutoStickyActive() {
|
||||
if (configs.stickyActiveTab)
|
||||
Tab.registerAutoStickyState(Constants.kTAB_STATE_ACTIVE);
|
||||
else
|
||||
Tab.unregisterAutoStickyState(Constants.kTAB_STATE_ACTIVE);
|
||||
}
|
||||
|
||||
function updateAutoStickySoundPlaying() {
|
||||
if (configs.stickySoundPlayingTab)
|
||||
Tab.registerAutoStickyState(Constants.kTAB_STATE_SOUND_PLAYING);
|
||||
else
|
||||
Tab.unregisterAutoStickyState(Constants.kTAB_STATE_SOUND_PLAYING);
|
||||
}
|
||||
|
||||
function updateAutoStickySharing() {
|
||||
if (configs.stickySharingTab)
|
||||
Tab.registerAutoStickyState([
|
||||
Constants.kTAB_STATE_SHARING_CAMERA,
|
||||
Constants.kTAB_STATE_SHARING_MICROPHONE,
|
||||
Constants.kTAB_STATE_SHARING_SCREEN,
|
||||
]);
|
||||
else
|
||||
Tab.unregisterAutoStickyState([
|
||||
Constants.kTAB_STATE_SHARING_CAMERA,
|
||||
Constants.kTAB_STATE_SHARING_MICROPHONE,
|
||||
Constants.kTAB_STATE_SHARING_SCREEN,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import { SequenceMatcher } from '/extlib/diff.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
wait,
|
||||
mapAndFilter,
|
||||
configs
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as CacheStorage from '/common/cache-storage.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TabsUpdate from '/common/tabs-update.js';
|
||||
import * as UniqueId from '/common/unique-id.js';
|
||||
|
||||
import MetricsData from '/common/MetricsData.js';
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/background-cache', ...args);
|
||||
}
|
||||
|
||||
const kCONTENTS_VERSION = 5;
|
||||
|
||||
let mActivated = false;
|
||||
const mCaches = {};
|
||||
|
||||
export function activate() {
|
||||
mActivated = true;
|
||||
configs.$addObserver(onConfigChange);
|
||||
|
||||
if (!configs.persistCachedTree) {
|
||||
// clear obsolete cache
|
||||
browser.windows.getAll().then(windows => {
|
||||
for (const win of windows) {
|
||||
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_TABS).catch(ApiTabs.createErrorSuppressor());
|
||||
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY).catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===================================================================
|
||||
// restoring tabs from cache
|
||||
// ===================================================================
|
||||
|
||||
export async function restoreWindowFromEffectiveWindowCache(windowId, options = {}) {
|
||||
MetricsData.add('restoreWindowFromEffectiveWindowCache: start');
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId} start`);
|
||||
const owner = options.owner || getWindowCacheOwner(windowId);
|
||||
if (!owner) {
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId} fail: no owner`);
|
||||
return false;
|
||||
}
|
||||
cancelReservedCacheTree(windowId); // prevent to break cache before loading
|
||||
const tabs = options.tabs || await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler());
|
||||
if (configs.debug)
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId} tabs: `, () => tabs.map(dumpTab));
|
||||
const actualSignature = getWindowSignature(tabs);
|
||||
let cache = options.caches?.get(`window-${owner.windowId}`) || await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: window cache', getWindowCache(owner, Constants.kWINDOW_STATE_CACHED_TABS));
|
||||
if (!cache) {
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId} fail: no cache`);
|
||||
return false;
|
||||
}
|
||||
const promisedPermanentStates = Promise.all(tabs.map(tab => Tab.get(tab.id).$TST.getPermanentStates())); // don't await at here for better performance
|
||||
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: start');
|
||||
let cachedSignature = cache?.signature;
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: got from the owner `, {
|
||||
owner, cachedSignature, cache
|
||||
});
|
||||
const signatureGeneratedFromCache = getWindowSignature(cache.tabs).join('\n');
|
||||
if (cache &&
|
||||
cache.tabs &&
|
||||
cachedSignature &&
|
||||
cachedSignature.join('\n') != signatureGeneratedFromCache) {
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: cache is broken.`, {
|
||||
cachedSignature: cachedSignature.join('\n'),
|
||||
signatureGeneratedFromCache
|
||||
});
|
||||
cache = cachedSignature = null;
|
||||
TabsInternalOperation.clearCache(owner);
|
||||
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: signature failed.');
|
||||
}
|
||||
else {
|
||||
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: signature passed.');
|
||||
}
|
||||
if (options.ignorePinnedTabs &&
|
||||
cache &&
|
||||
cache.tabs &&
|
||||
cachedSignature) {
|
||||
cache.tabs = trimTabsCache(cache.tabs, cache.pinnedTabsCount);
|
||||
cachedSignature = trimSignature(cachedSignature, cache.pinnedTabsCount);
|
||||
}
|
||||
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: matching actual signature of got cache');
|
||||
const signatureMatchResult = matcheSignatures({
|
||||
actual: actualSignature,
|
||||
cached: cachedSignature
|
||||
});
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: verify cache`, {
|
||||
cache, actualSignature, cachedSignature,
|
||||
...signatureMatchResult,
|
||||
});
|
||||
if (!cache ||
|
||||
cache.version != kCONTENTS_VERSION ||
|
||||
!signatureMatchResult.matched) {
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: no effective cache`);
|
||||
TabsInternalOperation.clearCache(owner);
|
||||
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: actual signature failed.');
|
||||
return false;
|
||||
}
|
||||
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: actual signature passed.');
|
||||
cache.offset = signatureMatchResult.offset;
|
||||
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: restore from cache`);
|
||||
|
||||
const permanentStates = await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: permanentStatus', promisedPermanentStates); // await at here for better performance
|
||||
const restored = await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: restoreTabsFromCache', restoreTabsFromCache(windowId, { cache, tabs, permanentStates }));
|
||||
if (restored) {
|
||||
MetricsData.add(`restoreWindowFromEffectiveWindowCache: window ${windowId} succeeded`);
|
||||
// Now we reload the sidebar if it is opened, because it is the easiest way
|
||||
// to synchronize state of tabs completely.
|
||||
log('reload sidebar for a tree restored from cache');
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_RELOAD,
|
||||
windowId,
|
||||
}).catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
else {
|
||||
MetricsData.add(`restoreWindowFromEffectiveWindowCache: window ${windowId} failed`);
|
||||
}
|
||||
|
||||
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: restored = ${restored}`);
|
||||
return restored;
|
||||
}
|
||||
|
||||
function getWindowSignature(tabs) {
|
||||
return tabs.map(tab => `${tab.cookieStoreId},${tab.incognito},${tab.pinned},${tab.url}`);
|
||||
}
|
||||
|
||||
function trimSignature(signature, ignoreCount) {
|
||||
if (!ignoreCount || ignoreCount < 0)
|
||||
return signature;
|
||||
return signature.slice(ignoreCount);
|
||||
}
|
||||
|
||||
function trimTabsCache(cache, ignoreCount) {
|
||||
if (!ignoreCount || ignoreCount < 0)
|
||||
return cache;
|
||||
return cache.slice(ignoreCount);
|
||||
}
|
||||
|
||||
function matcheSignatures(signatures) {
|
||||
const operations = (new SequenceMatcher(signatures.cached, signatures.actual)).operations();
|
||||
log('matcheSignatures: operations ', operations);
|
||||
let matched = false;
|
||||
let offset = 0;
|
||||
for (const operation of operations) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
if (tag == 'equal' &&
|
||||
fromEnd - fromStart == signatures.cached.length) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
offset += toEnd - toStart;
|
||||
}
|
||||
log('matcheSignatures: ', { matched, offset });
|
||||
return { matched, offset };
|
||||
}
|
||||
|
||||
|
||||
async function restoreTabsFromCache(windowId, params = {}) {
|
||||
if (!params.cache ||
|
||||
params.cache.version != kCONTENTS_VERSION)
|
||||
return false;
|
||||
|
||||
return (await restoreTabsFromCacheInternal({
|
||||
windowId,
|
||||
tabs: params.tabs,
|
||||
permanentStates: params.permanentStates,
|
||||
offset: params.cache.offset || 0,
|
||||
cache: params.cache.tabs
|
||||
})).length > 0;
|
||||
}
|
||||
|
||||
async function restoreTabsFromCacheInternal(params) {
|
||||
MetricsData.add('restoreTabsFromCacheInternal: start');
|
||||
log(`restoreTabsFromCacheInternal: restore tabs for ${params.windowId} from cache`);
|
||||
const offset = params.offset || 0;
|
||||
const win = TabsStore.windows.get(params.windowId);
|
||||
const tabs = params.tabs.slice(offset).map(tab => Tab.get(tab.id));
|
||||
if (offset > 0 &&
|
||||
tabs.length <= offset) {
|
||||
log('restoreTabsFromCacheInternal: missing window');
|
||||
return [];
|
||||
}
|
||||
log(`restoreTabsFromCacheInternal: there is ${win.tabs.size} tabs`);
|
||||
if (params.cache.length != tabs.length) {
|
||||
log('restoreTabsFromCacheInternal: Mismatched number of restored tabs?');
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
await MetricsData.addAsync('rebuildAll: fixupTabsRestoredFromCache', fixupTabsRestoredFromCache(tabs, params.permanentStates, params.cache));
|
||||
}
|
||||
catch(e) {
|
||||
log(String(e), e.stack);
|
||||
throw e;
|
||||
}
|
||||
log('restoreTabsFromCacheInternal: done');
|
||||
if (configs.debug)
|
||||
Tab.dumpAll();
|
||||
return tabs;
|
||||
}
|
||||
|
||||
async function fixupTabsRestoredFromCache(tabs, permanentStates, cachedTabs) {
|
||||
MetricsData.add('fixupTabsRestoredFromCache: start');
|
||||
if (tabs.length != cachedTabs.length)
|
||||
throw new Error(`fixupTabsRestoredFromCache: Mismatched number of tabs restored from cache, tabs=${tabs.length}, cachedTabs=${cachedTabs.length}`);
|
||||
log('fixupTabsRestoredFromCache start ', () => ({ tabs: tabs.map(dumpTab), cachedTabs }));
|
||||
const idMap = new Map();
|
||||
let remappedCount = 0;
|
||||
// step 1: build a map from old id to new id
|
||||
tabs = tabs.map((tab, index) => {
|
||||
const cachedTab = cachedTabs[index];
|
||||
const oldId = cachedTab.id;
|
||||
tab = Tab.get(tab.id);
|
||||
log(`fixupTabsRestoredFromCache: remap ${oldId} => ${tab.id}`);
|
||||
idMap.set(oldId, tab);
|
||||
if (oldId != tab.id)
|
||||
remappedCount++;
|
||||
return tab;
|
||||
});
|
||||
if (remappedCount && remappedCount < tabs.length)
|
||||
throw new Error(`fixupTabsRestoredFromCache: not a window restoration, only ${remappedCount} tab(s) are restored (maybe restoration of closed tabs)`);
|
||||
MetricsData.add('fixupTabsRestoredFromCache: step 1 done.');
|
||||
// step 2: restore information of tabs
|
||||
// Do this from bottom to top, to reduce post operations for modified trees.
|
||||
// (Attaching a tab to an existing tree will trigger "update" task for
|
||||
// existing ancestors, but attaching existing subtree to a solo tab won't
|
||||
// trigger such tasks.)
|
||||
// See also: https://github.com/piroor/treestyletab/issues/2278#issuecomment-519387792
|
||||
for (let i = tabs.length - 1; i > -1; i--) {
|
||||
fixupTabRestoredFromCache(tabs[i], permanentStates[i], cachedTabs[i], idMap);
|
||||
}
|
||||
// step 3: restore collapsed/expanded state of tabs and finalize the
|
||||
// restoration process
|
||||
// Do this from top to bottom, because a tab can be placed under an
|
||||
// expanded parent but the parent can be placed under a collapsed parent.
|
||||
for (const tab of tabs) {
|
||||
fixupTabRestoredFromCachePostProcess(tab);
|
||||
}
|
||||
MetricsData.add('fixupTabsRestoredFromCache: step 2 done.');
|
||||
}
|
||||
|
||||
function fixupTabRestoredFromCache(tab, permanentStates, cachedTab, idMap) {
|
||||
tab.$TST.clear();
|
||||
const tabStates = new Set([...cachedTab.$TST.states, ...permanentStates]);
|
||||
for (const state of Constants.kTAB_TEMPORARY_STATES) {
|
||||
tabStates.delete(state);
|
||||
}
|
||||
tab.$TST.states = tabStates;
|
||||
tab.$TST.attributes = cachedTab.$TST.attributes;
|
||||
|
||||
log('fixupTabRestoredFromCache children: ', cachedTab.$TST.childIds);
|
||||
const childIds = mapAndFilter(cachedTab.$TST.childIds, oldId => {
|
||||
const tab = idMap.get(oldId);
|
||||
return tab?.id || undefined;
|
||||
});
|
||||
tab.$TST.children = childIds;
|
||||
if (childIds.length > 0)
|
||||
tab.$TST.setAttribute(Constants.kCHILDREN, `|${childIds.join('|')}|`);
|
||||
else
|
||||
tab.$TST.removeAttribute(Constants.kCHILDREN);
|
||||
log('fixupTabRestoredFromCache children: => ', tab.$TST.childIds);
|
||||
|
||||
log('fixupTabRestoredFromCache parent: ', cachedTab.$TST.parentId);
|
||||
const parentTab = idMap.get(cachedTab.$TST.parentId) || null;
|
||||
tab.$TST.parent = parentTab;
|
||||
if (parentTab)
|
||||
tab.$TST.setAttribute(Constants.kPARENT, parentTab.id);
|
||||
else
|
||||
tab.$TST.removeAttribute(Constants.kPARENT);
|
||||
log('fixupTabRestoredFromCache parent: => ', tab.$TST.parentId);
|
||||
|
||||
if (tab.discarded) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_PENDING);
|
||||
}
|
||||
|
||||
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
|
||||
}
|
||||
|
||||
function fixupTabRestoredFromCachePostProcess(tab) {
|
||||
const parentTab = tab.$TST.parent;
|
||||
if (parentTab &&
|
||||
(parentTab.$TST.collapsed ||
|
||||
parentTab.$TST.subtreeCollapsed)) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_COLLAPSED);
|
||||
tab.$TST.addState(Constants.kTAB_STATE_COLLAPSED_DONE);
|
||||
}
|
||||
else {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_COLLAPSED);
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_COLLAPSED_DONE);
|
||||
}
|
||||
|
||||
TabsStore.updateIndexesForTab(tab);
|
||||
TabsUpdate.updateTab(tab, tab, { forceApply: true, onlyApply: true });
|
||||
}
|
||||
|
||||
|
||||
// ===================================================================
|
||||
// updating cache
|
||||
// ===================================================================
|
||||
|
||||
async function updateWindowCache(owner, key, value) {
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
if (configs.persistCachedTree) {
|
||||
try {
|
||||
if (value)
|
||||
await CacheStorage.setValue({
|
||||
windowId: owner.windowId,
|
||||
key,
|
||||
value,
|
||||
store: CacheStorage.BACKGROUND,
|
||||
});
|
||||
else
|
||||
await CacheStorage.deleteValue({
|
||||
windowId: owner.windowId,
|
||||
key,
|
||||
store: CacheStorage.BACKGROUND,
|
||||
});
|
||||
return;
|
||||
}
|
||||
catch(error) {
|
||||
console.log(`BackgroundCache.updateWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error);
|
||||
}
|
||||
}
|
||||
|
||||
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
|
||||
if (value)
|
||||
mCaches[storageKey] = value;
|
||||
else
|
||||
delete mCaches[storageKey];
|
||||
}
|
||||
|
||||
export function markWindowCacheDirtyFromTab(tab, akey) {
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
if (!win) // the window may be closed
|
||||
return;
|
||||
if (win.markWindowCacheDirtyFromTabTimeout)
|
||||
clearTimeout(win.markWindowCacheDirtyFromTabTimeout);
|
||||
win.markWindowCacheDirtyFromTabTimeout = setTimeout(() => {
|
||||
win.markWindowCacheDirtyFromTabTimeout = null;
|
||||
updateWindowCache(win.lastWindowCacheOwner, akey, true);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function getWindowCache(owner, key) {
|
||||
if (configs.persistCachedTree) {
|
||||
try {
|
||||
const value = await CacheStorage.getValue({
|
||||
windowId: owner.windowId,
|
||||
key,
|
||||
store: CacheStorage.BACKGROUND,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
catch(error) {
|
||||
console.log(`BackgroundCache.getWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error);
|
||||
}
|
||||
}
|
||||
|
||||
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
|
||||
return mCaches[storageKey];
|
||||
}
|
||||
|
||||
function getWindowCacheOwner(windowId) {
|
||||
const tab = Tab.getFirstTab(windowId);
|
||||
if (!tab)
|
||||
return null;
|
||||
return {
|
||||
id: tab.id,
|
||||
windowId: tab.windowId
|
||||
};
|
||||
}
|
||||
|
||||
export async function reserveToCacheTree(windowId, trigger) {
|
||||
if (!mActivated ||
|
||||
!configs.useCachedTree)
|
||||
return;
|
||||
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (!win)
|
||||
return;
|
||||
|
||||
// If there is any opening (but not resolved its unique id yet) tab,
|
||||
// we are possibly restoring tabs. To avoid cache breakage before
|
||||
// restoration, we must wait until we know whether there is any other
|
||||
// restoring tab or not.
|
||||
if (Tab.needToWaitTracked(windowId))
|
||||
await Tab.waitUntilTrackedAll(windowId);
|
||||
|
||||
if (win.promisedAllTabsRestored) // not restored yet
|
||||
return;
|
||||
|
||||
if (!trigger && configs.debug)
|
||||
trigger = new Error().stack;
|
||||
|
||||
log('reserveToCacheTree for window ', windowId, trigger);
|
||||
TabsInternalOperation.clearCache(win.lastWindowCacheOwner);
|
||||
|
||||
if (trigger)
|
||||
reserveToCacheTree.triggers.add(trigger);
|
||||
|
||||
if (win.waitingToCacheTree)
|
||||
clearTimeout(win.waitingToCacheTree);
|
||||
win.waitingToCacheTree = setTimeout(() => {
|
||||
const triggers = [...reserveToCacheTree.triggers];
|
||||
reserveToCacheTree.triggers.clear();
|
||||
cacheTree(windowId, triggers);
|
||||
}, 500);
|
||||
}
|
||||
reserveToCacheTree.triggers = new Set();
|
||||
|
||||
function cancelReservedCacheTree(windowId) {
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (win?.waitingToCacheTree) {
|
||||
clearTimeout(win.waitingToCacheTree);
|
||||
delete win.waitingToCacheTree;
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheTree(windowId, triggers) {
|
||||
if (Tab.needToWaitTracked(windowId))
|
||||
await Tab.waitUntilTrackedAll(windowId);
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (!win ||
|
||||
!configs.useCachedTree)
|
||||
return;
|
||||
const signature = getWindowSignature(Tab.getAllTabs(windowId));
|
||||
if (win.promisedAllTabsRestored) // not restored yet
|
||||
return;
|
||||
//log('save cache for ', windowId);
|
||||
win.lastWindowCacheOwner = getWindowCacheOwner(windowId);
|
||||
if (!win.lastWindowCacheOwner)
|
||||
return;
|
||||
const firstTab = Tab.getFirstTab(windowId);
|
||||
if (firstTab.incognito) { // never save cache for incognito windows
|
||||
updateWindowCache(win.lastWindowCacheOwner, Constants.kWINDOW_STATE_CACHED_TABS, null);
|
||||
return;
|
||||
}
|
||||
log('cacheTree for window ', windowId, triggers/*{ stack: configs.debug && new Error().stack }*/);
|
||||
updateWindowCache(win.lastWindowCacheOwner, Constants.kWINDOW_STATE_CACHED_TABS, {
|
||||
version: kCONTENTS_VERSION,
|
||||
tabs: TabsStore.windows.get(windowId).export(true).tabs,
|
||||
pinnedTabsCount: Tab.getPinnedTabs(windowId).length,
|
||||
signature
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// update cache on events
|
||||
|
||||
Tab.onCreated.addListener((tab, _info = {}) => {
|
||||
if (!tab.$TST.previousTab) { // it is a new cache owner
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
if (win.lastWindowCacheOwner)
|
||||
TabsInternalOperation.clearCache(win.lastWindowCacheOwner);
|
||||
}
|
||||
reserveToCacheTree(tab.windowId, 'tab created');
|
||||
});
|
||||
|
||||
// Tree restoration for "Restore Previous Session"
|
||||
Tab.onWindowRestoring.addListener(async ({ windowId, restoredCount }) => {
|
||||
if (!configs.useCachedTree)
|
||||
return;
|
||||
|
||||
log('Tabs.onWindowRestoring ', { windowId, restoredCount });
|
||||
if (restoredCount == 1) {
|
||||
log('Tabs.onWindowRestoring: single tab restored');
|
||||
return;
|
||||
}
|
||||
|
||||
log('Tabs.onWindowRestoring: continue ', windowId);
|
||||
MetricsData.add('Tabs.onWindowRestoring restore start');
|
||||
|
||||
const tabs = await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler());
|
||||
try {
|
||||
await restoreWindowFromEffectiveWindowCache(windowId, {
|
||||
ignorePinnedTabs: true,
|
||||
owner: tabs[tabs.length - 1],
|
||||
tabs
|
||||
});
|
||||
MetricsData.add('Tabs.onWindowRestoring restore end');
|
||||
}
|
||||
catch(e) {
|
||||
log('Tabs.onWindowRestoring: FATAL ERROR while restoring tree from cache', String(e), e.stack);
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onRemoved.addListener((tab, info) => {
|
||||
if (!tab.$TST.previousTab) // the tab was the cache owner
|
||||
TabsInternalOperation.clearCache(tab);
|
||||
wait(0).then(() => {
|
||||
// "Restore Previous Session" closes some tabs at first, so we should not clear the old cache yet.
|
||||
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
|
||||
reserveToCacheTree(info.windowId, 'tab removed');
|
||||
});
|
||||
});
|
||||
|
||||
Tab.onMoved.addListener((tab, info) => {
|
||||
if (info.fromIndex == 0) // the tab is not the cache owner anymore
|
||||
TabsInternalOperation.clearCache(tab);
|
||||
reserveToCacheTree(info.windowId, 'tab moved');
|
||||
});
|
||||
|
||||
Tab.onUpdated.addListener((tab, info) => {
|
||||
markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
|
||||
if ('url' in info)
|
||||
reserveToCacheTree(tab.windowId, 'tab updated');
|
||||
});
|
||||
|
||||
Tab.onStateChanged.addListener((tab, state, _has) => {
|
||||
if (state == Constants.kTAB_STATE_STICKY)
|
||||
markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
|
||||
});
|
||||
|
||||
Tree.onSubtreeCollapsedStateChanging.addListener(tab => {
|
||||
reserveToCacheTree(tab.windowId, 'subtree collapsed/expanded');
|
||||
});
|
||||
|
||||
Tree.onAttached.addListener((tab, _info) => {
|
||||
wait(0).then(() => {
|
||||
// "Restore Previous Session" closes some tabs at first and it causes tree changes, so we should not clear the old cache yet.
|
||||
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
|
||||
reserveToCacheTree(tab.windowId, 'tab attached to tree');
|
||||
});
|
||||
});
|
||||
|
||||
Tree.onDetached.addListener((tab, _info) => {
|
||||
TabsInternalOperation.clearCache(tab);
|
||||
wait(0).then(() => {
|
||||
// "Restore Previous Session" closes some tabs at first and it causes tree changes, so we should not clear the old cache yet.
|
||||
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
|
||||
reserveToCacheTree(tab.windowId, 'tab detached from tree');
|
||||
});
|
||||
});
|
||||
|
||||
Tab.onPinned.addListener(tab => {
|
||||
reserveToCacheTree(tab.windowId, 'tab pinned');
|
||||
});
|
||||
|
||||
Tab.onUnpinned.addListener(tab => {
|
||||
if (tab.$TST.previousTab) // the tab was the cache owner
|
||||
TabsInternalOperation.clearCache(tab);
|
||||
reserveToCacheTree(tab.windowId, 'tab unpinned');
|
||||
});
|
||||
|
||||
Tab.onShown.addListener(tab => {
|
||||
reserveToCacheTree(tab.windowId, 'tab shown');
|
||||
});
|
||||
|
||||
Tab.onHidden.addListener(tab => {
|
||||
reserveToCacheTree(tab.windowId, 'tab hidden');
|
||||
});
|
||||
|
||||
browser.windows.onRemoved.addListener(async windowId => {
|
||||
try {
|
||||
CacheStorage.clearForWindow(windowId);
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
|
||||
const storageKeyPart = `Cache-${await UniqueId.ensureWindowId(windowId)}-`;
|
||||
for (const key in mCaches) {
|
||||
if (key.includes(storageKeyPart))
|
||||
delete mCaches[key];
|
||||
}
|
||||
});
|
||||
|
||||
function onConfigChange(key) {
|
||||
switch (key) {
|
||||
case 'useCachedTree':
|
||||
case 'persistCachedTree':
|
||||
browser.windows.getAll({
|
||||
populate: true,
|
||||
windowTypes: ['normal']
|
||||
}).then(windows => {
|
||||
for (const win of windows) {
|
||||
const owner = win.tabs[win.tabs.length - 1];
|
||||
if (configs[key]) {
|
||||
reserveToCacheTree(win.id, 'config change');
|
||||
}
|
||||
else {
|
||||
TabsInternalOperation.clearCache(owner);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
}).catch(ApiTabs.createErrorSuppressor());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script type="module" src="./index-ws.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
928
waterfox/browser/components/sidebar/background/background.js
Normal file
928
waterfox/browser/components/sidebar/background/background.js
Normal file
@@ -0,0 +1,928 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
wait,
|
||||
configs,
|
||||
sanitizeForHTMLText,
|
||||
waitUntilStartupOperationsUnblocked,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as ContextualIdentities from '/common/contextual-identities.js';
|
||||
import * as Dialog from '/common/dialog.js';
|
||||
import * as Permissions from '/common/permissions.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
import * as Sync from '/common/sync.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TabsUpdate from '/common/tabs-update.js';
|
||||
import * as TSTAPI from '/common/tst-api.js';
|
||||
import * as UniqueId from '/common/unique-id.js';
|
||||
import '/common/bookmark.js'; // we need to load this once in the background page to register the global listener
|
||||
|
||||
import MetricsData from '/common/MetricsData.js';
|
||||
import { Tab, TabGroup } from '/common/TreeItem.js';
|
||||
import Window from '/common/Window.js';
|
||||
|
||||
import * as ApiTabsListener from './api-tabs-listener.js';
|
||||
import * as BackgroundCache from './background-cache.js';
|
||||
import * as Commands from './commands.js';
|
||||
import * as ContextMenu from './context-menu.js';
|
||||
import * as Migration from './migration.js';
|
||||
import * as NativeTabGroups from './native-tab-groups.js';
|
||||
import * as TabContextMenu from './tab-context-menu.js';
|
||||
import * as Tree from './tree.js';
|
||||
import * as TreeStructure from './tree-structure.js';
|
||||
import './browser-action-menu.js';
|
||||
import './duplicated-tab-detection.js';
|
||||
import './successor-tab.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/background', ...args);
|
||||
}
|
||||
|
||||
// This needs to be large enough for bulk updates on multiple tabs.
|
||||
const DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS = 250;
|
||||
|
||||
export const onInit = new EventListenerManager();
|
||||
export const onBuilt = new EventListenerManager();
|
||||
export const onReady = new EventListenerManager();
|
||||
export const onDestroy = new EventListenerManager();
|
||||
export const onTreeCompletelyAttached = new EventListenerManager();
|
||||
|
||||
export const instanceId = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
|
||||
|
||||
const mDarkModeMatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
let mInitialized = false;
|
||||
const mPreloadedCaches = new Map();
|
||||
|
||||
async function getAllWindows() {
|
||||
const [windows, tabGroups] = await Promise.all([
|
||||
browser.windows.getAll({
|
||||
populate: true,
|
||||
// We need to track all type windows because
|
||||
// popup windows can be destination of tabs.move().
|
||||
// See also: https://github.com/piroor/treestyletab/issues/3311
|
||||
windowTypes: ['normal', 'panel', 'popup'],
|
||||
}).catch(ApiTabs.createErrorHandler()),
|
||||
browser.tabGroups.query({}),
|
||||
]);
|
||||
|
||||
const groupsByWindow = new Map();
|
||||
for (const group of tabGroups) {
|
||||
const groupsInWindow = groupsByWindow.get(group.windowId) || [];
|
||||
groupsInWindow.push(group);
|
||||
groupsByWindow.set(group.windowId, groupsInWindow)
|
||||
}
|
||||
|
||||
for (const win of windows) {
|
||||
win.tabGroups = groupsByWindow.get(win.id) || [];
|
||||
}
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
log('init: Start queuing of messages notified via WE APIs');
|
||||
ApiTabsListener.init();
|
||||
const promisedRestored = waitUntilCompletelyRestored(); // this must be called synchronosly
|
||||
|
||||
export async function init() {
|
||||
log('init: start');
|
||||
MetricsData.add('init: start');
|
||||
window.addEventListener('pagehide', destroy, { once: true });
|
||||
|
||||
onInit.dispatch();
|
||||
SidebarConnection.init();
|
||||
|
||||
// Read caches from existing tabs at first, for better performance.
|
||||
// Those promises will be resolved while waiting for waitUntilCompletelyRestored().
|
||||
getAllWindows()
|
||||
.then(windows => {
|
||||
for (const win of windows) {
|
||||
browser.sessions.getWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_TABS)
|
||||
.catch(ApiTabs.createErrorSuppressor())
|
||||
.then(cache => mPreloadedCaches.set(`window-${win.id}`, cache));
|
||||
const tab = win.tabs[0];
|
||||
browser.sessions.getTabValue(tab.id, Constants.kWINDOW_STATE_CACHED_TABS)
|
||||
.catch(ApiTabs.createErrorSuppressor())
|
||||
.then(cache => mPreloadedCaches.set(`tab-${tab.id}`, cache));
|
||||
}
|
||||
});
|
||||
|
||||
let promisedWindows;
|
||||
log('init: Getting existing windows and tabs');
|
||||
await MetricsData.addAsync('init: waiting for waitUntilCompletelyRestored, ContextualIdentities.init and configs.$loaded', Promise.all([
|
||||
promisedRestored.then(() => {
|
||||
// don't wait at here for better performance
|
||||
promisedWindows = getAllWindows();
|
||||
}),
|
||||
ContextualIdentities.init(),
|
||||
configs.$loaded.then(waitUntilStartupOperationsUnblocked),
|
||||
]));
|
||||
MetricsData.add('init: prepare');
|
||||
EventListenerManager.debug = configs.debug;
|
||||
|
||||
Migration.migrateConfigs();
|
||||
Migration.migrateBookmarkUrls();
|
||||
configs.grantedRemovingTabIds = []; // clear!
|
||||
MetricsData.add('init: Migration.migrateConfigs');
|
||||
|
||||
updatePanelUrl();
|
||||
|
||||
const windows = await MetricsData.addAsync('init: getting all tabs across windows', promisedWindows); // wait at here for better performance
|
||||
const restoredFromCache = await MetricsData.addAsync('init: rebuildAll', rebuildAll(windows));
|
||||
mPreloadedCaches.clear();
|
||||
await MetricsData.addAsync('init: TreeStructure.loadTreeStructure', TreeStructure.loadTreeStructure(windows, restoredFromCache));
|
||||
|
||||
log('init: Start to process messages including queued ones');
|
||||
ApiTabsListener.start();
|
||||
|
||||
Migration.tryNotifyNewFeatures();
|
||||
|
||||
ContextualIdentities.startObserve();
|
||||
onBuilt.dispatch(); // after this line, this master process may receive "kCOMMAND_PING_TO_BACKGROUND" requests from sidebars.
|
||||
MetricsData.add('init: started listening');
|
||||
|
||||
TabContextMenu.init();
|
||||
ContextMenu.init().then(() => updateIconForBrowserTheme());
|
||||
MetricsData.add('init: started initializing of context menu');
|
||||
|
||||
Permissions.clearRequest();
|
||||
|
||||
for (const windowId of restoredFromCache.keys()) {
|
||||
if (!restoredFromCache[windowId])
|
||||
BackgroundCache.reserveToCacheTree(windowId, 'initialize');
|
||||
TabsUpdate.completeLoadingTabs(windowId);
|
||||
}
|
||||
|
||||
for (const tab of Tab.getAllTabs(null, { iterator: true })) {
|
||||
updateSubtreeCollapsed(tab);
|
||||
}
|
||||
for (const tab of Tab.getActiveTabs()) {
|
||||
for (const ancestor of tab.$TST.ancestors) {
|
||||
Tree.collapseExpandTabAndSubtree(ancestor, {
|
||||
collapsed: false,
|
||||
justNow: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// we don't need to await that for the initialization of TST itself.
|
||||
MetricsData.addAsync('init: initializing API for other addons', TSTAPI.initAsBackend());
|
||||
|
||||
mInitialized = true;
|
||||
UniqueId.readyToDetectDuplicatedTab();
|
||||
Tab.broadcastState.enabled = true;
|
||||
onReady.dispatch();
|
||||
BackgroundCache.activate();
|
||||
TreeStructure.startTracking();
|
||||
|
||||
Sync.init();
|
||||
|
||||
await NativeTabGroups.startToMaintainTree();
|
||||
|
||||
await MetricsData.addAsync('init: exporting tabs to sidebars', notifyReadyToSidebars());
|
||||
|
||||
log(`Startup metrics for ${TabsStore.tabs.size} tabs: `, MetricsData.toString());
|
||||
}
|
||||
|
||||
async function notifyReadyToSidebars() {
|
||||
log('notifyReadyToSidebars: start');
|
||||
const promisedResults = [];
|
||||
for (const win of TabsStore.windows.values()) {
|
||||
// Send PING to all windows whether they are detected as opened or not, because
|
||||
// the connection may be established before this background page starts listening
|
||||
// of messages from sidebar pages.
|
||||
// See also: https://github.com/piroor/treestyletab/issues/2200
|
||||
TabsUpdate.completeLoadingTabs(win.id); // failsafe
|
||||
log(`notifyReadyToSidebars: to ${win.id}`);
|
||||
promisedResults.push(browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_BACKGROUND_READY,
|
||||
windowId: win.id,
|
||||
exported: win.export(true), // send tabs together to optimizie further initialization tasks in the sidebar
|
||||
}).catch(ApiTabs.createErrorSuppressor()));
|
||||
}
|
||||
return Promise.all(promisedResults);
|
||||
}
|
||||
|
||||
async function updatePanelUrl(theme) {
|
||||
const url = new URL(Constants.kSHORTHAND_URIS.tabbar);
|
||||
url.searchParams.set('style', configs.style);
|
||||
url.searchParams.set('reloadMaskImage', !!configs.enableWorkaroundForBug1763420_reloadMaskImage);
|
||||
if (!theme)
|
||||
theme = await browser.theme.getCurrent();
|
||||
if (browser.sidebarAction)
|
||||
browser.sidebarAction.setPanel({ panel: url.href });
|
||||
/*
|
||||
const url = new URL(Constants.kSHORTHAND_URIS.tabbar);
|
||||
url.searchParams.set('style', configs.style);
|
||||
if (browser.sidebarAction)
|
||||
browser.sidebarAction.setPanel({ panel: url.href });
|
||||
*/
|
||||
}
|
||||
|
||||
async function waitUntilCompletelyRestored() {
|
||||
log('waitUntilCompletelyRestored');
|
||||
const initialTabs = await browser.tabs.query({});
|
||||
await Promise.all([
|
||||
MetricsData.addAsync('waitUntilCompletelyRestored: existing tabs ', Promise.all(
|
||||
initialTabs.map(tab => waitUntilPersistentIdBecomeAvailable(tab.id).catch(_error => {}))
|
||||
)),
|
||||
MetricsData.addAsync('waitUntilCompletelyRestored: opening tabs ', new Promise((resolve, _reject) => {
|
||||
let promises = [];
|
||||
let timeout;
|
||||
let resolver;
|
||||
let onNewTabRestored = async (tab, _info = {}) => {
|
||||
clearTimeout(timeout);
|
||||
log('new restored tab is detected.');
|
||||
promises.push(waitUntilPersistentIdBecomeAvailable(tab.id).catch(_error => {}));
|
||||
// Read caches from restored tabs while waiting, for better performance.
|
||||
browser.sessions.getWindowValue(tab.windowId, Constants.kWINDOW_STATE_CACHED_TABS)
|
||||
.catch(ApiTabs.createErrorSuppressor())
|
||||
.then(cache => mPreloadedCaches.set(`window-${tab.windowId}`, cache));
|
||||
browser.sessions.getTabValue(tab.id, Constants.kWINDOW_STATE_CACHED_TABS)
|
||||
.catch(ApiTabs.createErrorSuppressor())
|
||||
.then(cache => mPreloadedCaches.set(`tab-${tab.id}`, cache));
|
||||
//uniqueId = uniqueId?.id || '?'; // not used
|
||||
timeout = setTimeout(resolver, 100);
|
||||
};
|
||||
browser.tabs.onCreated.addListener(onNewTabRestored);
|
||||
resolver = (async () => {
|
||||
log(`timeout: all ${promises.length} tabs are restored. `, promises);
|
||||
browser.tabs.onCreated.removeListener(onNewTabRestored);
|
||||
timeout = resolver = onNewTabRestored = undefined;
|
||||
await Promise.all(promises);
|
||||
promises = undefined;
|
||||
resolve();
|
||||
});
|
||||
timeout = setTimeout(resolver, 500);
|
||||
})),
|
||||
]);
|
||||
}
|
||||
async function waitUntilPersistentIdBecomeAvailable(tabId, retryCount = 0) {
|
||||
if (retryCount > 10) {
|
||||
console.log(`could not get persistent ID for ${tabId}`);
|
||||
return false;
|
||||
}
|
||||
const uniqueId = await browser.sessions.getTabValue(tabId, Constants.kPERSISTENT_ID);
|
||||
if (!uniqueId)
|
||||
return wait(100).then(() => waitUntilPersistentIdBecomeAvailable(tabId, retryCount + 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
browser.runtime.sendMessage({
|
||||
type: TSTAPI.kUNREGISTER_SELF
|
||||
}).catch(ApiTabs.createErrorSuppressor());
|
||||
|
||||
// This API doesn't work as expected because it is not notified to
|
||||
// other addons actually when browser.runtime.sendMessage() is called
|
||||
// on pagehide or something unloading event.
|
||||
TSTAPI.broadcastMessage({
|
||||
type: TSTAPI.kNOTIFY_SHUTDOWN
|
||||
}).catch(ApiTabs.createErrorSuppressor());
|
||||
|
||||
onDestroy.dispatch();
|
||||
ApiTabsListener.destroy();
|
||||
ContextualIdentities.endObserve();
|
||||
}
|
||||
|
||||
async function rebuildAll(windows) {
|
||||
if (!windows)
|
||||
windows = await getAllWindows();
|
||||
const restoredFromCache = new Map();
|
||||
await Promise.all(windows.map(async win => {
|
||||
await MetricsData.addAsync(`rebuildAll: tabs in window ${win.id}`, async () => {
|
||||
let trackedWindow = TabsStore.windows.get(win.id);
|
||||
if (!trackedWindow)
|
||||
trackedWindow = Window.init(win.id, win.tabGroups.map(TabGroup.init));
|
||||
|
||||
for (const tab of win.tabs) {
|
||||
Tab.track(tab);
|
||||
Tab.init(tab, { existing: true });
|
||||
tryStartHandleAccelKeyOnTab(tab);
|
||||
}
|
||||
try {
|
||||
if (configs.useCachedTree) {
|
||||
log(`trying to restore window ${win.id} from cache`);
|
||||
const restored = await MetricsData.addAsync(`rebuildAll: restore tabs in window ${win.id} from cache`, BackgroundCache.restoreWindowFromEffectiveWindowCache(win.id, {
|
||||
owner: win.tabs[win.tabs.length - 1],
|
||||
tabs: win.tabs,
|
||||
caches: mPreloadedCaches
|
||||
}));
|
||||
restoredFromCache.set(win.id, restored);
|
||||
log(`window ${win.id}: restored from cache?: `, restored);
|
||||
if (restored)
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
log(`failed to restore tabs for ${win.id} from cache `, e);
|
||||
}
|
||||
try {
|
||||
log(`build tabs for ${win.id} from scratch`);
|
||||
Window.init(win.id, win.tabGroups.map(TabGroup.init));
|
||||
const promises = [];
|
||||
for (let tab of win.tabs) {
|
||||
tab = Tab.get(tab.id);
|
||||
tab.$TST.clear(); // clear dirty restored states
|
||||
promises.push(
|
||||
tab.$TST.getPermanentStates()
|
||||
.then(states => {
|
||||
tab.$TST.states = new Set(states);
|
||||
tab.$TST.addState(Constants.kTAB_STATE_PENDING);
|
||||
})
|
||||
.catch(console.error)
|
||||
.then(() => {
|
||||
TabsUpdate.updateTab(tab, tab, { forceApply: true });
|
||||
})
|
||||
);
|
||||
tryStartHandleAccelKeyOnTab(tab);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
catch(e) {
|
||||
log(`failed to build tabs for ${win.id}`, e);
|
||||
}
|
||||
restoredFromCache.set(win.id, false);
|
||||
});
|
||||
for (const tab of Tab.getGroupTabs(win.id, { iterator: true })) {
|
||||
if (!tab.discarded)
|
||||
tab.$TST.temporaryMetadata.set('shouldReloadOnSelect', true);
|
||||
}
|
||||
}));
|
||||
return restoredFromCache;
|
||||
}
|
||||
|
||||
export async function reload(options = {}) {
|
||||
mPreloadedCaches.clear();
|
||||
for (const win of TabsStore.windows.values()) {
|
||||
win.clear();
|
||||
}
|
||||
TabsStore.clear();
|
||||
const windows = await getAllWindows();
|
||||
await MetricsData.addAsync('reload: rebuildAll', rebuildAll(windows));
|
||||
await MetricsData.addAsync('reload: TreeStructure.loadTreeStructure', TreeStructure.loadTreeStructure(windows));
|
||||
if (!options.all)
|
||||
return;
|
||||
for (const win of TabsStore.windows.values()) {
|
||||
if (!SidebarConnection.isOpen(win.id))
|
||||
continue;
|
||||
log('reload all sidebars: ', new Error().stack);
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_RELOAD
|
||||
}).catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryStartHandleAccelKeyOnTab(tab) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
return;
|
||||
const granted = await Permissions.isGranted(Permissions.ALL_URLS);
|
||||
if (!granted ||
|
||||
/^(about|chrome|resource):/.test(tab.url))
|
||||
return;
|
||||
try {
|
||||
//log(`tryStartHandleAccelKeyOnTab: initialize tab ${tab.id}`);
|
||||
if (browser.scripting) // Manifest V3
|
||||
browser.scripting.executeScript({
|
||||
target: {
|
||||
tabId: tab.id,
|
||||
allFrames: true,
|
||||
},
|
||||
files: ['/common/handle-accel-key.js'],
|
||||
}).catch(ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
|
||||
else
|
||||
browser.tabs.executeScript(tab.id, {
|
||||
file: '/common/handle-accel-key.js',
|
||||
allFrames: true,
|
||||
matchAboutBlank: true,
|
||||
runAt: 'document_start'
|
||||
}).catch(ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
export function reserveToUpdateInsertionPosition(tabOrTabs) {
|
||||
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
|
||||
for (const tab of tabs) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
continue;
|
||||
const reserved = reserveToUpdateInsertionPosition.reserved.get(tab.windowId) || {
|
||||
timer: null,
|
||||
tabs: new Set()
|
||||
};
|
||||
if (reserved.timer)
|
||||
clearTimeout(reserved.timer);
|
||||
reserved.tabs.add(tab);
|
||||
reserved.timer = setTimeout(() => {
|
||||
reserveToUpdateInsertionPosition.reserved.delete(tab.windowId);
|
||||
for (const tab of reserved.tabs) {
|
||||
if (!tab.$TST)
|
||||
continue;
|
||||
updateInsertionPosition(tab);
|
||||
}
|
||||
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
|
||||
reserveToUpdateInsertionPosition.reserved.set(tab.windowId, reserved);
|
||||
}
|
||||
}
|
||||
reserveToUpdateInsertionPosition.reserved = new Map();
|
||||
|
||||
async function updateInsertionPosition(tab) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
return;
|
||||
|
||||
const prev = tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab;
|
||||
if (prev)
|
||||
browser.sessions.setTabValue(
|
||||
tab.id,
|
||||
Constants.kPERSISTENT_INSERT_AFTER,
|
||||
prev.$TST.uniqueId.id
|
||||
).catch(ApiTabs.createErrorSuppressor(
|
||||
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
|
||||
));
|
||||
else
|
||||
browser.sessions.removeTabValue(
|
||||
tab.id,
|
||||
Constants.kPERSISTENT_INSERT_AFTER
|
||||
).catch(ApiTabs.createErrorSuppressor(
|
||||
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
|
||||
));
|
||||
|
||||
// This code should be removed after legacy data are cleared enough, maybe after Firefox 128 is released.
|
||||
browser.sessions.removeTabValue(
|
||||
tab.id,
|
||||
Constants.kPERSISTENT_INSERT_AFTER_LEGACY
|
||||
).catch(ApiTabs.createErrorSuppressor(
|
||||
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
|
||||
));
|
||||
|
||||
const next = tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab;
|
||||
if (next)
|
||||
browser.sessions.setTabValue(
|
||||
tab.id,
|
||||
Constants.kPERSISTENT_INSERT_BEFORE,
|
||||
next.$TST.uniqueId.id
|
||||
).catch(ApiTabs.createErrorSuppressor(
|
||||
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
|
||||
));
|
||||
else
|
||||
browser.sessions.removeTabValue(
|
||||
tab.id,
|
||||
Constants.kPERSISTENT_INSERT_BEFORE
|
||||
).catch(ApiTabs.createErrorSuppressor(
|
||||
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
export function reserveToUpdateAncestors(tabOrTabs) {
|
||||
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
|
||||
for (const tab of tabs) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
continue;
|
||||
const reserved = reserveToUpdateAncestors.reserved.get(tab.windowId) || {
|
||||
timer: null,
|
||||
tabs: new Set()
|
||||
};
|
||||
if (reserved.timer)
|
||||
clearTimeout(reserved.timer);
|
||||
reserved.tabs.add(tab);
|
||||
reserved.timer = setTimeout(() => {
|
||||
reserveToUpdateAncestors.reserved.delete(tab.windowId);
|
||||
for (const tab of reserved.tabs) {
|
||||
if (!tab.$TST)
|
||||
continue;
|
||||
updateAncestors(tab);
|
||||
}
|
||||
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
|
||||
reserveToUpdateAncestors.reserved.set(tab.windowId, reserved);
|
||||
}
|
||||
}
|
||||
reserveToUpdateAncestors.reserved = new Map();
|
||||
|
||||
async function updateAncestors(tab) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
return;
|
||||
|
||||
const ancestors = tab.$TST.ancestors.map(ancestor => ancestor.$TST.uniqueId.id);
|
||||
log(`updateAncestors: save persistent ancestors for ${tab.id}: `, ancestors);
|
||||
browser.sessions.setTabValue(
|
||||
tab.id,
|
||||
Constants.kPERSISTENT_ANCESTORS,
|
||||
ancestors
|
||||
).catch(ApiTabs.createErrorSuppressor(
|
||||
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
|
||||
));
|
||||
}
|
||||
|
||||
export function reserveToUpdateChildren(tabOrTabs) {
|
||||
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
|
||||
for (const tab of tabs) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
continue;
|
||||
const reserved = reserveToUpdateChildren.reserved.get(tab.windowId) || {
|
||||
timer: null,
|
||||
tabs: new Set()
|
||||
};
|
||||
if (reserved.timer)
|
||||
clearTimeout(reserved.timer);
|
||||
reserved.tabs.add(tab);
|
||||
reserved.timer = setTimeout(() => {
|
||||
reserveToUpdateChildren.reserved.delete(tab.windowId);
|
||||
for (const tab of reserved.tabs) {
|
||||
if (!tab.$TST)
|
||||
continue;
|
||||
updateChildren(tab);
|
||||
}
|
||||
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
|
||||
reserveToUpdateChildren.reserved.set(tab.windowId, reserved);
|
||||
}
|
||||
}
|
||||
reserveToUpdateChildren.reserved = new Map();
|
||||
|
||||
async function updateChildren(tab) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
return;
|
||||
|
||||
const children = tab.$TST.children.map(child => child.$TST.uniqueId.id);
|
||||
log(`updateChildren: save persistent children for ${tab.id}: `, children);
|
||||
browser.sessions.setTabValue(
|
||||
tab.id,
|
||||
Constants.kPERSISTENT_CHILDREN,
|
||||
children
|
||||
).catch(ApiTabs.createErrorSuppressor(
|
||||
ApiTabs.handleMissingTabError // The tab can be closed while waiting.
|
||||
));
|
||||
}
|
||||
|
||||
function reserveToUpdateSubtreeCollapsed(tab) {
|
||||
if (!mInitialized ||
|
||||
!TabsStore.ensureLivingItem(tab))
|
||||
return;
|
||||
const reserved = reserveToUpdateSubtreeCollapsed.reserved.get(tab.windowId) || {
|
||||
timer: null,
|
||||
tabs: new Set()
|
||||
};
|
||||
if (reserved.timer)
|
||||
clearTimeout(reserved.timer);
|
||||
reserved.tabs.add(tab);
|
||||
reserved.timer = setTimeout(() => {
|
||||
reserveToUpdateSubtreeCollapsed.reserved.delete(tab.windowId);
|
||||
for (const tab of reserved.tabs) {
|
||||
if (!tab.$TST)
|
||||
continue;
|
||||
updateSubtreeCollapsed(tab);
|
||||
}
|
||||
}, DELAY_TO_PROCESS_RESERVED_UPDATE_TASKS);
|
||||
reserveToUpdateSubtreeCollapsed.reserved.set(tab.windowId, reserved);
|
||||
}
|
||||
reserveToUpdateSubtreeCollapsed.reserved = new Map();
|
||||
|
||||
async function updateSubtreeCollapsed(tab) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
return;
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_SUBTREE_COLLAPSED, tab.$TST.subtreeCollapsed, { permanently: true });
|
||||
}
|
||||
|
||||
export async function confirmToCloseTabs(tabs, { windowId, configKey, messageKey, titleKey, minConfirmCount } = {}) {
|
||||
if (!windowId)
|
||||
windowId = tabs[0].windowId;
|
||||
|
||||
const grantedIds = new Set(configs.grantedRemovingTabIds);
|
||||
let count = 0;
|
||||
const tabIds = [];
|
||||
tabs = tabs.map(tab => Tab.get(tab?.id)).filter(tab => {
|
||||
if (tab && !grantedIds.has(tab.id)) {
|
||||
count++;
|
||||
tabIds.push(tab.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!configKey)
|
||||
configKey = 'warnOnCloseTabs';
|
||||
const shouldConfirm = configs[configKey];
|
||||
const deltaFromLastConfirmation = Date.now() - configs.lastConfirmedToCloseTabs;
|
||||
log('confirmToCloseTabs ', { tabIds, count, windowId, configKey, grantedIds, shouldConfirm, deltaFromLastConfirmation, minConfirmCount });
|
||||
if (count <= (typeof minConfirmCount == 'number' ? minConfirmCount : 1) ||
|
||||
!shouldConfirm ||
|
||||
deltaFromLastConfirmation < 500) {
|
||||
log('confirmToCloseTabs: skip confirmation and treated as granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
const win = await browser.windows.get(windowId);
|
||||
const listing = configs.warnOnCloseTabsWithListing ?
|
||||
Dialog.tabsToHTMLList(tabs, {
|
||||
maxHeight: Math.round(win.height * 0.8),
|
||||
maxWidth: Math.round(win.width * 0.75)
|
||||
}) :
|
||||
'';
|
||||
|
||||
const result = await Dialog.show(win, {
|
||||
content: `
|
||||
<div>${sanitizeForHTMLText(browser.i18n.getMessage(messageKey || 'warnOnCloseTabs_message', [count]))}</div>${listing}
|
||||
`.trim(),
|
||||
buttons: [
|
||||
browser.i18n.getMessage('warnOnCloseTabs_close'),
|
||||
browser.i18n.getMessage('warnOnCloseTabs_cancel')
|
||||
],
|
||||
checkMessage: browser.i18n.getMessage('warnOnCloseTabs_warnAgain'),
|
||||
checked: true,
|
||||
modal: !configs.debug, // for popup
|
||||
type: 'common-dialog', // for popup
|
||||
url: ((await Permissions.isGranted(Permissions.ALL_URLS)) ? null : '/resources/blank.html'), // for popup
|
||||
title: browser.i18n.getMessage(titleKey || 'warnOnCloseTabs_title'), // for popup
|
||||
onShownInPopup(container) {
|
||||
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
|
||||
// this need to be done on the next tick, to use the height of
|
||||
// the box for calculation of dialog size
|
||||
const style = container.querySelector('ul').style;
|
||||
style.height = '0px'; // this makes the box shrinkable
|
||||
style.maxHeight = 'none';
|
||||
style.minHeight = '0px';
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
log('confirmToCloseTabs: result = ', result);
|
||||
switch (result.buttonIndex) {
|
||||
case 0:
|
||||
if (!result.checked)
|
||||
configs[configKey] = false;
|
||||
configs.grantedRemovingTabIds = Array.from(new Set((configs.grantedRemovingTabIds || []).concat(tabIds)));
|
||||
log('confirmToCloseTabs: granted ', configs.grantedRemovingTabIds);
|
||||
reserveToClearGrantedRemovingTabs();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Commands.onTabsClosing.addListener((tabIds, options = {}) => {
|
||||
return confirmToCloseTabs(tabIds.map(Tab.get), options);
|
||||
});
|
||||
|
||||
function reserveToClearGrantedRemovingTabs() {
|
||||
const lastGranted = configs.grantedRemovingTabIds.join(',');
|
||||
setTimeout(() => {
|
||||
if (configs.grantedRemovingTabIds.join(',') == lastGranted)
|
||||
configs.grantedRemovingTabIds = [];
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
Tab.onCreated.addListener((tab, info = {}) => {
|
||||
if (!info.duplicated)
|
||||
return;
|
||||
// Duplicated tab has its own tree structure information inherited
|
||||
// from the original tab, but they must be cleared.
|
||||
reserveToUpdateAncestors(tab);
|
||||
reserveToUpdateChildren(tab);
|
||||
reserveToUpdateInsertionPosition([
|
||||
tab,
|
||||
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
|
||||
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab
|
||||
]);
|
||||
});
|
||||
|
||||
Tab.onUpdated.addListener((tab, changeInfo) => {
|
||||
if (!mInitialized)
|
||||
return;
|
||||
|
||||
// Loading of "about:(unknown type)" won't report new URL via tabs.onUpdated,
|
||||
// so we need to see the complete tab object.
|
||||
const status = changeInfo.status || tab?.status;
|
||||
const url = changeInfo.url ? changeInfo.url :
|
||||
status == 'complete' && tab ? tab.url : '';
|
||||
if (tab &&
|
||||
Constants.kSHORTHAND_ABOUT_URI.test(url)) {
|
||||
const shorthand = RegExp.$1;
|
||||
const oldUrl = tab.url;
|
||||
wait(100).then(() => { // redirect with delay to avoid infinite loop of recursive redirections.
|
||||
if (tab.url != oldUrl)
|
||||
return;
|
||||
browser.tabs.update(tab.id, {
|
||||
url: url.replace(Constants.kSHORTHAND_ABOUT_URI, Constants.kSHORTHAND_URIS[shorthand] || 'about:blank')
|
||||
}).catch(ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError));
|
||||
if (shorthand == 'group')
|
||||
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (changeInfo.status || changeInfo.url)
|
||||
tryStartHandleAccelKeyOnTab(tab);
|
||||
});
|
||||
|
||||
Tab.onShown.addListener(tab => {
|
||||
if (!mInitialized)
|
||||
return;
|
||||
|
||||
if (configs.fixupTreeOnTabVisibilityChanged) {
|
||||
reserveToUpdateAncestors(tab);
|
||||
reserveToUpdateChildren(tab);
|
||||
}
|
||||
reserveToUpdateInsertionPosition([
|
||||
tab,
|
||||
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
|
||||
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab
|
||||
]);
|
||||
});
|
||||
|
||||
Tab.onMutedStateChanged.addListener((root, toBeMuted) => {
|
||||
if (!mInitialized)
|
||||
return;
|
||||
|
||||
// Spread muted state of a parent tab to its collapsed descendants
|
||||
if (!root.$TST.subtreeCollapsed ||
|
||||
// We don't need to spread muted state to descendants of multiselected
|
||||
// tabs here, because tabs.update() was called with all multiselected tabs.
|
||||
root.$TST.multiselected ||
|
||||
// We should not spread muted state to descendants of collapsed tab
|
||||
// recursively, because they were already controlled from a visible
|
||||
// ancestor.
|
||||
root.$TST.collapsed)
|
||||
return;
|
||||
|
||||
const tabs = root.$TST.descendants;
|
||||
for (const tab of tabs) {
|
||||
const playing = tab.$TST.soundPlaying;
|
||||
const muted = tab.$TST.muted;
|
||||
log(`tab ${tab.id}: playing=${playing}, muted=${muted}`);
|
||||
if (configs.spreadMutedStateOnlyToSoundPlayingTabs &&
|
||||
!playing &&
|
||||
playing != toBeMuted)
|
||||
continue;
|
||||
|
||||
log(` => set muted=${toBeMuted}`);
|
||||
browser.tabs.update(tab.id, {
|
||||
muted: toBeMuted
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
|
||||
const add = [];
|
||||
const remove = [];
|
||||
if (toBeMuted) {
|
||||
add.push(Constants.kTAB_STATE_MUTED);
|
||||
tab.$TST.addState(Constants.kTAB_STATE_MUTED);
|
||||
}
|
||||
else {
|
||||
remove.push(Constants.kTAB_STATE_MUTED);
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_MUTED);
|
||||
}
|
||||
|
||||
if (tab.audible && !toBeMuted) {
|
||||
add.push(Constants.kTAB_STATE_SOUND_PLAYING);
|
||||
tab.$TST.addState(Constants.kTAB_STATE_SOUND_PLAYING);
|
||||
}
|
||||
else {
|
||||
remove.push(Constants.kTAB_STATE_SOUND_PLAYING);
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_SOUND_PLAYING);
|
||||
}
|
||||
|
||||
// tabs.onUpdated is too slow, so users will be confused
|
||||
// from still-not-updated tabs (in other words, they tabs
|
||||
// are unresponsive for quick-clicks).
|
||||
Tab.broadcastState(tab, { add, remove });
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onTabInternallyMoved.addListener((tab, info = {}) => {
|
||||
reserveToUpdateInsertionPosition([
|
||||
tab,
|
||||
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
|
||||
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab,
|
||||
info.oldPreviousTab,
|
||||
info.oldNextTab
|
||||
]);
|
||||
});
|
||||
|
||||
Tab.onMoved.addListener((tab, moveInfo) => {
|
||||
if (moveInfo.movedInBulk)
|
||||
return;
|
||||
reserveToUpdateInsertionPosition([
|
||||
tab,
|
||||
moveInfo.oldPreviousTab,
|
||||
moveInfo.oldNextTab,
|
||||
tab.hidden ? tab.$TST.unsafePreviousTab : tab.$TST.previousTab,
|
||||
tab.hidden ? tab.$TST.unsafeNextTab : tab.$TST.nextTab
|
||||
]);
|
||||
});
|
||||
|
||||
Tree.onAttached.addListener(async (tab, attachInfo) => {
|
||||
await tab.$TST.opened;
|
||||
|
||||
if (!TabsStore.ensureLivingItem(tab) || // not removed while waiting
|
||||
tab.$TST.parent != attachInfo.parent) // not detached while waiting
|
||||
return;
|
||||
|
||||
if (attachInfo.newlyAttached)
|
||||
reserveToUpdateAncestors([tab].concat(tab.$TST.descendants));
|
||||
reserveToUpdateChildren(tab.$TST.parent);
|
||||
reserveToUpdateInsertionPosition([
|
||||
tab,
|
||||
tab.$TST.nextTab,
|
||||
tab.$TST.previousTab
|
||||
]);
|
||||
});
|
||||
|
||||
Tree.onDetached.addListener((tab, detachInfo) => {
|
||||
reserveToUpdateAncestors([tab].concat(tab.$TST.descendants));
|
||||
reserveToUpdateChildren(detachInfo.oldParentTab);
|
||||
});
|
||||
|
||||
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info) => { reserveToUpdateSubtreeCollapsed(tab); });
|
||||
|
||||
|
||||
const BASE_ICONS = {
|
||||
'16': '/resources/16x16.svg',
|
||||
'20': '/resources/20x20.svg',
|
||||
'24': '/resources/24x24.svg',
|
||||
'32': '/resources/32x32.svg',
|
||||
};
|
||||
async function updateIconForBrowserTheme(theme) {
|
||||
// generate icons with theme specific color
|
||||
const toolbarIcons = {};
|
||||
const menuIcons = {};
|
||||
const sidebarIcons = {};
|
||||
|
||||
if (!theme) {
|
||||
const win = await browser.windows.getLastFocused();
|
||||
theme = await browser.theme.getCurrent(win.id);
|
||||
}
|
||||
|
||||
log('updateIconForBrowserTheme: ', theme);
|
||||
if (theme.colors) {
|
||||
const toolbarIconColor = theme.colors.icons || theme.colors.toolbar_text || theme.colors.tab_text || theme.colors.tab_background_text || theme.colors.bookmark_text || theme.colors.textcolor;
|
||||
const menuIconColor = theme.colors.popup_text || toolbarIconColor;
|
||||
const sidebarIconColor = theme.colors.sidebar_text || toolbarIconColor;
|
||||
log(' => ', { toolbarIconColor, menuIconColor, sidebarIconColor }, theme.colors);
|
||||
await Promise.all(Array.from(Object.entries(BASE_ICONS), async ([size, url]) => {
|
||||
const response = await fetch(url);
|
||||
const body = await response.text();
|
||||
const toolbarIconSource = body.replace(/transparent\s*\/\*\s*TO BE REPLACED WITH THEME COLOR\s*\*\//g, toolbarIconColor);
|
||||
toolbarIcons[size] = `data:image/svg+xml,${escape(toolbarIconSource)}#toolbar-theme`;
|
||||
const menuIconSource = body.replace(/transparent\s*\/\*\s*TO BE REPLACED WITH THEME COLOR\s*\*\//g, menuIconColor);
|
||||
menuIcons[size] = `data:image/svg+xml,${escape(menuIconSource)}#default-theme`;
|
||||
const sidebarIconSource = body.replace(/transparent\s*\/\*\s*TO BE REPLACED WITH THEME COLOR\s*\*\//g, sidebarIconColor);
|
||||
sidebarIcons[size] = `data:image/svg+xml,${escape(sidebarIconSource)}#default-theme`;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
for (const [size, url] of Object.entries(BASE_ICONS)) {
|
||||
toolbarIcons[size] = `${url}#toolbar`;
|
||||
menuIcons[size] = sidebarIcons[size] = `${url}#default`;
|
||||
}
|
||||
}
|
||||
|
||||
log('updateIconForBrowserTheme: applying icons: ', {
|
||||
toolbarIcons,
|
||||
menuIcons,
|
||||
sidebarIcons,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
...ContextMenu.getItemIdsWithIcon().map(id => browser.menus.update(id, { icons: menuIcons })),
|
||||
browser.menus.refresh().catch(ApiTabs.createErrorSuppressor()),
|
||||
browser.action?.setIcon({ path: toolbarIcons }), // Manifest v2
|
||||
browser.browserAction?.setIcon({ path: toolbarIcons }), // Manifest v3
|
||||
browser.sidebarAction?.setIcon({ path: sidebarIcons }),
|
||||
]);
|
||||
}
|
||||
|
||||
browser.theme.onUpdated.addListener(updateInfo => {
|
||||
updateIconForBrowserTheme(updateInfo.theme);
|
||||
});
|
||||
|
||||
mDarkModeMatchMedia.addListener(async _event => {
|
||||
updateIconForBrowserTheme();
|
||||
});
|
||||
|
||||
|
||||
|
||||
configs.$addObserver(key => {
|
||||
switch (key) {
|
||||
case 'style':
|
||||
updatePanelUrl();
|
||||
break;
|
||||
|
||||
case 'debug':
|
||||
EventListenerManager.debug = configs.debug;
|
||||
break;
|
||||
|
||||
case 'testKey': // for tests/utils.js
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TEST_KEY_CHANGED,
|
||||
value: configs.testKey
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
1566
waterfox/browser/components/sidebar/background/commands.js
Normal file
1566
waterfox/browser/components/sidebar/background/commands.js
Normal file
File diff suppressed because it is too large
Load Diff
764
waterfox/browser/components/sidebar/background/context-menu.js
Normal file
764
waterfox/browser/components/sidebar/background/context-menu.js
Normal file
@@ -0,0 +1,764 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs,
|
||||
} from '/common/common.js';
|
||||
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Bookmark from '/common/bookmark.js';
|
||||
import * as Sync from '/common/sync.js';
|
||||
import * as TSTAPI from '/common/tst-api.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Commands from './commands.js';
|
||||
import * as TabContextMenu from './tab-context-menu.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/context-menu', ...args);
|
||||
}
|
||||
|
||||
export async function init() {
|
||||
return Promise.all([
|
||||
addTabItems(),
|
||||
addBookmarkItems(),
|
||||
]);
|
||||
}
|
||||
|
||||
const SAFE_CREATE_PROPERTIES = [
|
||||
'checked',
|
||||
'contexts',
|
||||
'documentUrlPatterns',
|
||||
'enabled',
|
||||
'icons',
|
||||
'id',
|
||||
'parentId',
|
||||
'title',
|
||||
'type',
|
||||
'viewTypes',
|
||||
'visible'
|
||||
];
|
||||
|
||||
function getSafeCreateParams(params) {
|
||||
const safeParams = {};
|
||||
for (const property of SAFE_CREATE_PROPERTIES) {
|
||||
if (property in params)
|
||||
safeParams[property] = params[property];
|
||||
}
|
||||
return safeParams;
|
||||
}
|
||||
|
||||
const manifest = browser.runtime.getManifest();
|
||||
|
||||
const kROOT_TAB_ITEM = 'ws';
|
||||
const kROOT_BOOKMARK_ITEM = 'ws-bookmark';
|
||||
|
||||
const mTabItemsById = {
|
||||
'reloadTree': {
|
||||
title: browser.i18n.getMessage('context_reloadTree_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_reloadTree_label_multiselected')
|
||||
},
|
||||
'reloadDescendants': {
|
||||
title: browser.i18n.getMessage('context_reloadDescendants_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_reloadDescendants_label_multiselected')
|
||||
},
|
||||
// This item won't be handled by the onClicked handler, so you may need to handle it with something experiments API.
|
||||
'unblockAutoplayTree': {
|
||||
title: browser.i18n.getMessage('context_unblockAutoplayTree_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_unblockAutoplayTree_label_multiselected'),
|
||||
requireAutoplayBlockedTab: true,
|
||||
},
|
||||
// This item won't be handled by the onClicked handler, so you may need to handle it with something experiments API.
|
||||
'unblockAutoplayDescendants': {
|
||||
title: browser.i18n.getMessage('context_unblockAutoplayDescendants_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_unblockAutoplayDescendants_label_multiselected'),
|
||||
requireAutoplayBlockedDescendant: true,
|
||||
},
|
||||
'toggleMuteTree': {
|
||||
titleMuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_mute'),
|
||||
titleMultiselectedMuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_multiselected_mute'),
|
||||
titleUnmuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_unmute'),
|
||||
titleMultiselectedUnmuteTree: browser.i18n.getMessage('context_toggleMuteTree_label_multiselected_unmute')
|
||||
},
|
||||
'toggleMuteDescendants': {
|
||||
titleMuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_mute'),
|
||||
titleMultiselectedMuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_multiselected_mute'),
|
||||
titleUnmuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_unmute'),
|
||||
titleMultiselectedUnmuteDescendant: browser.i18n.getMessage('context_toggleMuteDescendants_label_multiselected_unmute')
|
||||
},
|
||||
'separatorAfterReload': {
|
||||
type: 'separator'
|
||||
},
|
||||
'closeTree': {
|
||||
title: browser.i18n.getMessage('context_closeTree_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_closeTree_label_multiselected')
|
||||
},
|
||||
'closeDescendants': {
|
||||
title: browser.i18n.getMessage('context_closeDescendants_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_closeDescendants_label_multiselected'),
|
||||
requireTree: true,
|
||||
},
|
||||
'closeOthers': {
|
||||
title: browser.i18n.getMessage('context_closeOthers_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_closeOthers_label_multiselected')
|
||||
},
|
||||
'separatorAfterClose': {
|
||||
type: 'separator'
|
||||
},
|
||||
'toggleSticky': {
|
||||
titleStick: browser.i18n.getMessage('context_toggleSticky_label_stick'),
|
||||
titleMultiselectedStick: browser.i18n.getMessage('context_toggleSticky_label_multiselected_stick'),
|
||||
titleUnstick: browser.i18n.getMessage('context_toggleSticky_label_unstick'),
|
||||
titleMultiselectedUnstick: browser.i18n.getMessage('context_toggleSticky_label_multiselected_unstick'),
|
||||
requireNormal: true,
|
||||
},
|
||||
'separatorAfterToggleSticky': {
|
||||
type: 'separator'
|
||||
},
|
||||
'collapseTree': {
|
||||
title: browser.i18n.getMessage('context_collapseTree_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_collapseTree_label_multiselected'),
|
||||
requireTree: true,
|
||||
},
|
||||
'collapseTreeRecursively': {
|
||||
title: browser.i18n.getMessage('context_collapseTreeRecursively_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_collapseTreeRecursively_label_multiselected'),
|
||||
requireTree: true,
|
||||
},
|
||||
'collapseAll': {
|
||||
title: browser.i18n.getMessage('context_collapseAll_label'),
|
||||
hideOnMultiselected: true
|
||||
},
|
||||
'expandTree': {
|
||||
title: browser.i18n.getMessage('context_expandTree_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_expandTree_label_multiselected'),
|
||||
requireTree: true,
|
||||
},
|
||||
'expandTreeRecursively': {
|
||||
title: browser.i18n.getMessage('context_expandTreeRecursively_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_expandTreeRecursively_label_multiselected'),
|
||||
requireTree: true,
|
||||
},
|
||||
'expandAll': {
|
||||
title: browser.i18n.getMessage('context_expandAll_label'),
|
||||
hideOnMultiselected: true
|
||||
},
|
||||
'separatorAfterCollapseExpand': {
|
||||
type: 'separator'
|
||||
},
|
||||
'bookmarkTree': {
|
||||
title: browser.i18n.getMessage('context_bookmarkTree_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_bookmarkTree_label_multiselected')
|
||||
},
|
||||
'sendTreeToDevice': {
|
||||
title: browser.i18n.getMessage('context_sendTreeToDevice_label'),
|
||||
titleMultiselected: browser.i18n.getMessage('context_sendTreeToDevice_label_multiselected')
|
||||
},
|
||||
'separatorAfterBookmark': {
|
||||
type: 'separator'
|
||||
},
|
||||
'collapsed': {
|
||||
title: browser.i18n.getMessage('context_collapsed_label'),
|
||||
requireTree: true,
|
||||
type: 'checkbox'
|
||||
},
|
||||
'pinnedTab': {
|
||||
title: browser.i18n.getMessage('context_pinnedTab_label'),
|
||||
type: 'radio'
|
||||
},
|
||||
'unpinnedTab': {
|
||||
title: browser.i18n.getMessage('context_unpinnedTab_label'),
|
||||
type: 'radio'
|
||||
}
|
||||
};
|
||||
const mTabItems = [];
|
||||
const mGroupedTabItems = [];
|
||||
const mGroupedTabItemsById = {};
|
||||
for (const id of Object.keys(mTabItemsById)) {
|
||||
const item = mTabItemsById[id];
|
||||
item.id = id;
|
||||
item.configKey = `context_${id}`;
|
||||
item.checked = false; // initialize as unchecked
|
||||
item.enabled = true;
|
||||
// Access key is not supported by WE API.
|
||||
// See also: https://bugzilla.mozilla.org/show_bug.cgi?id=1320462
|
||||
item.titleWithoutAccesskey = item.title?.replace(/\(&[a-z]\)|&([a-z])/i, '$1');
|
||||
item.titleMultiselectedWithoutAccesskey = item.titleMultiselected?.replace(/\(&[a-z]\)|&([a-z])/i, '$1');
|
||||
item.type = item.type || 'normal';
|
||||
item.contexts = ['tab'];
|
||||
item.lastVisible = item.visible = false;
|
||||
item.lastTitle = item.title;
|
||||
mTabItems.push(item);
|
||||
|
||||
const groupedItem = {
|
||||
...item,
|
||||
id: `grouped:${id}`,
|
||||
parentId: kROOT_TAB_ITEM
|
||||
};
|
||||
mGroupedTabItems.push(groupedItem);
|
||||
mGroupedTabItemsById[groupedItem.id] = groupedItem;
|
||||
}
|
||||
|
||||
const mTabSeparator = {
|
||||
id: `separatprBefore${kROOT_TAB_ITEM}`,
|
||||
type: 'separator',
|
||||
contexts: ['tab'],
|
||||
viewTypes: ['sidebar'],
|
||||
documentUrlPatterns: [`moz-extension://${location.host}/*`],
|
||||
visible: false,
|
||||
lastVisible: false
|
||||
};
|
||||
const mTabRootItem = {
|
||||
id: kROOT_TAB_ITEM,
|
||||
type: 'normal',
|
||||
contexts: ['tab'],
|
||||
title: browser.i18n.getMessage('context_menu_label'),
|
||||
icons: manifest.icons,
|
||||
visible: false,
|
||||
lastVisible: false
|
||||
};
|
||||
|
||||
const mAllTabItems = [
|
||||
mTabSeparator,
|
||||
mTabRootItem,
|
||||
...mTabItems,
|
||||
...mGroupedTabItems
|
||||
];
|
||||
|
||||
function addTabItems() {
|
||||
const promises = [];
|
||||
if (addTabItems.done) {
|
||||
for (const item of mAllTabItems) {
|
||||
promises.push(browser.menus.remove(item.id));
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of mAllTabItems) {
|
||||
const params = getSafeCreateParams(item);
|
||||
promises.push(browser.menus.create(params));
|
||||
if (item.id == mTabSeparator.id ||
|
||||
addTabItems.done)
|
||||
continue;
|
||||
promises.push(TabContextMenu.onMessageExternal({
|
||||
type: TSTAPI.kCONTEXT_MENU_CREATE,
|
||||
params
|
||||
}, browser.runtime));
|
||||
}
|
||||
|
||||
addTabItems.done = true;
|
||||
return Promise.all(promises);
|
||||
}
|
||||
addTabItems.done = false;
|
||||
|
||||
|
||||
const mBookmarkItemsById = {
|
||||
openAllBookmarksWithStructure: {
|
||||
title: browser.i18n.getMessage('context_openAllBookmarksWithStructure_label')
|
||||
},
|
||||
openAllBookmarksWithStructureRecursively: {
|
||||
title: browser.i18n.getMessage('context_openAllBookmarksWithStructureRecursively_label')
|
||||
}
|
||||
};
|
||||
const mBookmarkItems = [];
|
||||
const mGroupedBookmarkItems = []
|
||||
for (const id of Object.keys(mBookmarkItemsById)) {
|
||||
const item = mBookmarkItemsById[id];
|
||||
item.id = id;
|
||||
item.contexts = ['bookmark'];
|
||||
item.configKey = `context_${id}`;
|
||||
const groupedItem = {
|
||||
...item,
|
||||
id: `grouped:${id}`,
|
||||
parentId: kROOT_BOOKMARK_ITEM,
|
||||
ungroupedItem: item
|
||||
};
|
||||
item.icons = manifest.icons;
|
||||
|
||||
mBookmarkItems.push(item);
|
||||
|
||||
mBookmarkItemsById[groupedItem.id] = groupedItem;
|
||||
mGroupedBookmarkItems.push(groupedItem);
|
||||
}
|
||||
|
||||
const mBookmarkSeparator = {
|
||||
id: `separatprBefore${kROOT_BOOKMARK_ITEM}`,
|
||||
type: 'separator',
|
||||
contexts: ['bookmark'],
|
||||
viewTypes: ['sidebar'],
|
||||
documentUrlPatterns: [`moz-extension://${location.host}/*`],
|
||||
visible: false,
|
||||
lastVisible: false
|
||||
};
|
||||
const mBookmarkRootItem = {
|
||||
id: kROOT_BOOKMARK_ITEM,
|
||||
type: 'normal',
|
||||
contexts: ['bookmark'],
|
||||
title: manifest.name,
|
||||
icons: manifest.icons,
|
||||
visible: false,
|
||||
lastVisible: false
|
||||
};
|
||||
|
||||
const mAllBookmarkItems = [
|
||||
mBookmarkSeparator,
|
||||
mBookmarkRootItem,
|
||||
...mBookmarkItems,
|
||||
...mGroupedBookmarkItems
|
||||
];
|
||||
|
||||
function addBookmarkItems() {
|
||||
const promises = [];
|
||||
if (addBookmarkItems.done) {
|
||||
for (const item of mAllBookmarkItems) {
|
||||
promises.push(browser.menus.remove(item.id));
|
||||
}
|
||||
}
|
||||
for (const item of mAllBookmarkItems) {
|
||||
promises.push(browser.menus.create(getSafeCreateParams(item)));
|
||||
}
|
||||
addBookmarkItems.done = true;
|
||||
return Promise.all(promises);
|
||||
}
|
||||
addBookmarkItems.done = false;
|
||||
|
||||
|
||||
// Re-register items to put them after
|
||||
// top level items added by other addons.
|
||||
TabContextMenu.onTopLevelItemAdded.addListener(reserveToRefreshItems);
|
||||
|
||||
function reserveToRefreshItems() {
|
||||
if (reserveToRefreshItems.invoked)
|
||||
return;
|
||||
reserveToRefreshItems.invoked = true;
|
||||
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
|
||||
reserveToRefreshItems.invoked = false;
|
||||
addTabItems();
|
||||
addBookmarkItems();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function updateItem(id, params) {
|
||||
log('updateItem ', id, params);
|
||||
browser.menus.update(id, params).catch(ApiTabs.createErrorSuppressor());
|
||||
TabContextMenu.onMessageExternal({
|
||||
type: TSTAPI.kCONTEXT_MENU_UPDATE,
|
||||
params: [id, params]
|
||||
}, browser.runtime);
|
||||
}
|
||||
|
||||
function updateItemsVisibility(items, { forceVisible = null, multiselected = false, hasUnmutedTab = false, hasUnmutedDescendant = false, hasAutoplayBlockedTab = false, hasAutoplayBlockedDescendant = false, sticky = false, hidden = false } = {}) {
|
||||
log('updateItemsVisibility ', items, { forceVisible, multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky });
|
||||
let updated = false;
|
||||
let visibleItemsCount = 0;
|
||||
let visibleNormalItemsCount = 0;
|
||||
let lastSeparator;
|
||||
for (const item of items) {
|
||||
if (item.type == 'separator') {
|
||||
if (lastSeparator) {
|
||||
if (lastSeparator.lastVisible) {
|
||||
updateItem(lastSeparator.id, { visible: false });
|
||||
lastSeparator.lastVisible = false;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
lastSeparator = item;
|
||||
}
|
||||
else {
|
||||
const title = Commands.getMenuItemTitle(item, { multiselected, hasUnmutedTab, hasUnmutedDescendant, sticky });
|
||||
let visible = !hidden && (!(item.configKey in configs) || configs[item.configKey] || false);
|
||||
log('checking ', item.id, {
|
||||
config: visible,
|
||||
multiselected: item.hideOnMultiselected && multiselected,
|
||||
lastVisible: item.lastVisible,
|
||||
forceVisible,
|
||||
});
|
||||
if (forceVisible !== null && forceVisible !== undefined)
|
||||
visible = forceVisible;
|
||||
if ((item.hideOnMultiselected && multiselected) ||
|
||||
(item.requireAutoplayBlockedTab && !hasAutoplayBlockedTab) ||
|
||||
(item.requireAutoplayBlockedDescendant && !hasAutoplayBlockedDescendant))
|
||||
visible = false;
|
||||
if (visible) {
|
||||
if (lastSeparator) {
|
||||
updateItem(lastSeparator.id, { visible: visibleNormalItemsCount > 0 });
|
||||
lastSeparator.lastVisible = true;
|
||||
lastSeparator = null;
|
||||
updated = true;
|
||||
visibleNormalItemsCount = 0;
|
||||
}
|
||||
visibleNormalItemsCount++;
|
||||
visibleItemsCount++;
|
||||
}
|
||||
const updatedParams = {};
|
||||
if (visible !== item.lastVisible) {
|
||||
updatedParams.visible = visible;
|
||||
item.lastVisible = visible;
|
||||
}
|
||||
if (title !== item.lastTitle) {
|
||||
updatedParams.title = title;
|
||||
item.lastTitle = title;
|
||||
}
|
||||
if (Object.keys(updatedParams).length == 0)
|
||||
continue;
|
||||
updateItem(item.id, updatedParams);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (lastSeparator?.lastVisible) {
|
||||
updateItem(lastSeparator.id, { visible: false });
|
||||
lastSeparator.lastVisible = false;
|
||||
updated = true;
|
||||
}
|
||||
return { updated, visibleItemsCount };
|
||||
}
|
||||
|
||||
async function updateItems({ multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky, hidden } = {}) {
|
||||
let updated = false;
|
||||
|
||||
const groupedItems = updateItemsVisibility(mGroupedTabItems, { multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky, hidden });
|
||||
if (groupedItems.updated)
|
||||
updated = true;
|
||||
|
||||
const separatorVisible = !hidden && configs.emulateDefaultContextMenu && groupedItems.visibleItemsCount > 0;
|
||||
if (separatorVisible != mTabSeparator.lastVisible) {
|
||||
updateItem(mTabSeparator.id, { visible: separatorVisible });
|
||||
mTabSeparator.lastVisible = separatorVisible;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
const grouped = !hidden && configs.emulateDefaultContextMenu && groupedItems.visibleItemsCount > 1;
|
||||
if (grouped != mTabRootItem.lastVisible) {
|
||||
updateItem(mTabRootItem.id, { visible: grouped });
|
||||
mTabRootItem.lastVisible = grouped;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
const topLevelItems = updateItemsVisibility(mTabItems, { forceVisible: grouped ? false : null, multiselected, hasUnmutedTab, hasUnmutedDescendant, hasAutoplayBlockedTab, hasAutoplayBlockedDescendant, sticky, hidden });
|
||||
if (topLevelItems.updated)
|
||||
updated = true;
|
||||
|
||||
if (mGroupedTabItemsById['grouped:sendTreeToDevice'].lastVisible &&
|
||||
await TabContextMenu.updateSendToDeviceItems('grouped:sendTreeToDevice', {
|
||||
manage: navigator.userAgent.includes('Fennec'), // see also https://github.com/piroor/treestyletab/issues/3174
|
||||
}))
|
||||
updated = true;
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function onClick(info, tab) {
|
||||
if (info.bookmarkId)
|
||||
return onBookmarkItemClick(info);
|
||||
else
|
||||
return onTabItemClick(info, tab);
|
||||
}
|
||||
browser.menus.onClicked.addListener(onClick);
|
||||
|
||||
function onTabItemClick(info, tab) {
|
||||
// Extra context menu commands won't be available on the blank area of the tab bar.
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
log('context menu item clicked: ', info, tab);
|
||||
|
||||
const contextTab = Tab.get(tab.id);
|
||||
const contextTabs = contextTab.$TST.multiselected ? Tab.getSelectedTabs(contextTab.windowId) : [contextTab];
|
||||
|
||||
const itemId = info.menuItemId.replace(/^(?:grouped:|context_topLevel_)/, '');
|
||||
if (mTabItemsById[itemId] &&
|
||||
mTabItemsById[itemId].type == 'checkbox')
|
||||
mTabItemsById[itemId].checked = !mTabItemsById[itemId].checked;
|
||||
|
||||
const inverted = info.button == 1;
|
||||
switch (itemId) {
|
||||
case 'reloadTree':
|
||||
if (inverted)
|
||||
Commands.reloadDescendants(contextTabs);
|
||||
else
|
||||
Commands.reloadTree(contextTabs);
|
||||
break;
|
||||
case 'reloadDescendants':
|
||||
if (inverted)
|
||||
Commands.reloadTree(contextTabs);
|
||||
else
|
||||
Commands.reloadDescendants(contextTabs);
|
||||
break;
|
||||
|
||||
case 'toggleMuteTree':
|
||||
if (inverted)
|
||||
Commands.toggleMuteDescendants(contextTabs);
|
||||
else
|
||||
Commands.toggleMuteTree(contextTabs);
|
||||
break;
|
||||
case 'toggleMuteDescendants':
|
||||
if (inverted)
|
||||
Commands.toggleMuteTree(contextTabs);
|
||||
else
|
||||
Commands.toggleMuteDescendants(contextTabs);
|
||||
break;
|
||||
|
||||
case 'closeTree':
|
||||
if (inverted)
|
||||
Commands.closeDescendants(contextTabs);
|
||||
else
|
||||
Commands.closeTree(contextTabs);
|
||||
break;
|
||||
case 'closeDescendants':
|
||||
if (inverted)
|
||||
Commands.closeTree(contextTabs);
|
||||
else
|
||||
Commands.closeDescendants(contextTabs);
|
||||
break;
|
||||
case 'closeOthers':
|
||||
Commands.closeOthers(contextTabs);
|
||||
break;
|
||||
|
||||
case 'toggleSticky':
|
||||
Commands.toggleSticky(contextTabs, !contextTab.$TST.sticky);
|
||||
break;
|
||||
|
||||
case 'collapseTree':
|
||||
Commands.collapseTree(contextTabs, { recursively: inverted });
|
||||
break;
|
||||
case 'collapseTreeRecursively':
|
||||
Commands.collapseTree(contextTabs, { recursively: !inverted });
|
||||
break;
|
||||
case 'collapseAll':
|
||||
Commands.collapseAll(contextTab.windowId);
|
||||
break;
|
||||
case 'expandTree':
|
||||
Commands.expandTree(contextTabs, { recursively: inverted });
|
||||
break;
|
||||
case 'expandTreeRecursively':
|
||||
Commands.expandTree(contextTabs, { recursively: !inverted });
|
||||
break;
|
||||
case 'expandAll':
|
||||
Commands.expandAll(contextTab.windowId);
|
||||
break;
|
||||
|
||||
case 'bookmarkTree':
|
||||
Commands.bookmarkTree(contextTabs);
|
||||
break;
|
||||
|
||||
case 'sendTreeToDevice:all':
|
||||
Sync.sendTabsToAllDevices(contextTabs, { recursively: true });
|
||||
break;
|
||||
|
||||
case 'collapsed':
|
||||
if (info.wasChecked)
|
||||
Commands.expandTree(contextTab);
|
||||
else
|
||||
Commands.collapseTree(contextTab);
|
||||
break;
|
||||
case 'pinnedTab': {
|
||||
const tabs = Tab.getPinnedTabs(contextTab.windowId);
|
||||
if (tabs.length > 0)
|
||||
browser.tabs.update(tabs[0].id, { active: true })
|
||||
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
}; break;
|
||||
case 'unpinnedTab': {
|
||||
const tabs = Tab.getUnpinnedTabs(tab.windowId);
|
||||
if (tabs.length > 0)
|
||||
browser.tabs.update(tabs[0].id, { active: true })
|
||||
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
}; break;
|
||||
|
||||
default: {
|
||||
const sendToDeviceMatch = info.menuItemId.match(/^sendTreeToDevice:device:(.+)$/);
|
||||
if (contextTab &&
|
||||
sendToDeviceMatch)
|
||||
Sync.sendTabsToDevice(
|
||||
contextTabs,
|
||||
{ to: sendToDeviceMatch[1],
|
||||
recursively: true }
|
||||
);
|
||||
}; break;
|
||||
}
|
||||
}
|
||||
TabContextMenu.onTSTItemClick.addListener(onTabItemClick);
|
||||
|
||||
async function onBookmarkItemClick(info) {
|
||||
switch (info.menuItemId.replace(/^grouped:/, '')) {
|
||||
case 'openAllBookmarksWithStructure':
|
||||
Commands.openAllBookmarksWithStructure(info.bookmarkId, { recursively: false });
|
||||
break;
|
||||
|
||||
case 'openAllBookmarksWithStructureRecursively':
|
||||
Commands.openAllBookmarksWithStructure(info.bookmarkId, { recursively: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function onShown(info, tab) {
|
||||
if (info.contexts.includes('tab'))
|
||||
await onTabContextMenuShown(info, tab);
|
||||
else if (info.contexts.includes('bookmark'))
|
||||
onBookmarkContextMenuShown(info);
|
||||
}
|
||||
browser.menus.onShown.addListener(onShown);
|
||||
|
||||
let mLastContextTabId = null;
|
||||
async function onTabContextMenuShown(info, tab) {
|
||||
const contextTabId = tab?.id;
|
||||
mLastContextTabId = contextTabId;
|
||||
|
||||
tab = tab && Tab.get(contextTabId);
|
||||
const multiselected = tab?.$TST.multiselected;
|
||||
const contextTabs = multiselected ? Tab.getSelectedTabs(tab.windowId) : tab ? [tab] : [];
|
||||
const hasChild = contextTabs.length > 0 && contextTabs.some(tab => tab.$TST.hasChild);
|
||||
const subtreeCollapsed = contextTabs.length > 0 && contextTabs.some(tab => tab.$TST.subtreeCollapsed);
|
||||
const grouped = contextTabs.length > 0 && contextTabs.some(tab => tab.$TST.isGroupTab);
|
||||
const { hasUnmutedTab, hasUnmutedDescendant } = Commands.getUnmutedState(contextTabs);
|
||||
const { hasAutoplayBlockedTab, hasAutoplayBlockedDescendant } = Commands.getAutoplayBlockedState(contextTabs);
|
||||
|
||||
let updated = await updateItems({
|
||||
multiselected,
|
||||
hasUnmutedTab,
|
||||
hasUnmutedDescendant,
|
||||
hasAutoplayBlockedTab,
|
||||
hasAutoplayBlockedDescendant,
|
||||
sticky: tab?.$TST.sticky,
|
||||
hidden: !configs.showTreeCommandsInTabsContextMenuGlobally && info.viewType != 'sidebar',
|
||||
});
|
||||
if (mLastContextTabId != contextTabId)
|
||||
return; // Skip further operations if the menu was already reopened on a different context tab.
|
||||
|
||||
for (const item of mTabItems) {
|
||||
let newVisible;
|
||||
let newEnabled;
|
||||
if (item.id == 'sendTreeToDevice' &&
|
||||
item.visible) {
|
||||
newVisible = contextTabs.filter(Sync.isSendableTab).length > 0;
|
||||
newEnabled = (
|
||||
hasChild &&
|
||||
Sync.getOtherDevices().length > 0
|
||||
);
|
||||
}
|
||||
else if (item.requireTree) {
|
||||
newEnabled = hasChild;
|
||||
switch (item.id) {
|
||||
case 'collapseTree':
|
||||
if (subtreeCollapsed)
|
||||
newEnabled = false;
|
||||
break;
|
||||
case 'expandTree':
|
||||
if (!subtreeCollapsed)
|
||||
newEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (item.requireMultiselected) {
|
||||
newEnabled = multiselected;
|
||||
}
|
||||
else if (item.requireGrouped) {
|
||||
newEnabled = grouped;
|
||||
}
|
||||
else if (item.requireNormal) {
|
||||
newEnabled = tab?.pinned;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((newVisible === undefined ||
|
||||
newVisible == !!item.visible) &&
|
||||
(newEnabled === undefined ||
|
||||
newEnabled == !!item.enabled))
|
||||
continue;
|
||||
|
||||
const params = {};
|
||||
if (newVisible !== undefined &&
|
||||
newVisible != !!item.visible)
|
||||
params.visible = item.visible = !!newVisible;
|
||||
if (newEnabled !== undefined &&
|
||||
newEnabled != !!item.enabled)
|
||||
params.enabled = item.enabled = !!newEnabled;
|
||||
|
||||
updateItem(item.id, params);
|
||||
updateItem(`grouped:${item.id}`, params);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
{
|
||||
const canExpand = hasChild && subtreeCollapsed;
|
||||
mTabItemsById.collapsed.checked = canExpand;
|
||||
const params = {
|
||||
checked: canExpand
|
||||
};
|
||||
updateItem('collapsed', params);
|
||||
updateItem(`grouped:collapsed`, params);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated)
|
||||
browser.menus.refresh().catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
TabContextMenu.onTSTTabContextMenuShown.addListener(onTabContextMenuShown);
|
||||
|
||||
let mLastContextItemId = null;
|
||||
async function onBookmarkContextMenuShown(info) {
|
||||
const contextItemId = info.bookmarkId;
|
||||
mLastContextItemId = contextItemId;
|
||||
|
||||
let isFolder = true;
|
||||
if (info.bookmarkId) {
|
||||
const item = await Bookmark.getItemById(info.bookmarkId);
|
||||
if (mLastContextItemId != contextItemId)
|
||||
return; // Skip further operations if the menu was already reopened on a different context item.
|
||||
isFolder = (
|
||||
item.type == 'folder' ||
|
||||
(item.type == 'bookmark' &&
|
||||
/^place:parent=([^&]+)$/.test(item.url))
|
||||
);
|
||||
}
|
||||
|
||||
let visibleItemCount = 0;
|
||||
|
||||
mBookmarkItemsById.openAllBookmarksWithStructure.visible = !!(
|
||||
isFolder &&
|
||||
configs[mBookmarkItemsById.openAllBookmarksWithStructure.configKey] &&
|
||||
++visibleItemCount
|
||||
);
|
||||
mBookmarkItemsById.openAllBookmarksWithStructureRecursively.visible = !!(
|
||||
isFolder &&
|
||||
configs[mBookmarkItemsById.openAllBookmarksWithStructureRecursively.configKey] &&
|
||||
++visibleItemCount
|
||||
);
|
||||
|
||||
for (const item of mGroupedBookmarkItems) {
|
||||
item.visible = !!(
|
||||
visibleItemCount > 1 &&
|
||||
item.ungroupedItem.visible
|
||||
);
|
||||
if (item.visible)
|
||||
item.ungroupedItem.visible = false;
|
||||
}
|
||||
for (const item of [...mBookmarkItems, ...mGroupedBookmarkItems]) {
|
||||
browser.menus.update(item.id, {
|
||||
visible: !!item.visible,
|
||||
});
|
||||
}
|
||||
|
||||
browser.menus.update(mBookmarkSeparator.id, {
|
||||
visible: visibleItemCount > 0
|
||||
});
|
||||
browser.menus.update(mBookmarkRootItem.id, {
|
||||
visible: visibleItemCount > 1
|
||||
});
|
||||
browser.menus.refresh().catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
|
||||
|
||||
export function getItemIdsWithIcon() {
|
||||
return [
|
||||
kROOT_TAB_ITEM,
|
||||
kROOT_BOOKMARK_ITEM,
|
||||
...Object.keys(mBookmarkItemsById),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
configs,
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
async function doTest() {
|
||||
const win = await browser.windows.getLastFocused({ populate: true });
|
||||
const tab = win.tabs.find(tab => tab.active);
|
||||
|
||||
const maxTry = 10;
|
||||
const promises = [];
|
||||
const duplicatedTabIds = [];
|
||||
for (let i = 0; i < maxTry; i++) {
|
||||
promises.push(browser.tabs.duplicate(tab.id).then(async duplicated => {
|
||||
await Tab.waitUntilTracked(duplicated.id);
|
||||
const tracked = Tab.get(duplicated.id);
|
||||
const uniqueId = await tracked.$TST.promisedUniqueId;
|
||||
duplicatedTabIds.push(duplicated.id);
|
||||
return uniqueId?.duplicated;
|
||||
}));
|
||||
}
|
||||
const successCount = (await Promise.all(promises)).filter(result => !!result).length;
|
||||
await browser.tabs.remove(duplicatedTabIds);
|
||||
return successCount / maxTry;
|
||||
}
|
||||
|
||||
async function autoDetectSuitableDelay() {
|
||||
configs.delayForDuplicatedTabDetection = 0;
|
||||
let successRate = await doTest();
|
||||
if (successRate == 1)
|
||||
return;
|
||||
|
||||
configs.delayForDuplicatedTabDetection = 10;
|
||||
while (successRate < 1) {
|
||||
configs.delayForDuplicatedTabDetection = Math.round(configs.delayForDuplicatedTabDetection * (1 / Math.max(successRate, 0.5)));
|
||||
successRate = await doTest();
|
||||
}
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((message, _sender) => {
|
||||
if (!message || !message.type)
|
||||
return;
|
||||
|
||||
switch (message.type) {
|
||||
case Constants.kCOMMAND_TEST_DUPLICATED_TAB_DETECTION:
|
||||
return doTest();
|
||||
|
||||
case Constants.kCOMMAND_AUTODETECT_DUPLICATED_TAB_DETECTION_DELAY:
|
||||
autoDetectSuitableDelay();
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
configs,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Background from './background.js';
|
||||
|
||||
function log(...args) {
|
||||
if (configs.debug)
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
function uniqTabsAndDescendantsSet(tabs) {
|
||||
if (!Array.isArray(tabs))
|
||||
tabs = [tabs];
|
||||
return Array.from(new Set(tabs.map(tab => [tab].concat(tab.$TST.descendants)).flat())).sort(Tab.compare);
|
||||
}
|
||||
|
||||
function unblockAutoplayTree(tabs) {
|
||||
const tabsToUpdate = [];
|
||||
let shouldUnblockAutoplay = false;
|
||||
for (const tab of uniqTabsAndDescendantsSet(tabs)) {
|
||||
if (!shouldUnblockAutoplay && tab.$TST.autoplayBlocked)
|
||||
shouldUnblockAutoplay = true;
|
||||
tabsToUpdate.push(tab);
|
||||
}
|
||||
browser.waterfoxBridge.unblockAutoplay(tabsToUpdate.map(tab => tab.id));
|
||||
}
|
||||
|
||||
function unblockAutoplayDescendants(rootTabs) {
|
||||
const rootTabsSet = new Set(rootTabs);
|
||||
const tabsToUpdate = [];
|
||||
let shouldUnblockAutoplay = false;
|
||||
for (const tab of uniqTabsAndDescendantsSet(rootTabs)) {
|
||||
if (rootTabsSet.has(tab))
|
||||
continue;
|
||||
if (!shouldUnblockAutoplay && tab.$TST.autoplayBlocked)
|
||||
shouldUnblockAutoplay = true;
|
||||
tabsToUpdate.push(tab);
|
||||
}
|
||||
browser.waterfoxBridge.unblockAutoplay(tabsToUpdate.map(tab => tab.id));
|
||||
}
|
||||
|
||||
browser.menus.onClicked.addListener((info, contextTab) => {
|
||||
contextTab = contextTab && Tab.get(contextTab.id);
|
||||
const contextTabs = contextTab.$TST.multiselected ? Tab.getSelectedTabs(contextTab.windowId) : [contextTab];
|
||||
const inverted = info.button == 1;
|
||||
|
||||
switch (info.menuItemId.replace(/^grouped:/, '')) {
|
||||
case 'context_unblockAutoplay':
|
||||
browser.waterfoxBridge.unblockAutoplay(contextTabs.map(tab => tab.id));
|
||||
break;
|
||||
|
||||
case 'context_topLevel_unblockAutoplayTree':
|
||||
case 'unblockAutoplayTree':
|
||||
if (inverted)
|
||||
unblockAutoplayDescendants(contextTabs);
|
||||
else
|
||||
unblockAutoplayTree(contextTabs);
|
||||
break;
|
||||
|
||||
case 'context_topLevel_unblockAutoplayDescendants':
|
||||
case 'unblockAutoplayDescendants':
|
||||
if (inverted)
|
||||
unblockAutoplayTree(contextTabs);
|
||||
else
|
||||
unblockAutoplayDescendants(contextTabs);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
SidebarConnection.onMessage.addListener(async (windowId, message) => {
|
||||
switch (message.type) {
|
||||
case Constants.kCOMMAND_UNBLOCK_AUTOPLAY_FROM_SOUND_BUTTON: {
|
||||
await Tab.waitUntilTracked(message.tabId);
|
||||
const root = Tab.get(message.tabId);
|
||||
log('unblock autoplay from sound button: ', message, root);
|
||||
if (!root)
|
||||
break;
|
||||
|
||||
const multiselected = root.$TST.multiselected;
|
||||
const tabs = multiselected ?
|
||||
Tab.getSelectedTabs(root.windowId, { iterator: true }) :
|
||||
[root] ;
|
||||
|
||||
if (!multiselected &&
|
||||
root.$TST.subtreeCollapsed) {
|
||||
const tabsInTree = [root, ...root.$TST.descendants];
|
||||
const toBeUpdatedTabs = tabsInTree.filter(tab => tab.$TST.autoplayBlocked);
|
||||
log(' toBeUpdatedTabs: ', toBeUpdatedTabs);
|
||||
browser.waterfoxBridge.unblockAutoplay(toBeUpdatedTabs.map(tab => tab.id));
|
||||
}
|
||||
else {
|
||||
log(' tabs: ', tabs);
|
||||
browser.waterfoxBridge.unblockAutoplay(tabs.map(tab => tab.id));
|
||||
}
|
||||
}; break;
|
||||
}
|
||||
});
|
||||
|
||||
browser.commands.onCommand.addListener(async command => {
|
||||
let activeTabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
}).catch(ApiTabs.createErrorHandler());
|
||||
if (activeTabs.length == 0)
|
||||
activeTabs = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
}).catch(ApiTabs.createErrorHandler());
|
||||
const activeTab = Tab.get(activeTabs[0].id);
|
||||
const selectedTabs = activeTab.$TST.multiselected ? Tab.getSelectedTabs(activeTab.windowId) : [activeTab];
|
||||
|
||||
switch (command) {
|
||||
case 'unblockAutoplayTree':
|
||||
unblockAutoplayTree(selectedTabs);
|
||||
return;
|
||||
|
||||
case 'unblockAutoplayDescendants':
|
||||
unblockAutoplayDescendants(selectedTabs);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Background.onReady.addListener(() => {
|
||||
browser.waterfoxBridge.listAutoplayBlockedTabs().then(tabs => {
|
||||
for (const tab of tabs) {
|
||||
Tab.get(tab.id)?.$TST.addState(Constants.kTAB_STATE_AUTOPLAY_BLOCKED);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
browser.waterfoxBridge.onAutoplayBlocked.addListener(tab => {
|
||||
Tab.get(tab.id)?.$TST.addState(Constants.kTAB_STATE_AUTOPLAY_BLOCKED);
|
||||
});
|
||||
|
||||
browser.waterfoxBridge.onAutoplayUnblocked.addListener(tab => {
|
||||
Tab.get(tab.id)?.$TST.removeState(Constants.kTAB_STATE_AUTOPLAY_BLOCKED);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
configs,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
|
||||
import * as Commands from './commands.js';
|
||||
|
||||
browser.waterfoxBridge.onMenuCommand.addListener(async info => {
|
||||
switch (info.itemId) {
|
||||
case 'tabs-sidebar-newTab': {
|
||||
const behavior = info.button == 1 ?
|
||||
configs.autoAttachOnNewTabButtonMiddleClick :
|
||||
(info.ctrlKey || info.metaKey) ?
|
||||
configs.autoAttachOnNewTabButtonAccelClick :
|
||||
configs.autoAttachOnNewTabCommand;
|
||||
const win = await browser.windows.getLastFocused({ populate: true }).catch(ApiTabs.createErrorHandler());
|
||||
const activeTab = TabsStore.activeTabInWindow.get(win.id);
|
||||
Commands.openNewTabAs({
|
||||
baseTab: activeTab,
|
||||
as: behavior,
|
||||
});
|
||||
}; break;
|
||||
}
|
||||
});
|
||||
1161
waterfox/browser/components/sidebar/background/handle-misc.js
Normal file
1161
waterfox/browser/components/sidebar/background/handle-misc.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
configs,
|
||||
wait,
|
||||
isMacOS,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
import * as TSTAPI from '/common/tst-api.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Commands from './commands.js';
|
||||
import * as NativeTabGroups from './native-tab-groups.js';
|
||||
import * as TabsGroup from './tabs-group.js';
|
||||
import * as Tree from './tree.js';
|
||||
import * as TreeStructure from './tree-structure.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/handle-moved-tabs', ...args);
|
||||
}
|
||||
function logApiTabs(...args) {
|
||||
internalLogger('common/api-tabs', ...args);
|
||||
}
|
||||
|
||||
|
||||
let mMaybeTabMovingByShortcut = false;
|
||||
|
||||
|
||||
Tab.onCreated.addListener((tab, info = {}) => {
|
||||
if (!info.mayBeReplacedWithContainer &&
|
||||
(info.duplicated ||
|
||||
info.restored ||
|
||||
info.skipFixupTree ||
|
||||
// do nothing for already attached tabs
|
||||
(tab.openerTabId &&
|
||||
tab.$TST.parent == Tab.get(tab.openerTabId)))) {
|
||||
log('skip to fixup tree for replaced/duplicated/restored tab ', tab, info);
|
||||
return;
|
||||
}
|
||||
// if the tab is opened inside existing tree by someone, we must fixup the tree.
|
||||
if (!(info.positionedBySelf ||
|
||||
info.movedBySelfWhileCreation) &&
|
||||
(tab.$TST.nearestCompletelyOpenedNormalFollowingTab ||
|
||||
tab.$TST.nearestCompletelyOpenedNormalPrecedingTab ||
|
||||
(info.treeForActionDetection?.target &&
|
||||
(info.treeForActionDetection.target.next ||
|
||||
info.treeForActionDetection.target.previous)))) {
|
||||
tryFixupTreeForInsertedTab(tab, {
|
||||
toIndex: tab.index,
|
||||
fromIndex: Tab.getLastTab(tab.windowId).index,
|
||||
treeForActionDetection: info.treeForActionDetection,
|
||||
isTabCreating: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
log('no need to fixup tree for newly created tab ', tab, info);
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onMoving.addListener((tab, moveInfo) => {
|
||||
// avoid TabMove produced by browser.tabs.insertRelatedAfterCurrent=true or something.
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
const isNewlyOpenedTab = win.openingTabs.has(tab.id);
|
||||
const positionControlled = configs.insertNewChildAt != Constants.kINSERT_NO_CONTROL;
|
||||
if (!isNewlyOpenedTab ||
|
||||
!positionControlled ||
|
||||
moveInfo.byInternalOperation ||
|
||||
moveInfo.alreadyMoved ||
|
||||
!moveInfo.movedInBulk)
|
||||
return true;
|
||||
|
||||
// if there is no valid opener, it can be a restored initial tab in a restored window
|
||||
// and can be just moved as a part of window restoration process.
|
||||
if (!tab.$TST.openerTab)
|
||||
return true;
|
||||
|
||||
log('onTabMove for new child tab: move back '+moveInfo.toIndex+' => '+moveInfo.fromIndex);
|
||||
moveBack(tab, moveInfo);
|
||||
return false;
|
||||
});
|
||||
|
||||
async function tryFixupTreeForInsertedTab(tab, moveInfo = {}) {
|
||||
const internalGroupMoveCount = NativeTabGroups.internallyMovingNativeTabGroups.get(tab.groupId);
|
||||
if (internalGroupMoveCount) {
|
||||
log('ignore internal move of tab groups ', internalGroupMoveCount);
|
||||
return;
|
||||
}
|
||||
const parentTabOperationBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
|
||||
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
|
||||
...moveInfo,
|
||||
});
|
||||
log('tryFixupTreeForInsertedTab ', {
|
||||
tab: tab.id,
|
||||
parentTabOperationBehavior,
|
||||
moveInfo,
|
||||
childIds: tab.$TST.childIds,
|
||||
parentId: tab.$TST.parentId,
|
||||
});
|
||||
if (!moveInfo.isTabCreating &&
|
||||
parentTabOperationBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE) {
|
||||
if (tab.$TST.hasChild)
|
||||
tab.$TST.temporaryMetadata.set('childIdsBeforeMoved', tab.$TST.childIds.slice(0));
|
||||
tab.$TST.temporaryMetadata.set('parentIdBeforeMoved', tab.$TST.parentId);
|
||||
const replacedGroupTab = (parentTabOperationBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB) ?
|
||||
await TabsGroup.tryReplaceTabWithGroup(tab, { insertBefore: tab.$TST.firstChild }) :
|
||||
null;
|
||||
if (!replacedGroupTab && tab.$TST.hasChild) {
|
||||
if (tab.$TST.isGroupTab)
|
||||
await TabsGroup.clearTemporaryState(tab);
|
||||
await Tree.detachAllChildren(tab, {
|
||||
behavior: parentTabOperationBehavior,
|
||||
nearestFollowingRootTab: tab.$TST.firstChild.$TST.nearestFollowingRootTab,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
if (tab.$TST.parentId)
|
||||
await Tree.detachTab(tab, {
|
||||
broadcast: true
|
||||
});
|
||||
// Pinned tab is moved at first, so Tab.onPinned handler cannot know tree information
|
||||
// before the pinned tab was moved. Thus we cache tree information for the handler.
|
||||
wait(100).then(() => {
|
||||
tab.$TST.temporaryMetadata.delete('childIdsBeforeMoved');
|
||||
tab.$TST.temporaryMetadata.delete('parentIdBeforeMoved');
|
||||
});
|
||||
}
|
||||
|
||||
log('The tab can be placed inside existing tab unexpectedly, so now we are trying to fixup tree.');
|
||||
const action = Tree.detectTabActionFromNewPosition(tab, {
|
||||
isMovingByShortcut: mMaybeTabMovingByShortcut,
|
||||
...moveInfo,
|
||||
});
|
||||
log(' => action: ', action);
|
||||
if (!action.action) {
|
||||
log('no action');
|
||||
return;
|
||||
}
|
||||
|
||||
// When multiple tabs are moved at once by outside of TST (e.g. moving of multiselected tabs)
|
||||
// Tree.detectTabActionFromNewPosition() may be called for other tabs asynchronously
|
||||
// before this operation finishes. Thus we need to memorize the calculated "parent"
|
||||
// and Tree.detectTabActionFromNewPosition() will use it.
|
||||
if (action.parent)
|
||||
tab.$TST.temporaryMetadata.set('goingToBeAttachedTo', action.parent);
|
||||
|
||||
// notify event to helper addons with action and allow or deny
|
||||
const cache = {};
|
||||
const allowed = await TSTAPI.tryOperationAllowed(
|
||||
TSTAPI.kNOTIFY_TRY_FIXUP_TREE_ON_TAB_MOVED,
|
||||
{
|
||||
tab,
|
||||
fromIndex: moveInfo.fromIndex,
|
||||
toIndex: moveInfo.toIndex,
|
||||
action: action.action,
|
||||
parent: action.parent,
|
||||
insertBefore: action.insertBefore,
|
||||
insertAfter: action.insertAfter,
|
||||
},
|
||||
{ tabProperties: ['tab', 'parent', 'insertBefore', 'insertAfter'], cache }
|
||||
);
|
||||
TSTAPI.clearCache(cache);
|
||||
|
||||
if (!allowed) {
|
||||
log('no action - canceled by a helper addon');
|
||||
}
|
||||
else {
|
||||
log('action: ', action);
|
||||
switch (action.action) {
|
||||
case 'invalid':
|
||||
moveBack(tab, moveInfo);
|
||||
break;
|
||||
|
||||
default:
|
||||
log('tryFixupTreeForInsertedTab: apply action for unattached tab: ', tab, action);
|
||||
await action.apply();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tab.$TST.temporaryMetadata.get('goingToBeAttachedTo') == action.parent)
|
||||
tab.$TST.temporaryMetadata.delete('goingToBeAttachedTo');
|
||||
}
|
||||
|
||||
function reserveToEnsureRootTabVisible(tab) {
|
||||
reserveToEnsureRootTabVisible.tabIds.add(tab.id);
|
||||
if (reserveToEnsureRootTabVisible.reserved)
|
||||
clearTimeout(reserveToEnsureRootTabVisible.reserved);
|
||||
reserveToEnsureRootTabVisible.reserved = setTimeout(() => {
|
||||
delete reserveToEnsureRootTabVisible.reserved;
|
||||
const tabs = Array.from(reserveToEnsureRootTabVisible.tabIds, Tab.get);
|
||||
reserveToEnsureRootTabVisible.tabIds.clear();
|
||||
for (const tab of tabs) {
|
||||
if (!tab.$TST ||
|
||||
tab.$TST.parent ||
|
||||
!tab.$TST.collapsed)
|
||||
continue;
|
||||
Tree.collapseExpandTabAndSubtree(tab, {
|
||||
collapsed: false,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
reserveToEnsureRootTabVisible.tabIds = new Set();
|
||||
|
||||
Tab.onMoved.addListener((tab, moveInfo = {}) => {
|
||||
if (moveInfo.byInternalOperation ||
|
||||
!moveInfo.movedInBulk ||
|
||||
tab.$TST.duplicating) {
|
||||
log('internal move');
|
||||
tab.$TST.nativeTabGroup?.$TST.reindex();
|
||||
}
|
||||
else {
|
||||
log('process moved tab');
|
||||
tryFixupTreeForInsertedTab(tab, moveInfo).then(() => {
|
||||
tab.$TST.nativeTabGroup?.$TST.reindex();
|
||||
});
|
||||
}
|
||||
reserveToEnsureRootTabVisible(tab);
|
||||
});
|
||||
|
||||
function onMessage(message, _sender) {
|
||||
if (!message ||
|
||||
typeof message.type != 'string')
|
||||
return;
|
||||
|
||||
//log('onMessage: ', message, sender);
|
||||
switch (message.type) {
|
||||
case Constants.kNOTIFY_TAB_MOUSEDOWN:
|
||||
mMaybeTabMovingByShortcut = false;
|
||||
break;
|
||||
|
||||
case Constants.kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH:
|
||||
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
|
||||
return;
|
||||
mMaybeTabMovingByShortcut = true;
|
||||
break;
|
||||
|
||||
case Constants.kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH:
|
||||
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
|
||||
return;
|
||||
mMaybeTabMovingByShortcut = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
|
||||
|
||||
Commands.onMoveUp.addListener(async tab => {
|
||||
await tryFixupTreeForInsertedTab(tab, {
|
||||
toIndex: tab.index,
|
||||
fromIndex: tab.index + 1,
|
||||
});
|
||||
});
|
||||
|
||||
Commands.onMoveDown.addListener(async tab => {
|
||||
await tryFixupTreeForInsertedTab(tab, {
|
||||
toIndex: tab.index,
|
||||
fromIndex: tab.index - 1,
|
||||
});
|
||||
});
|
||||
|
||||
TreeStructure.onTabAttachedFromRestoredInfo.addListener((tab, moveInfo) => { tryFixupTreeForInsertedTab(tab, moveInfo); });
|
||||
|
||||
function moveBack(tab, moveInfo) {
|
||||
log('Move back tab from unexpected move: ', dumpTab(tab), moveInfo);
|
||||
const id = tab.id;
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
const index = moveInfo.fromIndex;
|
||||
win.internalMovingTabs.set(id, index);
|
||||
logApiTabs(`handle-moved-tabs:moveBack: browser.tabs.move() `, tab.id, {
|
||||
windowId: moveInfo.windowId,
|
||||
index: moveInfo.fromIndex
|
||||
});
|
||||
// Because we need to use the raw "fromIndex" directly,
|
||||
// we cannot use TabsMove.moveTabInternallyBefore/After here.
|
||||
return browser.tabs.move(tab.id, {
|
||||
windowId: moveInfo.windowId,
|
||||
index: moveInfo.fromIndex
|
||||
}).catch(ApiTabs.createErrorHandler(e => {
|
||||
if (win.internalMovingTabs.get(id) == index)
|
||||
win.internalMovingTabs.delete(id);
|
||||
ApiTabs.handleMissingTabError(e);
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
configs,
|
||||
isFirefoxViewTab,
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
import * as TSTAPI from '/common/tst-api.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as TabsMove from './tabs-move.js';
|
||||
import * as TabsOpen from './tabs-open.js';
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/handle-new-tabs', ...args);
|
||||
}
|
||||
|
||||
|
||||
Tab.onBeforeCreate.addListener(async (tab, info) => {
|
||||
const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);
|
||||
|
||||
// Special case, when all these conditions are true:
|
||||
// 1) A new blank tab is configured to be opened as a child of the active tab.
|
||||
// 2) The active tab is pinned.
|
||||
// 3) Tabs opened from a pinned parent are configured to be placed near the
|
||||
// opener pinned tab.
|
||||
// then we fakely attach the new blank tab to the active pinned tab.
|
||||
// See also https://github.com/piroor/treestyletab/issues/3296
|
||||
const shouldAttachToPinnedOpener = (
|
||||
!tab.openerTabId &&
|
||||
!tab.pinned &&
|
||||
tab.$TST.isNewTabCommandTab &&
|
||||
Constants.kCONTROLLED_NEWTAB_POSITION.has(configs.autoAttachOnNewTabCommand) &&
|
||||
(
|
||||
(activeTab?.pinned &&
|
||||
Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromPinnedTabAt)) ||
|
||||
(isFirefoxViewTab(activeTab) &&
|
||||
Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromFirefoxViewAt))
|
||||
)
|
||||
);
|
||||
if (shouldAttachToPinnedOpener)
|
||||
tab.openerTabId = activeTab.id;
|
||||
});
|
||||
|
||||
// this should return false if the tab is / may be moved while processing
|
||||
Tab.onCreating.addListener((tab, info = {}) => {
|
||||
if (info.duplicatedInternally)
|
||||
return true;
|
||||
|
||||
log('Tabs.onCreating ', dumpTab(tab), tab.openerTabId, info);
|
||||
|
||||
const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);
|
||||
const opener = tab.$TST.openerTab;
|
||||
if (opener) {
|
||||
tab.$TST.setAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID, opener.$TST.uniqueId.id);
|
||||
if (!info.bypassTabControl)
|
||||
TabsStore.addToBeGroupedTab(tab);
|
||||
}
|
||||
else {
|
||||
let dontMove = false;
|
||||
if (!info.maybeOrphan &&
|
||||
!info.bypassTabControl &&
|
||||
activeTab &&
|
||||
!info.restored) {
|
||||
let autoAttachBehavior = configs.autoAttachOnNewTabCommand;
|
||||
if (tab.$TST.nextTab &&
|
||||
activeTab == tab.$TST.previousTab) {
|
||||
// New tab opened with browser.tabs.insertAfterCurrent=true may have
|
||||
// next tab. In this case the tab is expected to be placed next to the
|
||||
// active tab always, so we should change the behavior specially.
|
||||
// See also:
|
||||
// https://github.com/piroor/treestyletab/issues/2054
|
||||
// https://github.com/piroor/treestyletab/issues/2194#issuecomment-505272940
|
||||
dontMove = true;
|
||||
switch (autoAttachBehavior) {
|
||||
case Constants.kNEWTAB_OPEN_AS_ORPHAN:
|
||||
case Constants.kNEWTAB_OPEN_AS_SIBLING:
|
||||
case Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING:
|
||||
if (activeTab.$TST.hasChild)
|
||||
autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_CHILD;
|
||||
else
|
||||
autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING;
|
||||
break;
|
||||
|
||||
case Constants.kNEWTAB_OPEN_AS_CHILD:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tab.$TST.isNewTabCommandTab) {
|
||||
if (!info.positionedBySelf) {
|
||||
log('behave as a tab opened by new tab command');
|
||||
return handleNewTabFromActiveTab(tab, {
|
||||
activeTab,
|
||||
autoAttachBehavior,
|
||||
dontMove,
|
||||
openedWithCookieStoreId: info.openedWithCookieStoreId,
|
||||
inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
|
||||
}).then(moved => !moved);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else if (activeTab != tab) {
|
||||
tab.$TST.temporaryMetadata.set('possibleOpenerTab', activeTab.id);
|
||||
}
|
||||
if (!info.fromExternal)
|
||||
tab.$TST.temporaryMetadata.set('isNewTab', true);
|
||||
}
|
||||
if (info.fromExternal &&
|
||||
!info.bypassTabControl) {
|
||||
log('behave as a tab opened from external application');
|
||||
// we may need to reopen the tab with loaded URL
|
||||
if (configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
|
||||
tab.$TST.temporaryMetadata.set('fromExternal', true);
|
||||
return notifyToTryHandleNewTab(tab, {
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
|
||||
activeTab,
|
||||
}).then(allowed => {
|
||||
if (!allowed) {
|
||||
log(' => handling is canceled by someone');
|
||||
return true;
|
||||
}
|
||||
return Tree.behaveAutoAttachedTab(tab, {
|
||||
baseTab: activeTab,
|
||||
behavior: configs.autoAttachOnOpenedFromExternal,
|
||||
dontMove,
|
||||
broadcast: true
|
||||
}).then(moved => !moved);
|
||||
});
|
||||
}
|
||||
log('behave as a tab opened with any URL ', tab.title, tab.url);
|
||||
if (!info.restored &&
|
||||
!info.positionedBySelf &&
|
||||
!info.bypassTabControl &&
|
||||
configs.autoAttachOnAnyOtherTrigger != Constants.kNEWTAB_DO_NOTHING) {
|
||||
if (configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
|
||||
tab.$TST.temporaryMetadata.set('anyOtherTrigger', true);
|
||||
log('controlled as a new tab from other unknown trigger');
|
||||
return notifyToTryHandleNewTab(tab, {
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
|
||||
activeTab,
|
||||
}).then(allowed => {
|
||||
if (!allowed) {
|
||||
log(' => handling is canceled by someone');
|
||||
return true;
|
||||
}
|
||||
return Tree.behaveAutoAttachedTab(tab, {
|
||||
baseTab: activeTab,
|
||||
behavior: configs.autoAttachOnAnyOtherTrigger,
|
||||
dontMove,
|
||||
broadcast: true
|
||||
}).then(moved => !moved);
|
||||
});
|
||||
}
|
||||
if (info.positionedBySelf)
|
||||
tab.$TST.temporaryMetadata.set('positionedBySelf', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
log(`opener: ${dumpTab(opener)}, positionedBySelf = ${info.positionedBySelf}`);
|
||||
if (!info.bypassTabControl &&
|
||||
opener &&
|
||||
(opener.pinned || isFirefoxViewTab(opener)) &&
|
||||
opener.windowId == tab.windowId) {
|
||||
return handleTabsFromPinnedOpener(tab, opener, { activeTab }).then(moved => !moved);
|
||||
}
|
||||
else if (!info.maybeOrphan || info.bypassTabControl) {
|
||||
if (info.fromExternal &&
|
||||
!info.bypassTabControl &&
|
||||
configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
|
||||
tab.$TST.temporaryMetadata.set('fromExternal', true);
|
||||
return notifyToTryHandleNewTab(tab, {
|
||||
context: info.fromExternal && !info.bypassTabControl ?
|
||||
TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL :
|
||||
info.duplicated ?
|
||||
TSTAPI.kNEWTAB_CONTEXT_DUPLICATED :
|
||||
TSTAPI.kNEWTAB_CONTEXT_WITH_OPENER,
|
||||
activeTab,
|
||||
openerTab: opener,
|
||||
}).then(allowed => {
|
||||
if (!allowed) {
|
||||
log(' => handling is canceled by someone');
|
||||
return true;
|
||||
}
|
||||
const behavior = info.fromExternal && !info.bypassTabControl ?
|
||||
configs.autoAttachOnOpenedFromExternal :
|
||||
info.duplicated ?
|
||||
configs.autoAttachOnDuplicated :
|
||||
configs.autoAttachOnOpenedWithOwner;
|
||||
return Tree.behaveAutoAttachedTab(tab, {
|
||||
baseTab: opener,
|
||||
behavior,
|
||||
dontMove: info.positionedBySelf || info.mayBeReplacedWithContainer,
|
||||
broadcast: true
|
||||
}).then(moved => !moved);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
async function notifyToTryHandleNewTab(tab, { context, activeTab, openerTab } = {}) {
|
||||
const cache = {};
|
||||
const result = TSTAPI.tryOperationAllowed(
|
||||
TSTAPI.kNOTIFY_TRY_HANDLE_NEWTAB,
|
||||
{ tab,
|
||||
activeTab,
|
||||
openerTab,
|
||||
context },
|
||||
{ tabProperties: ['tab', 'activeTab', 'openerTab'], cache }
|
||||
);
|
||||
TSTAPI.clearCache(cache);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function handleNewTabFromActiveTab(tab, { url, activeTab, autoAttachBehavior, dontMove, openedWithCookieStoreId, inheritContextualIdentityMode, context } = {}) {
|
||||
log('handleNewTabFromActiveTab: activeTab = ', dumpTab(activeTab), { url, activeTab, autoAttachBehavior, dontMove, inheritContextualIdentityMode, context });
|
||||
if (activeTab &&
|
||||
activeTab.$TST.ancestors.includes(tab)) {
|
||||
log(' => ignore restored ancestor tab');
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowed = await notifyToTryHandleNewTab(tab, {
|
||||
context,
|
||||
activeTab,
|
||||
});
|
||||
if (!allowed) {
|
||||
log(' => handling is canceled by someone');
|
||||
return false;
|
||||
}
|
||||
|
||||
const moved = await Tree.behaveAutoAttachedTab(tab, {
|
||||
baseTab: activeTab,
|
||||
behavior: autoAttachBehavior,
|
||||
broadcast: true,
|
||||
dontMove: dontMove || false
|
||||
});
|
||||
if (openedWithCookieStoreId) {
|
||||
log('handleNewTabFromActiveTab: do not reopen tab opened with contextual identity explicitly');
|
||||
return moved;
|
||||
}
|
||||
if (tab.cookieStoreId && tab.cookieStoreId != 'firefox-default') {
|
||||
log('handleNewTabFromActiveTab: do not reopen tab opened with non-default contextual identity ', tab.cookieStoreId);
|
||||
return moved;
|
||||
}
|
||||
|
||||
const parent = tab.$TST.parent;
|
||||
let cookieStoreId = null;
|
||||
switch (inheritContextualIdentityMode) {
|
||||
case Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT:
|
||||
if (parent)
|
||||
cookieStoreId = parent.cookieStoreId;
|
||||
break;
|
||||
|
||||
case Constants.kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE:
|
||||
cookieStoreId = activeTab.cookieStoreId
|
||||
break;
|
||||
|
||||
default:
|
||||
return moved;
|
||||
}
|
||||
if ((tab.cookieStoreId || 'firefox-default') == (cookieStoreId || 'firefox-default')) {
|
||||
log('handleNewTabFromActiveTab: no need to reopen with inherited contextual identity ', cookieStoreId);
|
||||
return moved;
|
||||
}
|
||||
|
||||
if (!configs.inheritContextualIdentityToUnopenableURLTabs &&
|
||||
!TabsOpen.isOpenable(url)) {
|
||||
log('handleNewTabFromActiveTab: not openable URL, skip reopening ', cookieStoreId, url);
|
||||
return moved;
|
||||
}
|
||||
|
||||
log('handleNewTabFromActiveTab: reopen with inherited contextual identity ', cookieStoreId);
|
||||
// We need to prevent grouping of this original tab and the reopened tab
|
||||
// by the "multiple tab opened in XXX msec" feature.
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
win.openedNewTabs.delete(tab.id);
|
||||
await TabsOpen.openURIInTab(url || null, {
|
||||
windowId: activeTab.windowId,
|
||||
parent,
|
||||
insertBefore: tab,
|
||||
active: tab.active,
|
||||
cookieStoreId
|
||||
});
|
||||
TabsInternalOperation.removeTab(tab);
|
||||
return moved;
|
||||
}
|
||||
|
||||
async function handleTabsFromPinnedOpener(tab, opener, { activeTab } = {}) {
|
||||
const allowed = await notifyToTryHandleNewTab(tab, {
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_FROM_PINNED,
|
||||
activeTab,
|
||||
openerTab: opener,
|
||||
});
|
||||
if (!allowed) {
|
||||
log('handleTabsFromPinnedOpener: handling is canceled by someone');
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent = Tab.getGroupTabForOpener(opener);
|
||||
if (parent) {
|
||||
log('handleTabsFromPinnedOpener: attach to corresponding group tab');
|
||||
tab.$TST.setAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, true);
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
// it could be updated already...
|
||||
const lastRelatedTab = opener.$TST.lastRelatedTabId == tab.id ?
|
||||
opener.$TST.previousLastRelatedTab :
|
||||
opener.$TST.lastRelatedTab;
|
||||
// If there is already opened group tab, it is more natural that
|
||||
// opened tabs are treated as a tab opened from unpinned tabs.
|
||||
const insertAt = configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB ?
|
||||
Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB :
|
||||
configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_TOP ?
|
||||
Constants.kINSERT_TOP :
|
||||
configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_END ?
|
||||
Constants.kINSERT_END :
|
||||
undefined;
|
||||
return Tree.attachTabTo(tab, parent, {
|
||||
lastRelatedTab,
|
||||
insertAt,
|
||||
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
|
||||
if ((configs.autoGroupNewTabsFromPinned ||
|
||||
configs.autoGroupNewTabsFromFirefoxView) &&
|
||||
tab.$TST.needToBeGroupedSiblings.length > 0) {
|
||||
log('handleTabsFromPinnedOpener: controlled by auto-grouping');
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (isFirefoxViewTab(opener) ? configs.insertNewTabFromFirefoxViewAt : configs.insertNewTabFromPinnedTabAt) {
|
||||
case Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB: {
|
||||
// it could be updated already...
|
||||
const lastRelatedTab = opener.$TST.lastRelatedTab != tab ?
|
||||
opener.$TST.lastRelatedTab :
|
||||
opener.$TST.previousLastRelatedTab;
|
||||
if (lastRelatedTab) {
|
||||
log(`handleTabsFromPinnedOpener: place after last related tab ${dumpTab(lastRelatedTab)}`);
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
return TabsMove.moveTabAfter(tab, lastRelatedTab.$TST.lastDescendant || lastRelatedTab, {
|
||||
delayedMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
|
||||
if (lastPinnedTab) {
|
||||
log(`handleTabsFromPinnedOpener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
return TabsMove.moveTabAfter(tab, lastPinnedTab, {
|
||||
delayedMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
|
||||
if (firstNormalTab) {
|
||||
log(`handleTabsFromPinnedOpener: place before first unpinned tab ${dumpTab(firstNormalTab)}`);
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
return TabsMove.moveTabBefore(tab, firstNormalTab, {
|
||||
delayedMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
};
|
||||
case Constants.kINSERT_TOP: {
|
||||
const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
|
||||
if (lastPinnedTab) {
|
||||
log(`handleTabsFromPinnedOpener: opened from pinned opener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
return TabsMove.moveTabAfter(tab, lastPinnedTab, {
|
||||
delayedMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
|
||||
if (firstNormalTab) {
|
||||
log(`handleTabsFromPinnedOpener: opened from pinned opener: place before first pinned tab ${dumpTab(firstNormalTab)}`);
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
return TabsMove.moveTabBefore(tab, firstNormalTab, {
|
||||
delayedMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}; break;
|
||||
|
||||
case Constants.kINSERT_END: {
|
||||
const lastTab = Tab.getLastTab(tab.windowId);
|
||||
log('handleTabsFromPinnedOpener: opened from pinned opener: place after the last tab ', lastTab);
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
return TabsMove.moveTabAfter(tab, lastTab, {
|
||||
delayedMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
Tab.onCreated.addListener((tab, info = {}) => {
|
||||
if (!info.duplicated ||
|
||||
info.bypassTabControl)
|
||||
return;
|
||||
const original = info.originalTab;
|
||||
log('duplicated ', dumpTab(tab), dumpTab(original));
|
||||
if (info.duplicatedInternally) {
|
||||
log('duplicated by internal operation');
|
||||
tab.$TST.addState(Constants.kTAB_STATE_DUPLICATING, { broadcast: true });
|
||||
TabsStore.addDuplicatingTab(tab);
|
||||
}
|
||||
else {
|
||||
// On old versions of Firefox, duplicated tabs had no openerTabId so they were
|
||||
// not handled by Tab.onCreating listener. Today they are already handled before
|
||||
// here, so this is just a failsafe (or for old versions of Firefox).
|
||||
// See also: https://github.com/piroor/treestyletab/issues/2830#issuecomment-831414189
|
||||
Tree.behaveAutoAttachedTab(tab, {
|
||||
baseTab: original,
|
||||
behavior: configs.autoAttachOnDuplicated,
|
||||
dontMove: info.positionedBySelf || info.movedBySelfWhileCreation || info.mayBeReplacedWithContainer,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onUpdated.addListener((tab, changeInfo) => {
|
||||
if ('openerTabId' in changeInfo &&
|
||||
configs.syncParentTabAndOpenerTab &&
|
||||
!tab.$TST.updatingOpenerTabIds.includes(changeInfo.openerTabId) /* accept only changes from outside of TST */) {
|
||||
Tab.waitUntilTrackedAll(tab.windowId).then(() => {
|
||||
const parent = tab.$TST.openerTab;
|
||||
if (!parent ||
|
||||
parent.windowId != tab.windowId ||
|
||||
parent == tab.$TST.parent)
|
||||
return;
|
||||
Tree.attachTabTo(tab, parent, {
|
||||
insertAt: Constants.kINSERT_NEAREST,
|
||||
forceExpand: tab.active,
|
||||
broadcast: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (tab.$TST.temporaryMetadata.has('openedCompletely') &&
|
||||
tab.windowId == tab.$windowIdOnCreated && // Don't treat tab as "opened from active tab" if it is moved across windows while loading
|
||||
(changeInfo.url || changeInfo.status == 'complete') &&
|
||||
(tab.$TST.temporaryMetadata.has('isNewTab') ||
|
||||
tab.$TST.temporaryMetadata.has('fromExternal') ||
|
||||
tab.$TST.temporaryMetadata.has('anyOtherTrigger'))) {
|
||||
log('loaded tab ', dumpTab(tab), {
|
||||
isNewTab: tab.$TST.temporaryMetadata.has('isNewTab'),
|
||||
fromExternal: tab.$TST.temporaryMetadata.has('fromExternal'),
|
||||
anyOtherTrigger: tab.$TST.temporaryMetadata.has('anyOtherTrigger'),
|
||||
});
|
||||
tab.$TST.temporaryMetadata.delete('isNewTab');
|
||||
const possibleOpenerTab = Tab.get(tab.$TST.temporaryMetadata.get('possibleOpenerTab'));
|
||||
tab.$TST.temporaryMetadata.delete('possibleOpenerTab');
|
||||
log('possibleOpenerTab ', dumpTab(possibleOpenerTab));
|
||||
|
||||
if (tab.$TST.temporaryMetadata.has('fromExternal')) {
|
||||
tab.$TST.temporaryMetadata.delete('fromExternal');
|
||||
log('behave as a tab opened from external application (delayed)');
|
||||
handleNewTabFromActiveTab(tab, {
|
||||
url: tab.url,
|
||||
activeTab: possibleOpenerTab,
|
||||
autoAttachBehavior: configs.autoAttachOnOpenedFromExternal,
|
||||
inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromExternalMode,
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab.$TST.temporaryMetadata.has('anyOtherTrigger')) {
|
||||
tab.$TST.temporaryMetadata.delete('anyOtherTrigger');
|
||||
log('behave as a tab opened from any other trigger (delayed)');
|
||||
handleNewTabFromActiveTab(tab, {
|
||||
url: tab.url,
|
||||
activeTab: possibleOpenerTab,
|
||||
autoAttachBehavior: configs.autoAttachOnAnyOtherTrigger,
|
||||
inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode,
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
log('win.openedNewTabs ', win.openedNewTabs);
|
||||
if (tab.$TST.parent ||
|
||||
!possibleOpenerTab ||
|
||||
win.openedNewTabs.has(tab.id) ||
|
||||
tab.$TST.temporaryMetadata.has('openedWithOthers') ||
|
||||
tab.$TST.temporaryMetadata.has('positionedBySelf')) {
|
||||
log(' => no need to control ', {
|
||||
parent: tab.$TST.parent,
|
||||
possibleOpenerTab,
|
||||
openedNewTab: win.openedNewTabs.has(tab.id),
|
||||
openedWithOthers: tab.$TST.temporaryMetadata.has('openedWithOthers'),
|
||||
positionedBySelf: tab.$TST.temporaryMetadata.has('positionedBySelf')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab.$TST.isNewTabCommandTab) {
|
||||
log('behave as a tab opened by new tab command (delayed)');
|
||||
tab.$TST.addState(Constants.kTAB_STATE_NEW_TAB_COMMAND_TAB);
|
||||
handleNewTabFromActiveTab(tab, {
|
||||
activeTab: possibleOpenerTab,
|
||||
autoAttachBehavior: configs.autoAttachOnNewTabCommand,
|
||||
inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const siteMatcher = /^\w+:\/\/([^\/]+)(?:$|\/.*$)/;
|
||||
const openerTabSite = possibleOpenerTab.url.match(siteMatcher);
|
||||
const newTabSite = tab.url.match(siteMatcher);
|
||||
if (openerTabSite &&
|
||||
newTabSite &&
|
||||
tab.url != possibleOpenerTab.url && // It may be opened by "Duplciate Tab" or "Open in New Container Tab" if the URL is completely same.
|
||||
openerTabSite[1] == newTabSite[1]) {
|
||||
log('behave as a tab opened from same site (delayed)');
|
||||
tab.$TST.addState(Constants.kTAB_STATE_OPENED_FOR_SAME_WEBSITE);
|
||||
handleNewTabFromActiveTab(tab, {
|
||||
url: tab.url,
|
||||
activeTab: possibleOpenerTab,
|
||||
autoAttachBehavior: configs.autoAttachSameSiteOrphan,
|
||||
inheritContextualIdentityMode: configs.inheritContextualIdentityToSameSiteOrphanMode,
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_WEBSITE_SAME_TO_ACTIVE_TAB,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log('checking special openers (delayed)', { opener: possibleOpenerTab.url, child: tab.url });
|
||||
for (const rule of Constants.kAGGRESSIVE_OPENER_TAB_DETECTION_RULES_WITH_URL) {
|
||||
if (rule.opener.test(possibleOpenerTab.url) &&
|
||||
rule.child.test(tab.url)) {
|
||||
log('behave as a tab opened from special opener (delayed)', { rule });
|
||||
handleNewTabFromActiveTab(tab, {
|
||||
url: tab.url,
|
||||
activeTab: possibleOpenerTab,
|
||||
autoAttachBehavior: configs.autoAttachOnOpenedWithOwner,
|
||||
context: TSTAPI.kNEWTAB_CONTEXT_FROM_ABOUT_ADDONS,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Tab.onAttached.addListener(async (tab, attachInfo = {}) => {
|
||||
if (!attachInfo.windowId)
|
||||
return;
|
||||
|
||||
const parentTabOperationBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
|
||||
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
|
||||
...attachInfo,
|
||||
});
|
||||
if (parentTabOperationBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
|
||||
return;
|
||||
|
||||
log('Tabs.onAttached ', dumpTab(tab), attachInfo);
|
||||
|
||||
log('descendants of attached tab: ', () => attachInfo.descendants.map(dumpTab));
|
||||
const movedTabs = await Tree.moveTabs(attachInfo.descendants, {
|
||||
destinationWindowId: tab.windowId,
|
||||
insertAfter: tab
|
||||
});
|
||||
log('moved descendants: ', () => movedTabs.map(dumpTab));
|
||||
if (attachInfo.descendants.length == movedTabs.length) {
|
||||
await Tree.applyTreeStructureToTabs(
|
||||
[tab, ...movedTabs],
|
||||
attachInfo.structure
|
||||
);
|
||||
}
|
||||
else {
|
||||
for (const movedTab of movedTabs) {
|
||||
Tree.attachTabTo(movedTab, tab, {
|
||||
broadcast: true,
|
||||
dontMove: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
wait,
|
||||
mapAndFilterUniq,
|
||||
countMatched,
|
||||
configs
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Background from './background.js';
|
||||
import * as TabsGroup from './tabs-group.js';
|
||||
import * as Tree from './tree.js';
|
||||
import * as Commands from './commands.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/handle-removed-tabs', ...args);
|
||||
}
|
||||
|
||||
|
||||
Tab.onRemoving.addListener(async (tab, removeInfo = {}) => {
|
||||
log('Tabs.onRemoving ', dumpTab(tab), removeInfo);
|
||||
if (removeInfo.isWindowClosing)
|
||||
return;
|
||||
|
||||
let closeParentBehavior;
|
||||
let newParent;
|
||||
const successor = tab.$TST.possibleSuccessorWithDifferentContainer;
|
||||
if (successor) {
|
||||
log('override closeParentBehaior with kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD for different container successor ', successor);
|
||||
closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
|
||||
// When a new tab is created with a different container and this tab
|
||||
// is removed immediately before the new tab is completely handled,
|
||||
// TST fails to detect the new tab as the successor of this tab. Thus,
|
||||
// we treat the new tab as the successor - the first child of this
|
||||
// (actually not attached to this tab yet).
|
||||
if (successor && successor != tab.$TST.firstChild)
|
||||
newParent = successor;
|
||||
}
|
||||
else {
|
||||
closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
|
||||
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
|
||||
...removeInfo,
|
||||
});
|
||||
log('detected closeParentBehaior: ', closeParentBehavior);
|
||||
}
|
||||
|
||||
if (closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
|
||||
tab.$TST.subtreeCollapsed)
|
||||
Tree.collapseExpandSubtree(tab, {
|
||||
collapsed: false,
|
||||
justNow: true,
|
||||
broadcast: false // because the tab is going to be closed, broadcasted Tree.collapseExpandSubtree can be ignored.
|
||||
});
|
||||
|
||||
const postProcessParams = {
|
||||
windowId: tab.windowId,
|
||||
removedTab: tab.$TST.export(true),
|
||||
structure: TreeBehavior.getTreeStructureFromTabs([tab, ...tab.$TST.descendants], {
|
||||
full: true,
|
||||
keepParentOfRootTabs: true
|
||||
}),
|
||||
insertBefore: tab, // not firstChild, because the "tab" is disappeared from tree.
|
||||
parent: tab.$TST.parent,
|
||||
newParent,
|
||||
children: tab.$TST.children,
|
||||
descendants: tab.$TST.descendants,
|
||||
nearestFollowingRootTab: tab.$TST.nearestFollowingRootTab,
|
||||
closeParentBehavior
|
||||
};
|
||||
|
||||
if (tab.$TST.subtreeCollapsed) {
|
||||
tryGrantCloseTab(tab, closeParentBehavior).then(async granted => {
|
||||
if (!granted)
|
||||
return;
|
||||
log('Tabs.onRemoving: granted to close ', dumpTab(tab));
|
||||
handleRemovingPostProcess(postProcessParams)
|
||||
});
|
||||
// First we always need to detach children from the closing parent.
|
||||
// They will be processed again after confirmation.
|
||||
Tree.detachAllChildren(tab, {
|
||||
newParent,
|
||||
behavior: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN,
|
||||
dontExpand: true,
|
||||
dontUpdateIndent: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
await handleRemovingPostProcess(postProcessParams)
|
||||
}
|
||||
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
if (!win.internalClosingTabs.has(tab.$TST.parentId))
|
||||
Tree.detachTab(tab, {
|
||||
dontUpdateIndent: true,
|
||||
dontSyncParentToOpenerTab: true,
|
||||
broadcast: true
|
||||
});
|
||||
});
|
||||
async function handleRemovingPostProcess({ closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure } = {}) {
|
||||
log('handleRemovingPostProcess ', { closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure });
|
||||
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
|
||||
await closeChildTabs(descendants, {
|
||||
triggerTab: removedTab,
|
||||
originalStructure: structure
|
||||
});
|
||||
|
||||
const replacedGroupTab = (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB) ?
|
||||
await TabsGroup.tryReplaceTabWithGroup(null, { windowId, parent, children, insertBefore, newParent }) :
|
||||
null;
|
||||
|
||||
if (!replacedGroupTab) {
|
||||
await Tree.detachAllChildren(null, {
|
||||
windowId,
|
||||
parent,
|
||||
newParent,
|
||||
children,
|
||||
descendants,
|
||||
nearestFollowingRootTab,
|
||||
behavior: closeParentBehavior,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function tryGrantCloseTab(tab, closeParentBehavior) {
|
||||
log('tryGrantClose: ', { alreadyGranted: configs.grantedRemovingTabIds, closing: dumpTab(tab) });
|
||||
const alreadyGranted = configs.grantedRemovingTabIds.includes(tab.id);
|
||||
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
|
||||
if (!tab || alreadyGranted) {
|
||||
log(' => no need to confirm');
|
||||
return true;
|
||||
}
|
||||
|
||||
const self = tryGrantCloseTab;
|
||||
|
||||
self.closingTabIds.push(tab.id);
|
||||
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE) {
|
||||
self.closingDescendantTabIds = self.closingDescendantTabIds
|
||||
.concat(TreeBehavior.getClosingTabsFromParent(tab).map(tab => tab.id));
|
||||
self.closingDescendantTabIds = Array.from(new Set(self.closingDescendantTabIds));
|
||||
}
|
||||
|
||||
if (self.promisedGrantedToCloseTabs) {
|
||||
log(' => have promisedGrantedToCloseTabs');
|
||||
return self.promisedGrantedToCloseTabs;
|
||||
}
|
||||
|
||||
self.closingTabWasActive = self.closingTabWasActive || tab.active;
|
||||
|
||||
let shouldRestoreCount;
|
||||
self.promisedGrantedToCloseTabs = wait(250).then(async () => {
|
||||
log(' => confirmation with delay');
|
||||
const closingTabIds = new Set(self.closingTabIds);
|
||||
let allClosingTabs = new Set();
|
||||
allClosingTabs.add(tab);
|
||||
self.closingTabIds = Array.from(closingTabIds);
|
||||
self.closingDescendantTabIds = mapAndFilterUniq(self.closingDescendantTabIds, id => {
|
||||
if (closingTabIds.has(id))
|
||||
return undefined;
|
||||
const tab = Tab.get(id);
|
||||
if (tab) // ignore already closed tabs
|
||||
allClosingTabs.add(tab);
|
||||
return id;
|
||||
});
|
||||
allClosingTabs = Array.from(allClosingTabs);
|
||||
shouldRestoreCount = self.closingTabIds.length;
|
||||
const restorableClosingTabsCount = countMatched(
|
||||
allClosingTabs,
|
||||
tab => tab.url != 'about:blank' &&
|
||||
!tab.$TST.isNewTabCommandTab
|
||||
);
|
||||
log(' => restorableClosingTabsCount: ', restorableClosingTabsCount);
|
||||
if (restorableClosingTabsCount > 0) {
|
||||
log('tryGrantClose: show confirmation for ', allClosingTabs);
|
||||
return Background.confirmToCloseTabs(allClosingTabs.slice(1).map(tab => tab.$TST.sanitized), {
|
||||
windowId: tab.windowId,
|
||||
messageKey: 'warnOnCloseTabs_fromOutside_message',
|
||||
titleKey: 'warnOnCloseTabs_fromOutside_title',
|
||||
minConfirmCount: 0
|
||||
});
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.then(async (granted) => {
|
||||
log(' => granted: ', granted);
|
||||
// remove the closed tab itself because it is already closed!
|
||||
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
|
||||
if (granted)
|
||||
return true;
|
||||
log(`tryGrantClose: not granted, restore ${shouldRestoreCount} tabs`);
|
||||
// this is required to wait until the closing tab is stored to the "recently closed" list
|
||||
wait(0).then(async () => {
|
||||
const restoredTabs = await Commands.restoreTabs(shouldRestoreCount);
|
||||
log('tryGrantClose: restored ', restoredTabs);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const granted = await self.promisedGrantedToCloseTabs;
|
||||
self.closingTabIds = [];
|
||||
self.closingDescendantTabIds = [];
|
||||
self.closingTabWasActive = false;
|
||||
self.promisedGrantedToCloseTabs = null;
|
||||
return granted;
|
||||
}
|
||||
tryGrantCloseTab.closingTabIds = [];
|
||||
tryGrantCloseTab.closingDescendantTabIds = [];
|
||||
tryGrantCloseTab.closingTabWasActive = false;
|
||||
tryGrantCloseTab.promisedGrantedToCloseTabs = null;
|
||||
|
||||
async function closeChildTabs(tabs, { triggerTab, originalStructure } = {}) {
|
||||
//if (!fireTabSubtreeClosingEvent(parent, tabs))
|
||||
// return;
|
||||
|
||||
//markAsClosedSet([parent].concat(tabs));
|
||||
// close bottom to top!
|
||||
await TabsInternalOperation.removeTabs(tabs.slice(0).reverse(), { triggerTab, originalStructure });
|
||||
//fireTabSubtreeClosedEvent(parent, tabs);
|
||||
}
|
||||
|
||||
Tab.onRemoved.addListener((tab, info) => {
|
||||
log('Tabs.onRemoved: removed ', dumpTab(tab));
|
||||
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
|
||||
|
||||
if (info.isWindowClosing)
|
||||
return;
|
||||
|
||||
// The removing tab may be attached to another tab or
|
||||
// other tabs may be attached to the removing tab.
|
||||
// We need to detach such relations always on this timing.
|
||||
if (info.oldChildren.length > 0) {
|
||||
Tree.detachAllChildren(tab, {
|
||||
children: info.oldChildren,
|
||||
parent: info.oldParent,
|
||||
behavior: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
if (info.oldParent) {
|
||||
Tree.detachTab(tab, {
|
||||
dontSyncParentToOpenerTab: true,
|
||||
parent: info.oldParent,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
browser.windows.onRemoved.addListener(windowId => {
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (!win)
|
||||
return;
|
||||
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => !win.tabs.has(id));
|
||||
});
|
||||
|
||||
|
||||
Tab.onDetached.addListener((tab, info = {}) => {
|
||||
log('Tabs.onDetached ', dumpTab(tab));
|
||||
let closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
|
||||
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
|
||||
...info
|
||||
});
|
||||
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
|
||||
closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
|
||||
|
||||
const dontSyncParentToOpenerTab = info.trigger == 'tabs.onDetached';
|
||||
Tree.detachAllChildren(tab, {
|
||||
dontSyncParentToOpenerTab,
|
||||
behavior: closeParentBehavior,
|
||||
broadcast: true
|
||||
});
|
||||
//reserveCloseRelatedTabs(toBeClosedTabs);
|
||||
Tree.detachTab(tab, {
|
||||
dontSyncParentToOpenerTab,
|
||||
dontUpdateIndent: true,
|
||||
broadcast: true
|
||||
});
|
||||
//restoreTabAttributes(tab, backupAttributes);
|
||||
});
|
||||
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
configs,
|
||||
sanitizeForHTMLText,
|
||||
compareAsNumber,
|
||||
isFirefoxViewTab,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Bookmark from '/common/bookmark.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as Dialog from '/common/dialog.js';
|
||||
import * as Permissions from '/common/permissions.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TSTAPI from '/common/tst-api.js';
|
||||
|
||||
import { Tab, TreeItem } from '/common/TreeItem.js';
|
||||
|
||||
import * as TabsGroup from './tabs-group.js';
|
||||
import * as TabsOpen from './tabs-open.js';
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/handle-tab-bunches', ...args);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Detection of a bunch of tabs opened at same time.
|
||||
// Firefox's WebExtensions API doesn't provide ability to know which tabs
|
||||
// are opened together by a single trigger. Thus TST tries to detect such
|
||||
// "tab bunches" based on their opened timing.
|
||||
// ====================================================================
|
||||
|
||||
Tab.onBeforeCreate.addListener(async (tab, info) => {
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
if (!win)
|
||||
return;
|
||||
|
||||
const openerId = tab.openerTabId;
|
||||
const openerTab = openerId && (await browser.tabs.get(openerId).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError)));
|
||||
if ((openerTab &&
|
||||
(openerTab.pinned || isFirefoxViewTab(openerTab)) &&
|
||||
openerTab.windowId == tab.windowId) ||
|
||||
(!openerTab &&
|
||||
!info.maybeOrphan)) {
|
||||
if (win.preventToDetectTabBunchesUntil > Date.now()) {
|
||||
win.preventToDetectTabBunchesUntil += configs.tabBunchesDetectionTimeout;
|
||||
}
|
||||
else {
|
||||
win.openedNewTabs.set(tab.id, {
|
||||
id: tab.id,
|
||||
windowId: tab.windowId,
|
||||
indexOnCreated: tab.$indexOnCreated,
|
||||
openerId: openerTab?.id,
|
||||
openerIsPinned: openerTab?.pinned,
|
||||
openerIsFirefoxView: isFirefoxViewTab(openerTab),
|
||||
maybeFromBookmark: tab.$TST.maybeFromBookmark,
|
||||
shouldNotGrouped: TSTAPI.isGroupingBlocked(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (win.delayedTabBunchesDetection)
|
||||
clearTimeout(win.delayedTabBunchesDetection);
|
||||
win.delayedTabBunchesDetection = setTimeout(
|
||||
tryDetectTabBunches,
|
||||
configs.tabBunchesDetectionTimeout,
|
||||
win
|
||||
);
|
||||
});
|
||||
|
||||
const mPossibleTabBunchesToBeGrouped = [];
|
||||
const mPossibleTabBunchesFromBookmarks = [];
|
||||
|
||||
async function tryDetectTabBunches(win) {
|
||||
if (Tab.needToWaitTracked(win.id))
|
||||
await Tab.waitUntilTrackedAll(win.id);
|
||||
if (Tab.needToWaitMoved(win.id))
|
||||
await Tab.waitUntilMovedAll(win.id);
|
||||
|
||||
let tabReferences = Array.from(win.openedNewTabs.values());
|
||||
log('tryDetectTabBunches for ', tabReferences);
|
||||
|
||||
win.openedNewTabs.clear();
|
||||
|
||||
tabReferences = tabReferences.filter(tabReference => {
|
||||
if (!tabReference.id)
|
||||
return false;
|
||||
const tab = Tab.get(tabReference.id);
|
||||
if (!tab)
|
||||
return false;
|
||||
const uniqueId = tab?.$TST?.uniqueId;
|
||||
return !uniqueId || (!uniqueId.duplicated && !uniqueId.restored);
|
||||
});
|
||||
if (tabReferences.length == 0) {
|
||||
log(' => there is no possible bunches of tabs.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabReferences.length > 1) {
|
||||
await Promise.all(tabReferences.map(tabReference => {
|
||||
const tab = Tab.get(tabReference.id);
|
||||
tab.$TST.temporaryMetadata.set('openedWithOthers', true);
|
||||
// We need to wait until all tabs are handlede completely.
|
||||
// Otherwise `tab.$TST.needToBeGroupedSiblings` may contain unrelated tabs
|
||||
// (tabs opened from any other parent tab) unexpectedly.
|
||||
return tab.$TST.opened;
|
||||
}));
|
||||
}
|
||||
|
||||
if (areTabsFromOtherDeviceWithInsertAfterCurrent(tabReferences) &&
|
||||
configs.fixupOrderOfTabsFromOtherDevice) {
|
||||
const ids = tabReferences.map(tabReference => tabReference.id);
|
||||
const index = tabReferences.map(tabReference => Tab.get(tabReference.id).index).sort(compareAsNumber)[0];
|
||||
log(' => gather tabs from other device at ', ids, index);
|
||||
await browser.tabs.move(ids, { index });
|
||||
}
|
||||
|
||||
if (configs.autoGroupNewTabsFromPinned ||
|
||||
configs.autoGroupNewTabsFromFirefoxView ||
|
||||
configs.autoGroupNewTabsFromOthers) {
|
||||
mPossibleTabBunchesToBeGrouped.push(tabReferences);
|
||||
tryGroupTabBunches();
|
||||
}
|
||||
if (configs.autoGroupNewTabsFromBookmarks ||
|
||||
configs.restoreTreeForTabsFromBookmarks) {
|
||||
mPossibleTabBunchesFromBookmarks.push(tabReferences);
|
||||
tryHandlTabBunchesFromBookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
async function tryGroupTabBunches() {
|
||||
if (tryGroupTabBunches.running)
|
||||
return;
|
||||
|
||||
const tabReferences = mPossibleTabBunchesToBeGrouped.shift();
|
||||
if (!tabReferences)
|
||||
return;
|
||||
|
||||
log('tryGroupTabBunches for ', tabReferences);
|
||||
tryGroupTabBunches.running = true;
|
||||
try {
|
||||
const fromPinned = [];
|
||||
const fromOthers = [];
|
||||
// extract only pure new tabs
|
||||
for (const tabReference of tabReferences) {
|
||||
if (tabReference.shouldNotGrouped)
|
||||
continue;
|
||||
const tab = Tab.get(tabReference.id);
|
||||
if (!tab)
|
||||
continue;
|
||||
if (tabReference.openerTabId)
|
||||
tab.openerTabId = parseInt(tabReference.openerTabId); // restore the opener information
|
||||
const uniqueId = tab.$TST.uniqueId;
|
||||
if (tab.$TST.isGroupTab || uniqueId.duplicated || uniqueId.restored)
|
||||
continue;
|
||||
if (tabReference.openerIsPinned ||
|
||||
tabReference.openerIsFirefoxView) {
|
||||
// We should check the "autoGroupNewTabsFromPinned" config here,
|
||||
// because to-be-grouped tabs should be ignored by the handler for
|
||||
// "autoAttachSameSiteOrphan" behavior.
|
||||
if ((tab.$TST.hasPinnedOpener &&
|
||||
configs.autoGroupNewTabsFromPinned) ||
|
||||
(tab.$TST.hasFirefoxViewOpener &&
|
||||
configs.autoGroupNewTabsFromFirefoxView))
|
||||
fromPinned.push(tab);
|
||||
}
|
||||
else {
|
||||
fromOthers.push(tab);
|
||||
}
|
||||
}
|
||||
log(' => ', { fromPinned, fromOthers });
|
||||
|
||||
if (fromPinned.length > 0 &&
|
||||
(configs.autoGroupNewTabsFromPinned ||
|
||||
configs.autoGroupNewTabsFromFirefoxView)) {
|
||||
const newRootTabs = Tab.collectRootTabs(TreeItem.sort(fromPinned));
|
||||
if (newRootTabs.length > 0) {
|
||||
await tryGroupTabBunchesFromPinnedOpener(newRootTabs);
|
||||
}
|
||||
}
|
||||
|
||||
// We can assume that new tabs from a bookmark folder and from other
|
||||
// sources won't be mixed.
|
||||
const openedFromBookmarkFolder = fromOthers.length > 0 && await detectBookmarkFolderFromTabs(fromOthers, tabReferences.length);
|
||||
log(' => tryGroupTabBunches:openedFromBookmarkFolder: ', !!openedFromBookmarkFolder);
|
||||
const newRootTabs = Tab.collectRootTabs(TreeItem.sort(openedFromBookmarkFolder ? openedFromBookmarkFolder.tabs : fromOthers));
|
||||
log(' newRootTabs: ', newRootTabs);
|
||||
if (newRootTabs.length > 1 &&
|
||||
!openedFromBookmarkFolder && // we should ignore tabs from bookmark folder: they should be handled by tryHandlTabBunchesFromBookmarks
|
||||
configs.autoGroupNewTabsFromOthers) {
|
||||
const granted = await confirmToAutoGroupNewTabsFromOthers(fromOthers);
|
||||
if (granted)
|
||||
await TabsGroup.groupTabs(newRootTabs, {
|
||||
...TabsGroup.temporaryStateParams(configs.groupTabTemporaryStateForNewTabsFromOthers),
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
log('Error on tryGroupTabBunches: ', String(error), error.stack);
|
||||
}
|
||||
finally {
|
||||
tryGroupTabBunches.running = false;
|
||||
if (mPossibleTabBunchesToBeGrouped.length > 0)
|
||||
tryGroupTabBunches();
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmToAutoGroupNewTabsFromOthers(tabs) {
|
||||
if (tabs.length <= 1 ||
|
||||
!configs.warnOnAutoGroupNewTabs)
|
||||
return true;
|
||||
|
||||
const windowId = tabs[0].windowId;
|
||||
const win = await browser.windows.get(windowId);
|
||||
|
||||
const listing = configs.warnOnAutoGroupNewTabsWithListing ?
|
||||
Dialog.tabsToHTMLList(tabs, {
|
||||
maxRows: configs.warnOnAutoGroupNewTabsWithListingMaxRows,
|
||||
maxHeight: Math.round(win.height * 0.8),
|
||||
maxWidth: Math.round(win.width * 0.75)
|
||||
}) :
|
||||
'';
|
||||
const result = await Dialog.show(win, {
|
||||
content: `
|
||||
<div>${sanitizeForHTMLText(browser.i18n.getMessage('warnOnAutoGroupNewTabs_message', [tabs.length]))}</div>${listing}
|
||||
`.trim(),
|
||||
buttons: [
|
||||
browser.i18n.getMessage('warnOnAutoGroupNewTabs_close'),
|
||||
browser.i18n.getMessage('warnOnAutoGroupNewTabs_cancel')
|
||||
],
|
||||
checkMessage: browser.i18n.getMessage('warnOnAutoGroupNewTabs_warnAgain'),
|
||||
checked: true,
|
||||
modal: true, // for popup
|
||||
type: 'common-dialog', // for popup
|
||||
url: ((await Permissions.isGranted(Permissions.ALL_URLS)) ? null : '/resources/blank.html'), // for popup
|
||||
title: browser.i18n.getMessage('warnOnAutoGroupNewTabs_title'), // for popup
|
||||
onShownInPopup(container) {
|
||||
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
|
||||
// this need to be done on the next tick, to use the height of the box for calculation of dialog size
|
||||
const style = container.querySelector('ul').style;
|
||||
style.height = '0px'; // this makes the box shrinkable
|
||||
style.maxHeight = 'none';
|
||||
style.minHeight = '0px';
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
switch (result.buttonIndex) {
|
||||
case 0:
|
||||
if (!result.checked)
|
||||
configs.warnOnAutoGroupNewTabs = false;
|
||||
return true;
|
||||
case 1:
|
||||
if (!result.checked) {
|
||||
configs.warnOnAutoGroupNewTabs = false;
|
||||
configs.autoGroupNewTabsFromOthers = false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryGroupTabBunchesFromPinnedOpener(rootTabs) {
|
||||
log(`tryGroupTabBunchesFromPinnedOpener: ${rootTabs.length} root tabs are opened from pinned tabs`);
|
||||
|
||||
// First, collect pinned opener tabs.
|
||||
let pinnedOpeners = [];
|
||||
const childrenOfPinnedTabs = {};
|
||||
for (const tab of rootTabs) {
|
||||
const opener = tab.$TST.openerTab;
|
||||
if (!pinnedOpeners.includes(opener))
|
||||
pinnedOpeners.push(opener);
|
||||
}
|
||||
log('pinnedOpeners ', () => pinnedOpeners.map(dumpTab));
|
||||
|
||||
// Second, collect tabs opened from pinned openers including existing tabs
|
||||
// (which were left ungrouped in previous process).
|
||||
const openerOf = {};
|
||||
const allRootTabs = await Tab.getRootTabs(rootTabs[0].windowId)
|
||||
for (const tab of allRootTabs) {
|
||||
if (tab.$TST.getAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER))
|
||||
continue;
|
||||
if (rootTabs.includes(tab)) { // newly opened tab
|
||||
const opener = tab.$TST.openerTab;
|
||||
if (!opener)
|
||||
continue;
|
||||
openerOf[tab.id] = opener;
|
||||
const tabs = childrenOfPinnedTabs[opener.id] || [];
|
||||
childrenOfPinnedTabs[opener.id] = tabs.concat([tab]);
|
||||
continue;
|
||||
}
|
||||
const opener = Tab.getByUniqueId(tab.$TST.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID));
|
||||
if (!opener ||
|
||||
!(opener.pinned || isFirefoxViewTab(opener)) ||
|
||||
opener.windowId != tab.windowId)
|
||||
continue;
|
||||
// existing and not yet grouped tab
|
||||
if (!pinnedOpeners.includes(opener))
|
||||
pinnedOpeners.push(opener);
|
||||
openerOf[tab.id] = opener;
|
||||
const tabs = childrenOfPinnedTabs[opener.id] || [];
|
||||
childrenOfPinnedTabs[opener.id] = tabs.concat([tab]);
|
||||
}
|
||||
|
||||
// Ignore pinned openeres which has no child tab to be grouped.
|
||||
pinnedOpeners = pinnedOpeners.filter(opener => {
|
||||
return childrenOfPinnedTabs[opener.id].length > 1 || Tab.getGroupTabForOpener(opener);
|
||||
});
|
||||
log(' => ', () => pinnedOpeners.map(dumpTab));
|
||||
|
||||
// Move newly opened tabs to expected position before grouping!
|
||||
// Note that we should refer "insertNewChildAt" instead of "insertNewTabFromPinnedTabAt" / "insertNewTabFromFirefoxViewAt"
|
||||
// because these children are going to be controlled in a sub tree.
|
||||
for (const tab of rootTabs.slice(0).sort((a, b) => a.id - b.id)/* process them in the order they were opened */) {
|
||||
const opener = openerOf[tab.id];
|
||||
const siblings = tab.$TST.needToBeGroupedSiblings;
|
||||
if (!pinnedOpeners.includes(opener) ||
|
||||
Tab.getGroupTabForOpener(opener) ||
|
||||
siblings.length == 0 ||
|
||||
tab.$TST.temporaryMetadata.has('alreadyMovedAsOpenedFromPinnedOpener'))
|
||||
continue;
|
||||
let refTabs = {};
|
||||
try {
|
||||
refTabs = Tree.getReferenceTabsForNewChild(tab, null, {
|
||||
lastRelatedTab: opener.$TST.previousLastRelatedTab,
|
||||
parent: siblings[0],
|
||||
children: siblings,
|
||||
descendants: siblings.map(sibling => [sibling, ...sibling.$TST.descendants]).flat()
|
||||
});
|
||||
}
|
||||
catch(_error) {
|
||||
// insertChildAt == "no control" case
|
||||
}
|
||||
if (refTabs.insertAfter) {
|
||||
await Tree.moveTabSubtreeAfter(
|
||||
tab,
|
||||
refTabs.insertAfter,
|
||||
{ broadcast: true }
|
||||
);
|
||||
log(`newly opened child ${tab.id} has been moved after ${refTabs.insertAfter?.id}`);
|
||||
}
|
||||
else if (refTabs.insertBefore) {
|
||||
await Tree.moveTabSubtreeBefore(
|
||||
tab,
|
||||
refTabs.insertBefore,
|
||||
{ broadcast: true }
|
||||
);
|
||||
log(`newly opened child ${tab.id} has been moved before ${refTabs.insertBefore?.id}`);
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
|
||||
}
|
||||
|
||||
// Finally, try to group opened tabs.
|
||||
const newGroupTabs = new Map();
|
||||
for (const opener of pinnedOpeners) {
|
||||
const children = childrenOfPinnedTabs[opener.id].sort((a, b) => a.index - b.index);
|
||||
let parent = Tab.getGroupTabForOpener(opener);
|
||||
if (parent) {
|
||||
for (const child of children) {
|
||||
TabsStore.removeToBeGroupedTab(child);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
log(`trying to group children of ${dumpTab(opener)}: `, () => children.map(dumpTab));
|
||||
const uri = TabsGroup.makeGroupTabURI({
|
||||
title: browser.i18n.getMessage('groupTab_fromPinnedTab_label', opener.title),
|
||||
openerTabId: opener.$TST.uniqueId.id,
|
||||
...TabsGroup.temporaryStateParams(isFirefoxViewTab(opener) ? configs.groupTabTemporaryStateForChildrenOfFirefoxView : configs.groupTabTemporaryStateForChildrenOfPinned)
|
||||
});
|
||||
parent = await TabsOpen.openURIInTab(uri, {
|
||||
windowId: opener.windowId,
|
||||
insertBefore: children[0],
|
||||
cookieStoreId: opener.cookieStoreId,
|
||||
inBackground: true
|
||||
});
|
||||
log('opened group tab: ', dumpTab(parent));
|
||||
newGroupTabs.set(opener, true);
|
||||
for (const child of children) {
|
||||
// Prevent the tab to be grouped again after it is ungrouped manually.
|
||||
child.$TST.setAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, true);
|
||||
TabsStore.removeToBeGroupedTab(child);
|
||||
await Tree.attachTabTo(child, parent, {
|
||||
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
|
||||
dontMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
if (opener.active)
|
||||
parent.$TST.addState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function tryHandlTabBunchesFromBookmarks() {
|
||||
if (tryHandlTabBunchesFromBookmarks.running)
|
||||
return;
|
||||
|
||||
const tabReferences = mPossibleTabBunchesFromBookmarks.shift();
|
||||
if (!tabReferences)
|
||||
return;
|
||||
|
||||
log('tryHandlTabBunchesFromBookmarks for ', tabReferences);
|
||||
tryHandlTabBunchesFromBookmarks.running = true;
|
||||
try {
|
||||
const tabs = [];
|
||||
for (const tabReference of tabReferences) {
|
||||
const tab = Tab.get(tabReference.id);
|
||||
if (!tab)
|
||||
continue;
|
||||
if (tabReference.openerTabId)
|
||||
tab.openerTabId = parseInt(tabReference.openerTabId); // restore the opener information
|
||||
const uniqueId = tab.$TST.uniqueId;
|
||||
if (uniqueId.duplicated || uniqueId.restored)
|
||||
continue;
|
||||
tabs.push(tab);
|
||||
}
|
||||
log(' => ', { tabs });
|
||||
|
||||
// We can assume that new tabs from a bookmark folder and from other
|
||||
// sources won't be mixed.
|
||||
const openedFromBookmarkFolder = tabs.length > 0 && await detectBookmarkFolderFromTabs(tabs, tabReferences.length);
|
||||
log(' => tryHandlTabBunchesFromBookmarks:openedFromBookmarkFolder: ', openedFromBookmarkFolder);
|
||||
if (openedFromBookmarkFolder) {
|
||||
if (configs.restoreTreeForTabsFromBookmarks) {
|
||||
log(' ==> trying to restore tree structure from bookmark information');
|
||||
const structure = await Bookmark.getTreeStructureFromBookmarkFolder(openedFromBookmarkFolder.folder);
|
||||
log(' ==> structure:', structure);
|
||||
if (structure.length == openedFromBookmarkFolder.tabs.length) {
|
||||
log(' ===> apply');
|
||||
await Tree.applyTreeStructureToTabs(openedFromBookmarkFolder.tabs, structure);
|
||||
}
|
||||
}
|
||||
|
||||
const newRootTabs = Tab.collectRootTabs(TreeItem.sort(openedFromBookmarkFolder.tabs));
|
||||
if (newRootTabs.length > 1 &&
|
||||
configs.autoGroupNewTabsFromBookmarks &&
|
||||
tabReferences.every(tabReference => !tabReference.shouldNotGrouped)) {
|
||||
log(' => tryHandlTabBunchesFromBookmarks:group');
|
||||
await TabsGroup.groupTabs(openedFromBookmarkFolder.tabs, {
|
||||
...TabsGroup.temporaryStateParams(configs.groupTabTemporaryStateForNewTabsFromBookmarks),
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
log('Error on tryHandlTabBunchesFromBookmarks: ', String(error), error.stack);
|
||||
}
|
||||
finally {
|
||||
tryHandlTabBunchesFromBookmarks.running = false;
|
||||
if (mPossibleTabBunchesFromBookmarks.length > 0)
|
||||
tryHandlTabBunchesFromBookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
async function detectBookmarkFolderFromTabs(tabs, allNewTabsCount = tabs.length) {
|
||||
log('detectBookmarkFolderFromTabs: ', tabs, allNewTabsCount);
|
||||
return new Promise((resolve, _reject) => {
|
||||
const maybeFromBookmarks = [];
|
||||
let restCount = tabs.length;
|
||||
for (const tab of tabs) {
|
||||
tab.$TST.promisedPossibleOpenerBookmarks.then(async bookmarks => {
|
||||
log(` bookmarks from tab ${tab.id}: `, bookmarks);
|
||||
restCount--;
|
||||
if (bookmarks.length > 0)
|
||||
maybeFromBookmarks.push(tab);
|
||||
const folder = await tryDetectMostTabsContainedBookmarkFolder(maybeFromBookmarks, allNewTabsCount);
|
||||
if (folder) {
|
||||
log('detectBookmarkFolderFromTabs: found folder => ', { folder, tabs });
|
||||
resolve({
|
||||
folder,
|
||||
tabs: maybeFromBookmarks,
|
||||
});
|
||||
}
|
||||
else if (restCount == 0) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function tryDetectMostTabsContainedBookmarkFolder(bookmarkedTabs, allNewTabsCount = bookmarkedTabs.length) {
|
||||
log('tryDetectMostTabsContainedBookmarkFolder ', { bookmarkedTabs, allNewTabsCount });
|
||||
const parentIds = bookmarkedTabs.map(tab => tab.$TST.possibleOpenerBookmarks.map(bookmark => bookmark.parentId)).flat();
|
||||
const counts = [];
|
||||
const countById = {};
|
||||
for (const id of parentIds) {
|
||||
if (!(id in countById))
|
||||
counts.push(countById[id] = { id, count: 0 });
|
||||
countById[id].count++;
|
||||
}
|
||||
log(' counts: ', counts);
|
||||
if (counts.length == 0)
|
||||
return null;
|
||||
|
||||
const greatestCountParent = counts.sort((a, b) => b.count - a.count)[0];
|
||||
const minCount = allNewTabsCount * configs.tabsFromSameFolderMinThresholdPercentage / 100;
|
||||
log(' => ', { greatestCountParent, minCount });
|
||||
if (greatestCountParent.count <= minCount)
|
||||
return null;
|
||||
|
||||
const items = await browser.bookmarks.get(greatestCountParent.id);
|
||||
return Array.isArray(items) ? items[0] : items;
|
||||
}
|
||||
|
||||
|
||||
// Detect tabs sent from other device with `browser.tabs.insertAfterCurrent`=true based on their index
|
||||
// (Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1596787 )
|
||||
// See also: https://github.com/piroor/treestyletab/issues/2419
|
||||
function areTabsFromOtherDeviceWithInsertAfterCurrent(tabReferences) {
|
||||
if (tabReferences.length == 0)
|
||||
return false;
|
||||
|
||||
const activeTab = Tab.getActiveTab(tabReferences[0].windowId);
|
||||
if (!activeTab)
|
||||
return false;
|
||||
|
||||
const activeIndex = activeTab.index;
|
||||
const followingTabsCount = Tab.getTabs(activeTab.windowId).filter(tab => tab.index > activeIndex).length;
|
||||
|
||||
const createdCount = tabReferences.length;
|
||||
const expectedIndices = [activeIndex + 1];
|
||||
const actualIndices = tabReferences.map(tabReference => tabReference.indexOnCreated);
|
||||
|
||||
const overTabsCount = Math.max(0, createdCount - followingTabsCount);
|
||||
const shouldCountDown = Math.min(createdCount - 1, createdCount - Math.floor(overTabsCount / 2));
|
||||
const shouldCountUp = createdCount - shouldCountDown - 1;
|
||||
for (let i = 0; i < shouldCountUp; i++) {
|
||||
expectedIndices.push(activeIndex + 2 + i + (createdCount - overTabsCount));
|
||||
}
|
||||
for (let i = shouldCountDown - 1; i > -1; i--) {
|
||||
expectedIndices.push(activeIndex + 1 + i);
|
||||
}
|
||||
const received = actualIndices.join(',') == expectedIndices.join(',');
|
||||
log('areTabsFromOtherDeviceWithInsertAfterCurrent:', received, { overTabsCount, shouldCountUp, shouldCountDown, size: expectedIndices.length, actualIndices, expectedIndices });
|
||||
return received;
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
wait,
|
||||
configs,
|
||||
isMacOS,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as Permissions from '/common/permissions.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
||||
import * as TSTAPI from '/common/tst-api.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
import Window from '/common/Window.js';
|
||||
|
||||
import * as Background from './background.js';
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/handle-tab-focus', ...args);
|
||||
}
|
||||
|
||||
|
||||
const PHASE_LOADING = 0;
|
||||
const PHASE_BACKGROUND_INITIALIZED = 1;
|
||||
const PHASE_BACKGROUND_BUILT = 2;
|
||||
const PHASE_BACKGROUND_READY = 3;
|
||||
let mInitializationPhase = PHASE_LOADING;
|
||||
|
||||
Background.onInit.addListener(() => {
|
||||
mInitializationPhase = PHASE_BACKGROUND_INITIALIZED;
|
||||
});
|
||||
Background.onBuilt.addListener(() => {
|
||||
mInitializationPhase = PHASE_BACKGROUND_BUILT;
|
||||
});
|
||||
Background.onReady.addListener(() => {
|
||||
mInitializationPhase = PHASE_BACKGROUND_READY;
|
||||
});
|
||||
|
||||
|
||||
let mTabSwitchedByShortcut = false;
|
||||
let mMaybeTabSwitchingByShortcut = false;
|
||||
|
||||
const mLastTabsCountInWindow = new Map();
|
||||
|
||||
Window.onInitialized.addListener(win => {
|
||||
browser.tabs.query({
|
||||
windowId: win.id,
|
||||
active: true
|
||||
})
|
||||
.then(activeTabs => {
|
||||
// There may be no active tab on a startup...
|
||||
if (activeTabs.length > 0 &&
|
||||
!win.lastActiveTab)
|
||||
win.lastActiveTab = activeTabs[0].id;
|
||||
});
|
||||
});
|
||||
|
||||
browser.windows.onRemoved.addListener(windowId => {
|
||||
mLastTabsCountInWindow.delete(windowId);
|
||||
});
|
||||
|
||||
|
||||
Tab.onActivating.addListener(async (tab, info = {}) => { // return false if the activation should be canceled
|
||||
log('Tabs.onActivating ', { tab: dumpTab(tab), info, mMaybeTabSwitchingByShortcut });
|
||||
|
||||
if (mMaybeTabSwitchingByShortcut) {
|
||||
const lastCount = mLastTabsCountInWindow.get(tab.windowId);
|
||||
const count = Tab.getAllTabs(tab.windowId).length;
|
||||
if (lastCount != count) {
|
||||
log('tabs are created or removed: cancel tab switching');
|
||||
mMaybeTabSwitchingByShortcut = false;
|
||||
}
|
||||
mLastTabsCountInWindow.set(tab.windowId, count);
|
||||
}
|
||||
|
||||
if (tab.$TST.temporaryMetadata.has('shouldReloadOnSelect')) {
|
||||
browser.tabs.reload(tab.id)
|
||||
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
tab.$TST.temporaryMetadata.delete('shouldReloadOnSelect');
|
||||
}
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
log(' lastActiveTab: ', win.lastActiveTab); // it may be blank on a startup
|
||||
const lastActiveTab = Tab.get(win.lastActiveTab || info.previousTabId);
|
||||
cancelDelayedExpand(lastActiveTab);
|
||||
const shouldSkipCollapsed = (
|
||||
!info.byInternalOperation &&
|
||||
mMaybeTabSwitchingByShortcut &&
|
||||
configs.skipCollapsedTabsForTabSwitchingShortcuts
|
||||
);
|
||||
mTabSwitchedByShortcut = mMaybeTabSwitchingByShortcut;
|
||||
const focusDirection = !lastActiveTab ?
|
||||
0 :
|
||||
(!lastActiveTab.$TST.nearestVisiblePrecedingTab &&
|
||||
!tab.$TST.nearestVisibleFollowingTab) ?
|
||||
-1 :
|
||||
(!lastActiveTab.$TST.nearestVisibleFollowingTab &&
|
||||
!tab.$TST.nearestVisiblePrecedingTab) ?
|
||||
1 :
|
||||
(lastActiveTab.index > tab.index) ?
|
||||
-1 :
|
||||
1;
|
||||
const cache = {};
|
||||
if (tab.$TST.collapsed) {
|
||||
if (!tab.$TST.parent) {
|
||||
// This is invalid case, generally never should happen,
|
||||
// but actually happen on some environment:
|
||||
// https://github.com/piroor/treestyletab/issues/1717
|
||||
// So, always expand orphan collapsed tab as a failsafe.
|
||||
Tree.collapseExpandTab(tab, {
|
||||
collapsed: false,
|
||||
broadcast: true
|
||||
});
|
||||
await handleNewActiveTab(tab, info);
|
||||
}
|
||||
else if (!shouldSkipCollapsed) {
|
||||
log('=> reaction for focus given from outside of TST');
|
||||
let allowed = false;
|
||||
if (configs.unfocusableCollapsedTab) {
|
||||
log(' => apply unfocusableCollapsedTab');
|
||||
allowed = await TSTAPI.tryOperationAllowed(
|
||||
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_COLLAPSED_TAB,
|
||||
{ tab,
|
||||
focusDirection },
|
||||
{ tabProperties: ['tab'], cache }
|
||||
);
|
||||
TSTAPI.clearCache(cache);
|
||||
if (allowed) {
|
||||
const toBeExpandedAncestors = [tab].concat(tab.$TST.ancestors) ;
|
||||
for (const ancestor of toBeExpandedAncestors) {
|
||||
Tree.collapseExpandSubtree(ancestor, {
|
||||
collapsed: false,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
log(' => canceled by someone.');
|
||||
}
|
||||
}
|
||||
info.allowed = allowed;
|
||||
await handleNewActiveTab(tab, info);
|
||||
}
|
||||
if (shouldSkipCollapsed) {
|
||||
log('=> reaction for focusing collapsed descendant while Ctrl-Tab/Ctrl-Shift-Tab');
|
||||
let successor = tab.$TST.nearestVisibleAncestorOrSelf;
|
||||
if (!successor) // this seems invalid case...
|
||||
return false;
|
||||
log('successor = ', successor.id);
|
||||
if (shouldSkipCollapsed &&
|
||||
(win.lastActiveTab == successor.id ||
|
||||
successor.$TST.descendants.some(tab => tab.id == win.lastActiveTab)) &&
|
||||
focusDirection > 0) {
|
||||
log('=> redirect successor (focus moved from the successor itself or its descendants)');
|
||||
successor = successor.$TST.nearestVisibleFollowingTab;
|
||||
if (successor &&
|
||||
successor.discarded &&
|
||||
configs.avoidDiscardedTabToBeActivatedIfPossible)
|
||||
successor = successor.$TST.nearestLoadedTabInTree ||
|
||||
successor.$TST.nearestLoadedTab ||
|
||||
successor;
|
||||
if (!successor)
|
||||
successor = Tab.getFirstVisibleTab(tab.windowId);
|
||||
log('=> ', successor.id);
|
||||
}
|
||||
else if (!mTabSwitchedByShortcut && // intentional focus to a discarded tabs by Ctrl-Tab/Ctrl-Shift-Tab is always allowed!
|
||||
successor.discarded &&
|
||||
configs.avoidDiscardedTabToBeActivatedIfPossible) {
|
||||
log('=> redirect successor (successor is discarded)');
|
||||
successor = successor.$TST.nearestLoadedTabInTree ||
|
||||
successor.$TST.nearestLoadedTab ||
|
||||
successor;
|
||||
log('=> ', successor.id);
|
||||
}
|
||||
const allowed = await TSTAPI.tryOperationAllowed(
|
||||
TSTAPI.kNOTIFY_TRY_REDIRECT_FOCUS_FROM_COLLAPSED_TAB,
|
||||
{ tab,
|
||||
focusDirection },
|
||||
{ tabProperties: ['tab'], cache }
|
||||
);
|
||||
TSTAPI.clearCache(cache);
|
||||
if (allowed) {
|
||||
win.lastActiveTab = successor.id;
|
||||
if (mMaybeTabSwitchingByShortcut)
|
||||
setupDelayedExpand(successor);
|
||||
TabsInternalOperation.activateTab(successor, { silently: true });
|
||||
log('Tabs.onActivating: discarded? ', dumpTab(tab), tab?.discarded);
|
||||
if (tab.discarded)
|
||||
tab.$TST.temporaryMetadata.set('discardURLAfterCompletelyLoaded', tab.url);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
log(' => canceled by someone.');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (info.byActiveTabRemove &&
|
||||
(!configs.autoCollapseExpandSubtreeOnSelect ||
|
||||
configs.autoCollapseExpandSubtreeOnSelectExceptActiveTabRemove)) {
|
||||
log('=> reaction for removing current tab');
|
||||
win.lastActiveTab = tab.id;
|
||||
tryHighlightBundledTab(tab, {
|
||||
...info,
|
||||
shouldSkipCollapsed
|
||||
});
|
||||
return true;
|
||||
}
|
||||
else if (tab.$TST.hasChild &&
|
||||
tab.$TST.subtreeCollapsed &&
|
||||
!shouldSkipCollapsed) {
|
||||
log('=> reaction for newly active parent tab');
|
||||
await handleNewActiveTab(tab, info);
|
||||
}
|
||||
tab.$TST.temporaryMetadata.delete('discardOnCompletelyLoaded');
|
||||
win.lastActiveTab = tab.id;
|
||||
|
||||
if (mMaybeTabSwitchingByShortcut)
|
||||
setupDelayedExpand(tab);
|
||||
else
|
||||
tryHighlightBundledTab(tab, {
|
||||
...info,
|
||||
shouldSkipCollapsed
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
async function handleNewActiveTab(tab, { allowed, silently } = {}) {
|
||||
log('handleNewActiveTab: ', dumpTab(tab), { allowed, silently });
|
||||
const shouldCollapseExpandNow = configs.autoCollapseExpandSubtreeOnSelect;
|
||||
const canCollapseTree = shouldCollapseExpandNow;
|
||||
const canExpandTree = shouldCollapseExpandNow && !silently;
|
||||
if (canExpandTree &&
|
||||
allowed !== false) {
|
||||
const cache = {};
|
||||
const allowed = await TSTAPI.tryOperationAllowed(
|
||||
tab.active ?
|
||||
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_PARENT :
|
||||
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_BUNDLED_PARENT,
|
||||
{ tab },
|
||||
{ tabProperties: ['tab'], cache }
|
||||
);
|
||||
if (!allowed)
|
||||
return;
|
||||
if (canCollapseTree &&
|
||||
configs.autoExpandIntelligently)
|
||||
await Tree.collapseExpandTreesIntelligentlyFor(tab, {
|
||||
broadcast: true
|
||||
});
|
||||
else
|
||||
Tree.collapseExpandSubtree(tab, {
|
||||
collapsed: false,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function tryHighlightBundledTab(tab, { shouldSkipCollapsed, allowed, silently } = {}) {
|
||||
const bundledTab = tab.$TST.bundledTab;
|
||||
const oldBundledTabs = TabsStore.bundledActiveTabsInWindow.get(tab.windowId);
|
||||
log('tryHighlightBundledTab ', {
|
||||
tab: tab.id,
|
||||
bundledTab: bundledTab?.id,
|
||||
oldBundledTabs,
|
||||
shouldSkipCollapsed,
|
||||
allowed,
|
||||
silently,
|
||||
});
|
||||
for (const tab of oldBundledTabs.values()) {
|
||||
if (tab == bundledTab)
|
||||
continue;
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
|
||||
}
|
||||
|
||||
if (!bundledTab)
|
||||
return;
|
||||
|
||||
bundledTab.$TST.addState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
|
||||
|
||||
await wait(100);
|
||||
if (!tab.active || // ignore tab already inactivated while waiting
|
||||
tab.$TST.hasOtherHighlighted || // ignore manual highlighting
|
||||
bundledTab.pinned ||
|
||||
!configs.syncActiveStateToBundledTabs)
|
||||
return;
|
||||
|
||||
if (bundledTab.$TST.hasChild &&
|
||||
bundledTab.$TST.subtreeCollapsed &&
|
||||
!shouldSkipCollapsed)
|
||||
await handleNewActiveTab(bundledTab, { allowed, silently });
|
||||
}
|
||||
|
||||
Tab.onUpdated.addListener((tab, changeInfo = {}) => {
|
||||
if ('url' in changeInfo) {
|
||||
if (tab.$TST.temporaryMetadata.has('discardURLAfterCompletelyLoaded') &&
|
||||
tab.$TST.temporaryMetadata.get('discardURLAfterCompletelyLoaded') != changeInfo.url)
|
||||
tab.$TST.temporaryMetadata.delete('discardURLAfterCompletelyLoaded');
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onStateChanged.addListener(tab => {
|
||||
if (!tab ||
|
||||
tab.status != 'complete')
|
||||
return;
|
||||
|
||||
if (typeof browser.tabs.discard == 'function') {
|
||||
if (tab.url == tab.$TST.temporaryMetadata.get('discardURLAfterCompletelyLoaded') &&
|
||||
configs.autoDiscardTabForUnexpectedFocus) {
|
||||
log('Try to discard accidentally restored tab (on restored) ', dumpTab(tab));
|
||||
wait(configs.autoDiscardTabForUnexpectedFocusDelay).then(() => {
|
||||
if (!TabsStore.ensureLivingItem(tab) ||
|
||||
tab.active)
|
||||
return;
|
||||
if (tab.status == 'complete')
|
||||
browser.tabs.discard(tab.id)
|
||||
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
else
|
||||
tab.$TST.temporaryMetadata.set('discardOnCompletelyLoaded', true);
|
||||
});
|
||||
}
|
||||
else if (tab.$TST.temporaryMetadata.has('discardOnCompletelyLoaded') && !tab.active) {
|
||||
log('Discard accidentally restored tab (on complete) ', dumpTab(tab));
|
||||
browser.tabs.discard(tab.id)
|
||||
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
}
|
||||
}
|
||||
tab.$TST.temporaryMetadata.delete('discardURLAfterCompletelyLoaded');
|
||||
tab.$TST.temporaryMetadata.delete('discardOnCompletelyLoaded');
|
||||
});
|
||||
|
||||
async function setupDelayedExpand(tab) {
|
||||
if (!tab)
|
||||
return;
|
||||
cancelDelayedExpand(tab);
|
||||
TabsStore.removeToBeExpandedTab(tab);
|
||||
const cache = {};
|
||||
const [ctrlTabHandlingEnabled, allowedToExpandViaAPI] = await Promise.all([
|
||||
Permissions.isGranted(Permissions.ALL_URLS),
|
||||
TSTAPI.tryOperationAllowed(
|
||||
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_LONG_PRESS_CTRL_KEY,
|
||||
{ tab },
|
||||
{ tabProperties: ['tab'], cache }
|
||||
),
|
||||
]);
|
||||
if (!configs.autoExpandOnTabSwitchingShortcuts ||
|
||||
!tab.$TST.hasChild ||
|
||||
!tab.$TST.subtreeCollapsed ||
|
||||
!ctrlTabHandlingEnabled ||
|
||||
!allowedToExpandViaAPI)
|
||||
return;
|
||||
TabsStore.addToBeExpandedTab(tab);
|
||||
tab.$TST.temporaryMetadata.set('delayedExpand', setTimeout(async () => {
|
||||
if (!tab.$TST.temporaryMetadata.has('delayedExpand')) { // already canceled
|
||||
log('delayed expand is already canceled ', tab.id);
|
||||
return;
|
||||
}
|
||||
log('delayed expand by long-press of ctrl key on ', tab.id);
|
||||
TabsStore.removeToBeExpandedTab(tab);
|
||||
await Tree.collapseExpandTreesIntelligentlyFor(tab, {
|
||||
broadcast: true
|
||||
});
|
||||
}, configs.autoExpandOnTabSwitchingShortcutsDelay));
|
||||
}
|
||||
|
||||
function cancelDelayedExpand(tab) {
|
||||
if (!tab ||
|
||||
!tab.$TST.temporaryMetadata.has('delayedExpand'))
|
||||
return;
|
||||
clearTimeout(tab.$TST.temporaryMetadata.get('delayedExpand'));
|
||||
tab.$TST.temporaryMetadata.delete('delayedExpand');
|
||||
TabsStore.removeToBeExpandedTab(tab);
|
||||
}
|
||||
|
||||
function cancelAllDelayedExpand(windowId) {
|
||||
for (const tab of TabsStore.toBeExpandedTabsInWindow.get(windowId)) {
|
||||
cancelDelayedExpand(tab);
|
||||
}
|
||||
}
|
||||
|
||||
Tab.onCollapsedStateChanged.addListener((tab, info = {}) => {
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_COLLAPSED_DONE, info.collapsed, { broadcast: false });
|
||||
});
|
||||
|
||||
|
||||
Background.onReady.addListener(() => {
|
||||
for (const tab of Tab.getAllTabs(null, { iterator: true })) {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_BUNDLED_ACTIVE);
|
||||
}
|
||||
for (const tab of Tab.getActiveTabs({ iterator: true })) {
|
||||
tryHighlightBundledTab(tab);
|
||||
}
|
||||
});
|
||||
|
||||
browser.windows.onFocusChanged.addListener(() => {
|
||||
mMaybeTabSwitchingByShortcut = false;
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
|
||||
function onMessage(message, sender) {
|
||||
if (mInitializationPhase < PHASE_BACKGROUND_BUILT ||
|
||||
!message ||
|
||||
typeof message.type != 'string')
|
||||
return;
|
||||
|
||||
//log('onMessage: ', message, sender);
|
||||
switch (message.type) {
|
||||
case Constants.kNOTIFY_TAB_MOUSEDOWN:
|
||||
mMaybeTabSwitchingByShortcut =
|
||||
mTabSwitchedByShortcut = false;
|
||||
break;
|
||||
|
||||
case Constants.kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH: {
|
||||
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
|
||||
return;
|
||||
log('kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH ', message.modifier);
|
||||
mMaybeTabSwitchingByShortcut = true;
|
||||
if (sender.tab?.active) {
|
||||
const win = TabsStore.windows.get(sender.tab.windowId);
|
||||
win.lastActiveTab = sender.tab.id;
|
||||
}
|
||||
if (sender.tab)
|
||||
mLastTabsCountInWindow.set(sender.tab.windowId, Tab.getAllTabs(sender.tab.windowId).length);
|
||||
}; break;
|
||||
case Constants.kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH:
|
||||
if (message.modifier != (configs.accelKey || (isMacOS() ? 'meta' : 'control')))
|
||||
return;
|
||||
log('kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH ', message.modifier);
|
||||
return (async () => {
|
||||
if (mTabSwitchedByShortcut &&
|
||||
configs.skipCollapsedTabsForTabSwitchingShortcuts &&
|
||||
sender.tab) {
|
||||
await Tab.waitUntilTracked(sender.tab.id);
|
||||
let tab = Tab.get(sender.tab.id);
|
||||
if (!tab) {
|
||||
let tabs = await browser.tabs.query({ currentWindow: true, active: true }).catch(ApiTabs.createErrorHandler());
|
||||
if (tabs.length == 0)
|
||||
tabs = await browser.tabs.query({ currentWindow: true }).catch(ApiTabs.createErrorHandler());
|
||||
await Tab.waitUntilTracked(tabs[0].id);
|
||||
tab = Tab.get(tabs[0].id);
|
||||
}
|
||||
cancelAllDelayedExpand(tab.windowId);
|
||||
const cache = {};
|
||||
if (configs.autoCollapseExpandSubtreeOnSelect &&
|
||||
tab &&
|
||||
TabsStore.windows.get(tab.windowId).lastActiveTab == tab.id &&
|
||||
(await TSTAPI.tryOperationAllowed(
|
||||
TSTAPI.kNOTIFY_TRY_EXPAND_TREE_FROM_END_TAB_SWITCH,
|
||||
{ tab },
|
||||
{ tabProperties: ['tab'], cache }
|
||||
))) {
|
||||
Tree.collapseExpandSubtree(tab, {
|
||||
collapsed: false,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
mMaybeTabSwitchingByShortcut =
|
||||
mTabSwitchedByShortcut = false;
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/handle-tab-multiselect', ...args);
|
||||
}
|
||||
|
||||
Tab.onUpdated.addListener((tab, info, options = {}) => {
|
||||
if (!('highlighted' in info) ||
|
||||
!tab.$TST.subtreeCollapsed ||
|
||||
tab.$TST.collapsed ||
|
||||
!tab.$TST.multiselected ||
|
||||
!options.inheritHighlighted)
|
||||
return;
|
||||
|
||||
const collapsedDescendants = tab.$TST.descendants;
|
||||
log('inherit highlighted state from root visible tab: ', {
|
||||
highlighted: info.highlighted,
|
||||
collapsedDescendants
|
||||
});
|
||||
for (const descendant of collapsedDescendants) {
|
||||
browser.tabs.update(descendant.id, {
|
||||
highlighted: info.highlighted,
|
||||
active: descendant.active
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
|
||||
import { Tab, TreeItem } from '/common/TreeItem.js';
|
||||
|
||||
import * as Background from './background.js';
|
||||
import * as BackgroundCache from './background-cache.js';
|
||||
import * as Tree from './tree.js';
|
||||
import * as TreeStructure from './tree-structure.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/handle-tree-changes', ...args);
|
||||
}
|
||||
|
||||
let mInitialized = false;
|
||||
|
||||
function reserveDetachHiddenTab(tab) {
|
||||
reserveDetachHiddenTab.tabs.add(tab);
|
||||
if (reserveDetachHiddenTab.reserved)
|
||||
clearTimeout(reserveDetachHiddenTab.reserved);
|
||||
reserveDetachHiddenTab.reserved = setTimeout(async () => {
|
||||
delete reserveDetachHiddenTab.reserved;
|
||||
const tabs = new Set(TreeItem.sort(Array.from(reserveDetachHiddenTab.tabs)));
|
||||
reserveDetachHiddenTab.tabs.clear();
|
||||
log('try to detach hidden tabs: ', tabs);
|
||||
for (const tab of tabs) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
continue;
|
||||
for (const descendant of tab.$TST.descendants) {
|
||||
if (descendant.hidden)
|
||||
continue;
|
||||
const nearestVisibleAncestor = descendant.$TST.ancestors.find(ancestor => !ancestor.hidden && !tabs.has(ancestor));
|
||||
if (nearestVisibleAncestor &&
|
||||
nearestVisibleAncestor == descendant.$TST.parent)
|
||||
continue;
|
||||
for (const ancestor of descendant.$TST.ancestors) {
|
||||
if (!ancestor.hidden &&
|
||||
!ancestor.$TST.collapsed)
|
||||
break;
|
||||
if (!ancestor.$TST.subtreeCollapsed)
|
||||
continue;
|
||||
await Tree.collapseExpandSubtree(ancestor, {
|
||||
collapsed: false,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
if (nearestVisibleAncestor) {
|
||||
log(` => reattach descendant ${descendant.id} to ${nearestVisibleAncestor.id}`);
|
||||
await Tree.attachTabTo(descendant, nearestVisibleAncestor, {
|
||||
dontMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
log(` => detach descendant ${descendant.id}`);
|
||||
await Tree.detachTab(descendant, {
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
if (tab.$TST.hasParent &&
|
||||
!tab.$TST.parent.hidden) {
|
||||
log(` => detach hidden tab ${tab.id}`);
|
||||
await Tree.detachTab(tab, {
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
reserveDetachHiddenTab.tabs = new Set();
|
||||
|
||||
Tab.onHidden.addListener(tab => {
|
||||
if (configs.fixupTreeOnTabVisibilityChanged)
|
||||
reserveDetachHiddenTab(tab);
|
||||
});
|
||||
|
||||
function reserveAttachShownTab(tab) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_SHOWING);
|
||||
reserveAttachShownTab.tabs.add(tab);
|
||||
if (reserveAttachShownTab.reserved)
|
||||
clearTimeout(reserveAttachShownTab.reserved);
|
||||
reserveAttachShownTab.reserved = setTimeout(async () => {
|
||||
delete reserveAttachShownTab.reserved;
|
||||
const tabs = new Set(TreeItem.sort(Array.from(reserveAttachShownTab.tabs)));
|
||||
reserveAttachShownTab.tabs.clear();
|
||||
log('try to attach shown tabs: ', tabs);
|
||||
for (const tab of tabs) {
|
||||
if (!TabsStore.ensureLivingItem(tab) ||
|
||||
tab.$TST.hasParent) {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_SHOWING);
|
||||
continue;
|
||||
}
|
||||
const referenceTabs = TreeBehavior.calculateReferenceItemsFromInsertionPosition(tab, {
|
||||
context: Constants.kINSERTION_CONTEXT_SHOWN,
|
||||
insertAfter: tab.$TST.nearestVisiblePrecedingTab,
|
||||
// Instead of nearestFollowingForeignerTab, to avoid placing the tab
|
||||
// after hidden tabs (too far from the target)
|
||||
insertBefore: tab.$TST.unsafeNearestFollowingForeignerTab
|
||||
});
|
||||
if (referenceTabs.parent) {
|
||||
log(` => attach shown tab ${tab.id} to ${referenceTabs.parent.id}`);
|
||||
await Tree.attachTabTo(tab, referenceTabs.parent, {
|
||||
insertBefore: referenceTabs.insertBefore,
|
||||
insertAfter: referenceTabs.insertAfter,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_SHOWING);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
reserveAttachShownTab.tabs = new Set();
|
||||
|
||||
Tab.onShown.addListener(tab => {
|
||||
if (configs.fixupTreeOnTabVisibilityChanged)
|
||||
reserveAttachShownTab(tab);
|
||||
});
|
||||
|
||||
Background.onReady.addListener(() => {
|
||||
mInitialized = true;
|
||||
});
|
||||
|
||||
Tree.onSubtreeCollapsedStateChanged.addListener((tab, _info) => {
|
||||
if (mInitialized)
|
||||
TreeStructure.reserveToSaveTreeStructure(tab.windowId);
|
||||
BackgroundCache.markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY);
|
||||
});
|
||||
36
waterfox/browser/components/sidebar/background/index-ws.js
Normal file
36
waterfox/browser/components/sidebar/background/index-ws.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import './index.js';
|
||||
|
||||
import {
|
||||
configs,
|
||||
} from '/common/common.js';
|
||||
import '/common/sync-provider.js';
|
||||
|
||||
import './auto-sticky-tabs.js';
|
||||
import './handle-autoplay-blocking.js';
|
||||
import './handle-chrome-menu-commands.js';
|
||||
import './prefs.js';
|
||||
import './sharing-service.js';
|
||||
import * as TabsOpen from './tabs-open.js';
|
||||
|
||||
browser.waterfoxBridge.initUI();
|
||||
|
||||
TabsOpen.onForbiddenURLRequested.addListener(url => {
|
||||
browser.waterfoxBridge.reserveToLoadForbiddenURL(url);
|
||||
});
|
||||
|
||||
browser.waterfoxBridge.onSidebarShown.addListener(windowId => {
|
||||
configs.sidebarVirtuallyClosedWindows = configs.sidebarVirtuallyClosedWindows.filter(id => id != windowId);
|
||||
});
|
||||
|
||||
browser.waterfoxBridge.onSidebarHidden.addListener(windowId => {
|
||||
if (!configs.sidebarVirtuallyClosedWindows.includes(windowId))
|
||||
configs.sidebarVirtuallyClosedWindows = [...configs.sidebarVirtuallyClosedWindows, windowId];
|
||||
});
|
||||
|
||||
48
waterfox/browser/components/sidebar/background/index.js
Normal file
48
waterfox/browser/components/sidebar/background/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log,
|
||||
configs
|
||||
} from '/common/common.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
|
||||
import MetricsData from '/common/MetricsData.js';
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Background from './background.js';
|
||||
import './handle-misc.js';
|
||||
import './handle-moved-tabs.js';
|
||||
import './handle-new-tabs.js';
|
||||
import './handle-removed-tabs.js';
|
||||
import './handle-tab-bunches.js';
|
||||
import './handle-tab-focus.js';
|
||||
import './handle-tab-multiselect.js';
|
||||
import './handle-tree-changes.js';
|
||||
import './sync-background.js';
|
||||
|
||||
log.context = 'BG';
|
||||
|
||||
MetricsData.add('index: Loaded');
|
||||
|
||||
window.addEventListener('DOMContentLoaded', Background.init, { once: true });
|
||||
|
||||
window.dumpMetricsData = () => {
|
||||
return MetricsData.toString();
|
||||
};
|
||||
window.dumpLogs = () => {
|
||||
return log.logs.join('\n');
|
||||
};
|
||||
|
||||
// for old debugging method
|
||||
window.log = log;
|
||||
window.gMetricsData = MetricsData;
|
||||
window.Tab = Tab;
|
||||
window.TabsStore = TabsStore;
|
||||
window.SidebarConnection = SidebarConnection;
|
||||
window.configs = configs;
|
||||
505
waterfox/browser/components/sidebar/background/migration.js
Normal file
505
waterfox/browser/components/sidebar/background/migration.js
Normal file
@@ -0,0 +1,505 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs,
|
||||
saveUserStyleRules,
|
||||
notify,
|
||||
wait,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as Permissions from '/common/permissions.js';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function log(...args) {
|
||||
internalLogger('background/migration', ...args);
|
||||
}
|
||||
|
||||
const kCONFIGS_VERSION = 33;
|
||||
const kFEATURES_VERSION = 9;
|
||||
|
||||
export function migrateConfigs() {
|
||||
switch (configs.configsVersion) {
|
||||
case 0:
|
||||
case 1:
|
||||
if (configs.startDragTimeout !== null)
|
||||
configs.longPressDuration = configs.startDragTimeout;
|
||||
if (configs.emulateDefaultContextMenu !== null)
|
||||
configs.emulateDefaultContextMenu = configs.emulateDefaultContextMenu;
|
||||
|
||||
case 2:
|
||||
if (configs.simulateSelectOwnerOnClose !== null &&
|
||||
!configs.simulateSelectOwnerOnClose)
|
||||
configs.successorTabControlLevel = Constants.kSUCCESSOR_TAB_CONTROL_NEVER;
|
||||
|
||||
case 3:
|
||||
if (!(configs.tabDragBehavior & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK))
|
||||
configs.tabDragBehavior |= Constants.kDRAG_BEHAVIOR_TEAR_OFF;
|
||||
if (!(configs.tabDragBehaviorShift & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK))
|
||||
configs.tabDragBehaviorShift |= Constants.kDRAG_BEHAVIOR_TEAR_OFF;
|
||||
|
||||
case 4:
|
||||
if (configs.fakeContextMenu !== null)
|
||||
configs.emulateDefaultContextMenu = configs.fakeContextMenu;
|
||||
if (configs.context_closeTabOptions_closeTree !== null)
|
||||
configs.context_topLevel_closeTree = configs.context_closeTabOptions_closeTree;
|
||||
if (configs.context_closeTabOptions_closeDescendants !== null)
|
||||
configs.context_topLevel_closeDescendants = configs.context_closeTabOptions_closeDescendants;
|
||||
if (configs.context_closeTabOptions_closeOthers !== null)
|
||||
configs.context_topLevel_closeOthers = configs.context_closeTabOptions_closeOthers;
|
||||
|
||||
case 5:
|
||||
if (configs.scrollbarMode !== null) {
|
||||
switch (configs.scrollbarMode < 0 ? (isMacOS() ? 3 : 1) : configs.scrollbarMode) {
|
||||
case 0: // default, refular width
|
||||
configs.userStyleRules += `
|
||||
|
||||
/* regular width scrollbar */
|
||||
#tabbar { scrollbar-width: auto; }`;
|
||||
break;
|
||||
case 1: // narrow width
|
||||
break;
|
||||
case 2: // hide
|
||||
configs.userStyleRules += `
|
||||
|
||||
/* hide scrollbar */
|
||||
#tabbar { scrollbar-width: none; }
|
||||
|
||||
/* cancel spaces for macOS overlay scrollbar */
|
||||
:root.platform-mac #tabbar:dir(rtl).overflow .tab:not(.pinned) {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
:root.platform-mac #tabbar:dir(ltr).overflow .tab:not(.pinned) {
|
||||
padding-inline-end: 0;
|
||||
}`;
|
||||
break;
|
||||
case 3: // overlay (macOS)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (configs.sidebarScrollbarPosition !== null) {
|
||||
switch (configs.sidebarScrollbarPosition) {
|
||||
default:
|
||||
case 0: // auto
|
||||
case 1: // left
|
||||
break;
|
||||
break;
|
||||
case 2: // right
|
||||
configs.userStyleRules += `
|
||||
|
||||
/* put scrollbar rightside */
|
||||
:root.left #tabbar { direction: ltr; }`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
case 6:
|
||||
if (configs.promoteFirstChildForClosedRoot != null &&
|
||||
configs.promoteFirstChildForClosedRoot &&
|
||||
configs.closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN)
|
||||
configs.closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY;
|
||||
if (configs.parentTabBehaviorForChanges !== null) {
|
||||
switch (configs.parentTabBehaviorForChanges) {
|
||||
case Constants.kPARENT_TAB_BEHAVIOR_ALWAYS:
|
||||
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT;
|
||||
break;
|
||||
default:
|
||||
case Constants.kPARENT_TAB_BEHAVIOR_ONLY_WHEN_VISIBLE:
|
||||
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL;
|
||||
break;
|
||||
case Constants.kPARENT_TAB_BEHAVIOR_ONLY_ON_SIDEBAR:
|
||||
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM;
|
||||
configs.closeParentBehavior_outsideSidebar_expanded = configs.closeParentBehavior_noSidebar_expanded = configs. closeParentBehavior;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
case 7:
|
||||
if (configs.collapseExpandSubtreeByDblClick !== null &&
|
||||
configs.collapseExpandSubtreeByDblClick)
|
||||
configs.treeDoubleClickBehavior = Constants.kTREE_DOUBLE_CLICK_BEHAVIOR_TOGGLE_COLLAPSED;
|
||||
|
||||
case 8:
|
||||
if (configs.autoExpandOnCollapsedChildActive !== null)
|
||||
configs.unfocusableCollapsedTab = configs.autoExpandOnCollapsedChildActive;
|
||||
|
||||
case 9:
|
||||
if (configs.simulateCloseTabByDblclick !== null &&
|
||||
configs.simulateCloseTabByDblclick)
|
||||
configs.treeDoubleClickBehavior = Constants.kTREE_DOUBLE_CLICK_BEHAVIOR_CLOSE;
|
||||
|
||||
case 10:
|
||||
if (configs.style == 'plain-dark' ||
|
||||
configs.style == 'metal')
|
||||
configs.style = configs.$default.style;
|
||||
|
||||
case 11:
|
||||
if (configs.userStyleRules) {
|
||||
configs.userStyleRules0 = configs.userStyleRules;
|
||||
configs.userStyleRules = '';
|
||||
}
|
||||
|
||||
case 12:
|
||||
try {
|
||||
saveUserStyleRules(Array.from(new Uint8Array(8), (_, index) => {
|
||||
const key = `userStyleRules${index}`;
|
||||
if (key in configs) {
|
||||
const chunk = configs[key];
|
||||
configs[key] = '';
|
||||
return chunk || '';
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
}).join(''));
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
case 13:
|
||||
if (configs.style == 'mixed' ||
|
||||
configs.style == 'vertigo')
|
||||
configs.style = 'photon';
|
||||
|
||||
case 14:
|
||||
if (configs.inheritContextualIdentityToNewChildTab !== null)
|
||||
configs.inheritContextualIdentityToChildTabMode = configs.inheritContextualIdentityToNewChildTab ? Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT : Constants.kCONTEXTUAL_IDENTITY_DEFAULT;
|
||||
if (configs.inheritContextualIdentityToSameSiteOrphan !== null)
|
||||
configs.inheritContextualIdentityToSameSiteOrphanMode = configs.inheritContextualIdentityToSameSiteOrphan ? Constants.kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE : Constants.kCONTEXTUAL_IDENTITY_DEFAULT;
|
||||
if (configs.inheritContextualIdentityToTabsFromExternal !== null)
|
||||
configs.inheritContextualIdentityToTabsFromExternalMode = configs.inheritContextualIdentityToTabsFromExternal ? Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT : Constants.kCONTEXTUAL_IDENTITY_DEFAULT;
|
||||
|
||||
case 15:
|
||||
if (configs.moveDroppedTabToNewWindowForUnhandledDragEvent !== null &&
|
||||
!configs.moveDroppedTabToNewWindowForUnhandledDragEvent) {
|
||||
if (configs.tabDragBehavior & Constants.kDRAG_BEHAVIOR_TEAR_OFF)
|
||||
configs.tabDragBehavior = configs.tabDragBehavior ^ Constants.kDRAG_BEHAVIOR_TEAR_OFF;
|
||||
else if (configs.tabDragBehavior & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK)
|
||||
configs.tabDragBehavior = configs.tabDragBehavior ^ Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK;
|
||||
|
||||
if (configs.tabDragBehaviorShift & Constants.kDRAG_BEHAVIOR_TEAR_OFF)
|
||||
configs.tabDragBehaviorShift = configs.tabDragBehaviorShift ^ Constants.kDRAG_BEHAVIOR_TEAR_OFF;
|
||||
else if (configs.tabDragBehaviorShift & Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK)
|
||||
configs.tabDragBehaviorShift = configs.tabDragBehaviorShift ^ Constants.kDRAG_BEHAVIOR_ALLOW_BOOKMARK;
|
||||
}
|
||||
|
||||
case 16:
|
||||
configs.maximumDelayForBug1561879 = Math.max(configs.$default.maximumDelayForBug1561879, configs.maximumDelayForBug1561879);
|
||||
|
||||
case 17:
|
||||
configs.tabDragBehavior |= Constants.kDRAG_BEHAVIOR_MOVE;
|
||||
configs.tabDragBehaviorShift |= Constants.kDRAG_BEHAVIOR_MOVE;
|
||||
|
||||
case 18:
|
||||
if (configs.connectionTimeoutDelay == 5000)
|
||||
configs.connectionTimeoutDelay = configs.$default.connectionTimeoutDelay;
|
||||
|
||||
case 19:
|
||||
if (configs.suppressGapFromShownOrHiddenToolbar !== null) {
|
||||
configs.suppressGapFromShownOrHiddenToolbarOnNewTab =
|
||||
configs.suppressGapFromShownOrHiddenToolbarOnFullScreen = configs.suppressGapFromShownOrHiddenToolbar;
|
||||
}
|
||||
|
||||
case 20:
|
||||
if (configs.treatTreeAsExpandedOnClosedWithNoSidebar !== null) {
|
||||
configs.treatTreeAsExpandedOnClosed_noSidebar = configs.treatTreeAsExpandedOnClosedWithNoSidebar;
|
||||
}
|
||||
|
||||
case 21:
|
||||
if (configs.style == 'plain')
|
||||
configs.style = 'photon';
|
||||
|
||||
case 22:
|
||||
case 23:
|
||||
if (configs.closeParentBehaviorMode !== null) {
|
||||
configs.parentTabOperationBehaviorMode = configs.closeParentBehaviorMode;
|
||||
}
|
||||
if (configs.closeParentBehavior !== null) {
|
||||
configs.closeParentBehavior_insideSidebar_expanded =
|
||||
configs.closeParentBehavior;
|
||||
}
|
||||
if (configs.closeParentBehavior_outsideSidebar !== null) {
|
||||
configs.closeParentBehavior_outsideSidebar_expanded =
|
||||
configs.moveParentBehavior_outsideSidebar_expanded =
|
||||
configs.closeParentBehavior_outsideSidebar;
|
||||
}
|
||||
if (configs.closeParentBehavior_noSidebar !== null) {
|
||||
configs.closeParentBehavior_noSidebar_expanded =
|
||||
configs.closeParentBehavior_noSidebar_expanded =
|
||||
configs.closeParentBehavior_noSidebar;
|
||||
}
|
||||
if (configs.treatTreeAsExpandedOnClosed_outsideSidebar === true) {
|
||||
configs.closeParentBehavior_outsideSidebar_collapsed =
|
||||
configs.moveParentBehavior_outsideSidebar_collapsed =
|
||||
configs.moveParentBehavior_outsideSidebar_expanded;
|
||||
}
|
||||
if (configs.treatTreeAsExpandedOnClosed_noSidebar === false) {
|
||||
configs.closeParentBehavior_noSidebar_collapsed =
|
||||
configs.moveParentBehavior_noSidebar_collapsed =
|
||||
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE;
|
||||
}
|
||||
if (configs.treatTreeAsExpandedOnClosed_outsideSidebar === true ||
|
||||
configs.treatTreeAsExpandedOnClosed_noSidebar === false) {
|
||||
configs.parentTabOperationBehaviorMode = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM;
|
||||
}
|
||||
|
||||
case 24:
|
||||
if (configs.autoGroupNewTabsTimeout !== null)
|
||||
configs.tabBunchesDetectionTimeout = configs.autoGroupNewTabsTimeout;
|
||||
if (configs.autoGroupNewTabsDelayOnNewWindow !== null)
|
||||
configs.tabBunchesDetectionDelayOnNewWindow = configs.autoGroupNewTabsDelayOnNewWindow;
|
||||
|
||||
case 25:
|
||||
if (configs.autoHiddenScrollbarPlaceholderSize !== null)
|
||||
configs.shiftTabsForScrollbarDistance = configs.autoHiddenScrollbarPlaceholderSize;
|
||||
|
||||
case 26:
|
||||
if (!configs.guessNewOrphanTabAsOpenedByNewTabCommandUrl.includes('about:privatebrowsing'))
|
||||
configs.guessNewOrphanTabAsOpenedByNewTabCommandUrl = `${configs.guessNewOrphanTabAsOpenedByNewTabCommandUrl.trim().replace(/\|$/, '')}|about:privatebrowsing`;
|
||||
|
||||
case 27:
|
||||
if (configs.openAllBookmarksWithGroupAlways !== null)
|
||||
configs.suppressGroupTabForStructuredTabsFromBookmarks = !configs.openAllBookmarksWithGroupAlways;
|
||||
|
||||
case 28:
|
||||
if (configs.heartbeatInterval == 1000)
|
||||
configs.heartbeatInterval = configs.$default.heartbeatInterval;
|
||||
|
||||
case 29:
|
||||
const autoAttachKeys = [
|
||||
'autoAttachOnOpenedWithOwner',
|
||||
'autoAttachOnNewTabCommand',
|
||||
'autoAttachOnContextNewTabCommand',
|
||||
'autoAttachOnNewTabButtonMiddleClick',
|
||||
'autoAttachOnNewTabButtonAccelClick',
|
||||
'autoAttachOnDuplicated',
|
||||
'autoAttachSameSiteOrphan',
|
||||
'autoAttachOnOpenedFromExternal',
|
||||
'autoAttachOnAnyOtherTrigger',
|
||||
];
|
||||
if (configs.insertNewChildAt != configs.$default.insertNewChildAt &&
|
||||
(configs.insertNewChildAt == Constants.kINSERT_TOP ||
|
||||
configs.insertNewChildAt == Constants.kINSERT_END)) {
|
||||
for (const key of autoAttachKeys) {
|
||||
if (configs[key] != configs.$default[key] &&
|
||||
configs[key] != Constants.kNEWTAB_OPEN_AS_CHILD)
|
||||
continue;
|
||||
if (configs.insertNewChildAt == Constants.kINSERT_TOP)
|
||||
configs[key] = Constants.kNEWTAB_OPEN_AS_CHILD_TOP;
|
||||
else
|
||||
configs[key] = Constants.kNEWTAB_OPEN_AS_CHILD_END;
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const key of autoAttachKeys) {
|
||||
if (configs[key] == Constants.kNEWTAB_OPEN_AS_CHILD)
|
||||
configs[key] = Constants.kNEWTAB_OPEN_AS_CHILD_TOP;
|
||||
}
|
||||
}
|
||||
|
||||
case 30:
|
||||
browser.windows.getAll().then(windows => {
|
||||
for (const win of windows) {
|
||||
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR);
|
||||
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
|
||||
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY);
|
||||
}
|
||||
});
|
||||
|
||||
case 31:
|
||||
if (configs.tabPreviewTooltipInSidebar !== null)
|
||||
configs.tabPreviewTooltipRenderIn = configs.tabPreviewTooltipInSidebar ?
|
||||
Constants.kIN_CONTENT_PANEL_RENDER_IN_ANYWHERE :
|
||||
Constants.kIN_CONTENT_PANEL_RENDER_IN_CONTENT;
|
||||
|
||||
case 32:
|
||||
if (configs.tabPreviewTooltipOffsetTop !== null)
|
||||
configs.inContentUIOffsetTop = configs.tabPreviewTooltipOffsetTop;
|
||||
}
|
||||
configs.configsVersion = kCONFIGS_VERSION;
|
||||
}
|
||||
|
||||
let mShouldShowInitialStartupPage = false;
|
||||
|
||||
export function tryNotifyNewFeatures() {
|
||||
/*
|
||||
let featuresVersionOffset = 0;
|
||||
const browserInfo = await browser.runtime.getBrowserInfo().catch(ApiTabs.createErrorHandler());
|
||||
// "search" permission becomes available!
|
||||
if (parseInt(browserInfo.version.split('.')[0]) >= 63)
|
||||
featuresVersionOffset++;
|
||||
// "menus.overrideContext" permission becomes available!
|
||||
if (parseInt(browserInfo.version.split('.')[0]) >= 64)
|
||||
featuresVersionOffset++;
|
||||
*/
|
||||
|
||||
const featuresVersion = kFEATURES_VERSION /*+ featuresVersionOffset*/;
|
||||
const isInitialInstall = configs.notifiedFeaturesVersion == 0;
|
||||
|
||||
if (configs.notifiedFeaturesVersion >= featuresVersion)
|
||||
return;
|
||||
configs.notifiedFeaturesVersion = featuresVersion;
|
||||
|
||||
if (isInitialInstall &&
|
||||
!configs.syncOtherDevicesDetected &&
|
||||
Object.keys(configs.syncDevices).length > 1) {
|
||||
configs.syncAvailableNotified = true;
|
||||
configs.syncOtherDevicesDetected = true;
|
||||
}
|
||||
|
||||
const typeSuffix = isInitialInstall ? 'installed' : 'updated';
|
||||
const platformSuffix = isLinux() ? '_linux' : '';
|
||||
const url = isInitialInstall ? Constants.kSHORTHAND_URIS.startup : browser.i18n.getMessage('message_startup_history_uri');
|
||||
notify({
|
||||
url,
|
||||
title: browser.i18n.getMessage(`startup_notification_title_${typeSuffix}`),
|
||||
message: browser.i18n.getMessage(`startup_notification_message_${typeSuffix}${platformSuffix}`),
|
||||
timeout: 90 * 1000
|
||||
});
|
||||
|
||||
if (isInitialInstall) {
|
||||
mShouldShowInitialStartupPage = true;
|
||||
browser.browserAction.setBadgeText({
|
||||
text: '!',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isInitialStartup() {
|
||||
return !!mShouldShowInitialStartupPage;
|
||||
}
|
||||
|
||||
export function openInitialStartupPage() {
|
||||
mShouldShowInitialStartupPage = false;
|
||||
browser.browserAction.setBadgeText({
|
||||
text: null,
|
||||
});
|
||||
browser.tabs.create({
|
||||
url: Constants.kSHORTHAND_URIS.startup,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Auto-migration of bookmarked internal URLs
|
||||
//
|
||||
// Internal URLs like "moz-extension://(UUID)/..." are runtime environment
|
||||
// dependent and unavailable when such bookmarks are loaded in different
|
||||
// runtime environment, for example they are synchronized from other devices.
|
||||
// Thus we should migrate such internal URLs to universal shorthand URIs like
|
||||
// "ext+ws:(name)".
|
||||
|
||||
export async function migrateBookmarkUrls() {
|
||||
const granted = await Permissions.isGranted(Permissions.BOOKMARKS);
|
||||
if (!granted)
|
||||
return;
|
||||
|
||||
startBookmarksUrlAutoMigration();
|
||||
|
||||
const urls = new Set(configs.migratedBookmarkUrls);
|
||||
const migrations = [];
|
||||
const updates = [];
|
||||
for (const key in Constants.kSHORTHAND_URIS) {
|
||||
const url = Constants.kSHORTHAND_URIS[key].split('?')[0];
|
||||
if (urls.has(url))
|
||||
continue;
|
||||
|
||||
const shorthand = `ext+ws:${key.toLowerCase()}`;
|
||||
migrations.push(browser.bookmarks.search({ query: url })
|
||||
.then(bookmarks => {
|
||||
for (const bookmark of bookmarks) {
|
||||
updates.push(browser.bookmarks.update(bookmark.id, {
|
||||
url: bookmark.url.replace(url, shorthand)
|
||||
}));
|
||||
}
|
||||
}));
|
||||
urls.add(url);
|
||||
}
|
||||
if (migrations.length > 0)
|
||||
await Promise.all(migrations);
|
||||
if (updates.length > 0)
|
||||
await Promise.all(updates);
|
||||
if (urls.size > configs.migratedBookmarkUrls.length)
|
||||
configs.migratedBookmarkUrls = Array.from(urls);
|
||||
}
|
||||
|
||||
async function migrateBookmarkUrl(bookmark) {
|
||||
for (const key in Constants.kSHORTHAND_URIS) {
|
||||
const url = Constants.kSHORTHAND_URIS[key].split('?')[0];
|
||||
if (!bookmark.url.startsWith(url))
|
||||
continue;
|
||||
|
||||
const shorthand = `ext+ws:${key.toLowerCase()}`;
|
||||
return browser.bookmarks.update(bookmark.id, {
|
||||
url: bookmark.url.replace(url, shorthand)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mObservingBookmarks = false;
|
||||
let mBookmarksListenersRegistered = false;
|
||||
|
||||
function onBookmarkCreated(id, bookmark) {
|
||||
if (!mObservingBookmarks)
|
||||
return;
|
||||
|
||||
if (bookmark.url)
|
||||
migrateBookmarkUrl(bookmark);
|
||||
}
|
||||
|
||||
async function onBookmarkChanged(id, changeInfo) {
|
||||
if (!mObservingBookmarks)
|
||||
return;
|
||||
|
||||
if (changeInfo.url &&
|
||||
changeInfo.url.startsWith(browser.runtime.getURL(''))) {
|
||||
const bookmark = await browser.bookmarks.get(id);
|
||||
if (Array.isArray(bookmark))
|
||||
bookmark.forEach(migrateBookmarkUrl);
|
||||
else
|
||||
migrateBookmarkUrl(bookmark);
|
||||
}
|
||||
}
|
||||
|
||||
if (browser.bookmarks &&
|
||||
browser.bookmarks.onCreated) {
|
||||
browser.bookmarks.onCreated.addListener(onBookmarkCreated);
|
||||
browser.bookmarks.onChanged.addListener(onBookmarkChanged);
|
||||
mBookmarksListenersRegistered = true;
|
||||
}
|
||||
|
||||
async function startBookmarksUrlAutoMigration() {
|
||||
if (mObservingBookmarks)
|
||||
return;
|
||||
|
||||
mObservingBookmarks = true;
|
||||
|
||||
if (mBookmarksListenersRegistered ||
|
||||
!browser.bookmarks ||
|
||||
!browser.bookmarks.onCreated)
|
||||
return;
|
||||
|
||||
browser.bookmarks.onCreated.addListener(onBookmarkCreated);
|
||||
browser.bookmarks.onChanged.addListener(onBookmarkChanged);
|
||||
mBookmarksListenersRegistered = true;
|
||||
}
|
||||
|
||||
|
||||
configs.$loaded.then(() => {
|
||||
configs.$addObserver(async key => {
|
||||
// This may be triggered not only "reset all", but while importing of configs also.
|
||||
// We should try initial migration after all configs are successfully imported.
|
||||
await wait(100);
|
||||
if (key == 'configsVersion' &&
|
||||
configs.configsVersion == 0)
|
||||
migrateConfigs();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
wait,
|
||||
configs,
|
||||
shouldApplyAnimation,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
|
||||
import { Tab, TabGroup, TreeItem } from '/common/TreeItem.js';
|
||||
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/native-tab-groups', ...args);
|
||||
}
|
||||
|
||||
export const internallyMovingNativeTabGroups = new Map();
|
||||
|
||||
export async function addTabsToGroup(tabs, groupIdOrProperties) {
|
||||
const initialGroupId = typeof groupIdOrProperties == 'number' ? groupIdOrProperties : null;
|
||||
const groupId = await addTabsToGroupInternal(tabs, groupIdOrProperties);
|
||||
const created = groupId != initialGroupId;
|
||||
return { groupId, created };
|
||||
}
|
||||
async function addTabsToGroupInternal(tabs, groupIdOrProperties) {
|
||||
let groupId = typeof groupIdOrProperties == 'number' ? groupIdOrProperties : null;
|
||||
const tabsToGrouped = tabs.filter(tab => tab.groupId != groupId);
|
||||
if (tabsToGrouped.length == 0) {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
log('addTabsToGroupInternal ', tabsToGrouped, groupId, groupIdOrProperties);
|
||||
|
||||
const pinnedTabs = tabsToGrouped.filter(tab => tab.pinned);
|
||||
if (pinnedTabs.length > 0) {
|
||||
await Promise.all(
|
||||
pinnedTabs.map(
|
||||
tab => browser.tabs.update(tab.id, { pinned: false })
|
||||
.catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const windowId = tabsToGrouped[0].windowId;
|
||||
const structure = TreeBehavior.getTreeStructureFromTabs(tabsToGrouped);
|
||||
|
||||
await Tree.detachTabsFromTree(tabsToGrouped, {
|
||||
fromParent: true,
|
||||
partial: true,
|
||||
});
|
||||
|
||||
const { promisedGrouped, finish } = waitUntilGrouped(tabsToGrouped, {
|
||||
groupId,
|
||||
windowId,
|
||||
});
|
||||
log('addTabsToGroupInternal: group tabs!');
|
||||
await browser.tabs.group({
|
||||
groupId,
|
||||
tabIds: tabsToGrouped.map(tab => tab.id),
|
||||
...(groupId ? {} : {
|
||||
createProperties: {
|
||||
windowId, // We must specify the window ID explicitly, otherwise tabs moved across windows may be reverted and grouped in the old window!
|
||||
},
|
||||
})
|
||||
});
|
||||
const group = await promisedGrouped;
|
||||
groupId = group.id;
|
||||
log('addTabsToGroupInternal: => ', group);
|
||||
|
||||
for (const tab of tabsToGrouped) {
|
||||
TabsStore.addNativelyGroupedTab(tab, group.windowId);
|
||||
}
|
||||
|
||||
if (groupIdOrProperties &&
|
||||
typeof groupIdOrProperties == 'object') {
|
||||
log('addTabsToGroupInternal: applying group properties');
|
||||
const updateProperties = {};
|
||||
if ('title' in groupIdOrProperties) {
|
||||
updateProperties.title = groupIdOrProperties.title;
|
||||
}
|
||||
if ('color' in groupIdOrProperties) {
|
||||
updateProperties.color = groupIdOrProperties.color;
|
||||
}
|
||||
if ('collapsed' in groupIdOrProperties) {
|
||||
updateProperties.collapsed = groupIdOrProperties.collapsed;
|
||||
}
|
||||
await browser.tabGroups.update(groupId, updateProperties);
|
||||
}
|
||||
|
||||
finish();
|
||||
|
||||
await rejectGroupFromTree(group);
|
||||
|
||||
log('addTabsToGroupInternal: applying tree structure');
|
||||
await Tree.applyTreeStructureToTabs(tabsToGrouped, structure, {
|
||||
broadcast: true
|
||||
});
|
||||
|
||||
return groupId;
|
||||
}
|
||||
|
||||
export async function rejectGroupFromTree(group) {
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
group = TabGroup.get(group.id);
|
||||
if (!group?.$TST) {
|
||||
log('rejectGroupFromTree: failed to reject untracked group');
|
||||
return;
|
||||
}
|
||||
|
||||
const firstMember = group.$TST.firstMember;
|
||||
const lastMember = group.$TST.lastMember;
|
||||
const prevTab = firstMember?.$TST.previousTab;
|
||||
const nextTab = lastMember?.$TST.nextTab;
|
||||
const rootTab = prevTab?.$TST.rootTab;
|
||||
if (!prevTab ||
|
||||
!nextTab ||
|
||||
prevTab.groupId != nextTab.groupId ||
|
||||
prevTab.groupId != -1 ||
|
||||
rootTab != nextTab.$TST.rootTab) {
|
||||
log('rejectGroupFromTree: no need to reject from tree');
|
||||
return;
|
||||
}
|
||||
|
||||
log('rejectGroupFromTree ', group.id);
|
||||
await Tree.detachTabsFromTree(group.$TST.members, {
|
||||
fromParent: true,
|
||||
partial: true,
|
||||
});
|
||||
|
||||
// The group is in a middle of a tree. We need to move the new group away from the tree.
|
||||
const lastDescendant = rootTab.$TST.lastDescendant;
|
||||
if (firstMember.index - rootTab.index <= lastDescendant.index - lastMember.index) { // move above the tree
|
||||
log('rejectGroupFromTree: move ', group.id, ' before ', rootTab.id);
|
||||
await moveGroupBefore(group, rootTab);
|
||||
}
|
||||
else { // move below the tree
|
||||
log('rejectGroupFromTree: move ', group.id, ' after ', lastDescendant.id);
|
||||
await moveGroupAfter(group, lastDescendant);
|
||||
}
|
||||
}
|
||||
|
||||
function waitUntilGrouped(tabs, { groupId, windowId } = {}) {
|
||||
const toBeGroupedIds = tabs.map(tab => tab.id);
|
||||
const win = TabsStore.windows.get(windowId || tabs[0].windowId);
|
||||
|
||||
for (const tab of tabs) {
|
||||
win.internallyMovingTabsForUpdatedNativeTabGroups.add(tab.id);
|
||||
win.internalMovingTabs.set(tab.id, -1);
|
||||
}
|
||||
|
||||
let onUpdated = null;
|
||||
const { promisedMoved, finish: finishMoved } = waitUntilMoved(tabs, win.id)
|
||||
const promisedGrouped = new Promise((resolve, _reject) => {
|
||||
if (!groupId) {
|
||||
const onGroupCreated = group => {
|
||||
groupId = group.id;
|
||||
browser.tabGroups.onCreated.removeListener(onGroupCreated);
|
||||
};
|
||||
browser.tabGroups.onCreated.addListener(onGroupCreated);
|
||||
}
|
||||
const toBeGroupedIdsSet = new Set(toBeGroupedIds);
|
||||
onUpdated = (tabId, changeInfo, _tab) => {
|
||||
if (changeInfo.groupId == groupId) {
|
||||
toBeGroupedIdsSet.delete(tabId);
|
||||
win.internallyMovingTabsForUpdatedNativeTabGroups.delete(tabId);
|
||||
}
|
||||
if (toBeGroupedIdsSet.size == 0) {
|
||||
resolve(changeInfo.groupId);
|
||||
}
|
||||
};
|
||||
browser.tabs.onUpdated.addListener(onUpdated, { properties: ['groupId'] });
|
||||
});
|
||||
|
||||
const finish = () => {
|
||||
if (finish.done) {
|
||||
return;
|
||||
}
|
||||
browser.tabs.onUpdated.removeListener(onUpdated);
|
||||
for (const tab of tabs) {
|
||||
win.internalMovingTabs.delete(tab.id);
|
||||
}
|
||||
finish.done = true;
|
||||
};
|
||||
|
||||
return {
|
||||
promisedGrouped: Promise.all([
|
||||
promisedGrouped,
|
||||
Promise.race([
|
||||
promisedMoved,
|
||||
wait(configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove),
|
||||
]),
|
||||
]).then(([groupId]) => {
|
||||
finish();
|
||||
finishMoved();
|
||||
return TabGroup.get(groupId);
|
||||
}),
|
||||
finish,
|
||||
};
|
||||
}
|
||||
|
||||
export async function removeTabsFromGroup(tabs) {
|
||||
const tabsToBeUngrouped = tabs.filter(tab => tab.groupId != -1);
|
||||
if (tabsToBeUngrouped.length == 0) {
|
||||
return;
|
||||
}
|
||||
const win = TabsStore.windows.get(tabs[0].windowId);
|
||||
for (const tab of tabs) {
|
||||
win.internallyMovingTabsForUpdatedNativeTabGroups.add(tab.id);
|
||||
win.internalMovingTabs.set(tab.id, -1);
|
||||
}
|
||||
const toBeUngroupedIds = tabsToBeUngrouped.map(tab => tab.id);
|
||||
let onUpdated = null;
|
||||
await new Promise((resolve, _reject) => {
|
||||
const toBeUngroupedIdsSet = new Set(toBeUngroupedIds);
|
||||
onUpdated = (tabId, changeInfo, _tab) => {
|
||||
if (changeInfo.groupId == -1) {
|
||||
toBeUngroupedIdsSet.delete(tabId);
|
||||
win.internallyMovingTabsForUpdatedNativeTabGroups.delete(tabId);
|
||||
}
|
||||
if (toBeUngroupedIdsSet.size == 0) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
browser.tabs.onUpdated.addListener(onUpdated, { properties: ['groupId'] });
|
||||
browser.tabs.ungroup(toBeUngroupedIds);
|
||||
});
|
||||
for (const tab of tabsToBeUngrouped) {
|
||||
win.internalMovingTabs.delete(tab.id);
|
||||
TabsStore.removeNativelyGroupedTab(tab, win.id);
|
||||
}
|
||||
browser.tabs.onUpdated.removeListener(onUpdated);
|
||||
}
|
||||
|
||||
export async function matchTabsGrouped(tabs, groupIdOrCreateParams) {
|
||||
if (groupIdOrCreateParams == -1) {
|
||||
await removeTabsFromGroup(tabs);
|
||||
}
|
||||
else {
|
||||
await addTabsToGroup(tabs, groupIdOrCreateParams);
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveGroupToNewWindow({ groupId, windowId, duplicate, left, top }) {
|
||||
log('moveGroupToNewWindow: ', groupId, windowId);
|
||||
const group = TabGroup.get(groupId);
|
||||
const members = group.$TST.members;
|
||||
const movedTabs = await Tree.openNewWindowFromTabs(members, { duplicate, left, top });
|
||||
await addTabsToGroupInternal(movedTabs, {
|
||||
title: group.title,
|
||||
color: group.color,
|
||||
});
|
||||
}
|
||||
|
||||
export async function moveGroupBefore(group, insertBefore) {
|
||||
log('moveGroupBefore: ', group, insertBefore);
|
||||
const beforeCount = internallyMovingNativeTabGroups.get(group.id) || 0;
|
||||
internallyMovingNativeTabGroups.set(group.id, beforeCount + 1);
|
||||
|
||||
const { promisedMoved, finish } = waitUntilMoved(group, insertBefore.windowId);
|
||||
|
||||
if (insertBefore.type == TreeItem.TYPE_GROUP) {
|
||||
insertBefore = insertBefore.$TST.firstMember;
|
||||
}
|
||||
|
||||
const members = group.$TST.members;
|
||||
const firstMember = group.$TST.firstMember;
|
||||
const delta = insertBefore.windowId == group.windowId && insertBefore.index > firstMember.index ? members.length : 0;
|
||||
const index = insertBefore.index - delta;
|
||||
log('moveGroupBefore: move to ', index, { delta, insertBeforeIndex: insertBefore.index });
|
||||
await browser.tabGroups.move(group.id, {
|
||||
index,
|
||||
windowId: insertBefore.windowId,
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
promisedMoved,
|
||||
wait(configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove).then(() => {
|
||||
if (finish.done) {
|
||||
return;
|
||||
}
|
||||
}),
|
||||
]);
|
||||
finish();
|
||||
|
||||
const afterCount = internallyMovingNativeTabGroups.get(group.id) || 0;
|
||||
if (afterCount <= 1) {
|
||||
internallyMovingNativeTabGroups.delete(group.id);
|
||||
}
|
||||
else {
|
||||
internallyMovingNativeTabGroups.set(group.id, afterCount - 1);
|
||||
}
|
||||
log('moveGroupBefore: finish');
|
||||
}
|
||||
|
||||
export async function moveGroupAfter(group, insertAfter) {
|
||||
log('moveGroupAfter: ', group, insertAfter);
|
||||
const beforeCount = internallyMovingNativeTabGroups.get(group.id) || 0;
|
||||
internallyMovingNativeTabGroups.set(group.id, beforeCount + 1);
|
||||
|
||||
const { promisedMoved, finish } = waitUntilMoved(group, insertAfter.windowId);
|
||||
|
||||
if (insertAfter.type == TreeItem.TYPE_GROUP) {
|
||||
if (insertAfter.collapsed) {
|
||||
insertAfter = insertAfter.$TST.lastMember;
|
||||
}
|
||||
else {
|
||||
return moveGroupBefore(group, insertAfter.$TST.firstMember);
|
||||
}
|
||||
}
|
||||
|
||||
const members = group.$TST.members;
|
||||
const firstMember = group.$TST.firstMember;
|
||||
const delta = insertAfter.windowId == group.windowId && insertAfter.index > firstMember.index ? members.length : 0;
|
||||
const index = insertAfter.index + 1 - delta;
|
||||
log('moveGroupAfter: move to ', index, { delta, insertAfterIndex: insertAfter.index });
|
||||
await browser.tabGroups.move(group.id, {
|
||||
index,
|
||||
windowId: insertAfter.windowId,
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
promisedMoved,
|
||||
wait(configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove).then(() => {
|
||||
if (finish.done) {
|
||||
return;
|
||||
}
|
||||
}),
|
||||
]);
|
||||
finish();
|
||||
|
||||
const afterCount = internallyMovingNativeTabGroups.get(group.id) || 0;
|
||||
if (afterCount <= 1) {
|
||||
internallyMovingNativeTabGroups.delete(group.id);
|
||||
}
|
||||
else {
|
||||
internallyMovingNativeTabGroups.set(group.id, afterCount - 1);
|
||||
}
|
||||
log('moveGroupAfter: finish');
|
||||
}
|
||||
|
||||
export function waitUntilMoved(groupOrMembers, destinationWindowId) {
|
||||
const members = Array.isArray(groupOrMembers) ?
|
||||
groupOrMembers :
|
||||
groupOrMembers.$TST.members;
|
||||
const win = TabsStore.windows.get(destinationWindowId || members[0].windowId);
|
||||
const toBeMovedTabs = new Set();
|
||||
for (const tab of members) {
|
||||
toBeMovedTabs.add(tab.id);
|
||||
win.internalMovingTabs.set(tab.id, -1);
|
||||
}
|
||||
let onTabMoved;
|
||||
const promisedMoved = new Promise((resolve, _reject) => {
|
||||
onTabMoved = (tabId, _moveInfo) => {
|
||||
if (toBeMovedTabs.has(tabId)) {
|
||||
toBeMovedTabs.delete(tabId);
|
||||
}
|
||||
if (toBeMovedTabs.size == 0) {
|
||||
log('waitUntilMoved: all members have been moved');
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
browser.tabs.onMoved.addListener(onTabMoved);
|
||||
});
|
||||
const finish = () => {
|
||||
if (finish.done) {
|
||||
return;
|
||||
}
|
||||
browser.tabs.onMoved.removeListener(onTabMoved);
|
||||
for (const tab of members) {
|
||||
win.internalMovingTabs.delete(tab.id);
|
||||
}
|
||||
finish.done = true;
|
||||
};
|
||||
return {
|
||||
promisedMoved: promisedMoved.then(finish),
|
||||
finish,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function reserveToMaintainTreeForGroup(groupId, options = {}) {
|
||||
let timer = reserveToMaintainTreeForGroup.delayed.get(groupId);
|
||||
if (timer)
|
||||
clearTimeout(timer);
|
||||
if (options.justNow || !shouldApplyAnimation()) {
|
||||
const group = TabGroup.get(groupId);
|
||||
rejectGroupFromTree(group);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
reserveToMaintainTreeForGroup.delayed.delete(groupId);
|
||||
const group = TabGroup.get(groupId);
|
||||
rejectGroupFromTree(group);
|
||||
}, configs.nativeTabGroupModificationDetectionTimeoutAfterTabMove);
|
||||
reserveToMaintainTreeForGroup.delayed.set(groupId, timer);
|
||||
}
|
||||
reserveToMaintainTreeForGroup.delayed = new Map();
|
||||
|
||||
export async function startToMaintainTree() {
|
||||
// fixup mismatched tree structure and tab groups constructed while TST is disabled
|
||||
const groups = await browser.tabGroups.query({});
|
||||
for (const group of groups) {
|
||||
await rejectGroupFromTree(TabGroup.get(group.id));
|
||||
}
|
||||
|
||||
// after all we start tracking of dynamic changes of tab groups
|
||||
|
||||
browser.tabGroups.onMoved.addListener(group => {
|
||||
group = TabGroup.get(group.id);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
log('detected tab group move: ', group);
|
||||
const internalMoveCount = internallyMovingNativeTabGroups.get(group.id);
|
||||
if (internalMoveCount) {
|
||||
log(' => ignore internal move ', internalMoveCount);
|
||||
return;
|
||||
}
|
||||
reserveToMaintainTreeForGroup(group.id);
|
||||
});
|
||||
|
||||
Tab.onNativeGroupModified.addListener(tab => {
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
if (win.internallyMovingTabsForUpdatedNativeTabGroups.has(tab.id)) {
|
||||
return;
|
||||
}
|
||||
reserveToMaintainTreeForGroup(tab.groupId);
|
||||
});
|
||||
}
|
||||
100
waterfox/browser/components/sidebar/background/prefs.js
Normal file
100
waterfox/browser/components/sidebar/background/prefs.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/* 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';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
configs,
|
||||
obsoleteConfigs,
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
|
||||
export const onChanged = new EventListenerManager();
|
||||
|
||||
if (Constants.IS_BACKGROUND) {
|
||||
for (const [name, value] of Object.entries(configs.$default)) {
|
||||
if (obsoleteConfigs.has(name))
|
||||
continue;
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
browser.prefs.setDefaultBoolValue(name, value);
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
browser.prefs.setDefaultStringValue(name, value);
|
||||
break;
|
||||
|
||||
default:
|
||||
browser.prefs.setDefaultStringValue(name, JSON.stringify(value));
|
||||
break;
|
||||
}
|
||||
|
||||
getPref(name).then(value => {
|
||||
if (JSON.stringify(configs[name]) == JSON.stringify(value))
|
||||
return;
|
||||
configs[name] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getPref(name) {
|
||||
const defaultValue = configs.$default[name];
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
return browser.prefs.getBoolValue(name, defaultValue);
|
||||
|
||||
case 'string':
|
||||
return browser.prefs.getStringValue(name, defaultValue);
|
||||
|
||||
default:
|
||||
return browser.prefs.getStringValue(name, JSON.stringify(defaultValue)).then(value => JSON.parse(value));
|
||||
}
|
||||
}
|
||||
|
||||
const mNamesSyncToChrome = new Set();
|
||||
const mNamesSyncFromChrome = new Set();
|
||||
|
||||
browser.prefs.onChanged.addListener(name => {
|
||||
if (mNamesSyncToChrome.has(name) ||
|
||||
mNamesSyncFromChrome.has(name))
|
||||
return;
|
||||
|
||||
mNamesSyncFromChrome.add(name);
|
||||
getPref(name)
|
||||
.then(value => {
|
||||
configs[name] = value;
|
||||
window.requestAnimationFrame(() => {
|
||||
mNamesSyncFromChrome.delete(name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs.$addObserver(async name => {
|
||||
if (mNamesSyncToChrome.has(name) ||
|
||||
mNamesSyncFromChrome.has(name))
|
||||
return;
|
||||
|
||||
mNamesSyncToChrome.add(name);
|
||||
|
||||
const value = configs[name];
|
||||
const defaultValue = configs.$default[name];
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
await browser.prefs.setBoolValue(name, value);
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
await browser.prefs.setStringValue(name, value);
|
||||
break;
|
||||
|
||||
default:
|
||||
await browser.prefs.setStringValue(name, JSON.stringify(value));
|
||||
break;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
mNamesSyncToChrome.delete(name);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import * as TabContextMenu from './tab-context-menu.js';
|
||||
|
||||
TabContextMenu.registerSharingService({
|
||||
async listServices(tab) {
|
||||
return browser.waterfoxBridge.listSharingServices(tab.id);
|
||||
},
|
||||
|
||||
share(tab, shareName = null) {
|
||||
return browser.waterfoxBridge.share(tab.id, shareName);
|
||||
},
|
||||
|
||||
openPreferences() {
|
||||
return browser.waterfoxBridge.openSharingPreferences();
|
||||
},
|
||||
});
|
||||
467
waterfox/browser/components/sidebar/background/successor-tab.js
Normal file
467
waterfox/browser/components/sidebar/background/successor-tab.js
Normal file
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
configs,
|
||||
wait,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/successor-tab', ...args);
|
||||
}
|
||||
|
||||
const mTabsToBeUpdated = new Set();
|
||||
const mInProgressUpdates = new Set();
|
||||
|
||||
const mPromisedUpdatedSuccessorTabId = new Map();
|
||||
|
||||
browser.tabs.onUpdated.addListener((tabId, updateInfo, _tab) => {
|
||||
if (!('successorTabId' in updateInfo) ||
|
||||
!mPromisedUpdatedSuccessorTabId.has(tabId))
|
||||
return;
|
||||
const promisedUpdate = mPromisedUpdatedSuccessorTabId.get(tabId);
|
||||
mPromisedUpdatedSuccessorTabId.delete(tabId);
|
||||
promisedUpdate.resolver(updateInfo.successorTabId);
|
||||
}, {
|
||||
// we cannot watch only the property...
|
||||
// properties: ['successorTabId'],
|
||||
});
|
||||
|
||||
TabsInternalOperation.onBeforeTabsRemove.addListener(async tabs => {
|
||||
let activeTab = null;
|
||||
const tabIds = tabs.map(tab => {
|
||||
if (tab.active)
|
||||
activeTab = tab;
|
||||
return tab.id;
|
||||
});
|
||||
if (activeTab)
|
||||
await updateInternal(activeTab.id, tabIds);
|
||||
});
|
||||
|
||||
function setSuccessor(tabId, successorTabId = -1) {
|
||||
const tab = Tab.get(tabId);
|
||||
const successorTab = Tab.get(successorTabId);
|
||||
if (configs.successorTabControlLevel == Constants.kSUCCESSOR_TAB_CONTROL_NEVER ||
|
||||
!tab ||
|
||||
!successorTab ||
|
||||
tab.windowId != successorTab.windowId)
|
||||
return;
|
||||
|
||||
const promisedUpdate = {};
|
||||
promisedUpdate.promisedSuccessorTabId = new Promise((resolve, _reject) => {
|
||||
promisedUpdate.resolver = resolve;
|
||||
setTimeout(() => {
|
||||
if (!mPromisedUpdatedSuccessorTabId.has(tabId))
|
||||
return;
|
||||
mPromisedUpdatedSuccessorTabId.delete(tabId);
|
||||
resolve(null);
|
||||
}, 2000);
|
||||
});
|
||||
mPromisedUpdatedSuccessorTabId.set(tabId, promisedUpdate);
|
||||
|
||||
const initialSuccessorTabId = tab.successorTabId;
|
||||
browser.tabs.update(tabId, {
|
||||
successorTabId
|
||||
}).then(async () => {
|
||||
// tabs.onUpdated listener won't be called sometimes, so this is a failsafe.
|
||||
while (true) {
|
||||
const promisedUpdate = mPromisedUpdatedSuccessorTabId.get(tabId);
|
||||
if (!promisedUpdate)
|
||||
break;
|
||||
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
if (tab.successorTabId == initialSuccessorTabId &&
|
||||
tab.successorTabId != successorTabId) {
|
||||
await wait(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
mPromisedUpdatedSuccessorTabId.delete(tabId);
|
||||
promisedUpdate.resolver(tab.successorTabId);
|
||||
}
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, error => {
|
||||
// ignore error for already closed tab
|
||||
if (!error ||
|
||||
!error.message ||
|
||||
(error.message.indexOf('Invalid successorTabId') != 0 &&
|
||||
// This error may happen at the time just after a tab is detached from its original window.
|
||||
error.message.indexOf('Successor tab must be in the same window as the tab being updated') != 0))
|
||||
throw error;
|
||||
}));
|
||||
}
|
||||
|
||||
function clearSuccessor(tabId) {
|
||||
setSuccessor(tabId, -1);
|
||||
}
|
||||
|
||||
|
||||
function update(tabId) {
|
||||
mTabsToBeUpdated.add(tabId);
|
||||
if (mInProgressUpdates.size == 0) {
|
||||
const waitingUpdate = new Promise((resolve, _reject) => {
|
||||
const timer = setInterval(() => {
|
||||
if (mInProgressUpdates.size > 1)
|
||||
return;
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
waitingUpdate.catch(_error => {}).then(() => mInProgressUpdates.delete(waitingUpdate));
|
||||
mInProgressUpdates.add(waitingUpdate);
|
||||
}
|
||||
setTimeout(() => {
|
||||
const ids = Array.from(mTabsToBeUpdated);
|
||||
mTabsToBeUpdated.clear();
|
||||
for (const id of ids) {
|
||||
if (!id)
|
||||
continue;
|
||||
try {
|
||||
const promisedUpdate = updateInternal(id);
|
||||
const promisedUpdateWithTimeout = new Promise((resolve, reject) => {
|
||||
promisedUpdate.then(resolve).catch(reject);
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
mInProgressUpdates.add(promisedUpdateWithTimeout);
|
||||
promisedUpdateWithTimeout.catch(_error => {}).then(() => mInProgressUpdates.delete(promisedUpdateWithTimeout));
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
async function updateInternal(tabId, excludeTabIds = []) {
|
||||
// tabs.onActivated can be notified before the tab is completely tracked...
|
||||
await Tab.waitUntilTracked(tabId);
|
||||
const tab = Tab.get(tabId);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
const promisedUpdate = mPromisedUpdatedSuccessorTabId.get(tabId);
|
||||
await Promise.all([
|
||||
tab.$TST.opened,
|
||||
promisedUpdate?.promisedSuccessorTabId,
|
||||
]);
|
||||
|
||||
const renewedTab = await browser.tabs.get(tabId).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
if (!renewedTab ||
|
||||
!TabsStore.ensureLivingItem(tab))
|
||||
return;
|
||||
log('updateInternal: ', dumpTab(tab), {
|
||||
tabSuccessorTabId: tab.successorTabId,
|
||||
renewedSuccessorTabId: renewedTab.successorTabId,
|
||||
lastSuccessorTabIdByOwner: tab.$TST.temporaryMetadata.get('lastSuccessorTabIdByOwner'),
|
||||
lastSuccessorTabId: tab.$TST.temporaryMetadata.get('lastSuccessorTabId'),
|
||||
});
|
||||
if (tab.$TST.temporaryMetadata.has('lastSuccessorTabIdByOwner')) {
|
||||
log('respect last successor by owner');
|
||||
const successor = Tab.get(renewedTab.successorTabId);
|
||||
if (successor) {
|
||||
log(` ${dumpTab(tab)} is already prepared for "selectOwnerOnClose" behavior (successor=${renewedTab.successorTabId})`);
|
||||
return;
|
||||
}
|
||||
log(` clear successor of ${dumpTab(tab)}`);
|
||||
tab.$TST.temporaryMetadata.delete('lastSuccessorTabIdByOwner');
|
||||
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
|
||||
clearSuccessor(tab.id);
|
||||
}
|
||||
const lastSuccessorTab = tab.$TST.temporaryMetadata.has('lastSuccessorTabId') && Tab.get(tab.$TST.temporaryMetadata.get('lastSuccessorTabId'));
|
||||
if (!lastSuccessorTab) {
|
||||
log(` ${dumpTab(tab)}'s successor is missing: it was already closed.`);
|
||||
}
|
||||
else {
|
||||
log(` ${dumpTab(tab)} is under control: `, {
|
||||
successorTabId: renewedTab.successorTabId,
|
||||
lastSuccessorTabId: tab.$TST.temporaryMetadata.get('lastSuccessorTabId'),
|
||||
});
|
||||
if (renewedTab.successorTabId != -1 &&
|
||||
renewedTab.successorTabId != tab.$TST.temporaryMetadata.get('lastSuccessorTabId')) {
|
||||
log(` ${dumpTab(tab)}'s successor is modified by someone! Now it is out of control.`);
|
||||
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
|
||||
return;
|
||||
}
|
||||
}
|
||||
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
|
||||
if (configs.successorTabControlLevel == Constants.kSUCCESSOR_TAB_CONTROL_NEVER)
|
||||
return;
|
||||
let successor = null;
|
||||
if (renewedTab.active) {
|
||||
log('it is active, so reset successor');
|
||||
const excludeTabIdsSet = new Set(excludeTabIds);
|
||||
const findSuccessor = (...candidates) => {
|
||||
for (const candidate of candidates) {
|
||||
if (!excludeTabIdsSet.has(candidate?.id) &&
|
||||
candidate)
|
||||
return candidate;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
if (configs.successorTabControlLevel == Constants.kSUCCESSOR_TAB_CONTROL_IN_TREE) {
|
||||
const closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
|
||||
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
|
||||
parent: tab.$TST.parent,
|
||||
windowId: tab.windowId,
|
||||
});
|
||||
const collapsedChildSuccessorAllowed = (
|
||||
closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
|
||||
closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB
|
||||
);
|
||||
const firstChild = collapsedChildSuccessorAllowed ? tab.$TST.firstChild : tab.$TST.firstVisibleChild;
|
||||
const nextVisibleSibling = tab.$TST.nextVisibleSiblingTab;
|
||||
const nearestVisiblePreceding = tab.$TST.nearestVisiblePrecedingTab;
|
||||
successor = findSuccessor(
|
||||
firstChild,
|
||||
nextVisibleSibling,
|
||||
nearestVisiblePreceding
|
||||
);
|
||||
log(` possible successor: ${dumpTab(tab)}: `, successor, {
|
||||
closeParentBehavior,
|
||||
collapsedChildSuccessorAllowed,
|
||||
parent: tab.$TST.parentId,
|
||||
firstChild: firstChild?.id,
|
||||
nextVisibleSibling: nextVisibleSibling?.id,
|
||||
nearestVisiblePreceding: nearestVisiblePreceding?.id,
|
||||
});
|
||||
if (successor &&
|
||||
successor.discarded &&
|
||||
configs.avoidDiscardedTabToBeActivatedIfPossible) {
|
||||
log(` ${dumpTab(successor)} is discarded.`);
|
||||
successor = findSuccessor(
|
||||
tab.$TST.nearestLoadedSiblingTab,
|
||||
tab.$TST.nearestLoadedTabInTree,
|
||||
tab.$TST.nearestLoadedTab,
|
||||
successor
|
||||
);
|
||||
log(` => redirected successor is: ${dumpTab(successor)}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
successor = findSuccessor(
|
||||
tab.$TST.nearestVisibleFollowingTab,
|
||||
tab.$TST.nearestVisiblePrecedingTab
|
||||
);
|
||||
log(` possible successor: ${dumpTab(tab)}`);
|
||||
if (successor &&
|
||||
successor.discarded &&
|
||||
configs.avoidDiscardedTabToBeActivatedIfPossible) {
|
||||
log(` ${dumpTab(successor)} is discarded.`);
|
||||
successor = findSuccessor(
|
||||
tab.$TST.nearestLoadedTab,
|
||||
successor
|
||||
);
|
||||
log(` => redirected successor is: ${dumpTab(successor)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (successor) {
|
||||
log(` ${dumpTab(tab)} is under control: successor = ${successor.id}`);
|
||||
setSuccessor(renewedTab.id, successor.id);
|
||||
tab.$TST.temporaryMetadata.set('lastSuccessorTabId', successor.id);
|
||||
}
|
||||
else {
|
||||
log(` ${dumpTab(tab)} is out of control.`, {
|
||||
active: renewedTab.active,
|
||||
});
|
||||
clearSuccessor(renewedTab.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryClearOwnerSuccessor(tab) {
|
||||
if (!tab ||
|
||||
!tab.$TST.temporaryMetadata.get('lastSuccessorTabIdByOwner'))
|
||||
return;
|
||||
tab.$TST.temporaryMetadata.delete('lastSuccessorTabIdByOwner');
|
||||
const renewedTab = await browser.tabs.get(tab.id).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
if (!renewedTab ||
|
||||
renewedTab.successorTabId != tab.$TST.temporaryMetadata.get('lastSuccessorTabId'))
|
||||
return;
|
||||
log(`${dumpTab(tab)} is unprepared for "selectOwnerOnClose" behavior`);
|
||||
tab.$TST.temporaryMetadata.delete('lastSuccessorTabId');
|
||||
clearSuccessor(tab.id);
|
||||
}
|
||||
|
||||
Tab.onActivated.addListener(async (newActiveTab, info = {}) => {
|
||||
update(newActiveTab.id);
|
||||
if (info.previousTabId) {
|
||||
const oldActiveTab = Tab.get(info.previousTabId);
|
||||
if (oldActiveTab) {
|
||||
await tryClearOwnerSuccessor(oldActiveTab);
|
||||
const lastRelatedTab = oldActiveTab.$TST.lastRelatedTab;
|
||||
const newRelatedTabsCount = oldActiveTab.$TST.newRelatedTabsCount;
|
||||
if (lastRelatedTab) {
|
||||
log(`clear lastRelatedTabs for the window ${info.windowId} by tabs.onActivated on ${newActiveTab.id}`);
|
||||
TabsStore.windows.get(info.windowId).clearLastRelatedTabs();
|
||||
if (lastRelatedTab.id != newActiveTab.id) {
|
||||
log(`non last-related-tab is activated: cancel "back to owner" behavior for ${lastRelatedTab.id}`);
|
||||
await tryClearOwnerSuccessor(lastRelatedTab);
|
||||
}
|
||||
}
|
||||
if (newRelatedTabsCount > 1) {
|
||||
log(`multiple related tabs were opened: cancel "back to owner" behavior for ${newActiveTab.id}`);
|
||||
await tryClearOwnerSuccessor(newActiveTab);
|
||||
}
|
||||
}
|
||||
update(info.previousTabId);
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onCreating.addListener((tab, info = {}) => {
|
||||
if (!info.activeTab)
|
||||
return;
|
||||
|
||||
const shouldControlSuccesor = (
|
||||
configs.successorTabControlLevel != Constants.kSUCCESSOR_TAB_CONTROL_NEVER &&
|
||||
configs.simulateSelectOwnerOnClose
|
||||
);
|
||||
|
||||
log(`Tab.onCreating: should control succesor of ${tab.id}: `, shouldControlSuccesor);
|
||||
if (shouldControlSuccesor) {
|
||||
// don't use await here, to prevent that other onCreating handlers are treated async.
|
||||
tryClearOwnerSuccessor(info.activeTab).then(() => {
|
||||
const ownerTabId = tab.openerTabId || tab.active ? info.activeTab.id : null
|
||||
if (!ownerTabId)
|
||||
return;
|
||||
|
||||
log(`${dumpTab(tab)} is prepared for "selectOwnerOnClose" behavior (successor=${ownerTabId})`);
|
||||
setSuccessor(tab.id, ownerTabId);
|
||||
tab.$TST.temporaryMetadata.set('lastSuccessorTabId', ownerTabId);
|
||||
tab.$TST.temporaryMetadata.set('lastSuccessorTabIdByOwner', true);
|
||||
|
||||
if (!tab.openerTabId)
|
||||
return;
|
||||
|
||||
const opener = Tab.get(tab.openerTabId);
|
||||
const lastRelatedTab = opener?.$TST.lastRelatedTab;
|
||||
log(`opener ${dumpTab(opener)}'s lastRelatedTab: ${dumpTab(lastRelatedTab)})`);
|
||||
if (lastRelatedTab) {
|
||||
log(' => clear successor');
|
||||
tryClearOwnerSuccessor(lastRelatedTab);
|
||||
}
|
||||
opener.$TST.lastRelatedTab = tab;
|
||||
});
|
||||
}
|
||||
else {
|
||||
const opener = Tab.get(tab.openerTabId);
|
||||
if (opener)
|
||||
opener.$TST.lastRelatedTab = tab;
|
||||
}
|
||||
});
|
||||
|
||||
function updateActiveTab(windowId) {
|
||||
const activeTab = Tab.getActiveTab(windowId);
|
||||
if (activeTab)
|
||||
update(activeTab.id);
|
||||
}
|
||||
|
||||
Tab.onCreated.addListener((tab, _info = {}) => {
|
||||
updateActiveTab(tab.windowId);
|
||||
});
|
||||
|
||||
Tab.onRemoving.addListener((tab, removeInfo = {}) => {
|
||||
if (removeInfo.isWindowClosing)
|
||||
return;
|
||||
|
||||
const lastRelatedTab = tab.$TST.lastRelatedTab;
|
||||
if (lastRelatedTab &&
|
||||
!lastRelatedTab.active)
|
||||
tryClearOwnerSuccessor(lastRelatedTab);
|
||||
});
|
||||
|
||||
Tab.onRemoved.addListener((tab, info = {}) => {
|
||||
updateActiveTab(info.windowId);
|
||||
|
||||
const win = TabsStore.windows.get(info.windowId);
|
||||
if (!win)
|
||||
return;
|
||||
log(`clear lastRelatedTabs for ${info.windowId} by tabs.onRemoved`);
|
||||
win.clearLastRelatedTabs();
|
||||
});
|
||||
|
||||
Tab.onMoved.addListener((tab, info = {}) => {
|
||||
updateActiveTab(tab.windowId);
|
||||
|
||||
if (!info.byInternalOperation) {
|
||||
log(`clear lastRelatedTabs for ${tab.windowId} by tabs.onMoved`);
|
||||
TabsStore.windows.get(info.windowId).clearLastRelatedTabs();
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onAttached.addListener((_tab, info = {}) => {
|
||||
updateActiveTab(info.newWindowId);
|
||||
});
|
||||
|
||||
Tab.onDetached.addListener((_tab, info = {}) => {
|
||||
updateActiveTab(info.oldWindowId);
|
||||
|
||||
const win = TabsStore.windows.get(info.oldWindowId);
|
||||
if (win) {
|
||||
log(`clear lastRelatedTabs for ${info.windowId} by tabs.onDetached`);
|
||||
win.clearLastRelatedTabs();
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onUpdated.addListener((tab, changeInfo = {}) => {
|
||||
if (!('discarded' in changeInfo))
|
||||
return;
|
||||
updateActiveTab(tab.windowId);
|
||||
});
|
||||
|
||||
Tab.onShown.addListener(tab => {
|
||||
updateActiveTab(tab.windowId);
|
||||
});
|
||||
|
||||
Tab.onHidden.addListener(tab => {
|
||||
updateActiveTab(tab.windowId);
|
||||
});
|
||||
|
||||
Tree.onAttached.addListener((child, { parent } = {}) => {
|
||||
updateActiveTab(child.windowId);
|
||||
|
||||
const lastRelatedTabId = parent.$TST.lastRelatedTabId;
|
||||
if (lastRelatedTabId &&
|
||||
child.$TST.previousSiblingTab &&
|
||||
lastRelatedTabId == child.$TST.previousSiblingTab.id)
|
||||
parent.$TST.lastRelatedTab = child;
|
||||
});
|
||||
|
||||
Tree.onDetached.addListener((child, _info = {}) => {
|
||||
updateActiveTab(child.windowId);
|
||||
});
|
||||
|
||||
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info = {}) => {
|
||||
updateActiveTab(tab.windowId);
|
||||
});
|
||||
|
||||
SidebarConnection.onConnected.addListener((windowId, _openCount) => {
|
||||
updateActiveTab(windowId);
|
||||
});
|
||||
|
||||
SidebarConnection.onDisconnected.addListener((windowId, _openCount) => {
|
||||
updateActiveTab(windowId);
|
||||
});
|
||||
|
||||
// for automated test
|
||||
browser.runtime.onMessage.addListener((message, _sender) => {
|
||||
if (!message || !message.type)
|
||||
return;
|
||||
|
||||
switch (message.type) {
|
||||
case Constants.kCOMMAND_WAIT_UNTIL_SUCCESSORS_UPDATED:
|
||||
return Promise.all([...mInProgressUpdates]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
configs,
|
||||
notify,
|
||||
wait,
|
||||
} from '/common/common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as ContextualIdentities from '/common/contextual-identities.js';
|
||||
import * as Sync from '/common/sync.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
Sync.onMessage.addListener(async message => {
|
||||
const data = message.data;
|
||||
if (data.type != Constants.kSYNC_DATA_TYPE_TABS ||
|
||||
!Array.isArray(data.tabs))
|
||||
return;
|
||||
|
||||
const multiple = data.tabs.length > 1 ? '_multiple' : '';
|
||||
notify({
|
||||
title: browser.i18n.getMessage(
|
||||
`receiveTabs_notification_title${multiple}`,
|
||||
[Sync.getDeviceName(message.from)]
|
||||
),
|
||||
message: browser.i18n.getMessage(
|
||||
`receiveTabs_notification_message${multiple}`,
|
||||
data.tabs.length > 1 ?
|
||||
[data.tabs[0].url, data.tabs.length, data.tabs.length - 1] :
|
||||
[data.tabs[0].url]
|
||||
),
|
||||
timeout: configs.syncReceivedTabsNotificationTimeout
|
||||
});
|
||||
|
||||
const windowId = TabsStore.getCurrentWindowId() || (await browser.windows.getCurrent()).id;
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
const initialIndex = win.tabs.size;
|
||||
win.toBeOpenedOrphanTabs += data.tabs.length;
|
||||
let index = 0;
|
||||
const tabs = [];
|
||||
for (const tab of data.tabs) {
|
||||
const createParams = {
|
||||
windowId,
|
||||
url: tab.url,
|
||||
index: initialIndex + index,
|
||||
active: index == 0
|
||||
};
|
||||
if (tab.cookieStoreId &&
|
||||
tab.cookieStoreId != 'firefox-default' &&
|
||||
ContextualIdentities.get(tab.cookieStoreId))
|
||||
createParams.cookieStoreId = tab.cookieStoreId;
|
||||
let openedTab;
|
||||
try {
|
||||
openedTab = await browser.tabs.create(createParams);
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error);
|
||||
}
|
||||
if (!openedTab)
|
||||
openedTab = await browser.tabs.create({
|
||||
...createParams,
|
||||
url: `about:blank?${tab.url}`
|
||||
});
|
||||
tabs.push(openedTab);
|
||||
index++;
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.structure))
|
||||
return;
|
||||
|
||||
await wait(100); // wait for all tabs are tracked
|
||||
await Tree.applyTreeStructureToTabs(tabs.map(tab => Tab.get(tab.id)), data.structure, {
|
||||
broadcast: true
|
||||
});
|
||||
});
|
||||
1860
waterfox/browser/components/sidebar/background/tab-context-menu.js
Normal file
1860
waterfox/browser/components/sidebar/background/tab-context-menu.js
Normal file
File diff suppressed because it is too large
Load Diff
662
waterfox/browser/components/sidebar/background/tabs-group.js
Normal file
662
waterfox/browser/components/sidebar/background/tabs-group.js
Normal file
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs,
|
||||
wait,
|
||||
countMatched,
|
||||
dumpTab
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
||||
import * as TabsUpdate from '/common/tabs-update.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as TabsMove from './tabs-move.js';
|
||||
import * as TabsOpen from './tabs-open.js';
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/tabs-group', ...args);
|
||||
}
|
||||
|
||||
export function makeGroupTabURI({ title, temporary, temporaryAggressive, openerTabId, aliasTabId, replacedParentCount } = {}) {
|
||||
const url = new URL(Constants.kGROUP_TAB_URI);
|
||||
|
||||
if (title)
|
||||
url.searchParams.set('title', title);
|
||||
|
||||
if (temporaryAggressive)
|
||||
url.searchParams.set('temporaryAggressive', 'true');
|
||||
else if (temporary)
|
||||
url.searchParams.set('temporary', 'true');
|
||||
|
||||
if (openerTabId)
|
||||
url.searchParams.set('openerTabId', openerTabId);
|
||||
|
||||
if (aliasTabId)
|
||||
url.searchParams.set('aliasTabId', aliasTabId);
|
||||
|
||||
if (replacedParentCount)
|
||||
url.searchParams.set('replacedParentCount', replacedParentCount);
|
||||
|
||||
return url.href;
|
||||
}
|
||||
|
||||
export function temporaryStateParams(state) {
|
||||
switch (state) {
|
||||
case Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE:
|
||||
return {
|
||||
temporary: true,
|
||||
temporaryAggressive: false,
|
||||
};
|
||||
case Constants.kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE:
|
||||
return {
|
||||
temporary: false,
|
||||
temporaryAggressive: true,
|
||||
};
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
temporary: false,
|
||||
temporaryAggressive: false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...groupTabOptions } = {}) {
|
||||
const rootTabs = Tab.collectRootTabs(tabs);
|
||||
if (rootTabs.length <= 0)
|
||||
return null;
|
||||
|
||||
log('groupTabs: ', () => tabs.map(dumpTab), { broadcast, parent, withDescendants });
|
||||
|
||||
const uri = makeGroupTabURI({
|
||||
title: browser.i18n.getMessage('groupTab_label', rootTabs[0].title),
|
||||
temporary: true,
|
||||
...groupTabOptions
|
||||
});
|
||||
const groupTab = await TabsOpen.openURIInTab(uri, {
|
||||
windowId: rootTabs[0].windowId,
|
||||
parent: parent || rootTabs[0].$TST.parent,
|
||||
insertBefore: rootTabs[0],
|
||||
inBackground: true
|
||||
});
|
||||
|
||||
if (!withDescendants) {
|
||||
const structure = TreeBehavior.getTreeStructureFromTabs(tabs);
|
||||
|
||||
await Tree.detachTabsFromTree(tabs, {
|
||||
broadcast: !!broadcast,
|
||||
});
|
||||
|
||||
log('structure: ', structure);
|
||||
await Tree.applyTreeStructureToTabs(tabs, structure, {
|
||||
broadcast: !!broadcast,
|
||||
});
|
||||
}
|
||||
|
||||
await TabsMove.moveTabsAfter(tabs.slice(1), tabs[0], {
|
||||
broadcast: !!broadcast
|
||||
});
|
||||
for (const tab of rootTabs) {
|
||||
await Tree.attachTabTo(tab, groupTab, {
|
||||
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
|
||||
dontMove: true,
|
||||
broadcast: !!broadcast,
|
||||
});
|
||||
}
|
||||
return groupTab;
|
||||
}
|
||||
|
||||
function reserveToCleanupNeedlessGroupTab(tabOrTabs) {
|
||||
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
|
||||
for (const tab of tabs) {
|
||||
if (!TabsStore.ensureLivingItem(tab))
|
||||
continue;
|
||||
if (tab.$TST.temporaryMetadata.has('reservedCleanupNeedlessGroupTab'))
|
||||
clearTimeout(tab.$TST.temporaryMetadata.get('reservedCleanupNeedlessGroupTab'));
|
||||
tab.$TST.temporaryMetadata.set('reservedCleanupNeedlessGroupTab', setTimeout(() => {
|
||||
if (!tab.$TST)
|
||||
return;
|
||||
tab.$TST.temporaryMetadata.delete('reservedCleanupNeedlessGroupTab');
|
||||
cleanupNeedlssGroupTab(tab);
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupNeedlssGroupTab(tabs) {
|
||||
if (!Array.isArray(tabs))
|
||||
tabs = [tabs];
|
||||
log('trying to clanup needless temporary group tabs from ', () => tabs.map(dumpTab));
|
||||
const tabsToBeRemoved = [];
|
||||
for (const tab of tabs) {
|
||||
if (tab.$TST.temporaryMetadata.has('movingAcrossWindows'))
|
||||
continue;
|
||||
|
||||
if (tab.$TST.isTemporaryGroupTab) {
|
||||
if (tab.$TST.childIds.length > 1)
|
||||
break;
|
||||
const lastChild = tab.$TST.firstChild;
|
||||
if (lastChild &&
|
||||
!lastChild.$TST.isTemporaryGroupTab &&
|
||||
!lastChild.$TST.isTemporaryAggressiveGroupTab)
|
||||
break;
|
||||
}
|
||||
else if (tab.$TST.isTemporaryAggressiveGroupTab) {
|
||||
if (tab.$TST.childIds.length > 1)
|
||||
break;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
tabsToBeRemoved.push(tab);
|
||||
}
|
||||
log('=> to be removed: ', () => tabsToBeRemoved.map(dumpTab));
|
||||
TabsInternalOperation.removeTabs(tabsToBeRemoved, { keepDescendants: true });
|
||||
}
|
||||
|
||||
export async function tryReplaceTabWithGroup(tab, { windowId, parent, children, insertBefore, newParent } = {}) {
|
||||
if (tab) {
|
||||
windowId = tab.windowId;
|
||||
parent = tab.$TST.parent;
|
||||
children = tab.$TST.children;
|
||||
insertBefore = insertBefore || tab.$TST.unsafeNextTab;
|
||||
}
|
||||
|
||||
if (children.length <= 1 ||
|
||||
countMatched(children,
|
||||
tab => !tab.$TST.states.has(Constants.kTAB_STATE_TO_BE_REMOVED)) <= 1)
|
||||
return null;
|
||||
|
||||
log('trying to replace the closing tab with a new group tab');
|
||||
|
||||
const firstChild = children[0];
|
||||
const uri = makeGroupTabURI({
|
||||
title: browser.i18n.getMessage('groupTab_label', firstChild.title),
|
||||
...temporaryStateParams(configs.groupTabTemporaryStateForOrphanedTabs),
|
||||
replacedParentCount: (tab?.$TST?.replacedParentGroupTabCount || 0) + 1,
|
||||
});
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
win.toBeOpenedTabsWithPositions++;
|
||||
const groupTab = await TabsOpen.openURIInTab(uri, {
|
||||
windowId,
|
||||
insertBefore,
|
||||
inBackground: true
|
||||
});
|
||||
log('group tab: ', dumpTab(groupTab));
|
||||
if (!groupTab) // the window is closed!
|
||||
return;
|
||||
if (newParent || parent)
|
||||
await Tree.attachTabTo(groupTab, newParent || parent, {
|
||||
dontMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
for (const child of children) {
|
||||
await Tree.attachTabTo(child, groupTab, {
|
||||
dontMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
|
||||
// This can be triggered on closing of multiple tabs,
|
||||
// so we should cleanup it on such cases for safety.
|
||||
// https://github.com/piroor/treestyletab/issues/2317
|
||||
wait(1000).then(() => reserveToCleanupNeedlessGroupTab(groupTab));
|
||||
|
||||
return groupTab;
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// init/update group tabs
|
||||
// ====================================================================
|
||||
|
||||
/*
|
||||
To prevent the tab is closed by Firefox, we need to inject scripts dynamically.
|
||||
See also: https://github.com/piroor/treestyletab/issues/1670#issuecomment-350964087
|
||||
*/
|
||||
async function tryInitGroupTab(tab) {
|
||||
if (!tab.$TST.isGroupTab &&
|
||||
!tab.$TST.hasGroupTabURL)
|
||||
return;
|
||||
log('tryInitGroupTab ', tab);
|
||||
const v3Options = {
|
||||
target: { tabId: tab.id },
|
||||
};
|
||||
const v2Options = {
|
||||
runAt: 'document_start',
|
||||
matchAboutBlank: true
|
||||
};
|
||||
try {
|
||||
const getPageState = function getPageState() {
|
||||
return [window.prepared, document.documentElement.matches('.initialized')];
|
||||
};
|
||||
const [prepared, initialized, reloaded] = (browser.scripting ?
|
||||
browser.scripting.executeScript({ // Manifest V3
|
||||
...v3Options,
|
||||
func: getPageState,
|
||||
}).then(results => results && results[0] && results[0].result || []) :
|
||||
browser.tabs.executeScript(tab.id, {
|
||||
...v2Options,
|
||||
code: `(${getPageState.toString()})()`,
|
||||
}).then(results => results && results[0] || [])
|
||||
).catch(error => {
|
||||
if (ApiTabs.isMissingHostPermissionError(error) &&
|
||||
tab.$TST.hasGroupTabURL) {
|
||||
log(' tryInitGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', tab.id);
|
||||
browser.tabs.reload(tab.id);
|
||||
return [[false, false, true]];
|
||||
}
|
||||
return ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError)(error);
|
||||
});
|
||||
log(' tryInitGroupTab: groupt tab state ', tab.id, { prepared, initialized, reloaded });
|
||||
if (reloaded) {
|
||||
log(' => reloaded ', tab.id);
|
||||
return;
|
||||
}
|
||||
if (prepared && initialized) {
|
||||
log(' => already initialized ', tab.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
log(' tryInitGroupTab: error while checking initialized: ', tab.id, error);
|
||||
}
|
||||
try {
|
||||
const getTitleExistence = function getState() {
|
||||
return !!document.querySelector('#title');
|
||||
};
|
||||
const titleElementExists = (browser.scripting ?
|
||||
browser.scripting.executeScript({ // Manifest V3
|
||||
...v3Options,
|
||||
func: getTitleExistence,
|
||||
}).then(results => results && results[0] && results[0].result) :
|
||||
browser.tabs.executeScript(tab.id, {
|
||||
...v2Options,
|
||||
code: `(${getTitleExistence.toString()})()`,
|
||||
}).then(results => results && results[0])
|
||||
).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
|
||||
if (!titleElementExists && tab.status == 'complete') { // we need to load resources/group-tab.html at first.
|
||||
log(' => title element exists, load again ', tab.id);
|
||||
return browser.tabs.update(tab.id, { url: tab.url }).catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
log(' tryInitGroupTab error while checking title element: ', tab.id, error);
|
||||
}
|
||||
|
||||
(browser.scripting ?
|
||||
browser.scripting.executeScript({ // Manifest V3
|
||||
...v3Options,
|
||||
files: [
|
||||
'/extlib/l10n-classic.js', // ES module does not supported as a content script...
|
||||
'/resources/group-tab.js',
|
||||
],
|
||||
}) :
|
||||
Promise.all([
|
||||
browser.tabs.executeScript(tab.id, {
|
||||
...v2Options,
|
||||
//file: '/common/l10n.js'
|
||||
file: '/extlib/l10n-classic.js', // ES module does not supported as a content script...
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
|
||||
browser.tabs.executeScript(tab.id, {
|
||||
...v2Options,
|
||||
file: '/resources/group-tab.js',
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
|
||||
])
|
||||
).then(() => {
|
||||
log('tryInitGroupTab completely initialized: ', tab.id);
|
||||
});
|
||||
|
||||
if (tab.$TST.states.has(Constants.kTAB_STATE_UNREAD)) {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TAB_UPDATED,
|
||||
windowId: tab.windowId,
|
||||
tabId: tab.id,
|
||||
removedStates: [Constants.kTAB_STATE_UNREAD]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function reserveToUpdateRelatedGroupTabs(tab, changedInfo) {
|
||||
const tabMetadata = tab.$TST.temporaryMetadata;
|
||||
const updatingTabs = tabMetadata.get('reserveToUpdateRelatedGroupTabsUpdatingTabs') || new Set();
|
||||
if (!tabMetadata.has('reserveToUpdateRelatedGroupTabsUpdatingTabs'))
|
||||
tabMetadata.set('reserveToUpdateRelatedGroupTabsUpdatingTabs', updatingTabs);
|
||||
|
||||
const ancestorGroupTabs = [
|
||||
tab,
|
||||
tab.$TST.bundledTab,
|
||||
...tab.$TST.ancestors,
|
||||
...tab.$TST.ancestors.map(tab => tab.$TST.bundledTab),
|
||||
].filter(tab => tab?.$TST.isGroupTab);
|
||||
for (const updatingTab of ancestorGroupTabs) {
|
||||
const updatingMetadata = updatingTab.$TST.temporaryMetadata;
|
||||
const reservedChangedInfo = updatingMetadata.get('reservedUpdateRelatedGroupTabChangedInfo') || new Set();
|
||||
for (const info of changedInfo) {
|
||||
reservedChangedInfo.add(info);
|
||||
}
|
||||
if (updatingTabs.has(updatingTab.id))
|
||||
continue;
|
||||
updatingTabs.add(updatingTab.id);
|
||||
const triggeredUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates') || new Set();
|
||||
triggeredUpdates.add(updatingTabs);
|
||||
updatingMetadata.set('reservedUpdateRelatedGroupTabTriggeredUpdates', triggeredUpdates);
|
||||
if (updatingMetadata.has('reservedUpdateRelatedGroupTab'))
|
||||
clearTimeout(updatingMetadata.get('reservedUpdateRelatedGroupTab'));
|
||||
updatingMetadata.set('reservedUpdateRelatedGroupTabChangedInfo', reservedChangedInfo);
|
||||
updatingMetadata.set('reservedUpdateRelatedGroupTab', setTimeout(() => {
|
||||
updatingMetadata.delete('reservedUpdateRelatedGroupTab');
|
||||
if (updatingTab.$TST) {
|
||||
try {
|
||||
if (reservedChangedInfo.size > 0)
|
||||
updateRelatedGroupTab(updatingTab, [...reservedChangedInfo]);
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
updatingMetadata.delete('reservedUpdateRelatedGroupTabChangedInfo');
|
||||
}
|
||||
setTimeout(() => {
|
||||
const triggerUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates')
|
||||
updatingMetadata.delete('reservedUpdateRelatedGroupTabTriggeredUpdates');
|
||||
if (!triggerUpdates)
|
||||
return;
|
||||
for (const updatingTabs of triggerUpdates) {
|
||||
updatingTabs.delete(updatingTab.id);
|
||||
}
|
||||
}, 100)
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRelatedGroupTab(groupTab, changedInfo = []) {
|
||||
if (!TabsStore.ensureLivingItem(groupTab))
|
||||
return;
|
||||
|
||||
await tryInitGroupTab(groupTab);
|
||||
if (changedInfo.includes('tree')) {
|
||||
try {
|
||||
await browser.tabs.sendMessage(groupTab.id, {
|
||||
type: 'ws:update-tree',
|
||||
}).catch(error => {
|
||||
if (ApiTabs.isMissingHostPermissionError(error))
|
||||
throw error;
|
||||
return ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleUnloadedError)(error);
|
||||
});
|
||||
}
|
||||
catch(error) {
|
||||
if (ApiTabs.isMissingHostPermissionError(error)) {
|
||||
log(' updateRelatedGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', groupTab.id);
|
||||
browser.tabs.reload(groupTab.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const firstChild = groupTab.$TST.firstChild;
|
||||
if (!firstChild) // the tab can be closed while waiting...
|
||||
return;
|
||||
|
||||
if (changedInfo.includes('title')) {
|
||||
let newTitle;
|
||||
if (Constants.kGROUP_TAB_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
|
||||
newTitle = browser.i18n.getMessage('groupTab_label', firstChild.title);
|
||||
}
|
||||
else if (Constants.kGROUP_TAB_FROM_PINNED_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
|
||||
const opener = groupTab.$TST.openerTab;
|
||||
if (opener) {
|
||||
if (opener &&
|
||||
opener.favIconUrl) {
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED,
|
||||
windowId: groupTab.windowId,
|
||||
tabId: groupTab.id,
|
||||
favIconUrl: opener.favIconUrl
|
||||
});
|
||||
}
|
||||
newTitle = browser.i18n.getMessage('groupTab_fromPinnedTab_label', opener.title);
|
||||
}
|
||||
}
|
||||
|
||||
if (newTitle && groupTab.title != newTitle) {
|
||||
browser.tabs.sendMessage(groupTab.id, {
|
||||
type: 'ws:update-title',
|
||||
title: newTitle,
|
||||
}).catch(ApiTabs.createErrorHandler(
|
||||
ApiTabs.handleMissingTabError,
|
||||
ApiTabs.handleMissingHostPermissionError,
|
||||
_error => {
|
||||
// failed to update the title by group tab itself, so we try to update it from outside
|
||||
groupTab.title = newTitle;
|
||||
TabsUpdate.updateTab(groupTab, { title: newTitle });
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tab.onRemoved.addListener((tab, _closeInfo = {}) => {
|
||||
const ancestors = tab.$TST.ancestors;
|
||||
wait(0).then(() => {
|
||||
reserveToCleanupNeedlessGroupTab(ancestors);
|
||||
});
|
||||
});
|
||||
|
||||
Tab.onUpdated.addListener((tab, changeInfo) => {
|
||||
if ('url' in changeInfo ||
|
||||
'previousUrl' in changeInfo ||
|
||||
'state' in changeInfo) {
|
||||
const status = changeInfo.status || tab?.status;
|
||||
const url = changeInfo.url ? changeInfo.url :
|
||||
status == 'complete' && tab ? tab.url : '';
|
||||
if (tab &&
|
||||
status == 'complete') {
|
||||
if (url.indexOf(Constants.kGROUP_TAB_URI) == 0) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
||||
}
|
||||
else if (!Constants.kSHORTHAND_ABOUT_URI.test(url)) {
|
||||
tab.$TST.getPermanentStates().then(async (states) => {
|
||||
if (url.indexOf(Constants.kGROUP_TAB_URI) == 0)
|
||||
return;
|
||||
// Detect group tab from different session - which can have different UUID for the URL.
|
||||
const PREFIX_REMOVER = /^moz-extension:\/\/[^\/]+/;
|
||||
const pathPart = url.replace(PREFIX_REMOVER, '');
|
||||
if (states.includes(Constants.kTAB_STATE_GROUP_TAB) &&
|
||||
pathPart.split('?')[0] == Constants.kGROUP_TAB_URI.replace(PREFIX_REMOVER, '')) {
|
||||
const parameters = pathPart.replace(/^[^\?]+\?/, '');
|
||||
const oldUrl = tab.url;
|
||||
await wait(100); // for safety
|
||||
if (tab.url != oldUrl)
|
||||
return;
|
||||
browser.tabs.update(tab.id, {
|
||||
url: `${Constants.kGROUP_TAB_URI}?${parameters}`
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB);
|
||||
}
|
||||
else {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// restored tab can be replaced with blank tab. we need to restore it manually.
|
||||
else if (changeInfo.url == 'about:blank' &&
|
||||
changeInfo.previousUrl &&
|
||||
changeInfo.previousUrl.indexOf(Constants.kGROUP_TAB_URI) == 0) {
|
||||
const oldUrl = tab.url;
|
||||
wait(100).then(() => { // redirect with delay to avoid infinite loop of recursive redirections.
|
||||
if (tab.url != oldUrl)
|
||||
return;
|
||||
browser.tabs.update(tab.id, {
|
||||
url: changeInfo.previousUrl
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (changeInfo.status ||
|
||||
changeInfo.url ||
|
||||
url.indexOf(Constants.kGROUP_TAB_URI) == 0)
|
||||
tryInitGroupTab(tab);
|
||||
}
|
||||
|
||||
if ('title' in changeInfo) {
|
||||
const group = Tab.getGroupTabForOpener(tab);
|
||||
if (group)
|
||||
reserveToUpdateRelatedGroupTabs(group, ['title', 'tree']);
|
||||
}
|
||||
});
|
||||
|
||||
Tab.onGroupTabDetected.addListener(tab => {
|
||||
tryInitGroupTab(tab);
|
||||
});
|
||||
|
||||
Tab.onLabelUpdated.addListener(tab => {
|
||||
reserveToUpdateRelatedGroupTabs(tab, ['title', 'tree']);
|
||||
});
|
||||
|
||||
Tab.onActivating.addListener((tab, _info = {}) => {
|
||||
tryInitGroupTab(tab);
|
||||
});
|
||||
|
||||
// returns a boolean: need to reload or not.
|
||||
export async function clearTemporaryState(tab) {
|
||||
if (!tab.$TST.isTemporaryGroupTab &&
|
||||
!tab.$TST.isTemporaryAggressiveGroupTab)
|
||||
return;
|
||||
|
||||
const url = new URL(tab.url);
|
||||
url.searchParams.delete('temporary');
|
||||
url.searchParams.delete('temporaryAggressive');
|
||||
await Promise.all([
|
||||
browser.tabs.sendMessage(tab.id, {
|
||||
type: 'ws:clear-temporary-state',
|
||||
}).catch(ApiTabs.createErrorHandler()),
|
||||
browser.tabs.executeScript(tab.id, { // failsafe
|
||||
runAt: 'document_start',
|
||||
code: `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
|
||||
}).catch(ApiTabs.createErrorHandler()),
|
||||
]);
|
||||
tab.url = url.href;
|
||||
}
|
||||
|
||||
Tab.onPinned.addListener(async tab => {
|
||||
log('handlePinnedParentTab ', tab);
|
||||
|
||||
await Tree.collapseExpandSubtree(tab, {
|
||||
collapsed: false,
|
||||
broadcast: true
|
||||
});
|
||||
|
||||
log(' childIdsBeforeMoved: ', tab.$TST.temporaryMetadata.get('childIdsBeforeMoved'));
|
||||
log(' parentIdBeforeMoved: ', tab.$TST.temporaryMetadata.get('parentIdBeforeMoved'));
|
||||
|
||||
const children = (
|
||||
tab.$TST.temporaryMetadata.has('childIdsBeforeMoved') ?
|
||||
tab.$TST.temporaryMetadata.get('childIdsBeforeMoved').map(id => Tab.get(id)) :
|
||||
tab.$TST.children
|
||||
).filter(tab => TabsStore.ensureLivingItem(tab));
|
||||
const parent = TabsStore.ensureLivingItem(
|
||||
tab.$TST.temporaryMetadata.has('parentIdBeforeMoved') ?
|
||||
Tab.get(tab.$TST.temporaryMetadata.get('parentIdBeforeMoved')) :
|
||||
tab.$TST.parent
|
||||
);
|
||||
|
||||
let openedGroupTab;
|
||||
const shouldGroupChildren = configs.autoGroupNewTabsFromPinned || tab.$TST.isGroupTab;
|
||||
if (shouldGroupChildren) {
|
||||
log(' => trying to group left tabs with a group: ', children);
|
||||
openedGroupTab = await groupTabs(children, {
|
||||
// If the tab is a group tab, the opened tab should be treated as an alias of the pinned group tab.
|
||||
// Otherwise it should be treated just as a temporary group tab to group children.
|
||||
title: tab.$TST.isGroupTab ? tab.title : browser.i18n.getMessage('groupTab_fromPinnedTab_label', tab.title),
|
||||
temporary: !tab.$TST.isGroupTab,
|
||||
openerTabId: tab.$TST.uniqueId.id,
|
||||
parent,
|
||||
withDescendants: true,
|
||||
});
|
||||
log(' openedGroupTab: ', openedGroupTab);
|
||||
// Tree structure of left tabs can be modified by someone like tryFixupTreeForInsertedTab@handle-moved-tabs.js.
|
||||
// On such cases we need to restore the original tree structure.
|
||||
const modifiedChildren = children.filter(child => children.includes(child.$TST.parent));
|
||||
log(' modifiedChildren: ', modifiedChildren);
|
||||
if (modifiedChildren.length > 0) {
|
||||
for (const child of modifiedChildren) {
|
||||
await Tree.detachTab(child, {
|
||||
broadcast: true,
|
||||
});
|
||||
await Tree.attachTabTo(child, openedGroupTab, {
|
||||
dontMove: true,
|
||||
broadcast: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log(' => no need to group left tabs, just detaching');
|
||||
await Tree.detachAllChildren(tab, {
|
||||
behavior: TreeBehavior.getParentTabOperationBehavior(tab, {
|
||||
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
|
||||
preventEntireTreeBehavior: true,
|
||||
}),
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
await Tree.detachTab(tab, {
|
||||
broadcast: true
|
||||
});
|
||||
|
||||
// Such a group tab will be closed automatically when all children are detached.
|
||||
// To prevent the auto close behavior, the tab type need to be turned to permanent.
|
||||
await clearTemporaryState(tab);
|
||||
|
||||
if (tab.$TST.isGroupTab && openedGroupTab) {
|
||||
const url = new URL(tab.url);
|
||||
url.searchParams.set('aliasTabId', openedGroupTab.$TST.uniqueId.id);
|
||||
await Promise.all([
|
||||
browser.tabs.sendMessage(tab.id, {
|
||||
type: 'ws:replace-state-url',
|
||||
url: url.href,
|
||||
}).catch(ApiTabs.createErrorHandler()),
|
||||
browser.tabs.executeScript(tab.id, { // failsafe
|
||||
runAt: 'document_start',
|
||||
code: `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
|
||||
}).catch(ApiTabs.createErrorHandler()),
|
||||
]);
|
||||
await browser.tabs.sendMessage(tab.id, {
|
||||
type: 'ws:update-tree',
|
||||
url: url.href,
|
||||
}).catch(ApiTabs.createErrorHandler());
|
||||
tab.url = url.href;
|
||||
}
|
||||
});
|
||||
|
||||
Tree.onAttached.addListener((tab, _info = {}) => {
|
||||
reserveToUpdateRelatedGroupTabs(tab, ['tree']);
|
||||
});
|
||||
|
||||
Tree.onDetached.addListener((_tab, detachInfo) => {
|
||||
if (!detachInfo.oldParentTab)
|
||||
return;
|
||||
if (detachInfo.oldParentTab.$TST.isGroupTab)
|
||||
reserveToCleanupNeedlessGroupTab(detachInfo.oldParentTab);
|
||||
reserveToUpdateRelatedGroupTabs(detachInfo.oldParentTab, ['tree']);
|
||||
});
|
||||
|
||||
/*
|
||||
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info) => {
|
||||
reserveToUpdateRelatedGroupTabs(tab);
|
||||
});
|
||||
*/
|
||||
465
waterfox/browser/components/sidebar/background/tabs-move.js
Normal file
465
waterfox/browser/components/sidebar/background/tabs-move.js
Normal file
@@ -0,0 +1,465 @@
|
||||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is the Tree Style Tab.
|
||||
*
|
||||
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011-2025
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
|
||||
* wanabe <https://github.com/wanabe>
|
||||
* Tetsuharu OHZEKI <https://github.com/saneyuki>
|
||||
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
|
||||
* lv7777 (https://github.com/lv7777)
|
||||
*
|
||||
* ***** END LICENSE BLOCK ******/
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
wait,
|
||||
toLines,
|
||||
configs
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import { SequenceMatcher } from '/extlib/diff.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
|
||||
import { Tab, TreeItem } from '/common/TreeItem.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/tabs-move', ...args);
|
||||
}
|
||||
function logApiTabs(...args) {
|
||||
internalLogger('common/api-tabs', ...args);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================
|
||||
// primitive methods for internal use
|
||||
|
||||
export async function moveTabsBefore(tabs, referenceTab, options = {}) {
|
||||
log('moveTabsBefore: ', tabs, referenceTab, options);
|
||||
if (!tabs.length ||
|
||||
!TabsStore.ensureLivingItem(referenceTab))
|
||||
return [];
|
||||
|
||||
if (referenceTab.$TST.isAllPlacedBeforeSelf(tabs)) {
|
||||
log('moveTabsBefore:no need to move');
|
||||
return [];
|
||||
}
|
||||
return moveTabsInternallyBefore(tabs, referenceTab, options);
|
||||
}
|
||||
export async function moveTabBefore(tab, referenceTab, options = {}) {
|
||||
return moveTabsBefore([tab], referenceTab, options).then(moved => moved.length > 0);
|
||||
}
|
||||
|
||||
async function moveTabsInternallyBefore(tabs, referenceTab, options = {}) {
|
||||
if (!tabs.length ||
|
||||
!TabsStore.ensureLivingItem(referenceTab))
|
||||
return [];
|
||||
|
||||
const win = TabsStore.windows.get(tabs[0].windowId);
|
||||
|
||||
log('moveTabsInternallyBefore: ', tabs, `${referenceTab.id}(index=${referenceTab.index})`, options);
|
||||
if (referenceTab.type == TreeItem.TYPE_GROUP) {
|
||||
referenceTab = referenceTab.$TST?.firstMember;
|
||||
if (!TabsStore.ensureLivingItem(referenceTab)) {
|
||||
log('missing reference tab');
|
||||
return [];
|
||||
}
|
||||
log(` => reference tab: ${referenceTab.id}(index=${referenceTab.index})`);
|
||||
}
|
||||
|
||||
const precedingReferenceTab = referenceTab.$TST.previousTab;
|
||||
if (referenceTab.pinned) {
|
||||
// unpinned tab cannot be moved before any pinned tab
|
||||
tabs = tabs.filter(tab => tab.pinned);
|
||||
}
|
||||
else if (precedingReferenceTab &&
|
||||
!precedingReferenceTab.pinned) {
|
||||
// pinned tab cannot be moved after any unpinned tab
|
||||
tabs = tabs.filter(tab => !tab.pinned);
|
||||
}
|
||||
if (!tabs.length)
|
||||
return [];
|
||||
|
||||
const movedTabs = [];
|
||||
try {
|
||||
/*
|
||||
Tab elements are moved by tabs.onMoved automatically, but
|
||||
the operation is asynchronous. To help synchronous operations
|
||||
following to this operation, we need to move tabs immediately.
|
||||
*/
|
||||
const tabGroups = new Set();
|
||||
for (const tab of tabs) {
|
||||
const oldPreviousTab = tab.$TST.unsafePreviousTab;
|
||||
const oldNextTab = tab.$TST.unsafeNextTab;
|
||||
if (oldNextTab?.id == referenceTab.id) // no move case
|
||||
continue;
|
||||
const fromIndex = tab.index;
|
||||
if (referenceTab.index > tab.index)
|
||||
tab.index = referenceTab.index - 1;
|
||||
else
|
||||
tab.index = referenceTab.index;
|
||||
tabGroups.add(tab.$TST.nativeTabGroup);
|
||||
if (SidebarConnection.isInitialized()) { // only on the background page
|
||||
win.internalMovingTabs.set(tab.id, tab.index);
|
||||
win.alreadyMovedTabs.set(tab.id, tab.index);
|
||||
}
|
||||
tab.reindexedBy = `moveTabsInternallyBefore (${tab.index})`;
|
||||
Tab.track(tab);
|
||||
movedTabs.push(tab);
|
||||
Tab.onTabInternallyMoved.dispatch(tab, {
|
||||
nextTab: referenceTab,
|
||||
oldPreviousTab,
|
||||
oldNextTab,
|
||||
broadcasted: !!options.broadcasted
|
||||
});
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TAB_INTERNALLY_MOVED,
|
||||
windowId: tab.windowId,
|
||||
tabId: tab.id,
|
||||
fromIndex,
|
||||
toIndex: tab.index,
|
||||
nextTabId: referenceTab?.id,
|
||||
broadcasted: !!options.broadcasted
|
||||
});
|
||||
if (options.doNotOptimize) {
|
||||
win.internalMovingTabs.set(tab.id, tab.index);
|
||||
win.alreadyMovedTabs.set(tab.id, tab.index);
|
||||
await browser.tabs.move(tab.id, { index: tab.index });
|
||||
win.internalMovingTabs.delete(tab.id);
|
||||
win.alreadyMovedTabs.delete(tab.id);
|
||||
}
|
||||
}
|
||||
for (const group of tabGroups) {
|
||||
group?.$TST.reindex();
|
||||
}
|
||||
if (movedTabs.length == 0) {
|
||||
log(' => actually nothing moved');
|
||||
}
|
||||
else {
|
||||
log(
|
||||
'Tab nodes rearranged by moveTabsInternallyBefore:\n',
|
||||
(!configs.debug ? '' :
|
||||
() => toLines(Array.from(win.getOrderedTabs()),
|
||||
tab => ` - ${tab.index}: ${tab.id}${tabs.includes(tab) ? '[MOVED]' : ''}`))
|
||||
);
|
||||
}
|
||||
if (SidebarConnection.isInitialized()) { // only on the background page
|
||||
if (options.delayedMove) { // Wait until opening animation is finished.
|
||||
await wait(configs.newTabAnimationDuration);
|
||||
}
|
||||
if (!options.doNotOptimize) {
|
||||
syncToNativeTabs(tabs);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
ApiTabs.handleMissingTabError(e);
|
||||
log('moveTabsInternallyBefore failed: ', String(e));
|
||||
}
|
||||
return movedTabs;
|
||||
}
|
||||
export async function moveTabInternallyBefore(tab, referenceTab, options = {}) {
|
||||
return moveTabsInternallyBefore([tab], referenceTab, options);
|
||||
}
|
||||
|
||||
export async function moveTabsAfter(tabs, referenceTab, options = {}) {
|
||||
log('moveTabsAfter: ', tabs, referenceTab, options);
|
||||
if (!tabs.length ||
|
||||
!TabsStore.ensureLivingItem(referenceTab))
|
||||
return [];
|
||||
|
||||
if (referenceTab.$TST.isAllPlacedAfterSelf(tabs)) {
|
||||
log('moveTabsAfter:no need to move');
|
||||
return [];
|
||||
}
|
||||
return moveTabsInternallyAfter(tabs, referenceTab, options);
|
||||
}
|
||||
export async function moveTabAfter(tab, referenceTab, options = {}) {
|
||||
return moveTabsAfter([tab], referenceTab, options).then(moved => moved.length > 0);
|
||||
}
|
||||
|
||||
async function moveTabsInternallyAfter(tabs, referenceTab, options = {}) {
|
||||
if (!tabs.length ||
|
||||
!TabsStore.ensureLivingItem(referenceTab))
|
||||
return [];
|
||||
|
||||
const win = TabsStore.windows.get(tabs[0].windowId);
|
||||
|
||||
log('moveTabsInternallyAfter: ', tabs, `${referenceTab.id}(index=${referenceTab.index})`, options);
|
||||
if (referenceTab.type == TreeItem.TYPE_GROUP) {
|
||||
if (!referenceTab.collapsed) {
|
||||
log(' => move before the first member tab of the reference group');
|
||||
return moveTabsInternallyBefore(tabs, referenceTab.$TST?.firstMember, options = {});
|
||||
}
|
||||
referenceTab = referenceTab.$TST?.lastMember;
|
||||
if (!TabsStore.ensureLivingItem(referenceTab)) {
|
||||
log('missing reference tab');
|
||||
return [];
|
||||
}
|
||||
log(` => reference tab: ${referenceTab.id}(index=${referenceTab.index})`);
|
||||
}
|
||||
|
||||
const followingReferenceTab = referenceTab.$TST.nextTab;
|
||||
if (followingReferenceTab &&
|
||||
followingReferenceTab.pinned) {
|
||||
// unpinned tab cannot be moved before any pinned tab
|
||||
tabs = tabs.filter(tab => tab.pinned);
|
||||
}
|
||||
else if (!referenceTab.pinned) {
|
||||
// pinned tab cannot be moved after any unpinned tab
|
||||
tabs = tabs.filter(tab => !tab.pinned);
|
||||
}
|
||||
if (!tabs.length)
|
||||
return [];
|
||||
|
||||
const movedTabs = [];
|
||||
try {
|
||||
/*
|
||||
Tab elements are moved by tabs.onMoved automatically, but
|
||||
the operation is asynchronous. To help synchronous operations
|
||||
following to this operation, we need to move tabs immediately.
|
||||
*/
|
||||
let nextTab = referenceTab.$TST.unsafeNextTab;
|
||||
while (nextTab && tabs.find(tab => tab.id == nextTab.id)) {
|
||||
nextTab = nextTab.$TST.unsafeNextTab;
|
||||
}
|
||||
const tabGroups = new Set();
|
||||
for (const tab of tabs) {
|
||||
const oldPreviousTab = tab.$TST.unsafePreviousTab;
|
||||
const oldNextTab = tab.$TST.unsafeNextTab;
|
||||
if ((!oldNextTab && !nextTab) ||
|
||||
(oldNextTab && nextTab && oldNextTab.id == nextTab.id)) // no move case
|
||||
continue;
|
||||
const fromIndex = tab.index;
|
||||
if (nextTab) {
|
||||
if (nextTab.index > tab.index)
|
||||
tab.index = nextTab.index - 1;
|
||||
else
|
||||
tab.index = nextTab.index;
|
||||
}
|
||||
else {
|
||||
tab.index = win.tabs.size - 1
|
||||
}
|
||||
tabGroups.add(tab.$TST.nativeTabGroup);
|
||||
if (SidebarConnection.isInitialized()) { // only on the background page
|
||||
win.internalMovingTabs.set(tab.id, tab.index);
|
||||
win.alreadyMovedTabs.set(tab.id, tab.index);
|
||||
}
|
||||
tab.reindexedBy = `moveTabsInternallyAfter (${tab.index})`;
|
||||
Tab.track(tab);
|
||||
movedTabs.push(tab);
|
||||
Tab.onTabInternallyMoved.dispatch(tab, {
|
||||
nextTab,
|
||||
oldPreviousTab,
|
||||
oldNextTab,
|
||||
broadcasted: !!options.broadcasted
|
||||
});
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TAB_INTERNALLY_MOVED,
|
||||
windowId: tab.windowId,
|
||||
tabId: tab.id,
|
||||
fromIndex,
|
||||
toIndex: tab.index,
|
||||
nextTabId: nextTab?.id,
|
||||
broadcasted: !!options.broadcasted
|
||||
});
|
||||
if (options.doNotOptimize) {
|
||||
win.internalMovingTabs.set(tab.id, tab.index);
|
||||
win.alreadyMovedTabs.set(tab.id, tab.index);
|
||||
await browser.tabs.move(tab.id, { index: tab.index });
|
||||
win.internalMovingTabs.delete(tab.id);
|
||||
win.alreadyMovedTabs.delete(tab.id);
|
||||
}
|
||||
}
|
||||
for (const group of tabGroups) {
|
||||
group?.$TST.reindex();
|
||||
}
|
||||
if (movedTabs.length == 0) {
|
||||
log(' => actually nothing moved');
|
||||
}
|
||||
else {
|
||||
log(
|
||||
'Tab nodes rearranged by moveTabsInternallyAfter:\n',
|
||||
(!configs.debug ? '' :
|
||||
() => toLines(Array.from(win.getOrderedTabs()),
|
||||
tab => ` - ${tab.index}: ${tab.id}${tabs.includes(tab) ? '[MOVED]' : ''}`))
|
||||
);
|
||||
}
|
||||
if (SidebarConnection.isInitialized()) { // only on the background page
|
||||
if (options.delayedMove) { // Wait until opening animation is finished.
|
||||
await wait(configs.newTabAnimationDuration);
|
||||
}
|
||||
if (!options.doNotOptimize) {
|
||||
syncToNativeTabs(tabs);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
ApiTabs.handleMissingTabError(e);
|
||||
log('moveTabsInternallyAfter failed: ', String(e));
|
||||
}
|
||||
return movedTabs;
|
||||
}
|
||||
export async function moveTabInternallyAfter(tab, referenceTab, options = {}) {
|
||||
return moveTabsInternallyAfter([tab], referenceTab, options);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================
|
||||
// Synchronize order of tab elements to browser's tabs
|
||||
|
||||
const mPreviousSync = new Map();
|
||||
const mDelayedSync = new Map();
|
||||
const mDelayedSyncTimer = new Map();
|
||||
|
||||
export async function waitUntilSynchronized(windowId) {
|
||||
const previous = mPreviousSync.get(windowId);
|
||||
if (previous)
|
||||
return previous.then(() => waitUntilSynchronized(windowId));
|
||||
return Promise.resolve(mDelayedSync.get(windowId)).then(() => {
|
||||
const previous = mPreviousSync.get(windowId);
|
||||
if (previous)
|
||||
return waitUntilSynchronized(windowId);
|
||||
});
|
||||
}
|
||||
|
||||
function syncToNativeTabs(tabs) {
|
||||
const windowId = tabs[0].windowId;
|
||||
//log(`syncToNativeTabs(${windowId})`);
|
||||
if (mDelayedSyncTimer.has(windowId))
|
||||
clearTimeout(mDelayedSyncTimer.get(windowId));
|
||||
const delayedSync = new Promise((resolve, _reject) => {
|
||||
mDelayedSyncTimer.set(windowId, setTimeout(() => {
|
||||
mDelayedSync.delete(windowId);
|
||||
let previousSync = mPreviousSync.get(windowId);
|
||||
if (previousSync)
|
||||
previousSync = previousSync.then(() => syncToNativeTabsInternal(windowId));
|
||||
else
|
||||
previousSync = syncToNativeTabsInternal(windowId);
|
||||
previousSync = previousSync.then(resolve);
|
||||
mPreviousSync.set(windowId, previousSync);
|
||||
}, 250));
|
||||
}).then(() => {
|
||||
mPreviousSync.delete(windowId);
|
||||
});
|
||||
mDelayedSync.set(windowId, delayedSync);
|
||||
return delayedSync;
|
||||
}
|
||||
async function syncToNativeTabsInternal(windowId) {
|
||||
mDelayedSyncTimer.delete(windowId);
|
||||
|
||||
if (Tab.needToWaitTracked(windowId))
|
||||
await Tab.waitUntilTrackedAll(windowId);
|
||||
if (Tab.needToWaitMoved(windowId))
|
||||
await Tab.waitUntilMovedAll(windowId);
|
||||
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (!win) // already destroyed
|
||||
return;
|
||||
|
||||
// Tabs may be removed while waiting.
|
||||
const internalOrder = TabsStore.windows.get(windowId).order;
|
||||
const nativeTabsOrder = (await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler())).map(tab => tab.id);
|
||||
log(`syncToNativeTabs(${windowId}): rearrange `, { internalOrder:internalOrder.join(','), nativeTabsOrder:nativeTabsOrder.join(',') });
|
||||
|
||||
log(`syncToNativeTabs(${windowId}): step1, internalOrder => nativeTabsOrder`);
|
||||
let tabIdsForUpdatedIndices = Array.from(nativeTabsOrder);
|
||||
|
||||
const moveOperations = (new SequenceMatcher(nativeTabsOrder, internalOrder)).operations();
|
||||
const movedTabs = new Set();
|
||||
for (const operation of moveOperations) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
log(`syncToNativeTabs(${windowId}): operation `, { tag, fromStart, fromEnd, toStart, toEnd });
|
||||
switch (tag) {
|
||||
case 'equal':
|
||||
case 'delete':
|
||||
break;
|
||||
|
||||
case 'insert':
|
||||
case 'replace':
|
||||
let moveTabIds = internalOrder.slice(toStart, toEnd);
|
||||
const referenceId = nativeTabsOrder[fromStart] || null;
|
||||
let toIndex = -1;
|
||||
let fromIndices = moveTabIds.map(id => tabIdsForUpdatedIndices.indexOf(id));
|
||||
if (referenceId) {
|
||||
toIndex = tabIdsForUpdatedIndices.indexOf(referenceId);
|
||||
}
|
||||
if (toIndex < 0)
|
||||
toIndex = internalOrder.length;
|
||||
// ignore already removed tabs!
|
||||
moveTabIds = moveTabIds.filter((id, index) => fromIndices[index] > -1);
|
||||
if (moveTabIds.length == 0)
|
||||
continue;
|
||||
fromIndices = fromIndices.filter(index => index > -1);
|
||||
const fromIndex = fromIndices[0];
|
||||
if (fromIndex < toIndex)
|
||||
toIndex--;
|
||||
log(`syncToNativeTabs(${windowId}): step1, move ${moveTabIds.join(',')} before ${referenceId} / from = ${fromIndex}, to = ${toIndex}`);
|
||||
for (const movedId of moveTabIds) {
|
||||
win.internalMovingTabs.set(movedId, -1);
|
||||
win.alreadyMovedTabs.set(movedId, -1);
|
||||
movedTabs.add(movedId);
|
||||
}
|
||||
logApiTabs(`tabs-move:syncToNativeTabs(${windowId}): step1, browser.tabs.move() `, moveTabIds, {
|
||||
windowId,
|
||||
index: toIndex
|
||||
});
|
||||
let reallyMovedTabIds = new Set();
|
||||
try {
|
||||
const reallyMovedTabs = await browser.tabs.move(moveTabIds, {
|
||||
windowId,
|
||||
index: toIndex
|
||||
}).catch(ApiTabs.createErrorHandler(e => {
|
||||
log(`syncToNativeTabs(${windowId}): step1, failed to move: `, String(e), e.stack);
|
||||
throw e;
|
||||
}));
|
||||
reallyMovedTabIds = new Set(reallyMovedTabs.map(tab => tab.id));
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
for (const id of moveTabIds) {
|
||||
if (reallyMovedTabIds.has(id))
|
||||
continue;
|
||||
log(`syncToNativeTabs(${windowId}): failed to move tab ${id}: maybe unplacable position (regular tabs in pinned tabs/pinned tabs in regular tabs), or any other reason`);
|
||||
win.internalMovingTabs.delete(id);
|
||||
win.alreadyMovedTabs.delete(id);
|
||||
}
|
||||
tabIdsForUpdatedIndices = tabIdsForUpdatedIndices.filter(id => !moveTabIds.includes(id));
|
||||
tabIdsForUpdatedIndices.splice(toIndex, 0, ...moveTabIds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
log(`syncToNativeTabs(${windowId}): step1, rearrange completed.`);
|
||||
|
||||
if (movedTabs.size > 0) {
|
||||
// tabs.onMoved produced by this operation can break the order of tabs
|
||||
// in the sidebar, so we need to synchronize complete order of tabs after
|
||||
// all.
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_SYNC_TABS_ORDER,
|
||||
windowId
|
||||
});
|
||||
|
||||
// Multiple times asynchronous tab move is unstable, so we retry again
|
||||
// for safety until all tabs are completely synchronized.
|
||||
syncToNativeTabs([{ windowId }]);
|
||||
}
|
||||
}
|
||||
342
waterfox/browser/components/sidebar/background/tabs-open.js
Normal file
342
waterfox/browser/components/sidebar/background/tabs-open.js
Normal file
@@ -0,0 +1,342 @@
|
||||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is the Tree Style Tab.
|
||||
*
|
||||
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011-2024
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
|
||||
* wanabe <https://github.com/wanabe>
|
||||
* Tetsuharu OHZEKI <https://github.com/saneyuki>
|
||||
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
|
||||
* lv7777 (https://github.com/lv7777)
|
||||
*
|
||||
* ***** END LICENSE BLOCK ******/
|
||||
'use strict';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs,
|
||||
tryRevokeObjectURL,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as TabsMove from './tabs-move.js';
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
export const onForbiddenURLRequested = new EventListenerManager();
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/tabs-open', ...args);
|
||||
}
|
||||
|
||||
const SEARCH_PREFIX_MATCHER = /^(ext\+ws:search:|about:ws-search\?)/;
|
||||
|
||||
export async function loadURI(uri, options = {}) {
|
||||
if (!options.windowId && !options.tab)
|
||||
throw new Error('missing loading target window or tab');
|
||||
try {
|
||||
let tabId;
|
||||
if (options.tab) {
|
||||
tabId = options.tab.id;
|
||||
}
|
||||
else {
|
||||
let tabs = await browser.tabs.query({
|
||||
windowId: options.windowId,
|
||||
active: true
|
||||
}).catch(ApiTabs.createErrorHandler());
|
||||
if (tabs.length == 0)
|
||||
tabs = await browser.tabs.query({
|
||||
windowId: options.windowId,
|
||||
}).catch(ApiTabs.createErrorHandler());
|
||||
tabId = tabs[0].id;
|
||||
}
|
||||
let searchQuery = null;
|
||||
if (SEARCH_PREFIX_MATCHER.test(uri)) {
|
||||
const query = uri.replace(SEARCH_PREFIX_MATCHER, '');
|
||||
if (browser.search &&
|
||||
typeof browser.search.search == 'function')
|
||||
searchQuery = query;
|
||||
else
|
||||
uri = configs.defaultSearchEngine.replace(/%s/gi, query);
|
||||
}
|
||||
if (searchQuery) {
|
||||
await browser.search.search({
|
||||
query: searchQuery,
|
||||
tabId
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
}
|
||||
else {
|
||||
await browser.tabs.update(tabId, {
|
||||
url: sanitizeURL(uri),
|
||||
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
ApiTabs.handleMissingTabError(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openNewTab(options = {}) {
|
||||
const win = TabsStore.windows.get(options.windowId);
|
||||
win.toBeOpenedNewTabCommandTab++;
|
||||
return openURIInTab(options.url || options.uri || null, options);
|
||||
}
|
||||
|
||||
export async function openURIInTab(uri, options = {}) {
|
||||
const tabs = await openURIsInTabs([uri], options);
|
||||
return tabs[0];
|
||||
}
|
||||
|
||||
const FORBIDDEN_URL_MATCHER = /^(about|chrome|resource|file):/;
|
||||
const ALLOWED_URL_MATCHER = /^about:blank(\?|$)/;
|
||||
|
||||
export async function openURIsInTabs(uris, { windowId, insertBefore, insertAfter, cookieStoreId, isOrphan, active, inBackground, discarded, opener, parent, fixPositions } = {}) {
|
||||
log('openURIsInTabs: ', { uris, windowId, insertBefore, insertAfter, cookieStoreId, isOrphan, active, inBackground, discarded, opener, parent, fixPositions });
|
||||
if (!windowId)
|
||||
throw new Error('missing loading target window\n' + new Error().stack);
|
||||
|
||||
const tabs = [];
|
||||
// Don't return the result of Tab.doAndGetNewTabs because their order can
|
||||
// be inverted due to browser.tabs.insertAfterCurrent=true
|
||||
const actuallyOpenedTabIds = new Set(await Tab.doAndGetNewTabs(async () => {
|
||||
await Tab.waitUntilTrackedAll(windowId);
|
||||
await TabsMove.waitUntilSynchronized(windowId);
|
||||
const startIndex = Tab.calculateNewTabIndex({ insertAfter, insertBefore });
|
||||
log('startIndex: ', startIndex);
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (insertBefore ||
|
||||
insertAfter ||
|
||||
uris.some(uri => uri && typeof uri == 'object' && 'index' in uri))
|
||||
win.toBeOpenedTabsWithPositions += uris.length;
|
||||
if (cookieStoreId)
|
||||
win.toBeOpenedTabsWithCookieStoreId += uris.length;
|
||||
if (isOrphan)
|
||||
win.toBeOpenedOrphanTabs += uris.length;
|
||||
return Promise.all(uris.map(async (uri, index) => {
|
||||
const params = {
|
||||
windowId,
|
||||
active: index == 0 && (active || (inBackground === false)),
|
||||
};
|
||||
if (uri && typeof uri == 'object') { // tabs.create() compatible
|
||||
if ('active' in uri)
|
||||
params.active = uri.active;
|
||||
if ('cookieStoreId' in uri)
|
||||
params.cookieStoreId = uri.cookieStoreId;
|
||||
if ('discarded' in uri)
|
||||
params.discarded = uri.discarded;
|
||||
if ('index' in uri)
|
||||
params.index = uri.index;
|
||||
if ('openerTabId' in uri)
|
||||
params.openerTabId = uri.openerTabId;
|
||||
if ('openInReaderMode' in uri)
|
||||
params.openInReaderMode = uri.openInReaderMode;
|
||||
if ('pinned' in uri)
|
||||
params.pinned = uri.pinned;
|
||||
if ('selected' in uri)
|
||||
params.active = uri.selected;
|
||||
if ('title' in uri)
|
||||
params.title = uri.title;
|
||||
uri = uri.uri || uri.url;
|
||||
}
|
||||
let searchQuery = null;
|
||||
if (uri) {
|
||||
if (SEARCH_PREFIX_MATCHER.test(uri)) {
|
||||
const query = uri.replace(SEARCH_PREFIX_MATCHER, '');
|
||||
if (browser.search &&
|
||||
typeof browser.search.search == 'function')
|
||||
searchQuery = query;
|
||||
else
|
||||
params.url = configs.defaultSearchEngine.replace(/%s/gi, query);
|
||||
}
|
||||
else {
|
||||
params.url = uri;
|
||||
}
|
||||
}
|
||||
if (discarded &&
|
||||
!params.active &&
|
||||
!('discarded' in params))
|
||||
params.discarded = true;
|
||||
if (params.url == 'about:newtab')
|
||||
delete params.url
|
||||
if (params.url)
|
||||
params.url = sanitizeURL(params.url);
|
||||
if (!('url' in params /* about:newtab case */) ||
|
||||
/^about:/.test(params.url))
|
||||
params.discarded = false; // discarded tab cannot be opened with any about: URL
|
||||
if (!params.discarded) // title cannot be set for non-discarded tabs
|
||||
params.title = null;
|
||||
if (opener && !params.openerTabId)
|
||||
params.openerTabId = opener.id;
|
||||
if (startIndex > -1 && !('index' in params))
|
||||
params.index = startIndex + index;
|
||||
if (cookieStoreId && !params.cookieStoreId)
|
||||
params.cookieStoreId = cookieStoreId;
|
||||
// Tabs opened with different container can take time to be tracked,
|
||||
// then TabsStore.waitUntilTabsAreCreated() may be resolved before it is
|
||||
// tracked like as "the tab is already closed". So we wait until the
|
||||
// tab is correctly tracked.
|
||||
const promisedNewTabTracked = new Promise((resolve, reject) => {
|
||||
const listener = (tab) => {
|
||||
Tab.onCreating.removeListener(listener);
|
||||
browser.tabs.get(tab.id)
|
||||
.then(resolve)
|
||||
.catch(ApiTabs.createErrorSuppressor(reject));
|
||||
};
|
||||
Tab.onCreating.addListener(listener);
|
||||
});
|
||||
const createdTab = await browser.tabs.create(params).catch(ApiTabs.createErrorHandler());
|
||||
await Promise.all([
|
||||
promisedNewTabTracked, // TabsStore.waitUntilTabsAreCreated(createdTab.id),
|
||||
searchQuery && browser.search.search({
|
||||
query: searchQuery,
|
||||
tabId: createdTab.id
|
||||
}).catch(ApiTabs.createErrorHandler())
|
||||
]);
|
||||
const tab = Tab.get(createdTab.id);
|
||||
log('created tab: ', tab);
|
||||
if (!tab)
|
||||
throw new Error('tab is already closed');
|
||||
if (!opener &&
|
||||
parent &&
|
||||
!isOrphan)
|
||||
await Tree.attachTabTo(tab, parent, {
|
||||
insertBefore: insertBefore,
|
||||
insertAfter: insertAfter,
|
||||
forceExpand: params.active,
|
||||
broadcast: true
|
||||
});
|
||||
else if (insertBefore)
|
||||
await TabsMove.moveTabInternallyBefore(tab, insertBefore, {
|
||||
broadcast: true
|
||||
});
|
||||
else if (insertAfter)
|
||||
await TabsMove.moveTabInternallyAfter(tab, insertAfter, {
|
||||
broadcast: true
|
||||
});
|
||||
log('tab is opened.');
|
||||
await tab.$TST.opened;
|
||||
tabs.push(tab);
|
||||
tryRevokeObjectURL(tab.url);
|
||||
return tab;
|
||||
}));
|
||||
}, windowId));
|
||||
const openedTabs = tabs.filter(tab => actuallyOpenedTabIds.has(tab));
|
||||
|
||||
if (fixPositions &&
|
||||
openedTabs.every((tab, index) => (index == 0) || (openedTabs[index-1].index - tab.index) == 1)) {
|
||||
// tabs are opened with reversed order due to browser.tabs.insertAfterCurrent=true
|
||||
let lastTab;
|
||||
for (const tab of openedTabs.slice(0).reverse()) {
|
||||
if (lastTab)
|
||||
TabsMove.moveTabInternallyBefore(tab, lastTab);
|
||||
lastTab = tab;
|
||||
}
|
||||
await TabsMove.waitUntilSynchronized(windowId);
|
||||
}
|
||||
return openedTabs;
|
||||
}
|
||||
|
||||
function sanitizeURL(url) {
|
||||
if (ALLOWED_URL_MATCHER.test(url))
|
||||
return url;
|
||||
|
||||
// tabs.create() doesn't accept about:reader URLs so we fallback them to regular URLs.
|
||||
if (/^about:reader\?/.test(url))
|
||||
return (new URL(url)).searchParams.get('url') || 'about:blank';
|
||||
|
||||
if (FORBIDDEN_URL_MATCHER.test(url)) {
|
||||
onForbiddenURLRequested.dispatch(url);
|
||||
return `about:blank?forbidden-url=${url}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function isOpenable(url) {
|
||||
return !url || url == sanitizeURL(url);
|
||||
}
|
||||
|
||||
|
||||
function onMessage(message, openerTab) {
|
||||
switch (message.type) {
|
||||
case Constants.kCOMMAND_LOAD_URI:
|
||||
loadURI(message.uri, {
|
||||
tab: Tab.get(message.tabId)
|
||||
});
|
||||
break;
|
||||
|
||||
case Constants.kCOMMAND_OPEN_TAB:
|
||||
if (!message.parentId && openerTab)
|
||||
message.parentId = openerTab.id;
|
||||
if (!message.windowId && openerTab)
|
||||
message.windowId = openerTab.windowId;
|
||||
Tab.waitUntilTracked([
|
||||
message.parentId,
|
||||
message.insertBeforeId,
|
||||
message.insertAfterId
|
||||
]).then(() => {
|
||||
openURIsInTabs(message.uris || [message.uri], {
|
||||
windowId: message.windowId,
|
||||
parent: Tab.get(message.parentId),
|
||||
insertBefore: Tab.get(message.insertBeforeId),
|
||||
insertAfter: Tab.get(message.insertAfterId),
|
||||
active: !!message.active,
|
||||
discarded: message.discarded,
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case Constants.kCOMMAND_NEW_TABS:
|
||||
Tab.waitUntilTracked([
|
||||
message.openerId,
|
||||
message.parentId,
|
||||
message.insertBeforeId,
|
||||
message.insertAfterId
|
||||
]).then(() => {
|
||||
log('new tabs requested: ', message);
|
||||
openURIsInTabs(message.uris, {
|
||||
windowId: message.windowId,
|
||||
opener: Tab.get(message.openerId),
|
||||
parent: Tab.get(message.parentId),
|
||||
insertBefore: Tab.get(message.insertBeforeId),
|
||||
insertAfter: Tab.get(message.insertAfterId),
|
||||
active: message.active,
|
||||
discarded: message.discarded,
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SidebarConnection.onMessage.addListener((windowId, message) => {
|
||||
onMessage(message);
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((message, sender) => {
|
||||
if (!message ||
|
||||
typeof message.type != 'string' ||
|
||||
message.type.indexOf('ws:') != 0)
|
||||
return;
|
||||
|
||||
onMessage(message, sender.tab);
|
||||
});
|
||||
691
waterfox/browser/components/sidebar/background/tree-structure.js
Normal file
691
waterfox/browser/components/sidebar/background/tree-structure.js
Normal file
@@ -0,0 +1,691 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
toLines,
|
||||
configs,
|
||||
wait,
|
||||
} from '/common/common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as SidebarConnection from '/common/sidebar-connection.js';
|
||||
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
||||
import * as TabsStore from '/common/tabs-store.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
import * as UserOperationBlocker from '/common/user-operation-blocker.js';
|
||||
|
||||
import MetricsData from '/common/MetricsData.js';
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
import * as Commands from './commands.js';
|
||||
import * as TabsMove from './tabs-move.js';
|
||||
import * as TabsOpen from './tabs-open.js';
|
||||
import * as Tree from './tree.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('background/tree-structure', ...args);
|
||||
}
|
||||
|
||||
export const onTabAttachedFromRestoredInfo = new EventListenerManager();
|
||||
|
||||
let mRecentlyClosedTabs = [];
|
||||
let mRecentlyClosedTabsTreeStructure = [];
|
||||
|
||||
export function startTracking() {
|
||||
Tab.onCreated.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
|
||||
Tab.onRemoved.addListener((tab, info) => {
|
||||
if (!info.isWindowClosing)
|
||||
reserveToSaveTreeStructure(tab.windowId);
|
||||
});
|
||||
Tab.onMoved.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
|
||||
Tab.onUpdated.addListener((tab, info) => {
|
||||
if ('openerTabId' in info)
|
||||
reserveToSaveTreeStructure(tab.windowId);
|
||||
});
|
||||
Tree.onAttached.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
|
||||
Tree.onDetached.addListener((tab, _info) => { reserveToSaveTreeStructure(tab.windowId); });
|
||||
Tree.onSubtreeCollapsedStateChanging.addListener(tab => { reserveToSaveTreeStructure(tab.windowId); });
|
||||
}
|
||||
|
||||
export function reserveToSaveTreeStructure(windowId) {
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (!win)
|
||||
return;
|
||||
|
||||
if (win.waitingToSaveTreeStructure)
|
||||
clearTimeout(win.waitingToSaveTreeStructure);
|
||||
win.waitingToSaveTreeStructure = setTimeout(() => {
|
||||
saveTreeStructure(windowId);
|
||||
}, 150);
|
||||
}
|
||||
async function saveTreeStructure(windowId) {
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
if (!win)
|
||||
return;
|
||||
|
||||
const structure = TreeBehavior.getTreeStructureFromTabs(Tab.getAllTabs(windowId));
|
||||
browser.sessions.setWindowValue(
|
||||
windowId,
|
||||
Constants.kWINDOW_STATE_TREE_STRUCTURE,
|
||||
structure
|
||||
).catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
|
||||
export async function loadTreeStructure(windows, restoredFromCacheResults) {
|
||||
log('loadTreeStructure');
|
||||
MetricsData.add('loadTreeStructure: start');
|
||||
return MetricsData.addAsync('loadTreeStructure: restoration for windows', Promise.all(windows.map(async win => {
|
||||
if (restoredFromCacheResults &&
|
||||
restoredFromCacheResults.get(win.id)) {
|
||||
log(`skip tree structure restoration for window ${win.id} (restored from cache)`);
|
||||
return;
|
||||
}
|
||||
const tabs = Tab.getAllTabs(win.id);
|
||||
let windowStateCompletelyApplied = false;
|
||||
try {
|
||||
const structure = await browser.sessions.getWindowValue(win.id, Constants.kWINDOW_STATE_TREE_STRUCTURE).catch(ApiTabs.createErrorHandler());
|
||||
let uniqueIds = tabs.map(tab => tab.$TST.uniqueId || '?');
|
||||
MetricsData.add('loadTreeStructure: read stored data');
|
||||
if (structure &&
|
||||
structure.length > 0 &&
|
||||
structure.length <= tabs.length) {
|
||||
uniqueIds = uniqueIds.map(id => id.id);
|
||||
let tabsOffset;
|
||||
if (structure[0].id) {
|
||||
const structureSignature = toLines(structure, item => item.id);
|
||||
tabsOffset = uniqueIds.join('\n').indexOf(structureSignature);
|
||||
windowStateCompletelyApplied = tabsOffset > -1;
|
||||
}
|
||||
else {
|
||||
tabsOffset = 0;
|
||||
windowStateCompletelyApplied = structure.length == tabs.length;
|
||||
}
|
||||
if (tabsOffset > -1) {
|
||||
const structureRestoreTabs = tabs.slice(tabsOffset);
|
||||
await Tree.applyTreeStructureToTabs(structureRestoreTabs, structure);
|
||||
for (const tab of structureRestoreTabs) {
|
||||
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
|
||||
}
|
||||
MetricsData.add('loadTreeStructure: Tree.applyTreeStructureToTabs');
|
||||
}
|
||||
else {
|
||||
MetricsData.add('loadTreeStructure: mismatched signature');
|
||||
}
|
||||
}
|
||||
else {
|
||||
MetricsData.add('loadTreeStructure: no valid structure information');
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.log(`TreeStructure.loadTreeStructure: Fatal error, ${error}`, error.stack);
|
||||
MetricsData.add('loadTreeStructure: failed to apply tree structure');
|
||||
}
|
||||
if (!windowStateCompletelyApplied) {
|
||||
log(`Tree information for the window ${win.id} is not same to actual state. Fallback to restoration from tab relations.`);
|
||||
MetricsData.add('loadTreeStructure: fallback to reserveToAttachTabFromRestoredInfo');
|
||||
const unattachedTabs = new Set(tabs);
|
||||
for (const tab of tabs) {
|
||||
reserveToAttachTabFromRestoredInfo(tab, {
|
||||
keepCurrentTree: true,
|
||||
canCollapse: true
|
||||
}).then(attached => {
|
||||
if (attached ||
|
||||
tab.$TST.parent ||
|
||||
tab.$TST.hasChild)
|
||||
unattachedTabs.delete(tab);
|
||||
});
|
||||
}
|
||||
await reserveToAttachTabFromRestoredInfo.promisedDone;
|
||||
MetricsData.add('loadTreeStructure: attachTabFromRestoredInfo finish');
|
||||
|
||||
// unknown tabs may appear inside tree, so we need to fixup tree based on their position.
|
||||
for (const tab of unattachedTabs) {
|
||||
const action = Tree.detectTabActionFromNewPosition(tab, {
|
||||
fromIndex: tabs.length - 1,
|
||||
toIndex: tab.index,
|
||||
isTabCreating: true,
|
||||
});
|
||||
switch (action.action) {
|
||||
default:
|
||||
break;
|
||||
|
||||
case 'attach':
|
||||
case 'detach':
|
||||
log('loadTreeStructure: apply action for unattached tab: ', tab, action);
|
||||
await action.applyIfNeeded();
|
||||
break;
|
||||
}
|
||||
}
|
||||
MetricsData.add('loadTreeStructure: finish to fixup tree structure');
|
||||
}
|
||||
Tab.dumpAll();
|
||||
})));
|
||||
}
|
||||
|
||||
async function reserveToAttachTabFromRestoredInfo(tab, options = {}) {
|
||||
if (reserveToAttachTabFromRestoredInfo.waiting)
|
||||
clearTimeout(reserveToAttachTabFromRestoredInfo.waiting);
|
||||
reserveToAttachTabFromRestoredInfo.tasks.push({ tab, options: options });
|
||||
if (!reserveToAttachTabFromRestoredInfo.promisedDone) {
|
||||
reserveToAttachTabFromRestoredInfo.promisedDone = new Promise((resolve, _reject) => {
|
||||
reserveToAttachTabFromRestoredInfo.onDone = resolve;
|
||||
});
|
||||
}
|
||||
reserveToAttachTabFromRestoredInfo.waiting = setTimeout(async () => {
|
||||
reserveToAttachTabFromRestoredInfo.waiting = null;
|
||||
const tasks = reserveToAttachTabFromRestoredInfo.tasks.slice(0);
|
||||
reserveToAttachTabFromRestoredInfo.tasks = [];
|
||||
const uniqueIds = tasks.map(task => task.tab.$TST.uniqueId);
|
||||
const bulk = tasks.length > 1;
|
||||
const attachedResults = await Promise.all(uniqueIds.map((uniqueId, index) => {
|
||||
const task = tasks[index];
|
||||
return attachTabFromRestoredInfo(task.tab, {
|
||||
...task.options,
|
||||
uniqueId,
|
||||
bulk
|
||||
}).catch(error => {
|
||||
console.log(`TreeStructure.reserveToAttachTabFromRestoredInfo: Fatal error on processing task ${index}, ${error}`, error.stack);
|
||||
return false;
|
||||
});
|
||||
}));
|
||||
if (typeof reserveToAttachTabFromRestoredInfo.onDone == 'function')
|
||||
reserveToAttachTabFromRestoredInfo.onDone(attachedResults.every(attached => !!attached));
|
||||
delete reserveToAttachTabFromRestoredInfo.onDone;
|
||||
delete reserveToAttachTabFromRestoredInfo.promisedDone;
|
||||
Tab.dumpAll();
|
||||
}, 100);
|
||||
return reserveToAttachTabFromRestoredInfo.promisedDone;
|
||||
}
|
||||
reserveToAttachTabFromRestoredInfo.waiting = null;
|
||||
reserveToAttachTabFromRestoredInfo.tasks = [];
|
||||
reserveToAttachTabFromRestoredInfo.promisedDone = null;
|
||||
|
||||
|
||||
async function attachTabFromRestoredInfo(tab, options = {}) {
|
||||
log('attachTabFromRestoredInfo ', tab, options);
|
||||
if (tab.$TST.temporaryMetadata.has('treeStructureAlreadyRestoredFromSessionData')) {
|
||||
log(' => already restored ', tab.id);
|
||||
return;
|
||||
}
|
||||
|
||||
let uniqueId, insertBefore, insertAfter, insertAfterLegacy, ancestors, children, states, collapsed /* for backward compatibility */;
|
||||
// eslint-disable-next-line prefer-const
|
||||
[uniqueId, insertBefore, insertAfter, insertAfterLegacy, ancestors, children, states, collapsed] = await Promise.all([
|
||||
options.uniqueId || tab.$TST.uniqueId || tab.$TST.promisedUniqueId,
|
||||
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_BEFORE).catch(ApiTabs.createErrorHandler()),
|
||||
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_AFTER).catch(ApiTabs.createErrorHandler()),
|
||||
// This legacy should be removed after legacy data are cleared enough, maybe after Firefox 128 is released.
|
||||
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_INSERT_AFTER_LEGACY).catch(ApiTabs.createErrorHandler()),
|
||||
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_ANCESTORS).catch(ApiTabs.createErrorHandler()),
|
||||
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_CHILDREN).catch(ApiTabs.createErrorHandler()),
|
||||
tab.$TST.getPermanentStates(),
|
||||
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_SUBTREE_COLLAPSED).catch(ApiTabs.createErrorHandler()) // for backward compatibility
|
||||
]);
|
||||
ancestors = ancestors || [];
|
||||
children = children || [];
|
||||
log(`persistent references for ${dumpTab(tab)} (${uniqueId.id}): `, {
|
||||
insertBefore, insertAfter,
|
||||
insertAfterLegacy,
|
||||
ancestors: ancestors.join(', '),
|
||||
children: children.join(', '),
|
||||
states,
|
||||
collapsed
|
||||
});
|
||||
if (collapsed && !states.includes(Constants.kTAB_STATE_SUBTREE_COLLAPSED)) {
|
||||
// migration
|
||||
states.push(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
|
||||
browser.sessions.removeTabValue(tab.id, Constants.kPERSISTENT_SUBTREE_COLLAPSED).catch(ApiTabs.createErrorSuppressor());
|
||||
}
|
||||
insertBefore = Tab.getByUniqueId(insertBefore);
|
||||
insertAfter = Tab.getByUniqueId(insertAfter || insertAfterLegacy);
|
||||
ancestors = ancestors.map(Tab.getByUniqueId);
|
||||
children = children.map(Tab.getByUniqueId);
|
||||
log(' => references: ', tab.id, () => ({
|
||||
insertBefore: dumpTab(insertBefore),
|
||||
insertAfter: dumpTab(insertAfter),
|
||||
ancestors: ancestors.map(dumpTab).join(', '),
|
||||
children: children.map(dumpTab).join(', ')
|
||||
}));
|
||||
if (configs.fixupTreeOnTabVisibilityChanged) {
|
||||
ancestors = ancestors.filter(ancestor => ancestor && (ancestor.hidden == tab.hidden));
|
||||
children = children.filter(child => child && (child.hidden == tab.hidden));
|
||||
log(' ==> references: ', tab.id, () => ({
|
||||
ancestors: ancestors.map(dumpTab).join(', '),
|
||||
children: children.map(dumpTab).join(', ')
|
||||
}));
|
||||
}
|
||||
|
||||
// clear wrong positioning information
|
||||
if (tab.pinned ||
|
||||
insertBefore?.pinned)
|
||||
insertBefore = null;
|
||||
const nextOfInsertAfter = insertAfter?.$TST.nextTab;
|
||||
if (nextOfInsertAfter &&
|
||||
nextOfInsertAfter.pinned)
|
||||
insertAfter = null;
|
||||
|
||||
let attached = false;
|
||||
const active = tab.active;
|
||||
const promises = [];
|
||||
for (const ancestor of ancestors) {
|
||||
if (!ancestor)
|
||||
continue;
|
||||
log(' attach to old ancestor: ', tab.id, { child: tab, parent: ancestor });
|
||||
const promisedDone = Tree.attachTabTo(tab, ancestor, {
|
||||
insertBefore,
|
||||
insertAfter,
|
||||
dontExpand: !active,
|
||||
forceExpand: active,
|
||||
broadcast: true
|
||||
});
|
||||
if (options.bulk)
|
||||
promises.push(promisedDone);
|
||||
else
|
||||
await promisedDone;
|
||||
attached = true;
|
||||
break;
|
||||
}
|
||||
if (!attached) {
|
||||
const opener = tab.$TST.openerTab;
|
||||
if (opener &&
|
||||
configs.syncParentTabAndOpenerTab) {
|
||||
log(' attach to opener: ', tab.id, { child: tab, parent: opener });
|
||||
const promisedDone = Tree.attachTabTo(tab, opener, {
|
||||
dontExpand: !active,
|
||||
forceExpand: active,
|
||||
broadcast: true,
|
||||
insertAt: Constants.kINSERT_NEAREST
|
||||
});
|
||||
if (options.bulk)
|
||||
promises.push(promisedDone);
|
||||
else
|
||||
await promisedDone;
|
||||
}
|
||||
else if (!options.bulk &&
|
||||
(tab.$TST.nearestCompletelyOpenedNormalFollowingTab ||
|
||||
tab.$TST.nearestCompletelyOpenedNormalPrecedingTab)) {
|
||||
log(' attach from position: ', tab.id);
|
||||
onTabAttachedFromRestoredInfo.dispatch(tab, {
|
||||
toIndex: tab.index,
|
||||
fromIndex: Tab.getLastTab(tab.windowId).index
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!options.keepCurrentTree &&
|
||||
// the restored tab is a roo tab
|
||||
ancestors.length == 0 &&
|
||||
// but attached to any parent based on its restored position
|
||||
tab.$TST.parent &&
|
||||
// when not in-middle position of existing tree (safely detachable position)
|
||||
!tab.$TST.nextSiblingTab) {
|
||||
Tree.detachTab(tab, {
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
if (options.children && !options.bulk) {
|
||||
let firstInTree = tab.$TST.firstChild || tab;
|
||||
let lastInTree = tab.$TST.lastDescendant || tab;
|
||||
for (const child of children) {
|
||||
if (!child)
|
||||
continue;
|
||||
await Tree.attachTabTo(child, tab, {
|
||||
dontExpand: !child.active,
|
||||
forceExpand: active,
|
||||
insertAt: Constants.kINSERT_NEAREST,
|
||||
dontMove: child.index >= firstInTree.index && child.index <= lastInTree.index + 1,
|
||||
broadcast: true
|
||||
});
|
||||
if (child.index < firstInTree.index)
|
||||
firstInTree = child;
|
||||
else if (child.index > lastInTree.index)
|
||||
lastInTree = child;
|
||||
}
|
||||
}
|
||||
|
||||
const subtreeCollapsed = states.includes(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
|
||||
log('restore subtree collapsed state: ', tab.id, { current: tab.$TST.subtreeCollapsed, expected: subtreeCollapsed, ...options });
|
||||
if ((options.canCollapse || options.bulk) &&
|
||||
tab.$TST.subtreeCollapsed != subtreeCollapsed) {
|
||||
const promisedDone = Tree.collapseExpandSubtree(tab, {
|
||||
broadcast: true,
|
||||
collapsed: subtreeCollapsed,
|
||||
justNow: true
|
||||
});
|
||||
promises.push(promisedDone);
|
||||
}
|
||||
|
||||
const updateCollapsedState = () => {
|
||||
const shouldBeCollapsed = tab.$TST.ancestors.some(ancestor => ancestor.$TST.collapsed || ancestor.$TST.subtreeCollapsed);
|
||||
log('update collapsed state: ', tab.id, { current: tab.$TST.collapsed, expected: shouldBeCollapsed });
|
||||
if ((options.canCollapse || options.bulk) &&
|
||||
tab.$TST.collapsed != shouldBeCollapsed) {
|
||||
Tree.collapseExpandTabAndSubtree(tab, {
|
||||
broadcast: true,
|
||||
collapsed: !tab.$TST.collapsed,
|
||||
justNow: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
|
||||
|
||||
if (options.bulk)
|
||||
await Promise.all(promises).then(updateCollapsedState);
|
||||
else
|
||||
updateCollapsedState();
|
||||
|
||||
return attached;
|
||||
}
|
||||
|
||||
const mRestoringTabs = new Map();
|
||||
const mMaxRestoringTabs = new Map();
|
||||
const mRestoredTabIds = new Set();
|
||||
const mProcessingTabRestorations = [];
|
||||
|
||||
Tab.onRestored.addListener(tab => {
|
||||
log('onTabRestored ', dumpTab(tab));
|
||||
mProcessingTabRestorations.push(async () => {
|
||||
try {
|
||||
const count = mRestoringTabs.get(tab.windowId) || 0;
|
||||
if (count == 0) {
|
||||
setTimeout(() => {
|
||||
const count = mRestoringTabs.get(tab.windowId) || 0;
|
||||
if (count > 0) {
|
||||
UserOperationBlocker.blockIn(tab.windowId, { throbber: true });
|
||||
UserOperationBlocker.setProgress(0, tab.windowId);
|
||||
}
|
||||
}, configs.delayToBlockUserOperationForTabsRestoration);
|
||||
}
|
||||
mRestoringTabs.set(tab.windowId, count + 1);
|
||||
const maxCount = mMaxRestoringTabs.get(tab.windowId) || 0;
|
||||
mMaxRestoringTabs.set(tab.windowId, Math.max(count, maxCount));
|
||||
|
||||
const uniqueId = await tab.$TST.promisedUniqueId;
|
||||
mRestoredTabIds.add(uniqueId.id);
|
||||
|
||||
if (count == 0) {
|
||||
// Force restore recycled active tab.
|
||||
// See also: https://github.com/piroor/treestyletab/issues/2191#issuecomment-489271889
|
||||
const activeTab = Tab.getActiveTab(tab.windowId);
|
||||
const [uniqueId, restoredUniqueId] = await Promise.all([
|
||||
activeTab.$TST.promisedUniqueId,
|
||||
browser.sessions.getTabValue(activeTab.id, Constants.kPERSISTENT_ID).catch(ApiTabs.createErrorHandler())
|
||||
]);
|
||||
if (restoredUniqueId?.id != uniqueId.id) {
|
||||
activeTab.$TST.updateUniqueId({ id: restoredUniqueId.id });
|
||||
reserveToAttachTabFromRestoredInfo(activeTab, {
|
||||
children: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
reserveToAttachTabFromRestoredInfo(tab, {
|
||||
children: true
|
||||
});
|
||||
|
||||
reserveToAttachTabFromRestoredInfo.promisedDone.then(() => {
|
||||
Tree.fixupSubtreeCollapsedState(tab, {
|
||||
justNow: true,
|
||||
broadcast: true
|
||||
});
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TAB_RESTORED,
|
||||
tabId: tab.id,
|
||||
windowId: tab.windowId
|
||||
});
|
||||
|
||||
let count = mRestoringTabs.get(tab.windowId) || 0;
|
||||
count--;
|
||||
if (count == 0) {
|
||||
mRestoringTabs.delete(tab.windowId);
|
||||
mMaxRestoringTabs.delete(tab.windowId);
|
||||
setTimeout(() => { // because window.requestAnimationFrame is decelerate for an invisible document.
|
||||
// unblock in the next event loop, after other asynchronous operations are finished
|
||||
UserOperationBlocker.unblockIn(tab.windowId, { throbber: true });
|
||||
}, 0);
|
||||
|
||||
const countToBeRestored = mRecentlyClosedTabs.filter(tab => !mRestoredTabIds.has(tab.uniqueId));
|
||||
log('countToBeRestored: ', countToBeRestored);
|
||||
if (countToBeRestored > 0)
|
||||
tryRestoreClosedSetFor(tab, countToBeRestored);
|
||||
mRestoredTabIds.clear();
|
||||
}
|
||||
else {
|
||||
mRestoringTabs.set(tab.windowId, count);
|
||||
const maxCount = mMaxRestoringTabs.get(tab.windowId);
|
||||
UserOperationBlocker.setProgress(Math.round(maxCount - count / maxCount * 100), tab.windowId);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(_e) {
|
||||
}
|
||||
mProcessingTabRestorations.shift();
|
||||
if (mProcessingTabRestorations.length > 0)
|
||||
mProcessingTabRestorations[0]();
|
||||
});
|
||||
if (mProcessingTabRestorations.length == 1)
|
||||
mProcessingTabRestorations[0]();
|
||||
});
|
||||
|
||||
|
||||
// Implementation for the "Undo Close Tab*s*" feature
|
||||
// https://github.com/piroor/treestyletab/issues/2627
|
||||
|
||||
const mPendingRecentlyClosedTabsInfo = {
|
||||
tabs: [],
|
||||
structure: []
|
||||
};
|
||||
|
||||
Tab.onRemoved.addListener((_tab, _info) => {
|
||||
const currentlyRestorable = mRecentlyClosedTabs.length > 1;
|
||||
|
||||
mRecentlyClosedTabs = [];
|
||||
mRecentlyClosedTabsTreeStructure = [];
|
||||
|
||||
const newlyRestorable = mRecentlyClosedTabs.length > 1;
|
||||
if (currentlyRestorable != newlyRestorable)
|
||||
Tab.onChangeMultipleTabsRestorability.dispatch(newlyRestorable);
|
||||
});
|
||||
|
||||
Tab.onMultipleTabsRemoving.addListener((tabs, { triggerTab, originalStructure } = {}) => {
|
||||
if (triggerTab)
|
||||
tabs = [triggerTab, ...tabs];
|
||||
mPendingRecentlyClosedTabsInfo.tabs = tabs.map(tab => ({
|
||||
originalId: tab.id,
|
||||
uniqueId: tab.$TST.uniqueId.id,
|
||||
windowId: tab.windowId,
|
||||
title: tab.title,
|
||||
url: tab.url,
|
||||
cookieStoreId: tab.cookieStoreId
|
||||
}));
|
||||
mPendingRecentlyClosedTabsInfo.structure = originalStructure || TreeBehavior.getTreeStructureFromTabs(tabs, {
|
||||
full: true,
|
||||
keepParentOfRootTabs: true
|
||||
});
|
||||
log('mPendingRecentlyClosedTabsInfo.tabs = ', mPendingRecentlyClosedTabsInfo.tabs);
|
||||
log('mPendingRecentlyClosedTabsInfo.structure = ', mPendingRecentlyClosedTabsInfo.structure);
|
||||
});
|
||||
|
||||
Tab.onMultipleTabsRemoved.addListener((tabs, { triggerTab } = {}) => {
|
||||
log('multiple tabs are removed');
|
||||
const currentlyRestorable = mRecentlyClosedTabs.length > 1;
|
||||
|
||||
if (triggerTab)
|
||||
tabs = [triggerTab, ...tabs];
|
||||
const tabIds = new Set(tabs.map(tab => tab.id));
|
||||
mRecentlyClosedTabs = mPendingRecentlyClosedTabsInfo.tabs.filter(info => tabIds.has(info.originalId));
|
||||
mRecentlyClosedTabsTreeStructure = mPendingRecentlyClosedTabsInfo.structure.filter(structure => tabIds.has(structure.originalId));
|
||||
log(' structure: ', mRecentlyClosedTabsTreeStructure);
|
||||
|
||||
const newlyRestorable = mRecentlyClosedTabs.length > 1;
|
||||
if (currentlyRestorable != newlyRestorable)
|
||||
Tab.onChangeMultipleTabsRestorability.dispatch(newlyRestorable);
|
||||
|
||||
mPendingRecentlyClosedTabsInfo.tabs = [];
|
||||
mPendingRecentlyClosedTabsInfo.structure = [];
|
||||
});
|
||||
|
||||
let mToBeActivatedRestoredTabId = null;
|
||||
|
||||
function onRestoredTabActivated(activeInfo) {
|
||||
if (mToBeActivatedRestoredTabId &&
|
||||
activeInfo.id != mToBeActivatedRestoredTabId) {
|
||||
TabsInternalOperation.activateTab(mToBeActivatedRestoredTabId);
|
||||
}
|
||||
mToBeActivatedRestoredTabId = null;
|
||||
}
|
||||
|
||||
browser.tabs.onActivated.addListener(onRestoredTabActivated);
|
||||
|
||||
async function tryRestoreClosedSetFor(tab, countToBeRestored) {
|
||||
const lastRecentlyClosedTabs = mRecentlyClosedTabs;
|
||||
const lastRecentlyClosedTabsTreeStructure = mRecentlyClosedTabsTreeStructure;
|
||||
mRecentlyClosedTabs = [];
|
||||
mRecentlyClosedTabsTreeStructure = [];
|
||||
if (lastRecentlyClosedTabs.length > 1)
|
||||
Tab.onChangeMultipleTabsRestorability.dispatch(false);
|
||||
|
||||
if (!configs.undoMultipleTabsClose)
|
||||
return;
|
||||
|
||||
const alreadRestoredIndex = lastRecentlyClosedTabs.findIndex(info => info.uniqueId == tab.$TST.uniqueId.id && info.windowId == tab.windowId);
|
||||
log('tryRestoreClosedSetFor ', tab, lastRecentlyClosedTabs, lastRecentlyClosedTabsTreeStructure);
|
||||
if (alreadRestoredIndex < 0) {
|
||||
log(' => not a member of restorable tab set.');
|
||||
return;
|
||||
}
|
||||
|
||||
const toBeRestoredTabsCount = Math.min(
|
||||
typeof countToBeRestored == 'number' ? countToBeRestored : Number.MAX_SAFE_INTEGER,
|
||||
lastRecentlyClosedTabs.filter(tabInfo => tabInfo.uniqueId != tab.$TST.uniqueId.id).length
|
||||
);
|
||||
if (toBeRestoredTabsCount == 0) {
|
||||
log(' => no more tab to be restored.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sessions = (await browser.sessions.getRecentlyClosed({
|
||||
maxResults: browser.sessions.MAX_SESSION_RESULTS
|
||||
}).catch(ApiTabs.createErrorHandler())).filter(session => session.tab);
|
||||
|
||||
const canRestoreWithSession = toBeRestoredTabsCount <= sessions.length;
|
||||
|
||||
let restoredTabs;
|
||||
if (canRestoreWithSession) {
|
||||
log(`tryRestoreClosedSetFor: restore ${toBeRestoredTabsCount} tabs with the sessions API`);
|
||||
const unsortedRestoredTabs = await Commands.restoreTabs(toBeRestoredTabsCount);
|
||||
unsortedRestoredTabs.push(tab);
|
||||
// tabs can be restored in different order, then we need to rearrange them manually
|
||||
const tabsByUniqueId = new Map();
|
||||
for (const tab of unsortedRestoredTabs) {
|
||||
tabsByUniqueId.set(tab.$TST.uniqueId.id, tab);
|
||||
}
|
||||
restoredTabs = await Promise.all(lastRecentlyClosedTabsTreeStructure.map(tabInfo => {
|
||||
const restoredTab = tabsByUniqueId.get(tabInfo.id);
|
||||
if (restoredTab)
|
||||
return restoredTab;
|
||||
log('tryRestoreClosedSetFor: recreate tab for ', tabInfo);
|
||||
return TabsOpen.openURIInTab({
|
||||
title: tabInfo.title,
|
||||
url: tabInfo.url,
|
||||
cookieStoreId: tabInfo.cookieStoreId
|
||||
}, {
|
||||
windowId: tab.windowId,
|
||||
isOrphan: true,
|
||||
inBackground: true,
|
||||
discarded: true,
|
||||
fixPositions: true
|
||||
});
|
||||
}));
|
||||
let lastTab;
|
||||
for (const tab of restoredTabs) {
|
||||
if (lastTab && tab.$TST.previousTab != lastTab)
|
||||
await TabsMove.moveTabAfter(
|
||||
tab,
|
||||
lastTab,
|
||||
{ broadcast: true }
|
||||
);
|
||||
lastTab = tab;
|
||||
}
|
||||
}
|
||||
else {
|
||||
log('tryRestoreClosedSetFor: recreate tabs');
|
||||
const tabOpenOptions = {
|
||||
windowId: lastRecentlyClosedTabs[0].windowId,
|
||||
isOrphan: true,
|
||||
inBackground: true,
|
||||
discarded: true,
|
||||
fixPositions: true
|
||||
};
|
||||
const beforeTabs = await TabsOpen.openURIsInTabs(
|
||||
lastRecentlyClosedTabs.slice(0, alreadRestoredIndex).map(info => ({
|
||||
title: info.title,
|
||||
url: info.url,
|
||||
cookieStoreId: info.cookieStoreId
|
||||
})),
|
||||
tabOpenOptions
|
||||
);
|
||||
const afterTabs = await TabsOpen.openURIsInTabs(
|
||||
lastRecentlyClosedTabs.slice(alreadRestoredIndex + 1).map(info => ({
|
||||
title: info.title,
|
||||
url: info.url,
|
||||
cookieStoreId: info.cookieStoreId
|
||||
})),
|
||||
tabOpenOptions
|
||||
);
|
||||
// We need to move tabs after they are opened instead of
|
||||
// specifying the "index" option for TabsOpen.openURIsInTabs(),
|
||||
// because the given restored tab can be moved while this
|
||||
// operation.
|
||||
if (beforeTabs.length > 0)
|
||||
await TabsMove.moveTabsBefore(
|
||||
beforeTabs,
|
||||
tab,
|
||||
{ broadcast: true }
|
||||
);
|
||||
if (afterTabs.length > 0)
|
||||
await TabsMove.moveTabsAfter(
|
||||
afterTabs,
|
||||
tab,
|
||||
{ broadcast: true }
|
||||
);
|
||||
restoredTabs = [...beforeTabs, tab, ...afterTabs];
|
||||
await TabsInternalOperation.activateTab(restoredTabs[0]);
|
||||
}
|
||||
|
||||
const rootTabs = restoredTabs.filter((tab, index) => lastRecentlyClosedTabsTreeStructure[index].parent == TreeBehavior.STRUCTURE_KEEP_PARENT || lastRecentlyClosedTabsTreeStructure[index].parent == TreeBehavior.STRUCTURE_NO_PARENT);
|
||||
log(`tryRestoreClosedSetFor: rootTabs, restoredTabs = `, rootTabs, restoredTabs);
|
||||
for (const rootTab of rootTabs) {
|
||||
const referenceTabs = TreeBehavior.calculateReferenceItemsFromInsertionPosition(rootTab, {
|
||||
context: Constants.kINSERTION_CONTEXT_MOVED,
|
||||
insertAfter: rootTab.$TST.previousTab,
|
||||
insertBefore: restoredTabs[restoredTabs.length - 1].$TST.nextTab
|
||||
});
|
||||
log(`tryRestoreClosedSetFor: referenceTabs for ${rootTab.id} => `, referenceTabs);
|
||||
if (referenceTabs.parent)
|
||||
await Tree.attachTabTo(rootTab, referenceTabs.parent, {
|
||||
dontExpand: true,
|
||||
insertAfter: referenceTabs.insertAfter,
|
||||
dontMove: true,
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
|
||||
await Tree.applyTreeStructureToTabs(
|
||||
restoredTabs,
|
||||
lastRecentlyClosedTabsTreeStructure
|
||||
);
|
||||
|
||||
// Firefox itself activates the initially restored tab with delay,
|
||||
// so we need to activate the first tab of the restored tabs again.
|
||||
mToBeActivatedRestoredTabId = restoredTabs[0].id;
|
||||
wait(100).then(() => onRestoredTabActivated({ id: -1 })); // failsafe
|
||||
}
|
||||
2251
waterfox/browser/components/sidebar/background/tree.js
Normal file
2251
waterfox/browser/components/sidebar/background/tree.js
Normal file
File diff suppressed because it is too large
Load Diff
60
waterfox/browser/components/sidebar/common/MetricsData.js
Normal file
60
waterfox/browser/components/sidebar/common/MetricsData.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
# 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/.
|
||||
*/
|
||||
|
||||
export default class MetricsData {
|
||||
constructor(name) {
|
||||
this.mItems = [];
|
||||
this.mName = name || '';
|
||||
|
||||
const now = Date.now();
|
||||
this.mInitialTime = now;
|
||||
this.mLastTime = now;
|
||||
this.mDeltaBetweenLastItem = 0;
|
||||
}
|
||||
|
||||
add(label) {
|
||||
const now = Date.now();
|
||||
this.mItems.push({
|
||||
label: label,
|
||||
delta: now - this.mLastTime
|
||||
});
|
||||
this.mDeltaBetweenLastItem = now - this.mInitialTime;
|
||||
this.mLastTime = now;
|
||||
}
|
||||
|
||||
addAsync(label, asyncTask) {
|
||||
const start = Date.now();
|
||||
if (typeof asyncTask == 'function')
|
||||
asyncTask = asyncTask();
|
||||
return asyncTask.then(result => {
|
||||
this.mItems.push({
|
||||
label: `(async) ${label}`,
|
||||
delta: Date.now() - start,
|
||||
async: true
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
toString() {
|
||||
const logs = this.mItems.map(item => `${item.delta || 0}: ${item.label}`);
|
||||
return `${this.mName ? this.mName + ': ' : ''}total ${this.mDeltaBetweenLastItem} msec\n${logs.join('\n')}`;
|
||||
}
|
||||
|
||||
static add(label) {
|
||||
return MetricsData.$static.add(label);
|
||||
}
|
||||
|
||||
static addAsync(label, asyncTask) {
|
||||
return MetricsData.$static.addAsync(label, asyncTask);
|
||||
}
|
||||
|
||||
static toString() {
|
||||
return MetricsData.$static.toString();
|
||||
}
|
||||
}
|
||||
|
||||
MetricsData.$static = new MetricsData();
|
||||
3933
waterfox/browser/components/sidebar/common/TreeItem.js
Normal file
3933
waterfox/browser/components/sidebar/common/TreeItem.js
Normal file
File diff suppressed because it is too large
Load Diff
303
waterfox/browser/components/sidebar/common/Window.js
Normal file
303
waterfox/browser/components/sidebar/common/Window.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
configs
|
||||
} from './common.js';
|
||||
|
||||
import * as TabsStore from './tabs-store.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/Window', ...args);
|
||||
}
|
||||
|
||||
export default class Window {
|
||||
constructor(windowId, tabGroups) {
|
||||
const alreadyTracked = TabsStore.windows.get(windowId);
|
||||
if (alreadyTracked)
|
||||
return alreadyTracked;
|
||||
|
||||
log(`window ${windowId} is newly tracked`);
|
||||
|
||||
this.id = windowId;
|
||||
this.tabs = new Map();
|
||||
this.tabGroups = new Map();
|
||||
if (tabGroups) {
|
||||
this.initTabGroups(tabGroups);
|
||||
}
|
||||
this.order = [];
|
||||
|
||||
this.containerElement = null;
|
||||
this.containerClassList = null;
|
||||
this.pinnedContainerElement = null;
|
||||
|
||||
this.internalMovingTabs = new Map();
|
||||
this.alreadyMovedTabs = new Map();
|
||||
this.internalClosingTabs = new Set();
|
||||
this.keepDescendantsTabs = new Set();
|
||||
this.highlightingTabs = new Set();
|
||||
this.tabsToBeHighlightedAlone = new Set();
|
||||
this.internallyMovingTabsForUpdatedNativeTabGroups = new Set();
|
||||
|
||||
this.subTreeMovingCount =
|
||||
this.subTreeChildrenMovingCount =
|
||||
this.doingIntelligentlyCollapseExpandCount =
|
||||
this.duplicatingTabsCount = 0;
|
||||
|
||||
this.internallyFocusingTabs = new Set();
|
||||
this.internallyFocusingByMouseTabs = new Set();
|
||||
this.internallyFocusingSilentlyTabs = new Set();
|
||||
|
||||
this.preventToDetectTabBunchesUntil = Date.now() + configs.tabBunchesDetectionDelayOnNewWindow;
|
||||
|
||||
this.openingTabs = new Set();
|
||||
|
||||
this.openedNewTabs = new Map();
|
||||
|
||||
this.bypassTabControlCount = 0;
|
||||
this.toBeOpenedNewTabCommandTab = 0;
|
||||
this.toBeOpenedTabsWithPositions = 0;
|
||||
this.toBeOpenedTabsWithCookieStoreId = 0;
|
||||
this.toBeOpenedOrphanTabs = 0;
|
||||
|
||||
this.toBeAttachedTabs = new Set();
|
||||
this.toBeDetachedTabs = new Set();
|
||||
|
||||
this.lastRelatedTabs = new Map();
|
||||
this.previousLastRelatedTabs = new Map();
|
||||
|
||||
TabsStore.windows.set(windowId, this);
|
||||
TabsStore.prepareIndexesForWindow(windowId);
|
||||
|
||||
// We should initialize private properties with blank value for better performance with a fixed shape.
|
||||
this.delayedDestroy = null;
|
||||
}
|
||||
initTabGroups(tabGroups) {
|
||||
log(`initializing tabGroups of window ${this.id}: `, tabGroups);
|
||||
this.tabGroups = new Map((tabGroups || []).map(group => [group.id, group]));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const tab of this.tabs.values()) {
|
||||
if (tab.$TST)
|
||||
tab.$TST.destroy();
|
||||
}
|
||||
this.tabs.clear();
|
||||
this.tabGroups.clear();
|
||||
TabsStore.windows.delete(this.id);
|
||||
TabsStore.unprepareIndexesForWindow(this.id);
|
||||
|
||||
if (this.containerElement) {
|
||||
const element = this.containerElement;
|
||||
if (element.parentNode && !element.hasChildNodes())
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
if (this.pinnedContainerElement) {
|
||||
const element = this.element;
|
||||
if (element.parentNode && !element.hasChildNodes())
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
this.unbindElements();
|
||||
|
||||
this.tabs = null;
|
||||
this.tabGroups = null;
|
||||
this.order = null;
|
||||
this.id = null;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.tabs.clear();
|
||||
this.order = [];
|
||||
TabsStore.unprepareIndexesForWindow(this.id);
|
||||
TabsStore.prepareIndexesForWindow(this.id);
|
||||
this.clearLastRelatedTabs();
|
||||
}
|
||||
|
||||
clearLastRelatedTabs() {
|
||||
for (const openerId of this.lastRelatedTabs.keys()) {
|
||||
const opener = this.tabs.get(openerId);
|
||||
if (!opener)
|
||||
continue;
|
||||
opener.$TST.newRelatedTabsCount = 0;
|
||||
}
|
||||
this.lastRelatedTabs.clear();
|
||||
this.previousLastRelatedTabs.clear();
|
||||
}
|
||||
|
||||
bindContainerElement(element) {
|
||||
this.containerElement = element;
|
||||
this.containerClassList = element.classList;
|
||||
}
|
||||
|
||||
bindPinnedContainerElement(element) {
|
||||
this.pinnedContainerElement = element;
|
||||
this.pinnedContainerClassList = element.classList;
|
||||
}
|
||||
|
||||
unbindElements() {
|
||||
this.containerElement = null;
|
||||
this.containerClassList = null;
|
||||
this.pinnedContainerElement = null;
|
||||
this.pinnedContainerClassList = null;
|
||||
}
|
||||
|
||||
getOrderedTabs(startId, endId, tabs) {
|
||||
const orderedIds = this.sliceOrder(startId, endId);
|
||||
tabs = tabs || this.tabs;
|
||||
return (function*() {
|
||||
for (const id of orderedIds) {
|
||||
const tab = tabs.get(id);
|
||||
if (tab)
|
||||
yield tab;
|
||||
}
|
||||
}).call(this);
|
||||
}
|
||||
|
||||
getReversedOrderedTabs(startId, endId, tabs) {
|
||||
const orderedIds = this.sliceOrder(startId, endId, this.order.slice(0).reverse());
|
||||
tabs = tabs || this.tabs;
|
||||
return (function*() {
|
||||
for (const id of orderedIds) {
|
||||
const tab = tabs.get(id);
|
||||
if (tab)
|
||||
yield tab;
|
||||
}
|
||||
}).call(this);
|
||||
}
|
||||
|
||||
sliceOrder(startId, endId, orderedIds) {
|
||||
if (!orderedIds)
|
||||
orderedIds = this.order;
|
||||
if (startId) {
|
||||
if (!this.tabs.has(startId))
|
||||
return [];
|
||||
orderedIds = orderedIds.slice(orderedIds.indexOf(startId));
|
||||
}
|
||||
if (endId) {
|
||||
if (!this.tabs.has(endId))
|
||||
return [];
|
||||
orderedIds = orderedIds.slice(0, orderedIds.indexOf(endId) + 1);
|
||||
}
|
||||
return orderedIds;
|
||||
}
|
||||
|
||||
trackTab(tab) {
|
||||
const alreadyTracked = TabsStore.tabs.get(tab.id);
|
||||
if (alreadyTracked)
|
||||
tab = alreadyTracked;
|
||||
|
||||
if (this.delayedDestroy) {
|
||||
clearTimeout(this.delayedDestroy);
|
||||
this.delayedDestroy = null;
|
||||
}
|
||||
|
||||
const order = this.order;
|
||||
if (this.tabs.has(tab.id)) { // already tracked: update
|
||||
const prevState = tab.reindexedBy;
|
||||
const index = order.indexOf(tab.id);
|
||||
order.splice(index, 1);
|
||||
order.splice(tab.index, 0, tab.id);
|
||||
for (let i = Math.min(index, tab.index), maxi = Math.min(Math.max(index, tab.index) + 1, order.length); i < maxi; i++) {
|
||||
const tab = this.tabs.get(order[i]);
|
||||
if (!tab)
|
||||
throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
|
||||
tab.index = i;
|
||||
tab.reindexedBy = `Window.property.trackTab/update (${tab.index})`;
|
||||
tab.$TST.invalidateCache();
|
||||
}
|
||||
const parent = tab.$TST.parent;
|
||||
if (parent) {
|
||||
parent.$TST.sortAndInvalidateChildren();
|
||||
parent.$TST.invalidateCachedAncestors();
|
||||
}
|
||||
log(`tab ${dumpTab(tab)} is re-tracked under the window ${this.id}: `, prevState, index, '=>', tab.reindexedBy, order.join(', '));
|
||||
}
|
||||
else { // not tracked yet: add
|
||||
this.tabs.set(tab.id, tab);
|
||||
order.splice(tab.index, 0, tab.id);
|
||||
for (let i = tab.index + 1, maxi = order.length; i < maxi; i++) {
|
||||
const tab = this.tabs.get(order[i]);
|
||||
if (!tab)
|
||||
throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
|
||||
tab.index = i;
|
||||
tab.reindexedBy = `Window.property.trackTab/new (${tab.index})`;
|
||||
tab.$TST.invalidateCache();
|
||||
}
|
||||
log(`tab ${dumpTab(tab)} is newly tracked under the window ${this.id}: `, order);
|
||||
}
|
||||
TabsStore.updateIndexesForTab(tab);
|
||||
tab.$TST.invalidateCache();
|
||||
return tab;
|
||||
}
|
||||
|
||||
detachTab(tabId) {
|
||||
const tab = TabsStore.tabs.get(tabId);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
TabsStore.removeTabFromIndexes(tab);
|
||||
|
||||
tab.$TST.detach();
|
||||
this.tabs.delete(tabId);
|
||||
const order = this.order;
|
||||
const index = order.indexOf(tab.id);
|
||||
if (index < 0) // the tab is not tracked yet!
|
||||
return;
|
||||
order.splice(index, 1);
|
||||
if (this.tabs.size == 0) {
|
||||
if (!TabsStore.getCurrentWindowId()) { // only in the background page - the sidebar has no need to destroy itself manually.
|
||||
// the last tab can be removed with browser.tabs.closeWindowWithLastTab=false,
|
||||
// so we should not destroy the window immediately.
|
||||
if (this.delayedDestroy)
|
||||
clearTimeout(this.delayedDestroy);
|
||||
this.delayedDestroy = setTimeout(() => {
|
||||
if (this.tabs &&
|
||||
this.tabs.size == 0)
|
||||
this.destroy();
|
||||
}, (configs.collapseDuration, 1000) * 5);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = index, maxi = order.length; i < maxi; i++) {
|
||||
this.tabs.get(order[i]).index = i;
|
||||
}
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
untrackTab(tabId) {
|
||||
const tab = this.detachTab(tabId);
|
||||
if (tab)
|
||||
tab.$TST.destroy();
|
||||
}
|
||||
|
||||
export(full) {
|
||||
const tabs = [];
|
||||
for (const tab of this.getOrderedTabs()) {
|
||||
tabs.push(tab.$TST.export(full));
|
||||
}
|
||||
return {
|
||||
tabs,
|
||||
tabGroups: [...this.tabGroups.values()].map(group => group.$TST.sanitized),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Window.onInitialized = new EventListenerManager();
|
||||
|
||||
Window.init = (windowId, tabGroups) => {
|
||||
const win = TabsStore.windows.get(windowId) || new Window(windowId, tabGroups);
|
||||
if (tabGroups && tabGroups.size != win.tabGroups.size) {
|
||||
win.initTabGroups(tabGroups);
|
||||
}
|
||||
Window.onInitialized.dispatch(win);
|
||||
return win;
|
||||
}
|
||||
137
waterfox/browser/components/sidebar/common/api-tabs.js
Normal file
137
waterfox/browser/components/sidebar/common/api-tabs.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs
|
||||
} from './common.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/api-tabs', ...args);
|
||||
}
|
||||
|
||||
export async function getIndexes(...queriedTabIds) {
|
||||
log('getIndexes ', queriedTabIds);
|
||||
if (queriedTabIds.length == 0)
|
||||
return [];
|
||||
|
||||
const indexes = await Promise.all(queriedTabIds.map((tabId) => {
|
||||
return browser.tabs.get(tabId)
|
||||
.catch(e => {
|
||||
handleMissingTabError(e);
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
return indexes.map(tab => tab ? tab.index : -1);
|
||||
}
|
||||
|
||||
export function isMissingTabError(error) {
|
||||
return (
|
||||
error &&
|
||||
error.message &&
|
||||
error.message.includes('Invalid tab ID:')
|
||||
);
|
||||
}
|
||||
export function handleMissingTabError(error) {
|
||||
if (!isMissingTabError(error))
|
||||
throw error;
|
||||
// otherwise, this error is caused from a tab already closed.
|
||||
// we just ignore it.
|
||||
//console.log('Invalid Tab ID error on: ' + error.stack);
|
||||
}
|
||||
|
||||
export function isUnloadedError(error) {
|
||||
return (
|
||||
error &&
|
||||
error.message &&
|
||||
error.message.includes('can\'t access dead object')
|
||||
);
|
||||
}
|
||||
export function handleUnloadedError(error) {
|
||||
if (!isUnloadedError(error))
|
||||
throw error;
|
||||
}
|
||||
|
||||
export function isMissingHostPermissionError(error) {
|
||||
return (
|
||||
error &&
|
||||
error.message &&
|
||||
error.message.includes('Missing host permission for the tab')
|
||||
);
|
||||
}
|
||||
export function handleMissingHostPermissionError(error) {
|
||||
if (!isMissingHostPermissionError(error))
|
||||
throw error;
|
||||
}
|
||||
|
||||
export function createErrorHandler(...handlers) {
|
||||
const stack = configs.debug && new Error().stack;
|
||||
return (error) => {
|
||||
try {
|
||||
if (handlers.length > 0) {
|
||||
let unhandledCount = 0;
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(error);
|
||||
}
|
||||
catch(_error) {
|
||||
unhandledCount++;
|
||||
}
|
||||
});
|
||||
if (unhandledCount == handlers.length) // not handled
|
||||
throw error;
|
||||
}
|
||||
else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
catch(newError){
|
||||
if (!configs.debug)
|
||||
throw newError;
|
||||
if (error == newError)
|
||||
console.log('Unhandled Error: ', error, stack);
|
||||
else
|
||||
console.log('Unhandled Error: ', error, newError, stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createErrorSuppressor(...handlers) {
|
||||
const stack = configs.debug && new Error().stack;
|
||||
return (error) => {
|
||||
try {
|
||||
if (handlers.length > 0) {
|
||||
let unhandledCount = 0;
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(error);
|
||||
}
|
||||
catch(_error) {
|
||||
unhandledCount++;
|
||||
}
|
||||
});
|
||||
if (unhandledCount == handlers.length) // not handled
|
||||
throw error;
|
||||
}
|
||||
else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
catch(newError){
|
||||
if (error &&
|
||||
error.message &&
|
||||
error.message.indexOf('Could not establish connection. Receiving end does not exist.') == 0)
|
||||
return;
|
||||
if (!configs.debug)
|
||||
return;
|
||||
if (error == newError)
|
||||
console.log('Unhandled Error: ', error, stack);
|
||||
else
|
||||
console.log('Unhandled Error: ', error, newError, stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
1376
waterfox/browser/components/sidebar/common/bookmark.js
Normal file
1376
waterfox/browser/components/sidebar/common/bookmark.js
Normal file
File diff suppressed because it is too large
Load Diff
201
waterfox/browser/components/sidebar/common/browser-theme.js
Normal file
201
waterfox/browser/components/sidebar/common/browser-theme.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
configs
|
||||
} from '/common/common.js';
|
||||
import * as Color from './color.js';
|
||||
import * as Constants from './constants.js';
|
||||
|
||||
export function generateThemeRules(theme) {
|
||||
const rules = [];
|
||||
const generateCustomRule = (theme, prefix = '') => {
|
||||
for (const key of Object.keys(theme)) {
|
||||
if (!theme[key])
|
||||
continue;
|
||||
const propertyKey = prefix ? `${prefix}-${key}` : key;
|
||||
let value = theme[key];
|
||||
switch (typeof theme[key]) {
|
||||
case 'object':
|
||||
generateCustomRule(value, propertyKey);
|
||||
break;
|
||||
case 'string':
|
||||
if (/^[^:]+:\/\//.test(value))
|
||||
value = `url(${JSON.stringify(value)})`;
|
||||
rules.push(`--theme-${propertyKey}: ${value};`);
|
||||
for (let alpha = 10; alpha < 100; alpha += 10) {
|
||||
rules.push(`--theme-${propertyKey}-${alpha}: ${Color.overrideCSSAlpha(value, alpha / 100)};`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
generateCustomRule(theme);
|
||||
return rules.join('\n');
|
||||
}
|
||||
|
||||
export async function generateThemeDeclarations(theme) {
|
||||
if (!theme ||
|
||||
!theme.colors) {
|
||||
return `
|
||||
:root {
|
||||
/* https://searchfox.org/mozilla-central/rev/0c7c41109902cb8967ec3ef2c0ddb326701cfbee/browser/themes/windows/browser.css#15 */
|
||||
/* https://searchfox.org/mozilla-central/rev/0c7c41109902cb8967ec3ef2c0ddb326701cfbee/browser/themes/linux/browser.css#20 */
|
||||
--non-lwt-selected-tab-background-color-proton: rgba(255, 255, 255, 0.15);
|
||||
}`;
|
||||
}
|
||||
|
||||
const extraColors = [];
|
||||
const themeFrameColor = theme.colors.frame || theme.colors.accentcolor /* old name */;
|
||||
const inactiveTextColor = theme.colors.tab_background_text || theme.colors.textcolor /* old name */;
|
||||
const activeTextColor = theme.colors.tab_text || theme.colors.bookmark_text || theme.colors.toolbar_text /* old name */ || inactiveTextColor;
|
||||
let bgAlpha = 1;
|
||||
let hasImage = false;
|
||||
if (theme.images) {
|
||||
const isRightside = configs.sidebarPosition == Constants.kTABBAR_POSITION_RIGHT;
|
||||
const images = [];
|
||||
const frameImage = theme.images.theme_frame || theme.images.headerURL /* old name */;
|
||||
if (frameImage) {
|
||||
hasImage = true;
|
||||
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/browser/themes/shared/tabs.inc.css#537
|
||||
extraColors.push('--browser-bg-hover-for-header-image: rgba(0, 0, 0, 0.1);');
|
||||
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/browser/base/content/browser.css#20
|
||||
extraColors.push('--browser-bg-active-for-header-image: rgba(255, 255, 255, 0.4)');
|
||||
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/toolkit/themes/windows/global/global.css#138
|
||||
if (Color.isBrightColor(inactiveTextColor)) {
|
||||
// for bright text
|
||||
extraColors.push('--browser-textshadow-for-header-image: 1px 1px 1.5px black');
|
||||
}
|
||||
else {
|
||||
// for dark text
|
||||
extraColors.push('--browser-textshadow-for-header-image: 0 0 1.5px white');
|
||||
}
|
||||
images.push({
|
||||
url: frameImage,
|
||||
position: isRightside ? 'top right' : 'top left',
|
||||
repeat: 'no-repeat',
|
||||
});
|
||||
}
|
||||
|
||||
const positions = theme.properties?.additional_backgrounds_alignment || [];
|
||||
const repeats = theme.properties?.additional_backgrounds_tiling || [];
|
||||
if (Array.isArray(theme.images.additional_backgrounds) &&
|
||||
theme.images.additional_backgrounds.length > 0) {
|
||||
const leftImageCount = positions.filter(position => position.includes('left')).length;
|
||||
const rightImageCount = positions.filter(position => position.includes('right')).length;
|
||||
const repeatableImageCount = repeats.filter(repeat => repeat != 'no-repeat').length;
|
||||
for (let i = 0, maxi = theme.images.additional_backgrounds.length; i < maxi; i++) {
|
||||
const image = theme.images.additional_backgrounds[i];
|
||||
const position = positions.length > 0 && positions[Math.min(i, positions.length - 1)] || 'default';
|
||||
const repeat = repeats.length > 0 && repeats[Math.min(i, repeats.length - 1)] || 'default';
|
||||
if (repeatableImageCount > 0 &&
|
||||
repeat.includes('no-repeat'))
|
||||
continue;
|
||||
if (position &&
|
||||
position.includes('right') != isRightside &&
|
||||
repeat == 'no-repeat' &&
|
||||
leftImageCount > 0 &&
|
||||
rightImageCount > 0)
|
||||
continue;
|
||||
images.push({
|
||||
url: image,
|
||||
position,
|
||||
repeat,
|
||||
size: repeat == 'reepat-y' ? 'auto' : 'auto 100%',
|
||||
});
|
||||
}
|
||||
bgAlpha = 0.75;
|
||||
hasImage = true;
|
||||
}
|
||||
|
||||
await Promise.all(images.map(async image => {
|
||||
if (image.size)
|
||||
return;
|
||||
|
||||
const loader = new Image();
|
||||
try {
|
||||
const shouldRepeat = (
|
||||
theme.properties &&
|
||||
Array.isArray(theme.properties.additional_backgrounds_tiling) &&
|
||||
theme.properties.additional_backgrounds_tiling.some(value => value == 'repeat' || value == 'repeat-y')
|
||||
);
|
||||
const shouldNoRepeat = (
|
||||
!theme.properties ||
|
||||
!Array.isArray(theme.properties.additional_backgrounds_tiling) ||
|
||||
theme.properties.additional_backgrounds_tiling.some(value => value == 'no-repeat')
|
||||
);
|
||||
let maybeRepeatable = false;
|
||||
if (!shouldRepeat && !shouldNoRepeat) {
|
||||
await new Promise((resolve, reject) => {
|
||||
loader.addEventListener('load', resolve);
|
||||
loader.addEventListener('error', reject);
|
||||
loader.src = image;
|
||||
});
|
||||
maybeRepeatable = (loader.width / Math.max(1, loader.height)) <= configs.unrepeatableBGImageAspectRatio;
|
||||
}
|
||||
if (shouldNoRepeat)
|
||||
image.size = 'cover';
|
||||
else if (shouldRepeat || maybeRepeatable)
|
||||
image.size = 'auto';
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}));
|
||||
|
||||
if (hasImage) {
|
||||
extraColors.push('--browser-bg-images: ' + images.map(image => `url(${JSON.stringify(image.url)})`).join(','));
|
||||
extraColors.push('--browser-bg-position: ' + images.map(image => image.position).join(','));
|
||||
extraColors.push('--browser-bg-repeat: ' + images.map(image => image.repeat).join(','));
|
||||
extraColors.push('--browser-bg-size: ' + images.map(image => image.size).join(','));
|
||||
}
|
||||
}
|
||||
const themeBaseColor = Color.overrideCSSAlpha(themeFrameColor, bgAlpha);
|
||||
let toolbarColor = Color.mixCSSColors(themeBaseColor, 'rgba(255, 255, 255, 0.4)', bgAlpha);
|
||||
if (theme.colors.toolbar) {
|
||||
if (hasImage) {
|
||||
extraColors.push(`--browser-bg-for-header-image: ${theme.colors.toolbar};`);
|
||||
toolbarColor = theme.colors.toolbar;
|
||||
}
|
||||
else {
|
||||
toolbarColor = Color.mixCSSColors(themeBaseColor, theme.colors.toolbar);
|
||||
}
|
||||
extraColors.push(`--browser-toolbar: ${theme.colors.toolbar}`);
|
||||
if (Color.isParsable(theme.colors.toolbar) &&
|
||||
Color.isParsable(theme.colors.toolbar_text)) {
|
||||
const halfTransparentTextColor = Color.mixCSSColors(theme.colors.toolbar_text, theme.colors.toolbar_text, 0.5);
|
||||
extraColors.push(`--browser-toolbar_text-darker: ${Color.mixCSSColors(theme.colors.toolbar, halfTransparentTextColor)}`);
|
||||
}
|
||||
}
|
||||
else if (hasImage) {
|
||||
extraColors.push('--browser-bg-for-header-image: rgba(255, 255, 255, 0.25);');
|
||||
}
|
||||
if (theme.colors.tab_line)
|
||||
extraColors.push(`--browser-tab-highlighter: ${theme.colors.tab_line}`);
|
||||
if (theme.colors.tab_loading)
|
||||
extraColors.push(`--browser-loading-indicator: ${theme.colors.tab_loading}`);
|
||||
if (theme.colors.tab_selected)
|
||||
extraColors.push(`--browser-selected-tab-bg: ${theme.colors.tab_selected}`);
|
||||
extraColors.push(generateThemeRules(theme));
|
||||
return `
|
||||
:root {
|
||||
--browser-background: ${themeFrameColor};
|
||||
--browser-bg-base: ${toolbarColor};
|
||||
--browser-bg-less-lighter: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.05)', bgAlpha)};
|
||||
--browser-bg-lighter: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.1)', bgAlpha)};
|
||||
--browser-bg-more-lighter: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.25)', bgAlpha)};
|
||||
--browser-bg-lightest: ${Color.mixCSSColors(toolbarColor, 'rgba(255, 255, 255, 0.4)', bgAlpha)};
|
||||
--browser-bg-less-darker: ${Color.mixCSSColors(toolbarColor, 'rgba(0, 0, 0, 0.1)', bgAlpha)};
|
||||
--browser-bg-darker: ${Color.mixCSSColors(toolbarColor, 'rgba(0, 0, 0, 0.25)', bgAlpha)};
|
||||
--browser-bg-more-darker: ${Color.mixCSSColors(toolbarColor, 'rgba(0, 0, 0, 0.5)', bgAlpha)};
|
||||
--browser-fg: ${inactiveTextColor};
|
||||
--browser-fg-active: ${activeTextColor};
|
||||
--browser-border: ${Color.overrideCSSAlpha(inactiveTextColor, 0.4)};
|
||||
${extraColors.join(';\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
318
waterfox/browser/components/sidebar/common/cache-storage.js
Normal file
318
waterfox/browser/components/sidebar/common/cache-storage.js
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import * as UniqueId from '/common/unique-id.js';
|
||||
import {
|
||||
asyncRunWithTimeout,
|
||||
} from '/common/common.js';
|
||||
|
||||
const DB_NAME = 'PermanentStorage';
|
||||
const DB_VERSION = 3;
|
||||
const EXPIRATION_TIME_IN_MSEC = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const TIMEOUT_IN_MSEC = 1000 * 5; // 5 sec
|
||||
|
||||
export const BACKGROUND = 'backgroundCaches';
|
||||
const SIDEBAR = 'sidebarCaches'; // obsolete, but left here to delete old storage
|
||||
|
||||
let mOpenedDB;
|
||||
|
||||
async function openDB() {
|
||||
if (mOpenedDB)
|
||||
return mOpenedDB;
|
||||
return new Promise((resolve, _reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => {
|
||||
// This can fail if this is in a private window.
|
||||
// See: https://github.com/piroor/treestyletab/issues/3387
|
||||
//reject(new Error('Failed to open database'));
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
mOpenedDB = db;
|
||||
resolve(db);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
const objectStores = db.objectStoreNames;
|
||||
|
||||
const needToUpgrade = event.oldVersion < DB_VERSION;
|
||||
if (needToUpgrade) {
|
||||
if (objectStores.contains(BACKGROUND))
|
||||
db.deleteObjectStore(BACKGROUND);
|
||||
if (objectStores.contains(SIDEBAR))
|
||||
db.deleteObjectStore(SIDEBAR);
|
||||
}
|
||||
|
||||
if (needToUpgrade ||
|
||||
!objectStores.contains(BACKGROUND)) {
|
||||
const backgroundCachesStore = db.createObjectStore(BACKGROUND, { keyPath: 'key', unique: true });
|
||||
backgroundCachesStore.createIndex('windowId', 'windowId', { unique: false });
|
||||
backgroundCachesStore.createIndex('timestamp', 'timestamp');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function setValue({ windowId, key, value } = {}) {
|
||||
const [db, windowUniqueId] = await Promise.all([
|
||||
openDB(),
|
||||
UniqueId.ensureWindowId(windowId),
|
||||
]);
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
reserveToExpireOldEntries();
|
||||
|
||||
const store = BACKGROUND;
|
||||
|
||||
const cacheKey = `${windowUniqueId}-${key}`;
|
||||
asyncRunWithTimeout({
|
||||
task: () => new Promise((resolve, reject) => {
|
||||
const timestamp = Date.now();
|
||||
try {
|
||||
const transaction = db.transaction([store], 'readwrite');
|
||||
const cacheStore = transaction.objectStore(store);
|
||||
const cacheRequest = cacheStore.put({
|
||||
key: cacheKey,
|
||||
windowId: windowUniqueId,
|
||||
value,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
windowId = undefined;
|
||||
key = undefined;
|
||||
value = undefined;
|
||||
resolve();
|
||||
};
|
||||
|
||||
cacheRequest.onerror = event => {
|
||||
console.error(`Failed to store cache ${cacheKey} in the store ${store}`, event);
|
||||
reject(event);
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error(`Failed to store cache ${cacheKey} in the store ${store}`, error);
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
timeout: TIMEOUT_IN_MSEC,
|
||||
onTimedOut() {
|
||||
throw new Error(`CacheStorage.setValue for ${windowId}/key timed out`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteValue({ windowId, key } = {}) {
|
||||
const [db, windowUniqueId] = await Promise.all([
|
||||
openDB(),
|
||||
UniqueId.ensureWindowId(windowId),
|
||||
]);
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
reserveToExpireOldEntries();
|
||||
|
||||
const store = BACKGROUND;
|
||||
|
||||
const cacheKey = `${windowUniqueId}-${key}`;
|
||||
asyncRunWithTimeout({
|
||||
task: () => new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([store], 'readwrite');
|
||||
const cacheStore = transaction.objectStore(store);
|
||||
const cacheRequest = cacheStore.delete(cacheKey);
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
windowId = undefined;
|
||||
key = undefined;
|
||||
resolve();
|
||||
};
|
||||
|
||||
cacheRequest.onerror = event => {
|
||||
console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, event);
|
||||
reject(event);
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, error);
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
timeout: TIMEOUT_IN_MSEC,
|
||||
onTimedOut() {
|
||||
throw new Error(`CacheStorage.deleteValue for {windowId}/key timed out`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getValue({ windowId, key } = {}) {
|
||||
return asyncRunWithTimeout({
|
||||
task: () => getValueInternal({ windowId, key }),
|
||||
timeout: TIMEOUT_IN_MSEC,
|
||||
onTimedOut() {
|
||||
throw new Error(`CacheStorage.getValue for {windowId}/${key} timed out`);
|
||||
},
|
||||
});
|
||||
}
|
||||
async function getValueInternal({ windowId, key } = {}) {
|
||||
return new Promise(async (resolve, _reject) => {
|
||||
const [db, windowUniqueId] = await Promise.all([
|
||||
openDB(),
|
||||
UniqueId.ensureWindowId(windowId),
|
||||
]);
|
||||
if (!db) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const store = BACKGROUND;
|
||||
|
||||
const cacheKey = `${windowUniqueId}-${key}`;
|
||||
const timestamp = Date.now();
|
||||
try {
|
||||
const transaction = db.transaction([store], 'readwrite');
|
||||
const cacheStore = transaction.objectStore(store);
|
||||
|
||||
const cacheRequest = cacheStore.get(cacheKey);
|
||||
|
||||
cacheRequest.onsuccess = () => {
|
||||
const cache = cacheRequest.result;
|
||||
if (!cache) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
// IndexedDB does not support partial update, so
|
||||
// we need to put all properties not only timestamp.
|
||||
cacheStore.put({
|
||||
key: cacheKey,
|
||||
windowId: windowUniqueId,
|
||||
value: cache.value,
|
||||
timestamp,
|
||||
});
|
||||
resolve(cache.value);
|
||||
cache.key = undefined;
|
||||
cache.windowId = undefined;
|
||||
cache.value = undefined;
|
||||
};
|
||||
|
||||
cacheRequest.onerror = event => {
|
||||
console.error('Failed to get from cache:', event);
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
windowId = undefined;
|
||||
key = undefined;
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error('Failed to get from cache:', error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearForWindow(windowId) {
|
||||
return asyncRunWithTimeout({
|
||||
task: () => clearForWindowInternal(windowId),
|
||||
timeout: TIMEOUT_IN_MSEC,
|
||||
onTimedOut() {
|
||||
throw new Error(`CacheStorage.clearForWindow for ${windowId} timed out`);
|
||||
},
|
||||
});
|
||||
}
|
||||
async function clearForWindowInternal(windowId) {
|
||||
reserveToExpireOldEntries();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const [db, windowUniqueId] = await Promise.all([
|
||||
openDB(),
|
||||
UniqueId.ensureWindowId(windowId),
|
||||
]);
|
||||
if (!db) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transaction = db.transaction([BACKGROUND], 'readwrite');
|
||||
const backgroundCacheStore = transaction.objectStore(BACKGROUND);
|
||||
const backgroundCacheIndex = backgroundCacheStore.index('windowId');
|
||||
const backgroundCacheRequest = backgroundCacheIndex.openCursor(IDBKeyRange.only(windowUniqueId));
|
||||
backgroundCacheRequest.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (!cursor)
|
||||
return;
|
||||
const key = cursor.primaryKey;
|
||||
cursor.continue();
|
||||
backgroundCacheStore.delete(key);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error('Failed to clear caches:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function reserveToExpireOldEntries() {
|
||||
if (reserveToExpireOldEntries.reservedExpiration)
|
||||
clearTimeout(reserveToExpireOldEntries.reservedExpiration);
|
||||
reserveToExpireOldEntries.reservedExpiration = setTimeout(() => {
|
||||
reserveToExpireOldEntries.reservedExpiration = null;
|
||||
expireOldEntries();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function expireOldEntries() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const db = await openDB();
|
||||
if (!db) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transaction = db.transaction([BACKGROUND], 'readwrite');
|
||||
const backgroundCacheStore = transaction.objectStore(BACKGROUND);
|
||||
const backgroundCacheIndex = backgroundCacheStore.index('timestamp');
|
||||
|
||||
const expirationTimestamp = Date.now() - EXPIRATION_TIME_IN_MSEC;
|
||||
|
||||
const backgroundCacheRequest = backgroundCacheIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
|
||||
backgroundCacheRequest.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (!cursor)
|
||||
return;
|
||||
const key = cursor.primaryKey;
|
||||
cursor.continue();
|
||||
backgroundCacheStore.delete(key);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error('Failed to expire old entries:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
199
waterfox/browser/components/sidebar/common/color.js
Normal file
199
waterfox/browser/components/sidebar/common/color.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
export function mixCSSColors(base, over, alpha = 1) {
|
||||
base = parseCSSColor(base);
|
||||
over = parseCSSColor(over);
|
||||
const mixed = mixColors(base, over);
|
||||
return `rgba(${mixed.red}, ${mixed.green}, ${mixed.blue}, ${alpha})`;
|
||||
}
|
||||
|
||||
export function overrideCSSAlpha(color, alpha = 1) {
|
||||
const parsed = parseCSSColor(color);
|
||||
let base = color;
|
||||
let baseAlpha = parsed.alpha;
|
||||
if (baseAlpha > 0) {
|
||||
while (baseAlpha < 1) {
|
||||
base = mixCSSColors(base, base);
|
||||
baseAlpha *= 2;
|
||||
}
|
||||
}
|
||||
return mixCSSColors(base, base, alpha);
|
||||
}
|
||||
|
||||
function normalizeColorElement(value) {
|
||||
return Math.max(0, Math.min(255, value.endsWith('%') ? (parseFloat(value) / 100 * 255) : parseFloat(value)));
|
||||
}
|
||||
|
||||
function normalizeAlpha(value) {
|
||||
if (!value)
|
||||
return 1;
|
||||
return Math.max(0, Math.min(1, value.endsWith('%') ? (parseFloat(value) / 100) : parseFloat(value)));
|
||||
}
|
||||
|
||||
function normalizeAngle(value, unit) {
|
||||
value = parseFloat(value);
|
||||
switch ((unit || '').toLowerCase()) {
|
||||
case 'rad':
|
||||
value = value * (180 / Math.PI);
|
||||
break;
|
||||
|
||||
case 'grad':
|
||||
value = value * 0.9;
|
||||
break;
|
||||
|
||||
case 'turn':
|
||||
value = value * 360;
|
||||
break;
|
||||
|
||||
default: // deg
|
||||
break;
|
||||
}
|
||||
value = value % 360;
|
||||
if (value < 0)
|
||||
value += 360;
|
||||
return value;
|
||||
}
|
||||
|
||||
const HEX = '[0-9a-f]';
|
||||
const DOUBLE_HEX = `${HEX}{2}`;
|
||||
const FLOAT = '(?:\\.?[0-9]+|[0-9]+(?:\\.[0-9]+)?)';
|
||||
const FLOAT_OR_PERCENTAGE = `${FLOAT}%?`;
|
||||
|
||||
const HEX_RRGGBBAA_MATCHER = new RegExp(`^#?(${DOUBLE_HEX})(${DOUBLE_HEX})(${DOUBLE_HEX})(${DOUBLE_HEX})?$`, 'i');
|
||||
const HEX_RGBA_MATCHER = new RegExp(`^#?(${HEX})(${HEX})(${HEX})(${HEX})?$`, 'i');
|
||||
const RGBA_MATCHER = new RegExp(`^rgba?\\(\\s*(${FLOAT_OR_PERCENTAGE})\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})(?:\\s*[,/]?\\s*(${FLOAT_OR_PERCENTAGE})\\s*)?\\)$`, 'i');
|
||||
const HSLA_MATCHER = new RegExp(`^hsla?\\(\\s*(${FLOAT})(deg|rad|grad|turn)?\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})\\s*,?\\s*(${FLOAT_OR_PERCENTAGE})(?:\\s*[,/]?\\s*(${FLOAT_OR_PERCENTAGE})\\s*)?\\)$`, 'i');
|
||||
|
||||
export function isParsable(color) {
|
||||
if (!color)
|
||||
return false;
|
||||
const stringifiedColor = String(color);
|
||||
return HEX_RRGGBBAA_MATCHER.test(stringifiedColor) ||
|
||||
HEX_RGBA_MATCHER.test(stringifiedColor) ||
|
||||
RGBA_MATCHER.test(stringifiedColor) ||
|
||||
HSLA_MATCHER.test(stringifiedColor);
|
||||
}
|
||||
|
||||
export function parseCSSColor(color, baseColor) {
|
||||
if (typeof color!= 'string')
|
||||
return color;
|
||||
|
||||
let red, green, blue, alpha;
|
||||
|
||||
let parts = color.match(HEX_RRGGBBAA_MATCHER);
|
||||
if (parts) {
|
||||
red = parseInt(parts[1], 16);
|
||||
green = parseInt(parts[2], 16);
|
||||
blue = parseInt(parts[3], 16);
|
||||
alpha = parts[4] ? parseInt(parts[4], 16) / 255 : 1 ;
|
||||
}
|
||||
if (!parts) {
|
||||
// #RGB, #RGBA
|
||||
parts = color.match(HEX_RGBA_MATCHER);
|
||||
if (parts) {
|
||||
red = Math.min(255, Math.round(255 * (parseInt(parts[1], 16) / 16)));
|
||||
green = Math.min(255, Math.round(255 * (parseInt(parts[2], 16) / 16)));
|
||||
blue = Math.min(255, Math.round(255 * (parseInt(parts[3], 16) / 16)));
|
||||
alpha = parts[4] ? parseInt(parts[4], 16) / 16 : 1 ;
|
||||
}
|
||||
}
|
||||
if (!parts) {
|
||||
// rgb(), rgba()
|
||||
parts = color.match(RGBA_MATCHER);
|
||||
if (parts) {
|
||||
red = normalizeColorElement(parts[1]);
|
||||
green = normalizeColorElement(parts[2]);
|
||||
blue = normalizeColorElement(parts[3]);
|
||||
alpha = normalizeAlpha(parts[4]);
|
||||
}
|
||||
}
|
||||
if (!parts) {
|
||||
// hsl(), hsla()
|
||||
parts = color.match(HSLA_MATCHER);
|
||||
if (parts) {
|
||||
const hue = normalizeAngle(parts[1], parts[2]);
|
||||
const saturation = parseFloat(parts[3]);
|
||||
const lightness = parseFloat(parts[4]);
|
||||
let min, max;
|
||||
if (lightness < 50) {
|
||||
max = 2.55 * (lightness + (lightness * (saturation / 100)));
|
||||
min = 2.55 * (lightness - (lightness * (saturation / 100)));
|
||||
}
|
||||
else {
|
||||
max = 2.55 * (lightness + ((100 - lightness) * (saturation / 100)));
|
||||
min = 2.55 * (lightness - ((100 - lightness) * (saturation / 100)));
|
||||
}
|
||||
if (hue < 60) {
|
||||
red = max;
|
||||
green = (hue / 60) * (max - min) + min;
|
||||
blue = min;
|
||||
}
|
||||
else if (hue < 120) {
|
||||
red = ((120 - hue) / 60) * (max - min) + min;
|
||||
green = max;
|
||||
blue = min;
|
||||
}
|
||||
else if (hue < 180) {
|
||||
red = min;
|
||||
green = max;
|
||||
blue = ((hue - 120) / 60) * (max - min) + min;
|
||||
}
|
||||
else if (hue < 240) {
|
||||
red = min;
|
||||
green = ((240 - hue) / 60) * (max - min) + min;
|
||||
blue = max;
|
||||
}
|
||||
else if (hue < 300) {
|
||||
red = ((hue - 240) / 60) * (max - min) + min;
|
||||
green = min;
|
||||
blue = max;
|
||||
}
|
||||
else {
|
||||
red = max;
|
||||
green = min;
|
||||
blue = ((360 - hue) / 60) * (max - min) + min;
|
||||
}
|
||||
alpha = normalizeAlpha(parts[5]);
|
||||
}
|
||||
}
|
||||
if (!parts) {
|
||||
switch(color.toLowerCase()) {
|
||||
case 'transparent':
|
||||
red = 0;
|
||||
green = 0;
|
||||
blue = 0;
|
||||
alpha = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = { red, green, blue, alpha };
|
||||
|
||||
if (alpha < 1 && baseColor)
|
||||
return mixColors(parseCSSColor(baseColor), parsed);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function mixColors(base, over) {
|
||||
const alpha = over.alpha;
|
||||
const red = Math.min(255, Math.round((base.red * (1 - alpha)) + (over.red * alpha)));
|
||||
const green = Math.min(255, Math.round((base.green * (1 - alpha)) + (over.green * alpha)));
|
||||
const blue = Math.min(255, Math.round((base.blue * (1 - alpha)) + (over.blue * alpha)));
|
||||
return { red, green, blue, alpha: 1 };
|
||||
}
|
||||
|
||||
export function isBrightColor(color) {
|
||||
color = parseCSSColor(color);
|
||||
// https://searchfox.org/mozilla-central/rev/532e4b94b9e807d157ba8e55034aef05c1196dc9/browser/base/content/browser.js#8200
|
||||
const luminance = (color.red * 0.2125) + (color.green * 0.7154) + (color.blue * 0.0721);
|
||||
return luminance > 110;
|
||||
}
|
||||
1289
waterfox/browser/components/sidebar/common/common.js
Normal file
1289
waterfox/browser/components/sidebar/common/common.js
Normal file
File diff suppressed because it is too large
Load Diff
482
waterfox/browser/components/sidebar/common/constants.js
Normal file
482
waterfox/browser/components/sidebar/common/constants.js
Normal file
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
export const kCOMMAND_GET_INSTANCE_ID = 'ws:get-instance-id';
|
||||
export const kCOMMAND_RELOAD = 'ws:reload';
|
||||
export const kCOMMAND_PING_TO_BACKGROUND = 'ws:ping-to-background';
|
||||
export const kCOMMAND_PING_TO_SIDEBAR = 'ws:ping-to-sidebar';
|
||||
export const kCOMMAND_REQUEST_CONNECT_PREFIX = 'ws:request-connect-from:';
|
||||
export const kCOMMAND_REQUEST_UNIQUE_ID = 'ws:request-unique-id';
|
||||
export const kCOMMAND_GET_THEME_DECLARATIONS = 'ws:get-theme-declarations';
|
||||
export const kCOMMAND_GET_CONTEXTUAL_IDENTITIES_COLOR_INFO = 'ws:get-contextual-identities-color-info';
|
||||
export const kCOMMAND_GET_CONFIG_VALUE = 'ws:get-config-value';
|
||||
export const kCOMMAND_SET_CONFIG_VALUE = 'ws:set-config-value';
|
||||
export const kCOMMAND_GET_USER_STYLE_RULES = 'ws:get-user-style-rules';
|
||||
export const kCOMMAND_PULL_TABS = 'ws:pull-tabs';
|
||||
export const kCOMMAND_SYNC_TABS_ORDER = 'ws:sync-tabs-order';
|
||||
export const kCOMMAND_PULL_TABS_ORDER = 'ws:pull-tabs-order';
|
||||
export const kCOMMAND_PULL_TREE_STRUCTURE = 'ws:pull-tree-structure';
|
||||
export const kCOMMAND_GET_RENDERED_TAB_IDS = 'ws:get-rendered-tab-ids';
|
||||
export const kCOMMAND_ASK_TAB_IS_IN_VIEWPORT = 'ws:ask-tab-is-in-viewport';
|
||||
export const kCOMMAND_LOAD_URI = 'ws:load-uri';
|
||||
export const kCOMMAND_OPEN_TAB = 'ws:open-tab';
|
||||
export const kCOMMAND_NEW_WINDOW_FROM_TABS = 'ws:open-new-window-from-tabs';
|
||||
export const kCOMMAND_NEW_TABS = 'ws:open-new-tabs';
|
||||
export const kCOMMAND_NEW_TAB_AS = 'ws:open-new-tab-as';
|
||||
export const kCOMMAND_REMOVE_TABS_BY_MOUSE_OPERATION = 'ws:remove-tabs-by-mouse-operation';
|
||||
export const kCOMMAND_REMOVE_TABS_INTERNALLY = 'ws:remove-tabs-internally';
|
||||
export const kCOMMAND_UPDATE_LOADING_STATE = 'ws:update-loading-state';
|
||||
export const kCOMMAND_CONFIRM_TO_CLOSE_TABS = 'ws:confirm-to-close-tabs';
|
||||
export const kCOMMAND_SHOW_DIALOG = 'ws:show-dialog';
|
||||
export const kCOMMAND_NOTIFY_BACKGROUND_READY = 'ws:notify-background-ready';
|
||||
export const kCOMMAND_NOTIFY_CONNECTION_READY = 'ws:notify-connection-ready';
|
||||
export const kCOMMAND_NOTIFY_SIDEBAR_CLOSED = 'ws:notify-sidebar-closed';
|
||||
export const kCOMMAND_NOTIFY_TAB_CREATING = 'ws:notify-tab-creating';
|
||||
export const kCOMMAND_NOTIFY_TAB_CREATED = 'ws:notify-tab-created';
|
||||
export const kCOMMAND_NOTIFY_TAB_UPDATED = 'ws:notify-tab-updated';
|
||||
export const kCOMMAND_NOTIFY_TAB_MOVED = 'ws:notify-tab-moved';
|
||||
export const kCOMMAND_NOTIFY_TAB_INTERNALLY_MOVED = 'ws:notify-tab-internally-moved';
|
||||
export const kCOMMAND_NOTIFY_TAB_REMOVING = 'ws:notify-tab-removing';
|
||||
export const kCOMMAND_NOTIFY_TAB_REMOVED = 'ws:notify-tab-removed';
|
||||
export const kCOMMAND_NOTIFY_TAB_ACTIVATING = 'ws:notify-tab-activating';
|
||||
export const kCOMMAND_NOTIFY_TAB_ACTIVATED = 'ws:notify-tab-activated';
|
||||
export const kCOMMAND_NOTIFY_MAY_START_TAB_SWITCH = 'ws:notify-may-start-tab-switch';
|
||||
export const kCOMMAND_NOTIFY_MAY_END_TAB_SWITCH = 'ws:notify-may-end-tab-switch';
|
||||
export const kCOMMAND_NOTIFY_TAB_ATTACHED_TO_WINDOW = 'ws:notify-tab-attached-to-window';
|
||||
export const kCOMMAND_NOTIFY_TAB_DETACHED_FROM_WINDOW = 'ws:notify-tab-detached-from-window';
|
||||
export const kCOMMAND_NOTIFY_PERMISSIONS_GRANTED = 'ws:notify-permissions-granted';
|
||||
export const kCOMMAND_NOTIFY_TAB_PINNED = 'ws:notify-tab-pinned';
|
||||
export const kCOMMAND_NOTIFY_TAB_UNPINNED = 'ws:notify-tab-unpinned';
|
||||
export const kCOMMAND_NOTIFY_TAB_SHOWN = 'ws:notify-tab-shown';
|
||||
export const kCOMMAND_NOTIFY_TAB_HIDDEN = 'ws:notify-tab-hidden';
|
||||
export const kCOMMAND_NOTIFY_TAB_RESTORING = 'ws:notify-tab-restoring';
|
||||
export const kCOMMAND_NOTIFY_TAB_RESTORED = 'ws:notify-tab-restored';
|
||||
export const kCOMMAND_NOTIFY_TREE_ITEM_LABEL_UPDATED = 'ws:notify-tab-label-updated';
|
||||
export const kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED = 'ws:notify-tab-favicon-updated';
|
||||
export const kCOMMAND_NOTIFY_TAB_SOUND_STATE_UPDATED = 'ws:notify-tab-sound-state-updated';
|
||||
export const kCOMMAND_NOTIFY_TAB_SHARING_STATE_UPDATED = 'ws:notify-tab-sharing-state-updated';
|
||||
export const kCOMMAND_NOTIFY_TABS_CLOSING = 'ws:notify-tabs-closing';
|
||||
export const kCOMMAND_NOTIFY_HIGHLIGHTED_TABS_CHANGED = 'ws:notify-highlighted-tabs-changed';
|
||||
export const kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_IN_PROGRESS = 'ws:notify-tabs-highlighting-in-progress';
|
||||
export const kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_COMPLETE = 'ws:notify-tabs-highlighting-complete';
|
||||
export const kCOMMAND_NOTIFY_GROUP_TAB_DETECTED = 'ws:notify-group-tab-detected';
|
||||
export const kCOMMAND_NOTIFY_CHILDREN_CHANGED = 'ws:notify-children-changed';
|
||||
export const kCOMMAND_NOTIFY_TAB_COLLAPSED_STATE_CHANGED = 'ws:notify-tab-collapsed-state-changed';
|
||||
export const kCOMMAND_NOTIFY_SUBTREE_COLLAPSED_STATE_CHANGING = 'ws:notify-subtree-collapsed-state-changing';
|
||||
export const kCOMMAND_NOTIFY_SUBTREE_COLLAPSED_STATE_CHANGED = 'ws:notify-subtree-collapsed-state-changed';
|
||||
export const kCOMMAND_NOTIFY_TAB_GROUP_CREATED = 'ws:notify-tab-group-created';
|
||||
export const kCOMMAND_NOTIFY_TAB_GROUP_UPDATED = 'ws:notify-tab-group-updated';
|
||||
export const kCOMMAND_NOTIFY_TAB_GROUP_REMOVED = 'ws:notify-tab-group-removed';
|
||||
export const kCOMMAND_SET_SUBTREE_COLLAPSED_STATE = 'ws:set-subtree-collapsed-state';
|
||||
export const kCOMMAND_SET_SUBTREE_COLLAPSED_STATE_INTELLIGENTLY_FOR = 'ws:set-subtree-collapsed-state-intelligently-for';
|
||||
export const kCOMMAND_TOGGLE_STICKY = 'ws:toggle-sticky';
|
||||
export const kCOMMAND_NOTIFY_TAB_LEVEL_CHANGED = 'ws:notify-tab-level-changed';
|
||||
export const kCOMMAND_NOTIFY_TAB_ATTACHED_COMPLETELY = 'ws:notify-tab-attached-completely';
|
||||
export const kCOMMAND_BROADCAST_CURRENT_DRAG_DATA = 'ws:broadcast-current-drag-data';
|
||||
export const kCOMMAND_SHOW_CONTAINER_SELECTOR = 'ws:show-container-selector';
|
||||
export const kCOMMAND_SCROLL_TABBAR = 'ws:scroll-tabbar';
|
||||
export const kCOMMAND_TOGGLE_SUBPANEL = 'ws:toggle-subpanel';
|
||||
export const kCOMMAND_SWITCH_SUBPANEL = 'ws:switch-subpanel';
|
||||
export const kCOMMAND_INCREASE_SUBPANEL = 'ws:increase-subpanel';
|
||||
export const kCOMMAND_DECREASE_SUBPANEL = 'ws:decrease-subpanel';
|
||||
export const kCOMMAND_REQUEST_QUERY_LOGS = 'ws:request-query-logs';
|
||||
export const kCOMMAND_RESPONSE_QUERY_LOGS = 'ws:response-query-logs';
|
||||
export const kCOMMAND_REQUEST_CONNECTION_MESSAGE_LOGS = 'ws:request-connection-message-logs';
|
||||
export const kCOMMAND_RESPONSE_CONNECTION_MESSAGE_LOGS = 'ws:response-connection-message-logs';
|
||||
export const kCOMMAND_NOTIFY_TEST_KEY_CHANGED = 'ws:notify-test-key-changed';
|
||||
export const kCOMMAND_SIMULATE_SIDEBAR_MESSAGE = 'ws:simulate-sidebar-message';
|
||||
export const kCOMMAND_GET_CONTEXT_MENU_ITEMS = 'ws:contextMenu-get-items';
|
||||
export const kCOMMAND_NOTIFY_CONTEXT_MENU_UPDATED = 'ws:contextMenu-updated';
|
||||
export const kCOMMAND_NOTIFY_CONTEXT_ITEM_CHECKED_STATUS_CHANGED = 'ws:contextMenu-item-checked-status-changed';
|
||||
export const kCOMMAND_NOTIFY_CONTEXT_OVERRIDDEN = 'ws:notify-context-overridden';
|
||||
export const kCOMMAND_AUTODETECT_DUPLICATED_TAB_DETECTION_DELAY = 'ws:autodetect-duplicated-tab-detection-delay';
|
||||
export const kCOMMAND_TEST_DUPLICATED_TAB_DETECTION = 'ws:test-duplicated-tab-detection';
|
||||
export const kCOMMAND_WAIT_UNTIL_SUCCESSORS_UPDATED = 'ws:wait-until-successors-updated';
|
||||
export const kCOMMAND_GET_SIDEBAR_POSITION = 'ws:get-sidebar-position';
|
||||
export const kCOMMAND_GET_ABOVE_TAB = 'ws:get-above-tab';
|
||||
export const kCOMMAND_GET_BELOW_TAB = 'ws:get-below-tab';
|
||||
export const kCOMMAND_GET_LEFT_TAB = 'ws:get-left-tab';
|
||||
export const kCOMMAND_GET_RIGHT_TAB = 'ws:get-right-tab';
|
||||
export const kCOMMAND_GET_BOUNDING_CLIENT_RECT = 'ws:get-bounding-client-rect';
|
||||
export const kCOMMAND_SHOW_NATIVE_TAB_GROUP_MENU_PANEL = 'ws:show-native-tab-group-menu-panel';
|
||||
export const kCOMMAND_UPDATE_NATIVE_TAB_GROUP = 'ws:update-native-tab-group';
|
||||
export const kCOMMAND_INVOKE_NATIVE_TAB_GROUP_MENU_PANEL_COMMAND = 'ws:invoke-native-tab-group-menu-panel-command';
|
||||
export const kCOMMAND_NEW_WINDOW_FROM_NATIVE_TAB_GROUP = 'ws:new-window-from-native-tab-group';
|
||||
|
||||
export const kCOMMAND_ACTIVATE_TAB = 'ws:activate-tab';
|
||||
export const kCOMMAND_HIGHLIGHT_TABS = 'ws:highlight-tabs';
|
||||
export const kCOMMAND_TOGGLE_MUTED_FROM_SOUND_BUTTON = 'ws:toggle-muted-from-sound-button';
|
||||
export const kCOMMAND_UNBLOCK_AUTOPLAY_FROM_SOUND_BUTTON = 'ws:unblock-autoplay-from-sound-button';
|
||||
export const kCOMMAND_PERFORM_TABS_DRAG_DROP = 'ws:perform-tabs-drag-drop';
|
||||
export const kCOMMAND_BLOCK_USER_OPERATIONS = 'ws:block-user-operations';
|
||||
export const kCOMMAND_UNBLOCK_USER_OPERATIONS = 'ws:unblock-user-operations';
|
||||
export const kCOMMAND_PROGRESS_USER_OPERATIONS = 'ws:progress-user-operations';
|
||||
export const kCOMMAND_BROADCAST_TAB_STATE = 'ws:broadcast-tab-state';
|
||||
export const kCOMMAND_BROADCAST_TAB_TOOLTIP_TEXT = 'ws:broadcast-tab-tooltip-text';
|
||||
export const kCOMMAND_BROADCAST_TAB_AUTO_STICKY_STATE = 'ws:broadcast-tab-auto-sticky-state';
|
||||
|
||||
export const kCOMMAND_BOOKMARK_TAB_WITH_DIALOG = 'ws:bookmark-tab-with-dialog';
|
||||
export const kCOMMAND_BOOKMARK_TABS_WITH_DIALOG = 'ws:bookmark-tabs-with-dialog';
|
||||
|
||||
export const kNOTIFY_TAB_MOUSEDOWN = 'ws:tab-mousedown';
|
||||
export const kNOTIFY_TAB_MOUSEDOWN_EXPIRED = 'ws:tab-mousedown-expired';
|
||||
|
||||
export const kNOTIFY_SIDEBAR_FOCUS = 'ws:sidebar-focus';
|
||||
export const kNOTIFY_SIDEBAR_BLUR = 'ws:sidebar-blur';
|
||||
|
||||
export const kNOTIFY_CONFIRMATION_DIALOG_READY = 'ws:confirmation-dialog-ready';
|
||||
|
||||
export const kCONNECTION_HEARTBEAT = 'ws:connection-heartbeat';
|
||||
|
||||
export const kAPI_TAB_ID = 'data-tab-id';
|
||||
export const kAPI_WINDOW_ID = 'data-window-id';
|
||||
export const kAPI_NATIVE_TAB_GROUP_ID = 'data-native-tab-group-id';
|
||||
|
||||
export const kGROUP_ID = 'data-group-id';
|
||||
export const kPARENT = 'data-parent-id';
|
||||
export const kCHILDREN = 'data-child-ids';
|
||||
export const kLEVEL = 'data-level';
|
||||
export const kCLOSED_SET_ID = 'data-closed-set-id';
|
||||
export const kCURRENT_URI = 'data-current-uri';
|
||||
export const kCURRENT_FAVICON_URI = 'data-current-favicon-uri';
|
||||
export const kCONTEXTUAL_IDENTITY_NAME = 'data-contextual-identity-name';
|
||||
export const kMAX_TREE_LEVEL = 'data-max-tree-level';
|
||||
export const kLABEL_OVERFLOW = 'data-label-overflow';
|
||||
|
||||
export const kPERSISTENT_ID = 'data-persistent-id';
|
||||
export const kPERSISTENT_ANCESTORS = 'ancestors';
|
||||
export const kPERSISTENT_CHILDREN = 'children';
|
||||
export const kPERSISTENT_INSERT_BEFORE = 'insert-before';
|
||||
export const kPERSISTENT_INSERT_AFTER = 'insert-after';
|
||||
export const kPERSISTENT_INSERT_AFTER_LEGACY = 'isnert-after';
|
||||
export const kPERSISTENT_STATES = 'special-tab-states';
|
||||
export const kPERSISTENT_SUBTREE_COLLAPSED = 'subtree-collapsed'; // obsolete
|
||||
export const kPERSISTENT_ORIGINAL_OPENER_TAB_ID = 'data-original-opener-tab-id';
|
||||
export const kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER = 'data-already-grouped-for-pinned-opener';
|
||||
|
||||
export const kFAVICON_IMAGE = 'favicon-image';
|
||||
export const kFAVICON_BUILTIN = 'favicon-builtin';
|
||||
export const kFAVICON_DEFAULT = 'favicon-default'; // just for backward compatibility, and this should be removed from future versions
|
||||
export const kFAVICON_SHARING_STATE = 'favicon-sharing-state';
|
||||
export const kFAVICON_STICKY_STATE = 'favicon-sticky-state';
|
||||
export const kBACKGROUND = 'background';
|
||||
export const kTHROBBER = 'throbber';
|
||||
export const kHIGHLIGHTER = 'highlighter';
|
||||
export const kBURSTER = 'burster';
|
||||
export const kNEWTAB_BUTTON = 'newtab-button';
|
||||
export const kEXTRA_ITEMS_CONTAINER = 'extra-items-container';
|
||||
export const kCONTEXTUAL_IDENTITY_MARKER = 'contextual-identity-marker';
|
||||
export const kCONTEXTUAL_IDENTITY_SELECTOR = 'contextual-identities-selector';
|
||||
export const kCONTEXTUAL_IDENTITY_SELECTOR_CONTEXT_MENU = 'contextual-identities-selector-context';
|
||||
export const kNEWTAB_ACTION_SELECTOR = 'newtab-action-selector';
|
||||
export const kTABBAR_SPACER = 'tabs-spacer';
|
||||
|
||||
export const kTAB_STATE_ACTIVE = 'active';
|
||||
export const kTAB_STATE_PINNED = 'pinned';
|
||||
export const kTAB_STATE_LAST_ROW = 'last-row';
|
||||
export const kTAB_STATE_LAST_VISIBLE = 'last-visible';
|
||||
export const kTAB_STATE_AUDIBLE = 'audible';
|
||||
export const kTAB_STATE_SOUND_PLAYING = 'sound-playing';
|
||||
export const kTAB_STATE_HAS_SOUND_PLAYING_MEMBER = 'has-sound-playing-member';
|
||||
export const kTAB_STATE_MUTED = 'muted';
|
||||
export const kTAB_STATE_HAS_MUTED_MEMBER = 'has-muted-member';
|
||||
export const kTAB_STATE_AUTOPLAY_BLOCKED = 'autoplay-blocked';
|
||||
export const kTAB_STATE_HAS_AUTOPLAY_BLOCKED_MEMBER = 'has-autoplay-blocked-member';
|
||||
export const kTAB_STATE_PRIVATE_BROWSING = 'private-browsing';
|
||||
export const kTAB_STATE_HIDDEN = 'hidden';
|
||||
export const kTAB_STATE_SHARING_CAMERA = 'sharing-camera';
|
||||
export const kTAB_STATE_SHARING_MICROPHONE = 'sharing-microphone';
|
||||
export const kTAB_STATE_SHARING_SCREEN = 'sharing-screen';
|
||||
export const kTAB_STATE_HAS_SHARING_CAMERA_MEMBER = 'has-sharing-camera-member';
|
||||
export const kTAB_STATE_HAS_SHARING_MICROPHONE_MEMBER = 'has-sharing-microphone-member';
|
||||
export const kTAB_STATE_HAS_SHARING_SCREEN_MEMBER = 'has-sharing-screen-member';
|
||||
export const kTAB_STATE_ANIMATION_READY = 'animation-ready';
|
||||
export const kTAB_STATE_NOT_ACTIVATED_SINCE_LOAD = 'not-activated-since-load';
|
||||
export const kTAB_STATE_BURSTING = 'bursting';
|
||||
export const kTAB_STATE_CREATING = 'creating';
|
||||
export const kTAB_STATE_TO_BE_REMOVED = 'to-be-removed';
|
||||
export const kTAB_STATE_REMOVING = 'removing';
|
||||
export const kTAB_STATE_COLLAPSED = 'collapsed';
|
||||
export const kTAB_STATE_COLLAPSED_DONE = 'collapsed-completely';
|
||||
export const kTAB_STATE_COLLAPSING = 'collapsing';
|
||||
export const kTAB_STATE_EXPANDING = 'expanding';
|
||||
export const kTAB_STATE_MOVING = 'moving';
|
||||
export const kTAB_STATE_SHOWING = 'showing';
|
||||
export const kTAB_STATE_SUBTREE_COLLAPSED = 'subtree-collapsed';
|
||||
export const kTAB_STATE_SUBTREE_EXPANDED_MANUALLY = 'subtree-expanded-manually';
|
||||
export const kTAB_STATE_FAVICONIZED = 'faviconized';
|
||||
export const kTAB_STATE_UNREAD = 'unread';
|
||||
export const kTAB_STATE_PENDING = 'pending';
|
||||
export const kTAB_STATE_HIGHLIGHTED = 'highlighted';
|
||||
export const kTAB_STATE_BUNDLED_ACTIVE = 'bundled-active';
|
||||
export const kTAB_STATE_SOME_DESCENDANTS_HIGHLIGHTED = 'some-descendants-highlighted';
|
||||
export const kTAB_STATE_ALL_DESCENDANTS_HIGHLIGHTED = 'all-descendants-highlighted';
|
||||
export const kTAB_STATE_ATTENTION = 'attention';
|
||||
export const kTAB_STATE_DISCARDED = 'discarded';
|
||||
export const kTAB_STATE_SELECTED = 'selected';
|
||||
export const kTAB_STATE_DRAGGING = 'dragging';
|
||||
export const kTAB_STATE_DUPLICATING = 'duplicating';
|
||||
export const kTAB_STATE_DUPLICATED = 'duplicated';
|
||||
export const kTAB_STATE_RESTORED = 'restored';
|
||||
export const kTAB_STATE_FROM_EXTERNAL = 'from-external';
|
||||
export const kTAB_STATE_FROM_FIREFOX_VIEW = 'from-firefox-view';
|
||||
export const kTAB_STATE_THROBBER_UNSYNCHRONIZED = 'throbber-unsynchronized';
|
||||
export const kTAB_STATE_GROUP_TAB = 'group-tab';
|
||||
export const kTAB_STATE_NEW_TAB_COMMAND_TAB = 'newtab-command-tab';
|
||||
export const kTAB_STATE_OPENED_FOR_SAME_WEBSITE = 'opened-for-same-website';
|
||||
export const kTAB_STATE_STICKY = 'sticky';
|
||||
export const kTAB_STATE_STUCK = 'stuck'; // virtual state
|
||||
export const kTAB_INTERNAL_STATES = new Set([ // TST specific states
|
||||
'tab',
|
||||
kTAB_STATE_LAST_ROW,
|
||||
kTAB_STATE_LAST_VISIBLE,
|
||||
kTAB_STATE_ANIMATION_READY,
|
||||
kTAB_STATE_COLLAPSED_DONE,
|
||||
kTAB_STATE_CREATING,
|
||||
kTAB_STATE_DUPLICATING,
|
||||
kTAB_STATE_COLLAPSING,
|
||||
kTAB_STATE_EXPANDING,
|
||||
kTAB_STATE_MOVING,
|
||||
kTAB_STATE_TO_BE_REMOVED,
|
||||
kTAB_STATE_REMOVING,
|
||||
kTAB_STATE_SHOWING,
|
||||
kTAB_STATE_THROBBER_UNSYNCHRONIZED,
|
||||
kTAB_STATE_NEW_TAB_COMMAND_TAB,
|
||||
kTAB_STATE_DUPLICATED,
|
||||
kTAB_STATE_RESTORED,
|
||||
kTAB_STATE_FROM_EXTERNAL,
|
||||
kTAB_STATE_FROM_FIREFOX_VIEW,
|
||||
kTAB_STATE_OPENED_FOR_SAME_WEBSITE,
|
||||
kTAB_STATE_STICKY,
|
||||
kTAB_STATE_STUCK,
|
||||
kTAB_STATE_PENDING,
|
||||
]);
|
||||
export const kTAB_TEMPORARY_STATES = new Set([ // states not trigger updating of cache
|
||||
kTAB_STATE_CREATING,
|
||||
kTAB_STATE_BURSTING,
|
||||
kTAB_STATE_COLLAPSING,
|
||||
kTAB_STATE_DUPLICATING,
|
||||
kTAB_STATE_EXPANDING,
|
||||
kTAB_STATE_MOVING,
|
||||
kTAB_STATE_TO_BE_REMOVED,
|
||||
kTAB_STATE_REMOVING,
|
||||
kTAB_STATE_SHOWING,
|
||||
kTAB_STATE_NEW_TAB_COMMAND_TAB,
|
||||
kTAB_STATE_DUPLICATED,
|
||||
kTAB_STATE_RESTORED,
|
||||
kTAB_STATE_FROM_EXTERNAL,
|
||||
kTAB_STATE_FROM_FIREFOX_VIEW,
|
||||
kTAB_STATE_OPENED_FOR_SAME_WEBSITE,
|
||||
kTAB_STATE_PENDING,
|
||||
]);
|
||||
export const kTAB_SAFE_STATES = new Set([ // exportable via API
|
||||
kTAB_STATE_COLLAPSED,
|
||||
kTAB_STATE_SUBTREE_COLLAPSED,
|
||||
kTAB_STATE_GROUP_TAB,
|
||||
kTAB_STATE_CREATING,
|
||||
kTAB_STATE_NEW_TAB_COMMAND_TAB,
|
||||
kTAB_STATE_DUPLICATED,
|
||||
kTAB_STATE_RESTORED,
|
||||
kTAB_STATE_FROM_EXTERNAL,
|
||||
kTAB_STATE_FROM_FIREFOX_VIEW,
|
||||
kTAB_STATE_OPENED_FOR_SAME_WEBSITE,
|
||||
kTAB_STATE_STICKY,
|
||||
kTAB_STATE_STUCK,
|
||||
]);
|
||||
export const kTAB_SAFE_STATES_ARRAY = Array.from(kTAB_SAFE_STATES);
|
||||
|
||||
export const kTABBAR_STATE_OVERFLOW = 'overflow';
|
||||
export const kTABBAR_STATE_SCROLLED = 'scrolled';
|
||||
export const kTABBAR_STATE_FULLY_SCROLLED = 'fully-scrolled';
|
||||
export const kTABBAR_STATE_BLOCKING = 'blocking';
|
||||
export const kTABBAR_STATE_BLOCKING_WITH_THROBBER = 'blocking-throbber';
|
||||
export const kTABBAR_STATE_BLOCKING_WITH_SHADE = 'blocking-shade';
|
||||
export const kTABBAR_STATE_HAVE_LOADING_TAB = 'have-loading-tab';
|
||||
export const kTABBAR_STATE_HAVE_UNSYNCHRONIZED_THROBBER = 'have-unsynchronized-throbber';
|
||||
export const kTABBAR_STATE_THROBBER_SYNCHRONIZING = 'throbber-synchronizing';
|
||||
export const kTABBAR_STATE_CONTEXTUAL_IDENTITY_SELECTABLE = 'contextual-identity-selectable';
|
||||
export const kTABBAR_STATE_NEWTAB_ACTION_SELECTABLE = 'newtab-action-selectable';
|
||||
export const kTABBAR_STATE_MULTIPLE_HIGHLIGHTED = 'mutiple-highlighted';
|
||||
export const kTABBAR_STATE_HAS_VISUAL_GAP = 'has-visual-gap';
|
||||
export const kTABBAR_STATE_HOVER_ON_TOP_EDGE = 'hover-on-top-edge';
|
||||
export const kTABBAR_STATE_SCROLLBAR_AUTOHIDE = 'scrollbar-autohide';
|
||||
export const kTABBAR_STATE_FAVICONIZE_PINNED_TABS = 'faviconize-pinned-tabs';
|
||||
export const kTABBAR_STATE_TAB_DRAGGING = 'tab-dragging';
|
||||
export const kTABBAR_STATE_LINK_DRAGGING = 'link-dragging';
|
||||
|
||||
export const kWINDOW_STATE_TREE_STRUCTURE = 'tree-structure';
|
||||
export const kWINDOW_STATE_SCROLL_POSITION = 'scroll-position';
|
||||
export const kWINDOW_STATE_SUBPANEL_PROVIDER_ID = 'subpanel-provider-id';
|
||||
export const kWINDOW_STATE_SUBPANEL_HEIGHT = 'subpanel-height';
|
||||
export const kWINDOW_STATE_SUBPANEL_EFFECTIVE_HEIGHT = 'subpanel-effective-height';
|
||||
export const kWINDOW_STATE_CACHED_TABS = 'cached-tabs';
|
||||
export const kWINDOW_STATE_CACHED_SIDEBAR = 'cached-sidebar-contents';
|
||||
export const kWINDOW_STATE_CACHED_SIDEBAR_CONTENTS = 'cached-sidebar-contents:contents';
|
||||
export const kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY = 'cached-sidebar-contents:tabs-dirty';
|
||||
export const kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY = 'cached-sidebar-contents:collapsed-dirty';
|
||||
|
||||
export const kCACHE_KEYS = [
|
||||
kWINDOW_STATE_CACHED_TABS,
|
||||
kWINDOW_STATE_CACHED_SIDEBAR,
|
||||
kWINDOW_STATE_CACHED_SIDEBAR_CONTENTS,
|
||||
kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY,
|
||||
kWINDOW_STATE_CACHED_SIDEBAR_COLLAPSED_DIRTY,
|
||||
];
|
||||
|
||||
export const kCOUNTER_ROLE_ALL_TABS = 1;
|
||||
export const kCOUNTER_ROLE_CONTAINED_TABS = 2;
|
||||
|
||||
export const kTABBAR_POSITION_AUTO = 0;
|
||||
export const kTABBAR_POSITION_LEFT = 1;
|
||||
export const kTABBAR_POSITION_RIGHT = 2;
|
||||
|
||||
export const kACTION_MOVE = 1 << 0;
|
||||
export const kACTION_ATTACH = 1 << 10;
|
||||
export const kACTION_DETACH = 1 << 11;
|
||||
|
||||
export const kDROPLINK_ASK = 0;
|
||||
export const kDROPLINK_LOAD = 1 << 0;
|
||||
export const kDROPLINK_NEWTAB = 1 << 1;
|
||||
|
||||
export const kGROUP_BOOKMARK_ASK = 0;
|
||||
export const kGROUP_BOOKMARK_SUBTREE = 1 << 0;
|
||||
export const kGROUP_BOOKMARK_SEPARATE = 1 << 1;
|
||||
export const kGROUP_BOOKMARK_FIXED = kGROUP_BOOKMARK_SUBTREE | kGROUP_BOOKMARK_SEPARATE;
|
||||
export const kGROUP_BOOKMARK_USE_DUMMY = 1 << 8;
|
||||
export const kGROUP_BOOKMARK_USE_DUMMY_FORCE = 1 << 10;
|
||||
export const kGROUP_BOOKMARK_DONT_RESTORE_TREE_STRUCTURE = 1 << 9;
|
||||
export const kGROUP_BOOKMARK_EXPAND_ALL_TREE = 1 << 11;
|
||||
export const kGROUP_BOOKMARK_CANCEL = -1;
|
||||
|
||||
export const kGROUP_TAB_TEMPORARY_STATE_NOTHING = 0;
|
||||
export const kGROUP_TAB_TEMPORARY_STATE_PASSIVE = 1;
|
||||
export const kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE = 2;
|
||||
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL = 0;
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT = 1;
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM = -1;
|
||||
|
||||
export const kPARENT_TAB_OPERATION_CONTEXT_CLOSE = 1;
|
||||
export const kPARENT_TAB_OPERATION_CONTEXT_MOVE = 2;
|
||||
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE = 2;
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD = 3;
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN = 0;
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY = 6;
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_DETACH_ALL_CHILDREN = 1;
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_SIMPLY_DETACH_ALL_CHILDREN = 4; // just for internal use
|
||||
export const kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB = 5;
|
||||
|
||||
// just for migration from old versions
|
||||
export const kPARENT_TAB_BEHAVIOR_ALWAYS = 0;
|
||||
export const kPARENT_TAB_BEHAVIOR_ONLY_WHEN_VISIBLE = 1;
|
||||
export const kPARENT_TAB_BEHAVIOR_ONLY_ON_SIDEBAR = 2;
|
||||
|
||||
export const kINSERT_NO_CONTROL = -1;
|
||||
export const kINSERT_INHERIT = -2;
|
||||
export const kINSERT_TOP = 0;
|
||||
export const kINSERT_END = 1;
|
||||
export const kINSERT_NEAREST = 2;
|
||||
export const kINSERT_NEXT_TO_LAST_RELATED_TAB = 3;
|
||||
export const kCONTROLLED_INSERTION_POSITION = new Set([
|
||||
kINSERT_TOP,
|
||||
kINSERT_NEAREST,
|
||||
kINSERT_NEXT_TO_LAST_RELATED_TAB,
|
||||
]);
|
||||
|
||||
export const kSUCCESSOR_TAB_CONTROL_NEVER = 0;
|
||||
export const kSUCCESSOR_TAB_CONTROL_SIMULATE_DEFAULT = 1;
|
||||
export const kSUCCESSOR_TAB_CONTROL_IN_TREE = 2;
|
||||
|
||||
export const kTREE_DOUBLE_CLICK_BEHAVIOR_NONE = 0;
|
||||
export const kTREE_DOUBLE_CLICK_BEHAVIOR_TOGGLE_COLLAPSED = 1;
|
||||
export const kTREE_DOUBLE_CLICK_BEHAVIOR_TOGGLE_STICKY = 4;
|
||||
export const kTREE_DOUBLE_CLICK_BEHAVIOR_CLOSE = 3;
|
||||
// 2 is a retired number for a removed feature
|
||||
|
||||
export const kDRAG_BEHAVIOR_NONE = 0;
|
||||
export const kDRAG_BEHAVIOR_ENTIRE_TREE = 1 << 0;
|
||||
export const kDRAG_BEHAVIOR_ALLOW_BOOKMARK = 1 << 1;
|
||||
export const kDRAG_BEHAVIOR_TEAR_OFF = 1 << 2;
|
||||
export const kDRAG_BEHAVIOR_MOVE = 1 << 3;
|
||||
|
||||
export const kNEWTAB_DO_NOTHING = -1;
|
||||
export const kNEWTAB_OPEN_AS_ORPHAN = 0;
|
||||
export const kNEWTAB_OPEN_AS_CHILD = 1; // no control about position
|
||||
export const kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB = 5;
|
||||
export const kNEWTAB_OPEN_AS_CHILD_TOP = 6;
|
||||
export const kNEWTAB_OPEN_AS_CHILD_END = 7;
|
||||
export const kNEWTAB_OPEN_AS_SIBLING = 2;
|
||||
export const kNEWTAB_OPEN_AS_NEXT_SIBLING = 3;
|
||||
export const kNEWTAB_OPEN_AS_NEXT_SIBLING_WITH_INHERITED_CONTAINER = 4;
|
||||
export const kCONTROLLED_NEWTAB_POSITION = new Set([
|
||||
kNEWTAB_OPEN_AS_CHILD,
|
||||
kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB,
|
||||
kNEWTAB_OPEN_AS_CHILD_TOP,
|
||||
kNEWTAB_OPEN_AS_CHILD_END,
|
||||
kNEWTAB_OPEN_AS_SIBLING,
|
||||
kNEWTAB_OPEN_AS_NEXT_SIBLING,
|
||||
kNEWTAB_OPEN_AS_NEXT_SIBLING_WITH_INHERITED_CONTAINER,
|
||||
]);
|
||||
|
||||
export const kCONTEXTUAL_IDENTITY_DEFAULT = 0;
|
||||
export const kCONTEXTUAL_IDENTITY_FROM_PARENT = 1;
|
||||
export const kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE = 2;
|
||||
|
||||
export const kSCROLL_TO_NEW_TAB_IGNORE = 0;
|
||||
export const kSCROLL_TO_NEW_TAB_IF_POSSIBLE = 1;
|
||||
|
||||
export const kTABBAR_UPDATE_REASON_RESIZE = 1 << 0;
|
||||
export const kTABBAR_UPDATE_REASON_COLLAPSE = 1 << 1;
|
||||
export const kTABBAR_UPDATE_REASON_EXPAND = 1 << 2;
|
||||
export const kTABBAR_UPDATE_REASON_ANIMATION_END = 1 << 3;
|
||||
export const kTABBAR_UPDATE_REASON_TAB_OPEN = 1 << 4;
|
||||
export const kTABBAR_UPDATE_REASON_TAB_CLOSE = 1 << 5;
|
||||
export const kTABBAR_UPDATE_REASON_TAB_MOVE = 1 << 6;
|
||||
export const kTABBAR_UPDATE_REASON_VIRTUAL_SCROLL_VIEWPORT_UPDATE = 1 << 7;
|
||||
|
||||
export const kDEFAULT_MIN_INDENT = 3;
|
||||
|
||||
export const kGROUP_TAB_URI = browser.runtime.getURL('resources/group-tab.html');
|
||||
export const kGROUP_TAB_DEFAULT_TITLE_MATCHER = new RegExp(`^${browser.i18n.getMessage('groupTab_label', '.+')}$`);
|
||||
export const kGROUP_TAB_FROM_PINNED_DEFAULT_TITLE_MATCHER = new RegExp(`^${browser.i18n.getMessage('groupTab_fromPinnedTab_label', '.+')}$`);
|
||||
export const kSHORTHAND_CUSTOM_URI = /^ext\+ws:([^:?#]+)(?:[:?]([^#]*))?(#.*)?$/;
|
||||
export const kSHORTHAND_ABOUT_URI = /^about:ws-([^?]+)/;
|
||||
export const kSHORTHAND_URIS = {
|
||||
tabbar: browser.runtime.getURL('sidebar/sidebar.html'),
|
||||
group: kGROUP_TAB_URI,
|
||||
options: browser.runtime.getURL('options/options.html?independent=true'),
|
||||
startup: browser.runtime.getURL('resources/startup.html'),
|
||||
testRunner: browser.runtime.getURL('tests/runner.html'),
|
||||
'test-runner': browser.runtime.getURL('tests/runner.html')
|
||||
};
|
||||
|
||||
export const kINSERTION_CONTEXT_MOVED = 1;
|
||||
export const kINSERTION_CONTEXT_SHOWN = 2;
|
||||
export const kINSERTION_CONTEXT_CREATED = 3;
|
||||
|
||||
export const kIN_CONTENT_PANEL_RENDER_NOWHERE = 0;
|
||||
export const kIN_CONTENT_PANEL_RENDER_IN_SIDEBAR = 1 << 0;
|
||||
export const kIN_CONTENT_PANEL_RENDER_IN_CONTENT = 1 << 1;
|
||||
export const kIN_CONTENT_PANEL_RENDER_IN_ANYWHERE = kIN_CONTENT_PANEL_RENDER_IN_SIDEBAR | kIN_CONTENT_PANEL_RENDER_IN_CONTENT;
|
||||
|
||||
export const kAGGRESSIVE_OPENER_TAB_DETECTION_RULES_WITH_URL = [
|
||||
{ opener: /^about:addons/,
|
||||
child: /^https:\/\/addons.mozilla.org\/([^\/]+\/)?[^\/]+\/search\// }
|
||||
];
|
||||
|
||||
export const kNOTIFICATION_DEFAULT_ICON = '/resources/64x64.svg#default-bright';
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/sync
|
||||
// Use 6 * 1024 instead of 8 * 1024 (max of the quota) for safety.
|
||||
// For example, 6 * 8 = 48KB is the max size of the user style rules.
|
||||
export const kSYNC_STORAGE_SAFE_QUOTA = 6 * 1024;
|
||||
|
||||
export const kSYNC_DATA_TYPE_TABS = 'tabs';
|
||||
|
||||
export const IS_BACKGROUND = location.href.startsWith(browser.runtime.getURL('background/background.html'));
|
||||
export const IS_SIDEBAR = location.href.startsWith(browser.runtime.getURL('sidebar/sidebar.html'));
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
configs,
|
||||
log as internalLogger,
|
||||
isWindows,
|
||||
} from './common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function log(...args) {
|
||||
internalLogger('common/contextual-identities', ...args);
|
||||
}
|
||||
|
||||
const mContextualIdentities = new Map();
|
||||
let mIsObserving = false;
|
||||
|
||||
export function get(id) {
|
||||
return mContextualIdentities.get(id);
|
||||
}
|
||||
|
||||
export function getIdFromName(name) {
|
||||
for (const identity of mContextualIdentities.values()) {
|
||||
if (identity.name.toLowerCase() == name.toLowerCase())
|
||||
return identity.cookieStoreId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Respect container type stored by Container Bookmarks
|
||||
// https://addons.mozilla.org/firefox/addon/container-bookmarks/
|
||||
export function getIdFromBookmark(bookmark) {
|
||||
const containerMatcher = new RegExp(`#${configs.containerRedirectKey}-(.+)$`);
|
||||
const matchedContainer = bookmark.url.match(containerMatcher);
|
||||
if (!matchedContainer)
|
||||
return {};
|
||||
|
||||
const idPart = matchedContainer[matchedContainer.length-1];
|
||||
const url = bookmark.url.replace(containerMatcher, '');
|
||||
|
||||
// old method
|
||||
const identity = mContextualIdentities.get(decodeURIComponent(idPart));
|
||||
if (identity) {
|
||||
return {
|
||||
cookieStoreId: identity.cookieStoreId,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
for (const [cookieStoreId, identity] of mContextualIdentities.entries()) {
|
||||
if (idPart != encodeURIComponent(identity.name.toLowerCase().replace(/\s/g, '-')))
|
||||
continue;
|
||||
return {
|
||||
cookieStoreId,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getColorInfo() {
|
||||
const colors = {};
|
||||
const customColors = [];
|
||||
forEach(identity => {
|
||||
if (!identity.colorCode)
|
||||
return;
|
||||
let colorValue = identity.colorCode;
|
||||
if (identity.color) {
|
||||
const customColor = `--contextual-identity-color-${identity.color}`;
|
||||
customColors.push(`${customColor}: ${identity.colorCode};`);
|
||||
colorValue = `var(${customColor})`;
|
||||
}
|
||||
colors[identity.cookieStoreId] = colorValue;
|
||||
});
|
||||
|
||||
return {
|
||||
colors,
|
||||
colorDeclarations: customColors.length > 0 ? `:root { ${customColors.join('\n')} }` : ''
|
||||
};
|
||||
}
|
||||
|
||||
export function getCount() {
|
||||
return mContextualIdentities.size;
|
||||
}
|
||||
|
||||
export function forEach(callback) {
|
||||
for (const identity of mContextualIdentities.values()) {
|
||||
callback(identity);
|
||||
}
|
||||
}
|
||||
|
||||
if (browser.contextualIdentities) { // already granted
|
||||
browser.contextualIdentities.onCreated.addListener(onContextualIdentityCreated);
|
||||
browser.contextualIdentities.onRemoved.addListener(onContextualIdentityRemoved);
|
||||
browser.contextualIdentities.onUpdated.addListener(onContextualIdentityUpdated);
|
||||
mIsObserving = true;
|
||||
}
|
||||
|
||||
export function startObserve() {
|
||||
if (!browser.contextualIdentities ||
|
||||
mIsObserving)
|
||||
return;
|
||||
mIsObserving = true;
|
||||
browser.contextualIdentities.onCreated.addListener(onContextualIdentityCreated);
|
||||
browser.contextualIdentities.onRemoved.addListener(onContextualIdentityRemoved);
|
||||
browser.contextualIdentities.onUpdated.addListener(onContextualIdentityUpdated);
|
||||
}
|
||||
|
||||
export function endObserve() {
|
||||
if (!browser.contextualIdentities ||
|
||||
!mIsObserving)
|
||||
return;
|
||||
browser.contextualIdentities.onCreated.removeListener(onContextualIdentityCreated);
|
||||
browser.contextualIdentities.onRemoved.removeListener(onContextualIdentityRemoved);
|
||||
browser.contextualIdentities.onUpdated.removeListener(onContextualIdentityUpdated);
|
||||
}
|
||||
|
||||
export async function init() {
|
||||
if (!browser.contextualIdentities)
|
||||
return;
|
||||
const identities = await browser.contextualIdentities.query({}).catch(ApiTabs.createErrorHandler());
|
||||
for (const identity of identities) {
|
||||
mContextualIdentities.set(identity.cookieStoreId, fixupIcon(identity));
|
||||
}
|
||||
}
|
||||
|
||||
function fixupIcon(identity) {
|
||||
if (identity.icon && identity.color)
|
||||
identity.iconUrl = `/resources/icons/contextual-identities/${identity.icon}.svg#${safeColor(identity.color)}`;
|
||||
return identity;
|
||||
}
|
||||
|
||||
const mDarkModeMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mDarkModeMedia.addListener(async _event => {
|
||||
await init();
|
||||
forEach(identity => onContextualIdentityUpdated({ contextualIdentity: identity }));
|
||||
});
|
||||
|
||||
function safeColor(color) {
|
||||
switch (color) {
|
||||
case 'blue':
|
||||
case 'turquoise':
|
||||
case 'green':
|
||||
case 'yellow':
|
||||
case 'orange':
|
||||
case 'red':
|
||||
case 'pink':
|
||||
case 'purple':
|
||||
return color;
|
||||
|
||||
case 'toolbar':
|
||||
default:
|
||||
return !isWindows() && mDarkModeMedia.matches ? 'toolbar-dark' : 'toolbar-light';
|
||||
}
|
||||
}
|
||||
|
||||
export const onUpdated = new EventListenerManager();
|
||||
|
||||
function onContextualIdentityCreated(createdInfo) {
|
||||
if (!mIsObserving)
|
||||
return;
|
||||
|
||||
const identity = createdInfo.contextualIdentity;
|
||||
mContextualIdentities.set(identity.cookieStoreId, fixupIcon(identity));
|
||||
onUpdated.dispatch();
|
||||
}
|
||||
|
||||
function onContextualIdentityRemoved(removedInfo) {
|
||||
if (!mIsObserving)
|
||||
return;
|
||||
|
||||
const identity = removedInfo.contextualIdentity;
|
||||
delete mContextualIdentities.delete(identity.cookieStoreId);
|
||||
onUpdated.dispatch();
|
||||
}
|
||||
|
||||
function onContextualIdentityUpdated(updatedInfo) {
|
||||
if (!mIsObserving)
|
||||
return;
|
||||
|
||||
const identity = updatedInfo.contextualIdentity;
|
||||
mContextualIdentities.set(identity.cookieStoreId, fixupIcon(identity));
|
||||
onUpdated.dispatch();
|
||||
}
|
||||
|
||||
|
||||
export function generateMenuItems({ hasDefault } = {}) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
if (hasDefault) {
|
||||
const defaultCotnainerItem = document.createElement('li');
|
||||
defaultCotnainerItem.dataset.value = 'firefox-default';
|
||||
defaultCotnainerItem.textContent = browser.i18n.getMessage('tabbar_newTabWithContexualIdentity_default');
|
||||
fragment.appendChild(defaultCotnainerItem);
|
||||
|
||||
const separator = document.createElement('li');
|
||||
separator.classList.add('separator');
|
||||
fragment.appendChild(separator);
|
||||
}
|
||||
|
||||
forEach(identity => {
|
||||
const item = document.createElement('li');
|
||||
item.dataset.value = identity.cookieStoreId;
|
||||
item.textContent = identity.name;
|
||||
item.dataset.icon = identity.iconUrl;
|
||||
fragment.appendChild(item);
|
||||
});
|
||||
|
||||
return fragment;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
Original: https://github.com/joakimbeng/split-css-selector
|
||||
MIT License © Joakim Carlstein
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export function splitSelectors(selectors) {
|
||||
if (isAtRule(selectors))
|
||||
return [selectors];
|
||||
|
||||
return split(selectors, {
|
||||
splitter: ',',
|
||||
appendSplitter: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function splitSelectorParts(selector) {
|
||||
if (isAtRule(selector))
|
||||
return [selector];
|
||||
|
||||
return split(selector, {
|
||||
splitters: [
|
||||
' ', '\t', '\n', // https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator
|
||||
'>', // https://developer.mozilla.org/en-US/docs/Web/CSS/Child_combinator
|
||||
'+', // https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator
|
||||
'~', // https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_combinator
|
||||
// '||', // https://developer.mozilla.org/en-US/docs/Web/CSS/Column_combinator
|
||||
],
|
||||
appendSplitter: true,
|
||||
}).filter(part => part !== '');
|
||||
}
|
||||
|
||||
function split(input, { splitter, splitters, appendSplitter }) {
|
||||
const splittersSet = splitters && new Set(splitters);
|
||||
|
||||
const splitted = [];
|
||||
let parens = 0;
|
||||
let angulars = 0;
|
||||
let soFar = '';
|
||||
let escaped = false;
|
||||
let singleQuoted = false;
|
||||
let doubleQuoted = false;
|
||||
for (const char of input) {
|
||||
if (escaped) {
|
||||
soFar += char;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (char === '\\' && !escaped) {
|
||||
soFar += char;
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (char === "'") {
|
||||
if (singleQuoted)
|
||||
singleQuoted = false;
|
||||
else if (!doubleQuoted)
|
||||
singleQuoted = true;
|
||||
}
|
||||
else if (char === '"') {
|
||||
if (doubleQuoted)
|
||||
doubleQuoted = false;
|
||||
else if (!singleQuoted)
|
||||
doubleQuoted = true;
|
||||
}
|
||||
else if (char === '(') {
|
||||
parens++;
|
||||
}
|
||||
else if (char === ')') {
|
||||
parens--;
|
||||
}
|
||||
else if (char === '[') {
|
||||
angulars++;
|
||||
}
|
||||
else if (char === ']') {
|
||||
angulars--;
|
||||
}
|
||||
else if (splitter && char === splitter) {
|
||||
if (!parens &&
|
||||
!angulars &&
|
||||
!singleQuoted &&
|
||||
!doubleQuoted) {
|
||||
splitted.push(soFar.trim());
|
||||
soFar = '';
|
||||
if (appendSplitter)
|
||||
soFar += char;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (splittersSet?.has(char)) {
|
||||
if (!parens &&
|
||||
!angulars &&
|
||||
!singleQuoted &&
|
||||
!doubleQuoted) {
|
||||
splitted.push(soFar);
|
||||
soFar = '';
|
||||
if (appendSplitter)
|
||||
soFar += char;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
soFar += char;
|
||||
}
|
||||
splitted.push(soFar.trim());
|
||||
return splitted;
|
||||
}
|
||||
|
||||
function isAtRule(selector) {
|
||||
return selector.startsWith('@');
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
|
||||
const PSEUDO_ELEMENTS = `
|
||||
:after
|
||||
::after
|
||||
::backdrop
|
||||
:before
|
||||
::before
|
||||
::cue
|
||||
::cue-region
|
||||
:first-letter
|
||||
::first-letter
|
||||
:first-line
|
||||
::first-line
|
||||
::file-selector-button
|
||||
::grammer-error
|
||||
::marker
|
||||
::part
|
||||
::placeholder
|
||||
::selection
|
||||
::slotted
|
||||
::spelling-error
|
||||
::target-text
|
||||
`.trim().split('\n').map(item => item.trim());
|
||||
const PSEUDO_ELEMENTS_MATCHER = new RegExp(`(${PSEUDO_ELEMENTS.join('|')})$`, 'i');
|
||||
|
||||
export function appendPart(baseSelector, appendant) {
|
||||
if (PSEUDO_ELEMENTS_MATCHER.test(baseSelector))
|
||||
return baseSelector.replace(PSEUDO_ELEMENTS_MATCHER, `${appendant}$1`);
|
||||
return `${baseSelector}${appendant}`;
|
||||
}
|
||||
140
waterfox/browser/components/sidebar/common/dialog.js
Normal file
140
waterfox/browser/components/sidebar/common/dialog.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import RichConfirm from '/extlib/RichConfirm.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs,
|
||||
isMacOS,
|
||||
sanitizeForHTMLText,
|
||||
} from '/common/common.js';
|
||||
|
||||
import * as ApiTabs from './api-tabs.js';
|
||||
import * as Constants from './constants.js';
|
||||
import * as Permissions from './permissions.js';
|
||||
import * as SidebarConnection from './sidebar-connection.js';
|
||||
import * as UserOperationBlocker from './user-operation-blocker.js';
|
||||
|
||||
import { Tab } from './TreeItem.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/dialog', ...args);
|
||||
}
|
||||
|
||||
export async function show(ownerWindow, dialogParams) {
|
||||
let result;
|
||||
let unblocked = false;
|
||||
try {
|
||||
if (configs.showDialogInSidebar &&
|
||||
SidebarConnection.isOpen(ownerWindow.id)/* &&
|
||||
SidebarConnection.hasFocus(ownerWindow.id)*/) {
|
||||
UserOperationBlocker.blockIn(ownerWindow.id, { throbber: false });
|
||||
result = await browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_SHOW_DIALOG,
|
||||
params: {
|
||||
...dialogParams,
|
||||
onShown: null,
|
||||
onShownInTab: null,
|
||||
onShownInPopup: null,
|
||||
userOperationBlockerParams: { throbber: false },
|
||||
},
|
||||
windowId: ownerWindow.id
|
||||
}).catch(ApiTabs.createErrorHandler());
|
||||
}
|
||||
else if (isMacOS() &&
|
||||
ownerWindow.state == 'fullscreen') {
|
||||
// on macOS, a popup window opened from a fullscreen browser window is always
|
||||
// opened as a new fullscreen window, thus we need to fallback to a workaround.
|
||||
log('showDialog: show in a temporary tab in ', ownerWindow.id);
|
||||
UserOperationBlocker.blockIn(ownerWindow.id, { throbber: false, shade: true });
|
||||
const tempTab = await browser.tabs.create({
|
||||
windowId: ownerWindow.id,
|
||||
url: ((await Permissions.isGranted(Permissions.ALL_URLS)) ? null : '/resources/blank.html'),
|
||||
active: true
|
||||
});
|
||||
await Tab.waitUntilTracked(tempTab.id).then(() => {
|
||||
Tab.get(tempTab.id).$TST.addState('hidden', { broadcast: true });
|
||||
});
|
||||
result = await RichConfirm.showInTab(tempTab.id, {
|
||||
...dialogParams,
|
||||
onShown: [
|
||||
container => {
|
||||
const style = container.closest('.rich-confirm-dialog').style;
|
||||
style.maxWidth = `${Math.floor(window.innerWidth * 0.6)}px`;
|
||||
style.marginInlineStart = style.marginInlineEnd = 'auto';
|
||||
},
|
||||
dialogParams.onShownInTab || dialogParams.onShown
|
||||
],
|
||||
onHidden(...params) {
|
||||
UserOperationBlocker.unblockIn(ownerWindow.id, { throbber: false });
|
||||
unblocked = true;
|
||||
if (typeof dialogParams.onHidden == 'function')
|
||||
dialogParams.onHidden(...params);
|
||||
},
|
||||
});
|
||||
browser.tabs.remove(tempTab.id);
|
||||
}
|
||||
else {
|
||||
log('showDialog: show in a popup window on ', ownerWindow.id);
|
||||
UserOperationBlocker.blockIn(ownerWindow.id, { throbber: false });
|
||||
result = await RichConfirm.showInPopup(ownerWindow.id, {
|
||||
...dialogParams,
|
||||
onShown: dialogParams.onShownInPopup || dialogParams.onShown,
|
||||
onHidden(...params) {
|
||||
UserOperationBlocker.unblockIn(ownerWindow.id, { throbber: false });
|
||||
unblocked = true;
|
||||
if (typeof dialogParams.onHidden == 'function')
|
||||
dialogParams.onHidden(...params);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(_error) {
|
||||
result = { buttonIndex: -1 };
|
||||
}
|
||||
finally {
|
||||
if (!unblocked)
|
||||
UserOperationBlocker.unblockIn(ownerWindow.id, { throbber: false });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function tabsToHTMLList(tabs, { maxHeight, maxWidth }) {
|
||||
const rootLevelOffset = tabs.map(tab => parseInt(tab.$TST.getAttribute(Constants.kLEVEL) || 0)).sort()[0];
|
||||
return (
|
||||
`<ul style="border: 1px inset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-block: 0.5em;
|
||||
margin-inline: 0;
|
||||
min-height: 2em;
|
||||
max-height: calc(${maxHeight}px - 12em /* title bar, message, checkbox, buttons, and margins */);
|
||||
max-width: ${maxWidth}px;
|
||||
overflow: auto;
|
||||
padding-block: 0.5em;
|
||||
padding-inline: 0.5em;">` +
|
||||
tabs.map(tab => `<li style="align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-inline-start: calc((${tab.$TST.getAttribute(Constants.kLEVEL)} - ${rootLevelOffset}) * 0.25em);"
|
||||
title="${sanitizeForHTMLText(tab.title)}"
|
||||
><img style="display: flex;
|
||||
max-height: 1em;
|
||||
max-width: 1em;"
|
||||
alt=""
|
||||
src="${sanitizeForHTMLText(tab.favIconUrl || browser.runtime.getURL('resources/icons/defaultFavicon.svg'))}"
|
||||
><span style="margin-inline-start: 0.25em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;"
|
||||
>${sanitizeForHTMLText(tab.title)}</span></li>`).join('') +
|
||||
`</ul>`
|
||||
);
|
||||
}
|
||||
909
waterfox/browser/components/sidebar/common/diff.js
Normal file
909
waterfox/browser/components/sidebar/common/diff.js
Normal file
@@ -0,0 +1,909 @@
|
||||
/**
|
||||
* Mainly ported from difflib.py, the standard diff library of Python.
|
||||
* This code is distributed under the Python Software Foundation License.
|
||||
* Contributor(s): Sutou Kouhei <kou@clear-code.com> (porting)
|
||||
* YUKI "Piro" Hiroshi <yuki@clear-code.com>
|
||||
* (encoded diff, DOM Updater)
|
||||
* ------------------------------------------------------------------------
|
||||
* Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009
|
||||
* Python Software Foundation.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 2000 BeOpen.com.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 1995-2001 Corporation for National Research Initiatives.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 1991-1995 Stichting Mathematisch Centrum.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
export class SequenceMatcher {
|
||||
constructor(from, to, junkPredicate) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.junkPredicate = junkPredicate;
|
||||
this._updateToIndexes();
|
||||
}
|
||||
|
||||
longestMatch(fromStart, fromEnd, toStart, toEnd) {
|
||||
let bestInfo = this._findBestMatchPosition(fromStart, fromEnd,
|
||||
toStart, toEnd);
|
||||
const haveJunk = Object.keys(this.junks).length > 0;
|
||||
if (haveJunk) {
|
||||
const adjust = this._adjustBestInfoWithJunkPredicate;
|
||||
const args = [fromStart, fromEnd, toStart, toEnd];
|
||||
|
||||
bestInfo = adjust.apply(this, [false, bestInfo].concat(args));
|
||||
bestInfo = adjust.apply(this, [true, bestInfo].concat(args));
|
||||
}
|
||||
|
||||
return bestInfo;
|
||||
}
|
||||
|
||||
matches() {
|
||||
if (!this._matches)
|
||||
this._matches = this._computeMatches();
|
||||
return this._matches;
|
||||
}
|
||||
|
||||
blocks() {
|
||||
if (!this._blocks)
|
||||
this._blocks = this._computeBlocks();
|
||||
return this._blocks;
|
||||
}
|
||||
|
||||
operations() {
|
||||
if (!this._operations)
|
||||
this._operations = this._computeOperations();
|
||||
return this._operations;
|
||||
}
|
||||
|
||||
groupedOperations(contextSize) {
|
||||
if (!contextSize)
|
||||
contextSize = 3;
|
||||
|
||||
let operations = this.operations();
|
||||
if (operations.length == 0)
|
||||
operations = [['equal', 0, 0, 0, 0]];
|
||||
operations = this._expandEdgeEqualOperations(operations, contextSize);
|
||||
|
||||
const groupWindow = contextSize * 2;
|
||||
const groups = [];
|
||||
let group = [];
|
||||
for (const operation of operations) {
|
||||
const tag = operation[0];
|
||||
let fromStart = operation[1];
|
||||
const fromEnd = operation[2];
|
||||
let toStart = operation[3];
|
||||
const toEnd = operation[4];
|
||||
|
||||
if (tag == 'equal' && fromEnd - fromStart > groupWindow) {
|
||||
group.push([tag,
|
||||
fromStart,
|
||||
Math.min(fromEnd, fromStart + contextSize),
|
||||
toStart,
|
||||
Math.min(toEnd, toStart + contextSize)]);
|
||||
groups.push(group);
|
||||
group = [];
|
||||
fromStart = Math.max(fromStart, fromEnd - contextSize);
|
||||
toStart = Math.max(toStart, toEnd - contextSize);
|
||||
}
|
||||
group.push([tag, fromStart, fromEnd, toStart, toEnd]);
|
||||
}
|
||||
|
||||
if (group.length > 0)
|
||||
groups.push(group);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
ratio() {
|
||||
if (!this._ratio)
|
||||
this._ratio = this._computeRatio();
|
||||
return this._ratio;
|
||||
}
|
||||
|
||||
_updateToIndexes() {
|
||||
this.toIndexes = {};
|
||||
this.junks = {};
|
||||
|
||||
for (let i = 0, length = this.to.length; i < length; i++) {
|
||||
const item = this.to[i];
|
||||
|
||||
if (!this.toIndexes[item])
|
||||
this.toIndexes[item] = [];
|
||||
this.toIndexes[item].push(i);
|
||||
}
|
||||
|
||||
if (!this.junkPredicate)
|
||||
return;
|
||||
|
||||
const toIndexesWithoutJunk = {};
|
||||
for (const item in this.toIndexes) {
|
||||
if (this.junkPredicate(item)) {
|
||||
this.junks[item] = true;
|
||||
} else {
|
||||
toIndexesWithoutJunk[item] = this.toIndexes[item];
|
||||
}
|
||||
}
|
||||
this.toIndexes = toIndexesWithoutJunk;
|
||||
}
|
||||
|
||||
_findBestMatchPosition(fromStart, fromEnd, toStart, toEnd) {
|
||||
let bestFrom = fromStart;
|
||||
let bestTo = toStart;
|
||||
let bestSize = 0;
|
||||
let lastSizes = {};
|
||||
let fromIndex;
|
||||
|
||||
for (fromIndex = fromStart; fromIndex <= fromEnd; fromIndex++) {
|
||||
const sizes = {};
|
||||
|
||||
const toIndexes = this.toIndexes[this.from[fromIndex]] || [];
|
||||
for (let i = 0, length = toIndexes.length; i < length; i++) {
|
||||
const toIndex = toIndexes[i];
|
||||
|
||||
if (toIndex < toStart)
|
||||
continue;
|
||||
if (toIndex > toEnd)
|
||||
break;
|
||||
|
||||
const size = sizes[toIndex] = (lastSizes[toIndex - 1] || 0) + 1;
|
||||
if (size > bestSize) {
|
||||
bestFrom = fromIndex - size + 1;
|
||||
bestTo = toIndex - size + 1;
|
||||
bestSize = size;
|
||||
}
|
||||
}
|
||||
lastSizes = sizes;
|
||||
}
|
||||
|
||||
return [bestFrom, bestTo, bestSize];
|
||||
}
|
||||
|
||||
_adjustBestInfoWithJunkPredicate(shouldJunk, bestInfo,
|
||||
fromStart, fromEnd,
|
||||
toStart, toEnd) {
|
||||
let [bestFrom, bestTo, bestSize] = bestInfo;
|
||||
|
||||
while (bestFrom > fromStart &&
|
||||
bestTo > toStart &&
|
||||
(shouldJunk ?
|
||||
this.junks[this.to[bestTo - 1]] :
|
||||
!this.junks[this.to[bestTo - 1]]) &&
|
||||
this.from[bestFrom - 1] == this.to[bestTo - 1]) {
|
||||
bestFrom -= 1;
|
||||
bestTo -= 1;
|
||||
bestSize += 1;
|
||||
}
|
||||
|
||||
while (bestFrom + bestSize < fromEnd &&
|
||||
bestTo + bestSize < toEnd &&
|
||||
(shouldJunk ?
|
||||
this.junks[this.to[bestTo + bestSize]] :
|
||||
!this.junks[this.to[bestTo + bestSize]]) &&
|
||||
this.from[bestFrom + bestSize] == this.to[bestTo + bestSize]) {
|
||||
bestSize += 1;
|
||||
}
|
||||
|
||||
return [bestFrom, bestTo, bestSize];
|
||||
}
|
||||
|
||||
_computeMatches() {
|
||||
const matches = [];
|
||||
const queue = [[0, this.from.length, 0, this.to.length]];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const target = queue.pop();
|
||||
const [fromStart, fromEnd, toStart, toEnd] = target;
|
||||
|
||||
const match = this.longestMatch(fromStart, fromEnd - 1, toStart, toEnd - 1);
|
||||
const matchFromIndex = match[0];
|
||||
const matchToIndex = match[1];
|
||||
const size = match[2];
|
||||
if (size > 0) {
|
||||
if (fromStart < matchFromIndex && toStart < matchToIndex)
|
||||
queue.push([fromStart, matchFromIndex, toStart, matchToIndex]);
|
||||
|
||||
matches.push(match);
|
||||
if (matchFromIndex + size < fromEnd && matchToIndex + size < toEnd)
|
||||
queue.push([matchFromIndex + size, fromEnd,
|
||||
matchToIndex + size, toEnd]);
|
||||
}
|
||||
}
|
||||
|
||||
matches.sort((matchInfo1, matchInfo2) => {
|
||||
const fromIndex1 = matchInfo1[0];
|
||||
const fromIndex2 = matchInfo2[0];
|
||||
return fromIndex1 - fromIndex2;
|
||||
});
|
||||
return matches;
|
||||
}
|
||||
|
||||
_computeBlocks() {
|
||||
const blocks = [];
|
||||
let currentFromIndex = 0;
|
||||
let currentToIndex = 0;
|
||||
let currentSize = 0;
|
||||
|
||||
for (const match of this.matches()) {
|
||||
const [fromIndex, toIndex, size] = match;
|
||||
|
||||
if (currentFromIndex + currentSize == fromIndex &&
|
||||
currentToIndex + currentSize == toIndex) {
|
||||
currentSize += size;
|
||||
} else {
|
||||
if (currentSize > 0)
|
||||
blocks.push([currentFromIndex, currentToIndex, currentSize]);
|
||||
currentFromIndex = fromIndex;
|
||||
currentToIndex = toIndex;
|
||||
currentSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSize > 0)
|
||||
blocks.push([currentFromIndex, currentToIndex, currentSize]);
|
||||
|
||||
blocks.push([this.from.length, this.to.length, 0]);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
_computeOperations() {
|
||||
let fromIndex = 0;
|
||||
let toIndex = 0;
|
||||
const operations = [];
|
||||
|
||||
for (const block of this.blocks()) {
|
||||
const [matchFromIndex, matchToIndex, size] = block;
|
||||
|
||||
const tag = this._determineTag(fromIndex, toIndex,
|
||||
matchFromIndex, matchToIndex);
|
||||
if (tag != 'equal')
|
||||
operations.push([tag,
|
||||
fromIndex, matchFromIndex,
|
||||
toIndex, matchToIndex]);
|
||||
|
||||
fromIndex = matchFromIndex + size;
|
||||
toIndex = matchToIndex + size;
|
||||
|
||||
if (size > 0)
|
||||
operations.push(['equal',
|
||||
matchFromIndex, fromIndex,
|
||||
matchToIndex, toIndex]);
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
_determineTag(fromIndex, toIndex,
|
||||
matchFromIndex, matchToIndex) {
|
||||
if (fromIndex < matchFromIndex && toIndex < matchToIndex) {
|
||||
return 'replace';
|
||||
} else if (fromIndex < matchFromIndex) {
|
||||
return 'delete';
|
||||
} else if (toIndex < matchToIndex) {
|
||||
return 'insert';
|
||||
} else {
|
||||
return 'equal';
|
||||
}
|
||||
}
|
||||
|
||||
_expandEdgeEqualOperations(operations, contextSize) {
|
||||
const expandedOperations = [];
|
||||
|
||||
for (let index = 0, length = operations.length; index < length; index++) {
|
||||
const operation = operations[index];
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
|
||||
if (tag == 'equal' && index == 0) {
|
||||
expandedOperations.push([tag,
|
||||
Math.max(fromStart, fromEnd - contextSize),
|
||||
fromEnd,
|
||||
Math.max(toStart, toEnd - contextSize),
|
||||
toEnd]);
|
||||
} else if (tag == 'equal' && index == length - 1) {
|
||||
expandedOperations.push([tag,
|
||||
fromStart,
|
||||
Math.min(fromEnd, fromStart + contextSize),
|
||||
toStart,
|
||||
Math.min(toEnd, toStart + contextSize),
|
||||
toEnd]);
|
||||
} else {
|
||||
expandedOperations.push(operation);
|
||||
}
|
||||
}
|
||||
|
||||
return expandedOperations;
|
||||
}
|
||||
|
||||
_computeRatio() {
|
||||
const length = this.from.length + this.to.length;
|
||||
|
||||
if (length == 0)
|
||||
return 1.0;
|
||||
|
||||
let matches = 0;
|
||||
for (const block of this.blocks()) {
|
||||
const size = block[2];
|
||||
matches += size;
|
||||
}
|
||||
return 2.0 * matches / length;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
export class ReadableDiffer {
|
||||
constructor(from, to) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
diff() {
|
||||
let lines = [];
|
||||
const matcher = new SequenceMatcher(this.from, this.to);
|
||||
|
||||
for (const operation of matcher.operations()) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
let target;
|
||||
|
||||
switch (tag) {
|
||||
case 'replace':
|
||||
target = this._diffLines(fromStart, fromEnd, toStart, toEnd);
|
||||
lines = lines.concat(target);
|
||||
break;
|
||||
case 'delete':
|
||||
target = this.from.slice(fromStart, fromEnd);
|
||||
lines = lines.concat(this._tagDeleted(target));
|
||||
break;
|
||||
case 'insert':
|
||||
target = this.to.slice(toStart, toEnd);
|
||||
lines = lines.concat(this._tagInserted(target));
|
||||
break;
|
||||
case 'equal':
|
||||
target = this.from.slice(fromStart, fromEnd);
|
||||
lines = lines.concat(this._tagEqual(target));
|
||||
break;
|
||||
default:
|
||||
throw 'unknown tag: ' + tag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
encodedDiff() {
|
||||
let lines = [];
|
||||
const matcher = new SequenceMatcher(this.from, this.to);
|
||||
|
||||
for (const operation of matcher.operations()) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
let target;
|
||||
|
||||
switch (tag) {
|
||||
case 'replace':
|
||||
target = this._diffLines(fromStart, fromEnd, toStart, toEnd, true);
|
||||
lines = lines.concat(target);
|
||||
break;
|
||||
case 'delete':
|
||||
target = this.from.slice(fromStart, fromEnd);
|
||||
lines = lines.concat(this._tagDeleted(target, true));
|
||||
break;
|
||||
case 'insert':
|
||||
target = this.to.slice(toStart, toEnd);
|
||||
lines = lines.concat(this._tagInserted(target, true));
|
||||
break;
|
||||
case 'equal':
|
||||
target = this.from.slice(fromStart, fromEnd);
|
||||
lines = lines.concat(this._tagEqual(target, true));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown tag: ${tag}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const blocks = [];
|
||||
let lastBlock = '';
|
||||
let lastLineType = '';
|
||||
for (const line of lines) {
|
||||
const lineType = line.match(/^<span class="line ([^" ]+)/)[1];
|
||||
if (lineType != lastLineType) {
|
||||
blocks.push(lastBlock + (lastBlock ? '</span>' : '' ));
|
||||
lastBlock = `<span class="block ${lineType}">`;
|
||||
lastLineType = lineType;
|
||||
}
|
||||
lastBlock += line;
|
||||
}
|
||||
if (lastBlock)
|
||||
blocks.push(`${lastBlock}</span>`);
|
||||
|
||||
return blocks.join('');
|
||||
}
|
||||
|
||||
_tagLine(mark, contents) {
|
||||
return contents.map(content => `${mark} ${content}`);
|
||||
}
|
||||
|
||||
_encodedTagLine(encodedClass, contents) {
|
||||
return contents.map(content => `<span class="line ${encodedClass}">${this._escapeForEncoded(content)}</span>`);
|
||||
}
|
||||
|
||||
_escapeForEncoded(string) {
|
||||
return string
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
_tagDeleted(contents, encoded) {
|
||||
return encoded ?
|
||||
this._encodedTagLine('deleted', contents) :
|
||||
this._tagLine('-', contents);
|
||||
}
|
||||
|
||||
_tagInserted(contents, encoded) {
|
||||
return encoded ?
|
||||
this._encodedTagLine('inserted', contents) :
|
||||
this._tagLine('+', contents);
|
||||
}
|
||||
|
||||
_tagEqual(contents, encoded) {
|
||||
return encoded ?
|
||||
this._encodedTagLine('equal', contents) :
|
||||
this._tagLine(' ', contents);
|
||||
}
|
||||
|
||||
_tagDifference(contents, encoded) {
|
||||
return encoded ?
|
||||
this._encodedTagLine('difference', contents) :
|
||||
this._tagLine('?', contents);
|
||||
}
|
||||
|
||||
_findDiffLineInfo(fromStart, fromEnd, toStart, toEnd) {
|
||||
let bestRatio = 0.74;
|
||||
let fromEqualIndex, toEqualIndex;
|
||||
let fromBestIndex, toBestIndex;
|
||||
|
||||
for (let toIndex = toStart; toIndex < toEnd; toIndex++) {
|
||||
for (let fromIndex = fromStart; fromIndex < fromEnd; fromIndex++) {
|
||||
if (this.from[fromIndex] == this.to[toIndex]) {
|
||||
if (fromEqualIndex === undefined)
|
||||
fromEqualIndex = fromIndex;
|
||||
if (toEqualIndex === undefined)
|
||||
toEqualIndex = toIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
const matcher = new SequenceMatcher(this.from[fromIndex],
|
||||
this.to[toIndex],
|
||||
this._isSpaceCharacter);
|
||||
if (matcher.ratio() > bestRatio) {
|
||||
bestRatio = matcher.ratio();
|
||||
fromBestIndex = fromIndex;
|
||||
toBestIndex = toIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [bestRatio,
|
||||
fromEqualIndex, toEqualIndex,
|
||||
fromBestIndex, toBestIndex];
|
||||
}
|
||||
|
||||
_diffLines(fromStart, fromEnd, toStart, toEnd, encoded) {
|
||||
const cutOff = 0.75;
|
||||
const info = this._findDiffLineInfo(fromStart, fromEnd, toStart, toEnd);
|
||||
let bestRatio = info[0];
|
||||
const fromEqualIndex = info[1];
|
||||
const toEqualIndex = info[2];
|
||||
let fromBestIndex = info[3];
|
||||
let toBestIndex = info[4];
|
||||
|
||||
if (bestRatio < cutOff) {
|
||||
if (fromEqualIndex === undefined) {
|
||||
const taggedFrom = this._tagDeleted(this.from.slice(fromStart, fromEnd), encoded);
|
||||
const taggedTo = this._tagInserted(this.to.slice(toStart, toEnd), encoded);
|
||||
if (toEnd - toStart < fromEnd - fromStart)
|
||||
return taggedTo.concat(taggedFrom);
|
||||
else
|
||||
return taggedFrom.concat(taggedTo);
|
||||
}
|
||||
|
||||
fromBestIndex = fromEqualIndex;
|
||||
toBestIndex = toEqualIndex;
|
||||
bestRatio = 1.0;
|
||||
}
|
||||
|
||||
return [].concat(
|
||||
this.__diffLines(fromStart, fromBestIndex,
|
||||
toStart, toBestIndex,
|
||||
encoded),
|
||||
(encoded ?
|
||||
this._diffLineEncoded(this.from[fromBestIndex],
|
||||
this.to[toBestIndex]) :
|
||||
this._diffLine(this.from[fromBestIndex],
|
||||
this.to[toBestIndex])
|
||||
),
|
||||
this.__diffLines(fromBestIndex + 1, fromEnd,
|
||||
toBestIndex + 1, toEnd,
|
||||
encoded)
|
||||
);
|
||||
}
|
||||
|
||||
__diffLines(fromStart, fromEnd, toStart, toEnd, encoded) {
|
||||
if (fromStart < fromEnd) {
|
||||
if (toStart < toEnd) {
|
||||
return this._diffLines(fromStart, fromEnd, toStart, toEnd, encoded);
|
||||
} else {
|
||||
return this._tagDeleted(this.from.slice(fromStart, fromEnd), encoded);
|
||||
}
|
||||
} else {
|
||||
return this._tagInserted(this.to.slice(toStart, toEnd), encoded);
|
||||
}
|
||||
}
|
||||
|
||||
_diffLineEncoded(fromLine, toLine) {
|
||||
const fromChars = fromLine.split('');
|
||||
const toChars = toLine.split('');
|
||||
const matcher = new SequenceMatcher(fromLine, toLine,
|
||||
this._isSpaceCharacter);
|
||||
const phrases = [];
|
||||
for (const operation of matcher.operations()) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
const fromPhrase = fromChars.slice(fromStart, fromEnd).join('');
|
||||
const toPhrase = toChars.slice(toStart, toEnd).join('');
|
||||
switch (tag) {
|
||||
case 'replace':
|
||||
case 'delete':
|
||||
case 'insert':
|
||||
case 'equal':
|
||||
phrases.push({ tag : tag,
|
||||
from : fromPhrase,
|
||||
encodedFrom : this._escapeForEncoded(fromPhrase),
|
||||
to : toPhrase,
|
||||
encodedTo : this._escapeForEncoded(toPhrase), });
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown tag: ${tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
const encodedPhrases = [];
|
||||
let current;
|
||||
let replaced = 0;
|
||||
let inserted = 0;
|
||||
let deleted = 0;
|
||||
for (let i = 0, maxi = phrases.length; i < maxi; i++)
|
||||
{
|
||||
current = phrases[i];
|
||||
switch (current.tag) {
|
||||
case 'replace':
|
||||
encodedPhrases.push('<span class="phrase replaced">');
|
||||
encodedPhrases.push(this._encodedTagPhrase('deleted', current.encodedFrom));
|
||||
encodedPhrases.push(this._encodedTagPhrase('inserted', current.encodedTo));
|
||||
encodedPhrases.push('</span>');
|
||||
replaced++;
|
||||
break;
|
||||
case 'delete':
|
||||
encodedPhrases.push(this._encodedTagPhrase('deleted', current.encodedFrom));
|
||||
deleted++;
|
||||
break;
|
||||
case 'insert':
|
||||
encodedPhrases.push(this._encodedTagPhrase('inserted', current.encodedTo));
|
||||
inserted++;
|
||||
break;
|
||||
case 'equal':
|
||||
// \95ύX\93_\82̊Ԃɋ\B2\82܂ꂽ1\95\B6\8E\9A\82\BE\82\AF\82̖\B3\95ύX\95\94\95\AA\82\BE\82\AF\82͓\C1\95ʈ\B5\82\A2
|
||||
if (
|
||||
current.from.length == 1 &&
|
||||
(i > 0 && phrases[i-1].tag != 'equal') &&
|
||||
(i < maxi-1 && phrases[i+1].tag != 'equal')
|
||||
) {
|
||||
encodedPhrases.push('<span class="phrase equal">');
|
||||
encodedPhrases.push(this._encodedTagPhrase('duplicated', current.encodedFrom));
|
||||
encodedPhrases.push(this._encodedTagPhrase('duplicated', current.encodedTo));
|
||||
encodedPhrases.push('</span>');
|
||||
}
|
||||
else {
|
||||
encodedPhrases.push(current.encodedFrom);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const extraClass = (replaced || (deleted && inserted)) ?
|
||||
' includes-both-modification' :
|
||||
'' ;
|
||||
|
||||
return [
|
||||
`<span class="line replaced${extraClass}">${encodedPhrases.join('')}</span>`
|
||||
];
|
||||
}
|
||||
|
||||
_encodedTagPhrase(encodedClass, content) {
|
||||
return `<span class="phrase ${encodedClass}">${content}</span>`;
|
||||
}
|
||||
|
||||
_diffLine(fromLine, toLine) {
|
||||
let fromTags = '';
|
||||
let toTags = '';
|
||||
const matcher = new SequenceMatcher(fromLine, toLine,
|
||||
this._isSpaceCharacter);
|
||||
|
||||
for (const operation of matcher.operations()) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
|
||||
const fromLength = fromEnd - fromStart;
|
||||
const toLength = toEnd - toStart;
|
||||
switch (tag) {
|
||||
case 'replace':
|
||||
fromTags += this._repeat('^', fromLength);
|
||||
toTags += this._repeat('^', toLength);
|
||||
break;
|
||||
case 'delete':
|
||||
fromTags += this._repeat('-', fromLength);
|
||||
break;
|
||||
case 'insert':
|
||||
toTags += this._repeat('+', toLength);
|
||||
break;
|
||||
case 'equal':
|
||||
fromTags += this._repeat(' ', fromLength);
|
||||
toTags += this._repeat(' ', toLength);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown tag: ${tag}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this._formatDiffPoint(fromLine, toLine, fromTags, toTags);
|
||||
}
|
||||
|
||||
_formatDiffPoint(fromLine, toLine, fromTags, toTags) {
|
||||
let common;
|
||||
let result;
|
||||
|
||||
common = Math.min(this._nLeadingCharacters(fromLine, '\t'),
|
||||
this._nLeadingCharacters(toLine, '\t'));
|
||||
common = Math.min(common,
|
||||
this._nLeadingCharacters(fromTags.slice(0, common),
|
||||
' '));
|
||||
fromTags = fromTags.slice(common).replace(/\s*$/, '');
|
||||
toTags = toTags.slice(common).replace(/\s*$/, '');
|
||||
|
||||
result = this._tagDeleted([fromLine]);
|
||||
if (fromTags.length > 0) {
|
||||
fromTags = this._repeat('\t', common) + fromTags;
|
||||
result = result.concat(this._tagDifference([fromTags]));
|
||||
}
|
||||
result = result.concat(this._tagInserted([toLine]));
|
||||
if (toTags.length > 0) {
|
||||
toTags = this._repeat('\t', common) + toTags;
|
||||
result = result.concat(this._tagDifference([toTags]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_nLeadingCharacters(string, character) {
|
||||
let n = 0;
|
||||
while (string[n] == character) {
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
_isSpaceCharacter(character) {
|
||||
return character == ' ' || character == '\t';
|
||||
}
|
||||
|
||||
_repeat(string, n) {
|
||||
let result = '';
|
||||
for (; n > 0; n--) {
|
||||
result += string;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const Diff = {
|
||||
|
||||
readable(from, to, encoded) {
|
||||
const differ = new ReadableDiffer(this._splitWithLine(from), this._splitWithLine(to));
|
||||
return encoded ?
|
||||
differ.encodedDiff() :
|
||||
differ.diff(encoded).join('\n') ;
|
||||
},
|
||||
|
||||
foldedReadable(from, to, encoded) {
|
||||
const differ = new ReadableDiffer(this._splitWithLine(this._fold(from)),
|
||||
this._splitWithLine(this._fold(to)));
|
||||
return encoded ?
|
||||
differ.encodedDiff() :
|
||||
differ.diff(encoded).join('\n') ;
|
||||
},
|
||||
|
||||
isInterested(diff) {
|
||||
if (!diff)
|
||||
return false;
|
||||
|
||||
if (diff.length == 0)
|
||||
return false;
|
||||
|
||||
if (!diff.match(/^[-+]/mg))
|
||||
return false;
|
||||
|
||||
if (diff.match(/^[ ?]/mg))
|
||||
return true;
|
||||
|
||||
if (diff.match(/(?:.*\n){2,}/g))
|
||||
return true;
|
||||
|
||||
if (this.needFold(diff))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
needFold(diff) {
|
||||
if (!diff)
|
||||
return false;
|
||||
|
||||
if (diff.match(/^[-+].{79}/mg))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_splitWithLine(string) {
|
||||
string = String(string);
|
||||
return string.length == 0 ? [] : string.split(/\r?\n/);
|
||||
},
|
||||
|
||||
_fold(string) {
|
||||
string = String(string);
|
||||
const foldedLines = string.split('\n').map(line => line.replace(/(.{78})/g, '$1\n'));
|
||||
return foldedLines.join('\n');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const DOMUpdater = {
|
||||
/**
|
||||
* method
|
||||
* @param before {Node} - the node to be updated, e.g. Element
|
||||
* @param after {Node} - the node describing updated state,
|
||||
* e.g. DocumentFragment
|
||||
* @return count {number} - the count of appied changes
|
||||
*/
|
||||
update(before, after, counter = { count: 0 }) {
|
||||
if (before.nodeValue !== null ||
|
||||
after.nodeValue !== null) {
|
||||
if (before.nodeValue != after.nodeValue) {
|
||||
//console.log('node value: ', after.nodeValue);
|
||||
before.nodeValue = after.nodeValue;
|
||||
counter.count++;
|
||||
}
|
||||
return counter.count;
|
||||
}
|
||||
|
||||
const beforeNodes = Array.from(before.childNodes, this._getDiffableNodeString);
|
||||
const afterNodes = Array.from(after.childNodes, this._getDiffableNodeString);
|
||||
const nodeOerations = (new SequenceMatcher(beforeNodes, afterNodes)).operations();
|
||||
// Update from back to front for safety!
|
||||
for (const operation of nodeOerations.reverse()) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
switch (tag) {
|
||||
case 'equal':
|
||||
for (let i = 0, maxi = fromEnd - fromStart; i < maxi; i++) {
|
||||
this.update(
|
||||
before.childNodes[fromStart + i],
|
||||
after.childNodes[toStart + i],
|
||||
counter
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
for (let i = fromEnd - 1; i >= fromStart; i--) {
|
||||
//console.log('delete: delete node: ', i, before.childNodes[i]);
|
||||
before.removeChild(before.childNodes[i]);
|
||||
counter.count++;
|
||||
}
|
||||
break;
|
||||
case 'insert': {
|
||||
const reference = before.childNodes[fromStart] || null;
|
||||
for (let i = toStart; i < toEnd; i++) {
|
||||
if (!after.childNodes[i])
|
||||
continue;
|
||||
//console.console.log('insert: insert node: ', i, after.childNodes[i]);
|
||||
before.insertBefore(after.childNodes[i].cloneNode(true), reference);
|
||||
counter.count++;
|
||||
}
|
||||
}; break;
|
||||
case 'replace': {
|
||||
for (let i = fromEnd - 1; i >= fromStart; i--) {
|
||||
//console.log('replace: delete node: ', i, before.childNodes[i]);
|
||||
before.removeChild(before.childNodes[i]);
|
||||
counter.count++;
|
||||
}
|
||||
const reference = before.childNodes[fromStart] || null;
|
||||
for (let i = toStart; i < toEnd; i++) {
|
||||
if (!after.childNodes[i])
|
||||
continue;
|
||||
//console.log('replace: insert node: ', i, after.childNodes[i]);
|
||||
before.insertBefore(after.childNodes[i].cloneNode(true), reference);
|
||||
counter.count++;
|
||||
}
|
||||
}; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (before.nodeType == before.ELEMENT_NODE &&
|
||||
after.nodeType == after.ELEMENT_NODE) {
|
||||
const beforeAttrs = Array.from(before.attributes, attr => `${attr.name}:${attr.value}`). sort();
|
||||
const afterAttrs = Array.from(after.attributes, attr => `${attr.name}:${attr.value}`). sort();
|
||||
const attrOerations = (new SequenceMatcher(beforeAttrs, afterAttrs)).operations();
|
||||
for (const operation of attrOerations) {
|
||||
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
||||
switch (tag) {
|
||||
case 'equal':
|
||||
break;
|
||||
case 'delete':
|
||||
for (let i = fromStart; i < fromEnd; i++) {
|
||||
const name = beforeAttrs[i].split(':')[0];
|
||||
//console.log('delete: delete attr: ', name);
|
||||
before.removeAttribute(name);
|
||||
counter.count++;
|
||||
}
|
||||
break;
|
||||
case 'insert':
|
||||
for (let i = toStart; i < toEnd; i++) {
|
||||
const attr = afterAttrs[i].split(':');
|
||||
const name = attr[0];
|
||||
const value = attr.slice(1).join(':');
|
||||
//console.log('insert: set attr: ', name, value);
|
||||
before.setAttribute(name, value);
|
||||
counter.count++;
|
||||
}
|
||||
break;
|
||||
case 'replace':
|
||||
const insertedAttrs = new Set();
|
||||
for (let i = toStart; i < toEnd; i++) {
|
||||
const attr = afterAttrs[i].split(':');
|
||||
const name = attr[0];
|
||||
const value = attr.slice(1).join(':');
|
||||
//console.log('replace: set attr: ', name, value);
|
||||
before.setAttribute(name, value);
|
||||
insertedAttrs.add(name);
|
||||
counter.count++;
|
||||
}
|
||||
for (let i = fromStart; i < fromEnd; i++) {
|
||||
const name = beforeAttrs[i].split(':')[0];
|
||||
if (insertedAttrs.has(name))
|
||||
continue;
|
||||
//console.log('replace: delete attr: ', name);
|
||||
before.removeAttribute(name);
|
||||
counter.count++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return counter.count;
|
||||
},
|
||||
|
||||
_getDiffableNodeString(node) {
|
||||
if (node.nodeType == node.ELEMENT_NODE)
|
||||
return `element:${node.tagName}#${node.id}#${node.getAttribute('anonid')}`;
|
||||
else
|
||||
return `node:${node.nodeType}`;
|
||||
}
|
||||
|
||||
};
|
||||
103
waterfox/browser/components/sidebar/common/handle-accel-key.js
Normal file
103
waterfox/browser/components/sidebar/common/handle-accel-key.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
(function() {
|
||||
if (window.handleAccelKeyLoaded)
|
||||
return;
|
||||
|
||||
function stringifySoloModifier(event) {
|
||||
switch (event.key) {
|
||||
case 'Alt':
|
||||
return (
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey
|
||||
) ? 'alt' : null;
|
||||
|
||||
case 'Control':
|
||||
return (
|
||||
!event.altKey &&
|
||||
!event.metaKey
|
||||
) ? 'control' : null;
|
||||
|
||||
case 'Meta':
|
||||
return (
|
||||
!event.altKey &&
|
||||
!event.ctrlKey
|
||||
) ? 'meta' : null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyModifier(event) {
|
||||
if (event.altKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey)
|
||||
return 'alt';
|
||||
|
||||
if (!event.altKey &&
|
||||
event.ctrlKey &&
|
||||
!event.metaKey)
|
||||
return 'control';
|
||||
|
||||
if (!event.altKey &&
|
||||
!event.ctrlKey &&
|
||||
event.metaKey)
|
||||
return 'meta';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function stringifyUnshiftedSoloModifier(event) {
|
||||
if (event.key != 'Shift')
|
||||
return null;
|
||||
return stringifyModifier(event);
|
||||
}
|
||||
|
||||
function stringifyMayTabSwitchModifier(event) {
|
||||
if (!/^(Tab|Shift|PageUp|PageDown)$/.test(event.key))
|
||||
return null;
|
||||
return stringifyModifier(event);
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
const modifier = stringifySoloModifier(event);
|
||||
const mayTabSwitchModifier = stringifyMayTabSwitchModifier(event);
|
||||
if (modifier ||
|
||||
mayTabSwitchModifier)
|
||||
browser.runtime.sendMessage({
|
||||
type: 'ws:notify-may-start-tab-switch',
|
||||
modifier: modifier || mayTabSwitchModifier
|
||||
});
|
||||
}
|
||||
|
||||
function onKeyUp(event) {
|
||||
const modifier = stringifySoloModifier(event);
|
||||
const unshiftedModifier = stringifyUnshiftedSoloModifier(event);
|
||||
const mayTabSwitchModifier = stringifyMayTabSwitchModifier(event);
|
||||
if (modifier ||
|
||||
(!unshiftedModifier &&
|
||||
!mayTabSwitchModifier))
|
||||
browser.runtime.sendMessage({
|
||||
type: 'ws:notify-may-end-tab-switch',
|
||||
modifier: modifier || stringifyModifier(event)
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
window.handleAccelKeyLoaded = true;
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true });
|
||||
window.addEventListener('keyup', onKeyUp, { capture: true });
|
||||
window.addEventListener('pagehide', () => {
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true });
|
||||
window.addEventListener('keyup', onKeyUp, { capture: true });
|
||||
window.addEventListener('pageshow', init, { once: true });
|
||||
}, { once: true });
|
||||
}
|
||||
init();
|
||||
})();
|
||||
362
waterfox/browser/components/sidebar/common/permissions.js
Normal file
362
waterfox/browser/components/sidebar/common/permissions.js
Normal file
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
notify,
|
||||
configs
|
||||
} from './common.js';
|
||||
import * as ApiTabs from '/common/api-tabs.js';
|
||||
import * as Constants from './constants.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/permissions', ...args);
|
||||
}
|
||||
|
||||
export const ALL_URLS = { origins: ['<all_urls>'] };
|
||||
export const BOOKMARKS = { permissions: ['bookmarks'] };
|
||||
export const CLIPBOARD_READ = { permissions: ['clipboardRead'] };
|
||||
export const TAB_HIDE = { permissions: ['tabHide'] };
|
||||
|
||||
const checkboxesForPermission = new Map();
|
||||
|
||||
export function clearRequest() {
|
||||
configs.requestingPermissions = null;
|
||||
}
|
||||
|
||||
|
||||
const cachedGranted = new Map();
|
||||
|
||||
export async function isGranted(permissions) {
|
||||
try {
|
||||
const granted = await browser.permissions.contains(permissions).catch(ApiTabs.createErrorHandler());
|
||||
cachedGranted.set(JSON.stringify(permissions), granted);
|
||||
return granted;
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
return Promise.reject(new Error('unsupported permission'));
|
||||
}
|
||||
}
|
||||
|
||||
export function isGrantedSync(permissions) {
|
||||
return cachedGranted.get(JSON.stringify(permissions));
|
||||
}
|
||||
|
||||
// cache last state
|
||||
for (const permissions of [ALL_URLS, BOOKMARKS, CLIPBOARD_READ, TAB_HIDE]) {
|
||||
isGranted(permissions);
|
||||
}
|
||||
|
||||
|
||||
const CUSTOM_PANEL_AVAILABLE_URLS_MATCHER = new RegExp(`^((https?|data):|moz-extension://${location.host}/)`);
|
||||
|
||||
export async function canInjectScriptToTab(tab) {
|
||||
if (!tab ||
|
||||
!CUSTOM_PANEL_AVAILABLE_URLS_MATCHER.test(tab.url))
|
||||
return false;
|
||||
|
||||
return isGranted(ALL_URLS);
|
||||
}
|
||||
|
||||
export function canInjectScriptToTabSync(tab) {
|
||||
if (!tab ||
|
||||
!CUSTOM_PANEL_AVAILABLE_URLS_MATCHER.test(tab.url))
|
||||
return false;
|
||||
|
||||
return isGrantedSync(ALL_URLS);
|
||||
}
|
||||
|
||||
|
||||
const mRequests = new Map();
|
||||
|
||||
function destroyRequest(request) {
|
||||
const permissions = JSON.stringify(request.permissions);
|
||||
const requests = mRequests.get(permissions);
|
||||
if (requests)
|
||||
requests.delete(request);
|
||||
|
||||
const onChanged = request.resolve;
|
||||
const checkbox = request.url;
|
||||
|
||||
request.permissions = undefined;
|
||||
request.onChanged = undefined;
|
||||
request.checkbox = undefined;
|
||||
|
||||
return { onChanged, checkbox };
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((message, _sender) => {
|
||||
if (!message ||
|
||||
!message.type ||
|
||||
message.type != Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED)
|
||||
return;
|
||||
|
||||
const permissions = JSON.stringify(message.permissions);
|
||||
|
||||
isGranted(message.permissions); // to cache latest state
|
||||
|
||||
const requests = mRequests.get(permissions);
|
||||
if (!requests)
|
||||
return;
|
||||
|
||||
mRequests.delete(permissions);
|
||||
|
||||
for (const request of requests) {
|
||||
const { onChanged, checkbox } = destroyRequest(request);
|
||||
const checked =onChanged ?
|
||||
onChanged(true) :
|
||||
undefined;
|
||||
checkbox.checked = checked !== undefined ? !!checked : true;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
// These events are not available yet on Firefox...
|
||||
browser.permissions.onAdded.addListener(addedPermissions => {
|
||||
const permissions = JSON.stringify(addedPermissions.permissions);
|
||||
const requests = mRequests.get(permissions);
|
||||
if (!requests)
|
||||
return;
|
||||
|
||||
mRequests.delete(permissions);
|
||||
|
||||
for (const request of requests) {
|
||||
const { checkbox } = destroyRequest(request);
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
browser.permissions.onRemoved.addListener(removedPermissions => {
|
||||
const permissions = JSON.stringify(addedPermissions.permissions);
|
||||
const requests = mRequests.get(permissions);
|
||||
if (!requests)
|
||||
return;
|
||||
|
||||
mRequests.delete(permissions);
|
||||
|
||||
for (const request of requests) {
|
||||
const { checkbox } = destroyRequest(request);
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
export function bindToCheckbox(permissions, checkbox, options = {}) {
|
||||
const checkboxes = checkboxesForPermission.get(permissions) || [];
|
||||
checkboxes.push(checkbox);
|
||||
checkboxesForPermission.set(permissions, checkboxes);
|
||||
|
||||
isGranted(permissions)
|
||||
.then(granted => {
|
||||
const checked = options.onInitialized ?
|
||||
options.onInitialized(granted) :
|
||||
checkbox.dataset.relatedConfigKey ?
|
||||
configs[checkbox.dataset.relatedConfigKey] :
|
||||
undefined;
|
||||
checkbox.checked = checked !== undefined ? !!checked : granted;
|
||||
})
|
||||
.catch(_error => {
|
||||
checkbox.setAttribute('readonly', true);
|
||||
checkbox.setAttribute('disabled', true);
|
||||
const label = checkbox.closest('label') || document.querySelector(`label[for=${checkbox.id}]`);
|
||||
if (label)
|
||||
label.setAttribute('disabled', true);
|
||||
});
|
||||
|
||||
checkbox.addEventListener('change', _event => {
|
||||
checkbox.requestPermissions()
|
||||
});
|
||||
|
||||
const key = JSON.stringify(permissions);
|
||||
const requests = mRequests.get(key) || new Set();
|
||||
const request = {
|
||||
permissions,
|
||||
onChanged: options.onChanged,
|
||||
checkbox,
|
||||
};
|
||||
requests.add(request);
|
||||
mRequests.set(key, requests);
|
||||
|
||||
checkbox.requestPermissions = async () => {
|
||||
log('permission requested: ', permissions);
|
||||
const checkboxes = checkboxesForPermission.get(permissions);
|
||||
try {
|
||||
log('checkboxes: ', checkboxes);
|
||||
log('checkbox.checked: ', checkbox.checked);
|
||||
if (!checkbox.checked) {
|
||||
if (checkbox.dataset.relatedConfigKey)
|
||||
configs[checkbox.dataset.relatedConfigKey] = false;
|
||||
if (options.onChanged)
|
||||
options.onChanged(false);
|
||||
const canRevoke = Array.from(checkboxes, checkbox => checkbox.dataset.relatedConfigKey ? configs[checkbox.dataset.relatedConfigKey] : null).filter(state => state !== null).every(state => !state);
|
||||
log('canRevoke: ', canRevoke);
|
||||
if (!canRevoke)
|
||||
return;
|
||||
log('revoking the permission');
|
||||
await browser.permissions.remove(permissions).catch(ApiTabs.createErrorSuppressor());
|
||||
for (const otherCheckbox of checkboxes) {
|
||||
if (otherCheckbox != checkbox &&
|
||||
otherCheckbox.dataset.relatedConfigKey)
|
||||
continue;
|
||||
otherCheckbox.checked = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const otherCheckbox of checkboxes) {
|
||||
if (otherCheckbox != checkbox &&
|
||||
otherCheckbox.dataset.relatedConfigKey)
|
||||
continue;
|
||||
otherCheckbox.checked = false;
|
||||
}
|
||||
|
||||
if (configs.requestingPermissionsNatively)
|
||||
return;
|
||||
|
||||
log('requesting the permission');
|
||||
configs.requestingPermissionsNatively = permissions;
|
||||
let granted = await browser.permissions.request(permissions).catch(ApiTabs.createErrorHandler());
|
||||
configs.requestingPermissionsNatively = null;
|
||||
|
||||
log('granted: ', granted);
|
||||
if (granted === undefined) {
|
||||
granted = await isGranted(permissions);
|
||||
log('granted (retry): ', granted);
|
||||
}
|
||||
else if (!granted) {
|
||||
log('not granted: cacneled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (granted) {
|
||||
if (checkbox.dataset.relatedConfigKey)
|
||||
configs[checkbox.dataset.relatedConfigKey] = true;
|
||||
const configValue = checkbox.dataset.relatedConfigKey ? true : null;
|
||||
const onChangedResult = options.onChanged && options.onChanged(true);
|
||||
const checked = configValue !== null ? configValue :
|
||||
options.onChanged ?
|
||||
onChangedResult :
|
||||
undefined;
|
||||
log('update checkboxes with checked state ', checked);
|
||||
for (const otherCheckbox of checkboxes) {
|
||||
if (otherCheckbox != checkbox &&
|
||||
otherCheckbox.dataset.relatedConfigKey)
|
||||
continue;
|
||||
otherCheckbox.checked = checked !== undefined ? !!checked : true;
|
||||
}
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED,
|
||||
permissions
|
||||
}).catch(_error => {});
|
||||
log('finish');
|
||||
return;
|
||||
}
|
||||
|
||||
log('fallback to the failsafe method');
|
||||
configs.requestingPermissions = permissions;
|
||||
browser.browserAction.setBadgeText({ text: '!' });
|
||||
browser.browserAction.setPopup({ popup: '' });
|
||||
|
||||
notify({
|
||||
title: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_title'),
|
||||
message: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_message'),
|
||||
icon: 'resources/24x24.svg#default'
|
||||
});
|
||||
return;
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error);
|
||||
}
|
||||
for (const checkbox of checkboxes) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function bindToClickable(permissions, node, { onChanged } = {}) {
|
||||
node.addEventListener('click', _event => {
|
||||
node.requestPermissions()
|
||||
});
|
||||
|
||||
if (node.requestPermissions)
|
||||
return;
|
||||
|
||||
node.requestPermissions = async () => {
|
||||
try {
|
||||
const checkboxes = checkboxesForPermission.get(permissions);
|
||||
if (configs.requestingPermissionsNatively ||
|
||||
checkboxes.every(checkbox => checkbox.checked))
|
||||
return;
|
||||
|
||||
configs.requestingPermissionsNatively = permissions;
|
||||
// We need to call this without delay to avoid "permissions.request may only be called from a user input handler" error.
|
||||
let granted = await browser.permissions.request(permissions).catch(ApiTabs.createErrorHandler());
|
||||
configs.requestingPermissionsNatively = null;
|
||||
|
||||
if (granted === undefined)
|
||||
granted = await isGranted(permissions);
|
||||
else if (!granted)
|
||||
return;
|
||||
|
||||
if (granted) {
|
||||
for (const checkbox of checkboxes) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
if (onChanged)
|
||||
onChanged(true);
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED,
|
||||
permissions
|
||||
}).catch(_error => {});
|
||||
return;
|
||||
}
|
||||
|
||||
configs.requestingPermissions = permissions;
|
||||
browser.browserAction.setBadgeText({ text: '!' });
|
||||
browser.browserAction.setPopup({ popup: '' });
|
||||
|
||||
notify({
|
||||
title: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_title'),
|
||||
message: browser.i18n.getMessage('config_requestPermissions_fallbackToToolbarButton_message'),
|
||||
icon: 'resources/24x24.svg#default'
|
||||
});
|
||||
return;
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function requestPostProcess() {
|
||||
if (!configs.requestingPermissions)
|
||||
return false;
|
||||
|
||||
const permissions = configs.requestingPermissions;
|
||||
configs.requestingPermissions = null;
|
||||
configs.requestingPermissionsNatively = permissions;
|
||||
|
||||
browser.browserAction.setBadgeText({ text: '' });
|
||||
browser.permissions.request(permissions)
|
||||
.then(granted => {
|
||||
log('permission requested: ', permissions, granted);
|
||||
if (granted)
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED,
|
||||
permissions
|
||||
}).catch(_error => {});
|
||||
})
|
||||
.catch(ApiTabs.createErrorSuppressor())
|
||||
.finally(() => {
|
||||
configs.requestingPermissionsNatively = null;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
configs.$loaded.then(() => {
|
||||
configs.requestingPermissionsNatively = null;
|
||||
});
|
||||
197
waterfox/browser/components/sidebar/common/retrieve-url.js
Normal file
197
waterfox/browser/components/sidebar/common/retrieve-url.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
} from './common.js';
|
||||
import * as Permissions from './permissions.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/retrieve-url', ...args);
|
||||
}
|
||||
|
||||
export const kTYPE_PLAIN_TEXT = 'text/plain';
|
||||
export const kTYPE_X_MOZ_URL = 'text/x-moz-url';
|
||||
export const kTYPE_URI_LIST = 'text/uri-list';
|
||||
export const kTYPE_MOZ_TEXT_INTERNAL = 'text/x-moz-text-internal';
|
||||
const kBOOKMARK_FOLDER = 'x-moz-place:';
|
||||
|
||||
const ACCEPTABLE_DATA_TYPES = [
|
||||
kTYPE_URI_LIST,
|
||||
kTYPE_X_MOZ_URL,
|
||||
kTYPE_MOZ_TEXT_INTERNAL,
|
||||
kTYPE_PLAIN_TEXT,
|
||||
];
|
||||
|
||||
let mFileURLResolver = null;
|
||||
|
||||
export async function registerFileURLResolver(resolver) {
|
||||
mFileURLResolver = resolver;
|
||||
}
|
||||
|
||||
export async function fromDragEvent(event) {
|
||||
log('fromDragEvent ', event);
|
||||
const dt = event.dataTransfer;
|
||||
const urls = [];
|
||||
if (dt.files.length > 0) {
|
||||
for (const file of dt.files) {
|
||||
if (typeof mFileURLResolver == 'function') {
|
||||
urls.push(await mFileURLResolver(file));
|
||||
}
|
||||
else {
|
||||
// Created object URLs need to be revoked by tryRevokeObjectURL()
|
||||
// in common/common.js, after they are loaded.
|
||||
urls.push(URL.createObjectURL(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const type of ACCEPTABLE_DATA_TYPES) {
|
||||
const urlData = dt.getData(type);
|
||||
if (urlData)
|
||||
urls.push(...fromData(urlData, type));
|
||||
if (urls.length)
|
||||
break;
|
||||
}
|
||||
for (const type of dt.types) {
|
||||
if (!/^application\/x-ws-drag-data;(.+)$/.test(type))
|
||||
continue;
|
||||
const params = RegExp.$1;
|
||||
const providerId = /provider=([^;&]+)/.test(params) && RegExp.$1;
|
||||
const dataId = /id=([^;&]+)/.test(params) && RegExp.$1;
|
||||
try {
|
||||
const dragData = await browser.runtime.sendMessage(providerId, {
|
||||
type: 'get-drag-data',
|
||||
id: dataId
|
||||
});
|
||||
if (!dragData || typeof dragData != 'object')
|
||||
break;
|
||||
for (const type of ACCEPTABLE_DATA_TYPES) {
|
||||
const urlData = dragData[type];
|
||||
if (urlData)
|
||||
urls.push(...fromData(urlData, type));
|
||||
if (urls.length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
log(' => retrieved: ', urls);
|
||||
return sanitizeURLs(urls);
|
||||
}
|
||||
|
||||
let mSelectionClipboardProvider = null;
|
||||
|
||||
/* provider should have two methods:
|
||||
isAvailable(): returns boolean which indicates the selection clipboard is available or not.
|
||||
getTextData(): returns a string from the selection clipboard.
|
||||
*/
|
||||
export function registerSelectionClipboardProvider(provider) {
|
||||
mSelectionClipboardProvider = provider;
|
||||
}
|
||||
|
||||
export async function fromClipboard({ selection } = {}) {
|
||||
const urls = [];
|
||||
|
||||
if (selection && mSelectionClipboardProvider) {
|
||||
if (await mSelectionClipboardProvider.isAvailable()) {
|
||||
const maybeUrlString = await mSelectionClipboardProvider.getTextData();
|
||||
if (maybeUrlString)
|
||||
urls.push(...fromData(maybeUrlString, kTYPE_PLAIN_TEXT));
|
||||
return sanitizeURLs(urls);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await Permissions.isGranted(Permissions.CLIPBOARD_READ)) ||
|
||||
typeof navigator.clipboard.read != 'function')
|
||||
return urls;
|
||||
|
||||
const clipboardContents = await navigator.clipboard.read();
|
||||
log('fromClipboard ', clipboardContents);
|
||||
|
||||
for (const item of clipboardContents) {
|
||||
for (const type of item.types) {
|
||||
const maybeUrlBlob = await item.getType(type);
|
||||
const maybeUrlString = await maybeUrlBlob.text();
|
||||
if (maybeUrlString)
|
||||
urls.push(...fromData(maybeUrlString, type));
|
||||
if (urls.length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
log(' => retrieved: ', urls);
|
||||
return sanitizeURLs(urls);
|
||||
}
|
||||
|
||||
function sanitizeURLs(urls) {
|
||||
const filteredUrls = urls.filter(url =>
|
||||
url &&
|
||||
url.length &&
|
||||
url.indexOf(kBOOKMARK_FOLDER) == 0 ||
|
||||
!/^\s*(javascript|data):/.test(url)
|
||||
);
|
||||
log('sanitizeURLs filtered: ', filteredUrls);
|
||||
|
||||
const fixedUrls = filteredUrls.map(fixupURIFromText);
|
||||
log('sanitizeURLs fixed: ', fixedUrls);
|
||||
|
||||
return fixedUrls;
|
||||
}
|
||||
|
||||
function fromData(data, type) {
|
||||
log('fromData: ', type, data);
|
||||
switch (type) {
|
||||
case kTYPE_URI_LIST:
|
||||
return data
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\n\n+/g, '\n')
|
||||
.split('\n')
|
||||
.filter(line => {
|
||||
return line.charAt(0) != '#';
|
||||
});
|
||||
|
||||
case kTYPE_X_MOZ_URL:
|
||||
return data
|
||||
.trim()
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\n\n+/g, '\n')
|
||||
.split('\n')
|
||||
.filter((_line, index) => {
|
||||
return index % 2 == 0;
|
||||
});
|
||||
|
||||
case kTYPE_MOZ_TEXT_INTERNAL:
|
||||
return data
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\n\n+/g, '\n')
|
||||
.trim()
|
||||
.split('\n');
|
||||
|
||||
case kTYPE_PLAIN_TEXT:
|
||||
return data
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\n\n+/g, '\n')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
return /^\w+:\/\/.+/.test(line) ? line : `ext+ws:search:${line}`;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function fixupURIFromText(maybeURI) {
|
||||
if (/^(ext\+)?\w+:/.test(maybeURI))
|
||||
return maybeURI;
|
||||
|
||||
if (/^([^\.\s]+\.)+[^\.\s]{2}/.test(maybeURI))
|
||||
return `http://${maybeURI}`;
|
||||
|
||||
return maybeURI;
|
||||
}
|
||||
305
waterfox/browser/components/sidebar/common/sidebar-connection.js
Normal file
305
waterfox/browser/components/sidebar/common/sidebar-connection.js
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
mapAndFilterUniq,
|
||||
configs,
|
||||
wait,
|
||||
} from './common.js';
|
||||
import * as Constants from './constants.js';
|
||||
import * as TabsStore from './tabs-store.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/sidebar-connection', ...args);
|
||||
}
|
||||
|
||||
export const onMessage = new EventListenerManager();
|
||||
export const onConnected = new EventListenerManager();
|
||||
export const onDisconnected = new EventListenerManager();
|
||||
|
||||
const mConnections = new Map();
|
||||
const mReceivers = new Map();
|
||||
const mFocusState = new Map();
|
||||
let mIsListening = false;
|
||||
|
||||
export function isInitialized() {
|
||||
return mIsListening;
|
||||
}
|
||||
|
||||
export function isSidebarOpen(windowId) {
|
||||
if (!mIsListening ||
|
||||
Constants.IS_SIDEBAR)
|
||||
return false;
|
||||
|
||||
// for automated tests
|
||||
if (configs.sidebarVirtuallyOpenedWindows.length > 0 &&
|
||||
configs.sidebarVirtuallyOpenedWindows.includes(windowId))
|
||||
return true;
|
||||
if (configs.sidebarVirtuallyClosedWindows.length > 0 &&
|
||||
configs.sidebarVirtuallyClosedWindows.includes(windowId))
|
||||
return false;
|
||||
|
||||
if (windowId in configs.sidebarWidthInWindow &&
|
||||
configs.sidebarWidthInWindow[windowId] == 0)
|
||||
return false;
|
||||
|
||||
const connections = mConnections.get(windowId);
|
||||
if (!connections)
|
||||
return false;
|
||||
for (const connection of connections) {
|
||||
if (connection.type == 'sidebar')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isOpen(windowId) {
|
||||
if (!mIsListening ||
|
||||
Constants.IS_SIDEBAR)
|
||||
return false;
|
||||
|
||||
// for automated tests
|
||||
if (configs.sidebarVirtuallyOpenedWindows.length > 0 &&
|
||||
configs.sidebarVirtuallyOpenedWindows.includes(windowId))
|
||||
return true;
|
||||
if (configs.sidebarVirtuallyClosedWindows.length > 0 &&
|
||||
configs.sidebarVirtuallyClosedWindows.includes(windowId))
|
||||
return false;
|
||||
|
||||
if (windowId in configs.sidebarWidthInWindow &&
|
||||
configs.sidebarWidthInWindow[windowId] == 0)
|
||||
return false;
|
||||
|
||||
const connections = mConnections.get(windowId);
|
||||
return connections && connections.size > 0;
|
||||
}
|
||||
|
||||
export function hasFocus(windowId) {
|
||||
return mFocusState.has(windowId)
|
||||
}
|
||||
|
||||
export const counts = {
|
||||
broadcast: {}
|
||||
};
|
||||
|
||||
export function getOpenWindowIds() {
|
||||
return mIsListening ? Array.from(mConnections.keys()) : [];
|
||||
}
|
||||
|
||||
export function sendMessage(message) {
|
||||
if (!mIsListening ||
|
||||
Constants.IS_SIDEBAR)
|
||||
return false;
|
||||
|
||||
if (Array.isArray(message))
|
||||
log('Sending ', message.length, ' messages');
|
||||
|
||||
if (message.windowId) {
|
||||
if (configs.loggingConnectionMessages) {
|
||||
counts[message.windowId] = counts[message.windowId] || {};
|
||||
const localCounts = counts[message.windowId];
|
||||
localCounts[message.type] = localCounts[message.type] || 0;
|
||||
localCounts[message.type]++;
|
||||
}
|
||||
const connections = mConnections.get(message.windowId);
|
||||
if (!connections || connections.size == 0)
|
||||
return false;
|
||||
for (const connection of connections) {
|
||||
sendMessageToPort(connection.port, message);
|
||||
}
|
||||
//port.postMessage(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
// broadcast
|
||||
counts.broadcast[message.type] = counts.broadcast[message.type] || 0;
|
||||
counts.broadcast[message.type]++;
|
||||
for (const connections of mConnections.values()) {
|
||||
if (!connections || connections.size == 0)
|
||||
continue;
|
||||
for (const connection of connections) {
|
||||
sendMessageToPort(connection.port, message);
|
||||
}
|
||||
//port.postMessage(message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const mReservedTasks = new WeakMap();
|
||||
|
||||
// Se should not send messages immediately, instead we should throttle
|
||||
// it and bulk-send multiple messages, for better user experience.
|
||||
// Sending too much messages in one event loop may block everything
|
||||
// and makes Firefox like frozen.
|
||||
function sendMessageToPort(port, message) {
|
||||
const task = mReservedTasks.get(port) || { messages: [] };
|
||||
task.messages.push(message);
|
||||
mReservedTasks.set(port, task);
|
||||
if (!task.onFrame) {
|
||||
task.onFrame = () => {
|
||||
delete task.onFrame;
|
||||
const messages = task.messages;
|
||||
task.messages = [];
|
||||
port.postMessage(messages);
|
||||
if (configs.debug) {
|
||||
const types = mapAndFilterUniq(messages,
|
||||
message => message.type || undefined).join(', ');
|
||||
log(`${messages.length} messages sent (${types}):`, messages);
|
||||
}
|
||||
};
|
||||
// We should not use window.requestAnimationFrame for throttling,
|
||||
// because it is quite lagged on some environment. Firefox may
|
||||
// decelerate the method for an invisible document (the background
|
||||
// page).
|
||||
//window.requestAnimationFrame(task.onFrame);
|
||||
setTimeout(task.onFrame, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (Constants.IS_BACKGROUND) {
|
||||
const matcher = new RegExp(`^${Constants.kCOMMAND_REQUEST_CONNECT_PREFIX}([0-9]+):(.+)$`);
|
||||
browser.runtime.onConnect.addListener(port => {
|
||||
if (!mIsListening ||
|
||||
!matcher.test(port.name))
|
||||
return;
|
||||
|
||||
const windowId = parseInt(RegExp.$1);
|
||||
const type = RegExp.$2;
|
||||
const connection = { port, type };
|
||||
const connections = mConnections.get(windowId) || new Set();
|
||||
connections.add(connection);
|
||||
mConnections.set(windowId, connections);
|
||||
let connectionTimeoutTimer = null;
|
||||
const updateTimeoutTimer = () => {
|
||||
if (connectionTimeoutTimer) {
|
||||
clearTimeout(connectionTimeoutTimer);
|
||||
connectionTimeoutTimer = null;
|
||||
}
|
||||
// On slow situation (like having too many tabs - 5000 or more)
|
||||
// we should wait more for the pong. Otherwise the vital check
|
||||
// may produce needless reloadings even if the sidebar is still alive.
|
||||
// See also https://github.com/piroor/treestyletab/issues/3130
|
||||
const timeout = configs.heartbeatInterval + Math.max(configs.connectionTimeoutDelay, TabsStore.tabs.size);
|
||||
connectionTimeoutTimer = setTimeout(async () => {
|
||||
log(`Missing heartbeat from window ${windowId}. Maybe disconnected or resumed.`);
|
||||
try {
|
||||
const pong = await browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_PING_TO_SIDEBAR,
|
||||
windowId
|
||||
});
|
||||
if (pong) {
|
||||
log(`Sidebar for the window ${windowId} responded. Keep connected.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
log(`Sidebar for the window ${windowId} did not respond. Disconnect now.`);
|
||||
cleanup(); // eslint-disable-line no-use-before-define
|
||||
port.disconnect();
|
||||
}, timeout);
|
||||
};
|
||||
const cleanup = _diconnectedPort => {
|
||||
if (!port.onMessage.hasListener(receiver)) // eslint-disable-line no-use-before-define
|
||||
return;
|
||||
if (connectionTimeoutTimer) {
|
||||
clearTimeout(connectionTimeoutTimer);
|
||||
connectionTimeoutTimer = null;
|
||||
}
|
||||
connections.delete(connection);
|
||||
if (connections.size == 0) {
|
||||
mConnections.delete(windowId);
|
||||
const sidebarWidthInWindow = { ...configs.sidebarWidthInWindow };
|
||||
delete sidebarWidthInWindow[windowId];
|
||||
configs.sidebarWidthInWindow = sidebarWidthInWindow;
|
||||
}
|
||||
port.onMessage.removeListener(receiver); // eslint-disable-line no-use-before-define
|
||||
mReceivers.delete(windowId);
|
||||
mFocusState.delete(windowId);
|
||||
onDisconnected.dispatch(windowId, connections.size);
|
||||
|
||||
// We need to notify this to some conetnt scripts, to destroy themselves.
|
||||
/*
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_SIDEBAR_CLOSED,
|
||||
windowId,
|
||||
});
|
||||
*/
|
||||
browser.windows.get(windowId, { populate: true }).then(async win => {
|
||||
let count = 0;
|
||||
for (const tab of win.tabs) {
|
||||
count++;
|
||||
if (count >= 20) {
|
||||
// We should not block too long seconds on too much tabs case.
|
||||
await wait(10);
|
||||
count = 0;
|
||||
}
|
||||
try {
|
||||
browser.tabs.sendMessage(tab.id, {
|
||||
type: Constants.kCOMMAND_NOTIFY_SIDEBAR_CLOSED,
|
||||
});
|
||||
}
|
||||
catch (_error) {
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const receiver = message => {
|
||||
if (Array.isArray(message))
|
||||
return message.forEach(receiver);
|
||||
if (message.type == Constants.kCONNECTION_HEARTBEAT)
|
||||
updateTimeoutTimer();
|
||||
else
|
||||
onMessage.dispatch(windowId, message);
|
||||
};
|
||||
port.onMessage.addListener(receiver);
|
||||
mReceivers.set(windowId, receiver);
|
||||
onConnected.dispatch(windowId, connections.size);
|
||||
port.onDisconnect.addListener(cleanup);
|
||||
});
|
||||
|
||||
onMessage.addListener(async (windowId, message) => {
|
||||
switch (message.type) {
|
||||
case Constants.kNOTIFY_SIDEBAR_FOCUS:
|
||||
mFocusState.set(windowId, true);
|
||||
break;
|
||||
|
||||
case Constants.kNOTIFY_SIDEBAR_BLUR:
|
||||
mFocusState.delete(windowId);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function init() {
|
||||
if (mIsListening)
|
||||
return;
|
||||
|
||||
mIsListening = true;
|
||||
}
|
||||
|
||||
|
||||
//===================================================================
|
||||
// Logging
|
||||
//===================================================================
|
||||
|
||||
browser.runtime.onMessage.addListener((message, _sender) => {
|
||||
if (!mIsListening ||
|
||||
!message ||
|
||||
typeof message != 'object' ||
|
||||
message.type != Constants.kCOMMAND_REQUEST_CONNECTION_MESSAGE_LOGS ||
|
||||
!Constants.IS_BACKGROUND)
|
||||
return;
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_RESPONSE_CONNECTION_MESSAGE_LOGS,
|
||||
logs: JSON.parse(JSON.stringify(counts))
|
||||
});
|
||||
});
|
||||
30
waterfox/browser/components/sidebar/common/sync-provider.js
Normal file
30
waterfox/browser/components/sidebar/common/sync-provider.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import * as Sync from '/common/sync.js';
|
||||
|
||||
Sync.registerExternalProvider({
|
||||
async getOtherDevices() {
|
||||
return browser.waterfoxBridge.listSyncDevices();
|
||||
},
|
||||
|
||||
sendTabsToDevice(tabs, deviceId) {
|
||||
if (!Array.isArray(tabs))
|
||||
tabs = [tabs];
|
||||
return browser.waterfoxBridge.sendToDevice(tabs.map(tab => tab.id), deviceId);
|
||||
},
|
||||
|
||||
sendTabsToAllDevices(tabs) {
|
||||
if (!Array.isArray(tabs))
|
||||
tabs = [tabs];
|
||||
return browser.waterfoxBridge.sendToDevice(tabs.map(tab => tab.id));
|
||||
},
|
||||
|
||||
manageDevices(windowId) {
|
||||
return browser.waterfoxBridge.openSyncDeviceSettings(windowId)
|
||||
},
|
||||
});
|
||||
515
waterfox/browser/components/sidebar/common/sync.js
Normal file
515
waterfox/browser/components/sidebar/common/sync.js
Normal file
@@ -0,0 +1,515 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs,
|
||||
notify,
|
||||
wait,
|
||||
getChunkedConfig,
|
||||
setChunkedConfig,
|
||||
isLinux,
|
||||
} from './common.js';
|
||||
import * as Constants from '/common/constants.js';
|
||||
import * as TreeBehavior from '/common/tree-behavior.js';
|
||||
|
||||
import { Tab } from '/common/TreeItem.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/sync', ...args);
|
||||
}
|
||||
|
||||
export const onMessage = new EventListenerManager();
|
||||
export const onNewDevice = new EventListenerManager();
|
||||
export const onUpdatedDevice = new EventListenerManager();
|
||||
export const onObsoleteDevice = new EventListenerManager();
|
||||
|
||||
const SEND_TABS_SIMULATOR_ID = 'send-tabs-to-device-simulator@piro.sakura.ne.jp';
|
||||
|
||||
// Workaround for https://github.com/piroor/treestyletab/blob/trunk/README.md#user-content-feature-requests-send-tab-tree-to-device-does-not-work
|
||||
// and https://bugzilla.mozilla.org/show_bug.cgi?id=1417183 (resolved as WONTFIX)
|
||||
// Firefox does not provide any API to access to Sync features directly.
|
||||
// We need to provide it as experiments API or something way.
|
||||
// This module is designed to work with a service provide which has features:
|
||||
// * async getOtherDevices()
|
||||
// - Returns an array of sync devices.
|
||||
// - Retruned array should have 0 or more items like:
|
||||
// { id: "identifier of the device",
|
||||
// name: "name of the device",
|
||||
// type: "type of the device (desktop, mobile, and so on)" }
|
||||
// * sendTabsToDevice(tabs, deviceId)
|
||||
// - Returns nothing.
|
||||
// - Sends URLs of given tabs to the specified device.
|
||||
// - The device is one of values returned by getOtherDevices().
|
||||
// * sendTabsToAllDevices(tabs)
|
||||
// - Returns nothing.
|
||||
// - Sends URLs of given tabs to all other devices.
|
||||
// * manageDevices(windowId)
|
||||
// - Returns nothing.
|
||||
// - Opens settings page for Sync devices.
|
||||
// It will appear as a tab in the specified window.
|
||||
|
||||
let mExternalProvider = null;
|
||||
|
||||
export function registerExternalProvider(provider) {
|
||||
mExternalProvider = provider;
|
||||
}
|
||||
|
||||
export function hasExternalProvider() {
|
||||
return !!mExternalProvider;
|
||||
}
|
||||
|
||||
let mMyDeviceInfo = null;
|
||||
|
||||
async function getMyDeviceInfo() {
|
||||
if (!configs.syncDeviceInfo || !configs.syncDeviceInfo.id) {
|
||||
const newDeviceInfo = await generateDeviceInfo();
|
||||
if (!configs.syncDeviceInfo)
|
||||
configs.syncDeviceInfo = newDeviceInfo;
|
||||
}
|
||||
return mMyDeviceInfo = configs.syncDeviceInfo;
|
||||
}
|
||||
|
||||
async function ensureDeviceInfoInitialized() {
|
||||
await getMyDeviceInfo();
|
||||
}
|
||||
|
||||
export async function waitUntilDeviceInfoInitialized() {
|
||||
while (!configs.syncDeviceInfo) {
|
||||
await wait(100);
|
||||
}
|
||||
mMyDeviceInfo = configs.syncDeviceInfo;
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
let preChanges = [];
|
||||
|
||||
function onConfigChanged(key, value = undefined) {
|
||||
if (!initialized) {
|
||||
preChanges.push({ key, value: value === undefined ? configs[key] : value });
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case 'syncOtherDevicesDetected':
|
||||
if (!configs.syncAvailableNotified) {
|
||||
configs.syncAvailableNotified = true;
|
||||
notify({
|
||||
title: browser.i18n.getMessage('syncAvailable_notification_title'),
|
||||
message: browser.i18n.getMessage(`syncAvailable_notification_message${isLinux() ? '_linux' : ''}`),
|
||||
url: `${Constants.kSHORTHAND_URIS.options}#syncTabsToDeviceOptions`,
|
||||
timeout: configs.syncAvailableNotificationTimeout
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
case 'syncDevices':
|
||||
// This may happen when all configs are reset.
|
||||
// We need to try updating devices after syncDeviceInfo is completely cleared.
|
||||
wait(100).then(updateDevices);
|
||||
break;
|
||||
|
||||
case 'syncDeviceInfo':
|
||||
if (configs.syncDeviceInfo &&
|
||||
mMyDeviceInfo &&
|
||||
configs.syncDeviceInfo.id == mMyDeviceInfo.id &&
|
||||
configs.syncDeviceInfo.timestamp == mMyDeviceInfo.timestamp)
|
||||
return; // ignore updating triggered by myself
|
||||
mMyDeviceInfo = null;
|
||||
updateSelf();
|
||||
break;
|
||||
|
||||
default:
|
||||
if (key.startsWith('chunkedSyncData'))
|
||||
reserveToReceiveMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
browser.runtime.onMessageExternal.addListener((message, sender) => {
|
||||
if (!initialized ||
|
||||
!message ||
|
||||
typeof message != 'object' ||
|
||||
typeof message.type != 'string' ||
|
||||
sender.id != SEND_TABS_SIMULATOR_ID)
|
||||
return;
|
||||
|
||||
switch (message.type) {
|
||||
case 'ready':
|
||||
try {
|
||||
browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'register-self' }).catch(_error => {});
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
case 'device-added':
|
||||
case 'device-updated':
|
||||
case 'device-removed':
|
||||
updateSelf();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
export async function init() {
|
||||
configs.$addObserver(onConfigChanged); // we need to register the listener to collect pre-sent changes
|
||||
await configs.$loaded;
|
||||
await ensureDeviceInfoInitialized();
|
||||
await updateSelf();
|
||||
initialized = true;
|
||||
|
||||
reserveToReceiveMessage();
|
||||
window.setInterval(updateSelf, 1000 * 60 * 60 * 24); // update info every day!
|
||||
|
||||
for (const change of preChanges) {
|
||||
onConfigChanged(change.key, change.value);
|
||||
}
|
||||
preChanges = [];
|
||||
|
||||
try {
|
||||
browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'register-self' }).catch(_error => {});
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateDeviceInfo({ name, icon } = {}) {
|
||||
const [platformInfo, browserInfo] = await Promise.all([
|
||||
browser.runtime.getPlatformInfo(),
|
||||
browser.runtime.getBrowserInfo()
|
||||
]);
|
||||
return {
|
||||
id: `device-${Date.now()}-${Math.round(Math.random() * 65000)}`,
|
||||
name: name === undefined ?
|
||||
browser.i18n.getMessage('syncDeviceDefaultName', [toHumanReadableOSName(platformInfo.os), browserInfo.name]) :
|
||||
(name || null),
|
||||
icon: icon || 'device-desktop'
|
||||
};
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/PlatformOs
|
||||
function toHumanReadableOSName(os) {
|
||||
switch (os) {
|
||||
case 'mac': return 'macOS';
|
||||
case 'win': return 'Windows';
|
||||
case 'android': return 'Android';
|
||||
case 'cros': return 'Chrome OS';
|
||||
case 'linux': return 'Linux';
|
||||
case 'openbsd': return 'Open/FreeBSD';
|
||||
default: return 'Unknown Platform';
|
||||
}
|
||||
}
|
||||
|
||||
configs.$addObserver(key => {
|
||||
switch (key) {
|
||||
case 'syncUnsendableUrlPattern':
|
||||
isSendableTab.unsendableUrlMatcher = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
async function updateSelf() {
|
||||
if (updateSelf.updating)
|
||||
return;
|
||||
|
||||
updateSelf.updating = true;
|
||||
|
||||
const [devices] = await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
return await browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'list-devices' });
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
})(),
|
||||
ensureDeviceInfoInitialized(),
|
||||
]);
|
||||
if (devices) {
|
||||
const myDeviceFromSimulator = devices.find(device => device.myself);
|
||||
if (mMyDeviceInfo.simulatorId != myDeviceFromSimulator.id)
|
||||
mMyDeviceInfo.simulatorId = myDeviceFromSimulator.id;
|
||||
}
|
||||
|
||||
configs.syncDeviceInfo = mMyDeviceInfo = {
|
||||
...clone(configs.syncDeviceInfo),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
await updateDevices();
|
||||
|
||||
setTimeout(() => {
|
||||
updateSelf.updating = false;
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async function updateDevices() {
|
||||
if (updateDevices.updating)
|
||||
return;
|
||||
updateDevices.updating = true;
|
||||
const [devicesFromSimulator] = await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
return await browser.runtime.sendMessage(SEND_TABS_SIMULATOR_ID, { type: 'list-devices' });
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
})(),
|
||||
waitUntilDeviceInfoInitialized(),
|
||||
]);
|
||||
|
||||
const remote = clone(configs.syncDevices);
|
||||
const local = clone(configs.syncDevicesLocalCache);
|
||||
log('devices updated: ', local, remote);
|
||||
for (const [id, info] of Object.entries(remote)) {
|
||||
if (id == mMyDeviceInfo.id)
|
||||
continue;
|
||||
local[id] = info;
|
||||
if (id in local) {
|
||||
log('updated device: ', info);
|
||||
onUpdatedDevice.dispatch(info);
|
||||
}
|
||||
else {
|
||||
log('new device: ', info);
|
||||
onNewDevice.dispatch(info);
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesFromSimulator) {
|
||||
const knownDeviceIdsFromSimulator = new Set(Object.values(local).map(device => device.simulatorId).filter(id => !!id));
|
||||
for (const deviceFromSimulator of devicesFromSimulator) {
|
||||
if (knownDeviceIdsFromSimulator.has(deviceFromSimulator.id))
|
||||
continue;
|
||||
const localId = `device-from-simulator:${deviceFromSimulator.id}`;
|
||||
local[localId] = {
|
||||
...deviceFromSimulator,
|
||||
id: localId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, info] of Object.entries(local)) {
|
||||
if (id in remote ||
|
||||
id == mMyDeviceInfo.id)
|
||||
continue;
|
||||
log('obsolete device: ', info);
|
||||
delete local[id];
|
||||
onObsoleteDevice.dispatch(info);
|
||||
}
|
||||
|
||||
if (configs.syncDeviceExpirationDays > 0) {
|
||||
const expireDateInSeconds = Date.now() - (1000 * 60 * 60 * configs.syncDeviceExpirationDays);
|
||||
for (const [id, info] of Object.entries(local)) {
|
||||
if (info &&
|
||||
info.timestamp < expireDateInSeconds) {
|
||||
delete local[id];
|
||||
log('expired device: ', info);
|
||||
onObsoleteDevice.dispatch(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local[mMyDeviceInfo.id] = clone(mMyDeviceInfo);
|
||||
log('store myself: ', mMyDeviceInfo, local[mMyDeviceInfo.id]);
|
||||
|
||||
if (!configs.syncOtherDevicesDetected && Object.keys(local).length > 1)
|
||||
configs.syncOtherDevicesDetected = true;
|
||||
|
||||
configs.syncDevices = local;
|
||||
configs.syncDevicesLocalCache = clone(local);
|
||||
setTimeout(() => {
|
||||
updateDevices.updating = false;
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function reserveToReceiveMessage() {
|
||||
if (reserveToReceiveMessage.reserved)
|
||||
clearTimeout(reserveToReceiveMessage.reserved);
|
||||
reserveToReceiveMessage.reserved = setTimeout(() => {
|
||||
delete reserveToReceiveMessage.reserved;
|
||||
receiveMessage();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async function receiveMessage() {
|
||||
const myDeviceInfo = await getMyDeviceInfo();
|
||||
try {
|
||||
const messages = readMessages();
|
||||
log('receiveMessage: queued messages => ', messages);
|
||||
const restMessages = messages.filter(message => {
|
||||
if (message.timestamp <= configs.syncLastMessageTimestamp)
|
||||
return false;
|
||||
if (message.to == myDeviceInfo.id) {
|
||||
log('receiveMessage receive: ', message);
|
||||
configs.syncLastMessageTimestamp = message.timestamp;
|
||||
onMessage.dispatch(message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
log('receiveMessage: restMessages => ', restMessages);
|
||||
if (restMessages.length != messages.length)
|
||||
writeMessages(restMessages);
|
||||
}
|
||||
catch(error) {
|
||||
log('receiveMessage fatal error: ', error);
|
||||
writeMessages([]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendMessage(to, data) {
|
||||
const myDeviceInfo = await getMyDeviceInfo();
|
||||
try {
|
||||
const messages = readMessages();
|
||||
messages.push({
|
||||
timestamp: Date.now(),
|
||||
from: myDeviceInfo.id,
|
||||
to,
|
||||
data
|
||||
});
|
||||
log('sendMessage: queued messages => ', messages);
|
||||
writeMessages(messages);
|
||||
}
|
||||
catch(error) {
|
||||
console.log('Sync.sendMessage: failed to send message ', error);
|
||||
writeMessages([]);
|
||||
}
|
||||
}
|
||||
|
||||
function readMessages() {
|
||||
try {
|
||||
return uniqMessages([
|
||||
...JSON.parse(getChunkedConfig('chunkedSyncDataLocal') || '[]'),
|
||||
...JSON.parse(getChunkedConfig('chunkedSyncData') || '[]')
|
||||
]);
|
||||
}
|
||||
catch(error) {
|
||||
log('failed to read messages: ', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeMessages(messages) {
|
||||
const stringified = JSON.stringify(messages || []);
|
||||
setChunkedConfig('chunkedSyncDataLocal', stringified);
|
||||
setChunkedConfig('chunkedSyncData', stringified);
|
||||
}
|
||||
|
||||
function uniqMessages(messages) {
|
||||
const knownMessages = new Set();
|
||||
return messages.filter(message => {
|
||||
const key = JSON.stringify(message);
|
||||
if (knownMessages.has(key))
|
||||
return false;
|
||||
knownMessages.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
export async function getOtherDevices() {
|
||||
if (mExternalProvider)
|
||||
return mExternalProvider.getOtherDevices();
|
||||
|
||||
await waitUntilDeviceInfoInitialized();
|
||||
|
||||
const devices = configs.syncDevices || {};
|
||||
const result = [];
|
||||
for (const [id, info] of Object.entries(devices)) {
|
||||
if (id == mMyDeviceInfo.id ||
|
||||
!info.id /* ignore invalid device info accidentally saved (see also https://github.com/piroor/treestyletab/issues/2922 ) */)
|
||||
continue;
|
||||
result.push(info);
|
||||
}
|
||||
return result.sort((a, b) => a.name > b.name);
|
||||
}
|
||||
|
||||
export function getDeviceName(id) {
|
||||
const devices = configs.syncDevices || {};
|
||||
if (!(id in devices) || !devices[id])
|
||||
return browser.i18n.getMessage('syncDeviceUnknownDevice');
|
||||
return String(devices[id].name || '').trim() || browser.i18n.getMessage('syncDeviceMissingDeviceName');
|
||||
}
|
||||
|
||||
// https://searchfox.org/mozilla-central/rev/d866b96d74ec2a63f09ee418f048d23f4fd379a2/browser/base/content/browser-sync.js#1176
|
||||
export function isSendableTab(tab) {
|
||||
if (!tab.url ||
|
||||
tab.url.length > 65535)
|
||||
return false;
|
||||
|
||||
if (!isSendableTab.unsendableUrlMatcher)
|
||||
isSendableTab.unsendableUrlMatcher = new RegExp(configs.syncUnsendableUrlPattern);
|
||||
return !isSendableTab.unsendableUrlMatcher.test(tab.url);
|
||||
}
|
||||
|
||||
export function sendTabsToDevice(tabs, { to, recursively } = {}) {
|
||||
if (recursively)
|
||||
tabs = Tab.collectRootTabs(tabs).map(tab => [tab, ...tab.$TST.descendants]).flat();
|
||||
tabs = tabs.filter(isSendableTab);
|
||||
|
||||
if (mExternalProvider)
|
||||
return mExternalProvider.sendTabsToDevice(tabs, to);
|
||||
|
||||
sendMessage(to, getTabsDataToSend(tabs));
|
||||
|
||||
const multiple = tabs.length > 1 ? '_multiple' : '';
|
||||
notify({
|
||||
title: browser.i18n.getMessage(
|
||||
`sentTabs_notification_title${multiple}`,
|
||||
[getDeviceName(to)]
|
||||
),
|
||||
message: browser.i18n.getMessage(
|
||||
`sentTabs_notification_message${multiple}`,
|
||||
[getDeviceName(to)]
|
||||
),
|
||||
timeout: configs.syncSentTabsNotificationTimeout
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendTabsToAllDevices(tabs, { recursively } = {}) {
|
||||
if (recursively)
|
||||
tabs = Tab.collectRootTabs(tabs).map(tab => [tab, ...tab.$TST.descendants]).flat();
|
||||
tabs = tabs.filter(isSendableTab);
|
||||
|
||||
if (mExternalProvider)
|
||||
return mExternalProvider.sendTabsToAllDevices(tabs);
|
||||
|
||||
const data = getTabsDataToSend(tabs);
|
||||
const devices = await getOtherDevices();
|
||||
for (const device of devices) {
|
||||
sendMessage(device.id, data);
|
||||
}
|
||||
|
||||
const multiple = tabs.length > 1 ? '_multiple' : '';
|
||||
notify({
|
||||
title: browser.i18n.getMessage(`sentTabsToAllDevices_notification_title${multiple}`),
|
||||
message: browser.i18n.getMessage(`sentTabsToAllDevices_notification_message${multiple}`),
|
||||
timeout: configs.syncSentTabsNotificationTimeout
|
||||
});
|
||||
}
|
||||
|
||||
function getTabsDataToSend(tabs) {
|
||||
return {
|
||||
type: Constants.kSYNC_DATA_TYPE_TABS,
|
||||
tabs: tabs.map(tab => ({ url: tab.url, cookieStoreId: tab.cookieStoreId })),
|
||||
structure : TreeBehavior.getTreeStructureFromTabs(tabs).map(item => item.parent)
|
||||
};
|
||||
}
|
||||
|
||||
export function manageDevices(windowId) {
|
||||
if (mExternalProvider)
|
||||
return mExternalProvider.manageDevices(windowId);
|
||||
|
||||
browser.tabs.create({
|
||||
windowId,
|
||||
url: `${Constants.kSHORTHAND_URIS.options}#syncTabsToDeviceOptions`
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
// internal operations means operations bypassing WebExtensions' tabs APIs.
|
||||
|
||||
import EventListenerManager from '/extlib/EventListenerManager.js';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
mapAndFilter,
|
||||
configs,
|
||||
wait,
|
||||
} from './common.js';
|
||||
import * as ApiTabs from './api-tabs.js';
|
||||
import * as Constants from './constants.js';
|
||||
import * as SidebarConnection from './sidebar-connection.js';
|
||||
import * as TabsStore from './tabs-store.js';
|
||||
import * as TabsUpdate from './tabs-update.js';
|
||||
|
||||
import { Tab, TreeItem } from '/common/TreeItem.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/tabs-internal-operation', ...args);
|
||||
}
|
||||
|
||||
export const onBeforeTabsRemove = new EventListenerManager();
|
||||
|
||||
export async function activateTab(tab, { byMouseOperation, keepMultiselection, silently } = {}) {
|
||||
if (!Constants.IS_BACKGROUND)
|
||||
throw new Error('Error: TabsInternalOperation.activateTab is available only on the background page, use a `kCOMMAND_ACTIVATE_TAB` message instead.');
|
||||
|
||||
tab = TabsStore.ensureLivingItem(tab);
|
||||
if (!tab)
|
||||
return;
|
||||
log('activateTab: ', dumpTab(tab));
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
win.internallyFocusingTabs.add(tab.id);
|
||||
if (byMouseOperation)
|
||||
win.internallyFocusingByMouseTabs.add(tab.id);
|
||||
if (silently)
|
||||
win.internallyFocusingSilentlyTabs.add(tab.id);
|
||||
let tabs = [tab];
|
||||
if (tab.$TST.hasOtherHighlighted &&
|
||||
keepMultiselection) {
|
||||
const highlightedTabs = Tab.getHighlightedTabs(tab.windowId);
|
||||
if (highlightedTabs.some(highlightedTab => highlightedTab.id == tab.id)) {
|
||||
// switch active tab with highlighted state
|
||||
tabs = tabs.concat(mapAndFilter(highlightedTabs,
|
||||
highlightedTab => highlightedTab.id != tab.id && highlightedTab || undefined));
|
||||
}
|
||||
}
|
||||
if (tabs.length == 1)
|
||||
win.tabsToBeHighlightedAlone.add(tab.id);
|
||||
highlightTabs(tabs);
|
||||
}
|
||||
|
||||
export async function blurTab(bluredTabs, { windowId, silently, keepDiscarded } = {}) {
|
||||
if (bluredTabs &&
|
||||
!Array.isArray(bluredTabs))
|
||||
bluredTabs = [bluredTabs];
|
||||
|
||||
const bluredTabIds = new Set(Array.from(bluredTabs || [], tab => tab.id || tab));
|
||||
|
||||
// First, try to find successor based on successorTabId from left tabs.
|
||||
let successorTab = Tab.get(bluredTabs.find(tab => tab.active)?.successorTabId);
|
||||
const scannedTabIds = new Set();
|
||||
while (bluredTabIds.has(successorTab?.id) ||
|
||||
(keepDiscarded &&
|
||||
successorTab?.discarded)) {
|
||||
if (scannedTabIds.has(successorTab.id))
|
||||
break; // prevent infinite loop!
|
||||
scannedTabIds.add(successorTab.id);
|
||||
const nextSuccessorTab = (successorTab.successorTabId > 0 && successorTab.successorTabId != successorTab.id) ?
|
||||
Tab.get(successorTab.successorTabId) :
|
||||
null;
|
||||
if (!nextSuccessorTab)
|
||||
break;
|
||||
successorTab = nextSuccessorTab;
|
||||
}
|
||||
log('blurTab/step 1: found successor = ', successorTab?.id);
|
||||
|
||||
// Second, try to detect successor based on their order.
|
||||
if (!successorTab ||
|
||||
bluredTabIds.has(successorTab.id) ||
|
||||
(keepDiscarded &&
|
||||
successorTab.discarded)) {
|
||||
if (successorTab)
|
||||
log(' => it cannot become the successor, find again');
|
||||
let bluredTabsFound = false;
|
||||
for (const tab of Tab.getVisibleTabs(windowId || bluredTabs[0].windowId)) {
|
||||
if (keepDiscarded &&
|
||||
tab.discarded) {
|
||||
continue;
|
||||
}
|
||||
const blured = bluredTabIds.has(tab.id);
|
||||
if (blured)
|
||||
bluredTabsFound = true;
|
||||
if (!bluredTabsFound)
|
||||
successorTab = tab;
|
||||
if (bluredTabsFound &&
|
||||
!blured) {
|
||||
successorTab = tab;
|
||||
break;
|
||||
}
|
||||
}
|
||||
log('blurTab/step 2: found successor = ', successorTab?.id);
|
||||
}
|
||||
|
||||
if (successorTab)
|
||||
await activateTab(successorTab, { silently });
|
||||
return successorTab;
|
||||
}
|
||||
|
||||
export function removeTab(tab) {
|
||||
return removeTabs([tab]);
|
||||
}
|
||||
|
||||
export async function removeTabs(tabs, { keepDescendants, byMouseOperation, originalStructure, triggerTab } = {}) {
|
||||
if (!Constants.IS_BACKGROUND)
|
||||
throw new Error('TabsInternalOperation.removeTabs is available only on the background page, use a `kCOMMAND_REMOVE_TABS_INTERNALLY` message instead.');
|
||||
|
||||
log('TabsInternalOperation.removeTabs: ', () => tabs.map(dumpTab));
|
||||
if (tabs.length == 0)
|
||||
return;
|
||||
|
||||
await onBeforeTabsRemove.dispatch(tabs);
|
||||
|
||||
const win = TabsStore.windows.get(tabs[0].windowId);
|
||||
const tabIds = [];
|
||||
let willChangeFocus = false;
|
||||
tabs = tabs.filter(tab => {
|
||||
if ((!win ||
|
||||
!win.internalClosingTabs.has(tab.id)) &&
|
||||
TabsStore.ensureLivingItem(tab)) {
|
||||
tabIds.push(tab.id);
|
||||
if (tab.active)
|
||||
willChangeFocus = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
log(' => ', () => tabs.map(dumpTab));
|
||||
if (!tabs.length)
|
||||
return;
|
||||
|
||||
if (win) {
|
||||
// Flag tabs to be closed at a time. With this flag TST skips some
|
||||
// operations on tab close (for example, opening a group tab to replace
|
||||
// a closed parent tab to keep the tree structure).
|
||||
for (const tab of tabs) {
|
||||
win.internalClosingTabs.add(tab.id);
|
||||
tab.$TST.addState(Constants.kTAB_STATE_TO_BE_REMOVED);
|
||||
clearCache(tab);
|
||||
if (keepDescendants)
|
||||
win.keepDescendantsTabs.add(tab.id);
|
||||
if (willChangeFocus && byMouseOperation) {
|
||||
win.internallyFocusingByMouseTabs.add(tab.id);
|
||||
setTimeout(() => { // the operation can be canceled
|
||||
win.internallyFocusingByMouseTabs.delete(tab.id);
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedTabs = TreeItem.sort(Array.from(tabs));
|
||||
Tab.onMultipleTabsRemoving.dispatch(sortedTabs, { triggerTab, originalStructure });
|
||||
|
||||
const promisedRemoved = browser.tabs.remove(tabIds).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
||||
if (win) {
|
||||
promisedRemoved.then(() => {
|
||||
// "beforeunload" listeners in tabs blocks the operation and the
|
||||
// returned promise is resolved after all "beforeunload" listeners
|
||||
// are processed and "browser.tabs.onRemoved()" listeners are
|
||||
// processed for really closed tabs.
|
||||
// In other words, there may be some "canceled tab close"s and
|
||||
// we need to clear "to-be-closed" flags for such tabs.
|
||||
// See also: https://github.com/piroor/treestyletab/issues/2384
|
||||
const canceledTabs = new Set(tabs.filter(tab => tab.$TST && !tab.$TST.destroyed));
|
||||
log(`${canceledTabs.size} tabs may be canceled to close.`);
|
||||
if (canceledTabs.size == 0) {
|
||||
Tab.onMultipleTabsRemoved.dispatch(sortedTabs, { triggerTab, originalStructure });
|
||||
return;
|
||||
}
|
||||
log(`Clearing "to-be-removed" flag for requested ${tabs.length} tabs...`);
|
||||
for (const tab of canceledTabs) {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_TO_BE_REMOVED);
|
||||
win.internalClosingTabs.delete(tab.id);
|
||||
if (keepDescendants)
|
||||
win.keepDescendantsTabs.delete(tab.id);
|
||||
}
|
||||
Tab.onMultipleTabsRemoved.dispatch(sortedTabs.filter(tab => !canceledTabs.has(tab)), { triggerTab, originalStructure });
|
||||
});
|
||||
}
|
||||
return promisedRemoved;
|
||||
}
|
||||
|
||||
export function setTabActive(tab) {
|
||||
const oldActiveTabs = clearOldActiveStateInWindow(tab.windowId, tab);
|
||||
tab.active = true;
|
||||
tab.$TST.addState(Constants.kTAB_STATE_ACTIVE);
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_NOT_ACTIVATED_SINCE_LOAD);
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_PENDING, { broadcast: Constants.IS_BACKGROUND });
|
||||
TabsStore.activeTabsInWindow.get(tab.windowId).add(tab);
|
||||
TabsStore.activeTabInWindow.set(tab.windowId, tab);
|
||||
Tab.onActivated.dispatch(tab);
|
||||
return oldActiveTabs;
|
||||
}
|
||||
|
||||
export function clearOldActiveStateInWindow(windowId, exception) {
|
||||
const oldTabs = TabsStore.activeTabsInWindow.get(windowId);
|
||||
for (const oldTab of oldTabs) {
|
||||
if (oldTab.id == exception?.id)
|
||||
continue;
|
||||
oldTab.$TST.removeState(Constants.kTAB_STATE_ACTIVE);
|
||||
oldTab.active = false;
|
||||
oldTabs.delete(oldTab);
|
||||
Tab.onUnactivated.dispatch(oldTab);
|
||||
}
|
||||
return Array.from(oldTabs);
|
||||
}
|
||||
|
||||
export function clearCache(tab) {
|
||||
if (!tab)
|
||||
return;
|
||||
const errorHandler = ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError);
|
||||
for (const key of Constants.kCACHE_KEYS) {
|
||||
browser.sessions.removeTabValue(tab.id, key).catch(errorHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this treats the first specified tab as active.
|
||||
export async function highlightTabs(tabs, { inheritToCollapsedDescendants } = {}) {
|
||||
if (!Constants.IS_BACKGROUND)
|
||||
throw new Error('TabsInternalOperation.highlightTabs is available only on the background page, use a `kCOMMAND_HIGHLIGHT_TABS` message instead.');
|
||||
|
||||
if (!tabs || tabs.length == 0)
|
||||
throw new Error('TabsInternalOperation.highlightTabs requires one or more tabs.');
|
||||
|
||||
const highlightedTabs = Tab.getHighlightedTabs(tabs[0].windowId);
|
||||
if (tabs.map(tab => tab.id).join('\n') == highlightedTabs.map(tab => tab.id).join('\n')) {
|
||||
log('highlightTabs: already highlighted');
|
||||
return;
|
||||
}
|
||||
|
||||
log('setting tabs highlighted ', tabs, { inheritToCollapsedDescendants });
|
||||
|
||||
const startAtTimestamp = Date.now();
|
||||
const startAt = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
|
||||
highlightTabs.lastStartedAt = startAt;
|
||||
|
||||
const windowId = tabs[0].windowId;
|
||||
const win = TabsStore.windows.get(windowId);
|
||||
|
||||
win.highlightingTabs.clear();
|
||||
win.tabsMovedWhileHighlighting = false;
|
||||
const tabIds = tabs.map(tab => {
|
||||
win.highlightingTabs.add(tab.id);
|
||||
return tab.id;
|
||||
});
|
||||
const toBeHighlightedTabIds = new Set([...win.highlightingTabs]);
|
||||
|
||||
TabsUpdate.updateTabsHighlighted({
|
||||
windowId,
|
||||
tabIds,
|
||||
inheritToCollapsedDescendants,
|
||||
});
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_HIGHLIGHTED_TABS_CHANGED,
|
||||
windowId,
|
||||
tabIds,
|
||||
});
|
||||
|
||||
// for better performance, we should not call browser.tabs.update() for each tab.
|
||||
const highlightedTabIds = new Set(tabIds);
|
||||
const activeTab = Tab.getActiveTab(windowId);
|
||||
const indices = mapAndFilter(highlightedTabIds,
|
||||
id => id == activeTab.id ? undefined : Tab.get(id).index);
|
||||
if (highlightedTabIds.has(activeTab.id))
|
||||
indices.unshift(activeTab.index);
|
||||
|
||||
// highlight tabs progressively, because massinve change at once may block updating of highlighted appearance of tabs.
|
||||
let count = 1; // 1 is for setActive()
|
||||
while (highlightTabs.lastStartedAt == startAt) {
|
||||
count += (configs.provressiveHighlightingStep <= 0 ? Number.MAX_SAFE_INTEGER : configs.provressiveHighlightingStep);
|
||||
await browser.tabs.highlight({
|
||||
windowId,
|
||||
populate: false,
|
||||
tabs: indices.slice(0, count),
|
||||
}).catch(ApiTabs.createErrorSuppressor());
|
||||
const progress = Math.ceil(Math.min(indices.length, count) / indices.length * 100);
|
||||
log(`highlightTabs: ${progress} %`);
|
||||
await wait(configs.progressievHighlightingInterval);
|
||||
|
||||
if (win.tabsMovedWhileHighlighting) {
|
||||
log('highlightTabs: tabs are moved while highlighting, retry');
|
||||
await wait(250);
|
||||
return highlightTabs(tabs, { inheritToCollapsedDescendants });
|
||||
}
|
||||
|
||||
if (win.highlightingTabs.size < toBeHighlightedTabIds.size) {
|
||||
log('highlightTabs: someone cleared multiselection while in-progress ', toBeHighlightedTabIds.size, win.highlightingTabs.size);
|
||||
break;
|
||||
}
|
||||
|
||||
const unifiedHighlightTabIds = new Set([...toBeHighlightedTabIds, ...win.highlightingTabs]);
|
||||
if (unifiedHighlightTabIds.size != toBeHighlightedTabIds.size) {
|
||||
log('highlightTabs: someone tried multiselection again while in-progress ', toBeHighlightedTabIds.size, win.highlightingTabs.size);
|
||||
break;
|
||||
}
|
||||
|
||||
if (count >= indices.length)
|
||||
break;
|
||||
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_IN_PROGRESS,
|
||||
windowId,
|
||||
progress,
|
||||
});
|
||||
}
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TABS_HIGHLIGHTING_COMPLETE,
|
||||
windowId,
|
||||
});
|
||||
log('highlightTabs done. ', Date.now() - startAtTimestamp, ' msec');
|
||||
}
|
||||
|
||||
|
||||
SidebarConnection.onMessage.addListener(async (windowId, message) => {
|
||||
switch (message.type) {
|
||||
case Constants.kCOMMAND_ACTIVATE_TAB: {
|
||||
await Tab.waitUntilTracked(message.tabId);
|
||||
const tab = Tab.get(message.tabId);
|
||||
if (!tab)
|
||||
return;
|
||||
activateTab(tab, {
|
||||
byMouseOperation: message.byMouseOperation,
|
||||
keepMultiselection: message.keepMultiselection,
|
||||
silently: message.silently
|
||||
});
|
||||
}; break;
|
||||
|
||||
case Constants.kCOMMAND_HIGHLIGHT_TABS: {
|
||||
await Tab.waitUntilTracked(message.tabIds);
|
||||
highlightTabs(message.tabIds.map(id => Tab.get(id)), {
|
||||
inheritToCollapsedDescendants: message.inheritToCollapsedDescendants,
|
||||
});
|
||||
}; break;
|
||||
|
||||
case Constants.kCOMMAND_REMOVE_TABS_INTERNALLY:
|
||||
await Tab.waitUntilTracked(message.tabIds);
|
||||
removeTabs(message.tabIds.map(id => Tab.get(id)), {
|
||||
byMouseOperation: message.byMouseOperation,
|
||||
keepDescendants: message.keepDescendants
|
||||
});
|
||||
break;
|
||||
|
||||
case Constants.kCOMMAND_REMOVE_TABS_BY_MOUSE_OPERATION:
|
||||
await Tab.waitUntilTracked(message.tabIds);
|
||||
removeTabs(message.tabIds.map(id => Tab.get(id)), {
|
||||
byMouseOperation: true,
|
||||
keepDescendants: message.keepDescendants
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (Constants.IS_BACKGROUND) {
|
||||
browser.runtime.onMessage.addListener((message, _sender) => {
|
||||
switch (message.type) {
|
||||
// for operations from group-tab.html
|
||||
case Constants.kCOMMAND_REMOVE_TABS_INTERNALLY:
|
||||
Tab.waitUntilTracked(message.tabIds).then(() => {
|
||||
removeTabs(message.tabIds.map(id => Tab.get(id)), {
|
||||
byMouseOperation: message.byMouseOperation,
|
||||
keepDescendants: message.keepDescendants,
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
// for automated tests
|
||||
case Constants.kCOMMAND_REMOVE_TABS_BY_MOUSE_OPERATION:
|
||||
Tab.waitUntilTracked(message.tabIds).then(() => {
|
||||
removeTabs(message.tabIds.map(id => Tab.get(id)), {
|
||||
byMouseOperation: true
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
798
waterfox/browser/components/sidebar/common/tabs-store.js
Normal file
798
waterfox/browser/components/sidebar/common/tabs-store.js
Normal file
@@ -0,0 +1,798 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs
|
||||
} from './common.js';
|
||||
import * as Constants from './constants.js';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function log(...args) {
|
||||
internalLogger('common/tabs', ...args);
|
||||
}
|
||||
|
||||
|
||||
let mTargetWindow;
|
||||
|
||||
export function setCurrentWindowId(targetWindow) {
|
||||
return mTargetWindow = targetWindow;
|
||||
}
|
||||
|
||||
export function getCurrentWindowId() {
|
||||
return mTargetWindow;
|
||||
}
|
||||
|
||||
|
||||
//===================================================================
|
||||
// Tab Tracking
|
||||
//===================================================================
|
||||
|
||||
export const windows = new Map();
|
||||
export const tabs = new Map();
|
||||
export const tabGroups = new Map();
|
||||
export const tabsByUniqueId = new Map();
|
||||
|
||||
export function clear() {
|
||||
tabs.clear();
|
||||
tabsByUniqueId.clear();
|
||||
}
|
||||
|
||||
export const queryLogs = [];
|
||||
const MAX_LOGS = 100000;
|
||||
|
||||
const MATCHING_ATTRIBUTES = `
|
||||
active
|
||||
attention
|
||||
audible
|
||||
autoDiscardable
|
||||
cookieStoreId
|
||||
discarded
|
||||
favIconUrl
|
||||
hidden
|
||||
highlighted
|
||||
id
|
||||
incognito
|
||||
index
|
||||
isArticle
|
||||
isInReaderMode
|
||||
pinned
|
||||
sessionId
|
||||
status
|
||||
successorId
|
||||
title
|
||||
url
|
||||
`.trim().split(/\s+/);
|
||||
|
||||
export function queryAll(query) {
|
||||
if (configs.loggingQueries) {
|
||||
queryLogs.push(query);
|
||||
queryLogs.splice(0, Math.max(0, queryLogs.length - MAX_LOGS));
|
||||
if (query.tabs?.name)
|
||||
query.indexedTabs = query.tabs.name;
|
||||
}
|
||||
fixupQuery(query);
|
||||
const startAt = Date.now();
|
||||
if (query.windowId || query.ordered) {
|
||||
let tabs = [];
|
||||
for (const win of windows.values()) {
|
||||
if (query.windowId && !matched(win.id, query.windowId))
|
||||
continue;
|
||||
const [sourceTabs, offset] = sourceTabsForQuery(query, win);
|
||||
tabs = tabs.concat(query.iterator ? getMatchedTabsIterator(sourceTabs, query, offset) : extractMatchedTabs(sourceTabs, query, offset));
|
||||
}
|
||||
query.elapsed = Date.now() - startAt;
|
||||
if (query.iterator) {
|
||||
return (function* () {
|
||||
for (const tabsIterator of tabs) {
|
||||
for (const tab of tabsIterator) {
|
||||
yield tab;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
else {
|
||||
return tabs;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const sourceTabs = (query.tabs || tabs).values();
|
||||
const matchedTabs = query.iterator ? getMatchedTabsIterator(sourceTabs, query) : extractMatchedTabs(sourceTabs, query);
|
||||
query.elapsed = Date.now() - startAt;
|
||||
return matchedTabs;
|
||||
}
|
||||
}
|
||||
|
||||
function sourceTabsForQuery(query, win) {
|
||||
let offset = 0;
|
||||
if (!query.ordered)
|
||||
return [query.tabs?.values() || win.tabs.values(), offset];
|
||||
let fromId;
|
||||
let toId = query.toId;
|
||||
if (typeof query.index == 'number') {
|
||||
fromId = win.order[query.index];
|
||||
offset = query.index;
|
||||
}
|
||||
if (typeof query.fromIndex == 'number') {
|
||||
fromId = win.order[query.fromIndex];
|
||||
offset = query.fromIndex;
|
||||
}
|
||||
if (typeof query.toIndex == 'number') {
|
||||
toId = win.order[query.toIndex];
|
||||
}
|
||||
if (typeof fromId != 'number') {
|
||||
fromId = query.fromId;
|
||||
offset = win.order.indexOf(query.fromId);
|
||||
}
|
||||
if (query.last || query.reversed)
|
||||
return [win.getReversedOrderedTabs(fromId, toId, query.tabs), offset];
|
||||
return [win.getOrderedTabs(fromId, toId, query.tabs), offset];
|
||||
}
|
||||
|
||||
function extractMatchedTabs(tabsInStore, query, offset) {
|
||||
const matchedTabs = [];
|
||||
let firstTime = true;
|
||||
let logicalIndex = offset || 0;
|
||||
for (const tab of tabsInStore) {
|
||||
if (!tabs.has(tab.id) ||
|
||||
(!query.skipMatching &&
|
||||
!matchedWithQuery(tab, query)))
|
||||
continue;
|
||||
|
||||
if (!firstTime)
|
||||
logicalIndex++;
|
||||
firstTime = false;
|
||||
if ('logicalIndex' in query &&
|
||||
!matched(logicalIndex, query.logicalIndex))
|
||||
continue;
|
||||
|
||||
matchedTabs.push(tab);
|
||||
if (query.first || query.last)
|
||||
break;
|
||||
}
|
||||
return matchedTabs;
|
||||
}
|
||||
|
||||
function* getMatchedTabsIterator(tabsInStore, query, offset) {
|
||||
let firstTime = true;
|
||||
let logicalIndex = offset || 0;
|
||||
for (const tab of tabsInStore) {
|
||||
if (!tabs.has(tab.id) ||
|
||||
(!query.skipMatching &&
|
||||
!matchedWithQuery(tab, query)))
|
||||
continue;
|
||||
|
||||
if (!firstTime)
|
||||
logicalIndex++;
|
||||
firstTime = false;
|
||||
if ('logicalIndex' in query &&
|
||||
!matched(logicalIndex, query.logicalIndex))
|
||||
continue;
|
||||
|
||||
yield tab;
|
||||
if (query.first || query.last)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function matchedWithQuery(tab, query) {
|
||||
for (const attribute of MATCHING_ATTRIBUTES) {
|
||||
if (attribute in query &&
|
||||
!matched(tab[attribute], query[attribute]))
|
||||
return false;
|
||||
const invertedAttribute = `!${attribute}`;
|
||||
if (invertedAttribute in query &&
|
||||
matched(tab[attribute], query[invertedAttribute]))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tab.$TST)
|
||||
return false;
|
||||
|
||||
const tabStates = tab.$TST.states;
|
||||
if ('states' in query && tabStates) {
|
||||
const queryStates = query.states;
|
||||
for (let i = 0, maxi = queryStates.length; i < maxi; i += 2) {
|
||||
const state = queryStates[i];
|
||||
const pattern = queryStates[i+1];
|
||||
if (!matched(tabStates.has(state), pattern))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const tabAttributes = tab.$TST.attributes;
|
||||
if ('attributes' in query && tabAttributes) {
|
||||
const queryAttributes = query.attributes;
|
||||
for (let i = 0, maxi = queryAttributes.length; i < maxi; i += 2) {
|
||||
const attribute = queryAttributes[i];
|
||||
const pattern = queryAttributes[i+1];
|
||||
if (!matched(tabAttributes[attribute], pattern))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (query.living &&
|
||||
!ensureLivingItem(tab))
|
||||
return false;
|
||||
if (query.normal &&
|
||||
(tab.hidden ||
|
||||
tabStates.has(Constants.kTAB_STATE_SHOWING) ||
|
||||
tab.pinned))
|
||||
return false;
|
||||
if (query.pinned &&
|
||||
(tab.hidden ||
|
||||
tabStates.has(Constants.kTAB_STATE_SHOWING) ||
|
||||
!tab.pinned))
|
||||
return false;
|
||||
if (query.visible &&
|
||||
((tabStates.has(Constants.kTAB_STATE_COLLAPSED) &&
|
||||
!tabStates.has(Constants.kTAB_STATE_EXPANDING)) ||
|
||||
tab.hidden ||
|
||||
tabStates.has(Constants.kTAB_STATE_SHOWING)))
|
||||
return false;
|
||||
if (query.hidden &&
|
||||
!tab.hidden)
|
||||
return false;
|
||||
if (query.controllable &&
|
||||
(tab.hidden ||
|
||||
tabStates.has(Constants.kTAB_STATE_SHOWING)))
|
||||
return false;
|
||||
if ('hasChild' in query &&
|
||||
query.hasChild != tab.$TST.hasChild)
|
||||
return false;
|
||||
if ('hasParent' in query &&
|
||||
query.hasParent != tab.$TST.hasParent)
|
||||
return false;
|
||||
if ('childOf' in query &&
|
||||
!tab.$TST.parentId != query.childOf)
|
||||
return false;
|
||||
if ('descendantOf' in query &&
|
||||
!tab.$TST.ancestorIds.includes(query.descendantOf))
|
||||
return false;
|
||||
if (query.groupId &&
|
||||
tab.groupId != query.groupId)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function matched(value, pattern) {
|
||||
if (pattern instanceof RegExp &&
|
||||
!pattern.test(String(value)))
|
||||
return false;
|
||||
if (pattern instanceof Set &&
|
||||
!pattern.has(value))
|
||||
return false;
|
||||
if (Array.isArray(pattern) &&
|
||||
!pattern.includes(value))
|
||||
return false;
|
||||
if (typeof pattern == 'function' &&
|
||||
!pattern(value))
|
||||
return false;
|
||||
if (typeof pattern == 'boolean' &&
|
||||
!!value !== pattern)
|
||||
return false;
|
||||
if (typeof pattern == 'string' &&
|
||||
String(value || '') != pattern)
|
||||
return false;
|
||||
if (typeof pattern == 'number' &&
|
||||
value != pattern)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function query(query) {
|
||||
if (configs.loggingQueries) {
|
||||
queryLogs.push(query);
|
||||
queryLogs.splice(0, Math.max(0, queryLogs.length - MAX_LOGS));
|
||||
if (query.tabs?.name)
|
||||
query.indexedTabs = query.tabs.name;
|
||||
}
|
||||
fixupQuery(query);
|
||||
if (query.last)
|
||||
query.ordered = true;
|
||||
else
|
||||
query.first = true;
|
||||
const startAt = Date.now();
|
||||
let tabs = [];
|
||||
if (query.windowId || query.ordered) {
|
||||
for (const win of windows.values()) {
|
||||
if (query.windowId && !matched(win.id, query.windowId))
|
||||
continue;
|
||||
const [sourceTabs, offset] = sourceTabsForQuery(query, win);
|
||||
tabs = tabs.concat(extractMatchedTabs(sourceTabs, query, offset));
|
||||
if (tabs.length > 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
tabs = extractMatchedTabs((query.tabs ||tabs).values(), query);
|
||||
}
|
||||
query.elapsed = Date.now() - startAt;
|
||||
return tabs.length > 0 ? tabs[0] : null ;
|
||||
}
|
||||
|
||||
function fixupQuery(query) {
|
||||
if (query.fromId ||
|
||||
query.toId ||
|
||||
typeof query.index == 'number' ||
|
||||
typeof query.fromIndex == 'number' ||
|
||||
typeof query.logicalIndex == 'number')
|
||||
query.ordered = true;
|
||||
if ((query.normal ||
|
||||
query.visible ||
|
||||
query.controllable ||
|
||||
query.pinned) &&
|
||||
!('living' in query))
|
||||
query.living = true;
|
||||
}
|
||||
|
||||
|
||||
//===================================================================
|
||||
// Indexes for optimization
|
||||
//===================================================================
|
||||
|
||||
export const activeTabInWindow = new Map();
|
||||
export const activeTabsInWindow = new Map();
|
||||
export const bundledActiveTabsInWindow = new Map();
|
||||
export const livingTabsInWindow = new Map();
|
||||
export const controllableTabsInWindow = new Map();
|
||||
export const removingTabsInWindow = new Map();
|
||||
export const removedTabsInWindow = new Map();
|
||||
export const visibleTabsInWindow = new Map();
|
||||
export const expandedTabsInWindow = new Map();
|
||||
export const selectedTabsInWindow = new Map();
|
||||
export const highlightedTabsInWindow = new Map();
|
||||
export const pinnedTabsInWindow = new Map();
|
||||
export const unpinnedTabsInWindow = new Map();
|
||||
export const rootTabsInWindow = new Map();
|
||||
export const groupTabsInWindow = new Map();
|
||||
export const toBeExpandedTabsInWindow = new Map();
|
||||
export const subtreeCollapsableTabsInWindow = new Map();
|
||||
export const draggingTabsInWindow = new Map();
|
||||
export const duplicatingTabsInWindow = new Map();
|
||||
export const toBeGroupedTabsInWindow = new Map();
|
||||
export const nativelyGroupedTabsInWindow = new Map();
|
||||
export const loadingTabsInWindow = new Map();
|
||||
export const unsynchronizedTabsInWindow = new Map();
|
||||
export const virtualScrollRenderableTabsInWindow = new Map();
|
||||
export const scrollPositionCalculationTargetTabsInWindow = new Map();
|
||||
export const canBecomeStickyTabsInWindow = new Map();
|
||||
|
||||
function createMapWithName(name) {
|
||||
const map = new Map();
|
||||
map.name = name;
|
||||
return map;
|
||||
}
|
||||
|
||||
export function prepareIndexesForWindow(windowId) {
|
||||
activeTabsInWindow.set(windowId, new Set());
|
||||
bundledActiveTabsInWindow.set(windowId, createMapWithName(`bundled active tabs in window ${windowId}`));
|
||||
livingTabsInWindow.set(windowId, createMapWithName(`living tabs in window ${windowId}`));
|
||||
controllableTabsInWindow.set(windowId, createMapWithName(`controllable tabs in window ${windowId}`));
|
||||
removingTabsInWindow.set(windowId, createMapWithName(`removing tabs in window ${windowId}`));
|
||||
removedTabsInWindow.set(windowId, createMapWithName(`removed tabs in window ${windowId}`));
|
||||
visibleTabsInWindow.set(windowId, createMapWithName(`visible tabs in window ${windowId}`));
|
||||
expandedTabsInWindow.set(windowId, createMapWithName(`expanded tabs in window ${windowId}`));
|
||||
selectedTabsInWindow.set(windowId, createMapWithName(`selected tabs in window ${windowId}`));
|
||||
highlightedTabsInWindow.set(windowId, createMapWithName(`highlighted tabs in window ${windowId}`));
|
||||
pinnedTabsInWindow.set(windowId, createMapWithName(`pinned tabs in window ${windowId}`));
|
||||
unpinnedTabsInWindow.set(windowId, createMapWithName(`unpinned tabs in window ${windowId}`));
|
||||
rootTabsInWindow.set(windowId, createMapWithName(`root tabs in window ${windowId}`));
|
||||
groupTabsInWindow.set(windowId, createMapWithName(`group tabs in window ${windowId}`));
|
||||
toBeExpandedTabsInWindow.set(windowId, createMapWithName(`to-be-expanded tabs in window ${windowId}`));
|
||||
subtreeCollapsableTabsInWindow.set(windowId, createMapWithName(`collapsable parent tabs in window ${windowId}`));
|
||||
draggingTabsInWindow.set(windowId, createMapWithName(`dragging tabs in window ${windowId}`));
|
||||
duplicatingTabsInWindow.set(windowId, createMapWithName(`duplicating tabs in window ${windowId}`));
|
||||
toBeGroupedTabsInWindow.set(windowId, createMapWithName(`to-be-grouped tabs in window ${windowId}`));
|
||||
nativelyGroupedTabsInWindow.set(windowId, createMapWithName(`natively grouped tabs in window ${windowId}`));
|
||||
loadingTabsInWindow.set(windowId, createMapWithName(`loading tabs in window ${windowId}`));
|
||||
unsynchronizedTabsInWindow.set(windowId, createMapWithName(`unsynchronized tabs in window ${windowId}`));
|
||||
virtualScrollRenderableTabsInWindow.set(windowId, createMapWithName(`virtual scroll renderable tabs in window ${windowId}`));
|
||||
scrollPositionCalculationTargetTabsInWindow.set(windowId, createMapWithName(`scroll position calculation target tabs in window ${windowId}`));
|
||||
canBecomeStickyTabsInWindow.set(windowId, createMapWithName(`can become sticky tabs in window ${windowId}`));
|
||||
}
|
||||
|
||||
export function unprepareIndexesForWindow(windowId) {
|
||||
activeTabInWindow.delete(windowId);
|
||||
activeTabsInWindow.delete(windowId);
|
||||
bundledActiveTabsInWindow.delete(windowId);
|
||||
livingTabsInWindow.delete(windowId);
|
||||
controllableTabsInWindow.delete(windowId);
|
||||
removingTabsInWindow.delete(windowId);
|
||||
removedTabsInWindow.delete(windowId);
|
||||
visibleTabsInWindow.delete(windowId);
|
||||
expandedTabsInWindow.delete(windowId);
|
||||
selectedTabsInWindow.delete(windowId);
|
||||
highlightedTabsInWindow.delete(windowId);
|
||||
pinnedTabsInWindow.delete(windowId);
|
||||
unpinnedTabsInWindow.delete(windowId);
|
||||
rootTabsInWindow.delete(windowId);
|
||||
groupTabsInWindow.delete(windowId);
|
||||
toBeExpandedTabsInWindow.delete(windowId);
|
||||
subtreeCollapsableTabsInWindow.delete(windowId);
|
||||
toBeGroupedTabsInWindow.delete(windowId);
|
||||
nativelyGroupedTabsInWindow.delete(windowId);
|
||||
loadingTabsInWindow.delete(windowId);
|
||||
unsynchronizedTabsInWindow.delete(windowId);
|
||||
virtualScrollRenderableTabsInWindow.delete(windowId);
|
||||
scrollPositionCalculationTargetTabsInWindow.delete(windowId);
|
||||
canBecomeStickyTabsInWindow.delete(windowId);
|
||||
}
|
||||
|
||||
export function getTabsMap(tabsStore, windowId = null) {
|
||||
return windowId ?
|
||||
tabsStore.get(windowId) :
|
||||
new Map([...tabsStore.values()].map(tabs => [...tabs.entries()]).flat());
|
||||
}
|
||||
|
||||
export function updateIndexesForTab(tab) {
|
||||
addLivingTab(tab);
|
||||
|
||||
if (!tab.hidden)
|
||||
addControllableTab(tab);
|
||||
else
|
||||
removeControllableTab(tab);
|
||||
|
||||
if (tab.$TST.collapsed)
|
||||
removeExpandedTab(tab);
|
||||
else
|
||||
addExpandedTab(tab);
|
||||
|
||||
if (tab.hidden || tab.$TST.collapsed)
|
||||
removeVisibleTab(tab);
|
||||
else
|
||||
addVisibleTab(tab);
|
||||
|
||||
if (tab.$TST.states.has(Constants.kTAB_STATE_SELECTED))
|
||||
addSelectedTab(tab);
|
||||
else
|
||||
removeSelectedTab(tab);
|
||||
|
||||
if (tab.highlighted)
|
||||
addHighlightedTab(tab);
|
||||
else
|
||||
removeHighlightedTab(tab);
|
||||
|
||||
if (tab.pinned) {
|
||||
removeUnpinnedTab(tab);
|
||||
addPinnedTab(tab);
|
||||
}
|
||||
else {
|
||||
removePinnedTab(tab);
|
||||
addUnpinnedTab(tab);
|
||||
}
|
||||
|
||||
if (tab.$TST.isGroupTab)
|
||||
addGroupTab(tab);
|
||||
else
|
||||
removeGroupTab(tab);
|
||||
|
||||
if (tab.$TST.duplicating)
|
||||
addDuplicatingTab(tab);
|
||||
else
|
||||
removeDuplicatingTab(tab);
|
||||
|
||||
if (tab.$TST.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID) &&
|
||||
!tab.$TST.getAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER))
|
||||
addToBeGroupedTab(tab);
|
||||
else
|
||||
removeToBeGroupedTab(tab);
|
||||
|
||||
if (tab.$TST.parent)
|
||||
removeRootTab(tab);
|
||||
else
|
||||
addRootTab(tab);
|
||||
|
||||
if (tab.$TST.hasChild &&
|
||||
!tab.$TST.subtreeCollapsed &&
|
||||
!tab.$TST.collapsed)
|
||||
addSubtreeCollapsableTab(tab);
|
||||
else
|
||||
removeSubtreeCollapsableTab(tab);
|
||||
|
||||
if (tab.status == 'loading')
|
||||
addLoadingTab(tab);
|
||||
else
|
||||
removeLoadingTab(tab);
|
||||
|
||||
if (tab.$TST.states.has(Constants.kTAB_STATE_BUNDLED_ACTIVE))
|
||||
addBundledActiveTab(tab);
|
||||
else
|
||||
removeBundledActiveTab(tab);
|
||||
|
||||
if (tab.groupId && tab.groupId != -1)
|
||||
addNativelyGroupedTab(tab);
|
||||
else
|
||||
removeNativelyGroupedTab(tab);
|
||||
|
||||
updateVirtualScrollRenderabilityIndexForTab(tab);
|
||||
|
||||
if (tab.$TST.states.has(Constants.kTAB_STATE_COLLAPSED_DONE))
|
||||
removeScrollPositionCalculationTargetTab(tab);
|
||||
else
|
||||
addScrollPositionCalculationTargetTab(tab);
|
||||
|
||||
if (tab.$TST.canBecomeSticky)
|
||||
addCanBecomeStickyTab(tab);
|
||||
else
|
||||
removeCanBecomeStickyTab(tab);
|
||||
}
|
||||
|
||||
export function updateVirtualScrollRenderabilityIndexForTab(tab) {
|
||||
if (tab.pinned ||
|
||||
(tab.hidden &&
|
||||
((tab.url == 'about:firefoxview' &&
|
||||
tab.cookieStoreId == 'firefox-default') ||
|
||||
!configs.renderHiddenTabs)) ||
|
||||
tab.$TST.states.has(Constants.kTAB_STATE_COLLAPSED_DONE) ||
|
||||
(tabGroups.get(tab.groupId)?.$TST.states.has(Constants.kTAB_STATE_COLLAPSED_DONE) &&
|
||||
!tab.active))
|
||||
removeVirtualScrollRenderableTab(tab);
|
||||
else
|
||||
addVirtualScrollRenderableTab(tab);
|
||||
}
|
||||
|
||||
export function removeTabFromIndexes(tab) {
|
||||
removeBundledActiveTab(tab);
|
||||
removeLivingTab(tab);
|
||||
removeControllableTab(tab);
|
||||
removeRemovingTab(tab);
|
||||
//removeRemovedTab(tab);
|
||||
removeVisibleTab(tab);
|
||||
removeExpandedTab(tab);
|
||||
removeSelectedTab(tab);
|
||||
removeHighlightedTab(tab);
|
||||
removePinnedTab(tab);
|
||||
removeUnpinnedTab(tab);
|
||||
removeRootTab(tab);
|
||||
removeGroupTab(tab);
|
||||
removeToBeExpandedTab(tab);
|
||||
removeSubtreeCollapsableTab(tab);
|
||||
removeDuplicatingTab(tab);
|
||||
removeDraggingTab(tab);
|
||||
removeToBeGroupedTab(tab);
|
||||
removeNativelyGroupedTab(tab);
|
||||
removeLoadingTab(tab);
|
||||
removeUnsynchronizedTab(tab);
|
||||
//removeVirtualScrollRenderableTab(tab);
|
||||
removeScrollPositionCalculationTargetTab(tab);
|
||||
removeCanBecomeStickyTab(tab);
|
||||
}
|
||||
|
||||
function addTabToIndex(tab, indexes, windowId = null) {
|
||||
if (!tab)
|
||||
throw new Error(`TabsStore.addTabToIndex gets non-tab parameter!: ${JSON.stringify(tab)} : ${new Error().stack}`);
|
||||
const tabs = indexes.get(windowId || tab.windowId);
|
||||
if (tabs)
|
||||
tabs.set(tab.id, tab);
|
||||
}
|
||||
|
||||
function removeTabFromIndex(tab, indexes, windowId = null) {
|
||||
if (!tab)
|
||||
throw new Error(`TabsStore.removeTabFromIndex gets non-tab parameter!: ${JSON.stringify(tab)} : ${new Error().stack}`);
|
||||
const tabs = indexes.get(windowId || tab.windowId);
|
||||
if (tabs)
|
||||
tabs.delete(tab.id);
|
||||
}
|
||||
|
||||
export function addLivingTab(tab) {
|
||||
addTabToIndex(tab, livingTabsInWindow);
|
||||
}
|
||||
export function removeLivingTab(tab) {
|
||||
removeTabFromIndex(tab, livingTabsInWindow);
|
||||
}
|
||||
|
||||
export function addControllableTab(tab) {
|
||||
addTabToIndex(tab, controllableTabsInWindow);
|
||||
}
|
||||
export function removeControllableTab(tab) {
|
||||
removeTabFromIndex(tab, controllableTabsInWindow);
|
||||
}
|
||||
|
||||
export function addRemovingTab(tab) {
|
||||
addTabToIndex(tab, removingTabsInWindow);
|
||||
removeTabFromIndexes(tab);
|
||||
}
|
||||
export function removeRemovingTab(tab) {
|
||||
removeTabFromIndex(tab, removingTabsInWindow);
|
||||
}
|
||||
|
||||
export function addRemovedTab(tab) {
|
||||
addTabToIndex(tab, removedTabsInWindow);
|
||||
setTimeout(removeRemovedTab, 100000, {
|
||||
id: tab.id,
|
||||
windowId: tab.windowId
|
||||
});
|
||||
}
|
||||
export function removeRemovedTab(tab) {
|
||||
removeTabFromIndex(tab, removedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addVisibleTab(tab) {
|
||||
addTabToIndex(tab, visibleTabsInWindow);
|
||||
}
|
||||
export function removeVisibleTab(tab) {
|
||||
removeTabFromIndex(tab, visibleTabsInWindow);
|
||||
}
|
||||
|
||||
export function addExpandedTab(tab) {
|
||||
addTabToIndex(tab, expandedTabsInWindow);
|
||||
}
|
||||
export function removeExpandedTab(tab) {
|
||||
removeTabFromIndex(tab, expandedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addSelectedTab(tab) {
|
||||
addTabToIndex(tab, selectedTabsInWindow);
|
||||
}
|
||||
export function removeSelectedTab(tab) {
|
||||
removeTabFromIndex(tab, selectedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addHighlightedTab(tab) {
|
||||
addTabToIndex(tab, highlightedTabsInWindow);
|
||||
}
|
||||
export function removeHighlightedTab(tab) {
|
||||
removeTabFromIndex(tab, highlightedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addPinnedTab(tab) {
|
||||
addTabToIndex(tab, pinnedTabsInWindow);
|
||||
}
|
||||
export function removePinnedTab(tab) {
|
||||
removeTabFromIndex(tab, pinnedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addUnpinnedTab(tab) {
|
||||
addTabToIndex(tab, unpinnedTabsInWindow);
|
||||
}
|
||||
export function removeUnpinnedTab(tab) {
|
||||
removeTabFromIndex(tab, unpinnedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addRootTab(tab) {
|
||||
addTabToIndex(tab, rootTabsInWindow);
|
||||
}
|
||||
export function removeRootTab(tab) {
|
||||
removeTabFromIndex(tab, rootTabsInWindow);
|
||||
}
|
||||
|
||||
export function addGroupTab(tab) {
|
||||
addTabToIndex(tab, groupTabsInWindow);
|
||||
}
|
||||
export function removeGroupTab(tab) {
|
||||
removeTabFromIndex(tab, groupTabsInWindow);
|
||||
}
|
||||
|
||||
export function addToBeExpandedTab(tab) {
|
||||
addTabToIndex(tab, toBeExpandedTabsInWindow);
|
||||
}
|
||||
export function removeToBeExpandedTab(tab) {
|
||||
removeTabFromIndex(tab, toBeExpandedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addSubtreeCollapsableTab(tab) {
|
||||
addTabToIndex(tab, subtreeCollapsableTabsInWindow);
|
||||
}
|
||||
export function removeSubtreeCollapsableTab(tab) {
|
||||
removeTabFromIndex(tab, subtreeCollapsableTabsInWindow);
|
||||
}
|
||||
|
||||
export function addDuplicatingTab(tab) {
|
||||
addTabToIndex(tab, duplicatingTabsInWindow);
|
||||
}
|
||||
export function removeDuplicatingTab(tab) {
|
||||
removeTabFromIndex(tab, duplicatingTabsInWindow);
|
||||
}
|
||||
|
||||
export function addDraggingTab(tab) {
|
||||
addTabToIndex(tab, draggingTabsInWindow);
|
||||
}
|
||||
export function removeDraggingTab(tab) {
|
||||
removeTabFromIndex(tab, draggingTabsInWindow);
|
||||
}
|
||||
|
||||
export function addToBeGroupedTab(tab) {
|
||||
addTabToIndex(tab, toBeGroupedTabsInWindow);
|
||||
}
|
||||
export function removeToBeGroupedTab(tab) {
|
||||
removeTabFromIndex(tab, toBeGroupedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addNativelyGroupedTab(tab, windowId = null) {
|
||||
addTabToIndex(tab, nativelyGroupedTabsInWindow, windowId);
|
||||
}
|
||||
export function removeNativelyGroupedTab(tab, windowId = null) {
|
||||
removeTabFromIndex(tab, nativelyGroupedTabsInWindow, windowId);
|
||||
}
|
||||
|
||||
export function addLoadingTab(tab) {
|
||||
addTabToIndex(tab, loadingTabsInWindow);
|
||||
}
|
||||
export function removeLoadingTab(tab) {
|
||||
removeTabFromIndex(tab, loadingTabsInWindow);
|
||||
}
|
||||
|
||||
export function addUnsynchronizedTab(tab) {
|
||||
addTabToIndex(tab, unsynchronizedTabsInWindow);
|
||||
}
|
||||
export function removeUnsynchronizedTab(tab) {
|
||||
removeTabFromIndex(tab, unsynchronizedTabsInWindow);
|
||||
}
|
||||
|
||||
export function addBundledActiveTab(tab) {
|
||||
addTabToIndex(tab, bundledActiveTabsInWindow);
|
||||
}
|
||||
export function removeBundledActiveTab(tab) {
|
||||
removeTabFromIndex(tab, bundledActiveTabsInWindow);
|
||||
}
|
||||
|
||||
export function addVirtualScrollRenderableTab(tab) {
|
||||
addTabToIndex(tab, virtualScrollRenderableTabsInWindow);
|
||||
}
|
||||
export function removeVirtualScrollRenderableTab(tab) {
|
||||
removeTabFromIndex(tab, virtualScrollRenderableTabsInWindow);
|
||||
}
|
||||
|
||||
export function addScrollPositionCalculationTargetTab(tab) {
|
||||
addTabToIndex(tab, scrollPositionCalculationTargetTabsInWindow);
|
||||
}
|
||||
export function removeScrollPositionCalculationTargetTab(tab) {
|
||||
removeTabFromIndex(tab, scrollPositionCalculationTargetTabsInWindow);
|
||||
}
|
||||
|
||||
export function addCanBecomeStickyTab(tab) {
|
||||
addTabToIndex(tab, canBecomeStickyTabsInWindow);
|
||||
}
|
||||
export function removeCanBecomeStickyTab(tab) {
|
||||
removeTabFromIndex(tab, canBecomeStickyTabsInWindow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//===================================================================
|
||||
// Utilities
|
||||
//===================================================================
|
||||
|
||||
export function assertValidTab(tab) {
|
||||
if (tab?.$TST)
|
||||
return;
|
||||
const error = new Error('FATAL ERROR: invalid tab is given');
|
||||
console.log(error.message, tab, error.stack);
|
||||
throw error;
|
||||
}
|
||||
|
||||
export function ensureLivingItem(tab) {
|
||||
const isNativeTabGroup = tab?.type == 'group';
|
||||
if (!tab ||
|
||||
!tab.id ||
|
||||
!tab.$TST ||
|
||||
(!isNativeTabGroup && !tabs.has(tab.id)) ||
|
||||
tab.$TST.removing ||
|
||||
!windows.get(tab.windowId) ||
|
||||
(isNativeTabGroup && !windows.get(tab.windowId).tabGroups.has(tab.id)))
|
||||
return null;
|
||||
return tab;
|
||||
}
|
||||
|
||||
|
||||
//===================================================================
|
||||
// Logging
|
||||
//===================================================================
|
||||
|
||||
browser.runtime.onMessage.addListener((message, _sender) => {
|
||||
if (!message ||
|
||||
typeof message != 'object' ||
|
||||
message.type != Constants.kCOMMAND_REQUEST_QUERY_LOGS)
|
||||
return;
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_RESPONSE_QUERY_LOGS,
|
||||
logs: JSON.parse(JSON.stringify(queryLogs)),
|
||||
windowId: mTargetWindow || 'background'
|
||||
});
|
||||
});
|
||||
444
waterfox/browser/components/sidebar/common/tabs-update.js
Normal file
444
waterfox/browser/components/sidebar/common/tabs-update.js
Normal file
@@ -0,0 +1,444 @@
|
||||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is the Tree Style Tab.
|
||||
*
|
||||
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011-2025
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
|
||||
* wanabe <https://github.com/wanabe>
|
||||
* Tetsuharu OHZEKI <https://github.com/saneyuki>
|
||||
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
|
||||
* lv7777 (https://github.com/lv7777)
|
||||
*
|
||||
* ***** END LICENSE BLOCK ******/
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
dumpTab,
|
||||
wait,
|
||||
mapAndFilter
|
||||
} from './common.js';
|
||||
|
||||
import * as Constants from './constants.js';
|
||||
import * as ContextualIdentities from './contextual-identities.js';
|
||||
import * as SidebarConnection from './sidebar-connection.js';
|
||||
import * as TabsStore from './tabs-store.js';
|
||||
|
||||
import { Tab } from './TreeItem.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/tabs-update', ...args);
|
||||
}
|
||||
|
||||
|
||||
const mBufferedUpdates = new Map();
|
||||
|
||||
function getBufferedUpdate(tab) {
|
||||
const update = mBufferedUpdates.get(tab.id) || {
|
||||
windowId: tab.windowId,
|
||||
tabId: tab.id,
|
||||
attributes: {
|
||||
updated: {},
|
||||
added: {},
|
||||
removed: new Set(),
|
||||
},
|
||||
isGroupTab: false,
|
||||
updatedTitle: undefined,
|
||||
updatedLabel: undefined,
|
||||
favIconUrl: undefined,
|
||||
loadingState: undefined,
|
||||
loadingStateReallyChanged: undefined,
|
||||
pinned: undefined,
|
||||
hidden: undefined,
|
||||
groupId: undefined,
|
||||
soundStateChanged: false,
|
||||
};
|
||||
mBufferedUpdates.set(tab.id, update);
|
||||
return update;
|
||||
}
|
||||
|
||||
function flushBufferedUpdates() {
|
||||
if (!Constants.IS_BACKGROUND) {
|
||||
mBufferedUpdates.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const triedAt = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
|
||||
flushBufferedUpdates.triedAt = triedAt;
|
||||
(Constants.IS_BACKGROUND ?
|
||||
setTimeout : // because window.requestAnimationFrame is decelerate for an invisible document.
|
||||
window.requestAnimationFrame)(() => {
|
||||
if (flushBufferedUpdates.triedAt != triedAt)
|
||||
return;
|
||||
for (const update of mBufferedUpdates.values()) {
|
||||
// no need to notify attributes broadcasted via Tab.broadcastState()
|
||||
delete update.attributes.updated.highlighted;
|
||||
delete update.attributes.updated.hidden;
|
||||
delete update.attributes.updated.pinned;
|
||||
delete update.attributes.updated.audible;
|
||||
delete update.attributes.updated.mutedInfo;
|
||||
delete update.attributes.updated.sharingState;
|
||||
delete update.attributes.updated.incognito;
|
||||
delete update.attributes.updated.attention;
|
||||
delete update.attributes.updated.discarded;
|
||||
if (Object.keys(update.attributes.updated).length > 0 ||
|
||||
Object.keys(update.attributes.added).length > 0 ||
|
||||
update.attributes.removed.size > 0 ||
|
||||
update.soundStateChanged ||
|
||||
update.sharingStateChanged)
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TAB_UPDATED,
|
||||
windowId: update.windowId,
|
||||
tabId: update.tabId,
|
||||
updatedProperties: update.attributes.updated,
|
||||
addedAttributes: update.attributes.added,
|
||||
removedAttributes: [...update.attributes.removed],
|
||||
soundStateChanged: update.soundStateChanged,
|
||||
sharingStateChanged: update.sharingStateChanged,
|
||||
});
|
||||
|
||||
// SidebarConnection.sendMessage() has its own bulk-send mechanism,
|
||||
// so we don't need to bundle them like an array.
|
||||
if (update.isGroupTab)
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_GROUP_TAB_DETECTED,
|
||||
windowId: update.windowId,
|
||||
tabId: update.tabId,
|
||||
});
|
||||
if (update.updatedTitle !== undefined)
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TREE_ITEM_LABEL_UPDATED,
|
||||
windowId: update.windowId,
|
||||
tabId: update.tabId,
|
||||
title: update.updatedTitle,
|
||||
label: update.updatedLabel,
|
||||
});
|
||||
if (update.favIconUrl !== undefined)
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED,
|
||||
windowId: update.windowId,
|
||||
tabId: update.tabId,
|
||||
favIconUrl: update.favIconUrl,
|
||||
});
|
||||
if (update.loadingState !== undefined)
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_UPDATE_LOADING_STATE,
|
||||
windowId: update.windowId,
|
||||
tabId: update.tabId,
|
||||
status: update.loadingState,
|
||||
reallyChanged: update.loadingStateReallyChanged,
|
||||
});
|
||||
if (update.pinned !== undefined)
|
||||
SidebarConnection.sendMessage({
|
||||
type: update.pinned ? Constants.kCOMMAND_NOTIFY_TAB_PINNED : Constants.kCOMMAND_NOTIFY_TAB_UNPINNED,
|
||||
windowId: update.windowId,
|
||||
tabId: update.tabId,
|
||||
});
|
||||
if (update.hidden !== undefined)
|
||||
SidebarConnection.sendMessage({
|
||||
type: update.hidden ? Constants.kCOMMAND_NOTIFY_TAB_HIDDEN : Constants.kCOMMAND_NOTIFY_TAB_SHOWN,
|
||||
windowId: update.windowId,
|
||||
tabId: update.tabId,
|
||||
});
|
||||
}
|
||||
mBufferedUpdates.clear();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function updateTab(tab, newState = {}, options = {}) {
|
||||
const update = getBufferedUpdate(tab);
|
||||
const oldState = options.old || {};
|
||||
|
||||
if ('url' in newState) {
|
||||
tab.$TST.setAttribute(Constants.kCURRENT_URI, update.attributes.added[Constants.kCURRENT_URI] = newState.url);
|
||||
update.attributes.removed.delete(Constants.kCURRENT_URI);
|
||||
}
|
||||
|
||||
if ('url' in newState &&
|
||||
newState.url.indexOf(Constants.kGROUP_TAB_URI) == 0) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
||||
update.isGroupTab = true;
|
||||
Tab.onGroupTabDetected.dispatch(tab);
|
||||
}
|
||||
else if (tab.$TST.states.has(Constants.kTAB_STATE_GROUP_TAB) &&
|
||||
!tab.$TST.hasGroupTabURL) {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
||||
update.isGroupTab = false;
|
||||
}
|
||||
|
||||
if (options.forceApply ||
|
||||
('title' in newState &&
|
||||
newState.title != oldState.title)) {
|
||||
if (options.forceApply) {
|
||||
tab.$TST.getPermanentStates().then(states => {
|
||||
tab.$TST.toggleState(
|
||||
Constants.kTAB_STATE_UNREAD,
|
||||
(states.includes(Constants.kTAB_STATE_UNREAD) &&
|
||||
!tab.$TST.isGroupTab),
|
||||
{ permanently: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
else if (tab.$TST.isGroupTab) {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
|
||||
}
|
||||
else if (!tab.active) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_UNREAD, { permanently: true });
|
||||
}
|
||||
tab.$TST.label = newState.title;
|
||||
Tab.onLabelUpdated.dispatch(tab);
|
||||
update.updatedTitle = tab.title;
|
||||
update.updatedLabel = tab.$TST.label;
|
||||
}
|
||||
|
||||
const openerOfGroupTab = tab.$TST.isGroupTab && Tab.getOpenerFromGroupTab(tab);
|
||||
if (openerOfGroupTab &&
|
||||
openerOfGroupTab.favIconUrl) {
|
||||
update.favIconUrl = openerOfGroupTab.favIconUrl;
|
||||
}
|
||||
else if (options.forceApply ||
|
||||
'favIconUrl' in newState) {
|
||||
tab.$TST.setAttribute(Constants.kCURRENT_FAVICON_URI, update.attributes.added[Constants.kCURRENT_FAVICON_URI] = tab.favIconUrl);
|
||||
update.attributes.removed.delete(Constants.kCURRENT_FAVICON_URI);
|
||||
// "favIconUrl" will be "undefined" if the website has no favicon.
|
||||
// Keys with "undefined" value will be removed from JSON-stringified result,
|
||||
// so we need to use "null" instead of it.
|
||||
// See also: https://github.com/piroor/treestyletab/issues/3515
|
||||
update.favIconUrl = tab.favIconUrl || null;
|
||||
}
|
||||
else if (tab.$TST.isGroupTab) {
|
||||
// "about:ws-group" can set error icon for the favicon and
|
||||
// reloading doesn't cloear that, so we need to clear favIconUrl manually.
|
||||
tab.favIconUrl = null;
|
||||
delete update.attributes.added[Constants.kCURRENT_FAVICON_URI];
|
||||
update.attributes.removed.add(Constants.kCURRENT_URI);
|
||||
tab.$TST.removeAttribute(Constants.kCURRENT_FAVICON_URI);
|
||||
update.favIconUrl = null;
|
||||
}
|
||||
|
||||
if ('status' in newState) {
|
||||
const reallyChanged = !tab.$TST.states.has(newState.status);
|
||||
const removed = newState.status == 'loading' ? 'complete' : 'loading';
|
||||
tab.$TST.removeState(removed);
|
||||
tab.$TST.addState(newState.status);
|
||||
if (!options.forceApply) {
|
||||
update.loadingState = tab.status;
|
||||
update.loadingStateReallyChanged = reallyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
if ((options.forceApply ||
|
||||
'pinned' in newState) &&
|
||||
newState.pinned != tab.$TST.states.has(Constants.kTAB_STATE_PINNED)) {
|
||||
if (newState.pinned) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_PINNED);
|
||||
tab.$TST.removeAttribute(Constants.kLEVEL); // don't indent pinned tabs!
|
||||
delete update.attributes.added[Constants.kLEVEL];
|
||||
update.attributes.removed.add(Constants.kLEVEL);
|
||||
Tab.onPinned.dispatch(tab);
|
||||
update.pinned = true;
|
||||
}
|
||||
else {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_PINNED);
|
||||
Tab.onUnpinned.dispatch(tab);
|
||||
update.pinned = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.forceApply ||
|
||||
'audible' in newState)
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_AUDIBLE, newState.audible);
|
||||
|
||||
let soundStateChanged = false;
|
||||
|
||||
if (options.forceApply ||
|
||||
'mutedInfo' in newState) {
|
||||
soundStateChanged = true;
|
||||
const muted = newState.mutedInfo?.muted;
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_MUTED, muted, newState.audible);
|
||||
Tab.onMutedStateChanged.dispatch(tab, muted);
|
||||
}
|
||||
|
||||
if (options.forceApply ||
|
||||
soundStateChanged ||
|
||||
'audible' in newState) {
|
||||
soundStateChanged = true;
|
||||
tab.$TST.toggleState(
|
||||
Constants.kTAB_STATE_SOUND_PLAYING,
|
||||
(tab.audible &&
|
||||
!tab.mutedInfo.muted)
|
||||
);
|
||||
}
|
||||
|
||||
if (soundStateChanged) {
|
||||
const parent = tab.$TST.parent;
|
||||
if (parent)
|
||||
parent.$TST.inheritSoundStateFromChildren();
|
||||
}
|
||||
|
||||
let sharingStateChanged = false;
|
||||
if (options.forceApply ||
|
||||
'sharingState' in newState) {
|
||||
sharingStateChanged = true;
|
||||
const sharingCamera = !!newState.sharingState?.camera;
|
||||
const sharingMicrophone = !!newState.sharingState?.microphone;
|
||||
const sharingScreen = !!newState.sharingState?.screen;
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_CAMERA, sharingCamera);
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_MICROPHONE, sharingMicrophone);
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_SCREEN, sharingScreen);
|
||||
Tab.onSharingStateChanged.dispatch(tab, {
|
||||
camera: sharingCamera,
|
||||
microphone: sharingMicrophone,
|
||||
screen: sharingScreen,
|
||||
});
|
||||
const parent = tab.$TST.parent;
|
||||
if (parent)
|
||||
parent.$TST.inheritSharingStateFromChildren();
|
||||
}
|
||||
|
||||
if (options.forceApply ||
|
||||
'cookieStoreId' in newState) {
|
||||
for (const state of tab.$TST.states) {
|
||||
if (String(state).startsWith('contextual-identity-'))
|
||||
tab.$TST.removeState(state);
|
||||
}
|
||||
if (newState.cookieStoreId) {
|
||||
const state = `contextual-identity-${newState.cookieStoreId}`;
|
||||
tab.$TST.addState(state);
|
||||
const identity = ContextualIdentities.get(newState.cookieStoreId);
|
||||
if (identity)
|
||||
tab.$TST.setAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME, identity.name);
|
||||
else
|
||||
tab.$TST.removeAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME);
|
||||
}
|
||||
else {
|
||||
tab.$TST.removeAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.forceApply ||
|
||||
'incognito' in newState)
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_PRIVATE_BROWSING, newState.incognito);
|
||||
|
||||
if (options.forceApply ||
|
||||
'hidden' in newState) {
|
||||
if (newState.hidden) {
|
||||
if (!tab.$TST.states.has(Constants.kTAB_STATE_HIDDEN)) {
|
||||
tab.$TST.addState(Constants.kTAB_STATE_HIDDEN);
|
||||
Tab.onHidden.dispatch(tab);
|
||||
update.hidden = true;
|
||||
}
|
||||
}
|
||||
else if (tab.$TST.states.has(Constants.kTAB_STATE_HIDDEN)) {
|
||||
tab.$TST.removeState(Constants.kTAB_STATE_HIDDEN);
|
||||
Tab.onShown.dispatch(tab);
|
||||
update.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.forceApply ||
|
||||
'highlighted' in newState)
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_HIGHLIGHTED, newState.highlighted);
|
||||
|
||||
if (options.forceApply ||
|
||||
'attention' in newState)
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_ATTENTION, newState.attention);
|
||||
|
||||
if (options.forceApply ||
|
||||
'discarded' in newState) {
|
||||
wait(0).then(() => {
|
||||
// Don't set this class immediately, because we need to know
|
||||
// the newly active tab *was* discarded on onTabClosed handler.
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_DISCARDED, newState.discarded);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.forceApply ||
|
||||
'groupId' in newState) {
|
||||
tab.$TST.onNativeGroupModified(oldState.groupId);
|
||||
update.attributes.added[Constants.kGROUP_ID] = newState.groupId;
|
||||
}
|
||||
|
||||
update.soundStateChanged = update.soundStateChanged || soundStateChanged;
|
||||
update.sharingStateChanged = update.sharingStateChanged || sharingStateChanged;
|
||||
update.attributes.updated = {
|
||||
...update.attributes.updated,
|
||||
...(newState?.$TST?.sanitized || newState),
|
||||
};
|
||||
flushBufferedUpdates();
|
||||
|
||||
tab.$TST.invalidateCache();
|
||||
}
|
||||
|
||||
export async function updateTabsHighlighted(highlightInfo) {
|
||||
if (Tab.needToWaitTracked(highlightInfo.windowId))
|
||||
await Tab.waitUntilTrackedAll(highlightInfo.windowId);
|
||||
const win = TabsStore.windows.get(highlightInfo.windowId);
|
||||
if (!win)
|
||||
return;
|
||||
|
||||
//const startAt = Date.now();
|
||||
|
||||
const tabIds = highlightInfo.tabIds; // new Set(highlightInfo.tabIds);
|
||||
const toBeUnhighlightedTabs = Tab.getHighlightedTabs(highlightInfo.windowId, {
|
||||
ordered: false,
|
||||
'!id': tabIds
|
||||
});
|
||||
const alreadyHighlightedTabs = TabsStore.highlightedTabsInWindow.get(highlightInfo.windowId);
|
||||
const toBeHighlightedTabs = mapAndFilter(tabIds, id => {
|
||||
const tab = win.tabs.get(id);
|
||||
return tab && !alreadyHighlightedTabs.has(tab.id) && tab || undefined;
|
||||
});
|
||||
|
||||
//console.log(`updateTabsHighlighted: ${Date.now() - startAt}ms`, { toBeHighlightedTabs, toBeUnhighlightedTabs});
|
||||
|
||||
const inheritToCollapsedDescendants = !!highlightInfo.inheritToCollapsedDescendants;
|
||||
//log('updateTabsHighlighted ', { toBeHighlightedTabs, toBeUnhighlightedTabs});
|
||||
for (const tab of toBeUnhighlightedTabs) {
|
||||
TabsStore.removeHighlightedTab(tab);
|
||||
updateTabHighlighted(tab, false, { inheritToCollapsedDescendants });
|
||||
}
|
||||
for (const tab of toBeHighlightedTabs) {
|
||||
TabsStore.addHighlightedTab(tab);
|
||||
updateTabHighlighted(tab, true, { inheritToCollapsedDescendants });
|
||||
}
|
||||
}
|
||||
async function updateTabHighlighted(tab, highlighted, { inheritToCollapsedDescendants } = {}) {
|
||||
log(`highlighted status of ${dumpTab(tab)}: `, { old: tab.highlighted, new: highlighted });
|
||||
//if (tab.highlighted == highlighted)
|
||||
// return false;
|
||||
tab.$TST.toggleState(Constants.kTAB_STATE_HIGHLIGHTED, highlighted);
|
||||
tab.highlighted = highlighted;
|
||||
const win = TabsStore.windows.get(tab.windowId);
|
||||
const inheritHighlighted = !win.tabsToBeHighlightedAlone.has(tab.id);
|
||||
if (!inheritHighlighted)
|
||||
win.tabsToBeHighlightedAlone.delete(tab.id);
|
||||
updateTab(tab, { highlighted });
|
||||
Tab.onUpdated.dispatch(tab, { highlighted }, {
|
||||
inheritHighlighted: inheritToCollapsedDescendants && inheritHighlighted,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export async function completeLoadingTabs(windowId) {
|
||||
const completedTabs = new Set((await browser.tabs.query({ windowId, status: 'complete' })).map(tab => tab.id));
|
||||
for (const tab of Tab.getLoadingTabs(windowId, { ordered: false, iterator: true })) {
|
||||
if (completedTabs.has(tab.id))
|
||||
updateTab(tab, { status: 'complete' });
|
||||
}
|
||||
}
|
||||
417
waterfox/browser/components/sidebar/common/tree-behavior.js
Normal file
417
waterfox/browser/components/sidebar/common/tree-behavior.js
Normal file
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs
|
||||
} from './common.js';
|
||||
import * as Constants from './constants.js';
|
||||
import * as SidebarConnection from './sidebar-connection.js';
|
||||
|
||||
import { TreeItem } from './TreeItem.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/tree-behavior', ...args);
|
||||
}
|
||||
|
||||
export function getParentTabOperationBehavior(tab, { context, byInternalOperation, preventEntireTreeBehavior, parent, windowId } = {}) {
|
||||
const sidebarVisible = SidebarConnection.isInitialized() ? ((windowId || tab) && SidebarConnection.isOpen(windowId || tab.windowId)) : true;
|
||||
log('getParentTabOperationBehavior ', tab, { byInternalOperation, preventEntireTreeBehavior, parent, sidebarVisible /*, stack: configs.debug && new Error().stack */ });
|
||||
|
||||
// strategy: https://github.com/piroor/treestyletab/issues/2860#issuecomment-820622273
|
||||
let behavior;
|
||||
switch (configs.parentTabOperationBehaviorMode) {
|
||||
case Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT:
|
||||
log(' => kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CONSISTENT');
|
||||
if (context == Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE) {
|
||||
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE;
|
||||
}
|
||||
else {
|
||||
behavior = tab.$TST.subtreeCollapsed ?
|
||||
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
|
||||
configs.closeParentBehavior_insideSidebar_expanded;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
case Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL:
|
||||
log(' => kPARENT_TAB_OPERATION_BEHAVIOR_MODE_PARALLEL');
|
||||
if (context == Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE) {
|
||||
behavior = byInternalOperation ?
|
||||
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
|
||||
configs.moveParentBehavior_outsideSidebar_expanded;
|
||||
}
|
||||
else {
|
||||
behavior = byInternalOperation ?
|
||||
(tab.$TST.subtreeCollapsed ?
|
||||
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
|
||||
configs.closeParentBehavior_insideSidebar_expanded) :
|
||||
configs.closeParentBehavior_outsideSidebar_expanded;
|
||||
}
|
||||
break;
|
||||
case Constants.kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM: // kPARENT_TAB_BEHAVIOR_ONLY_ON_SIDEBAR
|
||||
log(' => kPARENT_TAB_OPERATION_BEHAVIOR_MODE_CUSTOM');
|
||||
if (context == Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE) {
|
||||
behavior = byInternalOperation ?
|
||||
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
|
||||
sidebarVisible ?
|
||||
(tab.$TST.subtreeCollapsed ?
|
||||
configs.moveParentBehavior_outsideSidebar_collapsed :
|
||||
configs.moveParentBehavior_outsideSidebar_expanded) :
|
||||
(tab.$TST.subtreeCollapsed ?
|
||||
configs.moveParentBehavior_noSidebar_collapsed :
|
||||
configs.moveParentBehavior_noSidebar_expanded);
|
||||
}
|
||||
else {
|
||||
behavior = byInternalOperation ?
|
||||
(tab.$TST.subtreeCollapsed ?
|
||||
Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE :
|
||||
configs.closeParentBehavior_insideSidebar_expanded) :
|
||||
sidebarVisible ?
|
||||
(tab.$TST.subtreeCollapsed ?
|
||||
configs.closeParentBehavior_outsideSidebar_collapsed :
|
||||
configs.closeParentBehavior_outsideSidebar_expanded) :
|
||||
(tab.$TST.subtreeCollapsed ?
|
||||
configs.closeParentBehavior_noSidebar_collapsed :
|
||||
configs.closeParentBehavior_noSidebar_expanded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
const parentTab = parent || tab.$TST.parent;
|
||||
|
||||
log(' => behavior: ', behavior);
|
||||
|
||||
const replacedParentCount = tab?.$TST?.replacedParentGroupTabCount;
|
||||
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB &&
|
||||
configs.closeParentBehavior_replaceWithGroup_thresholdToPrevent >= 0 &&
|
||||
replacedParentCount &&
|
||||
replacedParentCount >= configs.closeParentBehavior_replaceWithGroup_thresholdToPrevent) {
|
||||
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY;
|
||||
log(' => the group tab is already replaced parent, fallback to kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY');
|
||||
}
|
||||
|
||||
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
|
||||
preventEntireTreeBehavior) {
|
||||
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY;
|
||||
log(' => preventEntireTreeBehavior behavior, fallback to kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY');
|
||||
}
|
||||
|
||||
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_INTELLIGENTLY) {
|
||||
behavior = parentTab ? Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN : Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
|
||||
log(' => intelligent behavior: ', behavior);
|
||||
}
|
||||
|
||||
// Promote all children to upper level, if this is the last child of the parent.
|
||||
// This is similar to "taking by representation".
|
||||
if (behavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD &&
|
||||
parentTab &&
|
||||
parentTab.$TST.childIds.length == 1 &&
|
||||
configs.promoteAllChildrenWhenClosedParentIsLastChild) {
|
||||
behavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN;
|
||||
log(' => blast child ehavior: ', behavior);
|
||||
}
|
||||
|
||||
return behavior;
|
||||
}
|
||||
|
||||
export function getClosingTabsFromParent(tab, removeInfo = {}) {
|
||||
log('getClosingTabsFromParent: ', tab, removeInfo);
|
||||
if (tab?.type == TreeItem.TYPE_GROUP) {
|
||||
return tab.$TST.members;
|
||||
}
|
||||
const closeParentBehavior = getParentTabOperationBehavior(tab, {
|
||||
...removeInfo,
|
||||
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
|
||||
});
|
||||
log('getClosingTabsFromParent: closeParentBehavior ', closeParentBehavior);
|
||||
if (closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
|
||||
return [tab];
|
||||
return [tab].concat(tab.$TST.descendants);
|
||||
}
|
||||
|
||||
export function calculateReferenceItemsFromInsertionPosition(
|
||||
item,
|
||||
{ context, insertBefore, insertAfter } = {}
|
||||
) {
|
||||
let firstItem = (Array.isArray(item) ? item[0] : item) || item;
|
||||
let lastItem = (Array.isArray(item) ? item[item.length - 1] : item) || item;
|
||||
firstItem = firstItem?.$TST?.nativeTabGroup || firstItem;
|
||||
lastItem = lastItem?.$TST?.nativeTabGroup?.collapsed && lastItem?.$TST?.nativeTabGroup || lastItem;
|
||||
log('calculateReferenceItemsFromInsertionPosition ', {
|
||||
firstItem: firstItem?.id,
|
||||
lastItem: lastItem?.id,
|
||||
insertBefore: insertBefore?.id,
|
||||
insertAfter : insertAfter?.id
|
||||
});
|
||||
if (insertBefore) {
|
||||
/* strategy for moved case
|
||||
+------------------ CASE 1 ---------------------------
|
||||
| <= detach from parent, and move
|
||||
|[TARGET ]
|
||||
+------------------ CASE 2 ---------------------------
|
||||
| [ ]
|
||||
| <= attach to the parent of the target, and move
|
||||
|[TARGET ]
|
||||
+------------------ CASE 3 ---------------------------
|
||||
|[ ]
|
||||
| <= attach to the parent of the target, and move
|
||||
|[TARGET ]
|
||||
+------------------ CASE 4 ---------------------------
|
||||
|[ ]
|
||||
| <= attach to the parent of the target (previous item), and move
|
||||
| [TARGET]
|
||||
+-----------------------------------------------------
|
||||
*/
|
||||
/* strategy for shown case
|
||||
+------------------ CASE 5 ---------------------------
|
||||
| <= detach from parent, and move
|
||||
|[TARGET ]
|
||||
+------------------ CASE 6 ---------------------------
|
||||
| [ ]
|
||||
| <= if the inserted item has a parent and it is not the parent of the target, attach to the parent of the target. Otherwise keep inserted as a root.
|
||||
|[TARGET ]
|
||||
+------------------ CASE 7 ---------------------------
|
||||
|[ ]
|
||||
| <= attach to the parent of the target, and move
|
||||
|[TARGET ]
|
||||
+------------------ CASE 8 ---------------------------
|
||||
|[ ]
|
||||
| <= attach to the parent of the target (previous item), and move
|
||||
| [TARGET]
|
||||
+-----------------------------------------------------
|
||||
*/
|
||||
if (insertBefore.type == TreeItem.TYPE_GROUP) {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, special case for a group item');
|
||||
return {
|
||||
insertBefore,
|
||||
};
|
||||
}
|
||||
let prevItem = insertBefore &&
|
||||
(configs.fixupTreeOnTabVisibilityChanged ?
|
||||
insertBefore.$TST.nearestVisiblePrecedingTab :
|
||||
insertBefore.$TST.unsafeNearestExpandedPrecedingTab);
|
||||
if (prevItem == lastItem) // failsafe
|
||||
prevItem = !firstItem ? null :
|
||||
configs.fixupTreeOnTabVisibilityChanged ?
|
||||
firstItem?.$TST.nearestVisiblePrecedingTab :
|
||||
firstItem?.$TST.unsafeNearestExpandedPrecedingTab;
|
||||
if (!prevItem) {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 1/5');
|
||||
// allow to move pinned item to beside of another pinned item
|
||||
if (!firstItem ||
|
||||
!!firstItem.pinned == !!insertBefore?.pinned) {
|
||||
return {
|
||||
insertBefore
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
else {
|
||||
const prevLevel = Number(prevItem?.$TST?.getAttribute(Constants.kLEVEL) || 0);
|
||||
const targetLevel = Number(insertBefore?.$TST?.getAttribute(Constants.kLEVEL) || 0);
|
||||
let parent = null;
|
||||
if (!firstItem || !firstItem.pinned) {
|
||||
if (prevLevel < targetLevel) {
|
||||
if (context == Constants.kINSERTION_CONTEXT_MOVED) {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 4, prevItem = ', prevItem);
|
||||
parent = prevItem;
|
||||
}
|
||||
else {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 8, prevItem = ', prevItem);
|
||||
parent = (firstItem?.$TST?.parent != prevItem) ? prevItem : null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const possibleParent = insertBefore?.$TST?.parent;
|
||||
if (context == Constants.kINSERTION_CONTEXT_MOVED || prevLevel == targetLevel) {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 2/3/7');
|
||||
parent = possibleParent;
|
||||
}
|
||||
else {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertBefore, CASE 6');
|
||||
parent = firstItem?.$TST?.parent != possibleParent && possibleParent || firstItem?.$TST?.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
parent,
|
||||
insertAfter: prevItem,
|
||||
insertBefore
|
||||
};
|
||||
log(' => ', result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (insertAfter) {
|
||||
/* strategy for moved case
|
||||
+------------------ CASE 1 ---------------------------
|
||||
|[TARGET ]
|
||||
| <= if the target has a parent, attach to it and and move
|
||||
+------------------ CASE 2 ---------------------------
|
||||
| [TARGET]
|
||||
| <= attach to the parent of the target, and move
|
||||
|[ ]
|
||||
+------------------ CASE 3 ---------------------------
|
||||
|[TARGET ]
|
||||
| <= attach to the parent of the target, and move
|
||||
|[ ]
|
||||
+------------------ CASE 4 ---------------------------
|
||||
|[TARGET ]
|
||||
| <= attach to the target, and move
|
||||
| [ ]
|
||||
+-----------------------------------------------------
|
||||
*/
|
||||
/* strategy for shown case
|
||||
+------------------ CASE 5 ---------------------------
|
||||
|[TARGET ]
|
||||
| <= if the inserted item has a parent, detach. Otherwise keep inserted as a root.
|
||||
+------------------ CASE 6 ---------------------------
|
||||
| [TARGET]
|
||||
| <= if the inserted item has a parent and it is not the parent of the next item, attach to the parent of the target. Otherwise attach to the parent of the next item.
|
||||
|[ ]
|
||||
+------------------ CASE 7 ---------------------------
|
||||
|[TARGET ]
|
||||
| <= attach to the parent of the target, and move
|
||||
|[ ]
|
||||
+------------------ CASE 8 ---------------------------
|
||||
|[TARGET ]
|
||||
| <= attach to the target, and move
|
||||
| [ ]
|
||||
+-----------------------------------------------------
|
||||
*/
|
||||
// We need to refer unsafeNearestExpandedFollowingTab instead of a visible item, to avoid
|
||||
// placing the item after hidden items (it is too far from the target).
|
||||
let unsafeNextItem = insertAfter?.$TST?.unsafeNearestExpandedFollowingTab;
|
||||
if (firstItem && unsafeNextItem == firstItem) // failsafe
|
||||
unsafeNextItem = lastItem?.$TST?.unsafeNearestExpandedFollowingTab;
|
||||
let nextItem = insertAfter &&
|
||||
(configs.fixupTreeOnTabVisibilityChanged ?
|
||||
insertAfter.$TST?.nearestVisibleFollowingTab :
|
||||
unsafeNextItem);
|
||||
if (firstItem && nextItem == firstItem) // failsafe
|
||||
nextItem = configs.fixupTreeOnTabVisibilityChanged ?
|
||||
lastItem?.$TST?.nearestVisibleFollowingTab :
|
||||
unsafeNextItem;
|
||||
if (!nextItem) {
|
||||
let result;
|
||||
if (context == Constants.kINSERTION_CONTEXT_MOVED) {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 1');
|
||||
result = {
|
||||
parent: insertAfter?.$TST?.parent,
|
||||
insertBefore: unsafeNextItem,
|
||||
insertAfter
|
||||
};
|
||||
}
|
||||
else {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 5');
|
||||
result = {
|
||||
parent: firstItem?.$TST?.parent && insertAfter?.$TST?.parent,
|
||||
insertBefore: unsafeNextItem,
|
||||
insertAfter
|
||||
};
|
||||
}
|
||||
log(' => ', result);
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
const targetLevel = Number(insertAfter?.$TST?.getAttribute(Constants.kLEVEL) || 0);
|
||||
const nextLevel = Number(nextItem?.$TST?.getAttribute(Constants.kLEVEL) || 0);
|
||||
let parent = null;
|
||||
if (!firstItem || !firstItem.pinned) {
|
||||
if (targetLevel < nextLevel) {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 4/8');
|
||||
parent = insertAfter;
|
||||
}
|
||||
else {
|
||||
const possibleParent = insertAfter?.$TST?.parent;
|
||||
if (context == Constants.kINSERTION_CONTEXT_MOVED || targetLevel == nextLevel) {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 2/3/7');
|
||||
parent = possibleParent;
|
||||
}
|
||||
else {
|
||||
log('calculateReferenceItemsFromInsertionPosition: from insertAfter, CASE 6');
|
||||
parent = firstItem?.$TST?.parent != possibleParent && possibleParent || firstItem?.$TST.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
parent,
|
||||
insertBefore: unsafeNextItem || nextItem,
|
||||
insertAfter
|
||||
};
|
||||
log(' => ', result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
throw new Error('calculateReferenceItemsFromInsertionPosition requires one of insertBefore or insertAfter parameter!');
|
||||
}
|
||||
|
||||
|
||||
export const STRUCTURE_NO_PARENT = -1;
|
||||
export const STRUCTURE_KEEP_PARENT = -2;
|
||||
|
||||
export function getTreeStructureFromTabs(tabs, { full, keepParentOfRootTabs } = {}) {
|
||||
if (!tabs || !tabs.length)
|
||||
return [];
|
||||
|
||||
/* this returns...
|
||||
[A] => STRUCTURE_NO_PARENT (parent is not in this tree)
|
||||
[B] => 0 (parent is 1st item in this tree)
|
||||
[C] => 0 (parent is 1st item in this tree)
|
||||
[D] => 2 (parent is 2nd in this tree)
|
||||
[E] => STRUCTURE_NO_PARENT (parent is not in this tree, and this creates another tree)
|
||||
[F] => 0 (parent is 1st item in this another tree)
|
||||
*/
|
||||
const tabIds = tabs.map(tab => tab.id);
|
||||
return cleanUpTreeStructureArray(
|
||||
tabs.map((tab, index) => {
|
||||
const parentId = tab.$TST.parentId;
|
||||
const indexInGivenTabs = parent ? tabIds.indexOf(parentId) : STRUCTURE_NO_PARENT ;
|
||||
return indexInGivenTabs >= index ? STRUCTURE_NO_PARENT : indexInGivenTabs ;
|
||||
}),
|
||||
STRUCTURE_NO_PARENT
|
||||
).map((parentIndex, index) => {
|
||||
if (parentIndex == STRUCTURE_NO_PARENT &&
|
||||
keepParentOfRootTabs)
|
||||
parentIndex = STRUCTURE_KEEP_PARENT;
|
||||
const tab = tabs[index];
|
||||
const item = {
|
||||
id: tab.$TST.uniqueId.id,
|
||||
parent: parentIndex,
|
||||
collapsed: tab.$TST.subtreeCollapsed
|
||||
};
|
||||
if (full) {
|
||||
item.title = tab.title;
|
||||
item.url = tab.url;
|
||||
item.pinned = tab.pinned;
|
||||
item.originalId = tab.id;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
function cleanUpTreeStructureArray(treeStructure, defaultParent) {
|
||||
let offset = 0;
|
||||
treeStructure = treeStructure
|
||||
.map((position, index) => {
|
||||
return (position == index) ? STRUCTURE_NO_PARENT : position ;
|
||||
})
|
||||
.map((position, index) => {
|
||||
if (position == STRUCTURE_NO_PARENT) {
|
||||
offset = index;
|
||||
return position;
|
||||
}
|
||||
return position - offset;
|
||||
});
|
||||
|
||||
/* The final step, this validates all of values.
|
||||
Smaller than STRUCTURE_NO_PARENT is invalid, so it becomes to STRUCTURE_NO_PARENT. */
|
||||
treeStructure = treeStructure.map(index => {
|
||||
return index < STRUCTURE_NO_PARENT ? defaultParent : index ;
|
||||
});
|
||||
return treeStructure;
|
||||
}
|
||||
1502
waterfox/browser/components/sidebar/common/tst-api.js
Normal file
1502
waterfox/browser/components/sidebar/common/tst-api.js
Normal file
File diff suppressed because it is too large
Load Diff
181
waterfox/browser/components/sidebar/common/unique-id.js
Normal file
181
waterfox/browser/components/sidebar/common/unique-id.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs,
|
||||
wait,
|
||||
} from './common.js';
|
||||
import * as ApiTabs from './api-tabs.js';
|
||||
import * as Constants from './constants.js';
|
||||
import * as TabsStore from './tabs-store.js';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function log(...args) {
|
||||
internalLogger('common/unique-id', ...args);
|
||||
}
|
||||
|
||||
//===================================================================
|
||||
// Unique Tab ID
|
||||
//===================================================================
|
||||
|
||||
// for generated IDs
|
||||
const kID_ADJECTIVES = `
|
||||
Agile
|
||||
Breezy
|
||||
Cheerful
|
||||
Dapper
|
||||
Edgy
|
||||
Feisty
|
||||
Gutsy
|
||||
Hoary
|
||||
Intrepid
|
||||
Jaunty
|
||||
Karmic
|
||||
Lucid
|
||||
Marveric
|
||||
Natty
|
||||
Oneiric
|
||||
Precise
|
||||
Quantal
|
||||
Raring
|
||||
Saucy
|
||||
Trusty
|
||||
Utopic
|
||||
Vivid
|
||||
Warty
|
||||
Xenial
|
||||
Yakkety
|
||||
Zesty
|
||||
`.toLowerCase().trim().split(/\s+/);
|
||||
|
||||
const kID_NOUNS = `
|
||||
Alpaca
|
||||
Badger
|
||||
Cat
|
||||
Drake
|
||||
Eft
|
||||
Fawn
|
||||
Gibbon
|
||||
Heron
|
||||
Ibis
|
||||
Jackalope
|
||||
Koala
|
||||
Lynx
|
||||
Meerkat
|
||||
Narwhal
|
||||
Ocelot
|
||||
Pangolin
|
||||
Quetzal
|
||||
Ringtail
|
||||
Salamander
|
||||
Tahr
|
||||
Unicorn
|
||||
Vervet
|
||||
Werwolf
|
||||
Xerus
|
||||
Yak
|
||||
Zapus
|
||||
`.toLowerCase().trim().split(/\s+/);
|
||||
|
||||
let mReadyToDetectDuplicatedTab = false;
|
||||
|
||||
export function readyToDetectDuplicatedTab() {
|
||||
mReadyToDetectDuplicatedTab = true;
|
||||
}
|
||||
|
||||
export async function request(tabOrId, options = {}) {
|
||||
if (typeof options != 'object')
|
||||
options = {};
|
||||
|
||||
let tab = tabOrId;
|
||||
if (typeof tabOrId == 'number')
|
||||
tab = TabsStore.tabs.get(tabOrId);
|
||||
|
||||
if (TabsStore.getCurrentWindowId()) {
|
||||
return browser.runtime.sendMessage({
|
||||
type: Constants.kCOMMAND_REQUEST_UNIQUE_ID,
|
||||
tabId: tab.id
|
||||
}).catch(ApiTabs.createErrorHandler());
|
||||
}
|
||||
|
||||
let originalId = null;
|
||||
let originalTabId = null;
|
||||
let duplicated = false;
|
||||
if (!options.forceNew) {
|
||||
// https://github.com/piroor/treestyletab/issues/2845
|
||||
// This delay may break initial restoration of tabs, so we should
|
||||
// ignore it until all restoration processes are finished.
|
||||
if (mReadyToDetectDuplicatedTab &&
|
||||
configs.delayForDuplicatedTabDetection > 0)
|
||||
await wait(configs.delayForDuplicatedTabDetection);
|
||||
|
||||
let oldId = await browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_ID).catch(ApiTabs.createErrorHandler());
|
||||
if (oldId && !oldId.tabId) // ignore broken information!
|
||||
oldId = null;
|
||||
|
||||
if (oldId) {
|
||||
// If the tab detected from stored tabId is different, it is duplicated tab.
|
||||
try {
|
||||
const tabWithOldId = TabsStore.tabs.get(oldId.tabId);
|
||||
if (!tabWithOldId)
|
||||
throw new Error(`Invalid tab ID: ${oldId.tabId}`);
|
||||
originalId = (tabWithOldId.$TST.uniqueId || await tabWithOldId.$TST.promisedUniqueId).id;
|
||||
duplicated = tab && tabWithOldId.id != tab.id && originalId == oldId.id;
|
||||
if (duplicated)
|
||||
originalTabId = oldId.tabId;
|
||||
else
|
||||
throw new Error(`Invalid tab ID: ${oldId.tabId}`);
|
||||
}
|
||||
catch(e) {
|
||||
ApiTabs.handleMissingTabError(e);
|
||||
// It fails if the tab doesn't exist.
|
||||
// There is no live tab for the tabId, thus
|
||||
// this seems to be a tab restored from session.
|
||||
// We need to update the related tab id.
|
||||
browser.sessions.setTabValue(tab.id, Constants.kPERSISTENT_ID, {
|
||||
id: oldId.id,
|
||||
tabId: tab.id
|
||||
}).catch(ApiTabs.createErrorSuppressor());
|
||||
return {
|
||||
id: oldId.id,
|
||||
originalId: null,
|
||||
originalTabId: oldId.tabId,
|
||||
restored: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const id = `tab-${generate()}`;
|
||||
// tabId is for detecttion of duplicated tabs
|
||||
await browser.sessions.setTabValue(tab.id, Constants.kPERSISTENT_ID, { id, tabId: tab.id }).catch(ApiTabs.createErrorSuppressor());
|
||||
return { id, originalId, originalTabId, duplicated };
|
||||
}
|
||||
|
||||
function generate() {
|
||||
const adjective = kID_ADJECTIVES[Math.floor(Math.random() * kID_ADJECTIVES.length)];
|
||||
const noun = kID_NOUNS[Math.floor(Math.random() * kID_NOUNS.length)];
|
||||
const randomValue = Math.floor(Math.random() * 1000);
|
||||
return `${adjective}-${noun}-${Date.now()}-${randomValue}`;
|
||||
}
|
||||
|
||||
export async function getFromTabs(tabs) {
|
||||
return Promise.all(tabs.map(tab =>
|
||||
browser.sessions.getTabValue(tab.id, Constants.kPERSISTENT_ID).catch(ApiTabs.createErrorHandler())
|
||||
));
|
||||
}
|
||||
|
||||
export async function ensureWindowId(windowId) {
|
||||
const storedUniqueId = await browser.sessions.getWindowValue(windowId, 'uniqueId').catch(_ => null);
|
||||
if (storedUniqueId)
|
||||
return storedUniqueId;
|
||||
|
||||
const uniqueId = `window-${generate()}`;
|
||||
await browser.sessions.setWindowValue(windowId, 'uniqueId', uniqueId).catch(_ => null);
|
||||
return uniqueId;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
# 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';
|
||||
|
||||
import {
|
||||
log as internalLogger,
|
||||
configs
|
||||
} from './common.js';
|
||||
import * as Constants from './constants.js';
|
||||
import * as SidebarConnection from './sidebar-connection.js';
|
||||
import * as TabsStore from './tabs-store.js';
|
||||
|
||||
function log(...args) {
|
||||
internalLogger('common/user-operation-blocker', ...args);
|
||||
}
|
||||
|
||||
let mBlockingCount = 0;
|
||||
let mBlockingThrobberCount = 0;
|
||||
const mProgressbar = document.querySelector('#blocking-screen progress');
|
||||
|
||||
export function block({ throbber, shade } = {}) {
|
||||
mBlockingCount++;
|
||||
document.documentElement.classList.add(Constants.kTABBAR_STATE_BLOCKING);
|
||||
log('block ', mBlockingCount, () => new Error().stack);
|
||||
if (throbber) {
|
||||
mBlockingThrobberCount++;
|
||||
mProgressbar.delayedShow = setTimeout(() => {
|
||||
mProgressbar.delayedShow = null;
|
||||
mProgressbar.classList.add('shown');
|
||||
}, configs.delayToShowProgressForBlockedUserOperation);
|
||||
document.documentElement.classList.add(Constants.kTABBAR_STATE_BLOCKING_WITH_THROBBER);
|
||||
}
|
||||
else if (shade) {
|
||||
document.documentElement.classList.add(Constants.kTABBAR_STATE_BLOCKING_WITH_SHADE);
|
||||
}
|
||||
}
|
||||
|
||||
export function setProgress(percentage, windowId = null) {
|
||||
percentage = Math.max(0, Math.min(100, percentage));
|
||||
if (mProgressbar)
|
||||
mProgressbar.value = percentage;
|
||||
if (windowId && !TabsStore.getCurrentWindowId())
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_PROGRESS_USER_OPERATIONS,
|
||||
windowId,
|
||||
percentage
|
||||
});
|
||||
}
|
||||
|
||||
export function blockIn(windowId, { throbber, shade } = {}) {
|
||||
const targetWindow = TabsStore.getCurrentWindowId();
|
||||
if (targetWindow && targetWindow != windowId)
|
||||
return;
|
||||
|
||||
log(`blockIn(${windowId}) `, () => new Error().stack);
|
||||
if (!targetWindow) {
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_BLOCK_USER_OPERATIONS,
|
||||
windowId,
|
||||
throbber: !!throbber,
|
||||
shade: !!shade
|
||||
});
|
||||
return;
|
||||
}
|
||||
block({ throbber, shade });
|
||||
}
|
||||
|
||||
export function unblock() {
|
||||
mBlockingThrobberCount--;
|
||||
log('unblock ', mBlockingCount, () => new Error().stack);
|
||||
if (mBlockingThrobberCount < 0)
|
||||
mBlockingThrobberCount = 0;
|
||||
if (mBlockingThrobberCount == 0) {
|
||||
setProgress(0);
|
||||
mProgressbar.classList.remove('shown');
|
||||
if (mProgressbar.delayedShow)
|
||||
clearTimeout(mProgressbar.delayedShow);
|
||||
document.documentElement.classList.remove(Constants.kTABBAR_STATE_BLOCKING_WITH_THROBBER);
|
||||
document.documentElement.classList.remove(Constants.kTABBAR_STATE_BLOCKING_WITH_SHADE);
|
||||
}
|
||||
|
||||
mBlockingCount--;
|
||||
if (mBlockingCount < 0)
|
||||
mBlockingCount = 0;
|
||||
if (mBlockingCount == 0)
|
||||
document.documentElement.classList.remove(Constants.kTABBAR_STATE_BLOCKING);
|
||||
}
|
||||
|
||||
export function unblockIn(windowId, { throbber, shade } = {}) {
|
||||
const targetWindow = TabsStore.getCurrentWindowId();
|
||||
if (targetWindow && targetWindow != windowId)
|
||||
return;
|
||||
|
||||
log(`unblockIn(${windowId}) `, () => new Error().stack);
|
||||
if (!targetWindow) {
|
||||
SidebarConnection.sendMessage({
|
||||
type: Constants.kCOMMAND_UNBLOCK_USER_OPERATIONS,
|
||||
windowId,
|
||||
throbber: !!throbber,
|
||||
shade: !!shade
|
||||
});
|
||||
return;
|
||||
}
|
||||
unblock({ throbber, shade });
|
||||
}
|
||||
|
||||
165
waterfox/browser/components/sidebar/experiments/prefs.js
Normal file
165
waterfox/browser/components/sidebar/experiments/prefs.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/* 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/. */
|
||||
/* Original: https://github.com/mozilla-extensions/webcompat-addon/blob/main/src/experiment-apis/aboutConfigPrefs.js */
|
||||
'use strict';
|
||||
|
||||
const AboutPreferencesWatcher = {
|
||||
BASE_URL: null, // this need to be replaced with "moz-extension://..../"
|
||||
BASE_PREF: 'browser.sidebar.', // null,
|
||||
|
||||
onPrefChanged(name) {
|
||||
switch (name) {
|
||||
case 'browser.tabs.selectOwnerOnClose':
|
||||
Services.prefs.setBoolPref(`${this.BASE_PREF}simulateSelectOwnerOnClose`, Services.prefs.getBoolPref(name));
|
||||
break;
|
||||
|
||||
case 'browser.tabs.loadInBackground':
|
||||
Services.prefs.setBoolPref(`${this.BASE_PREF}simulateTabsLoadInBackgroundInverted`, Services.prefs.getBoolPref(name));
|
||||
break;
|
||||
|
||||
case 'browser.tabs.warnOnClose':
|
||||
Services.prefs.setBoolPref(`${this.BASE_PREF}warnOnCloseTabs`, Services.prefs.getBoolPref(name));
|
||||
break;
|
||||
|
||||
case 'browser.tabs.searchclipboardfor.middleclick':
|
||||
Services.prefs.setBoolPref(`${this.BASE_PREF}middleClickPasteURLOnNewTabButton`, Services.prefs.getBoolPref(name));
|
||||
break;
|
||||
|
||||
case 'browser.tabs.insertAfterCurrent':
|
||||
case 'browser.tabs.insertRelatedAfterCurrent': {
|
||||
const insertAfterCurrent = Services.prefs.getBoolPref('browser.tabs.insertAfterCurrent');
|
||||
const insertRelatedAfterCurrent = Services.prefs.getBoolPref('browser.tabs.insertRelatedAfterCurrent');
|
||||
const useTree = (
|
||||
Services.prefs.getBoolPref(`${this.BASE_PREF}autoAttach`, false) &&
|
||||
Services.prefs.getBoolPref(`${this.BASE_PREF}syncParentTabAndOpenerTab`, false)
|
||||
);
|
||||
Services.prefs.setStringPref(`${this.BASE_PREF}autoAttachOnOpenedWithOwner`,
|
||||
!useTree ? -1 :
|
||||
insertRelatedAfterCurrent ? 5 :
|
||||
insertAfterCurrent ? 6 :
|
||||
0);
|
||||
Services.prefs.setStringPref(`${this.BASE_PREF}insertNewTabFromPinnedTabAt`,
|
||||
!useTree ? -1 :
|
||||
insertRelatedAfterCurrent ? 3 :
|
||||
insertAfterCurrent ? 0 :
|
||||
1);
|
||||
Services.prefs.setStringPref(`${this.BASE_PREF}insertNewTabFromFirefoxViewAt`,
|
||||
!useTree ? -1 :
|
||||
insertRelatedAfterCurrent ? 3 :
|
||||
insertAfterCurrent ? 0 :
|
||||
1);
|
||||
}; break;
|
||||
}
|
||||
},
|
||||
|
||||
// as an XPCOM component...
|
||||
classDescription: 'Waterfox Chrome Window Watcher for about:preferences',
|
||||
contractID: '@waterfox.net/chrome-window-watche-about-preferences;1',
|
||||
classID: Components.ID('{c8a990cf-b9a3-4b4c-829c-a1dfc5753527}'),
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
'nsIObserver',
|
||||
'nsISupportsWeakReference',
|
||||
]),
|
||||
|
||||
// nsIObserver
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case 'nsPref:changed':
|
||||
this.onPrefChanged(data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
createInstance(iid) {
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
};
|
||||
|
||||
this.prefs = class extends ExtensionAPI {
|
||||
getAPI(context) {
|
||||
const EventManager = ExtensionCommon.EventManager;
|
||||
const extensionIDBase = context.extension.id.split('@')[0];
|
||||
|
||||
AboutPreferencesWatcher.BASE_URL = context.extension.baseURL;
|
||||
|
||||
// Synchronize simulation configs with the browser's preferences
|
||||
for (const [source, dest] of Object.entries({
|
||||
'browser.tabs.selectOwnerOnClose': `${AboutPreferencesWatcher.BASE_PREF}simulateSelectOwnerOnClose`,
|
||||
'browser.tabs.loadInBackground': `${AboutPreferencesWatcher.BASE_PREF}simulateTabsLoadInBackgroundInverted`,
|
||||
'browser.tabs.warnOnClose': `${AboutPreferencesWatcher.BASE_PREF}warnOnCloseTabs`,
|
||||
'browser.tabs.searchclipboardfor.middleclick': `${AboutPreferencesWatcher.BASE_PREF}middleClickPasteURLOnNewTabButton`,
|
||||
})) {
|
||||
Services.prefs.setBoolPref(dest, Services.prefs.getBoolPref(source));
|
||||
}
|
||||
Services.prefs.addObserver('browser.tabs.', AboutPreferencesWatcher);
|
||||
AboutPreferencesWatcher.onPrefChanged('browser.tabs.insertAfterCurrent');
|
||||
|
||||
return {
|
||||
prefs: {
|
||||
onChanged: new EventManager({
|
||||
context,
|
||||
name: 'prefs.onChanged',
|
||||
register: (fire) => {
|
||||
const observe = (_subject, _topic, data) => {
|
||||
fire.async(data.replace(AboutPreferencesWatcher.BASE_PREF, '')).catch(() => {}); // ignore Message Manager disconnects
|
||||
};
|
||||
Services.prefs.addObserver(AboutPreferencesWatcher.BASE_PREF, observe);
|
||||
return () => {
|
||||
Services.prefs.removeObserver(AboutPreferencesWatcher.BASE_PREF, observe);
|
||||
};
|
||||
},
|
||||
}).api(),
|
||||
async getBoolValue(name, defaultValue = false) {
|
||||
try {
|
||||
return Services.prefs.getBoolPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, defaultValue);
|
||||
}
|
||||
catch(_error) {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
async setBoolValue(name, value) {
|
||||
Services.prefs.setBoolPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
|
||||
},
|
||||
async setDefaultBoolValue(name, value) {
|
||||
Services.prefs.getDefaultBranch(null).setBoolPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
|
||||
},
|
||||
async getStringValue(name, defaultValue = '') {
|
||||
try {
|
||||
return Services.prefs.getStringPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, defaultValue);
|
||||
}
|
||||
catch(_error) {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
async setStringValue(name, value) {
|
||||
Services.prefs.setStringPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
|
||||
},
|
||||
async setDefaultStringValue(name, value) {
|
||||
Services.prefs.getDefaultBranch(null).setStringPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
|
||||
},
|
||||
async getIntValue(name, defaultValue = 0) {
|
||||
try {
|
||||
return Services.prefs.getIntPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, defaultValue);
|
||||
}
|
||||
catch(_error) {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
async setIntValue(name, value) {
|
||||
Services.prefs.setIntPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
|
||||
},
|
||||
async setDefaultIntValue(name, value) {
|
||||
Services.prefs.getDefaultBranch(null).setIntPref(`${AboutPreferencesWatcher.BASE_PREF}${name}`, value);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onShutdown(isAppShutdown) {
|
||||
if (isAppShutdown)
|
||||
return;
|
||||
|
||||
Services.prefs.removeObserver('browser.tabs.', AboutPreferencesWatcher);
|
||||
}
|
||||
};
|
||||
199
waterfox/browser/components/sidebar/experiments/prefs.json
Normal file
199
waterfox/browser/components/sidebar/experiments/prefs.json
Normal file
@@ -0,0 +1,199 @@
|
||||
[
|
||||
{
|
||||
"namespace": "prefs",
|
||||
"description": "",
|
||||
"functions": [
|
||||
{
|
||||
"name": "getBoolValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "default",
|
||||
"optional": true,
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "boolean",
|
||||
"description": ""
|
||||
},
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "setBoolValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "value",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "setDefaultBoolValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "value",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "getStringValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "default",
|
||||
"optional": true,
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "string",
|
||||
"description": ""
|
||||
},
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "setStringValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "value",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "setDefaultStringValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "value",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "getIntValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "default",
|
||||
"optional": true,
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "integer",
|
||||
"description": ""
|
||||
},
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "setIntValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "value",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"name": "setDefaultIntValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "value",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"async": true
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onChanged",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
41
waterfox/browser/components/sidebar/experiments/syncPrefs.js
Normal file
41
waterfox/browser/components/sidebar/experiments/syncPrefs.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/* 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/. */
|
||||
/* Original: https://github.com/mozilla-extensions/webcompat-addon/blob/main/src/experiment-apis/aboutConfigPrefsChild.js */
|
||||
'use strict';
|
||||
|
||||
this.syncPrefs = class extends ExtensionAPI {
|
||||
getAPI(context) {
|
||||
const extensionIDBase = context.extension.id.split('@')[0];
|
||||
const extensionPrefNameBase = `extensions.${extensionIDBase}.`;
|
||||
|
||||
return {
|
||||
syncPrefs: {
|
||||
getBoolValue(name, defaultValue = false) {
|
||||
try {
|
||||
return Services.prefs.getBoolPref(`${extensionPrefNameBase}${name}`, defaultValue);
|
||||
}
|
||||
catch(_error) {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
getStringValue(name, defaultValue = '') {
|
||||
try {
|
||||
return Services.prefs.getStringPref(`${extensionPrefNameBase}${name}`, defaultValue);
|
||||
}
|
||||
catch(_error) {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
getIntValue(name, defaultValue = 0) {
|
||||
try {
|
||||
return Services.prefs.getIntPref(`${extensionPrefNameBase}${name}`, defaultValue);
|
||||
}
|
||||
catch(_error) {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
[
|
||||
{
|
||||
"namespace": "syncPrefs",
|
||||
"description": "",
|
||||
"functions": [
|
||||
{
|
||||
"name": "getBoolValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "default",
|
||||
"optional": true,
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "boolean",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "getStringValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "default",
|
||||
"optional": true,
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "string",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "getIntValue",
|
||||
"description": "",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "default",
|
||||
"optional": true,
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "integer",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,887 @@
|
||||
/* 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 HTML = 'http://www.w3.org/1999/xhtml';
|
||||
const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
|
||||
|
||||
const TYPE_TREE = 'application/x-ws-tree';
|
||||
const TST_ID = 'treestyletab@piro.sakura.ne.jp';
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
AddonManager: 'resource://gre/modules/AddonManager.sys.mjs',
|
||||
CustomizableUI: 'resource:///modules/CustomizableUI.sys.mjs',
|
||||
ExtensionPermissions: 'resource://gre/modules/ExtensionPermissions.sys.mjs',
|
||||
PageThumbs: 'resource://gre/modules/PageThumbs.sys.mjs',
|
||||
PlacesUtils: 'resource://gre/modules/PlacesUtils.sys.mjs',
|
||||
});
|
||||
|
||||
// Range.createContextualFragment() unexpectedly drops XUL elements.
|
||||
// Moreover, the security mechanism of the browser rejects adoptation of elements
|
||||
// created by DOMParser(). Thus we need to create elements manually...
|
||||
function element(document, NS, localName, attributes, children) {
|
||||
if (Array.isArray(attributes)) {
|
||||
children = attributes;
|
||||
attributes = {};
|
||||
}
|
||||
const element = document.createElementNS(NS, localName);
|
||||
if (attributes) {
|
||||
for (const [name, value] of Object.entries(attributes)) {
|
||||
element.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
if (children) {
|
||||
for (const child of children) {
|
||||
if (typeof child == 'string')
|
||||
element.appendChild(document.createTextNode(child));
|
||||
else
|
||||
element.appendChild(child);
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
const BrowserWindowWatcher = {
|
||||
WATCHING_URLS: [
|
||||
'chrome://browser/content/browser.xhtml',
|
||||
],
|
||||
BASE_URL: null, // this need to be replaced with "moz-extension://..../"
|
||||
BASE_PREF: 'browser.sidebar.', // null,
|
||||
locale: null, // this need to be replaced with a map
|
||||
loadingForbiddenURLs: [],
|
||||
autoplayBlockedListeners: new Set(),
|
||||
autoplayUnblockedListeners: new Set(),
|
||||
visibilityChangedListeners: new Set(),
|
||||
menuCommandListeners: new Set(),
|
||||
sidebarShownListeners: new Set(),
|
||||
sidebarHiddenListeners: new Set(),
|
||||
lastTransferredFiles: new Map(),
|
||||
|
||||
handleWindow(win) {
|
||||
if (!win ||
|
||||
!win.location)
|
||||
return false;
|
||||
|
||||
const document = win.document;
|
||||
if (!document)
|
||||
return false;
|
||||
|
||||
if (win.location.href.startsWith('chrome://browser/content/browser.xhtml')) {
|
||||
const installed = this.installTabsSidebar(win);
|
||||
if (installed) {
|
||||
win.addEventListener('DOMAudioPlaybackBlockStarted', this, { capture: true });
|
||||
win.addEventListener('DOMAudioPlaybackBlockStopped', this, { capture: true });
|
||||
win.addEventListener('visibilitychange', this);
|
||||
win.addEventListener('TreeVerticalTabsShown', this);
|
||||
win.addEventListener('TreeVerticalTabsHidden', this);
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
unhandleWindow(win) {
|
||||
if (!win ||
|
||||
!win.location)
|
||||
return;
|
||||
|
||||
const document = win.document;
|
||||
if (!document)
|
||||
return;
|
||||
|
||||
if (win.location.href.startsWith('chrome://browser/content/browser.xhtml')) {
|
||||
this.uninstallTabsSidebar(win);
|
||||
try {
|
||||
win.removeEventListener('DOMAudioPlaybackBlockStarted', this, { capture: true });
|
||||
win.removeEventListener('DOMAudioPlaybackBlockStopped', this, { capture: true });
|
||||
win.removeEventListener('visibilitychange', this);
|
||||
win.removeEventListener('TreeVerticalTabsShown', this);
|
||||
win.removeEventListener('TreeVerticalTabsHidden', this);
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
installTabsSidebar(win) {
|
||||
const document = win.document;
|
||||
|
||||
const tabsSidebarElement = document.querySelector('#tree-vertical-tabs-box');
|
||||
if (tabsSidebarElement?.getAttribute('initialized') == 'true')
|
||||
return true;
|
||||
|
||||
if (tabsSidebarElement) {
|
||||
tabsSidebarElement.setAttribute('initialized', 'true');
|
||||
tabsSidebarElement.addEventListener('dragover', this, { capture: true });
|
||||
} else {
|
||||
console.error('WaterfoxBridge: #tree-vertical-tabs element not found. Cannot attach event listeners or load panel.');
|
||||
}
|
||||
|
||||
document.addEventListener('command', this);
|
||||
document.addEventListener('customizationchange', this, { capture: true });
|
||||
|
||||
this.updateToggleButton(document);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
getKeyFromFile(file) {
|
||||
if (!file)
|
||||
return '';
|
||||
return `${file.name}?lastModified=${file.lastModified}&size=${file.size}&type=${file.type}`;
|
||||
},
|
||||
getFileURL(file) {
|
||||
if (!file)
|
||||
return '';
|
||||
return this.lastTransferredFiles[this.getKeyFromFile(file)];
|
||||
},
|
||||
|
||||
uninstallTabsSidebar(win) {
|
||||
const document = win.document;
|
||||
|
||||
document.removeEventListener('command', this);
|
||||
document.removeEventListener('customizationchange', this, { capture: true });
|
||||
|
||||
const tabsSidebarElement = document.querySelector('#tree-vertical-tabs-box');
|
||||
if (tabsSidebarElement?.getAttribute('initialized') == 'true') {
|
||||
tabsSidebarElement.removeAttribute('initialized');
|
||||
tabsSidebarElement.removeEventListener('dragover', this, { capture: true });
|
||||
}
|
||||
},
|
||||
|
||||
updateToggleButton(document, button) {
|
||||
button = button || document.querySelector('#toggle-tree-vertical-tabs');
|
||||
if (!button)
|
||||
return;
|
||||
|
||||
button.removeAttribute('disabled');
|
||||
},
|
||||
|
||||
*iterateTargetWindows() {
|
||||
const browserWindows = Services.wm.getEnumerator('navigator:browser');
|
||||
while (browserWindows.hasMoreElements()) {
|
||||
const win = browserWindows.getNext()/*.QueryInterface(Components.interfaces.nsIDOMWindow)*/
|
||||
yield win;
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
openOptions(win, full = false) {
|
||||
const url = full ? `${this.BASE_URL}options/options.html#!` : 'about:preferences#tabsSidebar';
|
||||
|
||||
const windows = Services.wm.getEnumerator('navigator:browser');
|
||||
while (windows.hasMoreElements()) {
|
||||
const win = windows.getNext()/*.QueryInterface(Components.interfaces.nsIDOMWindow)*/;
|
||||
if (!win.gBrowser)
|
||||
continue;
|
||||
for (const tab of win.gBrowser.tabs) {
|
||||
if (tab.linkedBrowser.currentURI.spec != url)
|
||||
continue;
|
||||
win.gBrowser.selectedTab = tab;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
(win || Services.wm.getMostRecentBrowserWindow())
|
||||
.openLinkIn(url, 'tab', {
|
||||
allowThirdPartyFixup: false,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
inBackground: false,
|
||||
});
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
const win = event.target.ownerDocument?.defaultView || event.target.defaultView;
|
||||
switch (event.type) {
|
||||
case 'command':
|
||||
switch (event.target.id) {
|
||||
case 'toggle-tree-vertical-tabs':
|
||||
case 'toggle-tree-vertical-tabs-command':
|
||||
case 'viewmenu-toggle-tree-vertical-tabs':
|
||||
this.updateToggleButton(event.target.ownerDocument);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'customizationchange':
|
||||
this.updateToggleButton(event.target.ownerDocument);
|
||||
break;
|
||||
|
||||
case 'DOMAudioPlaybackBlockStarted': {
|
||||
const gBrowser = event.target.ownerDocument.defaultView.gBrowser;
|
||||
const tab = gBrowser.getTabForBrowser(event.target);
|
||||
for (const listener of this.autoplayBlockedListeners) {
|
||||
listener(tab);
|
||||
}
|
||||
}; break;
|
||||
|
||||
case 'DOMAudioPlaybackBlockStopped': {
|
||||
const gBrowser = event.target.ownerDocument.defaultView.gBrowser;
|
||||
const tab = gBrowser.getTabForBrowser(event.target);
|
||||
for (const listener of this.autoplayUnblockedListeners) {
|
||||
listener(tab);
|
||||
}
|
||||
}; break;
|
||||
|
||||
case 'visibilitychange':
|
||||
for (const listener of this.visibilityChangedListeners) {
|
||||
listener(event.currentTarget);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TreeVerticalTabsShown':
|
||||
for (const listener of this.sidebarShownListeners) {
|
||||
listener(event.target.ownerDocument.defaultView);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TreeVerticalTabsHidden':
|
||||
for (const listener of this.sidebarHiddenListeners) {
|
||||
listener(event.target.ownerDocument.defaultView);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dragover': {
|
||||
const tabsSidebarElement = event.currentTarget;
|
||||
this.lastTransferredFiles.clear();
|
||||
for (const file of event.dataTransfer.files) {
|
||||
const fileInternal = Cc['@mozilla.org/file/local;1']
|
||||
.createInstance(Components.interfaces.nsIFile);
|
||||
fileInternal.initWithPath(file.mozFullPath);
|
||||
const url = Services.io.getProtocolHandler('file')
|
||||
.QueryInterface(Components.interfaces.nsIFileProtocolHandler)
|
||||
.getURLSpecFromActualFile(fileInternal);
|
||||
this.lastTransferredFiles[this.getKeyFromFile(file)] = url;
|
||||
}
|
||||
}; break;
|
||||
}
|
||||
},
|
||||
tryHidePopup(event) {
|
||||
if (event.target.closest)
|
||||
event.target.closest('panel')?.hidePopup();
|
||||
},
|
||||
|
||||
// as an XPCOM component...
|
||||
classDescription: 'Waterfox Chrome Window Watcher for Browser Windows',
|
||||
contractID: '@waterfox.net/chrome-window-watche-browser-windows;1',
|
||||
classID: Components.ID('{8d25e5cc-1d67-4556-819e-e25bd37c79c5}'),
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
'nsIContentPolicy',
|
||||
'nsIObserver',
|
||||
'nsISupportsWeakReference',
|
||||
]),
|
||||
|
||||
// nsIContentPolicy
|
||||
shouldLoad(contentLocation, loadInfo, mimeTypeGuess) {
|
||||
const FORBIDDEN_URL_MATCHER = /^about:blank\?forbidden-url=/;
|
||||
if (FORBIDDEN_URL_MATCHER.test(contentLocation.spec)) {
|
||||
const url = contentLocation.spec.replace(FORBIDDEN_URL_MATCHER, '');
|
||||
const index = this.loadingForbiddenURLs.indexOf(url);
|
||||
if (index > -1) {
|
||||
this.loadingForbiddenURLs.splice(index, 1);
|
||||
const browser = loadInfo.browsingContext.embedderElement;
|
||||
browser.loadURI(Services.io.newURI(url), {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
return Components.interfaces.nsIContentPolicy.REJECT_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.WATCHING_URLS.some(url => contentLocation.spec.startsWith(url))) {
|
||||
const startAt = Date.now();
|
||||
const topWin = loadInfo.browsingContext.topChromeWindow;
|
||||
const timer = topWin.setInterval(() => {
|
||||
if (Date.now() - startAt > 1000) {
|
||||
// timeout
|
||||
topWin.clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
const win = loadInfo.browsingContext.window;
|
||||
if (!win)
|
||||
return;
|
||||
try {
|
||||
if (this.handleWindow(win))
|
||||
topWin.clearInterval(timer);
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
return Components.interfaces.nsIContentPolicy.ACCEPT;
|
||||
},
|
||||
|
||||
shouldProcess(contentLocation, loadInfo, mimeTypeGuess) {
|
||||
return Components.interfaces.nsIContentPolicy.ACCEPT;
|
||||
},
|
||||
|
||||
// nsIObserver
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case 'domwindowopened':
|
||||
subject
|
||||
//.QueryInterface(Components.interfaces.nsIDOMWindow)
|
||||
.addEventListener('DOMContentLoaded', () => {
|
||||
this.handleWindow(subject);
|
||||
}, { once: true });
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
createInstance(iid) {
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
|
||||
|
||||
// AddonManager listener callbacks
|
||||
|
||||
async tryConfirmUsingTST() {
|
||||
const ignorePrefKey = `${this.BASE_PREF}.ignoreConflictionWithTST`;
|
||||
if (Services.prefs.getBoolPref(ignorePrefKey, false))
|
||||
return;
|
||||
|
||||
const nsIPrompt = Components.interfaces.nsIPrompt;
|
||||
const shouldAsk = { value: true };
|
||||
const result = Services.prompt.confirmEx(
|
||||
Services.wm.getMostRecentBrowserWindow(),
|
||||
this.locale.get('tryConfirmUsingTST_title'),
|
||||
this.locale.get('tryConfirmUsingTST_message'),
|
||||
(nsIPrompt.BUTTON_TITLE_IS_STRING * nsIPrompt.BUTTON_POS_0 |
|
||||
nsIPrompt.BUTTON_TITLE_IS_STRING * nsIPrompt.BUTTON_POS_1 |
|
||||
nsIPrompt.BUTTON_TITLE_IS_STRING * nsIPrompt.BUTTON_POS_2),
|
||||
this.locale.get('tryConfirmUsingTST_WS'),
|
||||
this.locale.get('tryConfirmUsingTST_both'),
|
||||
this.locale.get('tryConfirmUsingTST_TST'),
|
||||
this.locale.get('tryConfirmUsingTST_ask'),
|
||||
shouldAsk
|
||||
);
|
||||
|
||||
if (result > -1 &&
|
||||
!shouldAsk.value)
|
||||
Services.prefs.setBoolPref(ignorePrefKey, true);
|
||||
|
||||
switch (result) {
|
||||
case 0: {
|
||||
const addon = await lazy.AddonManager.getAddonByID(TST_ID);
|
||||
addon.disable();
|
||||
}; return;
|
||||
|
||||
case 2:
|
||||
Services.prefs.setBoolPref('browser.sidebar.enabled', false);
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
// install listener callbacks
|
||||
onNewInstall(_install) {},
|
||||
onInstallCancelled(_install) {},
|
||||
onInstallPostponed(_install) {},
|
||||
onInstallFailed(_install) {},
|
||||
onInstallEnded(install) {
|
||||
if (install.addon.id == TST_ID)
|
||||
this.tryConfirmUsingTST();
|
||||
},
|
||||
onDownloadStarted(_install) {},
|
||||
onDownloadCancelled(_install) {},
|
||||
onDownloadEnded(_install) {},
|
||||
onDownloadFailed(_install) {},
|
||||
|
||||
// addon listener callbacks
|
||||
onUninstalled(_addon) {},
|
||||
onEnabled(addon) {
|
||||
if (addon.id == TST_ID)
|
||||
this.tryConfirmUsingTST();
|
||||
},
|
||||
onDisabled(_addon) {},
|
||||
};
|
||||
|
||||
this.waterfoxBridge = class extends ExtensionAPI {
|
||||
getAPI(context) {
|
||||
const EventManager = ExtensionCommon.EventManager;
|
||||
|
||||
return {
|
||||
waterfoxBridge: {
|
||||
async initUI() {
|
||||
BrowserWindowWatcher.EXTENSION_ID = context.extension.id;
|
||||
BrowserWindowWatcher.BASE_URL = context.extension.baseURL;
|
||||
//BrowserWindowWatcher.BASE_PREF = `extensions.${context.extension.id.split('@')[0]}.`;
|
||||
BrowserWindowWatcher.locale = {
|
||||
get(key) {
|
||||
key = key.toLowerCase();
|
||||
if (this.selected.has(key))
|
||||
return this.selected.get(key);
|
||||
return this.default.get(key) || key;
|
||||
},
|
||||
default: context.extension.localeData.messages.get(context.extension.localeData.defaultLocale),
|
||||
selected: context.extension.localeData.messages.get(context.extension.localeData.selectedLocale),
|
||||
};
|
||||
|
||||
//const resourceURI = Services.io.newURI('resources', null, context.extension.rootURI);
|
||||
//const handler = Cc['@mozilla.org/network/protocol;1?name=resource'].getService(Components.interfaces.nsISubstitutingProtocolHandler);
|
||||
//handler.setSubstitution('waterfox-bridge', resourceURI);
|
||||
|
||||
// watch loading of about:preferences in subframes
|
||||
const registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
|
||||
registrar.registerFactory(
|
||||
BrowserWindowWatcher.classID,
|
||||
BrowserWindowWatcher.classDescription,
|
||||
BrowserWindowWatcher.contractID,
|
||||
BrowserWindowWatcher
|
||||
);
|
||||
Services.catMan.addCategoryEntry(
|
||||
'content-policy',
|
||||
BrowserWindowWatcher.contractID,
|
||||
BrowserWindowWatcher.contractID,
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
// handle loading of browser windows
|
||||
Services.ww.registerNotification(BrowserWindowWatcher);
|
||||
|
||||
// handle already opened browser windows
|
||||
const windows = BrowserWindowWatcher.iterateTargetWindows();
|
||||
while (true) {
|
||||
const win = windows.next();
|
||||
if (win.done)
|
||||
break;
|
||||
BrowserWindowWatcher.handleWindow(win.value);
|
||||
}
|
||||
|
||||
// grant special permissions by default
|
||||
if (!Services.prefs.getBoolPref(`${BrowserWindowWatcher.BASE_PREF}permissionsGranted`, false)) {
|
||||
lazy.ExtensionPermissions.add(context.extension.id, {
|
||||
origins: ['<all_urls>'],
|
||||
permissions: ['internal:privateBrowsingAllowed'],
|
||||
}, true);
|
||||
Services.prefs.setBoolPref(`${BrowserWindowWatcher.BASE_PREF}permissionsGranted`, true);
|
||||
}
|
||||
|
||||
// auto detection and warning for TST
|
||||
lazy.AddonManager.addInstallListener(BrowserWindowWatcher);
|
||||
lazy.AddonManager.addAddonListener(BrowserWindowWatcher);
|
||||
const installedTST = await lazy.AddonManager.getAddonByID(TST_ID);
|
||||
if (installedTST?.isActive)
|
||||
BrowserWindowWatcher.tryConfirmUsingTST();
|
||||
},
|
||||
|
||||
async reserveToLoadForbiddenURL(url) {
|
||||
BrowserWindowWatcher.loadingForbiddenURLs.push(url);
|
||||
},
|
||||
|
||||
async getFileURL(file) {
|
||||
return BrowserWindowWatcher.getFileURL(file);
|
||||
},
|
||||
|
||||
async getTabPreview(tabId) {
|
||||
const info = {
|
||||
url: null,
|
||||
found: false,
|
||||
};
|
||||
const tab = context.extension.tabManager.get(tabId);
|
||||
if (!tab)
|
||||
return info;
|
||||
|
||||
const nativeTab = tab.nativeTab;
|
||||
const window = nativeTab.ownerDocument.defaultView;
|
||||
try {
|
||||
const canvas = await window.tabPreviews.get(nativeTab);
|
||||
/*
|
||||
// We can get a URL like "https%3A%2F%2Fwww.example.com.org%2F&revision=0000"
|
||||
// but Firefox does not allow loading of such a special internal URL from
|
||||
// addon's sidebar page.
|
||||
const image = await window.tabPreviews.get(nativeTab);
|
||||
return image.src;
|
||||
*/
|
||||
if (canvas) {
|
||||
info.url = canvas.toDataURL('image/png');
|
||||
info.found = true;
|
||||
return info;
|
||||
}
|
||||
}
|
||||
catch (_error) { // tabPreviews.capture() raises error if the tab is discarded.
|
||||
// console.error('waterfoxBridge: failed to take a tab preview: ', tabId, error);
|
||||
}
|
||||
|
||||
// simulate default preview
|
||||
// see also: https://searchfox.org/mozilla-esr115/rev/d0623081f317c92e0c7bc2a8b1b138687bdb23f5/browser/themes/shared/ctrlTab.css#85-94
|
||||
const canvas = lazy.PageThumbs.createCanvas(window);
|
||||
try {
|
||||
// TODO: we should change the fill color to "CanvasText"...
|
||||
const image = new window.Image();
|
||||
await new Promise((resolve, reject) => {
|
||||
image.addEventListener('load', resolve, { once: true });
|
||||
image.addEventListener('error', reject, { once: true });
|
||||
image.src = 'chrome://global/skin/icons/defaultFavicon.svg';
|
||||
});
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'Canvas';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const iconSize = canvas.width * 0.2;
|
||||
ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
0,
|
||||
image.width,
|
||||
image.height,
|
||||
(canvas.width - iconSize) / 2,
|
||||
(canvas.height - iconSize) / 2,
|
||||
iconSize,
|
||||
iconSize
|
||||
);
|
||||
}
|
||||
catch (_error) {
|
||||
}
|
||||
info.url = canvas.toDataURL('image/png');
|
||||
return info;
|
||||
},
|
||||
|
||||
async showPreviewPanel(tabId, top) {
|
||||
const tab = tabId && context.extension.tabManager.get(tabId);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
const document = tab.nativeTab.ownerDocument;
|
||||
const tabbrowserTabs = document.getElementById('tabbrowser-tabs');
|
||||
if (!tabbrowserTabs)
|
||||
return;
|
||||
|
||||
if (!tabbrowserTabs.previewPanel) {
|
||||
// load the tab preview component
|
||||
const TabHoverPreviewPanel = ChromeUtils.importESModule(
|
||||
'chrome://browser/content/tabbrowser/tab-hover-preview.mjs'
|
||||
).default;
|
||||
tabbrowserTabs.previewPanel = new TabHoverPreviewPanel(
|
||||
document.getElementById('tab-preview-panel')
|
||||
);
|
||||
}
|
||||
tabbrowserTabs.previewPanel.__ws__top = top;
|
||||
tabbrowserTabs.previewPanel.activate(tab.nativeTab);
|
||||
},
|
||||
|
||||
async hidePreviewPanel(windowId) {
|
||||
const win = windowId && context.extension.windowManager.get(windowId);
|
||||
if (!win || !win.window)
|
||||
return;
|
||||
|
||||
try {
|
||||
// Access the document through the window object
|
||||
const document = win.window.document;
|
||||
const tabPreview = document.getElementById('tabbrowser-tabs')?.previewPanel;
|
||||
if (!tabPreview)
|
||||
return;
|
||||
tabPreview.__ws__top = null;
|
||||
} catch (error) {
|
||||
console.log("Error in hidePreviewPanel:", error);
|
||||
}
|
||||
},
|
||||
|
||||
async openPreferences() {
|
||||
BrowserWindowWatcher.openOptions();
|
||||
},
|
||||
|
||||
onWindowVisibilityChanged: new EventManager({
|
||||
context,
|
||||
name: 'waterfoxBridge.onWindowVisibilityChanged',
|
||||
register: (fire) => {
|
||||
const onChanged = win => {
|
||||
const wrappedWindow = context.extension.windowManager.getWrapper(win);
|
||||
if (wrappedWindow)
|
||||
fire.async(wrappedWindow.id, win.document.visibilityState).catch(() => {}); // ignore Message Manager disconnects
|
||||
};
|
||||
BrowserWindowWatcher.visibilityChangedListeners.add(onChanged);
|
||||
return () => {
|
||||
BrowserWindowWatcher.visibilityChangedListeners.delete(onChanged);
|
||||
};
|
||||
},
|
||||
}).api(),
|
||||
|
||||
onMenuCommand: new EventManager({
|
||||
context,
|
||||
name: 'waterfoxBridge.onMenuCommand',
|
||||
register: (fire) => {
|
||||
const onCommand = event => {
|
||||
fire.async({
|
||||
itemId: event.target.id,
|
||||
detail: event.detail,
|
||||
button: event.button,
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
shiftKey: event.shiftKey,
|
||||
}).catch(() => {}); // ignore Message Manager disconnects
|
||||
};
|
||||
BrowserWindowWatcher.menuCommandListeners.add(onCommand);
|
||||
return () => {
|
||||
BrowserWindowWatcher.menuCommandListeners.delete(onCommand);
|
||||
};
|
||||
},
|
||||
}).api(),
|
||||
|
||||
onSidebarShown: new EventManager({
|
||||
context,
|
||||
name: 'waterfoxBridge.onSidebarShown',
|
||||
register: (fire) => {
|
||||
const onShown = win => {
|
||||
const wrappedWindow = context.extension.windowManager.getWrapper(win);
|
||||
if (wrappedWindow)
|
||||
fire.async(wrappedWindow.id).catch(() => {}); // ignore Message Manager disconnects
|
||||
};
|
||||
BrowserWindowWatcher.sidebarShownListeners.add(onShown);
|
||||
return () => {
|
||||
BrowserWindowWatcher.sidebarShownListeners.delete(onShown);
|
||||
};
|
||||
},
|
||||
}).api(),
|
||||
|
||||
onSidebarHidden: new EventManager({
|
||||
context,
|
||||
name: 'waterfoxBridge.onSidebarHidden',
|
||||
register: (fire) => {
|
||||
const onHidden = win => {
|
||||
const wrappedWindow = context.extension.windowManager.getWrapper(win);
|
||||
if (wrappedWindow)
|
||||
fire.async(wrappedWindow.id).catch(() => {}); // ignore Message Manager disconnects
|
||||
};
|
||||
BrowserWindowWatcher.sidebarHiddenListeners.add(onHidden);
|
||||
return () => {
|
||||
BrowserWindowWatcher.sidebarHiddenListeners.delete(onHidden);
|
||||
};
|
||||
},
|
||||
}).api(),
|
||||
|
||||
|
||||
async listSyncDevices() {
|
||||
const devices = [];
|
||||
const targets = Services.wm.getMostRecentBrowserWindow().gSync.getSendTabTargets();
|
||||
for (const target of targets) {
|
||||
devices.push({
|
||||
id: target.id,
|
||||
name: target.name,
|
||||
type: target.type,
|
||||
});
|
||||
}
|
||||
return devices;
|
||||
},
|
||||
|
||||
async sendToDevice(tabIds, deviceId) {
|
||||
if (!Array.isArray(tabIds))
|
||||
tabIds = [tabIds];
|
||||
const gSync = Services.wm.getMostRecentBrowserWindow().gSync;
|
||||
const tabs = tabIds.map(id => context.extension.tabManager.get(id));
|
||||
const targets = gSync.getSendTabTargets().filter(target => !deviceId || target.id == deviceId);
|
||||
for (const tab of tabs) {
|
||||
gSync.sendTabToDevice(
|
||||
tab.nativeTab.linkedBrowser.currentURI.spec,
|
||||
targets,
|
||||
tab.nativeTab.linkedBrowser.contentTitle
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async openSyncDeviceSettings(windowId) {
|
||||
let DOMWin = null;
|
||||
try {
|
||||
const win = windowId && context.extension.windowManager.get(windowId)
|
||||
DOMWin = win?.window;
|
||||
}
|
||||
catch (_error) {
|
||||
}
|
||||
(DOMWin || Services.wm.getMostRecentBrowserWindow()).gSync.openDevicesManagementPage('sendtab');
|
||||
},
|
||||
|
||||
|
||||
async listSharingServices(tabId) {
|
||||
const tab = tabId && context.extension.tabManager.get(tabId);
|
||||
|
||||
const services = [];
|
||||
const win = Services.wm.getMostRecentBrowserWindow();
|
||||
const sharingService = win.gBrowser.MacSharingService;
|
||||
if (!sharingService)
|
||||
return services;
|
||||
|
||||
const uri = win.gURLBar.makeURIReadable(
|
||||
tab?.nativeTab.linkedBrowser.currentURI ||
|
||||
Services.io.newURI('https://waterfox.net/', null, null)
|
||||
).displaySpec;
|
||||
for (const service of sharingService.getSharingProviders(uri)) {
|
||||
services.push({
|
||||
name: service.name,
|
||||
title: service.menuItemTitle,
|
||||
image: service.image,
|
||||
});
|
||||
}
|
||||
return services;
|
||||
},
|
||||
|
||||
async share(tabIds, shareName) {
|
||||
if (!Array.isArray(tabIds))
|
||||
tabIds = [tabIds];
|
||||
const tabs = tabIds.map(id => context.extension.tabManager.get(id));
|
||||
|
||||
// currently we can share only one URL at a time...
|
||||
const tab = tabs[0];
|
||||
const win = Services.wm.getMostRecentBrowserWindow();
|
||||
const uri = win.gURLBar.makeURIReadable(tab.nativeTab.linkedBrowser.currentURI).displaySpec;
|
||||
|
||||
if (AppConstants.platform == 'win') {
|
||||
win.WindowsUIUtils.shareUrl(uri, tab.nativeTab.linkedBrowser.contentTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shareName) { // for macOS
|
||||
win.gBrowser.MacSharingService.shareUrl(shareName, uri, tab.nativeTab.linkedBrowser.contentTitle);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
async openSharingPreferences() {
|
||||
Services.wm.getMostRecentBrowserWindow().gBrowser.MacSharingService.openSharingPreferences();
|
||||
},
|
||||
|
||||
|
||||
async listAutoplayBlockedTabs(windowId) {
|
||||
const tabs = new Set();
|
||||
const windows = windowId ?
|
||||
[context.extension.windowManager.get(windowId)] :
|
||||
context.extension.windowManager.getAll();
|
||||
for (const win of windows) {
|
||||
if (!win.window.gBrowser)
|
||||
continue;
|
||||
for (const tab of win.window.document.querySelectorAll('tab[activemedia-blocked="true"]')) {
|
||||
const wrappedTab = context.extension.tabManager.getWrapper(tab);
|
||||
if (wrappedTab)
|
||||
tabs.add(wrappedTab.convert());
|
||||
}
|
||||
}
|
||||
return [...tabs].sort((a, b) => a.index - b.index);
|
||||
},
|
||||
|
||||
async isAutoplayBlockedTab(tabId) {
|
||||
const tab = context.extension.tabManager.get(tabId);
|
||||
if (!tab)
|
||||
return false;
|
||||
return tab.nativeTab.getAttribute('activemedia-blocked') == 'true';
|
||||
},
|
||||
|
||||
async unblockAutoplay(tabIds) {
|
||||
if (!Array.isArray(tabIds))
|
||||
tabIds = [tabIds];
|
||||
const tabs = tabIds.map(id => context.extension.tabManager.get(id));
|
||||
for (const tab of tabs) {
|
||||
tab.nativeTab.linkedBrowser.resumeMedia();
|
||||
}
|
||||
},
|
||||
|
||||
onAutoplayBlocked: new EventManager({
|
||||
context,
|
||||
name: 'waterfoxBridge.onAutoplayBlocked',
|
||||
register: (fire) => {
|
||||
const onBlocked = tab => {
|
||||
const wrappedTab = context.extension.tabManager.getWrapper(tab);
|
||||
if (wrappedTab)
|
||||
fire.async(wrappedTab.convert()).catch(() => {}); // ignore Message Manager disconnects
|
||||
};
|
||||
BrowserWindowWatcher.autoplayBlockedListeners.add(onBlocked);
|
||||
return () => {
|
||||
BrowserWindowWatcher.autoplayBlockedListeners.delete(onBlocked);
|
||||
};
|
||||
},
|
||||
}).api(),
|
||||
|
||||
onAutoplayUnblocked: new EventManager({
|
||||
context,
|
||||
name: 'waterfoxBridge.onAutoplayUnblocked',
|
||||
register: (fire) => {
|
||||
const onUnblocked = tab => {
|
||||
const wrappedTab = context.extension.tabManager.getWrapper(tab);
|
||||
if (wrappedTab)
|
||||
fire.async(wrappedTab.convert()).catch(() => {}); // ignore Message Manager disconnects
|
||||
};
|
||||
BrowserWindowWatcher.autoplayUnblockedListeners.add(onUnblocked);
|
||||
return () => {
|
||||
BrowserWindowWatcher.autoplayUnblockedListeners.delete(onUnblocked);
|
||||
};
|
||||
},
|
||||
}).api(),
|
||||
|
||||
|
||||
async isSelectionClipboardAvailable() {
|
||||
try {
|
||||
return Services.clipboard.isClipboardTypeSupported(Services.clipboard.kSelectionClipboard);
|
||||
}
|
||||
catch(_error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async getSelectionClipboardContents() {
|
||||
try {
|
||||
const transferable = Components.classes['@mozilla.org/widget/transferable;1']
|
||||
.createInstance(Components.interfaces.nsITransferable);
|
||||
const loadContext = Services.wm.getMostRecentBrowserWindow()
|
||||
.docShell.QueryInterface(Components.interfaces.nsILoadContext);
|
||||
transferable.init(loadContext);
|
||||
transferable.addDataFlavor('text/plain');
|
||||
|
||||
Services.clipboard.getData(transferable, Services.clipboard.kSelectionClipboard);
|
||||
|
||||
const data = {};
|
||||
transferable.getTransferData('text/plain', data);
|
||||
if (data) {
|
||||
data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
catch(_error) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onShutdown(isAppShutdown) {
|
||||
if (isAppShutdown)
|
||||
return;
|
||||
|
||||
lazy.AddonManager.removeInstallListener(BrowserWindowWatcher);
|
||||
lazy.AddonManager.removeAddonListener(BrowserWindowWatcher);
|
||||
|
||||
if (lazy.PlacesUtils.__ws_orig__unwrapNodes) {
|
||||
lazy.PlacesUtils.unwrapNodes = lazy.PlacesUtils.__ws_orig__unwrapNodes;
|
||||
lazy.PlacesUtils.__ws_orig__unwrapNodes = null;
|
||||
}
|
||||
|
||||
const registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
|
||||
registrar.unregisterFactory(
|
||||
BrowserWindowWatcher.classID,
|
||||
BrowserWindowWatcher
|
||||
);
|
||||
Services.catMan.deleteCategoryEntry(
|
||||
'content-policy',
|
||||
BrowserWindowWatcher.contractID,
|
||||
false
|
||||
);
|
||||
|
||||
Services.ww.unregisterNotification(BrowserWindowWatcher);
|
||||
|
||||
const windows = BrowserWindowWatcher.iterateTargetWindows();
|
||||
while (true) {
|
||||
const win = windows.next();
|
||||
if (win.done)
|
||||
break;
|
||||
BrowserWindowWatcher.unhandleWindow(win.value);
|
||||
}
|
||||
|
||||
//const handler = Cc['@mozilla.org/network/protocol;1?name=resource'].getService(Components.interfaces.nsISubstitutingProtocolHandler);
|
||||
//handler.setSubstitution('waterfox-bridge', null);
|
||||
|
||||
Services.prefs.removeObserver('', BrowserWindowWatcher);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,459 @@
|
||||
[
|
||||
{
|
||||
"namespace": "waterfoxBridge",
|
||||
|
||||
"types": [
|
||||
{
|
||||
"id": "TabPreviewInfo",
|
||||
"type": "object",
|
||||
"description": "Information of a tab preview image.",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "A URL string to a tab preview image."
|
||||
},
|
||||
"found": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates availability of the preview image."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "SyncDeviceInfo",
|
||||
"type": "object",
|
||||
"description": "An object indicating sync device.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The identifier string of a sync device."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The visible name of a sync device."
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "The type string of a sync device."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ShareService",
|
||||
"type": "object",
|
||||
"description": "An object indicating sync device.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of a sharing service."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The menu label for a sharing service."
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"description": "The image for a sharing device."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"functions": [
|
||||
{
|
||||
"name": "initUI",
|
||||
"type": "function",
|
||||
"description": "Initializes the custom UI for Waterfox.",
|
||||
"async": true,
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"name": "reserveToLoadForbiddenURL",
|
||||
"type": "function",
|
||||
"description": "Reserve a URL to load by Waterfox.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "url"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getFileURL",
|
||||
"type": "function",
|
||||
"description": "Get file: URL from a file.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "file",
|
||||
"type": "object",
|
||||
"description": "Information of a file.",
|
||||
"properties": {
|
||||
"lastModified": {
|
||||
"type": "integer",
|
||||
"description": "Last modified timestamp of the file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the file."
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "Size of the file."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "MIME type of the file."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "string",
|
||||
"name": "url"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "getTabPreview",
|
||||
"type": "function",
|
||||
"description": "Returns a tab preview image URL and its metadata.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "waterfoxBridge.TabPreviewInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "showPreviewPanel",
|
||||
"type": "function",
|
||||
"description": "Shows the preview panel for the tab.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "top"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hidePreviewPanel",
|
||||
"type": "function",
|
||||
"description": "Hides the preview panel for the window.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "windowId",
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "openPreferences",
|
||||
"type": "function",
|
||||
"description": "Opens the preferences.",
|
||||
"async": true,
|
||||
"parameters": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "listSyncDevices",
|
||||
"type": "function",
|
||||
"description": "Returns an array of sync device information.",
|
||||
"async": true,
|
||||
"parameters": [],
|
||||
"returns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "waterfoxBridge.SyncDeviceInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sendToDevice",
|
||||
"type": "function",
|
||||
"description": "Sends tabs to a device or all devices.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tabIds",
|
||||
"description": "The tab or list of tabs to send.",
|
||||
"type": "array",
|
||||
"choices": [
|
||||
{ "type": "integer", "minimum": 0 },
|
||||
{ "type": "array", "items": { "type": "integer", "minimum": 0 } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "string",
|
||||
"description": "The ID of a device",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "openSyncDeviceSettings",
|
||||
"type": "function",
|
||||
"description": "Opens the settings of sync devices.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "windowId",
|
||||
"minimum": 0,
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "listSharingServices",
|
||||
"type": "function",
|
||||
"description": "Returns an array of share service on macOS.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0,
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "waterfoxBridge.ShareService"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "share",
|
||||
"type": "function",
|
||||
"description": "Shares tabs via browser's share feature.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tabIds",
|
||||
"description": "The tab or list of tabs to share.",
|
||||
"type": "array",
|
||||
"choices": [
|
||||
{ "type": "integer", "minimum": 0 },
|
||||
{ "type": "array", "items": { "type": "integer", "minimum": 0 } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "shareName",
|
||||
"type": "string",
|
||||
"description": "The name of a sharing method on macOS",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "openSharingPreferences",
|
||||
"type": "function",
|
||||
"description": "Opens the preferences of sharing services on macOS.",
|
||||
"async": true,
|
||||
"parameters": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "listAutoplayBlockedTabs",
|
||||
"type": "function",
|
||||
"description": "Returns an array of autoplay blocked tabs in a window.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "windowId",
|
||||
"minimum": 0,
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "tabs.Tab"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isAutoplayBlockedTab",
|
||||
"type": "function",
|
||||
"description": "Returns the tab's autoplay blocked state.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "windowId",
|
||||
"minimum": 0
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "boolean",
|
||||
"name": "autoplayBlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unblockAutoplay",
|
||||
"type": "function",
|
||||
"description": "Resumes blocked autoplay of tabs.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tabIds",
|
||||
"description": "The tab or list of tabs to resume autoplay.",
|
||||
"type": "array",
|
||||
"choices": [
|
||||
{ "type": "integer", "minimum": 0 },
|
||||
{ "type": "array", "items": { "type": "integer", "minimum": 0 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "isSelectionClipboardAvailable",
|
||||
"type": "function",
|
||||
"description": "Returns availability of the selection clipboard.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
],
|
||||
"returns": {
|
||||
"type": "boolean",
|
||||
"name": "selectionClipboardAvailable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "getSelectionClipboardContents",
|
||||
"type": "function",
|
||||
"description": "Returns the contents of the selection clipboard as a string.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
],
|
||||
"returns": {
|
||||
"type": "string",
|
||||
"name": "selectionClipboardContents"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"events": [
|
||||
{
|
||||
"name": "onWindowVisibilityChanged",
|
||||
"description": "Notified when the minimized state of a window is changed.",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "windowId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "visibilityState"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onMenuCommand",
|
||||
"description": "Notified when a menu command is invoked.",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventInfo",
|
||||
"type": "object",
|
||||
"description": "An object indicating command event.",
|
||||
"properties": {
|
||||
"itemId": {
|
||||
"type": "string",
|
||||
"description": "The id of the event target."
|
||||
},
|
||||
"detail": {
|
||||
"type": "integer",
|
||||
"description": "Corresponding to MouseEvent.prototype.detail."
|
||||
},
|
||||
"button": {
|
||||
"type": "integer",
|
||||
"description": "Corresponding to MouseEvent.prototype.button."
|
||||
},
|
||||
"altKey": {
|
||||
"type": "boolean",
|
||||
"description": "Corresponding to MouseEvent.prototype.altKey."
|
||||
},
|
||||
"ctrlKey": {
|
||||
"type": "boolean",
|
||||
"description": "Corresponding to MouseEvent.prototype.ctrlKey."
|
||||
},
|
||||
"metaKey": {
|
||||
"type": "boolean",
|
||||
"description": "Corresponding to MouseEvent.prototype.metaKey."
|
||||
},
|
||||
"shiftKey": {
|
||||
"type": "boolean",
|
||||
"description": "Corresponding to MouseEvent.prototype.shiftKey."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onSidebarShown",
|
||||
"description": "Notified when the sidebar is shown.",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "windowId"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onSidebarHidden",
|
||||
"description": "Notified when the sidebar is hidden.",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "windowId"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onAutoplayBlocked",
|
||||
"description": "Notified when a tab's autoplay is blocked.",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "tabs.Tab"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onAutoplayUnblocked",
|
||||
"description": "Notified when a tab's autoplay is unblocked.",
|
||||
"type": "function",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "tabs.Tab"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
630
waterfox/browser/components/sidebar/extlib/Configs.js
Normal file
630
waterfox/browser/components/sidebar/extlib/Configs.js
Normal file
@@ -0,0 +1,630 @@
|
||||
/*
|
||||
license: The MIT License, Copyright (c) 2016-2023 YUKI "Piro" Hiroshi
|
||||
original:
|
||||
http://github.com/piroor/webextensions-lib-configs
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
There are multiple level values:
|
||||
|
||||
(higher priority)
|
||||
* [default] locked managed values (given via GPO or policies.json)
|
||||
* [default] locked default values (given to the constructor)
|
||||
* [user] locked user values (given via API)
|
||||
=> [default] it should fallback to the default value if there is no user value
|
||||
* [user] user values (local storage)
|
||||
* [default] non-locked managed values (given via GPO or policies.json)
|
||||
* [default] overridden default values (given via API)
|
||||
* [default] built-in default values (given to the constructor)
|
||||
(lower priority)
|
||||
|
||||
Only values different from [default] are stored and synchronized.
|
||||
*/
|
||||
|
||||
const OBSERVABLE_AREA = new Set([
|
||||
'internal', // TST internal
|
||||
'local',
|
||||
'sync',
|
||||
'managed',
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class Configs {
|
||||
constructor(
|
||||
defaults,
|
||||
{ logging, logger, localKeys, syncKeys, sync } = { syncKeys: [], logger: null }
|
||||
) {
|
||||
this._defaultValues = {
|
||||
...this._clone(defaults),
|
||||
__ConfigsMigration__userValeusSameToDefaultAreCleared: false,
|
||||
};
|
||||
this._lockedDefaultKeys = new Set();
|
||||
|
||||
this._managedValues = {};
|
||||
this._lockedManagedKeys = new Set();
|
||||
|
||||
this._userValues = {};
|
||||
this._lockedUserKeys = new Set();
|
||||
|
||||
this._fetchedValues = {};
|
||||
|
||||
this.$default = {};
|
||||
this.$all = {};
|
||||
|
||||
for (const key of Object.keys(this._defaultValues)) {
|
||||
Object.defineProperty(this.$default, key, {
|
||||
get: () => this._getDefaultValue(key),
|
||||
set: (value) => this._setDefaultValue(key, value),
|
||||
enumerable: true,
|
||||
});
|
||||
const description = {
|
||||
get: () => this._getValue(key),
|
||||
set: (value) => this._setValue(key, value),
|
||||
enumerable: true,
|
||||
};
|
||||
Object.defineProperty(this, key, description);
|
||||
Object.defineProperty(this.$all, key, description);
|
||||
}
|
||||
|
||||
for (const [key, locked] of Object.entries(defaults)) {
|
||||
if (!key.endsWith(':locked'))
|
||||
continue;
|
||||
if (locked)
|
||||
this._lockedDefaultKeys.add(key.replace(/:locked$/, ''));
|
||||
delete defaults[key];
|
||||
}
|
||||
|
||||
this.$logging = logging || false;
|
||||
this.$logs = [];
|
||||
this.$logger = logger;
|
||||
this.sync = sync === undefined ? true : !!sync;
|
||||
this._updating = new Map();
|
||||
this._observers = new Set();
|
||||
this._changedObservers = new Set();
|
||||
this._localLoadedObservers = new Set();
|
||||
this._syncKeys = [
|
||||
...(localKeys ?
|
||||
Object.keys(defaults).filter(x => !localKeys.includes(x)) :
|
||||
(syncKeys || [])),
|
||||
'__ConfigsMigration__userValeusSameToDefaultAreCleared',
|
||||
];
|
||||
this.$loaded = this._load();
|
||||
|
||||
this.$preReceivedChanges = [];
|
||||
this.$listeningChanges = false;
|
||||
browser.storage.onChanged.addListener(this._onChanged.bind(this));
|
||||
|
||||
this.$preReceivedMessages = [];
|
||||
this.$listeningMessages = false;
|
||||
browser.runtime.onMessage.addListener(this._onMessage.bind(this));
|
||||
}
|
||||
|
||||
$reset(key, { broadcast } = {}) {
|
||||
if (!key) {
|
||||
for (const key of Object.keys(this._defaultValues)) {
|
||||
this.$reset(key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._defaultValues.hasOwnProperty(key))
|
||||
throw new Error(`failed to reset unknown key: ${key}`);
|
||||
|
||||
this._setValue(key, this._getDefaultValue(key), true, { broadcast });
|
||||
}
|
||||
|
||||
$cleanUp({ broadcast } = {}) {
|
||||
for (const [key, defaultValue] of Object.entries(this.$default)) {
|
||||
if (!this._userValues.hasOwnProperty(key))
|
||||
continue;
|
||||
const value = JSON.stringify(this._getNonDefaultValue(key));
|
||||
if (value == JSON.stringify(defaultValue) ||
|
||||
(this._managedValues.hasOwnProperty(key) &&
|
||||
value == JSON.stringify(this._managedValues[key])))
|
||||
this.$reset(key, { broadcast });
|
||||
}
|
||||
}
|
||||
|
||||
_getDefaultValue(key) {
|
||||
if (this._managedValues.hasOwnProperty(key))
|
||||
return this._managedValues[key];
|
||||
return this._defaultValues[key];
|
||||
}
|
||||
|
||||
_setDefaultValue(key, value, { broadcast } = {}) {
|
||||
if (!key)
|
||||
throw new Error(`missing key for default value ${value}`);
|
||||
|
||||
if (!this._defaultValues.hasOwnProperty(key))
|
||||
throw new Error(`failed to set default value for unknown key: ${key}`);
|
||||
|
||||
const currentValue = this[key];
|
||||
const currentDefaultValue = this._getDefaultValue(key);
|
||||
|
||||
this._defaultValues[key] = this._clone(value);
|
||||
const defaultValue = this._getDefaultValue(key);
|
||||
if (JSON.stringify(defaultValue) == JSON.stringify(this._getNonDefaultValue[key]))
|
||||
this.$reset(key, { broadcast });
|
||||
|
||||
const newDefaultValue = this._getDefaultValue(key);
|
||||
if (currentValue == currentDefaultValue &&
|
||||
currentValue != newDefaultValue &&
|
||||
this[key] == newDefaultValue) {
|
||||
const observers = [...this._observers, ...this._changedObservers];
|
||||
this.$notifyToObservers(key, value, observers, 'onChangeConfig');
|
||||
}
|
||||
|
||||
if (broadcast === false)
|
||||
return;
|
||||
|
||||
try {
|
||||
browser.runtime.sendMessage({
|
||||
type: 'Configs:updateDefaultValue',
|
||||
key: key,
|
||||
value: defaultValue,
|
||||
}).catch(_error => {});
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
}
|
||||
|
||||
_getNonDefaultValue(key) {
|
||||
if (this._userValues.hasOwnProperty(key))
|
||||
return this._userValues[key];
|
||||
if (this._managedValues.hasOwnProperty(key) &&
|
||||
!this._lockedManagedKeys.has(key))
|
||||
return this._managedValues[key];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
$addLocalLoadedObserver(observer) {
|
||||
if (!this._localLoadedObservers.has(observer))
|
||||
this._localLoadedObservers.add(observer);
|
||||
}
|
||||
$removeLocalLoadedObserver(observer) {
|
||||
this._localLoadedObservers.delete(observer);
|
||||
}
|
||||
|
||||
$addChangedObserver(observer) {
|
||||
if (!this._changedObservers.has(observer))
|
||||
this._changedObservers.add(observer);
|
||||
}
|
||||
$removeChangedObserver(observer) {
|
||||
this._changedObservers.delete(observer);
|
||||
}
|
||||
|
||||
$addObserver(observer) {
|
||||
// for backward compatibility
|
||||
if (typeof observer == 'function')
|
||||
this.$addChangedObserver(observer);
|
||||
else if (!this._observers.has(observer))
|
||||
this._observers.add(observer);
|
||||
}
|
||||
$removeObserver(observer) {
|
||||
// for backward compatibility
|
||||
if (typeof observer == 'function')
|
||||
this.$removeChangedObserver(observer);
|
||||
else
|
||||
this._observers.delete(observer);
|
||||
}
|
||||
|
||||
_log(message, ...args) {
|
||||
message = `Configs[${location.href}] ${message}`;
|
||||
this.$logs = this.$logs.slice(-1000);
|
||||
|
||||
if (!this.$logging)
|
||||
return;
|
||||
|
||||
if (typeof this.$logger === 'function')
|
||||
this.$logger(message, ...args);
|
||||
else
|
||||
console.log(message, ...args);
|
||||
}
|
||||
|
||||
_load() {
|
||||
return this.$_promisedLoad ||
|
||||
(this.$_promisedLoad = this._tryLoad());
|
||||
}
|
||||
|
||||
async _tryLoad() {
|
||||
this._log('load');
|
||||
try {
|
||||
this._log(`load: try load from storage on ${location.href}`);
|
||||
const [localValues, managedValues, lockedKeys] = await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
const localValues = await browser.storage.local.get(null); // keys must be "null" to get only stored values
|
||||
this._log('load: successfully loaded local storage');
|
||||
const observers = [...this._observers, ...this._localLoadedObservers];
|
||||
for (const [key, value] of Object.entries(localValues)) {
|
||||
this.$notifyToObservers(key, value, observers, 'onLocalLoaded');
|
||||
}
|
||||
return localValues;
|
||||
}
|
||||
catch(e) {
|
||||
this._log('load: failed to load local storage: ', String(e));
|
||||
}
|
||||
return {};
|
||||
})(),
|
||||
(async () => {
|
||||
if (!browser.storage.managed) {
|
||||
this._log('load: skip managed storage');
|
||||
return null;
|
||||
}
|
||||
return new Promise(async (resolve, _reject) => {
|
||||
const loadManagedStorage = () => {
|
||||
let resolved = false;
|
||||
return new Promise((resolve, reject) => {
|
||||
browser.storage.managed.get().then(managedValues => {
|
||||
if (resolved)
|
||||
return;
|
||||
resolved = true;
|
||||
this._log('load: successfully loaded managed storage');
|
||||
resolve(managedValues || null);
|
||||
}).catch(error => {
|
||||
if (resolved)
|
||||
return;
|
||||
resolved = true;
|
||||
this._log('load: failed to load managed storage: ', String(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// storage.managed.get() fails on options page in Thunderbird.
|
||||
// The problem should be fixed by Thunderbird side.
|
||||
setTimeout(() => {
|
||||
if (resolved)
|
||||
return;
|
||||
resolved = true;
|
||||
this._log('load: failed to load managed storage: timeout');
|
||||
reject(new Error('timeout'));
|
||||
}, 250);
|
||||
});
|
||||
};
|
||||
|
||||
for (let i = 0, maxi = 10; i < maxi; i++) {
|
||||
try {
|
||||
const result = await loadManagedStorage();
|
||||
// On old versions Firefox and Thunderbird, a value with
|
||||
// REG_MULTI_SZ type is always delivered as a simple string,
|
||||
// thus we need to parse it by self.
|
||||
for (const [key, value] of Object.entries(result)) {
|
||||
const defaultValue = this._defaultValues[key];
|
||||
if (typeof value != 'string')
|
||||
continue;
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (Array.isArray(defaultValue)) {
|
||||
result[key] = (trimmed.startsWith('[') && trimmed.endsWith(']')) ?
|
||||
JSON.parse(value) :
|
||||
trimmed.includes('\n') ?
|
||||
trimmed.split('\n') :
|
||||
trimmed.split(',');
|
||||
}
|
||||
else if (defaultValue &&
|
||||
typeof defaultValue == 'object' &&
|
||||
trimmed.startsWith('{') &&
|
||||
trimmed.endsWith('}')) {
|
||||
result[key] = JSON.parse(trimmed);
|
||||
}
|
||||
}
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
catch(error) {
|
||||
if (error.message != 'timeout') {
|
||||
console.log('managed storage is not provided');
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
console.log('failed to load managed storage ', error);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
console.log('failed to load managed storage with 10 times retly');
|
||||
resolve(null);
|
||||
});
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
const lockedKeys = await browser.runtime.sendMessage({
|
||||
type: 'Configs:getLockedKeys'
|
||||
});
|
||||
this._log('load: successfully synchronized locked state');
|
||||
return lockedKeys || [];
|
||||
}
|
||||
catch(e) {
|
||||
this._log('load: failed to synchronize locked state: ', String(e));
|
||||
}
|
||||
return [];
|
||||
})()
|
||||
]);
|
||||
this._log(`load: loaded:`, { localValues, managedValues, lockedKeys });
|
||||
|
||||
lockedKeys.push(...this._lockedDefaultKeys);
|
||||
|
||||
if (managedValues) {
|
||||
for (const [key, value] of Object.entries(managedValues)) {
|
||||
if (key.endsWith(':locked'))
|
||||
continue;
|
||||
const locked = managedValues[`${key}:locked`] !== false;
|
||||
this._managedValues[key] = value;
|
||||
if (locked)
|
||||
this._lockedManagedKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
this._userValues = this._clone({ ...(localValues || {}) });
|
||||
this._log('load: values are applied');
|
||||
|
||||
for (const key of new Set(lockedKeys)) {
|
||||
this._updateLocked(key, true);
|
||||
}
|
||||
this._log('load: locked state is applied');
|
||||
this.$listeningChanges = true;
|
||||
if (this.sync &&
|
||||
(this._syncKeys ||
|
||||
this._syncKeys.length > 0)) {
|
||||
try {
|
||||
browser.storage.sync.get(this._syncKeys).then(syncedValues => {
|
||||
this._log('load: successfully loaded sync storage');
|
||||
if (!syncedValues)
|
||||
return;
|
||||
for (const key of Object.keys(syncedValues)) {
|
||||
this[key] = syncedValues[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
this._log('load: failed to read sync storage: ', String(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
this.$listeningMessages = true;
|
||||
|
||||
if (!this.__ConfigsMigration__userValeusSameToDefaultAreCleared) {
|
||||
this.$cleanUp();
|
||||
this.__ConfigsMigration__userValeusSameToDefaultAreCleared = true;
|
||||
}
|
||||
|
||||
this.$_promisedLoad = this.$_promisedLoad.then(() => {
|
||||
if (this.$preReceivedChanges.length > 0) {
|
||||
const changes = [...this.$preReceivedChanges];
|
||||
this.$preReceivedChanges = [];
|
||||
for (const change of changes) {
|
||||
this._onChanged(change, 'internal');
|
||||
}
|
||||
}
|
||||
if (this.$preReceivedMessages.length > 0) {
|
||||
const messages = [...this.$preReceivedMessages];
|
||||
this.$preReceivedMessages = [];
|
||||
for (const message of messages) {
|
||||
this._onMessage(message.message, message.sender);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this.$all;
|
||||
}
|
||||
catch(e) {
|
||||
this._log('load: fatal error: ', e, e.stack);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
_getValue(key) {
|
||||
if (this._lockedManagedKeys.has(key))
|
||||
return this._managedValues[key];
|
||||
if (this._lockedDefaultKeys.has(key))
|
||||
return this._defaultValues[key];
|
||||
if (this._lockedUserKeys.has(key))
|
||||
return this._userValues[key] || this._getDefaultValue(key);
|
||||
if (this._userValues.hasOwnProperty(key))
|
||||
return this._userValues[key];
|
||||
if (this._managedValues.hasOwnProperty(key))
|
||||
return this._managedValues[key];
|
||||
if (this._defaultValues.hasOwnProperty(key))
|
||||
return this._defaultValues[key];
|
||||
throw new Error(`invalid access: unknown key ${key}`);
|
||||
}
|
||||
|
||||
_setValue(key, value, force = false, { broadcast } = {}) {
|
||||
const newValue = this._clone(value);
|
||||
|
||||
if (this._lockedDefaultKeys.has(key) ||
|
||||
this._lockedManagedKeys.has(key) ||
|
||||
this._lockedUserKeys.has(key)) {
|
||||
this._log(`warning: ${key} is locked and not updated`);
|
||||
return newValue;
|
||||
}
|
||||
|
||||
const stringified = JSON.stringify(value);
|
||||
if (stringified == JSON.stringify(this._userValues[key]) && !force) {
|
||||
this._log(`skip: ${key} is not changed`);
|
||||
return newValue;
|
||||
}
|
||||
|
||||
const oldValue = this._getValue(key);
|
||||
|
||||
const shouldReset = stringified == JSON.stringify(this._getDefaultValue(key));
|
||||
this._log(`set: ${key} = ${value}${shouldReset ? ' (reset to default)' : ''}`);
|
||||
if (shouldReset)
|
||||
delete this._userValues[key];
|
||||
else
|
||||
this._userValues[key] = newValue;
|
||||
|
||||
if (broadcast === false)
|
||||
return newValue;
|
||||
|
||||
const update = {};
|
||||
update[key] = newValue;
|
||||
try {
|
||||
const updatingValues = this._updating.get(key) || [];
|
||||
updatingValues.push(newValue);
|
||||
this._updating.set(key, updatingValues);
|
||||
const updated = shouldReset ?
|
||||
browser.storage.local.remove([key]).then(() => {
|
||||
this._log('local: successfully removed ', key);
|
||||
}) :
|
||||
browser.storage.local.set(update).then(() => {
|
||||
this._log('local: successfully saved ', update);
|
||||
});
|
||||
updated
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
const updatingValues = this._updating.get(key);
|
||||
if (!updatingValues ||
|
||||
!updatingValues.includes(newValue))
|
||||
return;
|
||||
// failsafe: on Thunderbird updates sometimes won't be notified to the page itself.
|
||||
const changes = {};
|
||||
changes[key] = {
|
||||
oldValue,
|
||||
newValue,
|
||||
};
|
||||
this._onChanged(changes, 'internal');
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
this._log('save: failed', e);
|
||||
}
|
||||
try {
|
||||
if (this.sync && this._syncKeys.includes(key)) {
|
||||
if (shouldReset)
|
||||
browser.storage.sync.remove([key]).then(() => {
|
||||
this._log('sync: successfully removed', update);
|
||||
});
|
||||
else
|
||||
browser.storage.sync.set(update).then(() => {
|
||||
this._log('sync: successfully synced', update);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
this._log('sync: failed', e);
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
|
||||
$lock(key) {
|
||||
this._log('locking: ' + key);
|
||||
this._updateLocked(key, true);
|
||||
}
|
||||
|
||||
$unlock(key) {
|
||||
this._log('unlocking: ' + key);
|
||||
this._updateLocked(key, false);
|
||||
}
|
||||
|
||||
$isLocked(key) {
|
||||
return this._lockedUserKeys.has(key);
|
||||
}
|
||||
|
||||
_updateLocked(key, locked, { broadcast } = {}) {
|
||||
if (locked)
|
||||
this._lockedUserKeys.add(key);
|
||||
else
|
||||
this._lockedUserKeys.delete(key);
|
||||
|
||||
if (browser.runtime &&
|
||||
broadcast !== false) {
|
||||
try {
|
||||
browser.runtime.sendMessage({
|
||||
type: 'Configs:updateLocked',
|
||||
key: key,
|
||||
locked: this._lockedUserKeys.has(key),
|
||||
}).catch(_error => {});
|
||||
}
|
||||
catch(_error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onMessage(message, sender) {
|
||||
if (!message ||
|
||||
typeof message.type != 'string')
|
||||
return;
|
||||
|
||||
if (!this.$listeningMessages) {
|
||||
this.$preReceivedMessages.push({ message, sender });
|
||||
return;
|
||||
}
|
||||
|
||||
this._log(`onMessage: ${message.type}`, message, sender);
|
||||
switch (message.type) {
|
||||
case 'Configs:getLockedKeys':
|
||||
return Promise.resolve(Array.from(this._lockedUserKeys));
|
||||
|
||||
case 'Configs:updateLocked':
|
||||
this._updateLocked(message.key, message.locked, { broadcast: false });
|
||||
break;
|
||||
|
||||
case 'Configs:updateDefaultValue':
|
||||
this._setDefaultValue(message.key, message.value, { broadcast: false });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onChanged(changes, areaName) {
|
||||
if (!OBSERVABLE_AREA.has(areaName))
|
||||
return;
|
||||
|
||||
if (!this.$listeningChanges) {
|
||||
this.$preReceivedChanges.push(changes);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log('_onChanged ', areaName, changes);
|
||||
const observers = [...this._observers, ...this._changedObservers];
|
||||
for (const [key, change] of Object.entries(changes)) {
|
||||
// storage.local.onChanged is sometimes notified with delay, and it
|
||||
// unexpctedly reverts stored user value after it is changed multiple
|
||||
// times in short time range, and it may produce "ghost value" problem, like:
|
||||
// 1. setting to "true" (updates the stored value to "true" immediately)
|
||||
// 2. setting to "false" (updates the stored value to "false" immediately)
|
||||
// 3. "true" is notified (updates the stored value to "true" with delay)
|
||||
// 4. getting the value - it gots "true" instead of "false"!
|
||||
// To avoid such problems, we need to skip applying notified new value
|
||||
// if the notification is from a local change.
|
||||
const updatingValues = this._updating.get(key);
|
||||
if (updatingValues &&
|
||||
updatingValues[0] == change.newValue) {
|
||||
updatingValues.shift();
|
||||
}
|
||||
else {
|
||||
if ('newValue' in change)
|
||||
this._userValues[key] = this._clone(change.newValue);
|
||||
else
|
||||
delete this._userValues[key];
|
||||
}
|
||||
|
||||
if (!updatingValues || updatingValues.length == 0)
|
||||
this._updating.delete(key);
|
||||
else
|
||||
this._updating.set(key, updatingValues);
|
||||
|
||||
const value = this._getNonDefaultValue(key);
|
||||
|
||||
if (JSON.stringify(value) == JSON.stringify(this._getDefaultValue(key)))
|
||||
return;
|
||||
|
||||
this.$notifyToObservers(key, value, observers, 'onChangeConfig');
|
||||
}
|
||||
}
|
||||
|
||||
$notifyToObservers(key, value, observers, observerMethod) {
|
||||
for (const observer of observers) {
|
||||
if (typeof observer === 'function')
|
||||
observer(key, value);
|
||||
else if (observer && typeof observer[observerMethod] === 'function')
|
||||
observer[observerMethod](key, value);
|
||||
}
|
||||
}
|
||||
|
||||
_clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
export default Configs;
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
license: The MIT License, Copyright (c) 2018 YUKI "Piro" Hiroshi
|
||||
original:
|
||||
https://github.com/piroor/webextensions-lib-event-listener-manager
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
export default class EventListenerManager {
|
||||
constructor() {
|
||||
this._listeners = new Set();
|
||||
this._stacksOnListenerAdded = new WeakMap();
|
||||
this._sourceOfListeners = new WeakMap();
|
||||
}
|
||||
|
||||
addListener(listener) {
|
||||
const listeners = this._listeners;
|
||||
if (!listeners.has(listener)) {
|
||||
listeners.add(listener);
|
||||
if (EventListenerManager.debug) {
|
||||
const stack = new Error().stack;
|
||||
this._stacksOnListenerAdded.set(listener, stack);
|
||||
this._sourceOfListeners.set(listener, stack.split('\n')[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeListener(listener) {
|
||||
this._listeners.delete(listener);
|
||||
this._stacksOnListenerAdded.delete(listener);
|
||||
this._sourceOfListeners.delete(listener);
|
||||
}
|
||||
|
||||
removeAllListeners() {
|
||||
this._listeners.clear();
|
||||
this._stacksOnListenerAdded.clear();
|
||||
this._sourceOfListeners.clear();
|
||||
}
|
||||
|
||||
hasListener() {
|
||||
return this._listeners.size > 0;
|
||||
}
|
||||
|
||||
dispatch(...args) {
|
||||
const results = this.dispatchWithDetails(...args);
|
||||
if (results instanceof Promise)
|
||||
return results.then(this.normalizeResults);
|
||||
else
|
||||
return this.normalizeResults(results);
|
||||
}
|
||||
|
||||
// We hope to process results synchronously if possibly,
|
||||
// so this method must not be "async".
|
||||
dispatchWithDetails(...args) {
|
||||
const results = Array(this._listeners.size);
|
||||
let index = 0;
|
||||
|
||||
for (let listener of this._listeners) {
|
||||
const startAt = EventListenerManager.debug && Date.now();
|
||||
const timer = EventListenerManager.debug && setTimeout(() => {
|
||||
const listenerAddedStack = this._stacksOnListenerAdded.get(listener);
|
||||
console.log(`listener does not respond in ${TIMEOUT}ms.\n----------------------\n${listenerAddedStack || 'non debug mode or already removed listener:\n'+listener.toString()}\n----------------------\n${new Error().stack}\n----------------------\nargs:`, args);
|
||||
}, TIMEOUT);
|
||||
|
||||
try {
|
||||
const result = listener(...args);
|
||||
if (result instanceof Promise)
|
||||
results[index++] = result
|
||||
.catch(e => {
|
||||
console.log(e);
|
||||
})
|
||||
.then(result => {
|
||||
if (timer)
|
||||
clearTimeout(timer);
|
||||
return {
|
||||
value: result,
|
||||
elapsed: EventListenerManager.debug && (Date.now() - startAt),
|
||||
async: true,
|
||||
listener: EventListenerManager.debug && this._sourceOfListeners.get(listener)
|
||||
};
|
||||
});
|
||||
if (timer)
|
||||
clearTimeout(timer);
|
||||
results[index++] = {
|
||||
value: result,
|
||||
elapsed: EventListenerManager.debug && (Date.now() - startAt),
|
||||
async: false,
|
||||
listener: EventListenerManager.debug && this._sourceOfListeners.get(listener)
|
||||
};
|
||||
}
|
||||
catch(e) {
|
||||
console.log(e);
|
||||
if (timer)
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.some(result => result instanceof Promise))
|
||||
return Promise.all(results);
|
||||
else
|
||||
return results;
|
||||
}
|
||||
|
||||
normalizeResults(results) {
|
||||
if (results.length == 1)
|
||||
return results[0].value;
|
||||
for (const result of results) {
|
||||
if (result.value === false)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
EventListenerManager.debug = false;
|
||||
1144
waterfox/browser/components/sidebar/extlib/MenuUI.js
Normal file
1144
waterfox/browser/components/sidebar/extlib/MenuUI.js
Normal file
File diff suppressed because it is too large
Load Diff
420
waterfox/browser/components/sidebar/extlib/Options.js
Normal file
420
waterfox/browser/components/sidebar/extlib/Options.js
Normal file
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
license: The MIT License, Copyright (c) 2016-2020 YUKI "Piro" Hiroshi
|
||||
original:
|
||||
http://github.com/piroor/webextensions-lib-options
|
||||
*/
|
||||
|
||||
class Options {
|
||||
constructor(configs, { steps, onImporting, onImported, onExporting, onExported } = {}) {
|
||||
this.configs = configs;
|
||||
this.steps = steps || {};
|
||||
this.$onImporting = onImporting;
|
||||
this.$onImported = onImported;
|
||||
this.$onExporting = onExporting;
|
||||
this.$onExported = onExported;
|
||||
this.uiNodes = new Map();
|
||||
this.throttleTimers = new Map();
|
||||
|
||||
this.onReady = this.onReady.bind(this);
|
||||
this.onConfigChanged = this.onConfigChanged.bind(this)
|
||||
document.addEventListener('DOMContentLoaded', this.onReady);
|
||||
}
|
||||
|
||||
findUIsForKey(key) {
|
||||
key = this._sanitizeForSelector(key);
|
||||
if (!key)
|
||||
return [];
|
||||
return document.querySelectorAll(`[name="${key}"], #${key}, [data-config-key="${key}"]`);
|
||||
}
|
||||
|
||||
detectUIType(node) {
|
||||
if (!node)
|
||||
return this.UI_MISSING;
|
||||
|
||||
if (node.localName == 'textarea')
|
||||
return this.UI_TYPE_TEXT_FIELD;
|
||||
|
||||
if (node.localName == 'select')
|
||||
return this.UI_TYPE_SELECTBOX;
|
||||
|
||||
if (node.localName != 'input')
|
||||
return this.UI_TYPE_UNKNOWN;
|
||||
|
||||
switch (node.type) {
|
||||
case 'text':
|
||||
case 'password':
|
||||
case 'number':
|
||||
case 'color':
|
||||
return this.UI_TYPE_TEXT_FIELD;
|
||||
|
||||
case 'checkbox':
|
||||
return this.UI_TYPE_CHECKBOX;
|
||||
|
||||
case 'radio':
|
||||
return this.UI_TYPE_RADIO;
|
||||
|
||||
default:
|
||||
return this.UI_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
throttledUpdate(key, uiNode, value) {
|
||||
if (this.throttleTimers.has(key))
|
||||
clearTimeout(this.throttleTimers.get(key));
|
||||
uiNode.dataset.configValueUpdating = true;
|
||||
this.throttleTimers.set(key, setTimeout(() => {
|
||||
this.throttleTimers.delete(key);
|
||||
this.configs[key] = this.UIValueToConfigValue(key, value);
|
||||
setTimeout(() => {
|
||||
uiNode.dataset.configValueUpdating = false;
|
||||
}, 50);
|
||||
}, 250));
|
||||
}
|
||||
|
||||
UIValueToConfigValue(key, value) {
|
||||
switch (typeof this.configs.$default[key]) {
|
||||
case 'string':
|
||||
return String(value);
|
||||
|
||||
case 'number':
|
||||
return Number(value);
|
||||
|
||||
case 'boolean':
|
||||
if (typeof value == 'string')
|
||||
return value != 'false';
|
||||
else
|
||||
return Boolean(value);
|
||||
|
||||
default: // object
|
||||
if (typeof value == 'string')
|
||||
return JSON.parse(value || 'null');
|
||||
else
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
configValueToUIValue(value) {
|
||||
if (typeof value == 'object') {
|
||||
value = JSON.stringify(value);
|
||||
if (value == 'null')
|
||||
value = '';
|
||||
return value;
|
||||
}
|
||||
else
|
||||
return value;
|
||||
}
|
||||
|
||||
applyLocked(node, key) {
|
||||
const locked = this.configs.$isLocked(key);
|
||||
node.disabled = locked;
|
||||
const selector = node.id && `label[for="${this._sanitizeForSelector(node.id)}"]`;
|
||||
const label = node.closest('label') || (node.id && node.ownerDocument.querySelector(selector)) || node;
|
||||
if (label)
|
||||
label.classList.toggle('locked', locked);
|
||||
}
|
||||
|
||||
bindToCheckbox(key, node) {
|
||||
node.checked = this.configValueToUIValue(this.configs[key]);
|
||||
node.addEventListener('change', () => {
|
||||
this.throttledUpdate(key, node, node.checked);
|
||||
});
|
||||
this.applyLocked(node, key);
|
||||
this.addResetMethod(key, node);
|
||||
const nodes = this.uiNodes.get(key) || [];
|
||||
nodes.push(node);
|
||||
this.uiNodes.set(key, nodes);
|
||||
}
|
||||
bindToRadio(key, node) {
|
||||
const group = node.getAttribute('name');
|
||||
const radios = document.querySelectorAll(`input[name="${this._sanitizeForSelector(group)}"]`);
|
||||
let activated = false;
|
||||
for (const radio of radios) {
|
||||
const nodes = this.uiNodes.get(key) || [];
|
||||
if (nodes.includes(radio))
|
||||
continue;
|
||||
this.applyLocked(radio, key);
|
||||
nodes.push(radio);
|
||||
this.uiNodes.set(key, nodes);
|
||||
radio.addEventListener('change', () => {
|
||||
if (!activated)
|
||||
return;
|
||||
const stringifiedValue = this.configs[key];
|
||||
if (stringifiedValue != radio.value)
|
||||
this.throttledUpdate(key, radio, radio.value);
|
||||
});
|
||||
}
|
||||
const selector = `input[type="radio"][value=${JSON.stringify(String(this.configs[key]))}]`;
|
||||
const chosens = (this.uiNodes.get(key) || []).filter(node => node.matches(selector));
|
||||
if (chosens && chosens.length > 0)
|
||||
chosens.map(chosen => { chosen.checked = true; });
|
||||
setTimeout(() => {
|
||||
activated = true;
|
||||
}, 0);
|
||||
}
|
||||
bindToTextField(key, node) {
|
||||
node.value = this.configValueToUIValue(this.configs[key]);
|
||||
node.addEventListener('input', () => {
|
||||
this.throttledUpdate(key, node, node.value);
|
||||
});
|
||||
this.applyLocked(node, key);
|
||||
this.addResetMethod(key, node);
|
||||
const nodes = this.uiNodes.get(key) || [];
|
||||
nodes.push(node);
|
||||
this.uiNodes.set(key, nodes);
|
||||
}
|
||||
bindToSelectBox(key, node) {
|
||||
node.value = this.configValueToUIValue(this.configs[key]);
|
||||
node.addEventListener('change', () => {
|
||||
this.throttledUpdate(key, node, node.value);
|
||||
});
|
||||
this.applyLocked(node, key);
|
||||
this.addResetMethod(key, node);
|
||||
const nodes = this.uiNodes.get(key) || [];
|
||||
nodes.push(node);
|
||||
this.uiNodes.set(key, nodes);
|
||||
}
|
||||
addResetMethod(key, node) {
|
||||
node.$reset = () => {
|
||||
this.configs.$reset(key);
|
||||
const value = this.configs.$default[key];
|
||||
if (this.detectUIType(node) == this.UI_TYPE_CHECKBOX)
|
||||
node.checked = value;
|
||||
else
|
||||
node.value = value;
|
||||
};
|
||||
}
|
||||
|
||||
async onReady() {
|
||||
document.removeEventListener('DOMContentLoaded', this.onReady);
|
||||
|
||||
if (!this.configs || !this.configs.$loaded)
|
||||
throw new Error('you must give configs!');
|
||||
|
||||
this.configs.$addObserver(this.onConfigChanged);
|
||||
await this.configs.$loaded;
|
||||
for (const key of Object.keys(this.configs.$default)) {
|
||||
const nodes = this.findUIsForKey(key);
|
||||
if (!nodes.length)
|
||||
continue;
|
||||
for (const node of nodes) {
|
||||
switch (this.detectUIType(node)) {
|
||||
case this.UI_TYPE_CHECKBOX:
|
||||
this.bindToCheckbox(key, node);
|
||||
break;
|
||||
|
||||
case this.UI_TYPE_RADIO:
|
||||
this.bindToRadio(key, node);
|
||||
break;
|
||||
|
||||
case this.UI_TYPE_TEXT_FIELD:
|
||||
this.bindToTextField(key, node);
|
||||
break;
|
||||
|
||||
case this.UI_TYPE_SELECTBOX:
|
||||
this.bindToSelectBox(key, node);
|
||||
break;
|
||||
|
||||
case this.UI_MISSING:
|
||||
continue;
|
||||
|
||||
default:
|
||||
throw new Error(`unknown type UI element for ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConfigChanged(key) {
|
||||
const nodes = this.uiNodes.get(key);
|
||||
if (!nodes || !nodes.length)
|
||||
return;
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.dataset.configValueUpdating == 'true')
|
||||
continue;
|
||||
if (node.matches('input[type="radio"]')) {
|
||||
node.checked = this.configs[key] == node.value;
|
||||
}
|
||||
else if (node.matches('input[type="checkbox"]')) {
|
||||
node.checked = !!this.configs[key];
|
||||
}
|
||||
else {
|
||||
node.value = this.configValueToUIValue(this.configs[key]);
|
||||
}
|
||||
this.applyLocked(node, key);
|
||||
}
|
||||
}
|
||||
|
||||
buildUIForAllConfigs(parent) {
|
||||
parent = parent || document.body;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(parent);
|
||||
range.collapse(false);
|
||||
const rows = [];
|
||||
for (const key of Object.keys(this.configs.$default).sort()) {
|
||||
const value = this.configs.$default[key];
|
||||
const type = typeof value == 'number' ? 'number' :
|
||||
typeof value == 'boolean' ? 'checkbox' :
|
||||
'text' ;
|
||||
// To accept decimal values like "1.1", we need to set "step" with decmimal values.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
|
||||
const step = type != 'number' ? '' : `step="${this.sanitizeForHTMLText(key in this.steps ? this.steps[key] : String(1.75).replace(/[1-9]/g, '0').replace(/0$/, '1'))}"`;
|
||||
const placeholder = type == 'checkbox' ? '' : `placeholder=${JSON.stringify(this.sanitizeForHTMLText(String(value)))}`;
|
||||
rows.push(`
|
||||
<tr ${rows.length > 0 ? 'style="border-top: 1px solid rgba(0, 0, 0, 0.2);"' : ''}>
|
||||
<td style="width: 45%; word-break: break-all;">
|
||||
<label for="allconfigs-field-${this.sanitizeForHTMLText(key)}">${this.sanitizeForHTMLText(key)}</label>
|
||||
</td>
|
||||
<td style="width: 35%;">
|
||||
<input id="allconfigs-field-${this.sanitizeForHTMLText(key)}"
|
||||
type="${type}"
|
||||
${type != 'checkbox' && type != 'radio' ? 'style="width: 100%;"' : ''}
|
||||
${step}
|
||||
${placeholder}>
|
||||
</td>
|
||||
<td>
|
||||
<button id="allconfigs-reset-${this.sanitizeForHTMLText(key)}">Reset</button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
const fragment = range.createContextualFragment(`
|
||||
<table id="allconfigs-table"
|
||||
style="border-collapse: collapse">
|
||||
<tbody>${rows.join('')}</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button id="allconfigs-reset-all">Reset All</button>
|
||||
<button id="allconfigs-export">Export</button>
|
||||
<a id="allconfigs-export-file"
|
||||
type="application/json"
|
||||
download="configs-${browser.runtime.id}.json"
|
||||
style="display:none"></a>
|
||||
<button id="allconfigs-import">Import</button>
|
||||
<input id="allconfigs-import-file"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
style="display:none">
|
||||
</div>
|
||||
`);
|
||||
range.insertNode(fragment);
|
||||
range.detach();
|
||||
const table = document.getElementById('allconfigs-table');
|
||||
for (const input of table.querySelectorAll('input')) {
|
||||
const key = input.id.replace(/^allconfigs-field-/, '');
|
||||
switch (this.detectUIType(input)) {
|
||||
case this.UI_TYPE_CHECKBOX:
|
||||
this.bindToCheckbox(key, input);
|
||||
break;
|
||||
|
||||
case this.UI_TYPE_TEXT_FIELD:
|
||||
this.bindToTextField(key, input);
|
||||
break;
|
||||
}
|
||||
const button = table.querySelector(`#allconfigs-reset-${this._sanitizeForSelector(key)}`);
|
||||
button.addEventListener('click', () => {
|
||||
input.$reset();
|
||||
});
|
||||
button.addEventListener('keyup', event => {
|
||||
if (event.key == 'Enter' || event.key == ' ')
|
||||
input.$reset();
|
||||
});
|
||||
}
|
||||
const resetAllButton = document.getElementById('allconfigs-reset-all');
|
||||
resetAllButton.addEventListener('keydown', event => {
|
||||
if (event.key == 'Enter' || event.key == ' ')
|
||||
this.resetAll();
|
||||
});
|
||||
resetAllButton.addEventListener('click', event => {
|
||||
if (event.button == 0)
|
||||
this.resetAll();
|
||||
});
|
||||
const exportButton = document.getElementById('allconfigs-export');
|
||||
exportButton.addEventListener('keydown', event => {
|
||||
if (event.key == 'Enter' || event.key == ' ')
|
||||
this.exportToFile();
|
||||
});
|
||||
exportButton.addEventListener('click', event => {
|
||||
if (event.button == 0)
|
||||
this.exportToFile();
|
||||
});
|
||||
const importButton = document.getElementById('allconfigs-import');
|
||||
importButton.addEventListener('keydown', event => {
|
||||
if (event.key == 'Enter' || event.key == ' ')
|
||||
this.importFromFile();
|
||||
});
|
||||
importButton.addEventListener('click', event => {
|
||||
if (event.button == 0)
|
||||
this.importFromFile();
|
||||
});
|
||||
const fileField = document.getElementById('allconfigs-import-file');
|
||||
fileField.addEventListener('change', async _event => {
|
||||
const text = await fileField.files.item(0).text();
|
||||
let values = JSON.parse(text);
|
||||
if (typeof this.$onImporting == 'function')
|
||||
values = await this.$onImporting(values);
|
||||
for (const key of Object.keys(this.configs.$default)) {
|
||||
const value = values[key] !== undefined ? values[key] : this.configs.$default[key];
|
||||
const changed = value != this.configs[key];
|
||||
this.configs[key] = value;
|
||||
if (changed)
|
||||
this.onConfigChanged(key);
|
||||
}
|
||||
if (typeof this.$onImported == 'function')
|
||||
await this.$onImported();
|
||||
});
|
||||
}
|
||||
sanitizeForHTMLText(text) {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
resetAll() {
|
||||
this.configs.$reset();
|
||||
}
|
||||
|
||||
importFromFile() {
|
||||
document.getElementById('allconfigs-import-file').click();
|
||||
}
|
||||
|
||||
async exportToFile() {
|
||||
let values = {};
|
||||
for (const key of Object.keys(this.configs.$default).sort()) {
|
||||
const defaultValue = JSON.stringify(this.configs.$default[key]);
|
||||
const currentValue = JSON.stringify(this.configs[key]);
|
||||
if (defaultValue !== currentValue) {
|
||||
values[key] = this.configs[key];
|
||||
}
|
||||
}
|
||||
if (typeof this.$onExporting == 'function')
|
||||
values = await this.$onExporting(values);
|
||||
// Pretty print the exported JSON, because some major addons
|
||||
// including Stylus and uBlock do that.
|
||||
const exported = JSON.stringify(values, null, 2);
|
||||
const browserInfo = browser.runtime.getBrowserInfo && await browser.runtime.getBrowserInfo();
|
||||
switch (browserInfo && browserInfo.name) {
|
||||
case 'Thunderbird':
|
||||
window.open(`data:application/json,${encodeURIComponent(exported)}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
const link = document.getElementById('allconfigs-export-file');
|
||||
link.href = URL.createObjectURL(new Blob([exported], { type: 'application/json' }));
|
||||
link.click();
|
||||
break;
|
||||
}
|
||||
if (typeof this.$onExported == 'function')
|
||||
await this.$onExported();
|
||||
}
|
||||
|
||||
_sanitizeForSelector(string) {
|
||||
return string.replace(/[:\[\]()]/g, '\\$&');
|
||||
}
|
||||
};
|
||||
|
||||
Options.prototype.UI_TYPE_UNKNOWN = 0;
|
||||
Options.prototype.UI_TYPE_TEXT_FIELD = 1 << 0;
|
||||
Options.prototype.UI_TYPE_CHECKBOX = 1 << 1;
|
||||
Options.prototype.UI_TYPE_RADIO = 1 << 2;
|
||||
Options.prototype.UI_TYPE_SELECTBOX = 1 << 3;
|
||||
|
||||
export default Options;
|
||||
1621
waterfox/browser/components/sidebar/extlib/RichConfirm.js
Normal file
1621
waterfox/browser/components/sidebar/extlib/RichConfirm.js
Normal file
File diff suppressed because it is too large
Load Diff
677
waterfox/browser/components/sidebar/extlib/TabFavIconHelper.js
Normal file
677
waterfox/browser/components/sidebar/extlib/TabFavIconHelper.js
Normal file
@@ -0,0 +1,677 @@
|
||||
/*
|
||||
# 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/.
|
||||
*/
|
||||
/*
|
||||
original:
|
||||
http://github.com/piroor/webextensions-lib-tab-favicon-helper
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const TabFavIconHelper = {
|
||||
LAST_EFFECTIVE_FAVICON: 'last-effective-favIcon',
|
||||
VALID_FAVICON_PATTERN: /^(about|app|chrome|data|file|ftp|https?|moz-extension|resource):/,
|
||||
DRAWABLE_FAVICON_PATTERN: /^(https?|moz-extension|resource):/,
|
||||
|
||||
// original: chrome://browser/content/aboutlogins/icons/favicon.svg
|
||||
FAVICON_LOCKWISE: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">
|
||||
<defs>
|
||||
<linearGradient id="b" x1="24.684" y1="5.756" x2="6.966" y2="26.663" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#ff9640"/>
|
||||
<stop offset=".6" stop-color="#fc4055"/>
|
||||
<stop offset="1" stop-color="#e31587"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="26.362" y1="4.459" x2="10.705" y2="21.897" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff36e" stop-opacity=".8"/>
|
||||
<stop offset=".094" stop-color="#fff36e" stop-opacity=".699"/>
|
||||
<stop offset=".752" stop-color="#fff36e" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" x1="7.175" y1="27.275" x2="23.418" y2="11.454" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#0090ed"/>
|
||||
<stop offset=".386" stop-color="#5b6df8"/>
|
||||
<stop offset=".629" stop-color="#9059ff"/>
|
||||
<stop offset="1" stop-color="#b833e1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="a" x1="29.104" y1="15.901" x2="26.135" y2="21.045" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#722291" stop-opacity=".5"/>
|
||||
<stop offset=".5" stop-color="#722291" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" x1="20.659" y1="21.192" x2="13.347" y2="28.399" xlink:href="#a"/>
|
||||
<linearGradient id="f" x1="2.97" y1="19.36" x2="10.224" y2="21.105" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#054096" stop-opacity=".5"/>
|
||||
<stop offset=".054" stop-color="#0f3d9c" stop-opacity=".441"/>
|
||||
<stop offset=".261" stop-color="#2f35b1" stop-opacity=".249"/>
|
||||
<stop offset=".466" stop-color="#462fbf" stop-opacity=".111"/>
|
||||
<stop offset=".669" stop-color="#542bc8" stop-opacity=".028"/>
|
||||
<stop offset=".864" stop-color="#592acb" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M15.986 31.076A5.635 5.635 0 0 1 12.3 29.76 103.855 103.855 0 0 1 2.249 19.7a5.841 5.841 0 0 1-.006-7.4A103.792 103.792 0 0 1 12.3 2.247a5.837 5.837 0 0 1 7.4-.006A104.1 104.1 0 0 1 29.751 12.3a5.842 5.842 0 0 1 .006 7.405c-.423.484-.584.661-.917 1.025l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 100.308 100.308 0 0 0-9.7-9.714 2.352 2.352 0 0 0-2.814.007 100.323 100.323 0 0 0-9.7 9.7 2.354 2.354 0 0 0 .006 2.815 100.375 100.375 0 0 0 9.7 9.708 2.352 2.352 0 0 0 2.815-.006c1.311-1.145 2.326-2.031 3.434-3.086l-3.4-3.609a2.834 2.834 0 0 1-.776-2.008 2.675 2.675 0 0 1 .834-1.9l.194-.184a2.493 2.493 0 0 0 1.124-2.333 2.81 2.81 0 0 0-5.6 0 2.559 2.559 0 0 0 1.092 2.279l.127.118a2.735 2.735 0 0 1 .324 3.7 1.846 1.846 0 0 1-.253.262l-1.578 1.326a1.75 1.75 0 0 1-2.252-2.68l.755-.634a5.758 5.758 0 0 1-1.715-4.366 6.305 6.305 0 0 1 12.6 0 5.642 5.642 0 0 1-1.854 4.528l3.84 4.082a2.071 2.071 0 0 1 .59 1.446 2.128 2.128 0 0 1-.628 1.526c-1.6 1.592-2.895 2.72-4.533 4.15a5.745 5.745 0 0 1-3.753 1.354z" fill="url(#b)"/>
|
||||
<path d="M15.986 31.076A5.635 5.635 0 0 1 12.3 29.76 103.855 103.855 0 0 1 2.249 19.7a5.841 5.841 0 0 1-.006-7.4A103.792 103.792 0 0 1 12.3 2.247a5.837 5.837 0 0 1 7.4-.006A104.1 104.1 0 0 1 29.751 12.3a5.842 5.842 0 0 1 .006 7.405c-.423.484-.584.661-.917 1.025l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 100.308 100.308 0 0 0-9.7-9.714 2.352 2.352 0 0 0-2.814.007 100.323 100.323 0 0 0-9.7 9.7 2.354 2.354 0 0 0 .006 2.815 100.375 100.375 0 0 0 9.7 9.708 2.352 2.352 0 0 0 2.815-.006c1.311-1.145 2.326-2.031 3.434-3.086l-3.4-3.609a2.834 2.834 0 0 1-.776-2.008 2.675 2.675 0 0 1 .834-1.9l.194-.184a2.493 2.493 0 0 0 1.124-2.333 2.81 2.81 0 0 0-5.6 0 2.559 2.559 0 0 0 1.092 2.279l.127.118a2.735 2.735 0 0 1 .324 3.7 1.846 1.846 0 0 1-.253.262l-1.578 1.326a1.75 1.75 0 0 1-2.252-2.68l.755-.634a5.758 5.758 0 0 1-1.715-4.366 6.305 6.305 0 0 1 12.6 0 5.642 5.642 0 0 1-1.854 4.528l3.84 4.082a2.071 2.071 0 0 1 .59 1.446 2.128 2.128 0 0 1-.628 1.526c-1.6 1.592-2.895 2.72-4.533 4.15a5.745 5.745 0 0 1-3.753 1.354z" fill="url(#c)"/>
|
||||
<path d="M29.58 17.75a34.267 34.267 0 0 0-2.473-3.15 2.352 2.352 0 0 1 .008 2.814c-.393.45-.546.617-.86.961l-.234.258a1.749 1.749 0 1 0 2.585 2.357l.234-.255c.231-.253.379-.415.59-.653a2.161 2.161 0 0 0 .15-2.332zm-8.734 6.275c-1.108 1.055-2.123 1.941-3.434 3.086a2.352 2.352 0 0 1-2.815.006 100.375 100.375 0 0 1-9.7-9.708 2.354 2.354 0 0 1-.006-2.815s-2.131 1.984-2.4 3.424a2.956 2.956 0 0 0 .724 2.782 103.772 103.772 0 0 0 9.085 8.96 5.635 5.635 0 0 0 3.683 1.316 5.745 5.745 0 0 0 3.753-1.351c.926-.808 1.741-1.52 2.565-2.273a1.558 1.558 0 0 0 0-1.476 11.55 11.55 0 0 0-1.455-1.951z" fill="url(#d)"/>
|
||||
<path d="M29.43 20.079c-.211.238-.359.4-.59.653l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 34.267 34.267 0 0 1 2.473 3.153 2.161 2.161 0 0 1-.15 2.329z" fill="url(#a)"/>
|
||||
<path d="M22.3 25.976a11.55 11.55 0 0 0-1.458-1.951c-1.108 1.055-2.123 1.941-3.434 3.086a2.352 2.352 0 0 1-2.815.006c-1.414-1.234-2.768-2.5-4.095-3.8L8.023 25.8c1.384 1.35 2.8 2.668 4.28 3.958a5.635 5.635 0 0 0 3.683 1.316 5.745 5.745 0 0 0 3.753-1.351c.926-.808 1.741-1.52 2.565-2.273a1.558 1.558 0 0 0-.004-1.474z" fill="url(#e)"/>
|
||||
<path d="M4.892 17.409a2.354 2.354 0 0 1-.006-2.815s-2.131 1.984-2.4 3.424a2.956 2.956 0 0 0 .729 2.782c1.56 1.739 3.158 3.4 4.808 5.007l2.479-2.48c-1.935-1.891-3.802-3.844-5.61-5.918z" opacity=".9" fill="url(#f)"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://browser/content/robot.ico
|
||||
FAVICON_ROBOT: `
|
||||
data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACGFjVEwAAAASAAAAAJNtBPIAAAAaZmNUTAAAAAAAAAAQAAAAEAAAAAAAAAAALuAD6AABhIDeugAAALhJREFUOI2Nk8sNxCAMRDlGohauXFOMpfTiAlxICqAELltHLqlgctg1InzMRhpFAc+LGWTnmoeZYamt78zXdZmaQtQMADlnU0OIAlbmJUBEcO4bRKQY2rUXIPmAGnDuG/Bx3/fvOPVaDUg+oAPUf1PArIMCSD5glMEsUGaG+kyAFWIBaCsKuA+HGCNijLgP133XgOEtaPFMy2vUolEGJoCIzBmoRUR9+7rxj16DZaW/mgtmxnJ8V3oAnApQwNS5zpcAAAAaZmNUTAAAAAEAAAAQAAAAEAAAAAAAAAAAAB4D6AIB52fclgAAACpmZEFUAAAAAjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9WF+u8QAAABpmY1RMAAAAAwAAABAAAAAQAAAAAAAAAAAAHgPoAgEK8Q9/AAAAFmZkQVQAAAAEOI1jYBgFo2AUjAIIAAAEEAAB0xIn4wAAABpmY1RMAAAABQAAABAAAAAQAAAAAAAAAAAAHgPoAgHnO30FAAAAQGZkQVQAAAAGOI1jYBieYKcaw39ixHCC/6cwFWMTw2rz/1MM/6Vu/f///xTD/51qEIwuRjsXILuEGLFRMApgAADhNCsVfozYcAAAABpmY1RMAAAABwAAABAAAAAQAAAAAAAAAAAAHgPoAgEKra7sAAAAFmZkQVQAAAAIOI1jYBgFo2AUjAIIAAAEEAABM9s3hAAAABpmY1RMAAAACQAAABAAAAAQAAAAAAAAAAAAHgPoAgHn3p+wAAAAKmZkQVQAAAAKOI1jYBgFWEFzc/N/YsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF/1BhPl6AAAAGmZjVEwAAAALAAAAEAAAABAAAAAAAAAAAAAeA+gCAQpITFkAAAAWZmRBVAAAAAw4jWNgGAWjYBSMAggAAAQQAAHaszpmAAAAGmZjVEwAAAANAAAAEAAAABAAAAAAAAAAAAAeA+gCAeeCPiMAAABAZmRBVAAAAA44jWNgGJ5gpxrDf2LEcIL/pzAVYxPDavP/Uwz/pW79////FMP/nWoQjC5GOxcgu4QYsVEwCmAAAOE0KxUmBL0KAAAAGmZjVEwAAAAPAAAAEAAAABAAAAAAAAAAAAAeA+gCAQoU7coAAAAWZmRBVAAAABA4jWNgGAWjYBSMAggAAAQQAAEpOBELAAAAGmZjVEwAAAARAAAAEAAAABAAAAAAAAAAAAAeA+gCAeYVWtoAAAAqZmRBVAAAABI4jWNgGAVYQXNz839ixHBq3qnG8B9ZAzYx2rlgFIwCcgAA8psX/WvpAecAAAAaZmNUTAAAABMAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC4OJMwAAABZmZEFUAAAAFDiNY2AYBaNgFIwCCAAABBAAAcBQHOkAAAAaZmNUTAAAABUAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5kn7SQAAAEBmZEFUAAAAFjiNY2AYnmCnGsN/YsRwgv+nMBVjE8Nq8/9TDP+lbv3///8Uw/+dahCMLkY7FyC7hBixUTAKYAAA4TQrFc+cEoQAAAAaZmNUTAAAABcAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC98ooAAAABZmZEFUAAAAGDiNY2AYBaNgFIwCCAAABBAAASCZDI4AAAAaZmNUTAAAABkAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5qwZ/AAAACpmZEFUAAAAGjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9cjJWbAAAABpmY1RMAAAAGwAAABAAAAAQAAAAAAAAAAAAHgPoAgELOsoVAAAAFmZkQVQAAAAcOI1jYBgFo2AUjAIIAAAEEAAByfEBbAAAABpmY1RMAAAAHQAAABAAAAAQAAAAAAAAAAAAHgPoAgHm8LhvAAAAQGZkQVQAAAAeOI1jYBieYKcaw39ixHCC/6cwFWMTw2rz/1MM/6Vu/f///xTD/51qEIwuRjsXILuEGLFRMApgAADhNCsVlxR3/gAAABpmY1RMAAAAHwAAABAAAAAQAAAAAAAAAAAAHgPoAgELZmuGAAAAFmZkQVQAAAAgOI1jYBgFo2AUjAIIAAAEEAABHP5cFQAAABpmY1RMAAAAIQAAABAAAAAQAAAAAAAAAAAAHgPoAgHlgtAOAAAAKmZkQVQAAAAiOI1jYBgFWEFzc/N/YsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF/0/MvDdAAAAAElFTkSuQmCC
|
||||
`.trim(),
|
||||
// original: chrome://browser/skin/controlcenter/dashboard.svg
|
||||
FAVICON_DASHBOARD: `
|
||||
<svg data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 12H4a2 2 0 0 1-2-2V3a1 1 0 0 0-2 0v7a4 4 0 0 0 4 4h11a1 1 0 0 0 0-2z"/>
|
||||
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M4 11a1 1 0 0 0 1-1V8a1 1 0 0 0-2 0v2a1 1 0 0 0 1 1zM7 11a1 1 0 0 0 1-1V4a1 1 0 0 0-2 0v6a1 1 0 0 0 1 1zM10 11a1 1 0 0 0 1-1V6a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1zM13 11a1 1 0 0 0 1-1V7a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://browser/skin/developer.svg
|
||||
FAVICON_DEVELOPER: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.555 3.2l-2.434 2.436a1.243 1.243 0 1 1-1.757-1.757L12.8 1.445A3.956 3.956 0 0 0 11 1a3.976 3.976 0 0 0-3.434 6.02l-6.273 6.273a1 1 0 1 0 1.414 1.414L8.98 8.434A3.96 3.96 0 0 0 11 9a4 4 0 0 0 4-4 3.956 3.956 0 0 0-.445-1.8z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://browser/skin/privatebrowsing/favicon.svg
|
||||
FAVICON_PRIVATE_BROWSING: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<circle cx="8" cy="8" r="8" fill="#8d20ae"/>
|
||||
<circle cx="8" cy="8" r="7.5" stroke="#7b149a" stroke-width="1" fill="none"/>
|
||||
<path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" stroke="#670c83" stroke-width="2" fill="none"/>
|
||||
<path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" fill="#fff"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://browser/skin/settings.svg
|
||||
FAVICON_SETTINGS: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 7h-2.1a4.967 4.967 0 0 0-.732-1.753l1.49-1.49a1 1 0 0 0-1.414-1.414l-1.49 1.49A4.968 4.968 0 0 0 9 3.1V1a1 1 0 0 0-2 0v2.1a4.968 4.968 0 0 0-1.753.732l-1.49-1.49a1 1 0 0 0-1.414 1.415l1.49 1.49A4.967 4.967 0 0 0 3.1 7H1a1 1 0 0 0 0 2h2.1a4.968 4.968 0 0 0 .737 1.763c-.014.013-.032.017-.045.03l-1.45 1.45a1 1 0 1 0 1.414 1.414l1.45-1.45c.013-.013.018-.031.03-.045A4.968 4.968 0 0 0 7 12.9V15a1 1 0 0 0 2 0v-2.1a4.968 4.968 0 0 0 1.753-.732l1.49 1.49a1 1 0 0 0 1.414-1.414l-1.49-1.49A4.967 4.967 0 0 0 12.9 9H15a1 1 0 0 0 0-2zM5 8a3 3 0 1 1 3 3 3 3 0 0 1-3-3z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://browser/skin/window.svg
|
||||
FAVICON_WINDOW: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13 1H3a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h11a2 2 0 0 0 2-2V4a3 3 0 0 0-3-3zm1 11a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h12zm0-7H2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://devtools/skin/images/profiler-stopwatch.svg
|
||||
FAVICON_PROFILER: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M10.85 6.85a.5.5 0 0 0-.7-.7l-2.5 2.5.7.7 2.5-2.5zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 1a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1zM8 4a5 5 0 1 0 0 10A5 5 0 0 0 8 4zM1 9a7 7 0 1 1 14 0A7 7 0 0 1 1 9z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://global/skin/icons/performance.svg
|
||||
FAVICON_PERFORMANCE: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" d="M8 1a8 8 0 0 0-8 8 7.89 7.89 0 0 0 .78 3.43 1 1 0 0 0 .9.57.94.94 0 0 0 .43-.1 1 1 0 0 0 .47-1.33A6.07 6.07 0 0 1 2 9a6 6 0 0 1 12 0 5.93 5.93 0 0 1-.59 2.57 1 1 0 0 0 1.81.86A7.89 7.89 0 0 0 16 9a8 8 0 0 0-8-8z"/>
|
||||
<path fill="context-fill" d="M11.77 7.08a.5.5 0 0 0-.69.15L8.62 11.1A2.12 2.12 0 0 0 8 11a2 2 0 0 0 0 4 2.05 2.05 0 0 0 1.12-.34 2.31 2.31 0 0 0 .54-.54 2 2 0 0 0 0-2.24 2.31 2.31 0 0 0-.2-.24l2.46-3.87a.5.5 0 0 0-.15-.69z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://global/skin/icons/warning.svg
|
||||
FAVICON_WARNING: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.742 12.106L9.789 2.2a2 2 0 0 0-3.578 0l-4.953 9.91A2 2 0 0 0 3.047 15h9.905a2 2 0 0 0 1.79-2.894zM7 5a1 1 0 0 1 2 0v4a1 1 0 0 1-2 0zm1 8.25A1.25 1.25 0 1 1 9.25 12 1.25 1.25 0 0 1 8 13.25z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: chrome://mozapps/skin/extensions/extensionGeneric-16.svg
|
||||
FAVICON_EXTENSION: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
|
||||
<path d="M14.5 8c-.971 0-1 1-1.75 1a.765.765 0 0 1-.75-.75V5a1 1 0 0 0-1-1H7.75A.765.765 0 0 1 7 3.25c0-.75 1-.779 1-1.75C8 .635 7.1 0 6 0S4 .635 4 1.5c0 .971 1 1 1 1.75a.765.765 0 0 1-.75.75H1a1 1 0 0 0-1 1v2.25A.765.765 0 0 0 .75 8c.75 0 .779-1 1.75-1C3.365 7 4 7.9 4 9s-.635 2-1.5 2c-.971 0-1-1-1.75-1a.765.765 0 0 0-.75.75V15a1 1 0 0 0 1 1h3.25a.765.765 0 0 0 .75-.75c0-.75-1-.779-1-1.75 0-.865.9-1.5 2-1.5s2 .635 2 1.5c0 .971-1 1-1 1.75a.765.765 0 0 0 .75.75H11a1 1 0 0 0 1-1v-3.25a.765.765 0 0 1 .75-.75c.75 0 .779 1 1.75 1 .865 0 1.5-.9 1.5-2s-.635-2-1.5-2z"/>
|
||||
</svg>
|
||||
`,
|
||||
// original: globe-16.svg
|
||||
FAVICON_GLOBE: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm5.163 4.958h-1.552a7.7 7.7 0 0 0-1.051-2.376 6.03 6.03 0 0 1 2.603 2.376zM14 8a5.963 5.963 0 0 1-.335 1.958h-1.821A12.327 12.327 0 0 0 12 8a12.327 12.327 0 0 0-.156-1.958h1.821A5.963 5.963 0 0 1 14 8zm-6 6c-1.075 0-2.037-1.2-2.567-2.958h5.135C10.037 12.8 9.075 14 8 14zM5.174 9.958a11.084 11.084 0 0 1 0-3.916h5.651A11.114 11.114 0 0 1 11 8a11.114 11.114 0 0 1-.174 1.958zM2 8a5.963 5.963 0 0 1 .335-1.958h1.821a12.361 12.361 0 0 0 0 3.916H2.335A5.963 5.963 0 0 1 2 8zm6-6c1.075 0 2.037 1.2 2.567 2.958H5.433C5.963 3.2 6.925 2 8 2zm-2.56.582a7.7 7.7 0 0 0-1.051 2.376H2.837A6.03 6.03 0 0 1 5.44 2.582zm-2.6 8.46h1.549a7.7 7.7 0 0 0 1.051 2.376 6.03 6.03 0 0 1-2.603-2.376zm7.723 2.376a7.7 7.7 0 0 0 1.051-2.376h1.552a6.03 6.03 0 0 1-2.606 2.376z"/>
|
||||
</svg>
|
||||
`,
|
||||
|
||||
async _urlToKey(url) { // sha1 hash
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(url);
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hashHex;
|
||||
},
|
||||
|
||||
DB_NAME: 'TabFavIconHelper',
|
||||
DB_VERSION: 2,
|
||||
STORE_FAVICONS: 'favIcons',
|
||||
STORE_EFFECTIVE_FAVICONS: 'effectiveFavIcons',
|
||||
STORE_UNEFFECTIVE_FAVICONS: 'uneffectiveFavIcons',
|
||||
EXPIRATION_TIME_IN_MSEC: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
|
||||
async _openDB() {
|
||||
if (this._openedDB)
|
||||
return this._openedDB;
|
||||
return new Promise((resolve, _reject) => {
|
||||
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
|
||||
|
||||
request.onerror = () => {
|
||||
// This can fail if this is in a private window.
|
||||
// See: https://github.com/piroor/treestyletab/issues/3387
|
||||
//reject(new Error('Failed to open database'));
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
this._openedDB = db;
|
||||
resolve(db);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
const objectStores = db.objectStoreNames;
|
||||
|
||||
const needToUpgrade = event.oldVersion < this.DB_VERSION;
|
||||
if (needToUpgrade) {
|
||||
if (objectStores.contains(this.STORE_FAVICONS))
|
||||
db.deleteObjectStore(this.STORE_FAVICONS);
|
||||
if (objectStores.contains(this.STORE_EFFECTIVE_FAVICONS))
|
||||
db.deleteObjectStore(this.STORE_EFFECTIVE_FAVICONS);
|
||||
if (objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS))
|
||||
db.deleteObjectStore(this.STORE_UNEFFECTIVE_FAVICONS);
|
||||
}
|
||||
|
||||
if (needToUpgrade ||
|
||||
!objectStores.contains(this.STORE_FAVICONS)) {
|
||||
const favIconsStore = db.createObjectStore(this.STORE_FAVICONS, { keyPath: 'key', unique: true });
|
||||
favIconsStore.createIndex('urlKey', 'urlKey', { unique: false });
|
||||
favIconsStore.createIndex('timestamp', 'timestamp');
|
||||
}
|
||||
|
||||
if (needToUpgrade ||
|
||||
!objectStores.contains(this.STORE_EFFECTIVE_FAVICONS)) {
|
||||
const effectiveFavIconsStore = db.createObjectStore(this.STORE_EFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true });
|
||||
effectiveFavIconsStore.createIndex('timestamp', 'timestamp');
|
||||
effectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false });
|
||||
}
|
||||
|
||||
if (needToUpgrade ||
|
||||
!objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS)) {
|
||||
const uneffectiveFavIconsStore = db.createObjectStore(this.STORE_UNEFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true });
|
||||
uneffectiveFavIconsStore.createIndex('timestamp', 'timestamp');
|
||||
uneffectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
async _associateFavIconUrlToTabUrl({ favIconUrl, tabUrl, store } = {}) {
|
||||
const [db, tabUrlKey, favIconKey] = await Promise.all([
|
||||
this._openDB(),
|
||||
this._urlToKey(tabUrl),
|
||||
this._urlToKey(favIconUrl),
|
||||
]);
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
try {
|
||||
const transaction = db.transaction([store, this.STORE_FAVICONS], 'readwrite');
|
||||
const associationStore = transaction.objectStore(store);
|
||||
const favIconStore = transaction.objectStore(this.STORE_FAVICONS);
|
||||
const timestamp = Date.now();
|
||||
|
||||
const associationRequest = associationStore.put({ urlKey: tabUrlKey, favIconKey, timestamp });
|
||||
const favIconRequest = favIconStore.put({ key: favIconKey, url: favIconUrl, timestamp });
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
this._reserveToExpireOldEntries();
|
||||
favIconUrl = undefined;
|
||||
tabUrl = undefined;
|
||||
store = undefined;
|
||||
};
|
||||
|
||||
associationRequest.onerror = event => {
|
||||
console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event);
|
||||
};
|
||||
|
||||
favIconRequest.onerror = event => {
|
||||
console.error(`Failed to store favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event);
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, error);
|
||||
}
|
||||
},
|
||||
|
||||
async _unassociateFavIconUrlFromTabUrl({ tabUrl, store } = {}) {
|
||||
const [db, tabUrlKey] = await Promise.all([
|
||||
this._openDB(),
|
||||
this._urlToKey(tabUrl),
|
||||
]);
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
try {
|
||||
const transaction = db.transaction([store], 'readwrite');
|
||||
const associationStore = transaction.objectStore(store);
|
||||
const unassociationRequest = associationStore.delete(tabUrlKey);
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
this._reserveToExpireOldEntries();
|
||||
tabUrl = undefined;
|
||||
store = undefined;
|
||||
};
|
||||
|
||||
unassociationRequest.onerror = event => {
|
||||
console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, event);
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, error);
|
||||
}
|
||||
},
|
||||
|
||||
async _getAssociatedFavIconUrlFromTabUrl({ tabUrl, store } = {}) {
|
||||
return new Promise(async (resolve, _reject) => {
|
||||
const [db, tabUrlKey] = await Promise.all([
|
||||
this._openDB(),
|
||||
this._urlToKey(tabUrl),
|
||||
]);
|
||||
if (!db) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transaction = db.transaction([store, this.STORE_FAVICONS], 'readonly');
|
||||
const associationStore = transaction.objectStore(store);
|
||||
const favIconStore = transaction.objectStore(this.STORE_FAVICONS);
|
||||
|
||||
const associationRequest = associationStore.get(tabUrlKey);
|
||||
|
||||
associationRequest.onsuccess = () => {
|
||||
const association = associationRequest.result;
|
||||
if (!association) {
|
||||
//console.log(`No associated favIconUrl for the tabUrl ${tabUrl} in the store ${store}`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const favIconRequest = favIconStore.get(association.favIconKey);
|
||||
|
||||
favIconRequest.onsuccess = () => {
|
||||
let favIcon = favIconRequest.result;
|
||||
if (!favIcon) {
|
||||
//console.log(`FavIcon data not found for the tabUrl ${tabUrl} in the store ${store}`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
if (favIcon.timestamp < Date.now() - this.EXPIRATION_TIME_IN_MSEC) {
|
||||
//console.log(`FavIcon data is expired for the tabUrl ${tabUrl} in the store ${store}`);
|
||||
this._reserveToExpireOldEntries();
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(favIcon.url);
|
||||
favIcon.url = undefined;
|
||||
favIcon = undefined;
|
||||
};
|
||||
|
||||
favIconRequest.onerror = event => {
|
||||
console.error(`Failed to get favIconUrl from tabUrl ${tabUrl}`, event);
|
||||
resolve(null);
|
||||
};
|
||||
};
|
||||
|
||||
associationRequest.onerror = event => {
|
||||
console.error(`Failed to get favIcon association from tabUrl ${tabUrl}`, event);
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
tabUrl = undefined;
|
||||
store = undefined;
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error('Failed to get from cache:', error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async _reserveToExpireOldEntries() {
|
||||
if (this._reservedExpiration)
|
||||
clearTimeout(this._reservedExpiration);
|
||||
this._reservedExpiration = setTimeout(() => {
|
||||
this._reservedExpiration = null;
|
||||
this._expireOldEntries();
|
||||
}, 500);
|
||||
},
|
||||
async _expireOldEntries() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const db = await this._openDB();
|
||||
if (!db) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transaction = db.transaction([this.STORE_FAVICONS, this.STORE_EFFECTIVE_FAVICONS, this.STORE_UNEFFECTIVE_FAVICONS], 'readwrite');
|
||||
const favIconsStore = transaction.objectStore(this.STORE_FAVICONS);
|
||||
const effectiveFavIconsStore = transaction.objectStore(this.STORE_EFFECTIVE_FAVICONS);
|
||||
const uneffectiveFavIconsStore = transaction.objectStore(this.STORE_UNEFFECTIVE_FAVICONS);
|
||||
|
||||
const favIconIndex = favIconsStore.index('timestamp');
|
||||
const effectiveFavIconIndex = effectiveFavIconsStore.index('timestamp');
|
||||
const uneffectiveFavIconIndex = uneffectiveFavIconsStore.index('timestamp');
|
||||
|
||||
const expirationTimestamp = Date.now() - this.EXPIRATION_TIME_IN_MSEC;
|
||||
|
||||
const favIconRequest = favIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
|
||||
favIconRequest.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (!cursor)
|
||||
return;
|
||||
const key = cursor.primaryKey;
|
||||
cursor.continue();
|
||||
const deleteRequest = favIconsStore.delete(key);
|
||||
deleteRequest.onerror = event => {
|
||||
console.error(`Failed to clear favicon index`, event);
|
||||
};
|
||||
};
|
||||
favIconRequest.onerror = event => {
|
||||
console.error(`Failed to retrieve favicon index`, event);
|
||||
};
|
||||
|
||||
const effectiveFavIconRequest = effectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
|
||||
effectiveFavIconRequest.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (!cursor)
|
||||
return;
|
||||
const url = cursor.primaryKey;
|
||||
cursor.continue();
|
||||
const deleteRequest = effectiveFavIconsStore.delete(url);
|
||||
deleteRequest.onerror = event => {
|
||||
console.error(`Failed to clear effective favicon index`, event);
|
||||
};
|
||||
};
|
||||
effectiveFavIconRequest.onerror = event => {
|
||||
console.error(`Failed to retrieve effective favicon index`, event);
|
||||
};
|
||||
|
||||
const uneffectiveFavIconRequest = uneffectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
|
||||
uneffectiveFavIconRequest.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (!cursor)
|
||||
return;
|
||||
const url = cursor.primaryKey;
|
||||
cursor.continue();
|
||||
const deleteRequest = uneffectiveFavIconsStore.delete(url);
|
||||
deleteRequest.onerror = event => {
|
||||
console.error(`Failed to clear uneffective favicon index`, event);
|
||||
};
|
||||
};
|
||||
uneffectiveFavIconRequest.onerror = event => {
|
||||
console.error(`Failed to retrieve uneffective favicon index`, event);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
//db.close();
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
console.error('Failed to expire old entries:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_tasks: [],
|
||||
_processStep: 5,
|
||||
FAVICON_SIZE: 16,
|
||||
|
||||
_init() {
|
||||
this._onTabUpdated = this._onTabUpdated.bind(this);
|
||||
browser.tabs.onUpdated.addListener(this._onTabUpdated);
|
||||
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = this.canvas.height = this.FAVICON_SIZE;
|
||||
this.canvas.setAttribute('style', `
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
position: fixed
|
||||
`);
|
||||
document.body.appendChild(this.canvas);
|
||||
|
||||
window.addEventListener('unload', () => {
|
||||
browser.tabs.onUpdated.removeListener(this._onTabUpdated);
|
||||
}, { once: true });
|
||||
},
|
||||
|
||||
_sessionAPIAvailable: (
|
||||
browser.sessions &&
|
||||
browser.sessions.getTabValue &&
|
||||
browser.sessions.setTabValue &&
|
||||
browser.sessions.removeTabValue
|
||||
),
|
||||
|
||||
_addTask(task) {
|
||||
this._tasks.push(task);
|
||||
this._run();
|
||||
},
|
||||
|
||||
_run() {
|
||||
if (this._running)
|
||||
return;
|
||||
this._running = true;
|
||||
const processOneTask = () => {
|
||||
if (this._tasks.length == 0) {
|
||||
this._running = false;
|
||||
}
|
||||
else {
|
||||
const tasks = this._tasks.splice(0, this._processStep);
|
||||
while (tasks.length > 0) {
|
||||
tasks.shift()();
|
||||
}
|
||||
window.requestAnimationFrame(processOneTask);
|
||||
}
|
||||
};
|
||||
processOneTask();
|
||||
},
|
||||
|
||||
// public
|
||||
loadToImage(params = {}) {
|
||||
this._addTask(() => {
|
||||
this._getEffectiveFavIconURL(params.tab, params.url)
|
||||
.then(url => {
|
||||
params.image.src = url;
|
||||
params.image.classList.remove('error');
|
||||
url = undefined;
|
||||
},
|
||||
_error => {
|
||||
params.image.src = '';
|
||||
params.image.classList.add('error');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// public
|
||||
maybeImageTab(_tab) { // for backward compatibility
|
||||
return false;
|
||||
},
|
||||
|
||||
_getSafeFaviconUrl(url) {
|
||||
switch (url) {
|
||||
case 'chrome://browser/content/aboutlogins/icons/favicon.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_LOCKWISE);
|
||||
case 'chrome://browser/content/robot.ico':
|
||||
return this.FAVICON_ROBOT;
|
||||
case 'chrome://browser/skin/controlcenter/dashboard.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_DASHBOARD);
|
||||
case 'chrome://browser/skin/developer.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_DEVELOPER);
|
||||
case 'chrome://browser/skin/privatebrowsing/favicon.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_PRIVATE_BROWSING);
|
||||
case 'chrome://browser/skin/settings.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_SETTINGS);
|
||||
case 'chrome://browser/skin/window.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_WINDOW);
|
||||
case 'chrome://devtools/skin/images/profiler-stopwatch.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_PROFILER);
|
||||
case 'chrome://global/skin/icons/performance.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_PERFORMANCE);
|
||||
case 'chrome://global/skin/icons/warning.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_WARNING);
|
||||
case 'chrome://mozapps/skin/extensions/extensionGeneric-16.svg':
|
||||
return this._getSVGDataURI(this.FAVICON_EXTENSION);
|
||||
default:
|
||||
if (/^chrome:\/\//.test(url) &&
|
||||
!/^chrome:\/\/branding\//.test(url))
|
||||
return this._getSVGDataURI(this.FAVICON_GLOBE);
|
||||
break;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
_getSVGDataURI(svg) {
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
||||
},
|
||||
|
||||
// public
|
||||
async getLastEffectiveFavIconURL(tab) {
|
||||
if (tab.favIconUrl?.startsWith('data:'))
|
||||
return tab.favIconUrl;
|
||||
|
||||
const uneffectiveFavIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
|
||||
if (uneffectiveFavIconUrl)
|
||||
return null;
|
||||
|
||||
const favIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
|
||||
if (favIconUrl)
|
||||
return favIconUrl;
|
||||
|
||||
if (!this._sessionAPIAvailable)
|
||||
return null;
|
||||
|
||||
const lastData = await browser.sessions.getTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
|
||||
return lastData && lastData.url == tab.url && lastData.favIconUrl;
|
||||
},
|
||||
|
||||
async _getEffectiveFavIconURL(tab, favIconUrl = null) {
|
||||
if (tab.favIconUrl?.startsWith('data:')) {
|
||||
browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
|
||||
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
|
||||
return tab.favIconUrl;
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
favIconUrl = this._getSafeFaviconUrl(favIconUrl || tab.favIconUrl);
|
||||
let storedFavIconUrl;
|
||||
if (!favIconUrl && tab.discarded) {
|
||||
// discarded tab doesn't have favIconUrl, so we should use cached data.
|
||||
storedFavIconUrl = favIconUrl = await this.getLastEffectiveFavIconURL(tab);
|
||||
}
|
||||
|
||||
let loader, onLoad, onError;
|
||||
const clear = (() => {
|
||||
if (loader) {
|
||||
loader.removeEventListener('load', onLoad, { once: true });
|
||||
loader.removeEventListener('error', onError, { once: true });
|
||||
}
|
||||
loader = onLoad = onError = favIconUrl = storedFavIconUrl = undefined;
|
||||
});
|
||||
|
||||
onLoad = async foundFavIconUrl => {
|
||||
let dataURL = null;
|
||||
if (this.DRAWABLE_FAVICON_PATTERN.test(favIconUrl)) {
|
||||
const context = this.canvas.getContext('2d');
|
||||
context.clearRect(0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE);
|
||||
context.drawImage(loader, 0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE);
|
||||
try {
|
||||
dataURL = this.canvas.toDataURL('image/png');
|
||||
}
|
||||
catch(_error) {
|
||||
// it can fail due to security reasons
|
||||
}
|
||||
}
|
||||
const oldFavIconUrl = foundFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
|
||||
if (!oldFavIconUrl ||
|
||||
oldFavIconUrl != favIconUrl) {
|
||||
if (this._sessionAPIAvailable)
|
||||
browser.sessions.setTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON, {
|
||||
url: tab.url,
|
||||
favIconUrl,
|
||||
});
|
||||
}
|
||||
this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_EFFECTIVE_FAVICONS });
|
||||
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
|
||||
resolve(dataURL || favIconUrl);
|
||||
clear();
|
||||
};
|
||||
onError = async error => {
|
||||
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
|
||||
this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS });
|
||||
if (this._sessionAPIAvailable)
|
||||
browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
|
||||
clear();
|
||||
reject(error || new Error('No effective icon'));
|
||||
};
|
||||
storedFavIconUrl = storedFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
|
||||
if (storedFavIconUrl)
|
||||
return onLoad(storedFavIconUrl);
|
||||
if (!favIconUrl ||
|
||||
!this.VALID_FAVICON_PATTERN.test(favIconUrl)) {
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
loader = new Image();
|
||||
if (/^https?:/.test(favIconUrl))
|
||||
loader.crossOrigin = 'anonymous';
|
||||
loader.addEventListener('load', () => onLoad(), { once: true });
|
||||
loader.addEventListener('error', onError, { once: true });
|
||||
try {
|
||||
loader.src = favIconUrl;
|
||||
}
|
||||
catch(error) {
|
||||
this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
|
||||
this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS });
|
||||
onError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onTabUpdated(tabId, changeInfo, _tab) {
|
||||
if (!this._hasFavIconInfo(changeInfo))
|
||||
return;
|
||||
let timer = this._updatingTabs.get(tabId);
|
||||
if (timer)
|
||||
clearTimeout(timer);
|
||||
// Updating of last effective favicon must be done after the loading
|
||||
// of the tab itself is correctly done, to avoid cookie problems on
|
||||
// some websites.
|
||||
// See also: https://github.com/piroor/treestyletab/issues/2064
|
||||
timer = setTimeout(async () => {
|
||||
this._updatingTabs.delete(tabId);
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
if (!tab ||
|
||||
(changeInfo.favIconUrl &&
|
||||
tab.favIconUrl != changeInfo.favIconUrl) ||
|
||||
(changeInfo.url &&
|
||||
tab.url != changeInfo.url) ||
|
||||
!this._hasFavIconInfo(tab))
|
||||
return; // expired
|
||||
|
||||
await this._getEffectiveFavIconURL(
|
||||
tab,
|
||||
changeInfo.favIconUrl
|
||||
).catch(_error => {});
|
||||
}, 5000);
|
||||
this._updatingTabs.set(tabId, timer);
|
||||
},
|
||||
_hasFavIconInfo(tabOrChangeInfo) {
|
||||
return 'favIconUrl' in tabOrChangeInfo;
|
||||
},
|
||||
_updatingTabs: new Map(),
|
||||
};
|
||||
TabFavIconHelper._init(); // eslint-disable-line no-underscore-dangle
|
||||
export default TabFavIconHelper;
|
||||
File diff suppressed because it is too large
Load Diff
10122
waterfox/browser/components/sidebar/extlib/codemirror-colorpicker.js
Normal file
10122
waterfox/browser/components/sidebar/extlib/codemirror-colorpicker.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,863 @@
|
||||
/* CodeMirror version 5.65.19 */
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineMode("css", function(config, parserConfig) {
|
||||
var inline = parserConfig.inline
|
||||
if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
|
||||
|
||||
var indentUnit = config.indentUnit,
|
||||
tokenHooks = parserConfig.tokenHooks,
|
||||
documentTypes = parserConfig.documentTypes || {},
|
||||
mediaTypes = parserConfig.mediaTypes || {},
|
||||
mediaFeatures = parserConfig.mediaFeatures || {},
|
||||
mediaValueKeywords = parserConfig.mediaValueKeywords || {},
|
||||
propertyKeywords = parserConfig.propertyKeywords || {},
|
||||
nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},
|
||||
fontProperties = parserConfig.fontProperties || {},
|
||||
counterDescriptors = parserConfig.counterDescriptors || {},
|
||||
colorKeywords = parserConfig.colorKeywords || {},
|
||||
valueKeywords = parserConfig.valueKeywords || {},
|
||||
allowNested = parserConfig.allowNested,
|
||||
lineComment = parserConfig.lineComment,
|
||||
supportsAtComponent = parserConfig.supportsAtComponent === true,
|
||||
highlightNonStandardPropertyKeywords = config.highlightNonStandardPropertyKeywords !== false;
|
||||
|
||||
var type, override;
|
||||
function ret(style, tp) { type = tp; return style; }
|
||||
|
||||
// Tokenizers
|
||||
|
||||
function tokenBase(stream, state) {
|
||||
var ch = stream.next();
|
||||
if (tokenHooks[ch]) {
|
||||
var result = tokenHooks[ch](stream, state);
|
||||
if (result !== false) return result;
|
||||
}
|
||||
if (ch == "@") {
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
return ret("def", stream.current());
|
||||
} else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
|
||||
return ret(null, "compare");
|
||||
} else if (ch == "\"" || ch == "'") {
|
||||
state.tokenize = tokenString(ch);
|
||||
return state.tokenize(stream, state);
|
||||
} else if (ch == "#") {
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
return ret("atom", "hash");
|
||||
} else if (ch == "!") {
|
||||
stream.match(/^\s*\w*/);
|
||||
return ret("keyword", "important");
|
||||
} else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
|
||||
stream.eatWhile(/[\w.%]/);
|
||||
return ret("number", "unit");
|
||||
} else if (ch === "-") {
|
||||
if (/[\d.]/.test(stream.peek())) {
|
||||
stream.eatWhile(/[\w.%]/);
|
||||
return ret("number", "unit");
|
||||
} else if (stream.match(/^-[\w\\\-]*/)) {
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
if (stream.match(/^\s*:/, false))
|
||||
return ret("variable-2", "variable-definition");
|
||||
return ret("variable-2", "variable");
|
||||
} else if (stream.match(/^\w+-/)) {
|
||||
return ret("meta", "meta");
|
||||
}
|
||||
} else if (/[,+>*\/]/.test(ch)) {
|
||||
return ret(null, "select-op");
|
||||
} else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
|
||||
return ret("qualifier", "qualifier");
|
||||
} else if (/[:;{}\[\]\(\)]/.test(ch)) {
|
||||
return ret(null, ch);
|
||||
} else if (stream.match(/^[\w-.]+(?=\()/)) {
|
||||
if (/^(url(-prefix)?|domain|regexp)$/i.test(stream.current())) {
|
||||
state.tokenize = tokenParenthesized;
|
||||
}
|
||||
return ret("variable callee", "variable");
|
||||
} else if (/[\w\\\-]/.test(ch)) {
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
return ret("property", "word");
|
||||
} else {
|
||||
return ret(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
function tokenString(quote) {
|
||||
return function(stream, state) {
|
||||
var escaped = false, ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch == quote && !escaped) {
|
||||
if (quote == ")") stream.backUp(1);
|
||||
break;
|
||||
}
|
||||
escaped = !escaped && ch == "\\";
|
||||
}
|
||||
if (ch == quote || !escaped && quote != ")") state.tokenize = null;
|
||||
return ret("string", "string");
|
||||
};
|
||||
}
|
||||
|
||||
function tokenParenthesized(stream, state) {
|
||||
stream.next(); // Must be '('
|
||||
if (!stream.match(/^\s*[\"\')]/, false))
|
||||
state.tokenize = tokenString(")");
|
||||
else
|
||||
state.tokenize = null;
|
||||
return ret(null, "(");
|
||||
}
|
||||
|
||||
// Context management
|
||||
|
||||
function Context(type, indent, prev) {
|
||||
this.type = type;
|
||||
this.indent = indent;
|
||||
this.prev = prev;
|
||||
}
|
||||
|
||||
function pushContext(state, stream, type, indent) {
|
||||
state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);
|
||||
return type;
|
||||
}
|
||||
|
||||
function popContext(state) {
|
||||
if (state.context.prev)
|
||||
state.context = state.context.prev;
|
||||
return state.context.type;
|
||||
}
|
||||
|
||||
function pass(type, stream, state) {
|
||||
return states[state.context.type](type, stream, state);
|
||||
}
|
||||
function popAndPass(type, stream, state, n) {
|
||||
for (var i = n || 1; i > 0; i--)
|
||||
state.context = state.context.prev;
|
||||
return pass(type, stream, state);
|
||||
}
|
||||
|
||||
// Parser
|
||||
|
||||
function wordAsValue(stream) {
|
||||
var word = stream.current().toLowerCase();
|
||||
if (valueKeywords.hasOwnProperty(word))
|
||||
override = "atom";
|
||||
else if (colorKeywords.hasOwnProperty(word))
|
||||
override = "keyword";
|
||||
else
|
||||
override = "variable";
|
||||
}
|
||||
|
||||
var states = {};
|
||||
|
||||
states.top = function(type, stream, state) {
|
||||
if (type == "{") {
|
||||
return pushContext(state, stream, "block");
|
||||
} else if (type == "}" && state.context.prev) {
|
||||
return popContext(state);
|
||||
} else if (supportsAtComponent && /@component/i.test(type)) {
|
||||
return pushContext(state, stream, "atComponentBlock");
|
||||
} else if (/^@(-moz-)?document$/i.test(type)) {
|
||||
return pushContext(state, stream, "documentTypes");
|
||||
} else if (/^@(media|supports|(-moz-)?document|import)$/i.test(type)) {
|
||||
return pushContext(state, stream, "atBlock");
|
||||
} else if (/^@(font-face|counter-style)/i.test(type)) {
|
||||
state.stateArg = type;
|
||||
return "restricted_atBlock_before";
|
||||
} else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(type)) {
|
||||
return "keyframes";
|
||||
} else if (type && type.charAt(0) == "@") {
|
||||
return pushContext(state, stream, "at");
|
||||
} else if (type == "hash") {
|
||||
override = "builtin";
|
||||
} else if (type == "word") {
|
||||
override = "tag";
|
||||
} else if (type == "variable-definition") {
|
||||
return "maybeprop";
|
||||
} else if (type == "interpolation") {
|
||||
return pushContext(state, stream, "interpolation");
|
||||
} else if (type == ":") {
|
||||
return "pseudo";
|
||||
} else if (allowNested && type == "(") {
|
||||
return pushContext(state, stream, "parens");
|
||||
}
|
||||
return state.context.type;
|
||||
};
|
||||
|
||||
states.block = function(type, stream, state) {
|
||||
if (type == "word") {
|
||||
var word = stream.current().toLowerCase();
|
||||
if (propertyKeywords.hasOwnProperty(word)) {
|
||||
override = "property";
|
||||
return "maybeprop";
|
||||
} else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
|
||||
override = highlightNonStandardPropertyKeywords ? "string-2" : "property";
|
||||
return "maybeprop";
|
||||
} else if (allowNested) {
|
||||
override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
|
||||
return "block";
|
||||
} else {
|
||||
override += " error";
|
||||
return "maybeprop";
|
||||
}
|
||||
} else if (type == "meta") {
|
||||
return "block";
|
||||
} else if (!allowNested && (type == "hash" || type == "qualifier")) {
|
||||
override = "error";
|
||||
return "block";
|
||||
} else {
|
||||
return states.top(type, stream, state);
|
||||
}
|
||||
};
|
||||
|
||||
states.maybeprop = function(type, stream, state) {
|
||||
if (type == ":") return pushContext(state, stream, "prop");
|
||||
return pass(type, stream, state);
|
||||
};
|
||||
|
||||
states.prop = function(type, stream, state) {
|
||||
if (type == ";") return popContext(state);
|
||||
if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
|
||||
if (type == "}" || type == "{") return popAndPass(type, stream, state);
|
||||
if (type == "(") return pushContext(state, stream, "parens");
|
||||
|
||||
if (type == "hash" && !/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(stream.current())) {
|
||||
override += " error";
|
||||
} else if (type == "word") {
|
||||
wordAsValue(stream);
|
||||
} else if (type == "interpolation") {
|
||||
return pushContext(state, stream, "interpolation");
|
||||
}
|
||||
return "prop";
|
||||
};
|
||||
|
||||
states.propBlock = function(type, _stream, state) {
|
||||
if (type == "}") return popContext(state);
|
||||
if (type == "word") { override = "property"; return "maybeprop"; }
|
||||
return state.context.type;
|
||||
};
|
||||
|
||||
states.parens = function(type, stream, state) {
|
||||
if (type == "{" || type == "}") return popAndPass(type, stream, state);
|
||||
if (type == ")") return popContext(state);
|
||||
if (type == "(") return pushContext(state, stream, "parens");
|
||||
if (type == "interpolation") return pushContext(state, stream, "interpolation");
|
||||
if (type == "word") wordAsValue(stream);
|
||||
return "parens";
|
||||
};
|
||||
|
||||
states.pseudo = function(type, stream, state) {
|
||||
if (type == "meta") return "pseudo";
|
||||
|
||||
if (type == "word") {
|
||||
override = "variable-3";
|
||||
return state.context.type;
|
||||
}
|
||||
return pass(type, stream, state);
|
||||
};
|
||||
|
||||
states.documentTypes = function(type, stream, state) {
|
||||
if (type == "word" && documentTypes.hasOwnProperty(stream.current())) {
|
||||
override = "tag";
|
||||
return state.context.type;
|
||||
} else {
|
||||
return states.atBlock(type, stream, state);
|
||||
}
|
||||
};
|
||||
|
||||
states.atBlock = function(type, stream, state) {
|
||||
if (type == "(") return pushContext(state, stream, "atBlock_parens");
|
||||
if (type == "}" || type == ";") return popAndPass(type, stream, state);
|
||||
if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
|
||||
|
||||
if (type == "interpolation") return pushContext(state, stream, "interpolation");
|
||||
|
||||
if (type == "word") {
|
||||
var word = stream.current().toLowerCase();
|
||||
if (word == "only" || word == "not" || word == "and" || word == "or")
|
||||
override = "keyword";
|
||||
else if (mediaTypes.hasOwnProperty(word))
|
||||
override = "attribute";
|
||||
else if (mediaFeatures.hasOwnProperty(word))
|
||||
override = "property";
|
||||
else if (mediaValueKeywords.hasOwnProperty(word))
|
||||
override = "keyword";
|
||||
else if (propertyKeywords.hasOwnProperty(word))
|
||||
override = "property";
|
||||
else if (nonStandardPropertyKeywords.hasOwnProperty(word))
|
||||
override = highlightNonStandardPropertyKeywords ? "string-2" : "property";
|
||||
else if (valueKeywords.hasOwnProperty(word))
|
||||
override = "atom";
|
||||
else if (colorKeywords.hasOwnProperty(word))
|
||||
override = "keyword";
|
||||
else
|
||||
override = "error";
|
||||
}
|
||||
return state.context.type;
|
||||
};
|
||||
|
||||
states.atComponentBlock = function(type, stream, state) {
|
||||
if (type == "}")
|
||||
return popAndPass(type, stream, state);
|
||||
if (type == "{")
|
||||
return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false);
|
||||
if (type == "word")
|
||||
override = "error";
|
||||
return state.context.type;
|
||||
};
|
||||
|
||||
states.atBlock_parens = function(type, stream, state) {
|
||||
if (type == ")") return popContext(state);
|
||||
if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
|
||||
return states.atBlock(type, stream, state);
|
||||
};
|
||||
|
||||
states.restricted_atBlock_before = function(type, stream, state) {
|
||||
if (type == "{")
|
||||
return pushContext(state, stream, "restricted_atBlock");
|
||||
if (type == "word" && state.stateArg == "@counter-style") {
|
||||
override = "variable";
|
||||
return "restricted_atBlock_before";
|
||||
}
|
||||
return pass(type, stream, state);
|
||||
};
|
||||
|
||||
states.restricted_atBlock = function(type, stream, state) {
|
||||
if (type == "}") {
|
||||
state.stateArg = null;
|
||||
return popContext(state);
|
||||
}
|
||||
if (type == "word") {
|
||||
if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||
|
||||
(state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))
|
||||
override = "error";
|
||||
else
|
||||
override = "property";
|
||||
return "maybeprop";
|
||||
}
|
||||
return "restricted_atBlock";
|
||||
};
|
||||
|
||||
states.keyframes = function(type, stream, state) {
|
||||
if (type == "word") { override = "variable"; return "keyframes"; }
|
||||
if (type == "{") return pushContext(state, stream, "top");
|
||||
return pass(type, stream, state);
|
||||
};
|
||||
|
||||
states.at = function(type, stream, state) {
|
||||
if (type == ";") return popContext(state);
|
||||
if (type == "{" || type == "}") return popAndPass(type, stream, state);
|
||||
if (type == "word") override = "tag";
|
||||
else if (type == "hash") override = "builtin";
|
||||
return "at";
|
||||
};
|
||||
|
||||
states.interpolation = function(type, stream, state) {
|
||||
if (type == "}") return popContext(state);
|
||||
if (type == "{" || type == ";") return popAndPass(type, stream, state);
|
||||
if (type == "word") override = "variable";
|
||||
else if (type != "variable" && type != "(" && type != ")") override = "error";
|
||||
return "interpolation";
|
||||
};
|
||||
|
||||
return {
|
||||
startState: function(base) {
|
||||
return {tokenize: null,
|
||||
state: inline ? "block" : "top",
|
||||
stateArg: null,
|
||||
context: new Context(inline ? "block" : "top", base || 0, null)};
|
||||
},
|
||||
|
||||
token: function(stream, state) {
|
||||
if (!state.tokenize && stream.eatSpace()) return null;
|
||||
var style = (state.tokenize || tokenBase)(stream, state);
|
||||
if (style && typeof style == "object") {
|
||||
type = style[1];
|
||||
style = style[0];
|
||||
}
|
||||
override = style;
|
||||
if (type != "comment")
|
||||
state.state = states[state.state](type, stream, state);
|
||||
return override;
|
||||
},
|
||||
|
||||
indent: function(state, textAfter) {
|
||||
var cx = state.context, ch = textAfter && textAfter.charAt(0);
|
||||
var indent = cx.indent;
|
||||
if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev;
|
||||
if (cx.prev) {
|
||||
if (ch == "}" && (cx.type == "block" || cx.type == "top" ||
|
||||
cx.type == "interpolation" || cx.type == "restricted_atBlock")) {
|
||||
// Resume indentation from parent context.
|
||||
cx = cx.prev;
|
||||
indent = cx.indent;
|
||||
} else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") ||
|
||||
ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
|
||||
// Dedent relative to current context.
|
||||
indent = Math.max(0, cx.indent - indentUnit);
|
||||
}
|
||||
}
|
||||
return indent;
|
||||
},
|
||||
|
||||
electricChars: "}",
|
||||
blockCommentStart: "/*",
|
||||
blockCommentEnd: "*/",
|
||||
blockCommentContinue: " * ",
|
||||
lineComment: lineComment,
|
||||
fold: "brace"
|
||||
};
|
||||
});
|
||||
|
||||
function keySet(array) {
|
||||
var keys = {};
|
||||
for (var i = 0; i < array.length; ++i) {
|
||||
keys[array[i].toLowerCase()] = true;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
var documentTypes_ = [
|
||||
"domain", "regexp", "url", "url-prefix"
|
||||
], documentTypes = keySet(documentTypes_);
|
||||
|
||||
var mediaTypes_ = [
|
||||
"all", "aural", "braille", "handheld", "print", "projection", "screen",
|
||||
"tty", "tv", "embossed"
|
||||
], mediaTypes = keySet(mediaTypes_);
|
||||
|
||||
var mediaFeatures_ = [
|
||||
"width", "min-width", "max-width", "height", "min-height", "max-height",
|
||||
"device-width", "min-device-width", "max-device-width", "device-height",
|
||||
"min-device-height", "max-device-height", "aspect-ratio",
|
||||
"min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
|
||||
"min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
|
||||
"max-color", "color-index", "min-color-index", "max-color-index",
|
||||
"monochrome", "min-monochrome", "max-monochrome", "resolution",
|
||||
"min-resolution", "max-resolution", "scan", "grid", "orientation",
|
||||
"device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
|
||||
"pointer", "any-pointer", "hover", "any-hover", "prefers-color-scheme",
|
||||
"dynamic-range", "video-dynamic-range"
|
||||
], mediaFeatures = keySet(mediaFeatures_);
|
||||
|
||||
var mediaValueKeywords_ = [
|
||||
"landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
|
||||
"interlace", "progressive",
|
||||
"dark", "light",
|
||||
"standard", "high"
|
||||
], mediaValueKeywords = keySet(mediaValueKeywords_);
|
||||
|
||||
var propertyKeywords_ = [
|
||||
"align-content", "align-items", "align-self", "alignment-adjust",
|
||||
"alignment-baseline", "all", "anchor-point", "animation", "animation-delay",
|
||||
"animation-direction", "animation-duration", "animation-fill-mode",
|
||||
"animation-iteration-count", "animation-name", "animation-play-state",
|
||||
"animation-timing-function", "appearance", "azimuth", "backdrop-filter",
|
||||
"backface-visibility", "background", "background-attachment",
|
||||
"background-blend-mode", "background-clip", "background-color",
|
||||
"background-image", "background-origin", "background-position",
|
||||
"background-position-x", "background-position-y", "background-repeat",
|
||||
"background-size", "baseline-shift", "binding", "bleed", "block-size",
|
||||
"bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target",
|
||||
"border", "border-bottom", "border-bottom-color", "border-bottom-left-radius",
|
||||
"border-bottom-right-radius", "border-bottom-style", "border-bottom-width",
|
||||
"border-collapse", "border-color", "border-image", "border-image-outset",
|
||||
"border-image-repeat", "border-image-slice", "border-image-source",
|
||||
"border-image-width", "border-left", "border-left-color", "border-left-style",
|
||||
"border-left-width", "border-radius", "border-right", "border-right-color",
|
||||
"border-right-style", "border-right-width", "border-spacing", "border-style",
|
||||
"border-top", "border-top-color", "border-top-left-radius",
|
||||
"border-top-right-radius", "border-top-style", "border-top-width",
|
||||
"border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing",
|
||||
"break-after", "break-before", "break-inside", "caption-side", "caret-color",
|
||||
"clear", "clip", "color", "color-profile", "column-count", "column-fill",
|
||||
"column-gap", "column-rule", "column-rule-color", "column-rule-style",
|
||||
"column-rule-width", "column-span", "column-width", "columns", "contain",
|
||||
"content", "counter-increment", "counter-reset", "crop", "cue", "cue-after",
|
||||
"cue-before", "cursor", "direction", "display", "dominant-baseline",
|
||||
"drop-initial-after-adjust", "drop-initial-after-align",
|
||||
"drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size",
|
||||
"drop-initial-value", "elevation", "empty-cells", "fit", "fit-content", "fit-position",
|
||||
"flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow",
|
||||
"flex-shrink", "flex-wrap", "float", "float-offset", "flow-from", "flow-into",
|
||||
"font", "font-family", "font-feature-settings", "font-kerning",
|
||||
"font-language-override", "font-optical-sizing", "font-size",
|
||||
"font-size-adjust", "font-stretch", "font-style", "font-synthesis",
|
||||
"font-variant", "font-variant-alternates", "font-variant-caps",
|
||||
"font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric",
|
||||
"font-variant-position", "font-variation-settings", "font-weight", "gap",
|
||||
"grid", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows",
|
||||
"grid-column", "grid-column-end", "grid-column-gap", "grid-column-start",
|
||||
"grid-gap", "grid-row", "grid-row-end", "grid-row-gap", "grid-row-start",
|
||||
"grid-template", "grid-template-areas", "grid-template-columns",
|
||||
"grid-template-rows", "hanging-punctuation", "height", "hyphens", "icon",
|
||||
"image-orientation", "image-rendering", "image-resolution", "inline-box-align",
|
||||
"inset", "inset-block", "inset-block-end", "inset-block-start", "inset-inline",
|
||||
"inset-inline-end", "inset-inline-start", "isolation", "justify-content",
|
||||
"justify-items", "justify-self", "left", "letter-spacing", "line-break",
|
||||
"line-height", "line-height-step", "line-stacking", "line-stacking-ruby",
|
||||
"line-stacking-shift", "line-stacking-strategy", "list-style",
|
||||
"list-style-image", "list-style-position", "list-style-type", "margin",
|
||||
"margin-bottom", "margin-left", "margin-right", "margin-top", "marks",
|
||||
"marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed",
|
||||
"marquee-style", "mask-clip", "mask-composite", "mask-image", "mask-mode",
|
||||
"mask-origin", "mask-position", "mask-repeat", "mask-size","mask-type",
|
||||
"max-block-size", "max-height", "max-inline-size",
|
||||
"max-width", "min-block-size", "min-height", "min-inline-size", "min-width",
|
||||
"mix-blend-mode", "move-to", "nav-down", "nav-index", "nav-left", "nav-right",
|
||||
"nav-up", "object-fit", "object-position", "offset", "offset-anchor",
|
||||
"offset-distance", "offset-path", "offset-position", "offset-rotate",
|
||||
"opacity", "order", "orphans", "outline", "outline-color", "outline-offset",
|
||||
"outline-style", "outline-width", "overflow", "overflow-style",
|
||||
"overflow-wrap", "overflow-x", "overflow-y", "padding", "padding-bottom",
|
||||
"padding-left", "padding-right", "padding-top", "page", "page-break-after",
|
||||
"page-break-before", "page-break-inside", "page-policy", "pause",
|
||||
"pause-after", "pause-before", "perspective", "perspective-origin", "pitch",
|
||||
"pitch-range", "place-content", "place-items", "place-self", "play-during",
|
||||
"position", "presentation-level", "punctuation-trim", "quotes",
|
||||
"region-break-after", "region-break-before", "region-break-inside",
|
||||
"region-fragment", "rendering-intent", "resize", "rest", "rest-after",
|
||||
"rest-before", "richness", "right", "rotate", "rotation", "rotation-point",
|
||||
"row-gap", "ruby-align", "ruby-overhang", "ruby-position", "ruby-span",
|
||||
"scale", "scroll-behavior", "scroll-margin", "scroll-margin-block",
|
||||
"scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom",
|
||||
"scroll-margin-inline", "scroll-margin-inline-end",
|
||||
"scroll-margin-inline-start", "scroll-margin-left", "scroll-margin-right",
|
||||
"scroll-margin-top", "scroll-padding", "scroll-padding-block",
|
||||
"scroll-padding-block-end", "scroll-padding-block-start",
|
||||
"scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end",
|
||||
"scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right",
|
||||
"scroll-padding-top", "scroll-snap-align", "scroll-snap-type",
|
||||
"shape-image-threshold", "shape-inside", "shape-margin", "shape-outside",
|
||||
"size", "speak", "speak-as", "speak-header", "speak-numeral",
|
||||
"speak-punctuation", "speech-rate", "stress", "string-set", "tab-size",
|
||||
"table-layout", "target", "target-name", "target-new", "target-position",
|
||||
"text-align", "text-align-last", "text-combine-upright", "text-decoration",
|
||||
"text-decoration-color", "text-decoration-line", "text-decoration-skip",
|
||||
"text-decoration-skip-ink", "text-decoration-style", "text-emphasis",
|
||||
"text-emphasis-color", "text-emphasis-position", "text-emphasis-style",
|
||||
"text-height", "text-indent", "text-justify", "text-orientation",
|
||||
"text-outline", "text-overflow", "text-rendering", "text-shadow",
|
||||
"text-size-adjust", "text-space-collapse", "text-transform",
|
||||
"text-underline-position", "text-wrap", "top", "touch-action", "transform", "transform-origin",
|
||||
"transform-style", "transition", "transition-delay", "transition-duration",
|
||||
"transition-property", "transition-timing-function", "translate",
|
||||
"unicode-bidi", "user-select", "vertical-align", "visibility", "voice-balance",
|
||||
"voice-duration", "voice-family", "voice-pitch", "voice-range", "voice-rate",
|
||||
"voice-stress", "voice-volume", "volume", "white-space", "widows", "width",
|
||||
"will-change", "word-break", "word-spacing", "word-wrap", "writing-mode", "z-index",
|
||||
// SVG-specific
|
||||
"clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
|
||||
"flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
|
||||
"color-interpolation", "color-interpolation-filters",
|
||||
"color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
|
||||
"marker", "marker-end", "marker-mid", "marker-start", "paint-order", "shape-rendering", "stroke",
|
||||
"stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
|
||||
"stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
|
||||
"baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
|
||||
"glyph-orientation-vertical", "text-anchor", "writing-mode",
|
||||
], propertyKeywords = keySet(propertyKeywords_);
|
||||
|
||||
var nonStandardPropertyKeywords_ = [
|
||||
"accent-color", "aspect-ratio", "border-block", "border-block-color", "border-block-end",
|
||||
"border-block-end-color", "border-block-end-style", "border-block-end-width",
|
||||
"border-block-start", "border-block-start-color", "border-block-start-style",
|
||||
"border-block-start-width", "border-block-style", "border-block-width",
|
||||
"border-inline", "border-inline-color", "border-inline-end",
|
||||
"border-inline-end-color", "border-inline-end-style",
|
||||
"border-inline-end-width", "border-inline-start", "border-inline-start-color",
|
||||
"border-inline-start-style", "border-inline-start-width",
|
||||
"border-inline-style", "border-inline-width", "content-visibility", "margin-block",
|
||||
"margin-block-end", "margin-block-start", "margin-inline", "margin-inline-end",
|
||||
"margin-inline-start", "overflow-anchor", "overscroll-behavior", "padding-block", "padding-block-end",
|
||||
"padding-block-start", "padding-inline", "padding-inline-end",
|
||||
"padding-inline-start", "scroll-snap-stop", "scrollbar-3d-light-color",
|
||||
"scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
|
||||
"scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
|
||||
"scrollbar-track-color", "searchfield-cancel-button", "searchfield-decoration",
|
||||
"searchfield-results-button", "searchfield-results-decoration", "shape-inside", "zoom"
|
||||
], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
|
||||
|
||||
var fontProperties_ = [
|
||||
"font-display", "font-family", "src", "unicode-range", "font-variant",
|
||||
"font-feature-settings", "font-stretch", "font-weight", "font-style"
|
||||
], fontProperties = keySet(fontProperties_);
|
||||
|
||||
var counterDescriptors_ = [
|
||||
"additive-symbols", "fallback", "negative", "pad", "prefix", "range",
|
||||
"speak-as", "suffix", "symbols", "system"
|
||||
], counterDescriptors = keySet(counterDescriptors_);
|
||||
|
||||
var colorKeywords_ = [
|
||||
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
|
||||
"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
|
||||
"burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
|
||||
"cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
|
||||
"darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen",
|
||||
"darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
|
||||
"darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet",
|
||||
"deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick",
|
||||
"floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
|
||||
"gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
|
||||
"hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
|
||||
"lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
|
||||
"lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink",
|
||||
"lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey",
|
||||
"lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
|
||||
"maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
|
||||
"mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
|
||||
"mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
|
||||
"navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
|
||||
"orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
|
||||
"papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
|
||||
"purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
|
||||
"salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
|
||||
"slateblue", "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan",
|
||||
"teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
|
||||
"whitesmoke", "yellow", "yellowgreen"
|
||||
], colorKeywords = keySet(colorKeywords_);
|
||||
|
||||
var valueKeywords_ = [
|
||||
"above", "absolute", "activeborder", "additive", "activecaption", "afar",
|
||||
"after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
|
||||
"always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
|
||||
"arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page",
|
||||
"avoid-region", "axis-pan", "background", "backwards", "baseline", "below", "bidi-override", "binary",
|
||||
"bengali", "blink", "block", "block-axis", "blur", "bold", "bolder", "border", "border-box",
|
||||
"both", "bottom", "break", "break-all", "break-word", "brightness", "bullets", "button",
|
||||
"buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
|
||||
"capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
|
||||
"cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
|
||||
"cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
|
||||
"col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
|
||||
"compact", "condensed", "conic-gradient", "contain", "content", "contents",
|
||||
"content-box", "context-menu", "continuous", "contrast", "copy", "counter", "counters", "cover", "crop",
|
||||
"cross", "crosshair", "cubic-bezier", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
|
||||
"decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
|
||||
"destination-in", "destination-out", "destination-over", "devanagari", "difference",
|
||||
"disc", "discard", "disclosure-closed", "disclosure-open", "document",
|
||||
"dot-dash", "dot-dot-dash",
|
||||
"dotted", "double", "down", "drop-shadow", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
|
||||
"element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
|
||||
"ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
|
||||
"ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
|
||||
"ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
|
||||
"ethiopic-halehame-gez", "ethiopic-halehame-om-et",
|
||||
"ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
|
||||
"ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
|
||||
"ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
|
||||
"extra-expanded", "fantasy", "fast", "fill", "fill-box", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
|
||||
"forwards", "from", "geometricPrecision", "georgian", "grayscale", "graytext", "grid", "groove",
|
||||
"gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
|
||||
"help", "hidden", "hide", "higher", "highlight", "highlighttext",
|
||||
"hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "hue-rotate", "icon", "ignore",
|
||||
"inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
|
||||
"infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
|
||||
"inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
|
||||
"italic", "japanese-formal", "japanese-informal", "justify", "kannada",
|
||||
"katakana", "katakana-iroha", "keep-all", "khmer",
|
||||
"korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
|
||||
"landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten",
|
||||
"line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
|
||||
"local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
|
||||
"lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
|
||||
"lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "manipulation", "match", "matrix", "matrix3d",
|
||||
"media-play-button", "media-slider", "media-sliderthumb",
|
||||
"media-volume-slider", "media-volume-sliderthumb", "medium",
|
||||
"menu", "menulist", "menulist-button",
|
||||
"menutext", "message-box", "middle", "min-intrinsic",
|
||||
"mix", "mongolian", "monospace", "move", "multiple", "multiple_mask_images", "multiply", "myanmar", "n-resize",
|
||||
"narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
|
||||
"no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
|
||||
"ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote",
|
||||
"optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
|
||||
"outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
|
||||
"painted", "page", "paused", "persian", "perspective", "pinch-zoom", "plus-darker", "plus-lighter",
|
||||
"pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
|
||||
"progress", "push-button", "radial-gradient", "radio", "read-only",
|
||||
"read-write", "read-write-plaintext-only", "rectangle", "region",
|
||||
"relative", "repeat", "repeating-linear-gradient", "repeating-radial-gradient",
|
||||
"repeating-conic-gradient", "repeat-x", "repeat-y", "reset", "reverse",
|
||||
"rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
|
||||
"rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
|
||||
"s-resize", "sans-serif", "saturate", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
|
||||
"scroll", "scrollbar", "scroll-position", "se-resize", "searchfield",
|
||||
"searchfield-cancel-button", "searchfield-decoration",
|
||||
"searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end",
|
||||
"semi-condensed", "semi-expanded", "separate", "sepia", "serif", "show", "sidama",
|
||||
"simp-chinese-formal", "simp-chinese-informal", "single",
|
||||
"skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
|
||||
"slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
|
||||
"small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
|
||||
"source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square",
|
||||
"square-button", "start", "static", "status-bar", "stretch", "stroke", "stroke-box", "sub",
|
||||
"subpixel-antialiased", "svg_masks", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table",
|
||||
"table-caption", "table-cell", "table-column", "table-column-group",
|
||||
"table-footer-group", "table-header-group", "table-row", "table-row-group",
|
||||
"tamil",
|
||||
"telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
|
||||
"thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
|
||||
"threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
|
||||
"tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
|
||||
"trad-chinese-formal", "trad-chinese-informal", "transform",
|
||||
"translate", "translate3d", "translateX", "translateY", "translateZ",
|
||||
"transparent", "ultra-condensed", "ultra-expanded", "underline", "unidirectional-pan", "unset", "up",
|
||||
"upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
|
||||
"upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
|
||||
"var", "vertical", "vertical-text", "view-box", "visible", "visibleFill", "visiblePainted",
|
||||
"visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
|
||||
"window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
|
||||
"xx-large", "xx-small"
|
||||
], valueKeywords = keySet(valueKeywords_);
|
||||
|
||||
var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)
|
||||
.concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)
|
||||
.concat(valueKeywords_);
|
||||
CodeMirror.registerHelper("hintWords", "css", allWords);
|
||||
|
||||
function tokenCComment(stream, state) {
|
||||
var maybeEnd = false, ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (maybeEnd && ch == "/") {
|
||||
state.tokenize = null;
|
||||
break;
|
||||
}
|
||||
maybeEnd = (ch == "*");
|
||||
}
|
||||
return ["comment", "comment"];
|
||||
}
|
||||
|
||||
CodeMirror.defineMIME("text/css", {
|
||||
documentTypes: documentTypes,
|
||||
mediaTypes: mediaTypes,
|
||||
mediaFeatures: mediaFeatures,
|
||||
mediaValueKeywords: mediaValueKeywords,
|
||||
propertyKeywords: propertyKeywords,
|
||||
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
|
||||
fontProperties: fontProperties,
|
||||
counterDescriptors: counterDescriptors,
|
||||
colorKeywords: colorKeywords,
|
||||
valueKeywords: valueKeywords,
|
||||
tokenHooks: {
|
||||
"/": function(stream, state) {
|
||||
if (!stream.eat("*")) return false;
|
||||
state.tokenize = tokenCComment;
|
||||
return tokenCComment(stream, state);
|
||||
}
|
||||
},
|
||||
name: "css"
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-scss", {
|
||||
mediaTypes: mediaTypes,
|
||||
mediaFeatures: mediaFeatures,
|
||||
mediaValueKeywords: mediaValueKeywords,
|
||||
propertyKeywords: propertyKeywords,
|
||||
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
|
||||
colorKeywords: colorKeywords,
|
||||
valueKeywords: valueKeywords,
|
||||
fontProperties: fontProperties,
|
||||
allowNested: true,
|
||||
lineComment: "//",
|
||||
tokenHooks: {
|
||||
"/": function(stream, state) {
|
||||
if (stream.eat("/")) {
|
||||
stream.skipToEnd();
|
||||
return ["comment", "comment"];
|
||||
} else if (stream.eat("*")) {
|
||||
state.tokenize = tokenCComment;
|
||||
return tokenCComment(stream, state);
|
||||
} else {
|
||||
return ["operator", "operator"];
|
||||
}
|
||||
},
|
||||
":": function(stream) {
|
||||
if (stream.match(/^\s*\{/, false))
|
||||
return [null, null]
|
||||
return false;
|
||||
},
|
||||
"$": function(stream) {
|
||||
stream.match(/^[\w-]+/);
|
||||
if (stream.match(/^\s*:/, false))
|
||||
return ["variable-2", "variable-definition"];
|
||||
return ["variable-2", "variable"];
|
||||
},
|
||||
"#": function(stream) {
|
||||
if (!stream.eat("{")) return false;
|
||||
return [null, "interpolation"];
|
||||
}
|
||||
},
|
||||
name: "css",
|
||||
helperType: "scss"
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-less", {
|
||||
mediaTypes: mediaTypes,
|
||||
mediaFeatures: mediaFeatures,
|
||||
mediaValueKeywords: mediaValueKeywords,
|
||||
propertyKeywords: propertyKeywords,
|
||||
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
|
||||
colorKeywords: colorKeywords,
|
||||
valueKeywords: valueKeywords,
|
||||
fontProperties: fontProperties,
|
||||
allowNested: true,
|
||||
lineComment: "//",
|
||||
tokenHooks: {
|
||||
"/": function(stream, state) {
|
||||
if (stream.eat("/")) {
|
||||
stream.skipToEnd();
|
||||
return ["comment", "comment"];
|
||||
} else if (stream.eat("*")) {
|
||||
state.tokenize = tokenCComment;
|
||||
return tokenCComment(stream, state);
|
||||
} else {
|
||||
return ["operator", "operator"];
|
||||
}
|
||||
},
|
||||
"@": function(stream) {
|
||||
if (stream.eat("{")) return [null, "interpolation"];
|
||||
if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/i, false)) return false;
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
if (stream.match(/^\s*:/, false))
|
||||
return ["variable-2", "variable-definition"];
|
||||
return ["variable-2", "variable"];
|
||||
},
|
||||
"&": function() {
|
||||
return ["atom", "atom"];
|
||||
}
|
||||
},
|
||||
name: "css",
|
||||
helperType: "less"
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-gss", {
|
||||
documentTypes: documentTypes,
|
||||
mediaTypes: mediaTypes,
|
||||
mediaFeatures: mediaFeatures,
|
||||
propertyKeywords: propertyKeywords,
|
||||
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
|
||||
fontProperties: fontProperties,
|
||||
counterDescriptors: counterDescriptors,
|
||||
colorKeywords: colorKeywords,
|
||||
valueKeywords: valueKeywords,
|
||||
supportsAtComponent: true,
|
||||
tokenHooks: {
|
||||
"/": function(stream, state) {
|
||||
if (!stream.eat("*")) return false;
|
||||
state.tokenize = tokenCComment;
|
||||
return tokenCComment(stream, state);
|
||||
}
|
||||
},
|
||||
name: "css",
|
||||
helperType: "gss"
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
|
||||
Name: 3024 day
|
||||
Author: Jan T. Sott (http://github.com/idleberg)
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
|
||||
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||
|
||||
*/
|
||||
|
||||
.cm-s-3024-day.CodeMirror { background: #f7f7f7; color: #3a3432; }
|
||||
.cm-s-3024-day div.CodeMirror-selected { background: #d6d5d4; }
|
||||
|
||||
.cm-s-3024-day .CodeMirror-line::selection, .cm-s-3024-day .CodeMirror-line > span::selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d6d5d4; }
|
||||
.cm-s-3024-day .CodeMirror-line::-moz-selection, .cm-s-3024-day .CodeMirror-line > span::-moz-selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d9d9d9; }
|
||||
|
||||
.cm-s-3024-day .CodeMirror-gutters { background: #f7f7f7; border-right: 0px; }
|
||||
.cm-s-3024-day .CodeMirror-guttermarker { color: #db2d20; }
|
||||
.cm-s-3024-day .CodeMirror-guttermarker-subtle { color: #807d7c; }
|
||||
.cm-s-3024-day .CodeMirror-linenumber { color: #807d7c; }
|
||||
|
||||
.cm-s-3024-day .CodeMirror-cursor { border-left: 1px solid #5c5855; }
|
||||
|
||||
.cm-s-3024-day span.cm-comment { color: #cdab53; }
|
||||
.cm-s-3024-day span.cm-atom { color: #a16a94; }
|
||||
.cm-s-3024-day span.cm-number { color: #a16a94; }
|
||||
|
||||
.cm-s-3024-day span.cm-property, .cm-s-3024-day span.cm-attribute { color: #01a252; }
|
||||
.cm-s-3024-day span.cm-keyword { color: #db2d20; }
|
||||
.cm-s-3024-day span.cm-string { color: #fded02; }
|
||||
|
||||
.cm-s-3024-day span.cm-variable { color: #01a252; }
|
||||
.cm-s-3024-day span.cm-variable-2 { color: #01a0e4; }
|
||||
.cm-s-3024-day span.cm-def { color: #e8bbd0; }
|
||||
.cm-s-3024-day span.cm-bracket { color: #3a3432; }
|
||||
.cm-s-3024-day span.cm-tag { color: #db2d20; }
|
||||
.cm-s-3024-day span.cm-link { color: #a16a94; }
|
||||
.cm-s-3024-day span.cm-error { background: #db2d20; color: #5c5855; }
|
||||
|
||||
.cm-s-3024-day .CodeMirror-activeline-background { background: #e8f2ff; }
|
||||
.cm-s-3024-day .CodeMirror-matchingbracket { text-decoration: underline; color: #a16a94 !important; }
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
|
||||
Name: 3024 night
|
||||
Author: Jan T. Sott (http://github.com/idleberg)
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
|
||||
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||
|
||||
*/
|
||||
|
||||
.cm-s-3024-night.CodeMirror { background: #090300; color: #d6d5d4; }
|
||||
.cm-s-3024-night div.CodeMirror-selected { background: #3a3432; }
|
||||
.cm-s-3024-night .CodeMirror-line::selection, .cm-s-3024-night .CodeMirror-line > span::selection, .cm-s-3024-night .CodeMirror-line > span > span::selection { background: rgba(58, 52, 50, .99); }
|
||||
.cm-s-3024-night .CodeMirror-line::-moz-selection, .cm-s-3024-night .CodeMirror-line > span::-moz-selection, .cm-s-3024-night .CodeMirror-line > span > span::-moz-selection { background: rgba(58, 52, 50, .99); }
|
||||
.cm-s-3024-night .CodeMirror-gutters { background: #090300; border-right: 0px; }
|
||||
.cm-s-3024-night .CodeMirror-guttermarker { color: #db2d20; }
|
||||
.cm-s-3024-night .CodeMirror-guttermarker-subtle { color: #5c5855; }
|
||||
.cm-s-3024-night .CodeMirror-linenumber { color: #5c5855; }
|
||||
|
||||
.cm-s-3024-night .CodeMirror-cursor { border-left: 1px solid #807d7c; }
|
||||
|
||||
.cm-s-3024-night span.cm-comment { color: #cdab53; }
|
||||
.cm-s-3024-night span.cm-atom { color: #a16a94; }
|
||||
.cm-s-3024-night span.cm-number { color: #a16a94; }
|
||||
|
||||
.cm-s-3024-night span.cm-property, .cm-s-3024-night span.cm-attribute { color: #01a252; }
|
||||
.cm-s-3024-night span.cm-keyword { color: #db2d20; }
|
||||
.cm-s-3024-night span.cm-string { color: #fded02; }
|
||||
|
||||
.cm-s-3024-night span.cm-variable { color: #01a252; }
|
||||
.cm-s-3024-night span.cm-variable-2 { color: #01a0e4; }
|
||||
.cm-s-3024-night span.cm-def { color: #e8bbd0; }
|
||||
.cm-s-3024-night span.cm-bracket { color: #d6d5d4; }
|
||||
.cm-s-3024-night span.cm-tag { color: #db2d20; }
|
||||
.cm-s-3024-night span.cm-link { color: #a16a94; }
|
||||
.cm-s-3024-night span.cm-error { background: #db2d20; color: #807d7c; }
|
||||
|
||||
.cm-s-3024-night .CodeMirror-activeline-background { background: #2F2F2F; }
|
||||
.cm-s-3024-night .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* abbott.css
|
||||
* A warm, dark theme for prose and code, with pastels and pretty greens.
|
||||
*
|
||||
* Ported from abbott.vim (https://github.com/bcat/abbott.vim) version 2.1.
|
||||
* Original design and CodeMirror port by Jonathan Rascher.
|
||||
*
|
||||
* This theme shares the following color palette with the Vim color scheme.
|
||||
*
|
||||
* Brown shades:
|
||||
* bistre: #231c14
|
||||
* chocolate: #3c3022
|
||||
* cocoa: #745d42
|
||||
* vanilla_cream: #fef3b4
|
||||
*
|
||||
* Red shades:
|
||||
* crimson: #d80450
|
||||
* cinnabar: #f63f05
|
||||
*
|
||||
* Green shades:
|
||||
* dark_olive: #273900
|
||||
* forest_green: #24a507
|
||||
* chartreuse: #a0ea00
|
||||
* pastel_chartreuse: #d8ff84
|
||||
*
|
||||
* Yellow shades:
|
||||
* marigold: #fbb32f
|
||||
* lemon_meringue: #fbec5d
|
||||
*
|
||||
* Blue shades:
|
||||
* cornflower_blue: #3f91f1
|
||||
* periwinkle_blue: #8ccdf0
|
||||
*
|
||||
* Magenta shades:
|
||||
* french_pink: #ec6c99
|
||||
* lavender: #e6a2f3
|
||||
*
|
||||
* Cyan shades:
|
||||
* zomp: #39a78d
|
||||
* seafoam_green: #00ff7f
|
||||
*/
|
||||
|
||||
/* Style the UI: */
|
||||
|
||||
/* Equivalent to Vim's Normal group. */
|
||||
.cm-s-abbott.CodeMirror {
|
||||
background: #231c14 /* bistre */;
|
||||
color: #d8ff84 /* pastel_chartreuse */;
|
||||
}
|
||||
|
||||
/* Roughly equivalent to Vim's LineNr group. */
|
||||
.cm-s-abbott .CodeMirror-gutters {
|
||||
background: #231c14 /* bistre */;
|
||||
border: none;
|
||||
}
|
||||
.cm-s-abbott .CodeMirror-linenumber { color: #fbec5d /* lemon_meringue */; }
|
||||
|
||||
.cm-s-abbott .CodeMirror-guttermarker { color: #f63f05 /* cinnabar */; }
|
||||
|
||||
/* Roughly equivalent to Vim's FoldColumn group. */
|
||||
.cm-s-abbott .CodeMirror-guttermarker-subtle { color: #fbb32f /* marigold */; }
|
||||
|
||||
/*
|
||||
* Roughly equivalent to Vim's CursorColumn group. (We use a brighter color
|
||||
* since Vim's cursorcolumn option highlights a whole column, whereas
|
||||
* CodeMirror's rule just highlights a thin line.)
|
||||
*/
|
||||
.cm-s-abbott .CodeMirror-ruler { border-color: #745d42 /* cocoa */; }
|
||||
|
||||
/* Equivalent to Vim's Cursor group in insert mode. */
|
||||
.cm-s-abbott .CodeMirror-cursor { border-color: #a0ea00 /* chartreuse */; }
|
||||
|
||||
/* Equivalent to Vim's Cursor group in normal mode. */
|
||||
.cm-s-abbott.cm-fat-cursor .CodeMirror-cursor,
|
||||
.cm-s-abbott .cm-animate-fat-cursor {
|
||||
/*
|
||||
* CodeMirror doesn't allow changing the foreground color of the character
|
||||
* under the cursor, so we can't use a reverse video effect for the cursor.
|
||||
* Instead, make it semitransparent.
|
||||
*/
|
||||
background: rgba(160, 234, 0, 0.5) /* chartreuse */;
|
||||
}
|
||||
.cm-s-abbott.cm-fat-cursor .CodeMirror-cursors {
|
||||
/*
|
||||
* Boost the z-index so the fat cursor shows up on top of text and
|
||||
* matchingbracket/matchingtag highlights.
|
||||
*/
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's Cursor group in replace mode. */
|
||||
.cm-s-abbott .CodeMirror-overwrite .CodeMirror-cursor {
|
||||
border-bottom: 1px solid #a0ea00 /* chartreuse */;
|
||||
border-left: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Roughly equivalent to Vim's CursorIM group. */
|
||||
.cm-s-abbott .CodeMirror-secondarycursor {
|
||||
border-color: #00ff7f /* seafoam_green */;
|
||||
}
|
||||
|
||||
/* Roughly equivalent to Vim's Visual group. */
|
||||
.cm-s-abbott .CodeMirror-selected,
|
||||
.cm-s-abbott.CodeMirror-focused .CodeMirror-selected {
|
||||
background: #273900 /* dark_olive */;
|
||||
}
|
||||
.cm-s-abbott .CodeMirror-line::selection,
|
||||
.cm-s-abbott .CodeMirror-line > span::selection,
|
||||
.cm-s-abbott .CodeMirror-line > span > span::selection {
|
||||
background: #273900 /* dark_olive */;
|
||||
}
|
||||
.cm-s-abbott .CodeMirror-line::-moz-selection,
|
||||
.cm-s-abbott .CodeMirror-line > span::-moz-selection,
|
||||
.cm-s-abbott .CodeMirror-line > span > span::-moz-selection {
|
||||
background: #273900 /* dark_olive */;
|
||||
}
|
||||
|
||||
/* Roughly equivalent to Vim's SpecialKey group. */
|
||||
.cm-s-abbott .cm-tab { color: #00ff7f /* seafoam_green */; }
|
||||
|
||||
/* Equivalent to Vim's Search group. */
|
||||
.cm-s-abbott .cm-searching {
|
||||
background: #fef3b4 /* vanilla_cream */ !important;
|
||||
color: #231c14 /* bistre */ !important;
|
||||
}
|
||||
|
||||
/* Style syntax highlighting modes: */
|
||||
|
||||
/* Equivalent to Vim's Comment group. */
|
||||
.cm-s-abbott span.cm-comment {
|
||||
color: #fbb32f /* marigold */;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's String group. */
|
||||
.cm-s-abbott span.cm-string,
|
||||
.cm-s-abbott span.cm-string-2 {
|
||||
color: #e6a2f3 /* lavender */;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's Constant group. */
|
||||
.cm-s-abbott span.cm-number,
|
||||
.cm-s-abbott span.cm-string.cm-url { color: #f63f05 /* cinnabar */; }
|
||||
|
||||
/* Roughly equivalent to Vim's SpecialKey group. */
|
||||
.cm-s-abbott span.cm-invalidchar { color: #00ff7f /* seafoam_green */; }
|
||||
|
||||
/* Equivalent to Vim's Special group. */
|
||||
.cm-s-abbott span.cm-atom { color: #fef3b4 /* vanilla_cream */; }
|
||||
|
||||
/* Equivalent to Vim's Delimiter group. */
|
||||
.cm-s-abbott span.cm-bracket,
|
||||
.cm-s-abbott span.cm-punctuation {
|
||||
color: #fef3b4 /* vanilla_cream */;
|
||||
}
|
||||
|
||||
/* Equivalent Vim's Operator group. */
|
||||
.cm-s-abbott span.cm-operator { font-weight: bold; }
|
||||
|
||||
/* Roughly equivalent to Vim's Identifier group. */
|
||||
.cm-s-abbott span.cm-def,
|
||||
.cm-s-abbott span.cm-variable,
|
||||
.cm-s-abbott span.cm-variable-2,
|
||||
.cm-s-abbott span.cm-variable-3 {
|
||||
color: #8ccdf0 /* periwinkle_blue */;
|
||||
}
|
||||
|
||||
/* Roughly equivalent to Vim's Function group. */
|
||||
.cm-s-abbott span.cm-builtin,
|
||||
.cm-s-abbott span.cm-property,
|
||||
.cm-s-abbott span.cm-qualifier {
|
||||
color: #3f91f1 /* cornflower_blue */;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's Type group. */
|
||||
.cm-s-abbott span.cm-type { color: #24a507 /* forest_green */; }
|
||||
|
||||
/* Equivalent to Vim's Keyword group. */
|
||||
.cm-s-abbott span.cm-keyword {
|
||||
color: #d80450 /* crimson */;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's PreProc group. */
|
||||
.cm-s-abbott span.cm-meta { color: #ec6c99 /* french_pink */; }
|
||||
|
||||
/* Equivalent to Vim's htmlTagName group (linked to Statement). */
|
||||
.cm-s-abbott span.cm-tag {
|
||||
color: #d80450 /* crimson */;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's htmlArg group (linked to Type). */
|
||||
.cm-s-abbott span.cm-attribute { color: #24a507 /* forest_green */; }
|
||||
|
||||
/* Equivalent to Vim's htmlH1, markdownH1, etc. groups (linked to Title). */
|
||||
.cm-s-abbott span.cm-header {
|
||||
color: #d80450 /* crimson */;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's markdownRule group (linked to PreProc). */
|
||||
.cm-s-abbott span.cm-hr { color: #ec6c99 /* french_pink */; }
|
||||
|
||||
/* Roughly equivalent to Vim's Underlined group. */
|
||||
.cm-s-abbott span.cm-link { color: #e6a2f3 /* lavender */; }
|
||||
|
||||
/* Equivalent to Vim's diffRemoved group. */
|
||||
.cm-s-abbott span.cm-negative {
|
||||
background: #d80450 /* crimson */;
|
||||
color: #231c14 /* bistre */;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's diffAdded group. */
|
||||
.cm-s-abbott span.cm-positive {
|
||||
background: #a0ea00 /* chartreuse */;
|
||||
color: #231c14 /* bistre */;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's Error group. */
|
||||
.cm-s-abbott span.cm-error {
|
||||
background: #d80450 /* crimson */;
|
||||
color: #231c14 /* bistre */;
|
||||
}
|
||||
|
||||
/* Style addons: */
|
||||
|
||||
/* Equivalent to Vim's MatchParen group. */
|
||||
.cm-s-abbott span.CodeMirror-matchingbracket {
|
||||
background: #745d42 /* cocoa */ !important;
|
||||
color: #231c14 /* bistre */ !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
* Roughly equivalent to Vim's Error group. (Vim doesn't seem to have a direct
|
||||
* equivalent in its own matchparen plugin, but many syntax highlighting plugins
|
||||
* mark mismatched brackets as Error.)
|
||||
*/
|
||||
.cm-s-abbott span.CodeMirror-nonmatchingbracket {
|
||||
background: #d80450 /* crimson */ !important;
|
||||
color: #231c14 /* bistre */ !important;
|
||||
}
|
||||
|
||||
.cm-s-abbott .CodeMirror-matchingtag,
|
||||
.cm-s-abbott .cm-matchhighlight {
|
||||
outline: 1px solid #39a78d /* zomp */;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's CursorLine group. */
|
||||
.cm-s-abbott .CodeMirror-activeline-background,
|
||||
.cm-s-abbott .CodeMirror-activeline-gutter {
|
||||
background: #3c3022 /* chocolate */;
|
||||
}
|
||||
|
||||
/* Equivalent to Vim's CursorLineNr group. */
|
||||
.cm-s-abbott .CodeMirror-activeline-gutter .CodeMirror-linenumber {
|
||||
color: #d8ff84 /* pastel_chartreuse */;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Roughly equivalent to Vim's Folded group. */
|
||||
.cm-s-abbott .CodeMirror-foldmarker {
|
||||
color: #f63f05 /* cinnabar */;
|
||||
text-shadow: none;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
.cm-s-abcdef.CodeMirror { background: #0f0f0f; color: #defdef; }
|
||||
.cm-s-abcdef div.CodeMirror-selected { background: #515151; }
|
||||
.cm-s-abcdef .CodeMirror-line::selection, .cm-s-abcdef .CodeMirror-line > span::selection, .cm-s-abcdef .CodeMirror-line > span > span::selection { background: rgba(56, 56, 56, 0.99); }
|
||||
.cm-s-abcdef .CodeMirror-line::-moz-selection, .cm-s-abcdef .CodeMirror-line > span::-moz-selection, .cm-s-abcdef .CodeMirror-line > span > span::-moz-selection { background: rgba(56, 56, 56, 0.99); }
|
||||
.cm-s-abcdef .CodeMirror-gutters { background: #555; border-right: 2px solid #314151; }
|
||||
.cm-s-abcdef .CodeMirror-guttermarker { color: #222; }
|
||||
.cm-s-abcdef .CodeMirror-guttermarker-subtle { color: azure; }
|
||||
.cm-s-abcdef .CodeMirror-linenumber { color: #FFFFFF; }
|
||||
.cm-s-abcdef .CodeMirror-cursor { border-left: 1px solid #00FF00; }
|
||||
|
||||
.cm-s-abcdef span.cm-keyword { color: darkgoldenrod; font-weight: bold; }
|
||||
.cm-s-abcdef span.cm-atom { color: #77F; }
|
||||
.cm-s-abcdef span.cm-number { color: violet; }
|
||||
.cm-s-abcdef span.cm-def { color: #fffabc; }
|
||||
.cm-s-abcdef span.cm-variable { color: #abcdef; }
|
||||
.cm-s-abcdef span.cm-variable-2 { color: #cacbcc; }
|
||||
.cm-s-abcdef span.cm-variable-3, .cm-s-abcdef span.cm-type { color: #def; }
|
||||
.cm-s-abcdef span.cm-property { color: #fedcba; }
|
||||
.cm-s-abcdef span.cm-operator { color: #ff0; }
|
||||
.cm-s-abcdef span.cm-comment { color: #7a7b7c; font-style: italic;}
|
||||
.cm-s-abcdef span.cm-string { color: #2b4; }
|
||||
.cm-s-abcdef span.cm-meta { color: #C9F; }
|
||||
.cm-s-abcdef span.cm-qualifier { color: #FFF700; }
|
||||
.cm-s-abcdef span.cm-builtin { color: #30aabc; }
|
||||
.cm-s-abcdef span.cm-bracket { color: #8a8a8a; }
|
||||
.cm-s-abcdef span.cm-tag { color: #FFDD44; }
|
||||
.cm-s-abcdef span.cm-attribute { color: #DDFF00; }
|
||||
.cm-s-abcdef span.cm-error { color: #FF0000; }
|
||||
.cm-s-abcdef span.cm-header { color: aquamarine; font-weight: bold; }
|
||||
.cm-s-abcdef span.cm-link { color: blueviolet; }
|
||||
|
||||
.cm-s-abcdef .CodeMirror-activeline-background { background: #314151; }
|
||||
@@ -0,0 +1,5 @@
|
||||
.cm-s-ambiance.CodeMirror {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,44 @@
|
||||
/* Based on https://github.com/dempfi/ayu */
|
||||
|
||||
.cm-s-ayu-dark.CodeMirror { background: #0a0e14; color: #b3b1ad; }
|
||||
.cm-s-ayu-dark div.CodeMirror-selected { background: #273747; }
|
||||
.cm-s-ayu-dark .CodeMirror-line::selection, .cm-s-ayu-dark .CodeMirror-line > span::selection, .cm-s-ayu-dark .CodeMirror-line > span > span::selection { background: rgba(39, 55, 71, 99); }
|
||||
.cm-s-ayu-dark .CodeMirror-line::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 55, 71, 99); }
|
||||
.cm-s-ayu-dark .CodeMirror-gutters { background: #0a0e14; border-right: 0px; }
|
||||
.cm-s-ayu-dark .CodeMirror-guttermarker { color: white; }
|
||||
.cm-s-ayu-dark .CodeMirror-guttermarker-subtle { color: #3d424d; }
|
||||
.cm-s-ayu-dark .CodeMirror-linenumber { color: #3d424d; }
|
||||
.cm-s-ayu-dark .CodeMirror-cursor { border-left: 1px solid #e6b450; }
|
||||
.cm-s-ayu-dark.cm-fat-cursor .CodeMirror-cursor { background-color: #a2a8a175 !important; }
|
||||
.cm-s-ayu-dark .cm-animate-fat-cursor { background-color: #a2a8a175 !important; }
|
||||
|
||||
.cm-s-ayu-dark span.cm-comment { color: #626a73; }
|
||||
.cm-s-ayu-dark span.cm-atom { color: #ae81ff; }
|
||||
.cm-s-ayu-dark span.cm-number { color: #e6b450; }
|
||||
|
||||
.cm-s-ayu-dark span.cm-comment.cm-attribute { color: #ffb454; }
|
||||
.cm-s-ayu-dark span.cm-comment.cm-def { color: rgba(57, 186, 230, 80); }
|
||||
.cm-s-ayu-dark span.cm-comment.cm-tag { color: #39bae6; }
|
||||
.cm-s-ayu-dark span.cm-comment.cm-type { color: #5998a6; }
|
||||
|
||||
.cm-s-ayu-dark span.cm-property, .cm-s-ayu-dark span.cm-attribute { color: #ffb454; }
|
||||
.cm-s-ayu-dark span.cm-keyword { color: #ff8f40; }
|
||||
.cm-s-ayu-dark span.cm-builtin { color: #e6b450; }
|
||||
.cm-s-ayu-dark span.cm-string { color: #c2d94c; }
|
||||
|
||||
.cm-s-ayu-dark span.cm-variable { color: #b3b1ad; }
|
||||
.cm-s-ayu-dark span.cm-variable-2 { color: #f07178; }
|
||||
.cm-s-ayu-dark span.cm-variable-3 { color: #39bae6; }
|
||||
.cm-s-ayu-dark span.cm-type { color: #ff8f40; }
|
||||
.cm-s-ayu-dark span.cm-def { color: #ffee99; }
|
||||
.cm-s-ayu-dark span.cm-bracket { color: #f8f8f2; }
|
||||
.cm-s-ayu-dark span.cm-tag { color: rgba(57, 186, 230, 80); }
|
||||
.cm-s-ayu-dark span.cm-header { color: #c2d94c; }
|
||||
.cm-s-ayu-dark span.cm-link { color: #39bae6; }
|
||||
.cm-s-ayu-dark span.cm-error { color: #ff3333; }
|
||||
|
||||
.cm-s-ayu-dark .CodeMirror-activeline-background { background: #01060e; }
|
||||
.cm-s-ayu-dark .CodeMirror-matchingbracket {
|
||||
text-decoration: underline;
|
||||
color: white !important;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/* Based on https://github.com/dempfi/ayu */
|
||||
|
||||
.cm-s-ayu-mirage.CodeMirror { background: #1f2430; color: #cbccc6; }
|
||||
.cm-s-ayu-mirage div.CodeMirror-selected { background: #34455a; }
|
||||
.cm-s-ayu-mirage .CodeMirror-line::selection, .cm-s-ayu-mirage .CodeMirror-line > span::selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::selection { background: #34455a; }
|
||||
.cm-s-ayu-mirage .CodeMirror-line::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::-moz-selection { background: rgba(25, 30, 42, 99); }
|
||||
.cm-s-ayu-mirage .CodeMirror-gutters { background: #1f2430; border-right: 0px; }
|
||||
.cm-s-ayu-mirage .CodeMirror-guttermarker { color: white; }
|
||||
.cm-s-ayu-mirage .CodeMirror-guttermarker-subtle { color: rgba(112, 122, 140, 66); }
|
||||
.cm-s-ayu-mirage .CodeMirror-linenumber { color: rgba(61, 66, 77, 99); }
|
||||
.cm-s-ayu-mirage .CodeMirror-cursor { border-left: 1px solid #ffcc66; }
|
||||
.cm-s-ayu-mirage.cm-fat-cursor .CodeMirror-cursor {background-color: #a2a8a175 !important;}
|
||||
.cm-s-ayu-mirage .cm-animate-fat-cursor { background-color: #a2a8a175 !important; }
|
||||
|
||||
.cm-s-ayu-mirage span.cm-comment { color: #5c6773; font-style:italic; }
|
||||
.cm-s-ayu-mirage span.cm-atom { color: #ae81ff; }
|
||||
.cm-s-ayu-mirage span.cm-number { color: #ffcc66; }
|
||||
|
||||
.cm-s-ayu-mirage span.cm-comment.cm-attribute { color: #ffd580; }
|
||||
.cm-s-ayu-mirage span.cm-comment.cm-def { color: #d4bfff; }
|
||||
.cm-s-ayu-mirage span.cm-comment.cm-tag { color: #5ccfe6; }
|
||||
.cm-s-ayu-mirage span.cm-comment.cm-type { color: #5998a6; }
|
||||
|
||||
.cm-s-ayu-mirage span.cm-property { color: #f29e74; }
|
||||
.cm-s-ayu-mirage span.cm-attribute { color: #ffd580; }
|
||||
.cm-s-ayu-mirage span.cm-keyword { color: #ffa759; }
|
||||
.cm-s-ayu-mirage span.cm-builtin { color: #ffcc66; }
|
||||
.cm-s-ayu-mirage span.cm-string { color: #bae67e; }
|
||||
|
||||
.cm-s-ayu-mirage span.cm-variable { color: #cbccc6; }
|
||||
.cm-s-ayu-mirage span.cm-variable-2 { color: #f28779; }
|
||||
.cm-s-ayu-mirage span.cm-variable-3 { color: #5ccfe6; }
|
||||
.cm-s-ayu-mirage span.cm-type { color: #ffa759; }
|
||||
.cm-s-ayu-mirage span.cm-def { color: #ffd580; }
|
||||
.cm-s-ayu-mirage span.cm-bracket { color: rgba(92, 207, 230, 80); }
|
||||
.cm-s-ayu-mirage span.cm-tag { color: #5ccfe6; }
|
||||
.cm-s-ayu-mirage span.cm-header { color: #bae67e; }
|
||||
.cm-s-ayu-mirage span.cm-link { color: #5ccfe6; }
|
||||
.cm-s-ayu-mirage span.cm-error { color: #ff3333; }
|
||||
|
||||
.cm-s-ayu-mirage .CodeMirror-activeline-background { background: #191e2a; }
|
||||
.cm-s-ayu-mirage .CodeMirror-matchingbracket {
|
||||
text-decoration: underline;
|
||||
color: white !important;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
|
||||
Name: Base16 Default Dark
|
||||
Author: Chris Kempson (http://chriskempson.com)
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
|
||||
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||
|
||||
*/
|
||||
|
||||
.cm-s-base16-dark.CodeMirror { background: #151515; color: #e0e0e0; }
|
||||
.cm-s-base16-dark div.CodeMirror-selected { background: #303030; }
|
||||
.cm-s-base16-dark .CodeMirror-line::selection, .cm-s-base16-dark .CodeMirror-line > span::selection, .cm-s-base16-dark .CodeMirror-line > span > span::selection { background: rgba(48, 48, 48, .99); }
|
||||
.cm-s-base16-dark .CodeMirror-line::-moz-selection, .cm-s-base16-dark .CodeMirror-line > span::-moz-selection, .cm-s-base16-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(48, 48, 48, .99); }
|
||||
.cm-s-base16-dark .CodeMirror-gutters { background: #151515; border-right: 0px; }
|
||||
.cm-s-base16-dark .CodeMirror-guttermarker { color: #ac4142; }
|
||||
.cm-s-base16-dark .CodeMirror-guttermarker-subtle { color: #505050; }
|
||||
.cm-s-base16-dark .CodeMirror-linenumber { color: #505050; }
|
||||
.cm-s-base16-dark .CodeMirror-cursor { border-left: 1px solid #b0b0b0; }
|
||||
.cm-s-base16-dark.cm-fat-cursor .CodeMirror-cursor { background-color: #8e8d8875 !important; }
|
||||
.cm-s-base16-dark .cm-animate-fat-cursor { background-color: #8e8d8875 !important; }
|
||||
|
||||
.cm-s-base16-dark span.cm-comment { color: #8f5536; }
|
||||
.cm-s-base16-dark span.cm-atom { color: #aa759f; }
|
||||
.cm-s-base16-dark span.cm-number { color: #aa759f; }
|
||||
|
||||
.cm-s-base16-dark span.cm-property, .cm-s-base16-dark span.cm-attribute { color: #90a959; }
|
||||
.cm-s-base16-dark span.cm-keyword { color: #ac4142; }
|
||||
.cm-s-base16-dark span.cm-string { color: #f4bf75; }
|
||||
|
||||
.cm-s-base16-dark span.cm-variable { color: #90a959; }
|
||||
.cm-s-base16-dark span.cm-variable-2 { color: #6a9fb5; }
|
||||
.cm-s-base16-dark span.cm-def { color: #d28445; }
|
||||
.cm-s-base16-dark span.cm-bracket { color: #e0e0e0; }
|
||||
.cm-s-base16-dark span.cm-tag { color: #ac4142; }
|
||||
.cm-s-base16-dark span.cm-link { color: #aa759f; }
|
||||
.cm-s-base16-dark span.cm-error { background: #ac4142; color: #b0b0b0; }
|
||||
|
||||
.cm-s-base16-dark .CodeMirror-activeline-background { background: #202020; }
|
||||
.cm-s-base16-dark .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
|
||||
Name: Base16 Default Light
|
||||
Author: Chris Kempson (http://chriskempson.com)
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
|
||||
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||
|
||||
*/
|
||||
|
||||
.cm-s-base16-light.CodeMirror { background: #f5f5f5; color: #202020; }
|
||||
.cm-s-base16-light div.CodeMirror-selected { background: #e0e0e0; }
|
||||
.cm-s-base16-light .CodeMirror-line::selection, .cm-s-base16-light .CodeMirror-line > span::selection, .cm-s-base16-light .CodeMirror-line > span > span::selection { background: #e0e0e0; }
|
||||
.cm-s-base16-light .CodeMirror-line::-moz-selection, .cm-s-base16-light .CodeMirror-line > span::-moz-selection, .cm-s-base16-light .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }
|
||||
.cm-s-base16-light .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }
|
||||
.cm-s-base16-light .CodeMirror-guttermarker { color: #ac4142; }
|
||||
.cm-s-base16-light .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
|
||||
.cm-s-base16-light .CodeMirror-linenumber { color: #b0b0b0; }
|
||||
.cm-s-base16-light .CodeMirror-cursor { border-left: 1px solid #505050; }
|
||||
|
||||
.cm-s-base16-light span.cm-comment { color: #8f5536; }
|
||||
.cm-s-base16-light span.cm-atom { color: #aa759f; }
|
||||
.cm-s-base16-light span.cm-number { color: #aa759f; }
|
||||
|
||||
.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #90a959; }
|
||||
.cm-s-base16-light span.cm-keyword { color: #ac4142; }
|
||||
.cm-s-base16-light span.cm-string { color: #f4bf75; }
|
||||
|
||||
.cm-s-base16-light span.cm-variable { color: #90a959; }
|
||||
.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }
|
||||
.cm-s-base16-light span.cm-def { color: #d28445; }
|
||||
.cm-s-base16-light span.cm-bracket { color: #202020; }
|
||||
.cm-s-base16-light span.cm-tag { color: #ac4142; }
|
||||
.cm-s-base16-light span.cm-link { color: #aa759f; }
|
||||
.cm-s-base16-light span.cm-error { background: #ac4142; color: #505050; }
|
||||
|
||||
.cm-s-base16-light .CodeMirror-activeline-background { background: #DDDCDC; }
|
||||
.cm-s-base16-light .CodeMirror-matchingbracket { color: #f5f5f5 !important; background-color: #6A9FB5 !important}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
|
||||
Name: Bespin
|
||||
Author: Mozilla / Jan T. Sott
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
|
||||
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||
|
||||
*/
|
||||
|
||||
.cm-s-bespin.CodeMirror {background: #28211c; color: #9d9b97;}
|
||||
.cm-s-bespin div.CodeMirror-selected {background: #59554f !important;}
|
||||
.cm-s-bespin .CodeMirror-gutters {background: #28211c; border-right: 0px;}
|
||||
.cm-s-bespin .CodeMirror-linenumber {color: #666666;}
|
||||
.cm-s-bespin .CodeMirror-cursor {border-left: 1px solid #797977 !important;}
|
||||
|
||||
.cm-s-bespin span.cm-comment {color: #937121;}
|
||||
.cm-s-bespin span.cm-atom {color: #9b859d;}
|
||||
.cm-s-bespin span.cm-number {color: #9b859d;}
|
||||
|
||||
.cm-s-bespin span.cm-property, .cm-s-bespin span.cm-attribute {color: #54be0d;}
|
||||
.cm-s-bespin span.cm-keyword {color: #cf6a4c;}
|
||||
.cm-s-bespin span.cm-string {color: #f9ee98;}
|
||||
|
||||
.cm-s-bespin span.cm-variable {color: #54be0d;}
|
||||
.cm-s-bespin span.cm-variable-2 {color: #5ea6ea;}
|
||||
.cm-s-bespin span.cm-def {color: #cf7d34;}
|
||||
.cm-s-bespin span.cm-error {background: #cf6a4c; color: #797977;}
|
||||
.cm-s-bespin span.cm-bracket {color: #9d9b97;}
|
||||
.cm-s-bespin span.cm-tag {color: #cf6a4c;}
|
||||
.cm-s-bespin span.cm-link {color: #9b859d;}
|
||||
|
||||
.cm-s-bespin .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}
|
||||
.cm-s-bespin .CodeMirror-activeline-background { background: #404040; }
|
||||
@@ -0,0 +1,32 @@
|
||||
/* Port of TextMate's Blackboard theme */
|
||||
|
||||
.cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; }
|
||||
.cm-s-blackboard div.CodeMirror-selected { background: #253B76; }
|
||||
.cm-s-blackboard .CodeMirror-line::selection, .cm-s-blackboard .CodeMirror-line > span::selection, .cm-s-blackboard .CodeMirror-line > span > span::selection { background: rgba(37, 59, 118, .99); }
|
||||
.cm-s-blackboard .CodeMirror-line::-moz-selection, .cm-s-blackboard .CodeMirror-line > span::-moz-selection, .cm-s-blackboard .CodeMirror-line > span > span::-moz-selection { background: rgba(37, 59, 118, .99); }
|
||||
.cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; }
|
||||
.cm-s-blackboard .CodeMirror-guttermarker { color: #FBDE2D; }
|
||||
.cm-s-blackboard .CodeMirror-guttermarker-subtle { color: #888; }
|
||||
.cm-s-blackboard .CodeMirror-linenumber { color: #888; }
|
||||
.cm-s-blackboard .CodeMirror-cursor { border-left: 1px solid #A7A7A7; }
|
||||
|
||||
.cm-s-blackboard .cm-keyword { color: #FBDE2D; }
|
||||
.cm-s-blackboard .cm-atom { color: #D8FA3C; }
|
||||
.cm-s-blackboard .cm-number { color: #D8FA3C; }
|
||||
.cm-s-blackboard .cm-def { color: #8DA6CE; }
|
||||
.cm-s-blackboard .cm-variable { color: #FF6400; }
|
||||
.cm-s-blackboard .cm-operator { color: #FBDE2D; }
|
||||
.cm-s-blackboard .cm-comment { color: #AEAEAE; }
|
||||
.cm-s-blackboard .cm-string { color: #61CE3C; }
|
||||
.cm-s-blackboard .cm-string-2 { color: #61CE3C; }
|
||||
.cm-s-blackboard .cm-meta { color: #D8FA3C; }
|
||||
.cm-s-blackboard .cm-builtin { color: #8DA6CE; }
|
||||
.cm-s-blackboard .cm-tag { color: #8DA6CE; }
|
||||
.cm-s-blackboard .cm-attribute { color: #8DA6CE; }
|
||||
.cm-s-blackboard .cm-header { color: #FF6400; }
|
||||
.cm-s-blackboard .cm-hr { color: #AEAEAE; }
|
||||
.cm-s-blackboard .cm-link { color: #8DA6CE; }
|
||||
.cm-s-blackboard .cm-error { background: #9D1E15; color: #F8F8F8; }
|
||||
|
||||
.cm-s-blackboard .CodeMirror-activeline-background { background: #3C3636; }
|
||||
.cm-s-blackboard .CodeMirror-matchingbracket { outline:1px solid grey;color:white !important; }
|
||||
@@ -0,0 +1,25 @@
|
||||
.cm-s-cobalt.CodeMirror { background: #002240; color: white; }
|
||||
.cm-s-cobalt div.CodeMirror-selected { background: #b36539; }
|
||||
.cm-s-cobalt .CodeMirror-line::selection, .cm-s-cobalt .CodeMirror-line > span::selection, .cm-s-cobalt .CodeMirror-line > span > span::selection { background: rgba(179, 101, 57, .99); }
|
||||
.cm-s-cobalt .CodeMirror-line::-moz-selection, .cm-s-cobalt .CodeMirror-line > span::-moz-selection, .cm-s-cobalt .CodeMirror-line > span > span::-moz-selection { background: rgba(179, 101, 57, .99); }
|
||||
.cm-s-cobalt .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; }
|
||||
.cm-s-cobalt .CodeMirror-guttermarker { color: #ffee80; }
|
||||
.cm-s-cobalt .CodeMirror-guttermarker-subtle { color: #d0d0d0; }
|
||||
.cm-s-cobalt .CodeMirror-linenumber { color: #d0d0d0; }
|
||||
.cm-s-cobalt .CodeMirror-cursor { border-left: 1px solid white; }
|
||||
|
||||
.cm-s-cobalt span.cm-comment { color: #08f; }
|
||||
.cm-s-cobalt span.cm-atom { color: #845dc4; }
|
||||
.cm-s-cobalt span.cm-number, .cm-s-cobalt span.cm-attribute { color: #ff80e1; }
|
||||
.cm-s-cobalt span.cm-keyword { color: #ffee80; }
|
||||
.cm-s-cobalt span.cm-string { color: #3ad900; }
|
||||
.cm-s-cobalt span.cm-meta { color: #ff9d00; }
|
||||
.cm-s-cobalt span.cm-variable-2, .cm-s-cobalt span.cm-tag { color: #9effff; }
|
||||
.cm-s-cobalt span.cm-variable-3, .cm-s-cobalt span.cm-def, .cm-s-cobalt .cm-type { color: white; }
|
||||
.cm-s-cobalt span.cm-bracket { color: #d8d8d8; }
|
||||
.cm-s-cobalt span.cm-builtin, .cm-s-cobalt span.cm-special { color: #ff9e59; }
|
||||
.cm-s-cobalt span.cm-link { color: #845dc4; }
|
||||
.cm-s-cobalt span.cm-error { color: #9d1e15; }
|
||||
|
||||
.cm-s-cobalt .CodeMirror-activeline-background { background: #002D57; }
|
||||
.cm-s-cobalt .CodeMirror-matchingbracket { outline:1px solid grey;color:white !important; }
|
||||
@@ -0,0 +1,33 @@
|
||||
.cm-s-colorforth.CodeMirror { background: #000000; color: #f8f8f8; }
|
||||
.cm-s-colorforth .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; }
|
||||
.cm-s-colorforth .CodeMirror-guttermarker { color: #FFBD40; }
|
||||
.cm-s-colorforth .CodeMirror-guttermarker-subtle { color: #78846f; }
|
||||
.cm-s-colorforth .CodeMirror-linenumber { color: #bababa; }
|
||||
.cm-s-colorforth .CodeMirror-cursor { border-left: 1px solid white; }
|
||||
|
||||
.cm-s-colorforth span.cm-comment { color: #ededed; }
|
||||
.cm-s-colorforth span.cm-def { color: #ff1c1c; font-weight:bold; }
|
||||
.cm-s-colorforth span.cm-keyword { color: #ffd900; }
|
||||
.cm-s-colorforth span.cm-builtin { color: #00d95a; }
|
||||
.cm-s-colorforth span.cm-variable { color: #73ff00; }
|
||||
.cm-s-colorforth span.cm-string { color: #007bff; }
|
||||
.cm-s-colorforth span.cm-number { color: #00c4ff; }
|
||||
.cm-s-colorforth span.cm-atom { color: #606060; }
|
||||
|
||||
.cm-s-colorforth span.cm-variable-2 { color: #EEE; }
|
||||
.cm-s-colorforth span.cm-variable-3, .cm-s-colorforth span.cm-type { color: #DDD; }
|
||||
.cm-s-colorforth span.cm-property {}
|
||||
.cm-s-colorforth span.cm-operator {}
|
||||
|
||||
.cm-s-colorforth span.cm-meta { color: yellow; }
|
||||
.cm-s-colorforth span.cm-qualifier { color: #FFF700; }
|
||||
.cm-s-colorforth span.cm-bracket { color: #cc7; }
|
||||
.cm-s-colorforth span.cm-tag { color: #FFBD40; }
|
||||
.cm-s-colorforth span.cm-attribute { color: #FFF700; }
|
||||
.cm-s-colorforth span.cm-error { color: #f00; }
|
||||
|
||||
.cm-s-colorforth div.CodeMirror-selected { background: #333d53; }
|
||||
|
||||
.cm-s-colorforth span.cm-compilation { background: rgba(255, 255, 255, 0.12); }
|
||||
|
||||
.cm-s-colorforth .CodeMirror-activeline-background { background: #253540; }
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
Name: IntelliJ IDEA darcula theme
|
||||
From IntelliJ IDEA by JetBrains
|
||||
*/
|
||||
|
||||
.cm-s-darcula { font-family: Consolas, Menlo, Monaco, 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace, serif;}
|
||||
.cm-s-darcula.CodeMirror { background: #2B2B2B; color: #A9B7C6; }
|
||||
|
||||
.cm-s-darcula span.cm-meta { color: #BBB529; }
|
||||
.cm-s-darcula span.cm-number { color: #6897BB; }
|
||||
.cm-s-darcula span.cm-keyword { color: #CC7832; line-height: 1em; font-weight: bold; }
|
||||
.cm-s-darcula span.cm-def { color: #A9B7C6; font-style: italic; }
|
||||
.cm-s-darcula span.cm-variable { color: #A9B7C6; }
|
||||
.cm-s-darcula span.cm-variable-2 { color: #A9B7C6; }
|
||||
.cm-s-darcula span.cm-variable-3 { color: #9876AA; }
|
||||
.cm-s-darcula span.cm-type { color: #AABBCC; font-weight: bold; }
|
||||
.cm-s-darcula span.cm-property { color: #FFC66D; }
|
||||
.cm-s-darcula span.cm-operator { color: #A9B7C6; }
|
||||
.cm-s-darcula span.cm-string { color: #6A8759; }
|
||||
.cm-s-darcula span.cm-string-2 { color: #6A8759; }
|
||||
.cm-s-darcula span.cm-comment { color: #61A151; font-style: italic; }
|
||||
.cm-s-darcula span.cm-link { color: #CC7832; }
|
||||
.cm-s-darcula span.cm-atom { color: #CC7832; }
|
||||
.cm-s-darcula span.cm-error { color: #BC3F3C; }
|
||||
.cm-s-darcula span.cm-tag { color: #629755; font-weight: bold; font-style: italic; text-decoration: underline; }
|
||||
.cm-s-darcula span.cm-attribute { color: #6897bb; }
|
||||
.cm-s-darcula span.cm-qualifier { color: #6A8759; }
|
||||
.cm-s-darcula span.cm-bracket { color: #A9B7C6; }
|
||||
.cm-s-darcula span.cm-builtin { color: #FF9E59; }
|
||||
.cm-s-darcula span.cm-special { color: #FF9E59; }
|
||||
.cm-s-darcula span.cm-matchhighlight { color: #FFFFFF; background-color: rgba(50, 89, 48, .7); font-weight: normal;}
|
||||
.cm-s-darcula span.cm-searching { color: #FFFFFF; background-color: rgba(61, 115, 59, .7); font-weight: normal;}
|
||||
|
||||
.cm-s-darcula .CodeMirror-cursor { border-left: 1px solid #A9B7C6; }
|
||||
.cm-s-darcula .CodeMirror-activeline-background { background: #323232; }
|
||||
.cm-s-darcula .CodeMirror-gutters { background: #313335; border-right: 1px solid #313335; }
|
||||
.cm-s-darcula .CodeMirror-guttermarker { color: #FFEE80; }
|
||||
.cm-s-darcula .CodeMirror-guttermarker-subtle { color: #D0D0D0; }
|
||||
.cm-s-darcula .CodeMirrir-linenumber { color: #606366; }
|
||||
.cm-s-darcula .CodeMirror-matchingbracket { background-color: #3B514D; color: #FFEF28 !important; font-weight: bold; }
|
||||
|
||||
.cm-s-darcula div.CodeMirror-selected { background: #214283; }
|
||||
|
||||
.CodeMirror-hints.darcula {
|
||||
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
color: #9C9E9E;
|
||||
background-color: #3B3E3F !important;
|
||||
}
|
||||
|
||||
.CodeMirror-hints.darcula .CodeMirror-hint-active {
|
||||
background-color: #494D4E !important;
|
||||
color: #9C9E9E !important;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
|
||||
Name: dracula
|
||||
Author: Michael Kaminsky (http://github.com/mkaminsky11)
|
||||
|
||||
Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
|
||||
|
||||
*/
|
||||
|
||||
|
||||
.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {
|
||||
background-color: #282a36 !important;
|
||||
color: #f8f8f2 !important;
|
||||
border: none;
|
||||
}
|
||||
.cm-s-dracula .CodeMirror-gutters { color: #282a36; }
|
||||
.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }
|
||||
.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }
|
||||
.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }
|
||||
.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }
|
||||
.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }
|
||||
.cm-s-dracula span.cm-comment { color: #6272a4; }
|
||||
.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }
|
||||
.cm-s-dracula span.cm-number { color: #bd93f9; }
|
||||
.cm-s-dracula span.cm-variable { color: #50fa7b; }
|
||||
.cm-s-dracula span.cm-variable-2 { color: white; }
|
||||
.cm-s-dracula span.cm-def { color: #50fa7b; }
|
||||
.cm-s-dracula span.cm-operator { color: #ff79c6; }
|
||||
.cm-s-dracula span.cm-keyword { color: #ff79c6; }
|
||||
.cm-s-dracula span.cm-atom { color: #bd93f9; }
|
||||
.cm-s-dracula span.cm-meta { color: #f8f8f2; }
|
||||
.cm-s-dracula span.cm-tag { color: #ff79c6; }
|
||||
.cm-s-dracula span.cm-attribute { color: #50fa7b; }
|
||||
.cm-s-dracula span.cm-qualifier { color: #50fa7b; }
|
||||
.cm-s-dracula span.cm-property { color: #66d9ef; }
|
||||
.cm-s-dracula span.cm-builtin { color: #50fa7b; }
|
||||
.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }
|
||||
|
||||
.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }
|
||||
.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Name: DuoTone-Dark
|
||||
Author: by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg), adapted by Bram de Haan (https://github.com/atelierbram/)
|
||||
*/
|
||||
|
||||
.cm-s-duotone-dark.CodeMirror { background: #2a2734; color: #6c6783; }
|
||||
.cm-s-duotone-dark div.CodeMirror-selected { background: #545167!important; }
|
||||
.cm-s-duotone-dark .CodeMirror-gutters { background: #2a2734; border-right: 0px; }
|
||||
.cm-s-duotone-dark .CodeMirror-linenumber { color: #545167; }
|
||||
|
||||
/* begin cursor */
|
||||
.cm-s-duotone-dark .CodeMirror-cursor { border-left: 1px solid #ffad5c; /* border-left: 1px solid #ffad5c80; */ border-right: .5em solid #ffad5c; /* border-right: .5em solid #ffad5c80; */ opacity: .5; }
|
||||
.cm-s-duotone-dark .CodeMirror-activeline-background { background: #363342; /* background: #36334280; */ opacity: .5;}
|
||||
.cm-s-duotone-dark .cm-fat-cursor .CodeMirror-cursor { background: #ffad5c; /* background: #ffad5c80; */ opacity: .5;}
|
||||
/* end cursor */
|
||||
|
||||
.cm-s-duotone-dark span.cm-atom, .cm-s-duotone-dark span.cm-number, .cm-s-duotone-dark span.cm-keyword, .cm-s-duotone-dark span.cm-variable, .cm-s-duotone-dark span.cm-attribute, .cm-s-duotone-dark span.cm-quote, .cm-s-duotone-dark span.cm-hr, .cm-s-duotone-dark span.cm-link { color: #ffcc99; }
|
||||
|
||||
.cm-s-duotone-dark span.cm-property { color: #9a86fd; }
|
||||
.cm-s-duotone-dark span.cm-punctuation, .cm-s-duotone-dark span.cm-unit, .cm-s-duotone-dark span.cm-negative { color: #e09142; }
|
||||
.cm-s-duotone-dark span.cm-string { color: #ffb870; }
|
||||
.cm-s-duotone-dark span.cm-operator { color: #ffad5c; }
|
||||
.cm-s-duotone-dark span.cm-positive { color: #6a51e6; }
|
||||
|
||||
.cm-s-duotone-dark span.cm-variable-2, .cm-s-duotone-dark span.cm-variable-3, .cm-s-duotone-dark span.cm-type, .cm-s-duotone-dark span.cm-string-2, .cm-s-duotone-dark span.cm-url { color: #7a63ee; }
|
||||
.cm-s-duotone-dark span.cm-def, .cm-s-duotone-dark span.cm-tag, .cm-s-duotone-dark span.cm-builtin, .cm-s-duotone-dark span.cm-qualifier, .cm-s-duotone-dark span.cm-header, .cm-s-duotone-dark span.cm-em { color: #eeebff; }
|
||||
.cm-s-duotone-dark span.cm-bracket, .cm-s-duotone-dark span.cm-comment { color: #a7a5b2; }
|
||||
|
||||
/* using #f00 red for errors, don't think any of the colorscheme variables will stand out enough, ... maybe by giving it a background-color ... */
|
||||
.cm-s-duotone-dark span.cm-error, .cm-s-duotone-dark span.cm-invalidchar { color: #f00; }
|
||||
|
||||
.cm-s-duotone-dark span.cm-header { font-weight: normal; }
|
||||
.cm-s-duotone-dark .CodeMirror-matchingbracket { text-decoration: underline; color: #eeebff !important; }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user