the huge 117+ fixes update
80
README.md
|
@ -1,36 +1,64 @@
|
|||
# MSFX
|
||||
|
||||
Fork of [the MSFX Firefox userChrome](https://github.com/WinClassic/MSFX).
|
||||
|
||||
![Screenshot of MSFX](.assets/screenshot.png)
|
||||
|
||||
## Changes
|
||||
* Uses normal [firefox-scripts](https://github.com/xiaoxiaoflood/firefox-scripts) instead of a stripped down version for greater userscript compatibility
|
||||
* More accurate styles in some places
|
||||
* Bookmarks bar folder menus
|
||||
* All tabs button
|
||||
* Replaced with overflow arrows icon
|
||||
* Scrollbars forced to light theme
|
||||
* Selected address bar forced to light theme
|
||||
* Resize grabber no longer appears when maximized
|
||||
* Autoplay Pending indicator icon replaced
|
||||
* Pinned tabs no longer have extra padding
|
||||
* Internet status bar widget is its own component
|
||||
* Empty boxes have been removed in favor of putting extensions on the status bar
|
||||
* Address bar is no longer forced to the Address toolbar when opening a new window
|
||||
|
||||
- Uses normal [firefox-scripts](https://github.com/xiaoxiaoflood/firefox-scripts) instead of a stripped down version for greater userscript compatibility
|
||||
- Fixed up for 117+ and now bundled with again, as the upstream repo's maintainer is MIA.
|
||||
- More accurate styles in some places
|
||||
- Bookmarks bar folder menus
|
||||
- All tabs button
|
||||
- Replaced with overflow arrows icon
|
||||
- Scrollbars forced to light theme
|
||||
- Selected address bar forced to light theme
|
||||
- Resize grabber no longer appears when maximized
|
||||
- Autoplay Pending indicator icon replaced
|
||||
- Accurate menus (117+)
|
||||
- Unlocked toolbar style has proper bottom resizer (visual only)
|
||||
- Properly styled address bar dropdown
|
||||
- Proper hover/active states for menu bar items
|
||||
- Scrollbar dithering
|
||||
- Internet status bar widget is its own component
|
||||
- Empty boxes have been removed in favor of putting extensions on the status bar
|
||||
- Address bar is no longer forced to the Address toolbar (configurable, enabled by default)
|
||||
- Works with Firefox 117+ (fixes developed for on 122.0b3)
|
||||
- A lot of legacy styling was removed to where the `appearance` property does nothing, everything that used this to achieve the classic style has been accurately recreated as best as possible.
|
||||
|
||||
## What's missing?
|
||||
|
||||
- The stock extensions menu, the main menu and the overflow tabs menu are not styled, as I do not personally use them.
|
||||
- More configuration options probably. There might've been some things I commented out for my own liking that you may not agree with in terms of accuracy to IE5.
|
||||
- Default favicons for bookmark items, as these cannot be changed in pure CSS. (needs investigation)
|
||||
- Extension button tooltips don't seem to be standard tooltips.
|
||||
|
||||
## Known issues
|
||||
|
||||
- Opening "Customize Toolbar" resets the access key for the Favorites (bookmarks) menu item back to "B", resulting in "Favorites (B)"
|
||||
|
||||
## What can be improved?
|
||||
|
||||
- Stop using base64 URLs everywhere.
|
||||
- More configuration.
|
||||
- [Nested rulesets](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting), since Firefox itself uses them everywhere. (117+)
|
||||
- Some of the 117+ fixes have been using them (e.g. context menus)
|
||||
- Multiple files.
|
||||
|
||||
## Installation
|
||||
* Set the following keys in `about:config`:
|
||||
* `svg.context-properties.content.enabled` to `true`
|
||||
* `ui.prefersReducedMotion` to `1`
|
||||
* `browser.display.windows.non_native_menus` to `0`
|
||||
* Follow [firefox-scripts](https://github.com/xiaoxiaoflood/firefox-scripts) installation.
|
||||
* Install [Status Bar userscript](https://raw.githubusercontent.com/xiaoxiaoflood/firefox-scripts/master/chrome/status-bar.uc.js).
|
||||
* Merge `chrome` folder from the repo into your profile's `chrome` folder.
|
||||
|
||||
- Set the following keys in `about:config`:
|
||||
- `toolkit.legacyUserProfileCustomizations.stylesheets` to `true`
|
||||
- `svg.context-properties.content.enabled` to `true`
|
||||
- `ui.prefersReducedMotion` to `1`
|
||||
- ~~`browser.display.windows.non_native_menus` to `0`~~ (removed in 117+)
|
||||
- Copy the contents of `ffroot` into your Firefox install folder (typically `C:\Program Files\Mozilla Firefox`)
|
||||
- There is an extra set of folders added (`browser\chrome\icons\default`) to change the main window icon. The default is the IE5 page icon, but the 2001-2003 Mozilla icon is included as well, just needs to be renamed from `_main-window.ico` to `main-window.ico`.
|
||||
- Merge `chrome` folder from the repo into your profile's `chrome` folder. (path can be found via `about:profiles` or `about:support`)
|
||||
|
||||
## Recommendations
|
||||
* [Extensions Options Menu](https://raw.githubusercontent.com/xiaoxiaoflood/firefox-scripts/master/chrome/extensionOptionsMenu.uc.js)
|
||||
* More lightweight than the stock extensions menu, which is disabled by the theme anyways
|
||||
* [userChromeJS Manager](https://raw.githubusercontent.com/xiaoxiaoflood/firefox-scripts/master/chrome/rebuild_userChrome.uc.js)
|
||||
* [Save File to](https://raw.githubusercontent.com/xiaoxiaoflood/firefox-scripts/master/extensions/savefileto/savefileto.xpi)
|
||||
* **Requires extensions support in firefox-scripts**
|
||||
* "Replaces" save dialog for hybrid classic theme setup with Windhawk until investigation is done to fix the save dialog on classic enabled programs.
|
||||
|
||||
- [Extensions Options Menu](https://raw.githubusercontent.com/xiaoxiaoflood/firefox-scripts/master/chrome/extensionOptionsMenu.uc.js)
|
||||
- More lightweight than the stock extensions menu, which is "hidden" by the theme anyways
|
||||
- [userChromeJS Manager](https://raw.githubusercontent.com/xiaoxiaoflood/firefox-scripts/master/chrome/rebuild_userChrome.uc.js)
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
// 'Activity throbber' script for Firefox 60+ by Aris
|
||||
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var { Services } = Components.utils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
{}
|
||||
);
|
||||
var sss = Components.classes[
|
||||
(function () {
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var Services =
|
||||
globalThis.Services ||
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
var sss = Components.classes[
|
||||
"@mozilla.org/content/style-sheet-service;1"
|
||||
].getService(Components.interfaces.nsIStyleSheetService);
|
||||
].getService(Components.interfaces.nsIStyleSheetService);
|
||||
|
||||
var at_label = "Activity Throbber";
|
||||
var at_label = "Activity Throbber";
|
||||
|
||||
var ActivityThrobber = {
|
||||
var ActivityThrobber = {
|
||||
init: function () {
|
||||
try {
|
||||
document.addEventListener("TabAttrModified", _ActivityThrobber, false);
|
||||
|
@ -27,7 +26,9 @@ var ActivityThrobber = {
|
|||
.querySelector("#activity_throbber")
|
||||
.setAttribute("busy", "true");
|
||||
} else
|
||||
document.querySelector("#activity_throbber").removeAttribute("busy");
|
||||
document
|
||||
.querySelector("#activity_throbber")
|
||||
.removeAttribute("busy");
|
||||
}
|
||||
|
||||
// create a default toolbar button
|
||||
|
@ -77,6 +78,7 @@ var ActivityThrobber = {
|
|||
Components.utils.reportError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", ActivityThrobber.init(), false);
|
||||
document.addEventListener("DOMContentLoaded", ActivityThrobber.init(), false);
|
||||
})();
|
||||
|
|
|
@ -7,15 +7,14 @@
|
|||
//
|
||||
// workaround on Fx 71 to save/restore toolbar visibility
|
||||
// creating an observer array always fails, so observers are created manually atm.
|
||||
(function () {
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var Services =
|
||||
globalThis.Services ||
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
var appversion = parseInt(Services.appinfo.version);
|
||||
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var { Services } = Components.utils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
{}
|
||||
);
|
||||
var appversion = parseInt(Services.appinfo.version);
|
||||
|
||||
var AdditionalTopToolbars = {
|
||||
var AdditionalTopToolbars = {
|
||||
init: function () {
|
||||
/* blank tab workaround */
|
||||
try {
|
||||
|
@ -91,21 +90,17 @@ var AdditionalTopToolbars = {
|
|||
var uri = Services.io.newURI(
|
||||
"data:text/css;charset=utf-8," +
|
||||
encodeURIComponent(
|
||||
'\
|
||||
\
|
||||
toolbar[id^="additional_top_toolbar"] { \
|
||||
-moz-appearance: none !important; \
|
||||
background-color: var(--toolbar-bgcolor); \
|
||||
background-image: var(--toolbar-bgimage); \
|
||||
background-clip: padding-box; \
|
||||
color: var(--toolbar-color, inherit); \
|
||||
} \
|
||||
#main-window[customizing] toolbar[id^="additional_top_toolbar"] { \
|
||||
outline: 1px dashed !important; \
|
||||
outline-offset: -2px !important; \
|
||||
} \
|
||||
\
|
||||
'
|
||||
`toolbar[id^="additional_top_toolbar"] {
|
||||
-moz-appearance: none !important;
|
||||
background-color: var(--toolbar-bgcolor);
|
||||
background-image: var(--toolbar-bgimage);
|
||||
background-clip: padding-box;
|
||||
color: var(--toolbar-color, inherit);
|
||||
}
|
||||
#main-window[customizing] toolbar[id^="additional_top_toolbar"] {
|
||||
outline: 1px dashed !important;
|
||||
outline-offset: -2px !important;
|
||||
}`
|
||||
),
|
||||
null,
|
||||
null
|
||||
|
@ -125,11 +120,14 @@ var AdditionalTopToolbars = {
|
|||
);
|
||||
});
|
||||
});
|
||||
observer1.observe(document.querySelector("#additional_top_toolbar1"), {
|
||||
observer1.observe(
|
||||
document.querySelector("#additional_top_toolbar1"),
|
||||
{
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
if (number_of_additional_top_toolbars >= 2) {
|
||||
var observer2 = new MutationObserver(function (mutations) {
|
||||
|
@ -142,11 +140,14 @@ var AdditionalTopToolbars = {
|
|||
);
|
||||
});
|
||||
});
|
||||
observer2.observe(document.querySelector("#additional_top_toolbar2"), {
|
||||
observer2.observe(
|
||||
document.querySelector("#additional_top_toolbar2"),
|
||||
{
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
if (number_of_additional_top_toolbars >= 3) {
|
||||
var observer3 = new MutationObserver(function (mutations) {
|
||||
|
@ -159,11 +160,14 @@ var AdditionalTopToolbars = {
|
|||
);
|
||||
});
|
||||
});
|
||||
observer3.observe(document.querySelector("#additional_top_toolbar3"), {
|
||||
observer3.observe(
|
||||
document.querySelector("#additional_top_toolbar3"),
|
||||
{
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
if (number_of_additional_top_toolbars >= 4) {
|
||||
var observer4 = new MutationObserver(function (mutations) {
|
||||
|
@ -176,11 +180,14 @@ var AdditionalTopToolbars = {
|
|||
);
|
||||
});
|
||||
});
|
||||
observer4.observe(document.querySelector("#additional_top_toolbar4"), {
|
||||
observer4.observe(
|
||||
document.querySelector("#additional_top_toolbar4"),
|
||||
{
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
if (number_of_additional_top_toolbars >= 5) {
|
||||
var observer5 = new MutationObserver(function (mutations) {
|
||||
|
@ -193,26 +200,30 @@ var AdditionalTopToolbars = {
|
|||
);
|
||||
});
|
||||
});
|
||||
observer5.observe(document.querySelector("#additional_top_toolbar5"), {
|
||||
observer5.observe(
|
||||
document.querySelector("#additional_top_toolbar5"),
|
||||
{
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/* initialization delay workaround */
|
||||
document.addEventListener(
|
||||
/* initialization delay workaround */
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
AdditionalTopToolbars.init(),
|
||||
false
|
||||
);
|
||||
);
|
||||
|
||||
// not needed anymore, but just in case someone prefers initialization that way
|
||||
/*
|
||||
// not needed anymore, but just in case someone prefers initialization that way
|
||||
/*
|
||||
setTimeout(function(){
|
||||
AdditionalTopToolbars.init();
|
||||
},500);
|
||||
*/
|
||||
})();
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
function waitForElm(selector) {
|
||||
return new Promise(resolve => {
|
||||
(function () {
|
||||
function waitForElm(selector) {
|
||||
return new Promise((resolve) => {
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve(document.querySelector(selector));
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(mutations => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (document.querySelector(selector)) {
|
||||
resolve(document.querySelector(selector));
|
||||
observer.disconnect();
|
||||
|
@ -13,30 +14,39 @@ function waitForElm(selector) {
|
|||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
subtree: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
waitForElm('#find-button').then((elm) => {
|
||||
waitForElm("#find-button").then((elm) => {
|
||||
var find = document.querySelector("#find-button label.toolbarbutton-text");
|
||||
if (find)
|
||||
find.setAttribute("value", "Search");
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
waitForElm('#nav-bar').then((elm) => {
|
||||
var library = document.querySelector("#bookmarks-menu-button label.toolbarbutton-text");
|
||||
setTimeout(function () {
|
||||
waitForElm("#nav-bar").then((elm) => {
|
||||
var library = document.querySelector(
|
||||
"#bookmarks-menu-button label.toolbarbutton-text"
|
||||
);
|
||||
if (library)
|
||||
library.setAttribute("value", "Favorites");
|
||||
});
|
||||
}, 0);
|
||||
}, 0);
|
||||
|
||||
waitForElm('#bookmarksMenu').then((elm) => {
|
||||
var bookmarksmenu = document.querySelector("#bookmarksMenu label.menubar-text");
|
||||
waitForElm("#bookmarksMenu").then((elm) => {
|
||||
var bookmarksmenu = document.querySelector(
|
||||
"#bookmarksMenu label.menubar-text"
|
||||
);
|
||||
if (bookmarksmenu)
|
||||
bookmarksmenu.setAttribute("value", "Favorites");
|
||||
bookmarksmenu.setAttribute("accesskey", "a");
|
||||
});
|
||||
//bookmarksmenu.setAttribute("accesskey", "a");
|
||||
});
|
||||
|
||||
waitForElm('#bookmarksMenu').then((elm) => {
|
||||
waitForElm("#bookmarksMenu").then((elm) => {
|
||||
var bookmarksmenu = document.querySelector("#bookmarksMenu");
|
||||
bookmarksmenu.setAttribute("accesskey", "a");
|
||||
});
|
||||
if (bookmarksmenu)
|
||||
bookmarksmenu.removeAttribute("accesskey");
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var { Services } = Components.utils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
{}
|
||||
);
|
||||
var sss = Components.classes[
|
||||
(function () {
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var Services =
|
||||
globalThis.Services ||
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
var sss = Components.classes[
|
||||
"@mozilla.org/content/style-sheet-service;1"
|
||||
].getService(Components.interfaces.nsIStyleSheetService);
|
||||
].getService(Components.interfaces.nsIStyleSheetService);
|
||||
|
||||
var IE6StatusBar = {
|
||||
var IE6StatusBar = {
|
||||
init: function () {
|
||||
try {
|
||||
// create a default toolbar button
|
||||
|
@ -65,6 +65,7 @@ var IE6StatusBar = {
|
|||
Components.utils.reportError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", IE6StatusBar.init(), false);
|
||||
document.addEventListener("DOMContentLoaded", IE6StatusBar.init(), false);
|
||||
})();
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
(function () {
|
||||
try {
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var { Services } = Components.utils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
{}
|
||||
);
|
||||
var Services =
|
||||
globalThis.Services ||
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
var sss = Components.classes[
|
||||
"@mozilla.org/content/style-sheet-service;1"
|
||||
].getService(Components.interfaces.nsIStyleSheetService);
|
||||
|
|
|
@ -2,40 +2,47 @@
|
|||
// option: place urlbar on a different toolbar
|
||||
// option: place back button on a different toolbar
|
||||
// option: place back button on a different toolbar
|
||||
|
||||
var { CustomizableUI } = Components.utils.import(
|
||||
(function () {
|
||||
var {CustomizableUI} = Components.utils.import(
|
||||
"resource:///modules/CustomizableUI.jsm",
|
||||
{}
|
||||
);
|
||||
);
|
||||
|
||||
var navigation = CustomizableUI.AREA_NAVBAR;
|
||||
var tabs = CustomizableUI.AREA_TABSTRIP;
|
||||
var menu = CustomizableUI.AREA_MENUBAR;
|
||||
var bookmarks = CustomizableUI.AREA_BOOKMARKS;
|
||||
var navigation = CustomizableUI.AREA_NAVBAR;
|
||||
var tabs = CustomizableUI.AREA_TABSTRIP;
|
||||
var menu = CustomizableUI.AREA_MENUBAR;
|
||||
var bookmarks = CustomizableUI.AREA_BOOKMARKS;
|
||||
|
||||
/* [target toolbar of item]
|
||||
/* [target toolbar of item]
|
||||
menu = 'menubar'
|
||||
tabs = 'tabs toolbar'
|
||||
bookmarks = 'bookmarks toolbar'
|
||||
navigation='navigation toolbar' */
|
||||
var backbutton_on_toolbar = navigation;
|
||||
var forwardbutton_on_toolbar = navigation;
|
||||
var urlbar_on_toolbar = navigation;
|
||||
var backbutton_on_toolbar = navigation;
|
||||
var forwardbutton_on_toolbar = navigation;
|
||||
var urlbar_on_toolbar = navigation;
|
||||
|
||||
/* [target position of item]
|
||||
/* [target position of item]
|
||||
0 = 1st
|
||||
1 = 2nd
|
||||
2 = 3rd
|
||||
...
|
||||
x = xth */
|
||||
var backbutton_on_toolbar_position = 0;
|
||||
var forwardbutton_on_toolbar_position = 1;
|
||||
var urlbar_on_toolbar_position = 0;
|
||||
var backbutton_on_toolbar_position = 0;
|
||||
var forwardbutton_on_toolbar_position = 1;
|
||||
var urlbar_on_toolbar_position = 0;
|
||||
|
||||
var MoveUrlbar = {
|
||||
const separateAddressBar = true;
|
||||
const moveNavigation = true;
|
||||
const bottomTabs = true;
|
||||
const menuToTop = true;
|
||||
|
||||
var MoveUrlbar = {
|
||||
init: function () {
|
||||
try {
|
||||
document.getElementById("back-button").setAttribute("removable", "true");
|
||||
document
|
||||
.getElementById("back-button")
|
||||
.setAttribute("removable", "true");
|
||||
document
|
||||
.getElementById("forward-button")
|
||||
.setAttribute("removable", "true");
|
||||
|
@ -43,20 +50,31 @@ var MoveUrlbar = {
|
|||
.getElementById("urlbar-container")
|
||||
.setAttribute("removable", "true");
|
||||
document
|
||||
.getElementById("additional_top_toolbar1")
|
||||
.setAttribute("fullscreentoolbar", "true");
|
||||
.getElementById("unified-extensions-button")
|
||||
.setAttribute("removable", "true");
|
||||
} catch (e) {}
|
||||
|
||||
/*CustomizableUI.addWidgetToArea("back-button", backbutton_on_toolbar);
|
||||
// Forces back/forward to navigation bar
|
||||
if (moveNavigation) {
|
||||
CustomizableUI.addWidgetToArea("back-button", backbutton_on_toolbar);
|
||||
CustomizableUI.moveWidgetWithinArea(
|
||||
"back-button",
|
||||
backbutton_on_toolbar_position
|
||||
);
|
||||
CustomizableUI.addWidgetToArea("forward-button", forwardbutton_on_toolbar);
|
||||
CustomizableUI.addWidgetToArea(
|
||||
"forward-button",
|
||||
forwardbutton_on_toolbar
|
||||
);
|
||||
CustomizableUI.moveWidgetWithinArea(
|
||||
"forward-button",
|
||||
forwardbutton_on_toolbar_position
|
||||
);
|
||||
}
|
||||
|
||||
// Forces address bar to additional_top_toolbar1 (Address)
|
||||
// THIS WILL SOFTBRICK YOUR FIREFOX IF ADDITIONAL TOP TOOLBARS AREN'T
|
||||
// ENABLED BUT THIS IS
|
||||
if (separateAddressBar) {
|
||||
CustomizableUI.addWidgetToArea(
|
||||
"urlbar-container",
|
||||
"additional_top_toolbar1"
|
||||
|
@ -64,20 +82,34 @@ var MoveUrlbar = {
|
|||
CustomizableUI.moveWidgetWithinArea(
|
||||
"urlbar-container",
|
||||
urlbar_on_toolbar_position
|
||||
);*/
|
||||
);
|
||||
}
|
||||
|
||||
// Moves tabs to the bottom of the toolbar stack
|
||||
if (bottomTabs) {
|
||||
document
|
||||
.getElementById("navigator-toolbox")
|
||||
.appendChild(document.getElementById("TabsToolbar"));
|
||||
}
|
||||
|
||||
/*
|
||||
try {
|
||||
document.getElementById('back-button').setAttribute('removable','false');
|
||||
document.getElementById('forward-button').setAttribute('removable','false');
|
||||
document.getElementById('urlbar-container').setAttribute('removable','false');
|
||||
} catch(e){}
|
||||
*/
|
||||
// Moves the main menu button to the top row, before the last item
|
||||
// (activity throbber). This is because the icon is hidden but not the
|
||||
// button and creates bad padding if you're using the navigation bar as
|
||||
// an address bar
|
||||
if (menuToTop) {
|
||||
setTimeout(() => {
|
||||
document
|
||||
.getElementById("PanelUI-button")
|
||||
.setAttribute("removable", "true");
|
||||
const menubar = document.getElementById(menu);
|
||||
menubar.insertBefore(
|
||||
document.getElementById("PanelUI-button"),
|
||||
menubar.lastChild
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", MoveUrlbar.init(), false);
|
||||
document.addEventListener("DOMContentLoaded", MoveUrlbar.init(), false);
|
||||
})();
|
||||
|
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
2503
chrome/msfx/msfx.css
Normal file
BIN
chrome/msfx/toolbar.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
chrome/msfx/toolbar_large.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
|
@ -9,23 +9,21 @@
|
|||
scrollbar[disabled="true"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar
|
||||
{
|
||||
scrollbar
|
||||
{
|
||||
color-scheme: light !important;
|
||||
-moz-default-appearance: none;
|
||||
-moz-binding: url("chrome://global/content/bindings/scrollbar.xml#scrollbar");
|
||||
cursor: default;
|
||||
min-width: 16px !important;
|
||||
background-repeat: repeat !important;
|
||||
background-color: ThreeDHighlight !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdib3g9IjAgMCAyIDIiIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIGZpbGw9Ii1tb3otZGlhbG9nIi8+PHJlY3QgZmlsbD0iVGhyZWVESGlnaGxpZ2h0IiB4PSIxIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMSIgeT0iMSIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0iLW1vei1kaWFsb2ciLz48cmVjdCBmaWxsPSJUaHJlZURIaWdobGlnaHQiIHg9IjAiIHk9IjEiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48L3N2Zz4=") !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
thumb
|
||||
{
|
||||
thumb
|
||||
{
|
||||
color-scheme: light !important;
|
||||
-moz-default-appearance: none !important;
|
||||
position: relative !important;
|
||||
|
@ -34,113 +32,118 @@ thumb
|
|||
pointer-events: auto !important;
|
||||
border: 0 !important;
|
||||
box-shadow: inset -1px -1px 0 WindowFrame, inset 1px 1px 0 -moz-Dialog, inset -2px -2px 0 ThreeDShadow, inset 2px 2px 0 ThreeDHighlight !important;
|
||||
}
|
||||
}
|
||||
|
||||
thumb[orient="horizontal"] {
|
||||
thumb[orient="horizontal"] {
|
||||
-moz-default-appearance: none;
|
||||
min-height: 16px !important;
|
||||
min-width: 8px !important;
|
||||
max-width: 100% !important;
|
||||
background-repeat: no-repeat !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbarbutton
|
||||
{
|
||||
scrollbarbutton
|
||||
{
|
||||
color-scheme: light !important;
|
||||
min-width: 16px !important;
|
||||
min-height: 16px !important;
|
||||
-moz-default-appearance: none !important;
|
||||
background-color: -moz-Dialog !important;
|
||||
box-shadow: inset -1px -1px 0 WindowFrame, inset 1px 1px 0 -moz-Dialog, inset -2px -2px 0 ThreeDShadow, inset 2px 2px 0 ThreeDHighlight !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbarbutton:not(.disabled):hover:active
|
||||
{
|
||||
scrollbarbutton:not(.disabled):hover:active
|
||||
{
|
||||
background-color: -moz-dialog !important;
|
||||
border: 1px solid ThreeDShadow !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
slider,
|
||||
slider[orient="vertical"]
|
||||
{
|
||||
slider,
|
||||
slider[orient="vertical"]
|
||||
{
|
||||
color-scheme: light !important;
|
||||
-moz-default-appearance: none;
|
||||
}
|
||||
background-color: ThreeDHighlight !important;
|
||||
background-repeat: repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMiIgaGVpZ2h0PSIyIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAuNTI5MTcgLjUyOTE3IiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnPjxnPjxyZWN0IHg9Ii4yNjQ1OCIgeT0iMC4wMDAwIiB3aWR0aD0iLjI2NDU4IiBoZWlnaHQ9Ii4yNjQ1OCIgZmlsbD0iVGhyZWVEU2hhZG93Ii8+PHJlY3QgeD0iMC4wMDAwIiB5PSIuMjY0NTgiIHdpZHRoPSIuMjY0NTgiIGhlaWdodD0iLjI2NDU4IiBmaWxsPSJUaHJlZURTaGFkb3ciLz48cmVjdCB4PSIwLjAwMDAiIHk9IjAuMDAwMCIgd2lkdGg9Ii4yNjQ1OCIgaGVpZ2h0PSIuMjY0NTgiIGZpbGw9IlRocmVlREhpZ2hsaWdodCIvPjxyZWN0IHg9Ii4yNjQ1OCIgeT0iLjI2NDU4IiB3aWR0aD0iLjI2NDU4IiBoZWlnaHQ9Ii4yNjQ1OCIgZmlsbD0iVGhyZWVESGlnaGxpZ2h0Ii8+PC9nPjwvZz48L3N2Zz4=") !important;
|
||||
}
|
||||
|
||||
scrollcorner
|
||||
{
|
||||
scrollcorner
|
||||
{
|
||||
color-scheme: light !important;
|
||||
-moz-default-appearance: none !important;
|
||||
-moz-binding: url(chrome://global/content/bindings/scrollbar.xml#scrollbar-base);
|
||||
width: 16px;
|
||||
cursor: default;
|
||||
background-color: -moz-dialog; !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbarbutton[type="increment"]
|
||||
{
|
||||
scrollbarbutton[type="increment"]
|
||||
{
|
||||
-moz-default-appearance: none;
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSI3IiB5PSI0IiB4PSI1IiBmaWxsPSJtZW51dGV4dCIvPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjUiIHk9IjUiIHg9IjYiIGZpbGw9Im1lbnV0ZXh0Ii8+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iMyIgeT0iNiIgeD0iNyIgZmlsbD0ibWVudXRleHQiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB5PSI3IiB4PSI4IiBmaWxsPSJtZW51dGV4dCIvPg0KCQ0KCQ0KPC9zdmc+") !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4KICA8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSI3IiB5PSI0IiB4PSI1IiBmaWxsPSJDYW52YXNUZXh0Ii8+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iNSIgeT0iNSIgeD0iNiIgZmlsbD0iQ2FudmFzVGV4dCIvPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjMiIHk9IjYiIHg9IjciIGZpbGw9IkNhbnZhc1RleHQiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB5PSI3IiB4PSI4IiBmaWxsPSJDYW52YXNUZXh0Ii8+CiAgCiAgCjwvc3ZnPg==") !important;
|
||||
background-position: center center !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="increment"]
|
||||
{
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="increment"]
|
||||
{
|
||||
-moz-default-appearance: none;
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTUiIHdpZHRoPSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCBoZWlnaHQ9IjEiIHdpZHRoPSI3IiB4PSIzIiB5PSI1IiBmaWxsPSJtZW51dGV4dCIvPjxyZWN0IGhlaWdodD0iMSIgd2lkdGg9IjUiIHk9IjYiIHg9IjQiIGZpbGw9Im1lbnV0ZXh0Ii8+PHJlY3QgaGVpZ2h0PSIxIiB3aWR0aD0iMyIgeD0iNSIgeT0iNyIgZmlsbD0ibWVudXRleHQiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4PSI2IiB5PSI4IiBmaWxsPSJtZW51dGV4dCIvPg0KCQ0KCQ0KPC9zdmc+") !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTUiIHdpZHRoPSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4KICA8cmVjdCBoZWlnaHQ9IjEiIHdpZHRoPSI3IiB4PSIzIiB5PSI1IiBmaWxsPSJDYW52YXNUZXh0Ii8+PHJlY3QgaGVpZ2h0PSIxIiB3aWR0aD0iNSIgeT0iNiIgeD0iNCIgZmlsbD0iQ2FudmFzVGV4dCIvPjxyZWN0IGhlaWdodD0iMSIgd2lkdGg9IjMiIHg9IjUiIHk9IjciIGZpbGw9IkNhbnZhc1RleHQiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4PSI2IiB5PSI4IiBmaWxsPSJDYW52YXNUZXh0Ii8+CiAgCiAgCjwvc3ZnPg==") !important;
|
||||
background-position: center center !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbarbutton[type="decrement"]
|
||||
{
|
||||
scrollbarbutton[type="decrement"]
|
||||
{
|
||||
-moz-default-appearance: none;
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTUiIHdpZHRoPSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSI3IiB5PSI0IiB4PSI4IiBmaWxsPSJtZW51dGV4dCIvPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjUiIHk9IjUiIHg9IjciIGZpbGw9Im1lbnV0ZXh0Ii8+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iMyIgeT0iNiIgeD0iNiIgZmlsbD0ibWVudXRleHQiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB5PSI3IiB4PSI1IiBmaWxsPSJtZW51dGV4dCIvPg0KCQ0KCQ0KPC9zdmc+") !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTUiIHdpZHRoPSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4KICA8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSI3IiB5PSI0IiB4PSI4IiBmaWxsPSJDYW52YXNUZXh0Ii8+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iNSIgeT0iNSIgeD0iNyIgZmlsbD0iQ2FudmFzVGV4dCIvPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjMiIHk9IjYiIHg9IjYiIGZpbGw9IkNhbnZhc1RleHQiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB5PSI3IiB4PSI1IiBmaWxsPSJDYW52YXNUZXh0Ii8+CiAgCiAgCjwvc3ZnPg==") !important;
|
||||
background-position: center center !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="decrement"]
|
||||
{
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="decrement"]
|
||||
{
|
||||
-moz-default-appearance: none;
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCBoZWlnaHQ9IjEiIHdpZHRoPSI3IiB5PSI4IiB4PSIzIiBmaWxsPSJtZW51dGV4dCIvPjxyZWN0IGhlaWdodD0iMSIgd2lkdGg9IjUiIHg9IjQiIHk9IjciIGZpbGw9Im1lbnV0ZXh0Ii8+PHJlY3QgaGVpZ2h0PSIxIiB3aWR0aD0iMyIgeT0iNiIgeD0iNSIgZmlsbD0ibWVudXRleHQiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB5PSI1IiB4PSI2IiBmaWxsPSJtZW51dGV4dCIvPg0KCQ0KCQ0KPC9zdmc+") !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4KCTxyZWN0IGhlaWdodD0iMSIgd2lkdGg9IjciIHk9IjgiIHg9IjMiIGZpbGw9IkNhbnZhc1RleHQiLz48cmVjdCBoZWlnaHQ9IjEiIHdpZHRoPSI1IiB4PSI0IiB5PSI3IiBmaWxsPSJDYW52YXNUZXh0Ii8+PHJlY3QgaGVpZ2h0PSIxIiB3aWR0aD0iMyIgeT0iNiIgeD0iNSIgZmlsbD0iQ2FudmFzVGV4dCIvPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHk9IjUiIHg9IjYiIGZpbGw9IkNhbnZhc1RleHQiLz4KCQoJCjwvc3ZnPg==") !important;
|
||||
background-position: center center !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbarbutton[type="increment"][disabled="true"]
|
||||
{
|
||||
scrollbarbutton[type="increment"][disabled="true"]
|
||||
{
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIHdpZHRoPSIxIiBoZWlnaHQ9IjciIHk9IjQiIHg9IjUiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIyIiBmaWxsPSJUaHJlZURIaWdobGlnaHQiIHg9IjYiIHk9IjEwIi8+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iMiIgZmlsbD0iVGhyZWVESGlnaGxpZ2h0IiB4PSI3IiB5PSI5Ii8+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iMiIgZmlsbD0iVGhyZWVESGlnaGxpZ2h0IiB4PSI4IiB5PSI4Ii8+PHJlY3Qgd2lkdGg9IjEiIGZpbGw9IlRocmVlREhpZ2hsaWdodCIgaGVpZ2h0PSIxIiB5PSI4IiB4PSI5Ii8+PHJlY3QgZmlsbD0iVGhyZWVEU2hhZG93IiB3aWR0aD0iMSIgaGVpZ2h0PSI1IiB5PSI1IiB4PSI2Ii8+PHJlY3QgZmlsbD0iVGhyZWVEU2hhZG93IiB3aWR0aD0iMSIgaGVpZ2h0PSIzIiB5PSI2IiB4PSI3Ii8+PHJlY3QgZmlsbD0iVGhyZWVEU2hhZG93IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB5PSI3IiB4PSI4Ii8+DQoJDQoJDQo8L3N2Zz4=");
|
||||
background-position: center center !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="increment"][disabled="true"]
|
||||
{
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="increment"][disabled="true"]
|
||||
{
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTUiIHdpZHRoPSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIGhlaWdodD0iMSIgd2lkdGg9IjciIHg9IjMiIHk9IjUiLz48cmVjdCBmaWxsPSJUaHJlZURIaWdobGlnaHQiIGhlaWdodD0iMSIgd2lkdGg9IjIiIHk9IjYiIHg9IjkiLz48cmVjdCBmaWxsPSJUaHJlZURIaWdobGlnaHQiIGhlaWdodD0iMSIgd2lkdGg9IjIiIHk9IjciIHg9IjgiLz48cmVjdCBmaWxsPSJUaHJlZURIaWdobGlnaHQiIGhlaWdodD0iMSIgd2lkdGg9IjIiIHg9IjciIHk9IjgiLz48cmVjdCBmaWxsPSJUaHJlZURIaWdobGlnaHQiIGhlaWdodD0iMSIgd2lkdGg9IjEiIHg9IjciIHk9IjkiLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIGhlaWdodD0iMSIgd2lkdGg9IjUiIHk9IjYiIHg9IjQiLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIGhlaWdodD0iMSIgd2lkdGg9IjMiIHg9IjUiIHk9IjciLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHg9IjYiIHk9IjgiLz4NCgkNCgkNCjwvc3ZnPg==");
|
||||
background-position: center center !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbarbutton[type="decrement"][disabled="true"]
|
||||
{
|
||||
scrollbarbutton[type="decrement"][disabled="true"]
|
||||
{
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTUiIHdpZHRoPSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIHdpZHRoPSIxIiBoZWlnaHQ9IjciIHk9IjQiIHg9IjgiLz48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSI3IiBmaWxsPSJUaHJlZURIaWdobGlnaHQiIHk9IjUiIHg9IjkiLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIHdpZHRoPSIxIiBoZWlnaHQ9IjUiIHk9IjUiIHg9IjciLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIHdpZHRoPSIxIiBoZWlnaHQ9IjMiIHk9IjYiIHg9IjYiLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHk9IjciIHg9IjUiLz4NCgkNCgkNCjwvc3ZnPg==");
|
||||
background-position: center center !important;
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="decrement"][disabled="true"]
|
||||
{
|
||||
scrollbar[orient="vertical"] > scrollbarbutton[type="decrement"][disabled="true"]
|
||||
{
|
||||
background-repeat: no-repeat !important;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDE1IDE1Ij4NCgk8cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIGhlaWdodD0iMSIgd2lkdGg9IjciIHk9IjgiIHg9IjMiLz48cmVjdCBmaWxsPSJUaHJlZURIaWdobGlnaHQiIHdpZHRoPSI3IiBoZWlnaHQ9IjEiIHk9IjkiIHg9IjQiLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIGhlaWdodD0iMSIgd2lkdGg9IjUiIHg9IjQiIHk9IjciLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIGhlaWdodD0iMSIgd2lkdGg9IjMiIHk9IjYiIHg9IjUiLz48cmVjdCBmaWxsPSJUaHJlZURTaGFkb3ciIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHk9IjUiIHg9IjYiLz4NCgkNCgkNCjwvc3ZnPg==") !important;
|
||||
background-position: center !important;
|
||||
}`;
|
||||
}`;
|
||||
var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(
|
||||
Ci.nsIStyleSheetService
|
||||
);
|
||||
var uri = makeURI("data:text/css;charset=UTF=8," + encodeURIComponent(css));
|
||||
var uri = Services.io.newURI(
|
||||
"data:text/css;charset=UTF=8," + encodeURIComponent(css)
|
||||
);
|
||||
|
||||
sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET);
|
||||
})();
|
||||
|
|
|
@ -12,22 +12,32 @@
|
|||
// [!] BUG: do not move main 'space'-item to palette or it will be hidden until customizing mode gets reopened
|
||||
|
||||
// [!] Fix for WebExtensions with own windows by 黒仪大螃蟹 (for 1-N scripts)
|
||||
(function () {
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var appversion = parseInt(Services.appinfo.version);
|
||||
|
||||
|
||||
Components.utils.import("resource:///modules/CustomizableUI.jsm");
|
||||
var {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {});
|
||||
var appversion = parseInt(Services.appinfo.version);
|
||||
|
||||
var AddSeparator = {
|
||||
init: function() {
|
||||
|
||||
if (appversion >= 76 && location != 'chrome://browser/content/browser.xhtml')
|
||||
var AddSeparator = {
|
||||
init: function () {
|
||||
if (
|
||||
appversion >= 76 &&
|
||||
location != "chrome://browser/content/browser.xhtml"
|
||||
)
|
||||
return;
|
||||
|
||||
try {
|
||||
document
|
||||
.getElementById("back-button")
|
||||
.setAttribute("removable", "true");
|
||||
document
|
||||
.getElementById("forward-button")
|
||||
.setAttribute("removable", "true");
|
||||
} catch (e) {}
|
||||
|
||||
/* blank tab workaround */
|
||||
try {
|
||||
if(gBrowser.selectedBrowser.getAttribute('blank')) gBrowser.selectedBrowser.removeAttribute('blank');
|
||||
} catch(e) {}
|
||||
if (gBrowser.selectedBrowser.getAttribute("blank"))
|
||||
gBrowser.selectedBrowser.removeAttribute("blank");
|
||||
} catch (e) {}
|
||||
|
||||
var tb_config_label = "Configuration Toolbar";
|
||||
var tb_spacer_label = "Space";
|
||||
|
@ -35,72 +45,85 @@ var AddSeparator = {
|
|||
var tb_spring_label = "Flexible Space";
|
||||
|
||||
try {
|
||||
if(document.getElementById('configuration_toolbar') == null) {
|
||||
|
||||
if(appversion <= 62) var tb_config = document.createElement("toolbar");
|
||||
if (document.getElementById("configuration_toolbar") == null) {
|
||||
if (appversion <= 62)
|
||||
var tb_config = document.createElement("toolbar");
|
||||
else var tb_config = document.createXULElement("toolbar");
|
||||
tb_config.setAttribute("id","configuration_toolbar");
|
||||
tb_config.setAttribute("customizable","true");
|
||||
tb_config.setAttribute("class","toolbar-primary chromeclass-toolbar browser-toolbar customization-target");
|
||||
tb_config.setAttribute("mode","icons");
|
||||
tb_config.setAttribute("iconsize","small");
|
||||
tb_config.setAttribute("toolboxid","navigator-toolbox");
|
||||
tb_config.setAttribute("lockiconsize","true");
|
||||
tb_config.setAttribute("ordinal","1005");
|
||||
tb_config.setAttribute("defaultset","toolbarspacer,toolbarseparator");
|
||||
tb_config.setAttribute("id", "configuration_toolbar");
|
||||
tb_config.setAttribute("customizable", "true");
|
||||
tb_config.setAttribute(
|
||||
"class",
|
||||
"toolbar-primary chromeclass-toolbar browser-toolbar customization-target"
|
||||
);
|
||||
tb_config.setAttribute("mode", "icons");
|
||||
tb_config.setAttribute("iconsize", "small");
|
||||
tb_config.setAttribute("toolboxid", "navigator-toolbox");
|
||||
tb_config.setAttribute("lockiconsize", "true");
|
||||
tb_config.setAttribute("ordinal", "1005");
|
||||
tb_config.setAttribute(
|
||||
"defaultset",
|
||||
"toolbarspacer,toolbarseparator"
|
||||
);
|
||||
|
||||
document.querySelector('#navigator-toolbox').appendChild(tb_config);
|
||||
document.querySelector("#navigator-toolbox").appendChild(tb_config);
|
||||
|
||||
CustomizableUI.registerArea("configuration_toolbar", {legacy: true});
|
||||
if(appversion >= 65) CustomizableUI.registerToolbarNode(tb_config);
|
||||
CustomizableUI.registerArea("configuration_toolbar", {
|
||||
legacy: true,
|
||||
});
|
||||
if (appversion >= 65) CustomizableUI.registerToolbarNode(tb_config);
|
||||
|
||||
if(appversion <= 62) var tb_label = document.createElement("label");
|
||||
if (appversion <= 62) var tb_label = document.createElement("label");
|
||||
else var tb_label = document.createXULElement("label");
|
||||
tb_label.setAttribute("label", tb_config_label+": ");
|
||||
tb_label.setAttribute("value", tb_config_label+": ");
|
||||
tb_label.setAttribute("id","tb_config_tb_label");
|
||||
tb_label.setAttribute("removable","false");
|
||||
tb_label.setAttribute("label", tb_config_label + ": ");
|
||||
tb_label.setAttribute("value", tb_config_label + ": ");
|
||||
tb_label.setAttribute("id", "tb_config_tb_label");
|
||||
tb_label.setAttribute("removable", "false");
|
||||
|
||||
tb_config.appendChild(tb_label);
|
||||
|
||||
|
||||
if(appversion <= 62) var tb_spacer = document.createElement("toolbarspacer");
|
||||
if (appversion <= 62)
|
||||
var tb_spacer = document.createElement("toolbarspacer");
|
||||
else var tb_spacer = document.createXULElement("toolbarspacer");
|
||||
tb_spacer.setAttribute("id","spacer");
|
||||
tb_spacer.setAttribute("class","chromeclass-toolbar-additional");
|
||||
tb_spacer.setAttribute("customizableui-areatype","toolbar");
|
||||
tb_spacer.setAttribute("removable","false");
|
||||
tb_spacer.setAttribute("id", "spacer");
|
||||
tb_spacer.setAttribute("class", "chromeclass-toolbar-additional");
|
||||
tb_spacer.setAttribute("customizableui-areatype", "toolbar");
|
||||
tb_spacer.setAttribute("removable", "false");
|
||||
tb_spacer.setAttribute("label", tb_spacer_label);
|
||||
|
||||
tb_config.appendChild(tb_spacer);
|
||||
|
||||
|
||||
if(appversion <= 62) var tb_sep = document.createElement("toolbarseparator");
|
||||
if (appversion <= 62)
|
||||
var tb_sep = document.createElement("toolbarseparator");
|
||||
else var tb_sep = document.createXULElement("toolbarseparator");
|
||||
tb_sep.setAttribute("id","separator");
|
||||
tb_sep.setAttribute("class","chromeclass-toolbar-additional");
|
||||
tb_sep.setAttribute("customizableui-areatype","toolbar");
|
||||
tb_sep.setAttribute("removable","false");
|
||||
tb_sep.setAttribute("id", "separator");
|
||||
tb_sep.setAttribute("class", "chromeclass-toolbar-additional");
|
||||
tb_sep.setAttribute("customizableui-areatype", "toolbar");
|
||||
tb_sep.setAttribute("removable", "false");
|
||||
tb_sep.setAttribute("label", tb_sep_label);
|
||||
|
||||
tb_config.appendChild(tb_sep);
|
||||
|
||||
|
||||
if(appversion <= 62) var tb_spring = document.createElement("toolbarspring");
|
||||
if (appversion <= 62)
|
||||
var tb_spring = document.createElement("toolbarspring");
|
||||
else var tb_spring = document.createXULElement("toolbarspring");
|
||||
tb_spring.setAttribute("id","spring");
|
||||
tb_spring.setAttribute("class","chromeclass-toolbar-additional");
|
||||
tb_spring.setAttribute("customizableui-areatype","toolbar");
|
||||
tb_spring.setAttribute("removable","false");
|
||||
tb_spring.setAttribute("flex","1");
|
||||
tb_spring.setAttribute("id", "spring");
|
||||
tb_spring.setAttribute("class", "chromeclass-toolbar-additional");
|
||||
tb_spring.setAttribute("customizableui-areatype", "toolbar");
|
||||
tb_spring.setAttribute("removable", "false");
|
||||
tb_spring.setAttribute("flex", "1");
|
||||
tb_spring.setAttribute("label", tb_spring_label);
|
||||
|
||||
tb_config.appendChild(tb_spring);
|
||||
|
||||
// CSS
|
||||
var sss = Components.classes["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService);
|
||||
var sss = Components.classes[
|
||||
"@mozilla.org/content/style-sheet-service;1"
|
||||
].getService(Components.interfaces.nsIStyleSheetService);
|
||||
|
||||
var uri = Services.io.newURI("data:text/css;charset=utf-8," + encodeURIComponent('\
|
||||
var uri = Services.io.newURI(
|
||||
"data:text/css;charset=utf-8," +
|
||||
encodeURIComponent(
|
||||
'\
|
||||
\
|
||||
#configuration_toolbar { \
|
||||
-moz-appearance: none !important; \
|
||||
|
@ -161,21 +184,24 @@ var AddSeparator = {
|
|||
max-width: 100% !important; \
|
||||
}\
|
||||
\
|
||||
'), null, null);
|
||||
'
|
||||
),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET);
|
||||
}
|
||||
} catch(e){}
|
||||
} catch (e) {}
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* initialization delay workaround */
|
||||
document.addEventListener("DOMContentLoaded", AddSeparator.init(), false);
|
||||
/* Use the below code instead of the one above this line, if issues occur */
|
||||
/*
|
||||
/* initialization delay workaround */
|
||||
document.addEventListener("DOMContentLoaded", AddSeparator.init(), false);
|
||||
/* Use the below code instead of the one above this line, if issues occur */
|
||||
/*
|
||||
setTimeout(function(){
|
||||
AddSeparator.init();
|
||||
},2000);
|
||||
*/
|
||||
})();
|
||||
|
|
207
chrome/status-bar.uc.js
Normal file
|
@ -0,0 +1,207 @@
|
|||
// ==UserScript==
|
||||
// @name Status Bar
|
||||
// @author xiaoxiaoflood
|
||||
// @include main
|
||||
// @startup UC.statusBar.exec(win);
|
||||
// @shutdown UC.statusBar.destroy();
|
||||
// @onlyonce
|
||||
// ==/UserScript==
|
||||
|
||||
const { CustomizableUI, StatusPanel } = window;
|
||||
|
||||
UC.statusBar = {
|
||||
PREF_ENABLED: "userChromeJS.statusbar.enabled",
|
||||
PREF_STATUSTEXT: "userChromeJS.statusbar.appendStatusText",
|
||||
|
||||
get enabled() {
|
||||
return xPref.get(this.PREF_ENABLED);
|
||||
},
|
||||
|
||||
get textInBar() {
|
||||
return this.enabled && xPref.get(this.PREF_STATUSTEXT);
|
||||
},
|
||||
|
||||
init: function () {
|
||||
xPref.set(this.PREF_ENABLED, true, true);
|
||||
xPref.set(this.PREF_STATUSTEXT, true, true);
|
||||
this.enabledListener = xPref.addListener(this.PREF_ENABLED, (isEnabled) => {
|
||||
CustomizableUI.getWidget("status-dummybar").instances.forEach(
|
||||
(dummyBar) => {
|
||||
dummyBar.node.setAttribute("collapsed", !isEnabled);
|
||||
}
|
||||
);
|
||||
});
|
||||
this.textListener = xPref.addListener(this.PREF_STATUSTEXT, (isEnabled) => {
|
||||
if (!UC.statusBar.enabled) return;
|
||||
|
||||
_uc.windows((doc, win) => {
|
||||
let StatusPanel = win.StatusPanel;
|
||||
if (isEnabled)
|
||||
win.statusbar.textNode.appendChild(StatusPanel._labelElement);
|
||||
else StatusPanel.panel.appendChild(StatusPanel._labelElement);
|
||||
});
|
||||
});
|
||||
|
||||
this.setStyle();
|
||||
_uc.sss.loadAndRegisterSheet(this.STYLE.url, this.STYLE.type);
|
||||
|
||||
CustomizableUI.registerArea("status-bar", {});
|
||||
|
||||
Services.obs.addObserver(this, "browser-delayed-startup-finished");
|
||||
},
|
||||
|
||||
exec: function (win) {
|
||||
let document = win.document;
|
||||
let StatusPanel = win.StatusPanel;
|
||||
|
||||
let dummystatusbar = _uc.createElement(document, "toolbar", {
|
||||
id: "status-dummybar",
|
||||
toolbarname: "Status Bar",
|
||||
hidden: "true",
|
||||
});
|
||||
dummystatusbar.collapsed = !this.enabled;
|
||||
dummystatusbar.setAttribute = function (att, value) {
|
||||
let result = Element.prototype.setAttribute.apply(this, arguments);
|
||||
|
||||
if (att == "collapsed") {
|
||||
let StatusPanel = win.StatusPanel;
|
||||
if (value === true) {
|
||||
xPref.set(UC.statusBar.PREF_ENABLED, false);
|
||||
win.statusbar.node.setAttribute("collapsed", true);
|
||||
StatusPanel.panel.appendChild(StatusPanel._labelElement);
|
||||
win.statusbar.node.parentNode.collapsed = true;
|
||||
} else {
|
||||
xPref.set(UC.statusBar.PREF_ENABLED, true);
|
||||
win.statusbar.node.setAttribute("collapsed", false);
|
||||
if (UC.statusBar.textInBar)
|
||||
win.statusbar.textNode.appendChild(StatusPanel._labelElement);
|
||||
win.statusbar.node.parentNode.collapsed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
win.gNavToolbox.appendChild(dummystatusbar);
|
||||
|
||||
win.statusbar.node = _uc.createElement(document, "toolbar", {
|
||||
id: "status-bar",
|
||||
customizable: "true",
|
||||
context: "toolbar-context-menu",
|
||||
mode: "icons",
|
||||
});
|
||||
|
||||
win.statusbar.textNode = _uc.createElement(document, "toolbaritem", {
|
||||
id: "status-text",
|
||||
flex: "1",
|
||||
width: "100",
|
||||
});
|
||||
if (this.textInBar)
|
||||
win.statusbar.textNode.appendChild(StatusPanel._labelElement);
|
||||
win.statusbar.node.appendChild(win.statusbar.textNode);
|
||||
|
||||
win.eval(
|
||||
'Object.defineProperty(StatusPanel, "_label", {' +
|
||||
Object.getOwnPropertyDescriptor(StatusPanel, "_label")
|
||||
.set.toString()
|
||||
.replace(/^set _label/, "set")
|
||||
.replace(
|
||||
/((\s+)this\.panel\.setAttribute\("inactive", "true"\);)/,
|
||||
"$2this._labelElement.value = val;$1"
|
||||
) +
|
||||
", enumerable: true, configurable: true});"
|
||||
);
|
||||
|
||||
let bottomBox = document.createElement("vbox");
|
||||
bottomBox.id = "browser-bottombox";
|
||||
bottomBox.append(win.statusbar.node);
|
||||
|
||||
if (!this.enabled) bottomBox.collapsed = true;
|
||||
|
||||
document
|
||||
.getElementById("fullscreen-and-pointerlock-wrapper")
|
||||
.insertAdjacentElement("afterend", bottomBox);
|
||||
|
||||
win.addEventListener("fullscreen", this.fsEvent);
|
||||
|
||||
if (document.readyState === "complete") this.observe(win);
|
||||
},
|
||||
|
||||
fsEvent: function (ev) {
|
||||
const { StatusPanel, fullScreen, statusbar } = ev.target;
|
||||
if (fullScreen) StatusPanel.panel.appendChild(StatusPanel._labelElement);
|
||||
else statusbar.textNode.appendChild(StatusPanel._labelElement);
|
||||
},
|
||||
|
||||
observe: function (win) {
|
||||
CustomizableUI.registerToolbarNode(win.statusbar.node);
|
||||
},
|
||||
|
||||
orig: Object.getOwnPropertyDescriptor(StatusPanel, "_label").set.toString(),
|
||||
|
||||
setStyle: function () {
|
||||
this.STYLE = {
|
||||
url: Services.io.newURI(
|
||||
"data:text/css;charset=UTF-8," +
|
||||
encodeURIComponent(`
|
||||
@-moz-document url('${_uc.BROWSERCHROME}') {
|
||||
#status-bar {
|
||||
color: initial !important;
|
||||
/*background-color: var(--toolbar-non-lwt-bgcolor);*/
|
||||
}
|
||||
#status-text > #statuspanel-label {
|
||||
border-top: 0 !important;
|
||||
/*background-color: unset !important;
|
||||
color: #444;*/
|
||||
}
|
||||
#status-bar > #status-text {
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-content: center !important;
|
||||
flex-direction: column !important;
|
||||
-moz-window-dragging: drag;
|
||||
}
|
||||
toolbarpaletteitem #status-text:before {
|
||||
content: "Status text";
|
||||
color: red;
|
||||
border: 1px #aaa solid;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
/*#browser-bottombox:not([collapsed]) {
|
||||
border-top: 1px solid #BCBEBF !important;
|
||||
}*/
|
||||
:root[inFullscreen]:not([macOSNativeFullscreen]) #browser-bottombox {
|
||||
visibility: collapse !important;
|
||||
}
|
||||
}
|
||||
`)
|
||||
),
|
||||
type: _uc.sss.USER_SHEET,
|
||||
};
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
const { CustomizableUI } = Services.wm.getMostRecentBrowserWindow();
|
||||
|
||||
xPref.removeListener(this.enabledListener);
|
||||
xPref.removeListener(this.textListener);
|
||||
CustomizableUI.unregisterArea("status-bar");
|
||||
_uc.sss.unregisterSheet(this.STYLE.url, this.STYLE.type);
|
||||
_uc.windows((doc, win) => {
|
||||
const { eval, statusbar, StatusPanel } = win;
|
||||
eval(
|
||||
'Object.defineProperty(StatusPanel, "_label", {' +
|
||||
this.orig.replace(/^set _label/, "set") +
|
||||
", enumerable: true, configurable: true});"
|
||||
);
|
||||
StatusPanel.panel.appendChild(StatusPanel._labelElement);
|
||||
doc.getElementById("status-dummybar").remove();
|
||||
statusbar.node.remove();
|
||||
win.removeEventListener("fullscreen", this.fsEvent);
|
||||
});
|
||||
Services.obs.removeObserver(this, "browser-delayed-startup-finished");
|
||||
delete UC.statusBar;
|
||||
},
|
||||
};
|
||||
|
||||
UC.statusBar.init();
|
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 197 B |
Before Width: | Height: | Size: 281 B |
Before Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 350 B |
Before Width: | Height: | Size: 317 B |
Before Width: | Height: | Size: 432 B |
Before Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 243 B |
Before Width: | Height: | Size: 417 B |
Before Width: | Height: | Size: 326 B |
Before Width: | Height: | Size: 187 B |
Before Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 173 B |
Before Width: | Height: | Size: 316 B |
Before Width: | Height: | Size: 322 B |
Before Width: | Height: | Size: 426 B |
Before Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 298 B |
Before Width: | Height: | Size: 200 B |
Before Width: | Height: | Size: 329 B |
Before Width: | Height: | Size: 301 B |
Before Width: | Height: | Size: 288 B |
Before Width: | Height: | Size: 191 B |
Before Width: | Height: | Size: 264 B |
Before Width: | Height: | Size: 227 B |
Before Width: | Height: | Size: 321 B |
Before Width: | Height: | Size: 326 B |
Before Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 469 B |
Before Width: | Height: | Size: 509 B |
Before Width: | Height: | Size: 524 B |
Before Width: | Height: | Size: 221 B |
Before Width: | Height: | Size: 276 B |
Before Width: | Height: | Size: 496 B |
Before Width: | Height: | Size: 389 B |
Before Width: | Height: | Size: 218 B |
Before Width: | Height: | Size: 326 B |
Before Width: | Height: | Size: 337 B |
Before Width: | Height: | Size: 329 B |
Before Width: | Height: | Size: 420 B |
Before Width: | Height: | Size: 437 B |
Before Width: | Height: | Size: 527 B |
Before Width: | Height: | Size: 221 B |
Before Width: | Height: | Size: 269 B |
Before Width: | Height: | Size: 487 B |
Before Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 231 B |
Before Width: | Height: | Size: 378 B |
Before Width: | Height: | Size: 357 B |
Before Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 204 B |
Before Width: | Height: | Size: 337 B |
Before Width: | Height: | Size: 248 B |
Before Width: | Height: | Size: 399 B |
Before Width: | Height: | Size: 350 B |
Before Width: | Height: | Size: 349 B |
Before Width: | Height: | Size: 210 B |
Before Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 334 B |
Before Width: | Height: | Size: 289 B |
Before Width: | Height: | Size: 297 B |
Before Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 4.4 KiB |
477
chrome/utils/BootstrapLoader.jsm
Normal file
|
@ -0,0 +1,477 @@
|
|||
/* 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";
|
||||
|
||||
let EXPORTED_SYMBOLS = [];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const Services =
|
||||
globalThis.Services ||
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
Blocklist: "resource://gre/modules/Blocklist.jsm",
|
||||
ConsoleAPI: "resource://gre/modules/Console.jsm",
|
||||
InstallRDF: "chrome://userchromejs/content/RDFManifestConverter.jsm",
|
||||
});
|
||||
|
||||
Services.obs.addObserver((doc) => {
|
||||
if (
|
||||
doc.location.protocol + doc.location.pathname === "about:addons" ||
|
||||
doc.location.protocol + doc.location.pathname ===
|
||||
"chrome:/content/extensions/aboutaddons.html"
|
||||
) {
|
||||
const win = doc.defaultView;
|
||||
let handleEvent_orig =
|
||||
win.customElements.get("addon-card").prototype.handleEvent;
|
||||
win.customElements.get("addon-card").prototype.handleEvent = function (e) {
|
||||
if (
|
||||
e.type === "click" &&
|
||||
e.target.getAttribute("action") === "preferences" &&
|
||||
this.addon.optionsType == 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/
|
||||
) {
|
||||
var windows = Services.wm.getEnumerator(null);
|
||||
while (windows.hasMoreElements()) {
|
||||
var win2 = windows.getNext();
|
||||
if (win2.closed) {
|
||||
continue;
|
||||
}
|
||||
if (win2.document.documentURI == this.addon.optionsURL) {
|
||||
win2.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
var features = "chrome,titlebar,toolbar,centerscreen";
|
||||
win.docShell.rootTreeItem.domWindow.openDialog(
|
||||
this.addon.optionsURL,
|
||||
this.addon.id,
|
||||
features
|
||||
);
|
||||
} else {
|
||||
handleEvent_orig.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
let update_orig = win.customElements.get("addon-options").prototype.update;
|
||||
win.customElements.get("addon-options").prototype.update = function (
|
||||
card,
|
||||
addon
|
||||
) {
|
||||
update_orig.apply(this, arguments);
|
||||
if (addon.optionsType == 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/)
|
||||
this.querySelector(
|
||||
'panel-item[data-l10n-id="preferences-addon-button"]'
|
||||
).hidden = false;
|
||||
};
|
||||
}
|
||||
}, "chrome-document-loaded");
|
||||
|
||||
const { AddonManager } = ChromeUtils.import(
|
||||
"resource://gre/modules/AddonManager.jsm"
|
||||
);
|
||||
const { XPIDatabase, AddonInternal } = ChromeUtils.import(
|
||||
"resource://gre/modules/addons/XPIDatabase.jsm"
|
||||
);
|
||||
|
||||
const { defineAddonWrapperProperty } = Cu.import(
|
||||
"resource://gre/modules/addons/XPIDatabase.jsm"
|
||||
);
|
||||
defineAddonWrapperProperty("optionsType", function optionsType() {
|
||||
if (!this.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let addon = this.__AddonInternal__;
|
||||
let hasOptionsURL = !!this.optionsURL;
|
||||
|
||||
if (addon.optionsType) {
|
||||
switch (parseInt(addon.optionsType, 10)) {
|
||||
case 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/:
|
||||
case AddonManager.OPTIONS_TYPE_TAB:
|
||||
case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
|
||||
return hasOptionsURL ? addon.optionsType : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
XPIDatabase.isDisabledLegacy = () => false;
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "BOOTSTRAP_REASONS", () => {
|
||||
const { XPIProvider } = ChromeUtils.import(
|
||||
"resource://gre/modules/addons/XPIProvider.jsm"
|
||||
);
|
||||
return XPIProvider.BOOTSTRAP_REASONS;
|
||||
});
|
||||
|
||||
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
|
||||
var logger = Log.repository.getLogger("addons.bootstrap");
|
||||
|
||||
/**
|
||||
* Valid IDs fit this pattern.
|
||||
*/
|
||||
var gIDTest =
|
||||
/^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
|
||||
|
||||
// Properties that exist in the install manifest
|
||||
const PROP_METADATA = [
|
||||
"id",
|
||||
"version",
|
||||
"type",
|
||||
"internalName",
|
||||
"updateURL",
|
||||
"optionsURL",
|
||||
"optionsType",
|
||||
"aboutURL",
|
||||
"iconURL",
|
||||
];
|
||||
const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
|
||||
const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
|
||||
|
||||
// Map new string type identifiers to old style nsIUpdateItem types.
|
||||
// Retired values:
|
||||
// 32 = multipackage xpi file
|
||||
// 8 = locale
|
||||
// 256 = apiextension
|
||||
// 128 = experiment
|
||||
// theme = 4
|
||||
const TYPES = {
|
||||
extension: 2,
|
||||
dictionary: 64,
|
||||
};
|
||||
|
||||
const COMPATIBLE_BY_DEFAULT_TYPES = {
|
||||
extension: true,
|
||||
dictionary: true,
|
||||
};
|
||||
|
||||
const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
|
||||
|
||||
function isXPI(filename) {
|
||||
let ext = filename.slice(-4).toLowerCase();
|
||||
return ext === ".xpi" || ext === ".zip";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an nsIURI for a file within another file, either a directory or an XPI
|
||||
* file. If aFile is a directory then this will return a file: URI, if it is an
|
||||
* XPI file then it will return a jar: URI.
|
||||
*
|
||||
* @param {nsIFile} aFile
|
||||
* The file containing the resources, must be either a directory or an
|
||||
* XPI file
|
||||
* @param {string} aPath
|
||||
* The path to find the resource at, '/' separated. If aPath is empty
|
||||
* then the uri to the root of the contained files will be returned
|
||||
* @returns {nsIURI}
|
||||
* An nsIURI pointing at the resource
|
||||
*/
|
||||
function getURIForResourceInFile(aFile, aPath) {
|
||||
if (!isXPI(aFile.leafName)) {
|
||||
let resource = aFile.clone();
|
||||
if (aPath) aPath.split("/").forEach((part) => resource.append(part));
|
||||
|
||||
return Services.io.newFileURI(resource);
|
||||
}
|
||||
|
||||
return buildJarURI(aFile, aPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a jar: URI for a file inside a ZIP file.
|
||||
*
|
||||
* @param {nsIFile} aJarfile
|
||||
* The ZIP file as an nsIFile
|
||||
* @param {string} aPath
|
||||
* The path inside the ZIP file
|
||||
* @returns {nsIURI}
|
||||
* An nsIURI for the file
|
||||
*/
|
||||
function buildJarURI(aJarfile, aPath) {
|
||||
let uri = Services.io.newFileURI(aJarfile);
|
||||
uri = "jar:" + uri.spec + "!/" + aPath;
|
||||
return Services.io.newURI(uri);
|
||||
}
|
||||
|
||||
var BootstrapLoader = {
|
||||
name: "bootstrap",
|
||||
manifestFile: "install.rdf",
|
||||
async loadManifest(pkg) {
|
||||
/**
|
||||
* Reads locale properties from either the main install manifest root or
|
||||
* an em:localized section in the install manifest.
|
||||
*
|
||||
* @param {Object} aSource
|
||||
* The resource to read the properties from.
|
||||
* @param {boolean} isDefault
|
||||
* True if the locale is to be read from the main install manifest
|
||||
* root
|
||||
* @param {string[]} aSeenLocales
|
||||
* An array of locale names already seen for this install manifest.
|
||||
* Any locale names seen as a part of this function will be added to
|
||||
* this array
|
||||
* @returns {Object}
|
||||
* an object containing the locale properties
|
||||
*/
|
||||
function readLocale(aSource, isDefault, aSeenLocales) {
|
||||
let locale = {};
|
||||
if (!isDefault) {
|
||||
locale.locales = [];
|
||||
for (let localeName of aSource.locales || []) {
|
||||
if (!localeName) {
|
||||
logger.warn("Ignoring empty locale in localized properties");
|
||||
continue;
|
||||
}
|
||||
if (aSeenLocales.includes(localeName)) {
|
||||
logger.warn("Ignoring duplicate locale in localized properties");
|
||||
continue;
|
||||
}
|
||||
aSeenLocales.push(localeName);
|
||||
locale.locales.push(localeName);
|
||||
}
|
||||
|
||||
if (locale.locales.length == 0) {
|
||||
logger.warn("Ignoring localized properties with no listed locales");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) {
|
||||
if (hasOwnProperty(aSource, prop)) {
|
||||
locale[prop] = aSource[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
let manifestData = await pkg.readString("install.rdf");
|
||||
let manifest = InstallRDF.loadFromString(manifestData).decode();
|
||||
|
||||
let addon = new AddonInternal();
|
||||
for (let prop of PROP_METADATA) {
|
||||
if (hasOwnProperty(manifest, prop)) {
|
||||
addon[prop] = manifest[prop];
|
||||
}
|
||||
}
|
||||
|
||||
if (!addon.type) {
|
||||
addon.type = "extension";
|
||||
} else {
|
||||
let type = addon.type;
|
||||
addon.type = null;
|
||||
for (let name in TYPES) {
|
||||
if (TYPES[name] == type) {
|
||||
addon.type = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(addon.type in TYPES))
|
||||
throw new Error("Install manifest specifies unknown type: " + addon.type);
|
||||
|
||||
if (!addon.id) throw new Error("No ID in install manifest");
|
||||
if (!gIDTest.test(addon.id))
|
||||
throw new Error("Illegal add-on ID " + addon.id);
|
||||
if (!addon.version) throw new Error("No version in install manifest");
|
||||
|
||||
addon.strictCompatibility =
|
||||
!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
|
||||
manifest.strictCompatibility == "true";
|
||||
|
||||
// Only read these properties for extensions.
|
||||
if (addon.type == "extension") {
|
||||
if (manifest.bootstrap != "true") {
|
||||
throw new Error("Non-restartless extensions no longer supported");
|
||||
}
|
||||
|
||||
if (
|
||||
addon.optionsType &&
|
||||
addon.optionsType != 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/ &&
|
||||
addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER &&
|
||||
addon.optionsType != AddonManager.OPTIONS_TYPE_TAB
|
||||
) {
|
||||
throw new Error(
|
||||
"Install manifest specifies unknown optionsType: " + addon.optionsType
|
||||
);
|
||||
}
|
||||
|
||||
if (addon.optionsType) addon.optionsType = parseInt(addon.optionsType);
|
||||
}
|
||||
|
||||
addon.defaultLocale = readLocale(manifest, true);
|
||||
|
||||
let seenLocales = [];
|
||||
addon.locales = [];
|
||||
for (let localeData of manifest.localized || []) {
|
||||
let locale = readLocale(localeData, false, seenLocales);
|
||||
if (locale) addon.locales.push(locale);
|
||||
}
|
||||
|
||||
let dependencies = new Set(manifest.dependencies);
|
||||
addon.dependencies = Object.freeze(Array.from(dependencies));
|
||||
|
||||
let seenApplications = [];
|
||||
addon.targetApplications = [];
|
||||
for (let targetApp of manifest.targetApplications || []) {
|
||||
if (!targetApp.id || !targetApp.minVersion || !targetApp.maxVersion) {
|
||||
logger.warn(
|
||||
"Ignoring invalid targetApplication entry in install manifest"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (seenApplications.includes(targetApp.id)) {
|
||||
logger.warn(
|
||||
"Ignoring duplicate targetApplication entry for " +
|
||||
targetApp.id +
|
||||
" in install manifest"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
seenApplications.push(targetApp.id);
|
||||
addon.targetApplications.push(targetApp);
|
||||
}
|
||||
|
||||
// Note that we don't need to check for duplicate targetPlatform entries since
|
||||
// the RDF service coalesces them for us.
|
||||
addon.targetPlatforms = [];
|
||||
for (let targetPlatform of manifest.targetPlatforms || []) {
|
||||
let platform = {
|
||||
os: null,
|
||||
abi: null,
|
||||
};
|
||||
|
||||
let pos = targetPlatform.indexOf("_");
|
||||
if (pos != -1) {
|
||||
platform.os = targetPlatform.substring(0, pos);
|
||||
platform.abi = targetPlatform.substring(pos + 1);
|
||||
} else {
|
||||
platform.os = targetPlatform;
|
||||
}
|
||||
|
||||
addon.targetPlatforms.push(platform);
|
||||
}
|
||||
|
||||
addon.userDisabled = false;
|
||||
addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
|
||||
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
|
||||
|
||||
addon.userPermissions = null;
|
||||
|
||||
addon.icons = {};
|
||||
if (await pkg.hasResource("icon.png")) {
|
||||
addon.icons[32] = "icon.png";
|
||||
addon.icons[48] = "icon.png";
|
||||
}
|
||||
|
||||
if (await pkg.hasResource("icon64.png")) {
|
||||
addon.icons[64] = "icon64.png";
|
||||
}
|
||||
|
||||
return addon;
|
||||
},
|
||||
|
||||
loadScope(addon) {
|
||||
let file = addon.file || addon._sourceBundle;
|
||||
let uri = getURIForResourceInFile(file, "bootstrap.js").spec;
|
||||
let principal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
|
||||
let sandbox = new Cu.Sandbox(principal, {
|
||||
sandboxName: uri,
|
||||
addonId: addon.id,
|
||||
wantGlobalProperties: ["ChromeUtils"],
|
||||
metadata: { addonID: addon.id, URI: uri },
|
||||
});
|
||||
|
||||
try {
|
||||
Object.assign(sandbox, BOOTSTRAP_REASONS);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(
|
||||
sandbox,
|
||||
"console",
|
||||
() => new ConsoleAPI({ consoleID: `addon/${addon.id}` })
|
||||
);
|
||||
|
||||
Services.scriptloader.loadSubScript(uri, sandbox);
|
||||
} catch (e) {
|
||||
logger.warn(`Error loading bootstrap.js for ${addon.id}`, e);
|
||||
}
|
||||
|
||||
function findMethod(name) {
|
||||
if (sandbox[name]) {
|
||||
return sandbox[name];
|
||||
}
|
||||
|
||||
try {
|
||||
let method = Cu.evalInSandbox(name, sandbox);
|
||||
return method;
|
||||
} catch (err) {}
|
||||
|
||||
return () => {
|
||||
logger.warn(`Add-on ${addon.id} is missing bootstrap method ${name}`);
|
||||
};
|
||||
}
|
||||
|
||||
let install = findMethod("install");
|
||||
let uninstall = findMethod("uninstall");
|
||||
let startup = findMethod("startup");
|
||||
let shutdown = findMethod("shutdown");
|
||||
|
||||
return {
|
||||
install(...args) {
|
||||
install(...args);
|
||||
// Forget any cached files we might've had from this extension.
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate");
|
||||
},
|
||||
|
||||
uninstall(...args) {
|
||||
uninstall(...args);
|
||||
// Forget any cached files we might've had from this extension.
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate");
|
||||
},
|
||||
|
||||
startup(...args) {
|
||||
if (addon.type == "extension") {
|
||||
logger.debug(`Registering manifest for ${file.path}\n`);
|
||||
Components.manager.addBootstrappedManifestLocation(file);
|
||||
}
|
||||
return startup(...args);
|
||||
},
|
||||
|
||||
shutdown(data, reason) {
|
||||
try {
|
||||
return shutdown(data, reason);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
if (reason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
|
||||
logger.debug(`Removing manifest for ${file.path}\n`);
|
||||
Components.manager.removeBootstrappedManifestLocation(file);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
AddonManager.addExternalExtensionLoader(BootstrapLoader);
|
||||
|
||||
if (AddonManager.isReady) {
|
||||
AddonManager.getAllAddons().then((addons) => {
|
||||
addons.forEach((addon) => {
|
||||
if (
|
||||
addon.type == "extension" &&
|
||||
!addon.isWebExtension &&
|
||||
!addon.userDisabled
|
||||
) {
|
||||
addon.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
441
chrome/utils/RDFDataSource.jsm
Normal file
|
@ -0,0 +1,441 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This module creates a new API for accessing and modifying RDF graphs. The
|
||||
* goal is to be able to serialise the graph in a human readable form. Also
|
||||
* if the graph was originally loaded from an RDF/XML the serialisation should
|
||||
* closely match the original with any new data closely following the existing
|
||||
* layout. The output should always be compatible with Mozilla's RDF parser.
|
||||
*
|
||||
* This is all achieved by using a DOM Document to hold the current state of the
|
||||
* graph in XML form. This can be initially loaded and parsed from disk or
|
||||
* a blank document used for an empty graph. As assertions are added to the
|
||||
* graph, appropriate DOM nodes are added to the document to represent them
|
||||
* along with any necessary whitespace to properly layout the XML.
|
||||
*
|
||||
* In general the order of adding assertions to the graph will impact the form
|
||||
* the serialisation takes. If a resource is first added as the object of an
|
||||
* assertion then it will eventually be serialised inside the assertion's
|
||||
* property element. If a resource is first added as the subject of an assertion
|
||||
* then it will be serialised at the top level of the XML.
|
||||
*/
|
||||
|
||||
const NS_XML = "http://www.w3.org/XML/1998/namespace";
|
||||
const NS_XMLNS = "http://www.w3.org/2000/xmlns/";
|
||||
const NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
|
||||
const NS_NC = "http://home.netscape.com/NC-rdf#";
|
||||
|
||||
/* eslint prefer-template: 1 */
|
||||
|
||||
var EXPORTED_SYMBOLS = ["RDFLiteral", "RDFBlankNode", "RDFResource", "RDFDataSource"];
|
||||
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const Services = globalThis.Services || ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "Element", "fetch"]);
|
||||
|
||||
function isElement(obj) {
|
||||
return Element.isInstance(obj);
|
||||
}
|
||||
function isText(obj) {
|
||||
return obj && typeof obj == "object" && ChromeUtils.getClassName(obj) == "Text";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either an rdf namespaced attribute or an un-namespaced attribute
|
||||
* value. Returns null if neither exists,
|
||||
*/
|
||||
function getRDFAttribute(element, name) {
|
||||
if (element.hasAttributeNS(NS_RDF, name))
|
||||
return element.getAttributeNS(NS_RDF, name);
|
||||
if (element.hasAttribute(name))
|
||||
return element.getAttribute(name);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an assertion in the datasource
|
||||
*/
|
||||
class RDFAssertion {
|
||||
constructor(subject, predicate, object) {
|
||||
// The subject on this assertion, an RDFSubject
|
||||
this._subject = subject;
|
||||
// The predicate, a string
|
||||
this._predicate = predicate;
|
||||
// The object, an RDFNode
|
||||
this._object = object;
|
||||
// The datasource this assertion exists in
|
||||
this._ds = this._subject._ds;
|
||||
// Marks that _DOMnode is the subject's element
|
||||
this._isSubjectElement = false;
|
||||
// The DOM node that represents this assertion. Could be a property element,
|
||||
// a property attribute or the subject's element for rdf:type
|
||||
this._DOMNode = null;
|
||||
}
|
||||
|
||||
getPredicate() {
|
||||
return this._predicate;
|
||||
}
|
||||
|
||||
getObject() {
|
||||
return this._object;
|
||||
}
|
||||
}
|
||||
|
||||
class RDFNode {
|
||||
equals(rdfnode) {
|
||||
return (rdfnode.constructor === this.constructor &&
|
||||
rdfnode._value == this._value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple literal value
|
||||
*/
|
||||
class RDFLiteral extends RDFNode {
|
||||
constructor(value) {
|
||||
super();
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an RDF node that can be a subject so a resource or a blank node
|
||||
*/
|
||||
class RDFSubject extends RDFNode {
|
||||
constructor(ds) {
|
||||
super();
|
||||
// A lookup of the assertions with this as the subject. Keyed on predicate
|
||||
this._assertions = {};
|
||||
// A lookup of the assertions with this as the object. Keyed on predicate
|
||||
this._backwards = {};
|
||||
// The datasource this subject belongs to
|
||||
this._ds = ds;
|
||||
// The DOM elements in the document that represent this subject. Array of Element
|
||||
this._elements = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given Element from the DOM document
|
||||
*/
|
||||
/* eslint-disable complexity */
|
||||
_parseElement(element) {
|
||||
this._elements.push(element);
|
||||
|
||||
// There might be an inferred rdf:type assertion in the element name
|
||||
if (element.namespaceURI != NS_RDF ||
|
||||
element.localName != "Description") {
|
||||
var assertion = new RDFAssertion(this, RDF_R("type"),
|
||||
this._ds.getResource(element.namespaceURI + element.localName));
|
||||
assertion._DOMnode = element;
|
||||
assertion._isSubjectElement = true;
|
||||
this._addAssertion(assertion);
|
||||
}
|
||||
|
||||
// Certain attributes can be literal properties
|
||||
for (let attr of element.attributes) {
|
||||
if (attr.namespaceURI == NS_XML || attr.namespaceURI == NS_XMLNS ||
|
||||
attr.nodeName == "xmlns")
|
||||
continue;
|
||||
if ((attr.namespaceURI == NS_RDF || !attr.namespaceURI) &&
|
||||
(["nodeID", "about", "resource", "ID", "parseType"].includes(attr.localName)))
|
||||
continue;
|
||||
var object = null;
|
||||
if (attr.namespaceURI == NS_RDF) {
|
||||
if (attr.localName == "type")
|
||||
object = this._ds.getResource(attr.nodeValue);
|
||||
}
|
||||
if (!object)
|
||||
object = new RDFLiteral(attr.nodeValue);
|
||||
assertion = new RDFAssertion(this, attr.namespaceURI + attr.localName, object);
|
||||
assertion._DOMnode = attr;
|
||||
this._addAssertion(assertion);
|
||||
}
|
||||
|
||||
var child = element.firstChild;
|
||||
element.listCounter = 1;
|
||||
while (child) {
|
||||
if (isElement(child)) {
|
||||
object = null;
|
||||
var predicate = child.namespaceURI + child.localName;
|
||||
if (child.namespaceURI == NS_RDF) {
|
||||
if (child.localName == "li") {
|
||||
predicate = RDF_R(`_${element.listCounter}`);
|
||||
element.listCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for and bail out on unknown attributes on the property element
|
||||
for (let attr of child.attributes) {
|
||||
// Ignore XML namespaced attributes
|
||||
if (attr.namespaceURI == NS_XML)
|
||||
continue;
|
||||
// These are reserved by XML for future use
|
||||
if (attr.localName.substring(0, 3).toLowerCase() == "xml")
|
||||
continue;
|
||||
// We can handle these RDF attributes
|
||||
if ((!attr.namespaceURI || attr.namespaceURI == NS_RDF) &&
|
||||
["resource", "nodeID"].includes(attr.localName))
|
||||
continue;
|
||||
// This is a special attribute we handle for compatibility with Mozilla RDF
|
||||
if (attr.namespaceURI == NS_NC &&
|
||||
attr.localName == "parseType")
|
||||
continue;
|
||||
}
|
||||
|
||||
var parseType = child.getAttributeNS(NS_NC, "parseType");
|
||||
|
||||
var resource = getRDFAttribute(child, "resource");
|
||||
var nodeID = getRDFAttribute(child, "nodeID");
|
||||
|
||||
if (resource !== undefined) {
|
||||
var base = Services.io.newURI(element.baseURI);
|
||||
object = this._ds.getResource(base.resolve(resource));
|
||||
} else if (nodeID !== undefined) {
|
||||
object = this._ds.getBlankNode(nodeID);
|
||||
} else {
|
||||
var hasText = false;
|
||||
var childElement = null;
|
||||
var subchild = child.firstChild;
|
||||
while (subchild) {
|
||||
if (isText(subchild) && /\S/.test(subchild.nodeValue)) {
|
||||
hasText = true;
|
||||
} else if (isElement(subchild)) {
|
||||
childElement = subchild;
|
||||
}
|
||||
subchild = subchild.nextSibling;
|
||||
}
|
||||
|
||||
if (childElement) {
|
||||
object = this._ds._getSubjectForElement(childElement);
|
||||
object._parseElement(childElement);
|
||||
} else
|
||||
object = new RDFLiteral(child.textContent);
|
||||
}
|
||||
|
||||
assertion = new RDFAssertion(this, predicate, object);
|
||||
this._addAssertion(assertion);
|
||||
assertion._DOMnode = child;
|
||||
}
|
||||
child = child.nextSibling;
|
||||
}
|
||||
}
|
||||
/* eslint-enable complexity */
|
||||
|
||||
/**
|
||||
* Adds a new assertion to the internal hashes. Should be called for every
|
||||
* new assertion parsed or created programmatically.
|
||||
*/
|
||||
_addAssertion(assertion) {
|
||||
var predicate = assertion.getPredicate();
|
||||
if (predicate in this._assertions)
|
||||
this._assertions[predicate].push(assertion);
|
||||
else
|
||||
this._assertions[predicate] = [ assertion ];
|
||||
|
||||
var object = assertion.getObject();
|
||||
if (object instanceof RDFSubject) {
|
||||
// Create reverse assertion
|
||||
if (predicate in object._backwards)
|
||||
object._backwards[predicate].push(assertion);
|
||||
else
|
||||
object._backwards[predicate] = [ assertion ];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all objects in assertions with this subject and the given predicate.
|
||||
*/
|
||||
getObjects(predicate) {
|
||||
if (predicate in this._assertions)
|
||||
return Array.from(this._assertions[predicate],
|
||||
i => i.getObject());
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the first property value for the given predicate.
|
||||
*/
|
||||
getProperty(predicate) {
|
||||
if (predicate in this._assertions)
|
||||
return this._assertions[predicate][0].getObject();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RDFResource for the datasource. Private.
|
||||
*/
|
||||
class RDFResource extends RDFSubject {
|
||||
constructor(ds, uri) {
|
||||
super(ds);
|
||||
// This is the uri that the resource represents.
|
||||
this._uri = uri;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new blank node. Private.
|
||||
*/
|
||||
class RDFBlankNode extends RDFSubject {
|
||||
constructor(ds, nodeID) {
|
||||
super(ds);
|
||||
// The nodeID of this node. May be null if there is no ID.
|
||||
this._nodeID = nodeID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets attributes on the DOM element to mark it as representing this node
|
||||
*/
|
||||
_applyToElement(element) {
|
||||
if (!this._nodeID)
|
||||
return;
|
||||
if (USE_RDFNS_ATTR) {
|
||||
var prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
|
||||
element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
|
||||
} else {
|
||||
element.setAttribute("nodeID", this._nodeID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Element in the document for holding assertions about this
|
||||
* subject. The URI controls what tagname to use.
|
||||
*/
|
||||
_createNewElement(uri) {
|
||||
// If there are already nodes representing this in the document then we need
|
||||
// a nodeID to match them
|
||||
if (!this._nodeID && this._elements.length > 0) {
|
||||
this._ds._createNodeID(this);
|
||||
for (let element of this._elements)
|
||||
this._applyToElement(element);
|
||||
}
|
||||
|
||||
return super._createNewElement.call(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a reference to this node to the given property Element.
|
||||
*/
|
||||
_addReferenceToElement(element) {
|
||||
if (this._elements.length > 0 && !this._nodeID) {
|
||||
// In document elsewhere already
|
||||
// Create a node ID and update the other nodes referencing
|
||||
this._ds._createNodeID(this);
|
||||
for (let element of this._elements)
|
||||
this._applyToElement(element);
|
||||
}
|
||||
|
||||
if (this._nodeID) {
|
||||
if (USE_RDFNS_ATTR) {
|
||||
let prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
|
||||
element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
|
||||
} else {
|
||||
element.setAttribute("nodeID", this._nodeID);
|
||||
}
|
||||
} else {
|
||||
// Add the empty blank node, this is generally right since further
|
||||
// assertions will be added to fill this out
|
||||
var newelement = this._ds._addElement(element, RDF_R("Description"));
|
||||
newelement.listCounter = 1;
|
||||
this._elements.push(newelement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any reference to this node from the given property Element.
|
||||
*/
|
||||
_removeReferenceFromElement(element) {
|
||||
if (element.hasAttributeNS(NS_RDF, "nodeID"))
|
||||
element.removeAttributeNS(NS_RDF, "nodeID");
|
||||
if (element.hasAttribute("nodeID"))
|
||||
element.removeAttribute("nodeID");
|
||||
}
|
||||
|
||||
getNodeID() {
|
||||
return this._nodeID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RDFDataSource from the given document. The document will be
|
||||
* changed as assertions are added and removed to the RDF. Pass a null document
|
||||
* to start with an empty graph.
|
||||
*/
|
||||
class RDFDataSource {
|
||||
constructor(document) {
|
||||
// All known resources, indexed on URI
|
||||
this._resources = {};
|
||||
// All blank nodes
|
||||
this._allBlankNodes = [];
|
||||
|
||||
// The underlying DOM document for this datasource
|
||||
this._document = document;
|
||||
this._parseDocument();
|
||||
}
|
||||
|
||||
static loadFromString(text) {
|
||||
let parser = new DOMParser();
|
||||
let document = parser.parseFromString(text, "application/xml");
|
||||
|
||||
return new this(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an rdf subject for the given DOM Element. If the subject has not
|
||||
* been seen before a new one is created.
|
||||
*/
|
||||
_getSubjectForElement(element) {
|
||||
var about = getRDFAttribute(element, "about");
|
||||
|
||||
if (about !== undefined) {
|
||||
let base = Services.io.newURI(element.baseURI);
|
||||
return this.getResource(base.resolve(about));
|
||||
}
|
||||
return this.getBlankNode(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the document for subjects at the top level.
|
||||
*/
|
||||
_parseDocument() {
|
||||
var domnode = this._document.documentElement.firstChild;
|
||||
while (domnode) {
|
||||
if (isElement(domnode)) {
|
||||
var subject = this._getSubjectForElement(domnode);
|
||||
subject._parseElement(domnode);
|
||||
}
|
||||
domnode = domnode.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a blank node. nodeID may be null and if so a new blank node is created.
|
||||
* If a nodeID is given then the blank node with that ID is returned or created.
|
||||
*/
|
||||
getBlankNode(nodeID) {
|
||||
var rdfnode = new RDFBlankNode(this, nodeID);
|
||||
this._allBlankNodes.push(rdfnode);
|
||||
return rdfnode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resource for the URI. The resource is created if it has not been
|
||||
* used already.
|
||||
*/
|
||||
getResource(uri) {
|
||||
if (uri in this._resources)
|
||||
return this._resources[uri];
|
||||
|
||||
var resource = new RDFResource(this, uri);
|
||||
this._resources[uri] = resource;
|
||||
return resource;
|
||||
}
|
||||
}
|
102
chrome/utils/RDFManifestConverter.jsm
Normal file
|
@ -0,0 +1,102 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["InstallRDF"];
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "RDFDataSource",
|
||||
"chrome://userchromejs/content/RDFDataSource.jsm");
|
||||
|
||||
const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
|
||||
|
||||
function EM_R(aProperty) {
|
||||
return `http://www.mozilla.org/2004/em-rdf#${aProperty}`;
|
||||
}
|
||||
|
||||
function getValue(literal) {
|
||||
return literal && literal.getValue();
|
||||
}
|
||||
|
||||
function getProperty(resource, property) {
|
||||
return getValue(resource.getProperty(EM_R(property)));
|
||||
}
|
||||
|
||||
class Manifest {
|
||||
constructor(ds) {
|
||||
this.ds = ds;
|
||||
}
|
||||
|
||||
static loadFromString(text) {
|
||||
return new this(RDFDataSource.loadFromString(text));
|
||||
}
|
||||
}
|
||||
|
||||
class InstallRDF extends Manifest {
|
||||
_readProps(source, obj, props) {
|
||||
for (let prop of props) {
|
||||
let val = getProperty(source, prop);
|
||||
if (val != null) {
|
||||
obj[prop] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_readArrayProp(source, obj, prop, target, decode = getValue) {
|
||||
let result = Array.from(source.getObjects(EM_R(prop)),
|
||||
target => decode(target));
|
||||
if (result.length) {
|
||||
obj[target] = result;
|
||||
}
|
||||
}
|
||||
|
||||
_readArrayProps(source, obj, props, decode = getValue) {
|
||||
for (let [prop, target] of Object.entries(props)) {
|
||||
this._readArrayProp(source, obj, prop, target, decode);
|
||||
}
|
||||
}
|
||||
|
||||
_readLocaleStrings(source, obj) {
|
||||
this._readProps(source, obj, ["name", "description", "creator", "homepageURL"]);
|
||||
this._readArrayProps(source, obj, {
|
||||
locale: "locales",
|
||||
developer: "developers",
|
||||
translator: "translators",
|
||||
contributor: "contributors",
|
||||
});
|
||||
}
|
||||
|
||||
decode() {
|
||||
let root = this.ds.getResource(RDFURI_INSTALL_MANIFEST_ROOT);
|
||||
let result = {};
|
||||
|
||||
let props = ["id", "version", "type", "updateURL", "optionsURL",
|
||||
"optionsType", "aboutURL", "iconURL",
|
||||
"bootstrap", "unpack", "strictCompatibility"];
|
||||
this._readProps(root, result, props);
|
||||
|
||||
let decodeTargetApplication = source => {
|
||||
let app = {};
|
||||
this._readProps(source, app, ["id", "minVersion", "maxVersion"]);
|
||||
return app;
|
||||
};
|
||||
|
||||
let decodeLocale = source => {
|
||||
let localized = {};
|
||||
this._readLocaleStrings(source, localized);
|
||||
return localized;
|
||||
};
|
||||
|
||||
this._readLocaleStrings(root, result);
|
||||
|
||||
this._readArrayProps(root, result, {"targetPlatform": "targetPlatforms"});
|
||||
this._readArrayProps(root, result, {"targetApplication": "targetApplications"},
|
||||
decodeTargetApplication);
|
||||
this._readArrayProps(root, result, {"localized": "localized"},
|
||||
decodeLocale);
|
||||
this._readArrayProps(root, result, {"dependency": "dependencies"},
|
||||
source => getProperty(source, "id"));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
102
chrome/utils/aboutconfig/aboutconfig.xhtml
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://global/skin/in-content/info-pages.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://userchromejs/content/aboutconfig/config.css" type="text/css"?>
|
||||
|
||||
<window id="config"
|
||||
title="about:config"
|
||||
csp="default-src chrome:; object-src 'none'"
|
||||
data-l10n-id="config-window"
|
||||
aria-describedby="warningTitle warningText"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
windowtype="Preferences:ConfigManager"
|
||||
role="application"
|
||||
width="750"
|
||||
height="500">
|
||||
|
||||
<script src="chrome://userchromejs/content/aboutconfig/config.js"/>
|
||||
<script src="chrome://global/content/editMenuOverlay.js"/>
|
||||
<script src="chrome://global/content/globalOverlay.js"/>
|
||||
|
||||
<menupopup id="configContext">
|
||||
<menuitem id="toggleSelected" label="Toggle" accesskey="T" default="true"/>
|
||||
<menuitem id="modifySelected" label="Modify" accesskey="M" default="true"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="copyPref" label="Copy" accesskey="C"/>
|
||||
<menuitem id="copyName" label="Copy Name" accesskey="N"/>
|
||||
<menuitem id="copyValue" label="Copy Value" accesskey="V"/>
|
||||
<menu label="New" accesskey="w">
|
||||
<menupopup>
|
||||
<menuitem id="configString" label="String" accesskey="S"/>
|
||||
<menuitem id="configInt" label="Integer" accesskey="I"/>
|
||||
<menuitem id="configBool" label="Boolean" accesskey="B"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuitem id="resetSelected" label="Reset" accesskey="R"/>
|
||||
</menupopup>
|
||||
|
||||
<keyset id="configTreeKeyset" disabled="true">
|
||||
<key id="keyVKReturn" keycode="VK_RETURN"/>
|
||||
<key id="configFocuSearch" modifiers="accel" key="r"/>
|
||||
<key id="configFocuSearch2" modifiers="accel" key="f"/>
|
||||
</keyset>
|
||||
<deck id="configDeck" flex="1">
|
||||
<vbox id="warningScreen" flex="1" align="center" style="overflow: auto;">
|
||||
<spacer flex="1"/>
|
||||
<vbox id="warningBox" class="container">
|
||||
<hbox class="title" flex="1">
|
||||
<label id="warningTitle" value="This might void your warranty!" class="title-text" flex="1"></label>
|
||||
</hbox>
|
||||
<vbox class="description" flex="1">
|
||||
<label id="warningText">Changing these advanced settings can be harmful to the stability, security, and performance of this application. You should only continue if you are sure of what you are doing.</label>
|
||||
<checkbox id="showWarningNextTime" label="Show this warning next time" checked="true"/>
|
||||
<hbox class="button-container">
|
||||
<button id="warningButton" label="I accept the risk!" class="primary"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</vbox>
|
||||
<spacer style="-moz-box-flex: 2"/>
|
||||
</vbox>
|
||||
<vbox flex="1">
|
||||
<hbox id="filterRow" align="center">
|
||||
<label value="Search:" accesskey="r" control="textbox"/>
|
||||
<search-textbox id="textbox" flex="1"
|
||||
aria-controls="configTree"/>
|
||||
</hbox>
|
||||
<tree id="configTree" flex="1" seltype="single"
|
||||
enableColumnDrag="true" context="configContext">
|
||||
<treecols>
|
||||
<!--
|
||||
The below code may suggest that 'ordinal' is still a supported XUL
|
||||
attribute. It is not. This is a crutch so that we can continue
|
||||
persisting the CSS -moz-box-ordinal-group attribute, which is the
|
||||
appropriate replacement for the ordinal attribute but cannot yet
|
||||
be easily persisted. The code that synchronizes the attribute with
|
||||
the CSS lives in toolkit/content/widget/tree.js and is specific to
|
||||
tree elements.
|
||||
-->
|
||||
<treecol id="prefCol" label="Preference Name" style="-moz-box-flex: 7"
|
||||
ignoreincolumnpicker="true"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
<splitter class="tree-splitter" />
|
||||
<treecol id="lockCol" label="Status" flex="1"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
<splitter class="tree-splitter" />
|
||||
<treecol id="typeCol" label="Type" flex="1"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
<splitter class="tree-splitter" />
|
||||
<treecol id="valueCol" label="Value" style="-moz-box-flex: 10"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
</treecols>
|
||||
|
||||
<treechildren id="configTreeBody"/>
|
||||
</tree>
|
||||
</vbox>
|
||||
</deck>
|
||||
</window>
|
49
chrome/utils/aboutconfig/config.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
/* 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/. */
|
||||
|
||||
#warningScreen {
|
||||
font-size: 15px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-inline-start: calc(48px + 4.6em);
|
||||
padding-inline-end: 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
background-image: url("chrome://global/skin/icons/warning.svg");
|
||||
fill: #fcd100;
|
||||
}
|
||||
|
||||
#warningTitle {
|
||||
font-weight: lighter;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
#warningText {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#warningButton {
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
#filterRow {
|
||||
margin-top: 4px;
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
#configTree {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#configTreeBody::-moz-tree-cell-text(user) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#configTreeBody::-moz-tree-cell-text(locked) {
|
||||
font-style: italic;
|
||||
}
|
781
chrome/utils/aboutconfig/config.js
Normal file
|
@ -0,0 +1,781 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
|
||||
/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const nsIPrefLocalizedString = Ci.nsIPrefLocalizedString;
|
||||
const nsISupportsString = Ci.nsISupportsString;
|
||||
const nsIPrefBranch = Ci.nsIPrefBranch;
|
||||
const nsIClipboardHelper = Ci.nsIClipboardHelper;
|
||||
|
||||
const nsClipboardHelper_CONTRACTID = "@mozilla.org/widget/clipboardhelper;1";
|
||||
|
||||
const gPrefBranch = Services.prefs;
|
||||
const gClipboardHelper = Cc[nsClipboardHelper_CONTRACTID].getService(
|
||||
nsIClipboardHelper
|
||||
);
|
||||
|
||||
var gLockProps = ["default", "user", "locked"];
|
||||
// we get these from a string bundle
|
||||
var gLockStrs = [];
|
||||
var gTypeStrs = [];
|
||||
|
||||
const PREF_IS_DEFAULT_VALUE = 0;
|
||||
const PREF_IS_MODIFIED = 1;
|
||||
const PREF_IS_LOCKED = 2;
|
||||
|
||||
var gPrefHash = {};
|
||||
var gPrefArray = [];
|
||||
var gPrefView = gPrefArray; // share the JS array
|
||||
var gSortedColumn = "prefCol";
|
||||
var gSortFunction = null;
|
||||
var gSortDirection = 1; // 1 is ascending; -1 is descending
|
||||
var gFilter = null;
|
||||
|
||||
var view = {
|
||||
get rowCount() {
|
||||
return gPrefView.length;
|
||||
},
|
||||
getCellText(index, col) {
|
||||
if (!(index in gPrefView)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var value = gPrefView[index][col.id];
|
||||
|
||||
switch (col.id) {
|
||||
case "lockCol":
|
||||
return gLockStrs[value];
|
||||
case "typeCol":
|
||||
return gTypeStrs[value];
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
},
|
||||
getRowProperties(index) {
|
||||
return "";
|
||||
},
|
||||
getCellProperties(index, col) {
|
||||
if (index in gPrefView) {
|
||||
return gLockProps[gPrefView[index].lockCol];
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
getColumnProperties(col) {
|
||||
return "";
|
||||
},
|
||||
treebox: null,
|
||||
selection: null,
|
||||
isContainer(index) {
|
||||
return false;
|
||||
},
|
||||
isContainerOpen(index) {
|
||||
return false;
|
||||
},
|
||||
isContainerEmpty(index) {
|
||||
return false;
|
||||
},
|
||||
isSorted() {
|
||||
return true;
|
||||
},
|
||||
canDrop(index, orientation) {
|
||||
return false;
|
||||
},
|
||||
drop(row, orientation) {},
|
||||
setTree(out) {
|
||||
this.treebox = out;
|
||||
},
|
||||
getParentIndex(rowIndex) {
|
||||
return -1;
|
||||
},
|
||||
hasNextSibling(rowIndex, afterIndex) {
|
||||
return false;
|
||||
},
|
||||
getLevel(index) {
|
||||
return 1;
|
||||
},
|
||||
getImageSrc(row, col) {
|
||||
return "";
|
||||
},
|
||||
toggleOpenState(index) {},
|
||||
cycleHeader(col) {
|
||||
var index = this.selection.currentIndex;
|
||||
if (col.id == gSortedColumn) {
|
||||
gSortDirection = -gSortDirection;
|
||||
gPrefArray.reverse();
|
||||
if (gPrefView != gPrefArray) {
|
||||
gPrefView.reverse();
|
||||
}
|
||||
if (index >= 0) {
|
||||
index = gPrefView.length - index - 1;
|
||||
}
|
||||
} else {
|
||||
var pref = null;
|
||||
if (index >= 0) {
|
||||
pref = gPrefView[index];
|
||||
}
|
||||
|
||||
var old = document.getElementById(gSortedColumn);
|
||||
old.removeAttribute("sortDirection");
|
||||
gPrefArray.sort((gSortFunction = gSortFunctions[col.id]));
|
||||
if (gPrefView != gPrefArray) {
|
||||
gPrefView.sort(gSortFunction);
|
||||
}
|
||||
gSortedColumn = col.id;
|
||||
if (pref) {
|
||||
index = getViewIndexOfPref(pref);
|
||||
}
|
||||
}
|
||||
col.element.setAttribute(
|
||||
"sortDirection",
|
||||
gSortDirection > 0 ? "ascending" : "descending"
|
||||
);
|
||||
this.treebox.invalidate();
|
||||
if (index >= 0) {
|
||||
this.selection.select(index);
|
||||
this.treebox.ensureRowIsVisible(index);
|
||||
}
|
||||
},
|
||||
selectionChanged() {},
|
||||
cycleCell(row, col) {},
|
||||
isEditable(row, col) {
|
||||
return false;
|
||||
},
|
||||
setCellValue(row, col, value) {},
|
||||
setCellText(row, col, value) {},
|
||||
isSeparator(index) {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
// find the index in gPrefView of a pref object
|
||||
// or -1 if it does not exist in the filtered view
|
||||
function getViewIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefView.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
var mid = gPrefView[index];
|
||||
if (mid == pref) {
|
||||
return index;
|
||||
}
|
||||
if (gSortFunction(mid, pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// find the index in gPrefView where a pref object belongs
|
||||
function getNearestViewIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefView.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
if (gSortFunction(gPrefView[index], pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
// find the index in gPrefArray of a pref object
|
||||
function getIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefArray.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
var mid = gPrefArray[index];
|
||||
if (mid == pref) {
|
||||
return index;
|
||||
}
|
||||
if (gSortFunction(mid, pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function getNearestIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefArray.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
if (gSortFunction(gPrefArray[index], pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
var gPrefListener = {
|
||||
observe(subject, topic, prefName) {
|
||||
if (topic != "nsPref:changed") {
|
||||
return;
|
||||
}
|
||||
|
||||
var arrayIndex = gPrefArray.length;
|
||||
var viewIndex = arrayIndex;
|
||||
var selectedIndex = view.selection.currentIndex;
|
||||
var pref;
|
||||
var updateView = false;
|
||||
var updateArray = false;
|
||||
var addedRow = false;
|
||||
if (prefName in gPrefHash) {
|
||||
pref = gPrefHash[prefName];
|
||||
viewIndex = getViewIndexOfPref(pref);
|
||||
arrayIndex = getIndexOfPref(pref);
|
||||
fetchPref(prefName, arrayIndex);
|
||||
// fetchPref replaces the existing pref object
|
||||
pref = gPrefHash[prefName];
|
||||
if (viewIndex >= 0) {
|
||||
// Might need to update the filtered view
|
||||
gPrefView[viewIndex] = gPrefHash[prefName];
|
||||
view.treebox.invalidateRow(viewIndex);
|
||||
}
|
||||
if (gSortedColumn == "lockCol" || gSortedColumn == "valueCol") {
|
||||
updateArray = true;
|
||||
gPrefArray.splice(arrayIndex, 1);
|
||||
if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) {
|
||||
updateView = true;
|
||||
gPrefView.splice(viewIndex, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fetchPref(prefName, arrayIndex);
|
||||
pref = gPrefArray.pop();
|
||||
updateArray = true;
|
||||
addedRow = true;
|
||||
if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) {
|
||||
updateView = true;
|
||||
}
|
||||
}
|
||||
if (updateArray) {
|
||||
// Reinsert in the data array
|
||||
var newIndex = getNearestIndexOfPref(pref);
|
||||
gPrefArray.splice(newIndex, 0, pref);
|
||||
|
||||
if (updateView) {
|
||||
// View is filtered, reinsert in the view separately
|
||||
newIndex = getNearestViewIndexOfPref(pref);
|
||||
gPrefView.splice(newIndex, 0, pref);
|
||||
} else if (gFilter) {
|
||||
// View is filtered, but nothing to update
|
||||
return;
|
||||
}
|
||||
|
||||
if (addedRow) {
|
||||
view.treebox.rowCountChanged(newIndex, 1);
|
||||
}
|
||||
|
||||
// Invalidate the changed range in the view
|
||||
var low = Math.min(viewIndex, newIndex);
|
||||
var high = Math.max(viewIndex, newIndex);
|
||||
view.treebox.invalidateRange(low, high);
|
||||
|
||||
if (selectedIndex == viewIndex) {
|
||||
selectedIndex = newIndex;
|
||||
} else if (selectedIndex >= low && selectedIndex <= high) {
|
||||
selectedIndex += newIndex > viewIndex ? -1 : 1;
|
||||
}
|
||||
if (selectedIndex >= 0) {
|
||||
view.selection.select(selectedIndex);
|
||||
if (selectedIndex == newIndex) {
|
||||
view.treebox.ensureRowIsVisible(selectedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function prefObject(prefName, prefIndex) {
|
||||
this.prefCol = prefName;
|
||||
}
|
||||
|
||||
prefObject.prototype = {
|
||||
lockCol: PREF_IS_DEFAULT_VALUE,
|
||||
typeCol: nsIPrefBranch.PREF_STRING,
|
||||
valueCol: "",
|
||||
};
|
||||
|
||||
function fetchPref(prefName, prefIndex) {
|
||||
var pref = new prefObject(prefName);
|
||||
|
||||
gPrefHash[prefName] = pref;
|
||||
gPrefArray[prefIndex] = pref;
|
||||
|
||||
if (gPrefBranch.prefIsLocked(prefName)) {
|
||||
pref.lockCol = PREF_IS_LOCKED;
|
||||
} else if (gPrefBranch.prefHasUserValue(prefName)) {
|
||||
pref.lockCol = PREF_IS_MODIFIED;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (gPrefBranch.getPrefType(prefName)) {
|
||||
case gPrefBranch.PREF_BOOL:
|
||||
pref.typeCol = gPrefBranch.PREF_BOOL;
|
||||
// convert to a string
|
||||
pref.valueCol = gPrefBranch.getBoolPref(prefName).toString();
|
||||
break;
|
||||
case gPrefBranch.PREF_INT:
|
||||
pref.typeCol = gPrefBranch.PREF_INT;
|
||||
// convert to a string
|
||||
pref.valueCol = gPrefBranch.getIntPref(prefName).toString();
|
||||
break;
|
||||
default:
|
||||
case gPrefBranch.PREF_STRING:
|
||||
pref.valueCol = gPrefBranch.getStringPref(prefName);
|
||||
// Try in case it's a localized string (will throw an exception if not)
|
||||
if (
|
||||
pref.lockCol == PREF_IS_DEFAULT_VALUE &&
|
||||
/^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.valueCol)
|
||||
) {
|
||||
pref.valueCol = gPrefBranch.getComplexValue(
|
||||
prefName,
|
||||
nsIPrefLocalizedString
|
||||
).data;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Also catch obscure cases in which you can't tell in advance
|
||||
// that the pref exists but has no user or default value...
|
||||
}
|
||||
}
|
||||
|
||||
async function onConfigLoad() {
|
||||
let configContext = document.getElementById("configContext");
|
||||
configContext.addEventListener("popupshowing", function(event) {
|
||||
if (event.target == this) {
|
||||
updateContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
let commandListeners = {
|
||||
toggleSelected: ModifySelected,
|
||||
modifySelected: ModifySelected,
|
||||
copyPref,
|
||||
copyName,
|
||||
copyValue,
|
||||
resetSelected: ResetSelected,
|
||||
};
|
||||
|
||||
configContext.addEventListener("command", e => {
|
||||
if (e.target.id in commandListeners) {
|
||||
commandListeners[e.target.id]();
|
||||
}
|
||||
});
|
||||
|
||||
let configString = document.getElementById("configString");
|
||||
configString.addEventListener("command", function() {
|
||||
NewPref(nsIPrefBranch.PREF_STRING);
|
||||
});
|
||||
|
||||
let configInt = document.getElementById("configInt");
|
||||
configInt.addEventListener("command", function() {
|
||||
NewPref(nsIPrefBranch.PREF_INT);
|
||||
});
|
||||
|
||||
let configBool = document.getElementById("configBool");
|
||||
configBool.addEventListener("command", function() {
|
||||
NewPref(nsIPrefBranch.PREF_BOOL);
|
||||
});
|
||||
|
||||
let keyVKReturn = document.getElementById("keyVKReturn");
|
||||
keyVKReturn.addEventListener("command", ModifySelected);
|
||||
|
||||
let textBox = document.getElementById("textbox");
|
||||
textBox.addEventListener("command", FilterPrefs);
|
||||
|
||||
let configFocuSearch = document.getElementById("configFocuSearch");
|
||||
configFocuSearch.addEventListener("command", function() {
|
||||
textBox.focus();
|
||||
});
|
||||
|
||||
let configFocuSearch2 = document.getElementById("configFocuSearch2");
|
||||
configFocuSearch2.addEventListener("command", function() {
|
||||
textBox.focus();
|
||||
});
|
||||
|
||||
let warningButton = document.getElementById("warningButton");
|
||||
warningButton.addEventListener("command", ShowPrefs);
|
||||
|
||||
let configTree = document.getElementById("configTree");
|
||||
configTree.addEventListener("select", function() {
|
||||
window.updateCommands("select");
|
||||
});
|
||||
|
||||
let configTreeBody = document.getElementById("configTreeBody");
|
||||
configTreeBody.addEventListener("dblclick", function(event) {
|
||||
if (event.button == 0) {
|
||||
ModifySelected();
|
||||
}
|
||||
});
|
||||
|
||||
gLockStrs[PREF_IS_DEFAULT_VALUE] = 'default';
|
||||
gLockStrs[PREF_IS_MODIFIED] = 'modified';
|
||||
gLockStrs[PREF_IS_LOCKED] = 'locked';
|
||||
gTypeStrs[nsIPrefBranch.PREF_STRING] = 'string';
|
||||
gTypeStrs[nsIPrefBranch.PREF_INT] = 'integer';
|
||||
gTypeStrs[nsIPrefBranch.PREF_BOOL] = 'boolean';
|
||||
|
||||
var showWarning = gPrefBranch.getBoolPref("general.warnOnAboutConfig");
|
||||
|
||||
if (showWarning) {
|
||||
document.getElementById("warningButton").focus();
|
||||
} else {
|
||||
ShowPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
// Unhide the warning message
|
||||
function ShowPrefs() {
|
||||
document.getElementById('configDeck').lastElementChild.style.visibility = 'visible';
|
||||
gPrefBranch.getChildList("").forEach(fetchPref);
|
||||
|
||||
var descending = document.getElementsByAttribute(
|
||||
"sortDirection",
|
||||
"descending"
|
||||
);
|
||||
if (descending.item(0)) {
|
||||
gSortedColumn = descending[0].id;
|
||||
gSortDirection = -1;
|
||||
} else {
|
||||
var ascending = document.getElementsByAttribute(
|
||||
"sortDirection",
|
||||
"ascending"
|
||||
);
|
||||
if (ascending.item(0)) {
|
||||
gSortedColumn = ascending[0].id;
|
||||
} else {
|
||||
document
|
||||
.getElementById(gSortedColumn)
|
||||
.setAttribute("sortDirection", "ascending");
|
||||
}
|
||||
}
|
||||
gSortFunction = gSortFunctions[gSortedColumn];
|
||||
gPrefArray.sort(gSortFunction);
|
||||
|
||||
gPrefBranch.addObserver("", gPrefListener);
|
||||
|
||||
var configTree = document.getElementById("configTree");
|
||||
configTree.view = view;
|
||||
configTree.controllers.insertControllerAt(0, configController);
|
||||
|
||||
document.getElementById("configDeck").setAttribute("selectedIndex", 1);
|
||||
document.getElementById("configTreeKeyset").removeAttribute("disabled");
|
||||
if (!document.getElementById("showWarningNextTime").checked) {
|
||||
gPrefBranch.setBoolPref("general.warnOnAboutConfig", false);
|
||||
}
|
||||
|
||||
// Process about:config?filter=<string>
|
||||
var textbox = document.getElementById("textbox");
|
||||
// About URIs don't support query params, so do this manually
|
||||
var loc = document.location.href;
|
||||
var matches = /[?&]filter\=([^&]+)/i.exec(loc);
|
||||
if (matches) {
|
||||
textbox.value = decodeURIComponent(matches[1]);
|
||||
}
|
||||
|
||||
// Even if we did not set the filter string via the URL query,
|
||||
// textbox might have been set via some other mechanism
|
||||
if (textbox.value) {
|
||||
FilterPrefs();
|
||||
}
|
||||
textbox.focus();
|
||||
}
|
||||
|
||||
function onConfigUnload() {
|
||||
if (
|
||||
document.getElementById("configDeck").getAttribute("selectedIndex") == 1
|
||||
) {
|
||||
gPrefBranch.removeObserver("", gPrefListener);
|
||||
var configTree = document.getElementById("configTree");
|
||||
configTree.view = null;
|
||||
configTree.controllers.removeController(configController);
|
||||
}
|
||||
}
|
||||
|
||||
function FilterPrefs() {
|
||||
if (
|
||||
document.getElementById("configDeck").getAttribute("selectedIndex") != 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
var substring = document.getElementById("textbox").value;
|
||||
// Check for "/regex/[i]"
|
||||
if (substring.charAt(0) == "/") {
|
||||
var r = substring.match(/^\/(.*)\/(i?)$/);
|
||||
try {
|
||||
gFilter = RegExp(r[1], r[2]);
|
||||
} catch (e) {
|
||||
return; // Do nothing on incomplete or bad RegExp
|
||||
}
|
||||
} else if (substring) {
|
||||
gFilter = RegExp(
|
||||
substring
|
||||
.replace(/([^* \w])/g, "\\$1")
|
||||
.replace(/^\*+/, "")
|
||||
.replace(/\*+/g, ".*"),
|
||||
"i"
|
||||
);
|
||||
} else {
|
||||
gFilter = null;
|
||||
}
|
||||
|
||||
var prefCol =
|
||||
view.selection && view.selection.currentIndex < 0
|
||||
? null
|
||||
: gPrefView[view.selection.currentIndex].prefCol;
|
||||
var oldlen = gPrefView.length;
|
||||
gPrefView = gPrefArray;
|
||||
if (gFilter) {
|
||||
gPrefView = [];
|
||||
for (var i = 0; i < gPrefArray.length; ++i) {
|
||||
if (gFilter.test(gPrefArray[i].prefCol + ";" + gPrefArray[i].valueCol)) {
|
||||
gPrefView.push(gPrefArray[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
view.treebox.invalidate();
|
||||
view.treebox.rowCountChanged(oldlen, gPrefView.length - oldlen);
|
||||
gotoPref(prefCol);
|
||||
}
|
||||
|
||||
function prefColSortFunction(x, y) {
|
||||
if (x.prefCol > y.prefCol) {
|
||||
return gSortDirection;
|
||||
}
|
||||
if (x.prefCol < y.prefCol) {
|
||||
return -gSortDirection;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function lockColSortFunction(x, y) {
|
||||
if (x.lockCol != y.lockCol) {
|
||||
return gSortDirection * (y.lockCol - x.lockCol);
|
||||
}
|
||||
return prefColSortFunction(x, y);
|
||||
}
|
||||
|
||||
function typeColSortFunction(x, y) {
|
||||
if (x.typeCol != y.typeCol) {
|
||||
return gSortDirection * (y.typeCol - x.typeCol);
|
||||
}
|
||||
return prefColSortFunction(x, y);
|
||||
}
|
||||
|
||||
function valueColSortFunction(x, y) {
|
||||
if (x.valueCol > y.valueCol) {
|
||||
return gSortDirection;
|
||||
}
|
||||
if (x.valueCol < y.valueCol) {
|
||||
return -gSortDirection;
|
||||
}
|
||||
return prefColSortFunction(x, y);
|
||||
}
|
||||
|
||||
const gSortFunctions = {
|
||||
prefCol: prefColSortFunction,
|
||||
lockCol: lockColSortFunction,
|
||||
typeCol: typeColSortFunction,
|
||||
valueCol: valueColSortFunction,
|
||||
};
|
||||
|
||||
const gCategoryLabelForSortColumn = {
|
||||
prefCol: "SortByName",
|
||||
lockCol: "SortByStatus",
|
||||
typeCol: "SortByType",
|
||||
valueCol: "SortByValue",
|
||||
};
|
||||
|
||||
const configController = {
|
||||
supportsCommand: function supportsCommand(command) {
|
||||
return command == "cmd_copy";
|
||||
},
|
||||
isCommandEnabled: function isCommandEnabled(command) {
|
||||
return view.selection && view.selection.currentIndex >= 0;
|
||||
},
|
||||
doCommand: function doCommand(command) {
|
||||
copyPref();
|
||||
},
|
||||
onEvent: function onEvent(event) {},
|
||||
};
|
||||
|
||||
function updateContextMenu() {
|
||||
var lockCol = PREF_IS_LOCKED;
|
||||
var typeCol = nsIPrefBranch.PREF_STRING;
|
||||
var valueCol = "";
|
||||
var copyDisabled = true;
|
||||
var prefSelected = view.selection.currentIndex >= 0;
|
||||
|
||||
if (prefSelected) {
|
||||
var prefRow = gPrefView[view.selection.currentIndex];
|
||||
lockCol = prefRow.lockCol;
|
||||
typeCol = prefRow.typeCol;
|
||||
valueCol = prefRow.valueCol;
|
||||
copyDisabled = false;
|
||||
}
|
||||
|
||||
var copyPref = document.getElementById("copyPref");
|
||||
copyPref.setAttribute("disabled", copyDisabled);
|
||||
|
||||
var copyName = document.getElementById("copyName");
|
||||
copyName.setAttribute("disabled", copyDisabled);
|
||||
|
||||
var copyValue = document.getElementById("copyValue");
|
||||
copyValue.setAttribute("disabled", copyDisabled);
|
||||
|
||||
var resetSelected = document.getElementById("resetSelected");
|
||||
resetSelected.setAttribute("disabled", lockCol != PREF_IS_MODIFIED);
|
||||
|
||||
var canToggle = typeCol == nsIPrefBranch.PREF_BOOL && valueCol != "";
|
||||
// indicates that a pref is locked or no pref is selected at all
|
||||
var isLocked = lockCol == PREF_IS_LOCKED;
|
||||
|
||||
var modifySelected = document.getElementById("modifySelected");
|
||||
modifySelected.setAttribute("disabled", isLocked);
|
||||
modifySelected.hidden = canToggle;
|
||||
|
||||
var toggleSelected = document.getElementById("toggleSelected");
|
||||
toggleSelected.setAttribute("disabled", isLocked);
|
||||
toggleSelected.hidden = !canToggle;
|
||||
}
|
||||
|
||||
function copyPref() {
|
||||
var pref = gPrefView[view.selection.currentIndex];
|
||||
gClipboardHelper.copyString(pref.prefCol + ";" + pref.valueCol);
|
||||
}
|
||||
|
||||
function copyName() {
|
||||
gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].prefCol);
|
||||
}
|
||||
|
||||
function copyValue() {
|
||||
gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].valueCol);
|
||||
}
|
||||
|
||||
function ModifySelected() {
|
||||
if (view.selection.currentIndex >= 0) {
|
||||
ModifyPref(gPrefView[view.selection.currentIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
function ResetSelected() {
|
||||
var entry = gPrefView[view.selection.currentIndex];
|
||||
gPrefBranch.clearUserPref(entry.prefCol);
|
||||
}
|
||||
|
||||
async function NewPref(type) {
|
||||
var result = { value: "" };
|
||||
var dummy = { value: 0 };
|
||||
|
||||
let [newTitle, newPrompt] = [`New ${gTypeStrs[type]} value`, 'Enter the preference name'];
|
||||
|
||||
if (
|
||||
Services.prompt.prompt(window, newTitle, newPrompt, result, null, dummy)
|
||||
) {
|
||||
result.value = result.value.trim();
|
||||
if (!result.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pref;
|
||||
if (result.value in gPrefHash) {
|
||||
pref = gPrefHash[result.value];
|
||||
} else {
|
||||
pref = {
|
||||
prefCol: result.value,
|
||||
lockCol: PREF_IS_DEFAULT_VALUE,
|
||||
typeCol: type,
|
||||
valueCol: "",
|
||||
};
|
||||
}
|
||||
if (ModifyPref(pref)) {
|
||||
setTimeout(gotoPref, 0, result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gotoPref(pref) {
|
||||
// make sure the pref exists and is displayed in the current view
|
||||
var index = pref in gPrefHash ? getViewIndexOfPref(gPrefHash[pref]) : -1;
|
||||
if (index >= 0) {
|
||||
view.selection.select(index);
|
||||
view.treebox.ensureRowIsVisible(index);
|
||||
} else {
|
||||
view.selection.clearSelection();
|
||||
view.selection.currentIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
async function ModifyPref(entry) {
|
||||
if (entry.lockCol == PREF_IS_LOCKED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let [title] = [`Enter ${gTypeStrs[entry.typeCol]} value`];
|
||||
|
||||
if (entry.typeCol == nsIPrefBranch.PREF_BOOL) {
|
||||
var check = { value: entry.valueCol == "false" };
|
||||
if (
|
||||
!entry.valueCol &&
|
||||
!Services.prompt.select(
|
||||
window,
|
||||
title,
|
||||
entry.prefCol,
|
||||
[false, true],
|
||||
check
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
gPrefBranch.setBoolPref(entry.prefCol, check.value);
|
||||
} else {
|
||||
var result = { value: entry.valueCol };
|
||||
var dummy = { value: 0 };
|
||||
if (
|
||||
!Services.prompt.prompt(window, title, entry.prefCol, result, null, dummy)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (entry.typeCol == nsIPrefBranch.PREF_INT) {
|
||||
// | 0 converts to integer or 0; - 0 to float or NaN.
|
||||
// Thus, this check should catch all cases.
|
||||
var val = result.value | 0;
|
||||
if (val != result.value - 0) {
|
||||
const [err_title, err_text] = ['Invalid value', 'The text you entered is not a number.'];
|
||||
|
||||
Services.prompt.alert(window, err_title, err_text);
|
||||
return false;
|
||||
}
|
||||
gPrefBranch.setIntPref(entry.prefCol, val);
|
||||
} else {
|
||||
gPrefBranch.setStringPref(entry.prefCol, result.value);
|
||||
}
|
||||
}
|
||||
|
||||
Services.prefs.savePrefFile(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
window.onload = onConfigLoad;
|
||||
window.onunload = onConfigUnload;
|
2
chrome/utils/chrome.manifest
Normal file
|
@ -0,0 +1,2 @@
|
|||
content userchromejs ./
|
||||
resource userchromejs ../
|
42
chrome/utils/hookFunction.jsm
Normal file
|
@ -0,0 +1,42 @@
|
|||
let EXPORTED_SYMBOLS = ['hookFunction'];
|
||||
|
||||
/**
|
||||
* Add hooks to a function to execute before and after it. The function to modify is functionContext[functionName]. Call only once per function - modification is not supported.
|
||||
*
|
||||
* Other addons wishing to access the original function may do so using the .originalFunction member of the replacement function. This member can also be set if required, to insert a new function replacement into the chain rather than appending.
|
||||
*
|
||||
* @param functionContext The object on which the function is a property
|
||||
* @param functionName The name of the property containing the function (on functionContext)
|
||||
* @param onBeforeFunction A function to be called before the hooked function is executed. It will be passed the same parameters as the hooked function. It's return value will be passed on to onAfterFunction
|
||||
* @param onAfterFunction A function to be called after the hooked function is executed. The parameters passed to it are: onBeforeFunction return value, arguments object from original hooked function, return value from original hooked function. It's return value will be returned in place of that of the original function.
|
||||
* @returns A function which can be called to safely un-hook the hook
|
||||
*/
|
||||
function hookFunction(functionContext, functionName, onBeforeFunction, onAfterFunction) {
|
||||
let originalFunction = functionContext[functionName];
|
||||
|
||||
if (!originalFunction) {
|
||||
throw new Error("Could not find function " + functionName);
|
||||
}
|
||||
|
||||
let replacementFunction = function() {
|
||||
let onBeforeResult = null;
|
||||
if (onBeforeFunction) {
|
||||
onBeforeResult = onBeforeFunction.apply(this, arguments);
|
||||
}
|
||||
let originalResult = replacementFunction.originalFunction.apply(this, arguments);
|
||||
if (onAfterFunction) {
|
||||
return onAfterFunction.call(this, onBeforeResult, arguments, originalResult);
|
||||
} else {
|
||||
return originalResult;
|
||||
}
|
||||
}
|
||||
replacementFunction.originalFunction = originalFunction;
|
||||
functionContext[functionName] = replacementFunction;
|
||||
|
||||
return function () {
|
||||
// Not safe to simply assign originalFunction back again, as something else might have chained onto this function, which would then break the chain
|
||||
// Unassigning these variables prevent any effects of the hook, though the function itself remains in place.
|
||||
onBeforeFunction = null;
|
||||
onAfterFunction = null;
|
||||
};
|
||||
}
|
845
chrome/utils/passwordmgr/passwordManager.js
Normal file
|
@ -0,0 +1,845 @@
|
|||
/* 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/. */
|
||||
|
||||
/** * =================== SAVED SIGNONS CODE =================== ***/
|
||||
/* eslint-disable-next-line no-var */
|
||||
var { AppConstants } = ChromeUtils.import(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
/* eslint-disable-next-line no-var */
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"DeferredTask",
|
||||
"resource://gre/modules/DeferredTask.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm"
|
||||
);
|
||||
|
||||
// Default value for signon table sorting
|
||||
let lastSignonSortColumn = "origin";
|
||||
let lastSignonSortAscending = true;
|
||||
|
||||
let showingPasswords = false;
|
||||
|
||||
// password-manager lists
|
||||
let signons = [];
|
||||
let deletedSignons = [];
|
||||
|
||||
// Elements that would be used frequently
|
||||
let filterField;
|
||||
let togglePasswordsButton;
|
||||
let signonsIntro;
|
||||
let removeButton;
|
||||
let removeAllButton;
|
||||
let signonsTree;
|
||||
|
||||
let signonReloadDisplay = {
|
||||
observe(subject, topic, data) {
|
||||
if (topic == "passwordmgr-storage-changed") {
|
||||
switch (data) {
|
||||
case "addLogin":
|
||||
case "modifyLogin":
|
||||
case "removeLogin":
|
||||
case "removeAllLogins":
|
||||
if (!signonsTree) {
|
||||
return;
|
||||
}
|
||||
signons.length = 0;
|
||||
LoadSignons();
|
||||
// apply the filter if needed
|
||||
if (filterField && filterField.value != "") {
|
||||
FilterPasswords();
|
||||
}
|
||||
signonsTree.ensureRowIsVisible(
|
||||
signonsTree.view.selection.currentIndex
|
||||
);
|
||||
break;
|
||||
}
|
||||
Services.obs.notifyObservers(null, "passwordmgr-dialog-updated");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Formatter for localization.
|
||||
let dateFormatter = new Services.intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "medium",
|
||||
});
|
||||
let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
|
||||
function Startup() {
|
||||
// be prepared to reload the display if anything changes
|
||||
Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed");
|
||||
|
||||
signonsTree = document.getElementById("signonsTree");
|
||||
filterField = document.getElementById("filter");
|
||||
togglePasswordsButton = document.getElementById("togglePasswords");
|
||||
signonsIntro = document.getElementById("signonsIntro");
|
||||
removeButton = document.getElementById("removeSignon");
|
||||
removeAllButton = document.getElementById("removeAllSignons");
|
||||
|
||||
togglePasswordsButton.label = "Show Passwords";
|
||||
togglePasswordsButton.accessKey = "P";
|
||||
signonsIntro.textContent = "Logins for the following sites are stored on your computer";
|
||||
removeAllButton.label = "Remove All";
|
||||
removeAllButton.accessKey = "A";
|
||||
|
||||
if (Services.policies && !Services.policies.isAllowed("passwordReveal")) {
|
||||
togglePasswordsButton.hidden = true;
|
||||
}
|
||||
|
||||
document
|
||||
.getElementsByTagName("treecols")[0]
|
||||
.addEventListener("click", event => {
|
||||
let { target, button } = event;
|
||||
let sortField = target.getAttribute("data-field-name");
|
||||
|
||||
if (target.nodeName != "treecol" || button != 0 || !sortField) {
|
||||
return;
|
||||
}
|
||||
|
||||
SignonColumnSort(sortField);
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById("PWMGR_MANAGE_SORTED")
|
||||
.add(sortField);
|
||||
});
|
||||
|
||||
LoadSignons();
|
||||
|
||||
// filter the table if requested by caller
|
||||
if (
|
||||
window.arguments &&
|
||||
window.arguments[0] &&
|
||||
window.arguments[0].filterString
|
||||
) {
|
||||
setFilter(window.arguments[0].filterString);
|
||||
}
|
||||
|
||||
FocusFilterBox();
|
||||
}
|
||||
|
||||
function Shutdown() {
|
||||
Services.obs.removeObserver(
|
||||
signonReloadDisplay,
|
||||
"passwordmgr-storage-changed"
|
||||
);
|
||||
}
|
||||
|
||||
function setFilter(aFilterString) {
|
||||
filterField.value = aFilterString;
|
||||
FilterPasswords();
|
||||
}
|
||||
|
||||
let signonsTreeView = {
|
||||
_filterSet: [],
|
||||
_lastSelectedRanges: [],
|
||||
selection: null,
|
||||
|
||||
rowCount: 0,
|
||||
setTree(tree) {},
|
||||
getImageSrc(row, column) {
|
||||
if (column.element.getAttribute("id") !== "siteCol") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const signon = GetVisibleLogins()[row];
|
||||
|
||||
return PlacesUtils.urlWithSizeRef(window, "page-icon:" + signon.origin, 16);
|
||||
},
|
||||
getCellValue(row, column) {},
|
||||
getCellText(row, column) {
|
||||
let time;
|
||||
let signon = GetVisibleLogins()[row];
|
||||
switch (column.id) {
|
||||
case "siteCol":
|
||||
return signon.httpRealm
|
||||
? signon.origin + " (" + signon.httpRealm + ")"
|
||||
: signon.origin;
|
||||
case "userCol":
|
||||
return signon.username || "";
|
||||
case "passwordCol":
|
||||
return signon.password || "";
|
||||
case "timeCreatedCol":
|
||||
time = new Date(signon.timeCreated);
|
||||
return dateFormatter.format(time);
|
||||
case "timeLastUsedCol":
|
||||
time = new Date(signon.timeLastUsed);
|
||||
return dateAndTimeFormatter.format(time);
|
||||
case "timePasswordChangedCol":
|
||||
time = new Date(signon.timePasswordChanged);
|
||||
return dateFormatter.format(time);
|
||||
case "timesUsedCol":
|
||||
return signon.timesUsed;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
},
|
||||
isEditable(row, col) {
|
||||
if (col.id == "userCol" || col.id == "passwordCol") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isSeparator(index) {
|
||||
return false;
|
||||
},
|
||||
isSorted() {
|
||||
return false;
|
||||
},
|
||||
isContainer(index) {
|
||||
return false;
|
||||
},
|
||||
cycleHeader(column) {},
|
||||
getRowProperties(row) {
|
||||
return "";
|
||||
},
|
||||
getColumnProperties(column) {
|
||||
return "";
|
||||
},
|
||||
getCellProperties(row, column) {
|
||||
if (column.element.getAttribute("id") == "siteCol") {
|
||||
return "ltr";
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
setCellText(row, col, value) {
|
||||
let table = GetVisibleLogins();
|
||||
function _editLogin(field) {
|
||||
if (value == table[row][field]) {
|
||||
return;
|
||||
}
|
||||
let existingLogin = table[row].clone();
|
||||
table[row][field] = value;
|
||||
table[row].timePasswordChanged = Date.now();
|
||||
Services.logins.modifyLogin(existingLogin, table[row]);
|
||||
signonsTree.invalidateRow(row);
|
||||
}
|
||||
|
||||
if (col.id == "userCol") {
|
||||
_editLogin("username");
|
||||
} else if (col.id == "passwordCol") {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
_editLogin("password");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function SortTree(column, ascending) {
|
||||
let table = GetVisibleLogins();
|
||||
// remember which item was selected so we can restore it after the sort
|
||||
let selections = GetTreeSelections();
|
||||
let selectedNumber = selections.length ? table[selections[0]].number : -1;
|
||||
function compareFunc(a, b) {
|
||||
let valA, valB;
|
||||
switch (column) {
|
||||
case "origin":
|
||||
let realmA = a.httpRealm;
|
||||
let realmB = b.httpRealm;
|
||||
realmA = realmA == null ? "" : realmA.toLowerCase();
|
||||
realmB = realmB == null ? "" : realmB.toLowerCase();
|
||||
|
||||
valA = a[column].toLowerCase() + realmA;
|
||||
valB = b[column].toLowerCase() + realmB;
|
||||
break;
|
||||
case "username":
|
||||
case "password":
|
||||
valA = a[column].toLowerCase();
|
||||
valB = b[column].toLowerCase();
|
||||
break;
|
||||
|
||||
default:
|
||||
valA = a[column];
|
||||
valB = b[column];
|
||||
}
|
||||
|
||||
if (valA < valB) {
|
||||
return -1;
|
||||
}
|
||||
if (valA > valB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// do the sort
|
||||
table.sort(compareFunc);
|
||||
if (!ascending) {
|
||||
table.reverse();
|
||||
}
|
||||
|
||||
// restore the selection
|
||||
let selectedRow = -1;
|
||||
if (selectedNumber >= 0 && false) {
|
||||
for (let s = 0; s < table.length; s++) {
|
||||
if (table[s].number == selectedNumber) {
|
||||
// update selection
|
||||
// note: we need to deselect before reselecting in order to trigger ...Selected()
|
||||
signonsTree.view.selection.select(-1);
|
||||
signonsTree.view.selection.select(s);
|
||||
selectedRow = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// display the results
|
||||
signonsTree.invalidate();
|
||||
if (selectedRow >= 0) {
|
||||
signonsTree.ensureRowIsVisible(selectedRow);
|
||||
}
|
||||
}
|
||||
|
||||
function LoadSignons() {
|
||||
// loads signons into table
|
||||
try {
|
||||
signons = Services.logins.getAllLogins();
|
||||
} catch (e) {
|
||||
signons = [];
|
||||
}
|
||||
signons.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo));
|
||||
signonsTreeView.rowCount = signons.length;
|
||||
|
||||
// sort and display the table
|
||||
signonsTree.view = signonsTreeView;
|
||||
// The sort column didn't change. SortTree (called by
|
||||
// SignonColumnSort) assumes we want to toggle the sort
|
||||
// direction but here we don't so we have to trick it
|
||||
lastSignonSortAscending = !lastSignonSortAscending;
|
||||
SignonColumnSort(lastSignonSortColumn);
|
||||
|
||||
// disable "remove all signons" button if there are no signons
|
||||
if (!signons.length) {
|
||||
removeAllButton.setAttribute("disabled", "true");
|
||||
togglePasswordsButton.setAttribute("disabled", "true");
|
||||
} else {
|
||||
removeAllButton.removeAttribute("disabled");
|
||||
togglePasswordsButton.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function GetVisibleLogins() {
|
||||
return signonsTreeView._filterSet.length
|
||||
? signonsTreeView._filterSet
|
||||
: signons;
|
||||
}
|
||||
|
||||
function GetTreeSelections() {
|
||||
let selections = [];
|
||||
let select = signonsTree.view.selection;
|
||||
if (select) {
|
||||
let count = select.getRangeCount();
|
||||
let min = {};
|
||||
let max = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
select.getRangeAt(i, min, max);
|
||||
for (let k = min.value; k <= max.value; k++) {
|
||||
if (k != -1) {
|
||||
selections[selections.length] = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selections;
|
||||
}
|
||||
|
||||
function SignonSelected() {
|
||||
let selections = GetTreeSelections();
|
||||
if (selections.length) {
|
||||
removeButton.removeAttribute("disabled");
|
||||
} else {
|
||||
removeButton.setAttribute("disabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
function DeleteSignon() {
|
||||
let syncNeeded = !!signonsTreeView._filterSet.length;
|
||||
let tree = signonsTree;
|
||||
let view = signonsTreeView;
|
||||
let table = GetVisibleLogins();
|
||||
|
||||
// Turn off tree selection notifications during the deletion
|
||||
tree.view.selection.selectEventsSuppressed = true;
|
||||
|
||||
// remove selected items from list (by setting them to null) and place in deleted list
|
||||
let selections = GetTreeSelections();
|
||||
for (let s = selections.length - 1; s >= 0; s--) {
|
||||
let i = selections[s];
|
||||
deletedSignons.push(table[i]);
|
||||
table[i] = null;
|
||||
}
|
||||
|
||||
// collapse list by removing all the null entries
|
||||
for (let j = 0; j < table.length; j++) {
|
||||
if (table[j] == null) {
|
||||
let k = j;
|
||||
while (k < table.length && table[k] == null) {
|
||||
k++;
|
||||
}
|
||||
table.splice(j, k - j);
|
||||
view.rowCount -= k - j;
|
||||
tree.rowCountChanged(j, j - k);
|
||||
}
|
||||
}
|
||||
|
||||
// update selection and/or buttons
|
||||
if (table.length) {
|
||||
// update selection
|
||||
let nextSelection =
|
||||
selections[0] < table.length ? selections[0] : table.length - 1;
|
||||
tree.view.selection.select(nextSelection);
|
||||
} else {
|
||||
// disable buttons
|
||||
removeButton.setAttribute("disabled", "true");
|
||||
removeAllButton.setAttribute("disabled", "true");
|
||||
}
|
||||
tree.view.selection.selectEventsSuppressed = false;
|
||||
FinalizeSignonDeletions(syncNeeded);
|
||||
}
|
||||
|
||||
async function DeleteAllSignons() {
|
||||
// Confirm the user wants to remove all passwords
|
||||
let dummy = { value: false };
|
||||
if (
|
||||
Services.prompt.confirmEx(
|
||||
window,
|
||||
"Remove all passwords",
|
||||
"Are you sure you wish to remove all passwords?",
|
||||
Services.prompt.STD_YES_NO_BUTTONS + Services.prompt.BUTTON_POS_1_DEFAULT,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
dummy
|
||||
) == 1
|
||||
) {
|
||||
// 1 == "No" button
|
||||
return;
|
||||
}
|
||||
|
||||
let syncNeeded = !!signonsTreeView._filterSet.length;
|
||||
let view = signonsTreeView;
|
||||
let table = GetVisibleLogins();
|
||||
|
||||
// remove all items from table and place in deleted table
|
||||
for (let i = 0; i < table.length; i++) {
|
||||
deletedSignons.push(table[i]);
|
||||
}
|
||||
table.length = 0;
|
||||
|
||||
// clear out selections
|
||||
view.selection.select(-1);
|
||||
|
||||
// update the tree view and notify the tree
|
||||
view.rowCount = 0;
|
||||
|
||||
signonsTree.rowCountChanged(0, -deletedSignons.length);
|
||||
signonsTree.invalidate();
|
||||
|
||||
// disable buttons
|
||||
removeButton.setAttribute("disabled", "true");
|
||||
removeAllButton.setAttribute("disabled", "true");
|
||||
FinalizeSignonDeletions(syncNeeded);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED_ALL").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_DELETED_ALL"
|
||||
);
|
||||
}
|
||||
|
||||
async function TogglePasswordVisible() {
|
||||
if (showingPasswords || (await masterPasswordLogin(AskUserShowPasswords))) {
|
||||
showingPasswords = !showingPasswords;
|
||||
togglePasswordsButton.label = showingPasswords ? "Hide Passwords" : "Show Passwords";
|
||||
togglePasswordsButton.accessKey = "P";
|
||||
document.getElementById("passwordCol").hidden = !showingPasswords;
|
||||
FilterPasswords();
|
||||
}
|
||||
|
||||
// Notify observers that the password visibility toggling is
|
||||
// completed. (Mostly useful for tests)
|
||||
Services.obs.notifyObservers(null, "passwordmgr-password-toggle-complete");
|
||||
Services.telemetry
|
||||
.getHistogramById("PWMGR_MANAGE_VISIBILITY_TOGGLED")
|
||||
.add(showingPasswords);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_VISIBILITY_TOGGLED"
|
||||
);
|
||||
}
|
||||
|
||||
async function AskUserShowPasswords() {
|
||||
let dummy = { value: false };
|
||||
|
||||
// Confirm the user wants to display passwords
|
||||
return (
|
||||
Services.prompt.confirmEx(
|
||||
window,
|
||||
null,
|
||||
"Are you sure you wish to show your passwords?",
|
||||
Services.prompt.STD_YES_NO_BUTTONS,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
dummy
|
||||
) == 0
|
||||
); // 0=="Yes" button
|
||||
}
|
||||
|
||||
function FinalizeSignonDeletions(syncNeeded) {
|
||||
for (let s = 0; s < deletedSignons.length; s++) {
|
||||
Services.logins.removeLogin(deletedSignons[s]);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_DELETED"
|
||||
);
|
||||
}
|
||||
// If the deletion has been performed in a filtered view, reflect the deletion in the unfiltered table.
|
||||
// See bug 405389.
|
||||
if (syncNeeded) {
|
||||
try {
|
||||
signons = Services.logins.getAllLogins();
|
||||
} catch (e) {
|
||||
signons = [];
|
||||
}
|
||||
}
|
||||
deletedSignons.length = 0;
|
||||
}
|
||||
|
||||
function HandleSignonKeyPress(e) {
|
||||
// If editing is currently performed, don't do anything.
|
||||
if (signonsTree.getAttribute("editing")) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.keyCode == KeyboardEvent.DOM_VK_DELETE ||
|
||||
(AppConstants.platform == "macosx" &&
|
||||
e.keyCode == KeyboardEvent.DOM_VK_BACK_SPACE)
|
||||
) {
|
||||
DeleteSignon();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function getColumnByName(column) {
|
||||
switch (column) {
|
||||
case "origin":
|
||||
return document.getElementById("siteCol");
|
||||
case "username":
|
||||
return document.getElementById("userCol");
|
||||
case "password":
|
||||
return document.getElementById("passwordCol");
|
||||
case "timeCreated":
|
||||
return document.getElementById("timeCreatedCol");
|
||||
case "timeLastUsed":
|
||||
return document.getElementById("timeLastUsedCol");
|
||||
case "timePasswordChanged":
|
||||
return document.getElementById("timePasswordChangedCol");
|
||||
case "timesUsed":
|
||||
return document.getElementById("timesUsedCol");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function SignonColumnSort(column) {
|
||||
let sortedCol = getColumnByName(column);
|
||||
let lastSortedCol = getColumnByName(lastSignonSortColumn);
|
||||
|
||||
// clear out the sortDirection attribute on the old column
|
||||
lastSortedCol.removeAttribute("sortDirection");
|
||||
|
||||
// determine if sort is to be ascending or descending
|
||||
lastSignonSortAscending =
|
||||
column == lastSignonSortColumn ? !lastSignonSortAscending : true;
|
||||
|
||||
// sort
|
||||
lastSignonSortColumn = column;
|
||||
SortTree(lastSignonSortColumn, lastSignonSortAscending);
|
||||
|
||||
// set the sortDirection attribute to get the styling going
|
||||
// first we need to get the right element
|
||||
sortedCol.setAttribute(
|
||||
"sortDirection",
|
||||
lastSignonSortAscending ? "ascending" : "descending"
|
||||
);
|
||||
}
|
||||
|
||||
function SignonClearFilter() {
|
||||
let singleSelection = signonsTreeView.selection.count == 1;
|
||||
|
||||
// Clear the Tree Display
|
||||
signonsTreeView.rowCount = 0;
|
||||
signonsTree.rowCountChanged(0, -signonsTreeView._filterSet.length);
|
||||
signonsTreeView._filterSet = [];
|
||||
|
||||
// Just reload the list to make sure deletions are respected
|
||||
LoadSignons();
|
||||
|
||||
// Restore selection
|
||||
if (singleSelection) {
|
||||
signonsTreeView.selection.clearSelection();
|
||||
for (let i = 0; i < signonsTreeView._lastSelectedRanges.length; ++i) {
|
||||
let range = signonsTreeView._lastSelectedRanges[i];
|
||||
signonsTreeView.selection.rangedSelect(range.min, range.max, true);
|
||||
}
|
||||
} else {
|
||||
signonsTreeView.selection.select(0);
|
||||
}
|
||||
signonsTreeView._lastSelectedRanges = [];
|
||||
|
||||
signonsIntro.textContent = "Logins for the following sites are stored on your computer";
|
||||
removeAllButton.label = "Remove All";
|
||||
removeAllButton.accessKey = "A";
|
||||
}
|
||||
|
||||
function FocusFilterBox() {
|
||||
if (filterField.getAttribute("focused") != "true") {
|
||||
filterField.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function SignonMatchesFilter(aSignon, aFilterValue) {
|
||||
if (aSignon.origin.toLowerCase().includes(aFilterValue)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
aSignon.username &&
|
||||
aSignon.username.toLowerCase().includes(aFilterValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
aSignon.httpRealm &&
|
||||
aSignon.httpRealm.toLowerCase().includes(aFilterValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
showingPasswords &&
|
||||
aSignon.password &&
|
||||
aSignon.password.toLowerCase().includes(aFilterValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _filterPasswords(aFilterValue, view) {
|
||||
aFilterValue = aFilterValue.toLowerCase();
|
||||
return signons.filter(s => SignonMatchesFilter(s, aFilterValue));
|
||||
}
|
||||
|
||||
function SignonSaveState() {
|
||||
// Save selection
|
||||
let seln = signonsTreeView.selection;
|
||||
signonsTreeView._lastSelectedRanges = [];
|
||||
let rangeCount = seln.getRangeCount();
|
||||
for (let i = 0; i < rangeCount; ++i) {
|
||||
let min = {};
|
||||
let max = {};
|
||||
seln.getRangeAt(i, min, max);
|
||||
signonsTreeView._lastSelectedRanges.push({
|
||||
min: min.value,
|
||||
max: max.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function FilterPasswords() {
|
||||
if (filterField.value == "") {
|
||||
SignonClearFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
let newFilterSet = _filterPasswords(filterField.value, signonsTreeView);
|
||||
if (!signonsTreeView._filterSet.length) {
|
||||
// Save Display Info for the Non-Filtered mode when we first
|
||||
// enter Filtered mode.
|
||||
SignonSaveState();
|
||||
}
|
||||
signonsTreeView._filterSet = newFilterSet;
|
||||
|
||||
// Clear the display
|
||||
let oldRowCount = signonsTreeView.rowCount;
|
||||
signonsTreeView.rowCount = 0;
|
||||
signonsTree.rowCountChanged(0, -oldRowCount);
|
||||
// Set up the filtered display
|
||||
signonsTreeView.rowCount = signonsTreeView._filterSet.length;
|
||||
signonsTree.rowCountChanged(0, signonsTreeView.rowCount);
|
||||
|
||||
// if the view is not empty then select the first item
|
||||
if (signonsTreeView.rowCount > 0) {
|
||||
signonsTreeView.selection.select(0);
|
||||
}
|
||||
|
||||
signonsIntro.textContent = "The following logins match your search:";
|
||||
removeAllButton.label = "Remove All Shown";
|
||||
removeAllButton.accessKey = "A";
|
||||
}
|
||||
|
||||
function CopySiteUrl() {
|
||||
// Copy selected site url to clipboard
|
||||
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
||||
Ci.nsIClipboardHelper
|
||||
);
|
||||
let row = signonsTree.currentIndex;
|
||||
let url = signonsTreeView.getCellText(row, { id: "siteCol" });
|
||||
clipboard.copyString(url);
|
||||
}
|
||||
|
||||
async function CopyPassword() {
|
||||
// Don't copy passwords if we aren't already showing the passwords & a master
|
||||
// password hasn't been entered.
|
||||
if (!showingPasswords && !(await masterPasswordLogin())) {
|
||||
return;
|
||||
}
|
||||
// Copy selected signon's password to clipboard
|
||||
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
||||
Ci.nsIClipboardHelper
|
||||
);
|
||||
let row = signonsTree.currentIndex;
|
||||
let password = signonsTreeView.getCellText(row, { id: "passwordCol" });
|
||||
clipboard.copyString(password);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_PASSWORD").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_COPIED_PASSWORD"
|
||||
);
|
||||
}
|
||||
|
||||
function CopyUsername() {
|
||||
// Copy selected signon's username to clipboard
|
||||
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
||||
Ci.nsIClipboardHelper
|
||||
);
|
||||
let row = signonsTree.currentIndex;
|
||||
let username = signonsTreeView.getCellText(row, { id: "userCol" });
|
||||
clipboard.copyString(username);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_USERNAME").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_COPIED_USERNAME"
|
||||
);
|
||||
}
|
||||
|
||||
function EditCellInSelectedRow(columnName) {
|
||||
let row = signonsTree.currentIndex;
|
||||
let columnElement = getColumnByName(columnName);
|
||||
signonsTree.startEditing(
|
||||
row,
|
||||
signonsTree.columns.getColumnFor(columnElement)
|
||||
);
|
||||
}
|
||||
|
||||
function LaunchSiteUrl() {
|
||||
let row = signonsTree.currentIndex;
|
||||
let url = signonsTreeView.getCellText(row, { id: "siteCol" });
|
||||
window.openWebLinkIn(url, "tab");
|
||||
}
|
||||
|
||||
function UpdateContextMenu() {
|
||||
let singleSelection = signonsTreeView.selection.count == 1;
|
||||
let menuItems = new Map();
|
||||
let menupopup = document.getElementById("signonsTreeContextMenu");
|
||||
for (let menuItem of menupopup.querySelectorAll("menuitem")) {
|
||||
menuItems.set(menuItem.id, menuItem);
|
||||
}
|
||||
|
||||
if (!singleSelection) {
|
||||
for (let menuItem of menuItems.values()) {
|
||||
menuItem.setAttribute("disabled", "true");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedRow = signonsTree.currentIndex;
|
||||
|
||||
// Don't display "Launch Site URL" if we're not a browser.
|
||||
if (window.openWebLinkIn) {
|
||||
menuItems.get("context-launchsiteurl").removeAttribute("disabled");
|
||||
} else {
|
||||
menuItems.get("context-launchsiteurl").setAttribute("disabled", "true");
|
||||
menuItems.get("context-launchsiteurl").setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
// Disable "Copy Username" if the username is empty.
|
||||
if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") {
|
||||
menuItems.get("context-copyusername").removeAttribute("disabled");
|
||||
} else {
|
||||
menuItems.get("context-copyusername").setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
menuItems.get("context-copysiteurl").removeAttribute("disabled");
|
||||
menuItems.get("context-editusername").removeAttribute("disabled");
|
||||
menuItems.get("context-copypassword").removeAttribute("disabled");
|
||||
|
||||
// Disable "Edit Password" if the password column isn't showing.
|
||||
if (!document.getElementById("passwordCol").hidden) {
|
||||
menuItems.get("context-editpassword").removeAttribute("disabled");
|
||||
} else {
|
||||
menuItems.get("context-editpassword").setAttribute("disabled", "true");
|
||||
}
|
||||
}
|
||||
|
||||
async function masterPasswordLogin(noPasswordCallback) {
|
||||
// This does no harm if master password isn't set.
|
||||
let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(
|
||||
Ci.nsIPK11TokenDB
|
||||
);
|
||||
let token = tokendb.getInternalKeyToken();
|
||||
|
||||
// If there is no master password, still give the user a chance to opt-out of displaying passwords
|
||||
if (token.checkPassword("")) {
|
||||
return noPasswordCallback ? noPasswordCallback() : true;
|
||||
}
|
||||
|
||||
// So there's a master password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl).
|
||||
try {
|
||||
// Relogin and ask for the master password.
|
||||
token.login(true); // 'true' means always prompt for token password. User will be prompted until
|
||||
// clicking 'Cancel' or entering the correct password.
|
||||
} catch (e) {
|
||||
// An exception will be thrown if the user cancels the login prompt dialog.
|
||||
// User is also logged out of Software Security Device.
|
||||
}
|
||||
|
||||
return token.isLoggedIn();
|
||||
}
|
||||
|
||||
function escapeKeyHandler() {
|
||||
// If editing is currently performed, don't do anything.
|
||||
if (signonsTree.getAttribute("editing")) {
|
||||
return;
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
function OpenMigrator() {
|
||||
const { MigrationUtils } = ChromeUtils.import(
|
||||
"resource:///modules/MigrationUtils.jsm"
|
||||
);
|
||||
// We pass in the type of source we're using for use in telemetry:
|
||||
MigrationUtils.showMigrationWizard(window, [
|
||||
MigrationUtils.MIGRATION_ENTRYPOINT_PASSWORDS,
|
||||
]);
|
||||
}
|
135
chrome/utils/passwordmgr/passwordManager.xhtml
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil -*- -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://global/skin/passwordmgr.css" type="text/css"?>
|
||||
|
||||
<window id="SignonViewerDialog"
|
||||
windowtype="Toolkit:PasswordManager"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
onload="Startup();"
|
||||
onunload="Shutdown();"
|
||||
title="Saved Logins"
|
||||
style="min-width: 45em;"
|
||||
persist="width height screenX screenY">
|
||||
|
||||
<script src="chrome://browser/content/utilityOverlay.js"/>
|
||||
<script src="chrome://userchromejs/content/passwordmgr/passwordManager.js"/>
|
||||
|
||||
<keyset>
|
||||
<key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();"/>
|
||||
<key key="w" modifiers="accel" oncommand="escapeKeyHandler();"/>
|
||||
<key key="f" modifiers="accel" oncommand="FocusFilterBox();"/>
|
||||
<key key="k" modifiers="accel" oncommand="FocusFilterBox();"/>
|
||||
</keyset>
|
||||
|
||||
<popupset id="signonsTreeContextSet">
|
||||
<menupopup id="signonsTreeContextMenu"
|
||||
onpopupshowing="UpdateContextMenu()">
|
||||
<menuitem id="context-copysiteurl"
|
||||
label="Copy URL"
|
||||
accesskey="y"
|
||||
oncommand="CopySiteUrl()"/>
|
||||
<menuitem id="context-launchsiteurl"
|
||||
label="Visit URL"
|
||||
accesskey="V"
|
||||
oncommand="LaunchSiteUrl()"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="context-copyusername"
|
||||
label="Copy Username"
|
||||
accesskey="U"
|
||||
oncommand="CopyUsername()"/>
|
||||
<menuitem id="context-editusername"
|
||||
label="Edit Username"
|
||||
accesskey="d"
|
||||
oncommand="EditCellInSelectedRow('username')"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="context-copypassword"
|
||||
label="Copy Password"
|
||||
accesskey="C"
|
||||
oncommand="CopyPassword()"/>
|
||||
<menuitem id="context-editpassword"
|
||||
label="Edit Password"
|
||||
accesskey="E"
|
||||
oncommand="EditCellInSelectedRow('password')"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
<!-- saved signons -->
|
||||
<vbox id="savedsignons" class="contentPane" flex="1">
|
||||
<!-- filter -->
|
||||
<hbox align="center">
|
||||
<search-textbox id="filter" flex="1"
|
||||
aria-controls="signonsTree"
|
||||
oncommand="FilterPasswords();"
|
||||
accesskey="S"
|
||||
placeholder="Search"/>
|
||||
</hbox>
|
||||
|
||||
<label control="signonsTree" id="signonsIntro"/>
|
||||
<separator class="thin"/>
|
||||
<tree id="signonsTree" flex="1"
|
||||
width="750"
|
||||
style="height: 20em;"
|
||||
onkeypress="HandleSignonKeyPress(event)"
|
||||
onselect="SignonSelected();"
|
||||
editable="true"
|
||||
context="signonsTreeContextMenu">
|
||||
<treecols>
|
||||
<treecol id="siteCol" label="Site" style="-moz-box-flex: 40"
|
||||
data-field-name="origin" persist="width"
|
||||
ignoreincolumnpicker="true"
|
||||
sortDirection="ascending"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="userCol" label="Username" style="-moz-box-flex: 25"
|
||||
ignoreincolumnpicker="true"
|
||||
data-field-name="username" persist="width"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="passwordCol" label="Password" style="-moz-box-flex: 15"
|
||||
ignoreincolumnpicker="true"
|
||||
data-field-name="password" persist="width"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timeCreatedCol" label="First Used" style="-moz-box-flex: 10"
|
||||
data-field-name="timeCreated" persist="width hidden"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timeLastUsedCol" label="Last Used" style="-moz-box-flex: 20"
|
||||
data-field-name="timeLastUsed" persist="width hidden"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timePasswordChangedCol" label="Last Changed" style="-moz-box-flex: 10"
|
||||
data-field-name="timePasswordChanged" persist="width hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timesUsedCol" label="Times Used" flex="1"
|
||||
data-field-name="timesUsed" persist="width hidden"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
</treecols>
|
||||
<treechildren/>
|
||||
</tree>
|
||||
<separator class="thin"/>
|
||||
<hbox id="SignonViewerButtons">
|
||||
<button id="removeSignon" disabled="true"
|
||||
label="Remove"
|
||||
accesskey="R"
|
||||
oncommand="DeleteSignon();"/>
|
||||
<button id="removeAllSignons"
|
||||
oncommand="DeleteAllSignons();"/>
|
||||
<spacer flex="1"/>
|
||||
<button label="Import"
|
||||
accesskey="I"
|
||||
oncommand="OpenMigrator();"/>
|
||||
<button id="togglePasswords"
|
||||
oncommand="TogglePasswordVisible();"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<hbox align="end">
|
||||
<hbox class="actionButtons">
|
||||
<spacer flex="1"/>
|
||||
<button oncommand="window.close();"
|
||||
label="Close"
|
||||
accesskey="C"/>
|
||||
</hbox>
|
||||
</hbox>
|
||||
</window>
|
22
chrome/utils/passwordmgr/passwordmgr.css
Normal file
|
@ -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/. */
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
.contentPane {
|
||||
margin: 9px 8px 5px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
margin: 0 3px 6px;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-image(siteCol) {
|
||||
list-style-image: url(chrome://mozapps/skin/places/defaultFavicon.svg);
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-inline-end: 5px;
|
||||
}
|
BIN
chrome/utils/styloaix/16.png
Normal file
After Width: | Height: | Size: 661 B |
BIN
chrome/utils/styloaix/16w.png
Normal file
After Width: | Height: | Size: 589 B |
320
chrome/utils/styloaix/autocomplete.js
Normal file
|
@ -0,0 +1,320 @@
|
|||
/* 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 AutocompletePopup = require('devtools/client/shared/autocomplete-popup');
|
||||
|
||||
let loader;
|
||||
try {
|
||||
({ loader } = ChromeUtils.import('resource://devtools/shared/loader/Loader.jsm'));
|
||||
} catch (e) {
|
||||
// tb91
|
||||
({ loader } = ChromeUtils.import('resource://devtools/shared/Loader.jsm'));
|
||||
}
|
||||
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"KeyCodes",
|
||||
"devtools/client/shared/keycodes",
|
||||
true
|
||||
);
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"CSSCompleter",
|
||||
"devtools/client/shared/sourceeditor/css-autocompleter"
|
||||
);
|
||||
|
||||
const autocompleteMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* Prepares an editor instance for autocompletion.
|
||||
*/
|
||||
function initializeAutoCompletion(ctx, options = {}) {
|
||||
const { cm, ed, Editor } = ctx;
|
||||
if (autocompleteMap.has(ed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const win = ed.container.contentWindow.wrappedJSObject;
|
||||
const { CodeMirror } = win;
|
||||
|
||||
let completer = null;
|
||||
const autocompleteKey =
|
||||
"Ctrl-" + Editor.keyFor("autocompletion", { noaccel: true });
|
||||
if (ed.config.mode == Editor.modes.css) {
|
||||
completer = new CSSCompleter({
|
||||
walker: options.walker,
|
||||
cssProperties: options.cssProperties,
|
||||
maxEntries: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
function insertSelectedPopupItem() {
|
||||
const autocompleteState = autocompleteMap.get(ed);
|
||||
if (!popup || !popup.isOpen || !autocompleteState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!autocompleteState.suggestionInsertedOnce && popup.selectedItem) {
|
||||
autocompleteMap.get(ed).insertingSuggestion = true;
|
||||
insertPopupItem(ed, popup.selectedItem);
|
||||
}
|
||||
|
||||
popup.once("popup-closed", () => {
|
||||
// This event is used in tests.
|
||||
ed.emit("popup-hidden");
|
||||
});
|
||||
popup.hidePopup();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Give each popup a new name to avoid sharing the elements.
|
||||
|
||||
let popup = new AutocompletePopup(win.parent.document, {
|
||||
position: "bottom",
|
||||
autoSelect: true,
|
||||
onClick: insertSelectedPopupItem,
|
||||
});
|
||||
|
||||
const cycle = reverse => {
|
||||
if (popup?.isOpen) {
|
||||
// eslint-disable-next-line mozilla/no-compare-against-boolean-literals
|
||||
cycleSuggestions(ed, reverse == true);
|
||||
return null;
|
||||
}
|
||||
|
||||
return CodeMirror.Pass;
|
||||
};
|
||||
|
||||
let keyMap = {
|
||||
Tab: cycle,
|
||||
Down: cycle,
|
||||
"Shift-Tab": cycle.bind(null, true),
|
||||
Up: cycle.bind(null, true),
|
||||
Enter: () => {
|
||||
const wasHandled = insertSelectedPopupItem();
|
||||
return wasHandled ? true : CodeMirror.Pass;
|
||||
},
|
||||
};
|
||||
|
||||
const autoCompleteCallback = autoComplete.bind(null, ctx);
|
||||
const keypressCallback = onEditorKeypress.bind(null, ctx);
|
||||
keyMap[autocompleteKey] = autoCompleteCallback;
|
||||
cm.addKeyMap(keyMap);
|
||||
|
||||
cm.on("keydown", keypressCallback);
|
||||
ed.on("change", autoCompleteCallback);
|
||||
ed.on("destroy", destroy);
|
||||
|
||||
function destroy() {
|
||||
ed.off("destroy", destroy);
|
||||
cm.off("keydown", keypressCallback);
|
||||
ed.off("change", autoCompleteCallback);
|
||||
cm.removeKeyMap(keyMap);
|
||||
popup.destroy();
|
||||
keyMap = popup = completer = null;
|
||||
autocompleteMap.delete(ed);
|
||||
}
|
||||
|
||||
autocompleteMap.set(ed, {
|
||||
popup: popup,
|
||||
completer: completer,
|
||||
keyMap: keyMap,
|
||||
destroy: destroy,
|
||||
insertingSuggestion: false,
|
||||
suggestionInsertedOnce: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides suggestions to autocomplete the current token/word being typed.
|
||||
*/
|
||||
function autoComplete({ ed, cm }) {
|
||||
const autocompleteOpts = autocompleteMap.get(ed);
|
||||
const { completer, popup } = autocompleteOpts;
|
||||
if (
|
||||
!completer ||
|
||||
autocompleteOpts.insertingSuggestion ||
|
||||
autocompleteOpts.doNotAutocomplete
|
||||
) {
|
||||
autocompleteOpts.insertingSuggestion = false;
|
||||
return;
|
||||
}
|
||||
const cur = ed.getCursor();
|
||||
completer
|
||||
.complete(cm.getRange({ line: 0, ch: 0 }, cur), cur)
|
||||
.then(suggestions => {
|
||||
if (
|
||||
!suggestions ||
|
||||
!suggestions.length ||
|
||||
suggestions[0].preLabel == null
|
||||
) {
|
||||
autocompleteOpts.suggestionInsertedOnce = false;
|
||||
popup.once("popup-closed", () => {
|
||||
// This event is used in tests.
|
||||
ed.emit("after-suggest");
|
||||
});
|
||||
popup.hidePopup();
|
||||
return;
|
||||
}
|
||||
// The cursor is at the end of the currently entered part of the token,
|
||||
// like "backgr|" but we need to open the popup at the beginning of the
|
||||
// character "b". Thus we need to calculate the width of the entered part
|
||||
// of the token ("backgr" here).
|
||||
|
||||
const cursorElement = cm.display.cursorDiv.querySelector(
|
||||
".CodeMirror-cursor"
|
||||
);
|
||||
const left = suggestions[0].preLabel.length * cm.defaultCharWidth();
|
||||
popup.hidePopup();
|
||||
popup.setItems(suggestions);
|
||||
|
||||
popup.once("popup-opened", () => {
|
||||
// This event is used in tests.
|
||||
ed.emit("after-suggest");
|
||||
});
|
||||
popup.openPopup(cursorElement, -1 * left, 0);
|
||||
autocompleteOpts.suggestionInsertedOnce = false;
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a popup item into the current cursor location
|
||||
* in the editor.
|
||||
*/
|
||||
function insertPopupItem(ed, popupItem) {
|
||||
const { preLabel, text } = popupItem;
|
||||
const cur = ed.getCursor();
|
||||
const textBeforeCursor = ed.getText(cur.line).substring(0, cur.ch);
|
||||
const backwardsTextBeforeCursor = textBeforeCursor
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("");
|
||||
const backwardsPreLabel = preLabel
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("");
|
||||
|
||||
// If there is additional text in the preLabel vs the line, then
|
||||
// just insert the entire autocomplete text. An example:
|
||||
// if you type 'a' and select '#about' from the autocomplete menu,
|
||||
// then the final text needs to the end up as '#about'.
|
||||
if (backwardsPreLabel.indexOf(backwardsTextBeforeCursor) === 0) {
|
||||
ed.replaceText(text, { line: cur.line, ch: 0 }, cur);
|
||||
} else {
|
||||
ed.replaceText(text.slice(preLabel.length), cur, cur);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycles through provided suggestions by the popup in a top to bottom manner
|
||||
* when `reverse` is not true. Opposite otherwise.
|
||||
*/
|
||||
function cycleSuggestions(ed, reverse) {
|
||||
const autocompleteOpts = autocompleteMap.get(ed);
|
||||
const { popup } = autocompleteOpts;
|
||||
const cur = ed.getCursor();
|
||||
autocompleteOpts.insertingSuggestion = true;
|
||||
if (!autocompleteOpts.suggestionInsertedOnce) {
|
||||
autocompleteOpts.suggestionInsertedOnce = true;
|
||||
let firstItem;
|
||||
if (reverse) {
|
||||
firstItem = popup.getItemAtIndex(popup.itemCount - 1);
|
||||
popup.selectPreviousItem();
|
||||
} else {
|
||||
firstItem = popup.getItemAtIndex(0);
|
||||
if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) {
|
||||
firstItem = popup.getItemAtIndex(1);
|
||||
popup.selectNextItem();
|
||||
}
|
||||
}
|
||||
if (popup.itemCount == 1) {
|
||||
popup.hidePopup();
|
||||
}
|
||||
insertPopupItem(ed, firstItem);
|
||||
} else {
|
||||
const fromCur = {
|
||||
line: cur.line,
|
||||
ch: cur.ch - popup.selectedItem.text.length,
|
||||
};
|
||||
if (reverse) {
|
||||
popup.selectPreviousItem();
|
||||
} else {
|
||||
popup.selectNextItem();
|
||||
}
|
||||
ed.replaceText(popup.selectedItem.text, fromCur, cur);
|
||||
}
|
||||
// This event is used in tests.
|
||||
ed.emit("suggestion-entered");
|
||||
}
|
||||
|
||||
/**
|
||||
* onkeydown handler for the editor instance to prevent autocompleting on some
|
||||
* keypresses.
|
||||
*/
|
||||
function onEditorKeypress({ ed, Editor }, cm, event) {
|
||||
const autocompleteOpts = autocompleteMap.get(ed);
|
||||
|
||||
// Do not try to autocomplete with multiple selections.
|
||||
if (ed.hasMultipleSelections()) {
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.keyCode == KeyCodes.DOM_VK_SPACE
|
||||
) {
|
||||
// When Ctrl/Cmd + Space is pressed, two simultaneous keypresses are emitted
|
||||
// first one for just the Ctrl/Cmd and second one for combo. The first one
|
||||
// leave the autocompleteOpts.doNotAutocomplete as true, so we have to make
|
||||
// it false
|
||||
autocompleteOpts.doNotAutocomplete = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.keyCode) {
|
||||
case KeyCodes.DOM_VK_RETURN:
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
break;
|
||||
case KeyCodes.DOM_VK_ESCAPE:
|
||||
if (autocompleteOpts.popup.isOpen) {
|
||||
// Prevent the Console input to open, but still remove the autocomplete popup.
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
case KeyCodes.DOM_VK_LEFT:
|
||||
case KeyCodes.DOM_VK_RIGHT:
|
||||
case KeyCodes.DOM_VK_HOME:
|
||||
case KeyCodes.DOM_VK_END:
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
break;
|
||||
case KeyCodes.DOM_VK_BACK_SPACE:
|
||||
case KeyCodes.DOM_VK_DELETE:
|
||||
if (ed.config.mode == Editor.modes.css) {
|
||||
autocompleteOpts.completer.invalidateCache(ed.getCursor().line);
|
||||
}
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
break;
|
||||
default:
|
||||
autocompleteOpts.doNotAutocomplete = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions
|
||||
|
||||
exports.initializeAutoCompletion = initializeAutoCompletion;
|