diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index b0e2f13..3926c47 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -1,10 +1,13 @@ "use strict"; var m = require("mithril"); +var FuzzySort = require("fuzzysort"); var app = "com.dannyvankooten.browserpass"; var activeTab; var searching = false; -var logins; +var resultLogins = []; +var logins = []; +var fillOnSubmit = false; var error; var domain, urlDuringSearch; @@ -24,10 +27,10 @@ function view() { results = m("div.status-text", "Error: " + error); error = undefined; } else if (logins) { - if (logins.length === 0) { + if (logins.length === 0 && domain && domain.length > 0) { results = m( "div.status-text", - m.trust(`No passwords found for ${domain}.`) + m.trust(`No matching passwords found for ${domain}.`) ); } else if (logins.length > 0) { results = logins.map(function(login) { @@ -61,22 +64,29 @@ function view() { m( "form", { - onsubmit: submitSearchForm + onsubmit: submitSearchForm, + onkeydown: searchKeyHandler }, [ - m("input", { - type: "text", - id: "search-field", - name: "s", - placeholder: "Search password..", - autocomplete: "off", - autofocus: "on" + m("div", { + "id": "filter-search" }), - m("input", { - type: "submit", - value: "Search", - style: "display: none;" - }) + m("div", [ + m("input", { + type: "text", + id: "search-field", + name: "s", + placeholder: "Search passwords..", + autocomplete: "off", + autofocus: "on", + oninput: filterLogins + }), + m("input", { + type: "submit", + value: "Search", + style: "display: none;" + }) + ]) ] ) ]), @@ -86,15 +96,73 @@ function view() { ]); } +function filterLogins(e) { + // use fuzzy search to filter results + var filter = e.target.value.trim().split(/[\s\/]+/); + if (filter.length > 0) { + logins = resultLogins.slice(0); + filter.forEach(function(word) { + if (word.length > 0) { + var refine = []; + FuzzySort.go(word, logins, {allowTypo: false}).forEach(function(result) { + refine.push(result.target); + }); + logins = refine.slice(0); + } + }); + + // fill login forms on submit rather than initiating a search + fillOnSubmit = logins.length > 0; + } else { + // reset the result list if the filter is empty + logins = resultLogins.slice(0); + } + + // redraw the list + m.redraw(); + + // show / hide the filter hint + showFilterHint(logins.length); +} + +function searchKeyHandler(e) { + // switch to search mode if backspace is pressed and no filter text has been entered + if (e.code == "Backspace" && logins.length > 0 && e.target.value.length == 0) { + e.preventDefault(); + logins = resultLogins = []; + e.target.value = fillOnSubmit ? '' : domain; + domain = ''; + showFilterHint(false); + } +} + +function showFilterHint(show=true) { + var filterHint = document.getElementById("filter-search"); + var searchField = document.getElementById("search-field"); + if (show) { + filterHint.style.display = "block"; + searchField.setAttribute("placeholder", "Refine search..."); + } else { + filterHint.style.display = "none"; + searchField.setAttribute("placeholder", "Search passwords..."); + } +} + function submitSearchForm(e) { e.preventDefault(); - // don't search without input. - if (!this.s.value.length) { - return; - } + if (fillOnSubmit && logins.length > 0) { + // fill using the first result + getLoginData.bind(logins[0])(); + } else { + // don't search without input. + if (!this.s.value.length) { + return; + } - searchPassword(this.s.value); + // search for matching entries + searchPassword(this.s.value, "search", false); + } } function init(tab) { @@ -108,9 +176,9 @@ function init(tab) { searchPassword(activeDomain, "match_domain"); } -function searchPassword(_domain, action="search") { +function searchPassword(_domain, action="search", useFillOnSubmit=true) { searching = true; - logins = null; + logins = resultLogins = []; domain = _domain; urlDuringSearch = activeTab.url; m.redraw(); @@ -132,7 +200,13 @@ function searchPassword(_domain, action="search") { } searching = false; - logins = response; + logins = resultLogins = response ? response : []; + document.getElementById("filter-search").textContent = domain; + fillOnSubmit = useFillOnSubmit && logins.length > 0; + if (logins.length > 0) { + showFilterHint(true); + document.getElementById("search-field").value = ''; + } m.redraw(); } ); @@ -160,13 +234,14 @@ function getFaviconUrl(domain) { function getLoginData() { searching = true; - logins = null; + logins = resultLogins = []; m.redraw(); chrome.runtime.sendMessage( { action: "login", entry: this, urlDuringSearch: urlDuringSearch }, function(response) { searching = false; + fillOnSubmit = false; if (response.error) { error = response.error; @@ -222,14 +297,16 @@ function keyHandler(e) { switchFocus("div.entry:first-child > .login", "nextElementSibling"); break; case "c": - if (e.ctrlKey) { + if (e.target.id != "search-field" && e.ctrlKey) { document.activeElement["nextElementSibling"][ "nextElementSibling" ].click(); } break; case "C": - document.activeElement["nextElementSibling"].click(); + if (e.target.id != "search-field") { + document.activeElement["nextElementSibling"].click(); + } break; } } diff --git a/chrome/styles.css b/chrome/styles.css index 3433999..7c88a7a 100644 --- a/chrome/styles.css +++ b/chrome/styles.css @@ -7,12 +7,21 @@ body { font-size: 14px; } +.search > form { + border-bottom: 1px solid #bbb; + display: flex; + flex-wrap: nowrap; +} + +.search > form :last-child { + width: 100%; +} + .search input { box-sizing: border-box; width: 100%; padding: 6px; border: 0; - border-bottom: 1px solid #bbb; background: url("icon-search.svg") center right 6px no-repeat; background-size: 16px 16px; background-color: white; @@ -20,6 +29,15 @@ body { padding-right: 20px; } +#filter-search { + background: #eee; + border: 0; + box-sizing: border-box; + display: none; + padding: 6px; + padding-top: 5px; +} + .search input:focus { outline: 0; } diff --git a/package.json b/package.json index 5e37070..8573aef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "browserify": "^14.4.0", - "mithril": "^1.1.4" + "mithril": "^1.1.4", + "fuzzysort": "^1.1.0" } } diff --git a/yarn.lock b/yarn.lock index 066259f..4fd6bb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -376,6 +376,10 @@ function-bind@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" +fuzzysort@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.1.tgz#bf128f1a4cc6e6b7188665ac5676de46a3d81768" + glob@^7.1.0: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"