diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index e302037b..b7c45fd6 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1434,7 +1434,7 @@
"description": "Text for label that shows the number of times a search result was installed during last week"
},
"searchStyleQueryHint": {
- "message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020",
+ "message": "Search style names (case-sensitively if an uppercase letter is used):\nsome words - all these words in any order\n\"some phrase\" - this exact phrase without quotes\n/foo.*bar/i - regular expression without spaces (use \\s instead)",
"description": "Tooltip shown for the text input in the popup's inline style finder"
},
"searchStylesAll": {
diff --git a/js/dlg/config-dialog.js b/js/dlg/config-dialog.js
index 9e539301..945ba046 100644
--- a/js/dlg/config-dialog.js
+++ b/js/dlg/config-dialog.js
@@ -255,18 +255,10 @@ async function configDialog(style) {
case 'dropdown':
case 'image':
// TODO: a image picker input?
- children = [
- $create('.select-resizer.config-value', [
- va.input = $create('select', {
- va,
- onchange: updateVarOnChange,
- },
- va.options.map(o =>
- $create('option', {value: o.name}, o.label))),
- $create('SVG:svg.svg-icon.select-arrow',
- $create('SVG:use', {'xlink:href': '#svg-icon-select-arrow'})),
- ]),
- ];
+ va.input = $create('select', {va, onchange: updateVarOnChange},
+ va.options.map(o => $create('option', {value: o.name}, o.label)));
+ children = [$.fancySelect(va.input)];
+ children[0].classList.add('config-value');
break;
case 'range':
diff --git a/js/dom.js b/js/dom.js
index 77b19290..f83da6a6 100644
--- a/js/dom.js
+++ b/js/dom.js
@@ -27,6 +27,17 @@ Object.assign(EventTarget.prototype, {
$.root = document.documentElement;
$.rootCL = $.root.classList;
+$.dummies = {
+ select: $create('.select-resizer',
+ $create('SVG:svg.svg-icon.select-arrow',
+ $create('SVG:use', {'xlink:href': '#svg-icon-select-arrow'}))),
+};
+$.fancySelect = el => {
+ const res = $.dummies.select.cloneNode(true);
+ if (el.parentNode) el.replaceWith(res);
+ res.prepend(el);
+ return res;
+};
// Makes the focus outline appear on keyboard tabbing, but not on mouse clicks.
const focusAccessibility = {
diff --git a/js/localization.js b/js/localization.js
index af020069..b99b972f 100644
--- a/js/localization.js
+++ b/js/localization.js
@@ -29,7 +29,7 @@ Object.assign(t, {
')',
/(?!\b|\s|$)/,
].map(rx => rx.source || rx).join(''), 'gu'),
- SELECTOR: '[i18n], template',
+ SELECTOR: '[i18n], template, select',
HTML(html) {
return typeof html !== 'string'
@@ -49,6 +49,9 @@ Object.assign(t, {
t.createTemplate(node);
continue;
}
+ if (node.localName === 'select' && (node.nextElementSibling || {}).localName !== 'svg') {
+ $.fancySelect(node);
+ }
const attr = node.getAttribute('i18n');
if (!attr) continue;
for (const part of attr.split(',')) {
diff --git a/popup/popup.css b/popup/popup.css
index 46b92d6c..82600261 100644
--- a/popup/popup.css
+++ b/popup/popup.css
@@ -624,11 +624,11 @@ a:hover .svg-icon {
fill: transparent;
stroke: currentColor;
}
-html:not(.styles-last) #popup-options .split-btn-menu {
+html:not(.styles-last):not(.search-results-shown) #popup-options .split-btn-menu {
bottom: 0;
transform: translateY(-20px); /* global button style: 13(font) * 1.2(line) + 4(pad) + 2(border) */
}
-html:not(.styles-last) .split-btn-pedal::after {
+html:not(.styles-last):not(.search-results-shown) .split-btn-pedal::after {
border-top: var(--side) solid transparent;
border-bottom: calc(var(--side) * 1.3) solid currentColor;
vertical-align: top;
diff --git a/popup/search.css b/popup/search.css
index a35f6471..53fd7115 100644
--- a/popup/search.css
+++ b/popup/search.css
@@ -2,7 +2,7 @@
This file is loaded dynamically when the inline search is invoked.
So don't put main popup's stuff here. */
-body.search-results-shown {
+.search-results-shown body {
overflow-y: auto;
overflow-x: hidden;
}
@@ -288,35 +288,39 @@ search-result-meta [data-type="rating"][data-class="none"] dd {
flex-direction: row;
text-align: center;
word-break: keep-all;
+ line-height: 24px;
+ font-size: 16px;
}
-
.search-results-nav[data-type="top"] {
padding-top: 1em;
margin-bottom: 1em;
}
-
.search-results-nav[data-type="bottom"] {
margin-top: -1em;
margin-bottom: 1em;
}
-
+.search-results-nav label {
+ vertical-align: middle;
+ -moz-user-select: none;
+ user-select: none;
+}
+.search-results-nav [data-type="page"] {
+ font-weight: bold;
+}
#search-results .search-results-nav button {
background: none;
border: none;
- padding: 0 1rem;
+ padding: 0 .5rem;
margin: 0 .5rem;
- font-size: 150%;
- line-height: 24px;
+ font-size: 18px;
vertical-align: middle;
cursor: pointer;
}
-
#search-results .search-results-nav button:disabled {
cursor: auto;
opacity: .25;
pointer-events: none;
}
-
#search-results .search-results-nav button:not(:disabled):hover {
text-shadow: 0 1px 4px rgba(0, 0, 0, .5);
}
@@ -338,3 +342,8 @@ search-result-meta [data-type="rating"][data-class="none"] dd {
margin-right: .5em;
flex: 1 1 0;
}
+
+#search-years {
+ text-align: center;
+ width: 100%;
+}
diff --git a/popup/search.html b/popup/search.html
index 3e451830..74cc597c 100644
--- a/popup/search.html
+++ b/popup/search.html
@@ -3,17 +3,15 @@
diff --git a/popup/search.js b/popup/search.js
index 4c57e627..d4243f92 100644
--- a/popup/search.js
+++ b/popup/search.js
@@ -2,7 +2,7 @@
/* global $entry tabURL */// popup.js
/* global API */// msg.js
/* global Events */
-/* global FIREFOX URLS debounce download tryURL */// toolbox.js
+/* global FIREFOX URLS debounce download stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global prefs */
/* global t */// localization.js
'use strict';
@@ -17,7 +17,7 @@
title: URLS.usw,
});
const STYLUS_CATEGORY = 'chrome-extension';
- const PAGE_LENGTH = 10;
+ const PAGE_LENGTH = 100;
// update USO style install counter if the style isn't uninstalled immediately
const PINGBACK_DELAY = 5e3;
const BUSY_DELAY = .5e3;
@@ -38,17 +38,20 @@
* @prop {string} an - authorName
* @prop {string} sn - screenshotName
* @prop {boolean} sa - screenshotArchived
- * --------------------- Stylus' internally added extras
- * @prop {boolean} installed
- * @prop {number} installedStyleId
+ *
+ * @prop {boolean} _installed
+ * @prop {number} _installedStyleId
+ * @prop {number} _year
*/
/** @type IndexEntry[] */
- let results;
+ let results, resultsAllYears;
/** @type IndexEntry[] */
let index;
let category = '';
+ /** @type RegExp */
+ let rxCategory;
let searchGlobals = $('#search-globals').checked;
- /** @type string[] */
+ /** @type {RegExp[]} */
let query = [];
let order = prefs.get('popup.findSort');
let scrollToFirstResult = true;
@@ -97,17 +100,24 @@
};
$('#search-query').oninput = function () {
query = [];
- const text = this.value.trim().toLocaleLowerCase();
- const thisYear = new Date().getFullYear();
- for (let re = /"(.+?)"|(\S+)/g, m; (m = re.exec(text));) {
- const n = Number(m[2]);
- query.push(n >= 2000 && n <= thisYear ? n : m[1] || m[2]);
+ const text = this.value.trim();
+ for (let re = /(")(.+?)"|((\/)?(\S+?)(\/\w*)?)(?=\s|$)/g, m; (m = re.exec(text));) {
+ const [
+ all,
+ q, qt,
+ t, rx1 = '', rx, rx2 = '',
+ ] = m;
+ query.push(rx1 && rx2 && tryRegExp(rx, rx2.slice(1)) ||
+ stringAsRegExp(q ? qt : t, all === all.toLocaleLowerCase() ? 'i' : ''));
}
- if (category === STYLUS_CATEGORY && !query.includes('stylus')) {
- query.push('stylus');
+ if (category === STYLUS_CATEGORY) {
+ query.push(/\bStylus\b/);
}
ready = ready.then(start);
};
+ $('#search-years').onchange = () => {
+ ready = ready.then(() => start({keepYears: true}));
+ };
$('#search-order').value = order;
$('#search-order').onchange = function () {
order = this.value;
@@ -146,7 +156,7 @@
window.on('styleDeleted', ({detail: {style: {id}}}) => {
restoreScrollPosition();
- const result = results.find(r => r.installedStyleId === id);
+ const result = results.find(r => r._installedStyleId === id);
if (result) {
API.uso.pingback(result.i, false);
renderActionButtons(result.i, -1);
@@ -178,11 +188,11 @@
dom.error.hidden = false;
dom.list.hidden = true;
if (dom.error.getBoundingClientRect().bottom < 0) {
- dom.error.scrollIntoView({behavior: 'smooth', block: 'start'});
+ dom.error.scrollIntoView(true);
}
}
- async function start() {
+ async function start({keepYears} = {}) {
resetUI.timer = resetUI.timer || setTimeout(resetUI, 500);
try {
results = [];
@@ -194,6 +204,8 @@
const allSupportedIds = new Set(installedStyles.map(calcId));
results = results.filter(r => !allSupportedIds.has(r.i));
}
+ if (!keepYears) resultsAllYears = results;
+ renderYears();
render();
dom.list.hidden = !results.length;
if (!results.length && !$('#search-query').value) {
@@ -201,6 +213,7 @@
} else {
resetUI();
}
+ resetUI();
} catch (reason) {
error(reason);
}
@@ -209,12 +222,44 @@
}
function resetUI() {
- document.body.classList.add('search-results-shown');
+ $.rootCL.add('search-results-shown');
dom.container.hidden = false;
dom.list.hidden = false;
dom.error.hidden = true;
}
+ function renderYears() {
+ const SCALE = 1000;
+ const BASE = new Date(0).getFullYear(); // 1970
+ const DAYS = 365.2425;
+ const DAY = 24 * 3600e3;
+ const YEAR = DAYS * DAY / SCALE;
+ const SAFETY = 1 / DAYS; // 1 day safety margin: recheck Jan 1 and Dec 31
+ const years = [];
+ for (const r of resultsAllYears) {
+ let y = r._year;
+ if (!y) {
+ y = r.u / YEAR + BASE;
+ r._year = y = Math.abs(y % 1 - 1) <= SAFETY
+ ? new Date(r.u * SCALE).getFullYear()
+ : y | 0;
+ }
+ years[y] = (years[y] || 0) + 1;
+ }
+ const texts = years.reduceRight((res, num, y) => res.push(`${y} (${num})`) && res, []);
+ const selects = $$('#search-years select');
+ selects.forEach((sel, selNum) => {
+ if (texts.length !== sel.length || texts.some((v, i) => v !== sel[i].text)) {
+ const {value} = sel;
+ sel.textContent = '';
+ sel.append(...texts.map(t => $create('option', {value: t.split(' ')[0]}, t)));
+ sel.value = value in years ? value : (sel[`${selNum ? 'first' : 'last'}Child`] || {}).value;
+ }
+ });
+ const [y1, y2] = selects.map(el => Number(el.value)).sort();
+ results = y1 ? resultsAllYears.filter(r => (r = r._year) >= y1 && r <= y2) : resultsAllYears;
+ }
+
function render() {
totalPages = Math.ceil(results.length / PAGE_LENGTH);
displayedPage = Math.min(displayedPage, totalPages) || 1;
@@ -262,13 +307,14 @@
nav._next.disabled = displayedPage >= totalPages;
nav._page.textContent = displayedPage;
nav._total.textContent = totalPages;
+ nav._num.textContent = results.length;
}
}
function doScrollToFirstResult() {
if (dom.container.scrollHeight > window.innerHeight * 2) {
scrollToFirstResult = false;
- dom.container.scrollIntoView({behavior: 'smooth', block: 'start'});
+ dom.container.scrollIntoView(true);
}
}
@@ -377,10 +423,10 @@
if (!entry) return;
const result = entry._result;
if (typeof installedId === 'number') {
- result.installed = installedId > 0;
- result.installedStyleId = installedId;
+ result._installed = installedId > 0;
+ result._installedStyleId = installedId;
}
- const isInstalled = result.installed;
+ const isInstalled = result._installed;
const status = $('.search-result-status', entry).textContent =
isInstalled ? t('clickToUninstall') :
entry.dataset.noImage != null ? t('installButton') :
@@ -422,7 +468,7 @@
}
function configure() {
- const styleEntry = $entry($resultEntry(this).result.installedStyleId);
+ const styleEntry = $entry($resultEntry(this).result._installedStyleId);
Events.configure.call(this, {target: styleEntry});
}
@@ -456,7 +502,7 @@
function uninstall() {
const {entry, result} = $resultEntry(this);
saveScrollPosition(entry);
- API.styles.delete(result.installedStyleId);
+ API.styles.delete(result._installedStyleId);
}
function saveScrollPosition(entry) {
@@ -495,6 +541,7 @@
);
category = (keepThird && `${third}.` || '') + main + (keepTld || keepThird ? `.${tld}` : '');
}
+ rxCategory = new RegExp(`\\b${stringAsRegExp(category, '', true)}\\b`, 'i');
return category !== old;
}
@@ -519,31 +566,29 @@
}
async function search({retry} = {}) {
- return retry && !calcCategory({retry})
+ return retry && !query.length && !calcCategory({retry})
? []
: (index || await fetchIndex()).filter(isResultMatching).sort(comparator);
}
function isResultMatching(res) {
- // We're trying to call calcHaystack only when needed, not on all 100K items
const {c} = res;
return (
c === category ||
(category === STYLUS_CATEGORY
? c === 'stylus' // USW
: c === 'global' && searchGlobals &&
- (query.length || calcHaystack(res)._nLC.includes(category))
+ (query.length || rxCategory.test(res.n))
)
- ) && (
- !query.length || // to skip calling calcHaystack
- query.every(isInHaystack, calcHaystack(res))
- );
+ ) && query.every(isInHaystack, res);
}
- /** @this {IndexEntry} haystack */
- function isInHaystack(needle) {
- return this._year === needle && this.c !== 'global' ||
- this._nLC.includes(needle);
+ /**
+ * @this {IndexEntry} haystack
+ * @param {RegExp} q
+ */
+ function isInHaystack(q) {
+ return q.test(this.n);
}
/**
@@ -570,10 +615,4 @@
function calcId(style) {
return calcUsoId(style) || calcUswId(style);
}
-
- function calcHaystack(res) {
- if (!res._nLC) res._nLC = res.n.toLocaleLowerCase();
- if (!res._year) res._year = new Date(res.u * 1000).getFullYear();
- return res;
- }
})();