Files
browserpass-extension/src/popup/interface.js
Erayd 92f2ecea1b Implement search (#3)
* Handle settings response properly
* Change location of default store settings
* Generate chrome extension
* Move popup into its own folder
* Move distribution javascript into new folder
* Remove checkpoint
* Ignore dist JS files
* Rename background source file
* Update clean rule
* Fix make rule for generated files
* Also tidy CSS files
* Implement searching
* Add icon to search bar
* Add copy-password action
* Add copy-user action
* Add launch action
* Button styling
* Send action to backend
* Set targets to .PHONY
* Highlight the first entry
* Allow disabling fuzzy search by starting search with a space
2018-04-19 23:57:51 +12:00

167 lines
5.1 KiB
JavaScript

module.exports = Interface;
var m = require("mithril");
var FuzzySort = require("fuzzysort");
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.active = true;
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) {
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;
}
}
},
[
badges ? m("div.store.badge", result.store) : null,
m("div.name", m.trust(result.display)),
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("copyUser");
}
}),
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
* @param bool fuzzyFirstWord Whether to use fuzzy search on the first word
* @return void
*/
function search(s, fuzzyFirstWord = true) {
var self = this;
// get candidate list
var candidates = this.logins.map(result => Object.assign(result, { display: result.login }));
if (this.active) {
candidates = candidates.filter(login => login.active);
}
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"],
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;
}