зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1164028 - Show login details and allow manual fill from a sliding subview in the login fill doorhanger. r=Gijs
--HG-- extra : rebase_source : a3d5245067823ada32d20583ac55e32a60eb87fe
This commit is contained in:
Родитель
fcc303b06a
Коммит
7799bd600f
|
@ -1291,3 +1291,7 @@ toolbarpaletteitem[place="palette"][hidden] {
|
|||
.popup-notification-footer[popupid="bad-content"][trackingblockdisabled] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#login-fill-doorhanger:not([inDetailView]) > #login-fill-clickcapturer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -62,12 +62,22 @@
|
|||
</popupnotificationcontent>
|
||||
</popupnotification>
|
||||
|
||||
<vbox id="login-fill-doorhanger" hidden="true">
|
||||
<description id="login-fill-testing"
|
||||
value="Thanks for testing the login fill doorhanger!"/>
|
||||
<textbox id="login-fill-filter"/>
|
||||
<richlistbox id="login-fill-list"/>
|
||||
</vbox>
|
||||
<stack id="login-fill-doorhanger" hidden="true">
|
||||
<vbox id="login-fill-mainview">
|
||||
<description id="login-fill-testing"
|
||||
value="Thanks for testing the login fill doorhanger!"/>
|
||||
<textbox id="login-fill-filter"/>
|
||||
<richlistbox id="login-fill-list"/>
|
||||
</vbox>
|
||||
<vbox id="login-fill-clickcapturer"/>
|
||||
<vbox id="login-fill-details">
|
||||
<textbox id="login-fill-username" readonly="true"/>
|
||||
<textbox id="login-fill-password" type="password" disabled="true"/>
|
||||
<hbox>
|
||||
<button id="login-fill-use" label="Use in form"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</stack>
|
||||
|
||||
#ifdef E10S_TESTING_ONLY
|
||||
<popupnotification id="enable-e10s-notification" hidden="true">
|
||||
|
|
|
@ -1,3 +1,38 @@
|
|||
#notification-popup[popupid="login-fill"] > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
/* Since we display a sliding subview that extends to the border, we cannot
|
||||
* keep the default padding of arrow panels. We use the same padding in the
|
||||
* individual content views instead. Since we removed the padding, we also
|
||||
* have to ensure the contents are clipped to the border box. */
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#login-fill-mainview,
|
||||
#login-fill-details {
|
||||
padding: var(--panel-arrowcontent-padding);
|
||||
}
|
||||
|
||||
#login-fill-doorhanger[inDetailView] > #login-fill-mainview {
|
||||
transform: translateX(-14px);
|
||||
}
|
||||
|
||||
#login-fill-mainview,
|
||||
#login-fill-details {
|
||||
transition: transform 150ms;
|
||||
}
|
||||
|
||||
#login-fill-doorhanger:not([inDetailView]) > #login-fill-details {
|
||||
transform: translateX(105%);
|
||||
}
|
||||
|
||||
#login-fill-doorhanger:not([inDetailView]) > #login-fill-details:-moz-locale-dir(rtl) {
|
||||
transform: translateX(-105%);
|
||||
}
|
||||
|
||||
#login-fill-doorhanger[inDetailView] > #login-fill-clickcapturer {
|
||||
background-color: hsla(210,4%,10%,.1);
|
||||
}
|
||||
|
||||
#login-fill-testing {
|
||||
color: #b33;
|
||||
font-weight: bold;
|
||||
|
@ -31,3 +66,14 @@
|
|||
margin: 4px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#login-fill-details {
|
||||
padding: 4px;
|
||||
background: var(--panel-arrowcontent-background);
|
||||
color: var(--panel-arrowcontent-color);
|
||||
background-clip: padding-box;
|
||||
border-left: 1px solid hsla(210,4%,10%,.3);
|
||||
box-shadow: 0 3px 5px hsla(210,4%,10%,.1),
|
||||
0 0 7px hsla(210,4%,10%,.1);
|
||||
-moz-margin-start: 38px;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,16 @@ Cu.import("resource://gre/modules/LoginManagerParent.jsm");
|
|||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
// Helper function needed because the "disabled" property may not be available
|
||||
// if the XBL binding of the UI control has not been constructed yet.
|
||||
function setDisabled(element, disabled) {
|
||||
if (disabled) {
|
||||
element.setAttribute("disabled", "true");
|
||||
} else {
|
||||
element.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
this.LoginDoorhangers = {};
|
||||
|
||||
/**
|
||||
|
@ -24,10 +34,20 @@ this.LoginDoorhangers = {};
|
|||
* Properties from this object will be applied to the new instance.
|
||||
*/
|
||||
this.LoginDoorhangers.FillDoorhanger = function (properties) {
|
||||
this.onFilterInput = this.onFilterInput.bind(this);
|
||||
this.onListDblClick = this.onListDblClick.bind(this);
|
||||
this.onListKeyPress = this.onListKeyPress.bind(this);
|
||||
|
||||
// Set up infrastructure to access our elements and listen to events.
|
||||
this.el = new Proxy({}, {
|
||||
get: (target, name) => {
|
||||
return this.chromeDocument.getElementById("login-fill-" + name);
|
||||
},
|
||||
});
|
||||
this.eventHandlers = [];
|
||||
for (let elementName of Object.keys(this.events)) {
|
||||
let handlers = this.events[elementName];
|
||||
for (let eventName of Object.keys(handlers)) {
|
||||
let handler = handlers[eventName];
|
||||
this.eventHandlers.push([elementName, eventName, handler.bind(this)]);
|
||||
}
|
||||
};
|
||||
for (let name of Object.getOwnPropertyNames(properties)) {
|
||||
this[name] = properties[name];
|
||||
}
|
||||
|
@ -52,7 +72,7 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
this._browser = browser;
|
||||
|
||||
let doorhanger = this;
|
||||
let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications;
|
||||
let PopupNotifications = this.chromeDocument.defaultView.PopupNotifications;
|
||||
let notification = PopupNotifications.show(
|
||||
browser,
|
||||
"login-fill",
|
||||
|
@ -71,8 +91,7 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
case "shown":
|
||||
// Since we specified the "dismissed" option, this event will only
|
||||
// be called after the "show" method returns, so the reference to
|
||||
// "this.notification" will be available at this point.
|
||||
doorhanger.bound = true;
|
||||
// "this.notification" will be available in "bind" at this point.
|
||||
doorhanger.promiseHidden =
|
||||
new Promise(resolve => doorhanger.onUnbind = resolve);
|
||||
doorhanger.bind();
|
||||
|
@ -109,7 +128,7 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
* This may change during the lifetime of the doorhanger, in case the web page
|
||||
* is moved to a different chrome window by the swapDocShells method.
|
||||
*/
|
||||
get chomeDocument() {
|
||||
get chromeDocument() {
|
||||
return this.browser.ownerDocument;
|
||||
},
|
||||
|
||||
|
@ -117,7 +136,7 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
* Hides this notification, if the notification panel is currently open.
|
||||
*/
|
||||
hide() {
|
||||
let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications;
|
||||
let PopupNotifications = this.chromeDocument.defaultView.PopupNotifications;
|
||||
if (PopupNotifications.isPanelOpen) {
|
||||
PopupNotifications.panel.hidePopup();
|
||||
}
|
||||
|
@ -139,36 +158,47 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
* Binds this doorhanger to its UI controls.
|
||||
*/
|
||||
bind() {
|
||||
this.element = this.chomeDocument.getElementById("login-fill-doorhanger");
|
||||
this.list = this.chomeDocument.getElementById("login-fill-list");
|
||||
this.filter = this.chomeDocument.getElementById("login-fill-filter");
|
||||
|
||||
this.filter.setAttribute("value", this.filterString);
|
||||
// Since this may ask for the master password, we must do it at bind time.
|
||||
if (this.autoDetailLogin) {
|
||||
let formLogins = Services.logins.findLogins({}, this.loginFormOrigin, "",
|
||||
null);
|
||||
if (formLogins.length == 1) {
|
||||
this.detailLogin = formLogins[0];
|
||||
}
|
||||
this.autoDetailLogin = false;
|
||||
}
|
||||
|
||||
this.el.filter.setAttribute("value", this.filterString);
|
||||
this.refreshList();
|
||||
this.refreshDetailView();
|
||||
|
||||
this.filter.addEventListener("input", this.onFilterInput);
|
||||
this.list.addEventListener("dblclick", this.onListDblClick);
|
||||
this.list.addEventListener("keypress", this.onListKeyPress);
|
||||
this.eventHandlers.forEach(([elementName, eventName, handler]) => {
|
||||
this.el[elementName].addEventListener(eventName, handler, true);
|
||||
});
|
||||
|
||||
// Move the main element to the notification panel for displaying.
|
||||
this.notification.owner.panel.firstElementChild.appendChild(this.element);
|
||||
this.element.hidden = false;
|
||||
this.notification.owner.panel.firstElementChild.appendChild(this.el.doorhanger);
|
||||
this.el.doorhanger.hidden = false;
|
||||
|
||||
this.bound = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unbinds this doorhanger from its UI controls.
|
||||
*/
|
||||
unbind() {
|
||||
this.filter.removeEventListener("input", this.onFilterInput);
|
||||
this.list.removeEventListener("dblclick", this.onListDblClick);
|
||||
this.list.removeEventListener("keypress", this.onListKeyPress);
|
||||
this.bound = false;
|
||||
|
||||
this.eventHandlers.forEach(([elementName, eventName, handler]) => {
|
||||
this.el[elementName].removeEventListener(eventName, handler, true);
|
||||
});
|
||||
|
||||
this.clearList();
|
||||
|
||||
// Place the element back in the document for the next time we need it.
|
||||
this.element.hidden = true;
|
||||
this.chomeDocument.getElementById("mainPopupSet").appendChild(this.element);
|
||||
this.el.doorhanger.hidden = true;
|
||||
this.chromeDocument.getElementById("mainPopupSet")
|
||||
.appendChild(this.el.doorhanger);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -189,11 +219,78 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
filterString: "",
|
||||
|
||||
/**
|
||||
* Handles text changes in the filter textbox.
|
||||
* Show login details automatically when the panel is first opened.
|
||||
*/
|
||||
onFilterInput() {
|
||||
this.filterString = this.filter.value;
|
||||
this.refreshList();
|
||||
autoDetailLogin: false,
|
||||
|
||||
/**
|
||||
* Indicates which particular login to show in the detail view.
|
||||
*/
|
||||
set detailLogin(detailLogin) {
|
||||
this._detailLogin = detailLogin;
|
||||
if (this.bound) {
|
||||
this.refreshDetailView();
|
||||
}
|
||||
},
|
||||
get detailLogin() {
|
||||
return this._detailLogin;
|
||||
},
|
||||
_detailLogin: null,
|
||||
|
||||
/**
|
||||
* Prototype functions for event handling.
|
||||
*/
|
||||
events: {
|
||||
mainview: {
|
||||
focus(event) {
|
||||
// If keyboard focus returns to any control in the the main view (for
|
||||
// example using SHIFT+TAB) close the details view.
|
||||
this.detailLogin = null;
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
input(event) {
|
||||
this.filterString = this.el.filter.value;
|
||||
this.refreshList();
|
||||
},
|
||||
},
|
||||
list: {
|
||||
click(event) {
|
||||
if (event.button == 0 && this.el.list.selectedItem) {
|
||||
this.displaySelectedLoginDetails();
|
||||
}
|
||||
},
|
||||
keypress(event) {
|
||||
if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
|
||||
this.el.list.selectedItem) {
|
||||
this.displaySelectedLoginDetails();
|
||||
}
|
||||
},
|
||||
},
|
||||
clickcapturer: {
|
||||
click(event) {
|
||||
this.detailLogin = null;
|
||||
},
|
||||
},
|
||||
details: {
|
||||
transitionend(event) {
|
||||
// We must set focus to the detail controls only when the transition has
|
||||
// ended, otherwise focus will interfere with the animation. We do this
|
||||
// only when we're showing the detail view, not when leaving.
|
||||
if (event.target == this.el.details && this.detailLogin) {
|
||||
if (this.loginFormPresent) {
|
||||
this.el.use.focus();
|
||||
} else {
|
||||
this.el.username.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
use: {
|
||||
command(event) {
|
||||
this.fillLogin();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -212,17 +309,14 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
}
|
||||
|
||||
for (let { hostname, username } of formLogins) {
|
||||
let item = this.chomeDocument.createElementNS(XUL_NS, "richlistitem");
|
||||
let item = this.chromeDocument.createElementNS(XUL_NS, "richlistitem");
|
||||
item.classList.add("login-fill-item");
|
||||
item.setAttribute("hostname", hostname);
|
||||
item.setAttribute("username", username);
|
||||
if (hostname != this.loginFormOrigin) {
|
||||
item.classList.add("different-hostname");
|
||||
}
|
||||
if (!this.loginFormPresent) {
|
||||
item.setAttribute("disabled", "true");
|
||||
}
|
||||
this.list.appendChild(item);
|
||||
this.el.list.appendChild(item);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -230,45 +324,51 @@ this.LoginDoorhangers.FillDoorhanger.prototype = {
|
|||
* Clears the list of logins.
|
||||
*/
|
||||
clearList() {
|
||||
while (this.list.firstChild) {
|
||||
this.list.removeChild(this.list.firstChild);
|
||||
let list = this.el.list;
|
||||
while (list.firstChild) {
|
||||
list.firstChild.remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the action associated to a login item.
|
||||
* Updates all the controls of the detail view based on the chosen login.
|
||||
*/
|
||||
onListDblClick(event) {
|
||||
if (event.button != 0 || !this.list.selectedItem) {
|
||||
return;
|
||||
}
|
||||
this.fillLogin();
|
||||
},
|
||||
onListKeyPress(event) {
|
||||
if (event.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
|
||||
!this.list.selectedItem) {
|
||||
return;
|
||||
}
|
||||
this.fillLogin();
|
||||
},
|
||||
fillLogin() {
|
||||
if (this.list.selectedItem.hasAttribute("disabled")) {
|
||||
return;
|
||||
}
|
||||
let formLogins = Services.logins.findLogins({}, "", "", null);
|
||||
let login = formLogins.find(login => {
|
||||
return login.hostname == this.list.selectedItem.getAttribute("hostname") &&
|
||||
login.username == this.list.selectedItem.getAttribute("username");
|
||||
});
|
||||
if (login) {
|
||||
LoginManagerParent.fillForm({
|
||||
browser: this.browser,
|
||||
loginFormOrigin: this.loginFormOrigin,
|
||||
login,
|
||||
}).catch(Cu.reportError);
|
||||
refreshDetailView() {
|
||||
if (this.detailLogin) {
|
||||
this.el.username.setAttribute("value", this.detailLogin.username);
|
||||
this.el.password.setAttribute("value", this.detailLogin.password);
|
||||
this.el.doorhanger.setAttribute("inDetailView", "true");
|
||||
setDisabled(this.el.username, false);
|
||||
setDisabled(this.el.use, !this.loginFormPresent);
|
||||
} else {
|
||||
Cu.reportError("The selected login has been removed in the meantime.");
|
||||
this.el.doorhanger.removeAttribute("inDetailView");
|
||||
// We must disable all the detail controls to ensure they cannot be
|
||||
// selected with the keyboard while they are outside the visible area.
|
||||
setDisabled(this.el.username, true);
|
||||
setDisabled(this.el.use, true);
|
||||
}
|
||||
},
|
||||
|
||||
displaySelectedLoginDetails() {
|
||||
let selectedItem = this.el.list.selectedItem;
|
||||
let hostLogins = Services.logins.findLogins({},
|
||||
selectedItem.getAttribute("hostname"), "", null);
|
||||
let login = hostLogins.find(login => {
|
||||
return login.username == selectedItem.getAttribute("username");
|
||||
});
|
||||
if (!login) {
|
||||
Cu.reportError("The selected login has been removed in the meantime.");
|
||||
return;
|
||||
}
|
||||
this.detailLogin = login;
|
||||
},
|
||||
|
||||
fillLogin() {
|
||||
LoginManagerParent.fillForm({
|
||||
browser: this.browser,
|
||||
loginFormOrigin: this.loginFormOrigin,
|
||||
login: this.detailLogin,
|
||||
}).catch(Cu.reportError);
|
||||
this.hide();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -607,7 +607,9 @@ var LoginManagerParent = {
|
|||
yield fillDoorhanger.promiseHidden;
|
||||
fillDoorhanger.loginFormPresent = loginFormPresent;
|
||||
fillDoorhanger.loginFormOrigin = loginFormOrigin;
|
||||
fillDoorhanger.filterString = loginFormOrigin;
|
||||
fillDoorhanger.filterString = hasLogins ? loginFormOrigin : "";
|
||||
fillDoorhanger.detailLogin = null;
|
||||
fillDoorhanger.autoDetailLogin = true;
|
||||
return;
|
||||
}
|
||||
if (showLoginAnchor) {
|
||||
|
@ -615,7 +617,8 @@ var LoginManagerParent = {
|
|||
browser,
|
||||
loginFormPresent,
|
||||
loginFormOrigin,
|
||||
filterString: loginFormOrigin,
|
||||
filterString: hasLogins ? loginFormOrigin : "",
|
||||
autoDetailLogin: true,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -51,10 +51,20 @@ add_task(function* test_fill() {
|
|||
Assert.equal(list.childNodes.length, 1,
|
||||
"list.childNodes.length === 1");
|
||||
|
||||
// The button will be focused after the "transitionend" event.
|
||||
list.focus();
|
||||
yield new Promise(resolve => executeSoon(resolve));
|
||||
let details = document.getElementById("login-fill-details");
|
||||
let promiseSubview = BrowserTestUtils.waitForEvent(details,
|
||||
"transitionend", true,
|
||||
e => e.target == details);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, list.childNodes[0]);
|
||||
yield promiseSubview;
|
||||
|
||||
// Clicking the button will dismiss the panel.
|
||||
let promiseHidden = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
|
||||
"popuphidden");
|
||||
list.focus();
|
||||
EventUtils.sendMouseEvent({ type: "dblclick" }, list.childNodes[0]);
|
||||
document.getElementById("login-fill-use").click();
|
||||
yield promiseHidden;
|
||||
|
||||
let result = yield ContentTask.spawn(browser, null, function* () {
|
||||
|
|
|
@ -4,6 +4,14 @@
|
|||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
/* ::::: Variables ::::: */
|
||||
.panel-arrowcontent {
|
||||
--panel-arrowcontent-padding: 10px;
|
||||
--panel-arrowcontent-background: -moz-Dialog;
|
||||
--panel-arrowcontent-color: -moz-DialogText;
|
||||
--panel-arrowcontent-border: 1px solid ThreeDShadow;
|
||||
}
|
||||
|
||||
/* ::::: menupopup ::::: */
|
||||
|
||||
menupopup,
|
||||
|
@ -32,10 +40,10 @@ panel[type="arrow"][side="right"] {
|
|||
}
|
||||
|
||||
.panel-arrowcontent {
|
||||
padding: 10px;
|
||||
color: -moz-DialogText;
|
||||
background: -moz-Dialog;
|
||||
border: 1px solid ThreeDShadow;
|
||||
padding: var(--panel-arrowcontent-padding);
|
||||
color: var(--panel-arrowcontent-color);
|
||||
background: var(--panel-arrowcontent-background);
|
||||
border: var(--panel-arrowcontent-border);
|
||||
}
|
||||
|
||||
.panel-arrow[side="top"],
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
.panel-arrowcontent {
|
||||
--panel-arrowcontent-padding: 16px;
|
||||
--panel-arrowcontent-background: linear-gradient(hsla(0,0%,99%,1), hsla(0,0%,99%,.975) 10%, hsla(0,0%,98%,.975));
|
||||
--panel-arrowcontent-color: hsl(0,0%,10%);
|
||||
--panel-arrowcontent-border: none;
|
||||
|
@ -52,7 +53,7 @@ panel[type="arrow"][side="right"] {
|
|||
box-shadow: 0 0 0 1px hsla(210,4%,10%,.05);
|
||||
color: var(--panel-arrowcontent-color);
|
||||
border: var(--panel-arrowcontent-border);
|
||||
padding: 16px;
|
||||
padding: var(--panel-arrowcontent-padding);
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
/* ::::: Variables ::::: */
|
||||
.panel-arrowcontent {
|
||||
--panel-arrowcontent-padding: 10px;
|
||||
--panel-arrowcontent-background: -moz-field;
|
||||
--panel-arrowcontent-color: -moz-FieldText;
|
||||
--panel-arrowcontent-border: 1px solid ThreeDShadow;
|
||||
|
@ -55,7 +56,7 @@ panel[type="arrow"][side="right"] {
|
|||
}
|
||||
|
||||
.panel-arrowcontent {
|
||||
padding: 10px;
|
||||
padding: var(--panel-arrowcontent-padding);
|
||||
color: var(--panel-arrowcontent-color);
|
||||
background: var(--panel-arrowcontent-background);
|
||||
background-clip: padding-box;
|
||||
|
|
Загрузка…
Ссылка в новой задаче