add scope selector to style search in manager
This commit is contained in:
parent
a1acf53539
commit
8b98baba6a
|
@ -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 <url:>, e.g. <url:https://github.com/openstyles/stylus>\nRegular expressions: include slashes and flags, e.g. </body.*?\\ba\\b/simguy>\nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">",
|
||||
"message": "</> or <Ctrl-F> 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. </body.*?\\ba\\b/i>\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"
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -470,11 +470,7 @@ const styleManager = (() => {
|
|||
}
|
||||
}
|
||||
if (sectionMatched) {
|
||||
result.push({
|
||||
data: getStyleWithNoCode(data),
|
||||
excluded,
|
||||
sloppy,
|
||||
});
|
||||
result.push({data, excluded, sloppy});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -142,6 +142,7 @@ select {
|
|||
transition: color .5s;
|
||||
}
|
||||
|
||||
.select-wrapper,
|
||||
.select-resizer {
|
||||
display: inline-flex!important;
|
||||
cursor: default;
|
||||
|
|
|
@ -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');
|
||||
|
|
12
manage.html
12
manage.html
|
@ -266,9 +266,19 @@
|
|||
</label>
|
||||
|
||||
<div id="search-wrapper">
|
||||
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false"
|
||||
<input id="search" type="search" i18n-placeholder="search" spellcheck="false"
|
||||
data-filter=":not(.not-matching)"
|
||||
data-filter-hide=".not-matching">
|
||||
<div class="select-wrapper">
|
||||
<select id="searchMode">
|
||||
<option i18n-text="searchStylesName" value="name"></option>
|
||||
<option i18n-text="searchStylesMeta" value="meta" selected></option>
|
||||
<option i18n-text="searchStylesCode" value="code"></option>
|
||||
<option i18n-text="searchStylesMatchUrl" value="url"></option>
|
||||
<option i18n-text="searchStylesAll" value="all"></option>
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<a href="#" id="search-help" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user