Files
browserpass-extension/src/popup/interface.js
Erayd af96b0309c Track recently used logins & display first in popup (#9)
* Track recently used logins & display first in popup

Results are sorted by:
 - Most recent store first
 - Within each store, login with highest use count first
 - Within same usage count, most recent first

There is a 60-sec debounce on logins, to avoid excessive incrementing of
the counter when a login is used multiple times in rapid succession
(e.g. for copying user / pass etc.).
2018-04-22 00:26:05 +12:00

205 lines
6.9 KiB
JavaScript

module.exports = Interface;
var m = require("mithril");
var FuzzySort = require("fuzzysort");
var Moment = require("moment");
var SearchInterface = require("./searchinterface");
/**
* Popup main interface
*
* @since 3.0.0
*
* @param object settings Settings object
* @param array logins Array of available logins
* @return void
*/
function Interface(settings, logins) {
// public methods
this.attach = attach;
this.view = view;
this.search = search;
// fields
this.settings = settings;
this.logins = logins;
this.results = [];
this.currentDomainOnly = !settings.tab.url.match(/^chrome:\/\//);
this.searchPart = new SearchInterface(this);
// initialise with empty search
this.search("");
}
/**
* Attach the interface on the given element
*
* @since 3.0.0
*
* @param DOMElement element Target element
* @return void
*/
function attach(element) {
m.mount(element, this);
}
/**
* Generates vnodes for render
*
* @since 3.0.0
*
* @param function ctl Controller
* @param object params Runtime params
* @return []Vnode
*/
function view(ctl, params) {
var badges = Object.keys(this.settings.stores).length > 1;
var nodes = [];
nodes.push(m(this.searchPart));
nodes.push(
m(
"div.logins",
this.results.map(function(result) {
return m(
"div.part.login",
{
key: result.index,
tabindex: 0,
onclick: function(e) {
result.doAction("fill");
},
onkeydown: function(e) {
e.preventDefault();
switch (e.code) {
case "ArrowDown":
if (e.target.nextSibling) {
e.target.nextSibling.focus();
}
break;
case "ArrowUp":
if (e.target.previousSibling) {
e.target.previousSibling.focus();
} else {
document
.querySelector(".part.search input[type=text]")
.focus();
}
break;
case "Enter":
result.doAction("fill");
break;
case "KeyC":
if (e.ctrlKey) {
result.doAction(
e.shiftKey ? "copyUsername" : "copyPassword"
);
}
break;
case "KeyG":
result.doAction("launch");
break;
}
}
},
[
badges ? m("div.store.badge", result.store.name) : null,
m("div.name", [
m.trust(result.display),
result.recent.when > 0
? m("div.recent", {
title:
"Used here " +
result.recent.count +
" time" +
(result.recent.count > 1 ? "s" : "") +
", last " +
Moment(new Date(result.recent.when)).fromNow()
})
: null
]),
m("div.action.copy-password", {
title: "Copy password",
onclick: function(e) {
e.stopPropagation();
result.doAction("copyPassword");
}
}),
m("div.action.copy-user", {
title: "Copy username",
onclick: function(e) {
e.stopPropagation();
result.doAction("copyUsername");
}
}),
m("div.action.launch", {
title: "Open URL",
onclick: function(e) {
e.stopPropagation();
result.doAction("launch");
}
})
]
);
})
)
);
return nodes;
}
/**
* Run a search
*
* @param string s Search string
* @return void
*/
function search(s) {
var self = this;
var fuzzyFirstWord = s.substr(0, 1) !== " ";
s = s.trim();
// get candidate list
var candidates = this.logins.map(result => Object.assign(result, { display: result.login }));
if (this.currentDomainOnly) {
var recent = candidates.filter(login => login.recent.count > 0);
recent.sort(function(a, b) {
if (a.store.when != b.store.when) {
return b.store.when - a.store.when;
}
if (a.recent.count != b.recent.count) {
return b.recent.count - a.recent.count;
}
return b.recent.when - a.recent.when;
});
candidates = recent.concat(
candidates.filter(login => login.inCurrentDomain && !login.recent.count)
);
}
if (s.length) {
var filter = s.split(/\s+/);
// fuzzy-search first word & add highlighting
if (fuzzyFirstWord) {
candidates = FuzzySort.go(filter[0], candidates, {
keys: ["login", "store.name"],
allowTypo: false
}).map(result =>
Object.assign(result.obj, {
display: result[0]
? FuzzySort.highlight(result[0], "<em>", "</em>")
: result.obj.login
})
);
}
// substring-search to refine against each remaining word
filter.slice(fuzzyFirstWord ? 1 : 0).forEach(function(word) {
candidates = candidates.filter(
login => login.login.toLowerCase().indexOf(word.toLowerCase()) >= 0
);
});
}
this.results = candidates;
}