From 8b98baba6a64fc1feb8636bc1e857479c33502d9 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 17 Nov 2020 21:55:38 +0300 Subject: [PATCH] add scope selector to style search in manager --- _locales/en/messages.json | 24 +++++- background/background.js | 34 ++++----- background/search-db.js | 145 +++++++++++++++++++----------------- background/style-manager.js | 6 +- global.css | 1 + js/usercss.js | 7 +- manage.html | 12 ++- manage/filters.js | 55 +++++++------- manage/manage.css | 14 +--- manage/manage.js | 4 +- popup/popup.js | 14 +--- 11 files changed, 166 insertions(+), 150 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c90203ce..c399716d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1285,14 +1285,30 @@ "message": "Weekly installs", "description": "Text for label that shows the number of times a search result was installed during last week" }, - "searchStyles": { - "message": "Search contents", - "description": "Label for the search filter textbox on the Manage styles page" + "searchStylesAll": { + "message": "All", + "description": "Option for `find styles` scope selector in the manager." + }, + "searchStylesCode": { + "message": "CSS code", + "description": "Option for `find styles` scope selector in the manager." }, "searchStylesHelp": { - "message": " key focuses the search field.\nPlain text: search within the name, code, homepage URL and sites it is applied to. Words with less than 3 letters are ignored.\nStyles matching a full URL: prefix the search with , e.g. \nRegular expressions: include slashes and flags, e.g. \nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">", + "message": " or key focuses the search field.\nDefault mode is plain text search for all space-separated terms in any order.\nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">\nRegular expressions: include slashes and flags, e.g. \n\"For URL\" in scope selector: finds styles that apply to a fully specified URL e.g. https://www.example.org/\n\"Metadata\" in scope selector: searches in names, \"applies to\" specifiers, installation URL, update URL, and the entire metadata block for usercss styles.", "description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page" }, + "searchStylesMatchUrl": { + "message": "For URL", + "description": "Option for `find styles` scope selector in the manager. See searchMatchUrlHint for more info." + }, + "searchStylesMeta": { + "message": "Metadata", + "description": "Option for `find styles` scope selector in the manager." + }, + "searchStylesName": { + "message": "Name", + "description": "Option for `find styles` scope selector in the manager." + }, "sectionAdd": { "message": "Add another section", "description": "Label for the button to add a section" diff --git a/background/background.js b/background/background.js index 766222e6..dfcad0bf 100644 --- a/background/background.js +++ b/background/background.js @@ -309,33 +309,29 @@ function openEditor(params) { }); } -function openManage({options = false, search} = {}) { +async function openManage({options = false, search, searchMode} = {}) { let url = chrome.runtime.getURL('manage.html'); if (search) { - url += `?search=${encodeURIComponent(search)}`; + url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`; } if (options) { url += '#stylus-options'; } - return findExistingTab({ + let tab = await findExistingTab({ url, currentWindow: null, ignoreHash: true, ignoreSearch: true, - }) - .then(tab => { - if (tab) { - return Promise.all([ - activateTab(tab), - (tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url}) - .catch(console.error), - ]); - } - return getActiveTab().then(tab => { - if (isTabReplaceable(tab, url)) { - return activateTab(tab, {url}); - } - return browser.tabs.create({url}); - }); - }); + }); + if (tab) { + await activateTab(tab); + if (url !== (tab.pendingUrl || tab.url)) { + await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error); + } + return tab; + } + tab = await getActiveTab(); + return isTabReplaceable(tab, url) + ? activateTab(tab, {url}) + : browser.tabs.create({url}); } diff --git a/background/search-db.js b/background/search-db.js index 21ef0572..fcea0a15 100644 --- a/background/search-db.js +++ b/background/search-db.js @@ -1,90 +1,97 @@ -/* global API_METHODS styleManager tryRegExp debounce */ +/* global + API_METHODS + debounce + stringAsRegExp + styleManager + tryRegExp + usercss +*/ 'use strict'; (() => { // toLocaleLowerCase cache, autocleared after 1 minute const cache = new Map(); - // top-level style properties to be searched - const PARTS = { - name: searchText, - url: searchText, - sourceCode: searchText, - sections: searchSections, - }; + const METAKEYS = ['customName', 'name', 'url', 'installationUrl', 'updateUrl']; + + const extractMeta = style => + style.usercssData + ? (style.sourceCode.match(usercss.RX_META) || [''])[0] + : null; + + const stripMeta = style => + style.usercssData + ? style.sourceCode.replace(usercss.RX_META, '') + : null; + + const MODES = Object.assign(Object.create(null), { + code: (style, test) => + style.usercssData + ? test(stripMeta(style)) + : searchSections(style, test, 'code'), + + meta: (style, test, part) => + METAKEYS.some(key => test(style[key])) || + test(part === 'all' ? style.sourceCode : extractMeta(style)) || + searchSections(style, test, 'funcs'), + + name: (style, test) => + test(style.customName) || + test(style.name), + + all: (style, test) => + MODES.meta(style, test, 'all') || + !style.usercssData && MODES.code(style, test), + }); /** * @param params * @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed") + * @param {'name'|'meta'|'code'|'all'|'url'} [params.mode=all] * @param {number[]} [params.ids] - if not specified, all styles are searched * @returns {number[]} - array of matched styles ids */ - API_METHODS.searchDB = ({query, ids}) => { - let rx, words, icase, matchUrl; - query = query.trim(); - - if (/^url:/i.test(query)) { - matchUrl = query.slice(query.indexOf(':') + 1).trim(); - if (matchUrl) { - return styleManager.getStylesByUrl(matchUrl) - .then(results => results.map(r => r.data.id)); - } - } - if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) { - rx = tryRegExp(RegExp.$1, RegExp.$2); - } - if (!rx) { - words = query - .split(/(".*?")|\s+/) - .filter(Boolean) - .map(w => w.startsWith('"') && w.endsWith('"') - ? w.slice(1, -1) - : w) - .filter(w => w.length > 1); - words = words.length ? words : [query]; - icase = words.some(w => w === lower(w)); - } - - return styleManager.getAllStyles().then(styles => { - if (ids) { - const idSet = new Set(ids); - styles = styles.filter(s => idSet.has(s.id)); - } - const results = []; - for (const style of styles) { - const id = style.id; - if (!query || words && !words.length) { - results.push(id); - continue; - } - for (const part in PARTS) { - const text = part === 'name' ? style.customName || style.name : style[part]; - if (text && PARTS[part](text, rx, words, icase)) { - results.push(id); - break; - } - } - } + API_METHODS.searchDB = async ({query, mode = 'all', ids}) => { + let res = []; + if (mode === 'url' && query) { + res = (await styleManager.getStylesByUrl(query)).map(r => r.data.id); + } else if (mode in MODES) { + const modeHandler = MODES[mode]; + const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query); + const rx = m && tryRegExp(m[1], m[2]); + const test = rx ? rx.test.bind(rx) : makeTester(query); + res = (await styleManager.getAllStyles()) + .filter(style => + (!ids || ids.includes(style.id)) && + (!query || modeHandler(style, test))) + .map(style => style.id); if (cache.size) debounce(clearCache, 60e3); - return results; - }); + } + return res; }; - function searchText(text, rx, words, icase) { - if (rx) return rx.test(text); - for (let pass = 1; pass <= (icase ? 2 : 1); pass++) { - if (words.every(w => text.includes(w))) return true; - text = lower(text); - } + function makeTester(query) { + const flags = `u${lower(query) === query ? 'i' : ''}`; + const words = query + .split(/(".*?")|\s+/) + .filter(Boolean) + .map(w => w.startsWith('"') && w.endsWith('"') + ? w.slice(1, -1) + : w) + .filter(w => w.length > 1); + const rxs = (words.length ? words : [query]) + .map(w => stringAsRegExp(w, flags)); + return text => rxs.every(rx => rx.test(text)); } - function searchSections(sections, rx, words, icase) { + function searchSections({sections}, test, part) { + const inCode = part === 'code' || part === 'all'; + const inFuncs = part === 'funcs' || part === 'all'; for (const section of sections) { for (const prop in section) { const value = section[prop]; - if (typeof value === 'string') { - if (searchText(value, rx, words, icase)) return true; - } else if (Array.isArray(value)) { - if (value.some(str => searchText(str, rx, words, icase))) return true; + if (inCode && prop === 'code' && test(value) || + inFuncs && Array.isArray(value) && value.some(str => test(str))) { + return true; } } } @@ -92,9 +99,7 @@ function lower(text) { let result = cache.get(text); - if (result) return result; - result = text.toLocaleLowerCase(); - cache.set(text, result); + if (!result) cache.set(text, result = text.toLocaleLowerCase()); return result; } diff --git a/background/style-manager.js b/background/style-manager.js index 569152f2..20713d15 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -470,11 +470,7 @@ const styleManager = (() => { } } if (sectionMatched) { - result.push({ - data: getStyleWithNoCode(data), - excluded, - sloppy, - }); + result.push({data, excluded, sloppy}); } } return result; diff --git a/global.css b/global.css index ad92d486..77489a74 100644 --- a/global.css +++ b/global.css @@ -142,6 +142,7 @@ select { transition: color .5s; } +.select-wrapper, .select-resizer { display: inline-flex!important; cursor: default; diff --git a/js/usercss.js b/js/usercss.js index 7f2742e1..4dd2177a 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -12,7 +12,12 @@ const usercss = (() => { }; const RX_META = /\/\*!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i; const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']); - return {buildMeta, buildCode, assignVars}; + return { + RX_META, + buildMeta, + buildCode, + assignVars, + }; function buildMeta(sourceCode) { sourceCode = sourceCode.replace(/\r\n?/g, '\n'); diff --git a/manage.html b/manage.html index 6ca13059..2854c1b0 100644 --- a/manage.html +++ b/manage.html @@ -266,9 +266,19 @@
- +
+ + +
diff --git a/manage/filters.js b/manage/filters.js index fcb7fc62..e844f852 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -11,8 +11,9 @@ const filtersSelector = { let initialized = false; -router.watch({search: ['search']}, ([search]) => { +router.watch({search: ['search', 'searchMode']}, ([search, mode]) => { $('#search').value = search || ''; + if (mode) $('#searchMode').value = mode; if (!initialized) { initFilters(); initialized = true; @@ -37,15 +38,15 @@ HTMLSelectElement.prototype.adjustWidth = function () { }; function initFilters() { - $('#search').oninput = e => { - router.updateSearch('search', e.target.value); + $('#search').oninput = $('#searchMode').oninput = function (e) { + router.updateSearch(this.id, e.target.value); }; $('#search-help').onclick = event => { event.preventDefault(); messageBox({ className: 'help-text', - title: t('searchStyles'), + title: t('search'), contents: $create('ul', t('searchStylesHelp').split('\n').map(line => @@ -141,7 +142,7 @@ function initFilters() { } -function filterOnChange({target: el, forceRefilter}) { +function filterOnChange({target: el, forceRefilter, alreadySearched}) { const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim()); if (!forceRefilter) { const value = getValue(el); @@ -164,7 +165,7 @@ function filterOnChange({target: el, forceRefilter}) { unhide: buildFilter(false), }); if (installed) { - reapplyFilter().then(sorter.updateStripes); + reapplyFilter(installed, alreadySearched).then(sorter.updateStripes); } } @@ -278,10 +279,12 @@ function showFiltersStats() { } -function searchStyles({immediately, container} = {}) { +async function searchStyles({immediately, container} = {}) { const el = $('#search'); + const elMode = $('#searchMode'); const query = el.value.trim(); - if (query === el.lastValue && !immediately && !container) { + const mode = elMode.value; + if (query === el.lastValue && mode === elMode.lastValue && !immediately && !container) { return; } if (!immediately) { @@ -289,24 +292,24 @@ function searchStyles({immediately, container} = {}) { return; } el.lastValue = query; + elMode.lastValue = mode; - const entries = container && container.children || container || installed.children; - return API.searchDB({ - query, - ids: [...entries].map(el => el.styleId), - }).then(ids => { - ids = new Set(ids); - let needsRefilter = false; - for (const entry of entries) { - const isMatching = ids.has(entry.styleId); - if (entry.classList.contains('not-matching') !== !isMatching) { - entry.classList.toggle('not-matching', !isMatching); - needsRefilter = true; - } + const all = installed.children; + const entries = container && container.children || container || all; + const idsToSearch = entries !== all && [...entries].map(el => el.styleId); + const ids = entries[0] + ? await API.searchDB({query, mode, ids: idsToSearch}) + : []; + let needsRefilter = false; + for (const entry of entries) { + const isMatching = ids.includes(entry.styleId); + if (entry.classList.contains('not-matching') !== !isMatching) { + entry.classList.toggle('not-matching', !isMatching); + needsRefilter = true; } - if (needsRefilter && !container) { - filterOnChange({forceRefilter: true}); - } - return container; - }); + } + if (needsRefilter && !container) { + filterOnChange({forceRefilter: true, alreadySearched: true}); + } + return container; } diff --git a/manage/manage.css b/manage/manage.css index 37861b7b..4cbf38a2 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -825,10 +825,6 @@ a:hover { background-color: hsla(0, 0%, 50%, .4); } -#filters { - border: 1px solid transparent; -} - .active #filters-stats { background-color: darkcyan; border-color: darkcyan; @@ -874,10 +870,11 @@ a:hover { #search-wrapper, #sort-wrapper { display: flex; align-items: center; - flex-wrap: wrap; margin-bottom: .5rem; } - +#searchMode { + margin-left: -1px; +} #search-wrapper { margin-top: .35rem; } @@ -886,17 +883,12 @@ a:hover { display: inline-flex; flex-grow: 1; position: relative; - max-width: calc(100% - 30px); } #manage\.newUI\.sort { max-width: 100%; } -#search { - max-width: calc(100% - 30px); -} - #search, #manage\.newUI\.sort { flex-grow: 1; background: #fff; diff --git a/manage/manage.js b/manage/manage.js index 8dc9d79d..7fb7ff04 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -75,7 +75,7 @@ const handleEvent = {}; const query = router.getSearch('search'); const [styles, ids, el] = await Promise.all([ API.getAllStyles(), - query && API.searchDB({query}), // FIXME: integrate this into filter.js + query && API.searchDB({query, mode: router.getSearch('searchMode')}), waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift prefs.initializing, ]); @@ -102,7 +102,7 @@ const handleEvent = {}; ].map(id => `--${id}:"${CSS.escape(t(id))}";`).join('') }}`); if (!VIVALDI) { - $$('#header select').forEach(el => el.adjustWidth()); + $$('#filters select').forEach(el => el.adjustWidth()); } if (CHROME >= 80 && CHROME <= 88) { // Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598 diff --git a/popup/popup.js b/popup/popup.js index dbe355fe..a968ff33 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -137,7 +137,6 @@ function initPopup(frames) { Object.assign($('#popup-manage-button'), { onclick: handleEvent.openManager, - onmouseup: handleEvent.openManager, oncontextmenu: handleEvent.openManager, }); @@ -654,17 +653,10 @@ Object.assign(handleEvent, { }, openManager(event) { - if (event.button === 2 && !tabURL) return; event.preventDefault(); - if (!this.eventHandled) { - // FIXME: this only works if popup is closed - this.eventHandled = true; - API.openManage({ - search: tabURL && (event.shiftKey || event.button === 2) ? - `url:${tabURL}` : null, - }); - window.close(); - } + const isSearch = tabURL && (event.shiftKey || event.button === 2); + API.openManage(isSearch ? {search: tabURL, searchMode: 'url'} : {}); + window.close(); }, copyContent(event) {