Switch to filter text/buttons

This commit is contained in:
Rob Garrison 2020-03-23 07:25:50 -05:00
parent 2fd08cb041
commit 5047ceb1fa
10 changed files with 534 additions and 390 deletions

View File

@ -1253,8 +1253,8 @@
"description": "Text for button to apply the selected action"
},
"bulkActionsTooltip": {
"message": "Click to open the filter, search and bulk actions panel",
"description": "Text for button to apply the selected action"
"message": "Bulk actions can be applied to selected styles in this column",
"description": "Select style for bulk action header tooltip"
},
"bulkActionsError": {
"message": "Choose at least one style",

View File

@ -4,13 +4,108 @@
(() => {
// 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,
};
// Creates an array of intermediate words (2 letter minimum)
// 'usercss' => ["us", "use", "user", "userc", "usercs", "usercss"]
// this makes it so the user can type partial queries and not have the search
// constantly switching between using & ignoring the filter
const createPartials = id => id.split('').reduce((acc, _, index) => {
if (index > 0) {
acc.push(id.substring(0, index + 1));
}
return acc;
}, []);
const searchWithin = [{
id: 'code',
labels: createPartials('code'),
get: style => style.sections.map(section => section.code).join(' ')
}, {
id: 'usercss',
labels: [...createPartials('usercss'), ...createPartials('meta')],
get: style => JSON.stringify(style.usercssData || {})
// remove JSON structure; restore urls
.replace(/[[\]{},":]/g, ' ').replace(/\s\/\//g, '://')
}, {
id: 'name', // default
labels: createPartials('name'),
get: style => style.name
}];
const styleProps = [{
id: 'enabled',
labels: ['on', ...createPartials('enabled')],
check: style => style.enabled
}, {
id: 'disabled',
labels: ['off', ...createPartials('disabled')],
check: style => !style.enabled
}, {
id: 'local',
labels: createPartials('local'),
check: style => !style.updateUrl
}, {
id: 'external',
labels: createPartials('external'),
check: style => style.updateUrl
}, {
id: 'usercss',
labels: createPartials('usercss'),
check: style => style.usercssData
}, {
id: 'non usercss',
labels: ['original', ...createPartials('nonusercss')],
check: style => !style.usercssData
}];
const matchers = [{
id: 'url',
test: query => /url:\w+/i.test(query),
matches: query => {
const matchUrl = query.match(/url:([/.-_\w]+)/);
const result = matchUrl && matchUrl[1]
? styleManager.getStylesByUrl(matchUrl[1])
.then(result => result.map(r => r.data.id))
: [];
return {result};
},
}, {
id: 'regex',
test: query => {
const x = query.includes('/') && !query.includes('//') &&
/^\/(.+?)\/([gimsuy]*)$/.test(query);
// console.log('regex match?', query, x);
return x;
},
matches: () => ({regex: tryRegExp(RegExp.$1, RegExp.$2)})
}, {
id: 'props',
test: query => /is:/.test(query),
matches: query => {
const label = /is:(\w+)/g.exec(query);
return label && label[1]
? {prop: styleProps.find(p => p.labels.includes(label[1]))}
: {};
}
}, {
id: 'within',
test: query => /in:/.test(query),
matches: query => {
const label = /in:(\w+)/g.exec(query);
return label && label[1]
? {within: searchWithin.find(s => s.labels.includes(label[1]))}
: {};
}
}, {
id: 'default',
test: () => true,
matches: query => {
const word = query.startsWith('"') && query.endsWith('"')
? query.slice(1, -1)
: query;
return {word: word || query};
}
}];
/**
* @param params
@ -19,77 +114,94 @@
* @returns {number[]} - array of matched styles ids
*/
API_METHODS.searchDB = ({query, ids}) => {
let rx, words, icase, matchUrl;
query = query.trim();
const parts = query.trim().split(/(".*?")|\s+/).filter(Boolean);
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));
const searchFilters = {
words: [],
regex: null, // only last regex expression is used
results: [],
props: [],
within: [],
};
const searchText = (text, searchFilters) => {
if (searchFilters.regex) return searchFilters.regex.test(text);
for (let pass = 1; pass <= (searchFilters.icase ? 2 : 1); pass++) {
if (searchFilters.words.every(w => text.includes(w))) return true;
text = lower(text);
}
};
const searchProps = (style, searchFilters) => {
const x = searchFilters.props.every(prop => {
const y = prop.check(style)
// if (y) console.log('found prop', prop.id, style.id)
return y;
});
// if (x) console.log('found prop', style.id)
return x;
};
parts.forEach(part => {
matchers.some(matcher => {
if (matcher.test(part)) {
const {result, regex, word, prop, within} = matcher.matches(part || '');
if (result) searchFilters.results.push(result);
if (regex) searchFilters.regex = regex; // limited to a single regexp
if (word) searchFilters.words.push(word);
if (prop) searchFilters.props.push(prop);
if (within) searchFilters.within.push(within);
return true;
}
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
rx = tryRegExp(RegExp.$1, RegExp.$2);
});
});
if (!searchFilters.within.length) {
searchFilters.within.push(...searchWithin.slice(-1));
}
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));
// console.log('matchers', searchFilters);
// url matches
if (searchFilters.results.length) {
return searchFilters.results;
}
searchFilters.icase = searchFilters.words.some(w => w === lower(w));
query = parts.join(' ').trim();
return styleManager.getAllStyles().then(styles => {
if (ids) {
const idSet = new Set(ids);
styles = styles.filter(s => idSet.has(s.id));
}
const results = [];
const propResults = [];
const hasProps = searchFilters.props.length > 0;
const noWords = searchFilters.words.length === 0;
for (const style of styles) {
const id = style.id;
if (!query || words && !words.length) {
if (noWords) {
// no query or only filters are matching -> show all styles
results.push(id);
continue;
}
for (const part in PARTS) {
const text = style[part];
if (text && PARTS[part](text, rx, words, icase)) {
} else {
const text = searchFilters.within.map(within => within.get(style)).join(' ');
if (searchText(text, searchFilters)) {
results.push(id);
break;
}
}
if (hasProps && searchProps(style, searchFilters) && results.includes(id)) {
propResults.push(id);
}
}
// results AND propResults
const finalResults = hasProps
? propResults.filter(id => results.includes(id))
: results;
if (cache.size) debounce(clearCache, 60e3);
return results;
// console.log('final', finalResults)
return finalResults;
});
};
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 searchSections(sections, rx, words, icase) {
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;
}
}
}
}
function lower(text) {
let result = cache.get(text);
if (result) return result;

View File

@ -221,10 +221,7 @@
<span class="filter-stats-wrapper">
<span id="filters-stats"></span>
<a id="reset-filters" class="tt-e" href="#" tabindex="0" i18n-data-title="genericResetLabel">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
</svg>
<svg class="svg-icon" viewBox="0 0 20 20"><use xlink:href="#svg-icon-x"/></svg>
</a>
</span>
</div>
@ -256,12 +253,6 @@
<span class="spacer"></span>
<a href="#" id="update-all" class="tt-w" i18n-data-title="checkAllUpdates">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M11 8v5l4.25 2.52.77-1.28-3.52-2.09V8zm10 2V3l-2.64 2.64A9 9 0 1 0 21 12h-2a7 7 0 1 1-2.05-4.95L14 10h7z"/>
</svg>
</a>
<div class="manage-backups tt-w" i18n-data-title="backupImport">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M20.54 5.23l-1.39-1.68A1.45 1.45 0 0 0 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V6.5c0-.48-.17-.93-.46-1.27zM6.24 5h11.52l.81.97H5.44l.8-.97zM5 19V8h14v11H5zm8.45-9h-2.9v3H8l4 4 4-4h-2.55z"/>
@ -329,98 +320,7 @@
</h1>
<div id="tools-wrapper" class="hidden">
<div class="manage-row">
<span id="search-wrapper">
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false"
data-filter=":not(.not-matching)"
data-filter-hide=".not-matching">
<a href="#" id="search-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
<span id="filters-wrapper">
<strong i18n-text="manageFilters"></strong>
<span class="filter-selection">
<label class="checkmate" tabindex="0">
<input id="manage.onlyEnabled" type="checkbox"
data-filter=".enabled"
data-filter-hide=".disabled">
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
</svg>
</label>
<span class="select-resizer">
<select id="manage.onlyEnabled.invert">
<option i18n-text="manageOnlyEnabled" value="false"></option>
<option i18n-text="manageOnlyDisabled" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</span>
</span>
<span class="filter-selection">
<label class="checkmate" tabindex="0">
<input id="manage.onlyLocal" type="checkbox"
data-filter=":not(.updatable):not(.update-done)"
data-filter-hide=".updatable, .update-done">
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
</svg>
</label>
<span class="select-resizer">
<select id="manage.onlyLocal.invert" class="tt-s" i18n-data-title="manageOnlyLocalTooltip">
<option i18n-text="manageOnlyLocal" value="false"></option>
<option i18n-text="manageOnlyExternal" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</span>
</span>
<span class="filter-selection">
<label class="checkmate" tabindex="0">
<input id="manage.onlyUsercss" type="checkbox"
data-filter=".usercss"
data-filter-hide=":not(.usercss)">
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
</svg>
</label>
<span class="select-resizer">
<select id="manage.onlyUsercss.invert">
<option i18n-text="manageOnlyUsercss" value="false"></option>
<option i18n-text="manageOnlyNonUsercss" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</span>
</span>
<span class="filter-selection">
<label id="only-updates" class="hidden checkmate" tabindex="0">
<input type="checkbox"
data-filter=".can-update, .update-problem, .update-done"
data-filter-hide=":not(.updatable):not(.update-done),
.no-update:not(.update-problem),
.updatable:not(.can-update):not(.update-problem):not(.update-done)">
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
</svg>
<span i18n-text="manageOnlyUpdates"></span>
</label>
</span>
</span>
</div>
<div id="tools-wrapper">
<div id="bulk-actions" class="manage-row">
<label class="checkmate toggle-all" tabindex="0">
@ -450,6 +350,13 @@
<button id="bulk-actions-apply" i18n-text="bulkActionsApply" class="tt-e" disabled>
<span id="update-progress"></span>
</button>
<button href="#" id="update-all" class="tt-w" i18n-data-title="checkAllUpdates" type="button">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M11 8v5l4.25 2.52.77-1.28-3.52-2.09V8zm10 2V3l-2.64 2.64A9 9 0 1 0 21 12h-2a7 7 0 1 1-2.05-4.95L14 10h7z"/>
</svg>
</button>
<span id="bulk-info">
<!-- Bulk update -->
<span data-bulk="update">
@ -485,13 +392,96 @@
</svg>
</div>
<div class="manage-row">
<div id="search-wrapper">
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false"
data-filter=":not(.not-matching)"
data-filter-hide=".not-matching">
</div>
<div id="filters-wrapper">
<button
class="reset-filters search-filter tt-w"
type="button"
i18n-data-title="genericResetLabel"
data-filter=".entry"
data-filter-hide=".disabled"
>
<svg class="svg-icon"><use xlink:href="#svg-icon-x"/></svg>
</button>
<span class="button-group">
<label class="search-filter tt-w" i18n-data-title="manageOnlyEnabled">
<input
id="manage.onlyEnabled"
name="enabled"
type="radio"
data-filter=".enabled"
data-filter-hide=".disabled"
/>
<svg class="svg-icon checkbox-enabled" viewBox="0 0 10 10">
<path d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
</svg>
</label>
<label class="search-filter tt-w" i18n-data-title="manageOnlyDisabled">
<input id="manage.onlyEnabled.invert" name="enabled" type="radio" />
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
</svg>
</label>
</span>
<span class="button-group">
<label class="search-filter tt-w" i18n-data-title="manageOnlyUsercss">
<input
id="manage.onlyUsercss"
name="usercss"
type="radio"
data-filter=".usercss"
data-filter-hide=":not(.usercss)"
/>
<span>usercss</span> <!-- TODO: localize -->
</label>
<label class="search-filter tt-w" i18n-data-title="manageOnlyNonUsercss">
<input type="radio" id="manage.onlyUsercss.invert" name="usercss" />
<span>non-usercss</span> <!-- TODO: localize -->
</label>
</span>
<span class="button-group">
<label class="search-filter tt-w" i18n-data-title="manageOnlyLocal">
<input
id="manage.onlyLocal"
name="local"
type="radio"
data-filter=":not(.updatable):not(.update-done)"
data-filter-hide=".updatable, .update-done"
/>
<span>local</span> <!-- TODO: localize -->
</label>
<label class="search-filter tt-w" i18n-data-title="manageOnlyExternal">
<input id="manage.onlyLocal.invert" name="local" type="radio" />
<span>external</span> <!-- TODO: localize -->
</label>
</span>
<label class="search-filter hidden">
<input
id="only-updates"
type="checkbox"
data-filter=".can-update, .update-problem, .update-done"
data-filter-hide=":not(.updatable):not(.update-done),
.no-update:not(.update-problem),
.updatable:not(.can-update):not(.update-problem):not(.update-done)"
/>
<span i18n-text="manageOnlyUpdates"></span>
</label>
<a href="#" id="search-help">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
</div>
</div>
<div id="installed" class="manage-col-entries">
<header class="entry-header">
<div class="entry-col header-filter center-text tt-se" i18n-data-title="bulkActionsTooltip">
<span></span>
<svg class="svg-icon" width="20" height="20" viewBox="0 0 14 14">
<svg class="svg-icon no-pointer" width="20" height="20" viewBox="0 0 14 14">
<path d="M6.42 7.58L2.92 3.5h8.75l-3.5 4.08v4.09c-1 0-1.75-.76-1.75-1.75V7.58z"/>
</svg>
</div>
@ -563,125 +553,6 @@
</div>
</div>
<!-- <div id="manage-settings">
<div class="settings-column">
<div id="style-actions">
<div id="add-style-wrapper">
<a href="edit.html">
<button id="add-style-label" i18n-text="addStyleLabel" tabindex="-1"></button>
</a>
<label id="add-style-as-usercss-wrapper">
<input type="checkbox" id="newStyleAsUsercss">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<span i18n-text="manageNewStyleAsUsercss" i18n-title="optionsAdvancedNewStyleAsUsercss"></span>
<a id="usercss-wiki"
href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n-title="externalUsercssDocument"
tabindex="0">
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
</svg>
</a>
</label>
</div>
</div>
</div>
<div class="settings-column">
<details id="options" data-pref="manage.options.expanded">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
<label>
<input id="manage.newUI" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<span i18n-text="manageNewUI"></span>
</label>
<div id="newUIoptions">
<div>
<label for="manage.newUI.favicons" i18n-text="manageFavicons">
<input id="manage.newUI.favicons" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<a href="#" data-toggle-on-click="#faviconsHelp" tabindex="0">
<svg class="svg-icon select-arrow">
<title i18n-text="optionsSubheading"></title>
<use xlink:href="#svg-icon-select-arrow"/>
</svg>
</a>
</label>
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
<div>
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray">
<input id="manage.newUI.faviconsGray" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
</div>
</div>
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
</div>
<div id="options-buttons">
<button id="manage-options-button" i18n-text="openOptions"></button>
<button id="manage-shortcuts-button" class="chromium-only"
i18n-text="shortcuts"
i18n-title="shortcutsNote"></button>
<a id="find-editor-styles"
href="https://userstyles.org/styles/browse/chrome-extension"
i18n-title="editorStylesButton"
target="_blank"><button i18n-text="cm_theme" tabindex="-1"></button></a>
</div>
</details>
<details id="backup" data-pref="manage.backup.expanded">
<summary><h2 id="backup-title" i18n-text="backupButtons"></h2></summary>
<span id="backup-message" i18n-text="backupMessage"></span>
<div id="backup-buttons">
<div class="dropdown">
<button class="dropbtn">
<span>Export</span>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</button>
<div class="dropdown-content">
<a href="#" id="file-all-styles" i18n-text="bckpInstStyles"></a>
<a id="sync-dropbox-export" i18n-text="syncDropboxStyles" i18n-title="syncDropboxDeprecated"></a>
</div>
</div>
<div class="dropdown">
<button class="dropbtn">
<span>Import</span>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</button>
<div class="dropdown-content">
<a href="#" id="unfile-all-styles" i18n-text="retrieveBckp"></a>
<a id="sync-dropbox-import" i18n-text="retrieveDropboxSync" i18n-title="syncDropboxDeprecated"></a>
</div>
</div>
</div>
</details>
<div id="manage-text">
<span><a href="https://userstyles.org" target="_blank" i18n-text="linkGetStyles"></a></span>
<span><a href="https://add0n.com/stylus.html#features" target="_blank" i18n-text="linkGetHelp"></a></span>
<span><a href="https://github.com/openstyles/stylus/wiki" target="_blank" i18n-text="linkStylusWiki"></a></span>
<span><a href="https://www.transifex.com/github-7/Stylus" target="_blank" i18n-text="linkTranslate"></a></span>
</div>
</div>
</div>
</div>
<div id="installed"></div> -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
<symbol id="svg-icon-checked" viewBox="0 0 1000 1000">
<path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,
@ -723,6 +594,10 @@
1.13zM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
</symbol>
<symbol id="svg-icon-x" viewBox="0 0 20 20">
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
</symbol>
</svg>
</body>

View File

@ -104,7 +104,6 @@ const bulk = {
if (installed.dataset.total) {
// ignore filter checkboxes
if (target.type === 'checkbox' && target.closest('.toggle-all, .entry-filter')) {
handleEvent.toggleBulkActions({hidden: false});
const bulk = $('#toggle-all-filters');
const state = target.checked;
const visibleEntries = $$('.entry-filter-toggle')

View File

@ -1,4 +1,4 @@
/* global installed messageBox sorter $ $$ $create t debounce prefs API router */
/* global installed messageBox sorter $ $$ $create t debounce prefs API UI router resetUpdates */
/* exported filterAndAppend */
'use strict';
@ -37,8 +37,9 @@ HTMLSelectElement.prototype.adjustWidth = function () {
};
function init() {
$('#search').oninput = e => {
router.updateSearch('search', e.target.value);
$('#search').oninput = event => {
router.updateSearch('search', event.target.value);
UI.updateFilterLabels();
};
$('#search-help').onclick = event => {
@ -57,48 +58,13 @@ function init() {
} else {
return s;
}
})))),
}))
)
),
buttons: [t('confirmOK')],
});
};
$$('select[id$=".invert"]').forEach(el => {
const slave = $('#' + el.id.replace('.invert', ''));
const slaveData = slave.dataset;
const valueMap = new Map([
[false, slaveData.filter],
[true, slaveData.filterHide],
]);
// enable slave control when user switches the value
el.oninput = () => {
if (!slave.checked) {
// oninput occurs before onchange
setTimeout(() => {
if (!slave.checked) {
slave.checked = true;
slave.dispatchEvent(new Event('change', {bubbles: true}));
}
});
}
};
// swap slave control's filtering rules
el.onchange = event => {
const value = el.value === 'true';
const filter = valueMap.get(value);
if (slaveData.filter === filter) {
return;
}
slaveData.filter = filter;
slaveData.filterHide = valueMap.get(!value);
debounce(filterOnChange, 0, event);
// avoid triggering MutationObserver during page load
if (document.readyState === 'complete') {
el.adjustWidth();
}
};
el.onchange({target: el});
});
$$('[data-filter]').forEach(el => {
el.onchange = filterOnChange;
if (el.closest('.hidden')) {
@ -108,9 +74,9 @@ function init() {
$('#reset-filters').onclick = event => {
event.preventDefault();
if (!filtersSelector.hide) {
return;
}
// if (!filtersSelector.hide) {
// return;
// }
for (const el of $$('#tools-wrapper [data-filter]')) {
let value;
if (el.type === 'checkbox' && el.checked) {
@ -127,6 +93,8 @@ function init() {
}
filterOnChange({forceRefilter: true});
router.updateSearch('search', '');
resetUpdates();
UI.updateFilterLabels();
};
filterOnChange({forceRefilter: true});
@ -134,7 +102,7 @@ function init() {
function filterOnChange({target: el, forceRefilter}) {
const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim());
const getValue = elm => (elm.type === 'search') ? elm.value.trim() : elm.checked;
if (!forceRefilter) {
const value = getValue(el);
if (value === el.lastValue) {
@ -155,6 +123,7 @@ function filterOnChange({target: el, forceRefilter}) {
hide: buildFilter(true),
unhide: buildFilter(false),
});
console.log('filter on change', filtersSelector, installed)
if (installed) {
reapplyFilter().then(sorter.updateStripes);
}
@ -283,6 +252,7 @@ function searchStyles({immediately, container} = {}) {
el.lastValue = query;
const entries = container && container.children || container || installed.children;
console.log('search?', query)
return API.searchDB({
query,
ids: [...entries].map(el => el.styleId),

View File

@ -1,5 +1,6 @@
/* global messageBox styleSectionsEqual API onDOMready
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
handleEvent
styleJSONseemsValid */
'use strict';
@ -46,8 +47,9 @@ onDOMready().then(() => {
this.ondragend();
if (event.dataTransfer.files.length) {
event.preventDefault();
if ($('#only-updates input').checked) {
$('#only-updates input').click();
const updates = $('#only-updates');
if (updates.checked) {
handleEvent.checkFilterSelectors(updates);
}
importFromFile({file: event.dataTransfer.files[0]});
}

View File

@ -1,7 +1,7 @@
/*
global messageBox getStyleWithNoCode
filterAndAppend showFiltersStats
checkUpdate handleUpdateInstalled
checkUpdate handleUpdateInstalled resetUpdates
objectDiff
configDialog
sorter msg prefs API onDOMready $ $$ setupLivePrefs
@ -80,8 +80,19 @@ function initGlobalEvents() {
$('#update-all').onclick = event => {
event.preventDefault();
handleEvent.toggleBulkActions({hidden: false});
bulk.updateAll();
};
$('#filters-wrapper').onclick = event => {
event.preventDefault();
handleEvent.toggleFilter(event.target);
};
$('#search').onsearch = event => {
if (event.target.value === '') {
console.log('search empty')
handleEvent.resetFilters();
}
}
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
@ -95,28 +106,12 @@ function initGlobalEvents() {
$$('.applies-to-extra[open]').forEach(el => {
el.removeAttribute('open');
});
// Close bulk actions
handleEvent.toggleBulkActions({hidden: true});
} else if (event.which === 32 && event.target.classList.contains('checkmate')) {
// pressing space toggles the containing checkbox
$('input[type="checkbox"]', event.target).click();
}
});
$$('[data-toggle-on-click]').forEach(el => {
// dataset on SVG doesn't work in Chrome 49-??, works in 57+
const target = $(el.getAttribute('data-toggle-on-click'));
el.onclick = event => {
event.preventDefault();
target.classList.toggle('hidden');
if (target.classList.contains('hidden')) {
el.removeAttribute('open');
} else {
el.setAttribute('open', '');
}
};
});
// triggered automatically by setupLivePrefs() below
enforceInputRange($('#manage.newUI.targets'));
@ -146,7 +141,6 @@ Object.assign(handleEvent, {
'.update': 'update',
'.entry-delete': 'delete',
'.entry-configure-usercss': 'config',
'.header-filter': 'toggleBulkActions',
'.sortable': 'updateSort',
'#applies-to-config': 'appliesConfig',
'.applies-to-extra-expander': 'toggleExtraAppliesTo'
@ -215,12 +209,6 @@ Object.assign(handleEvent, {
UI.addLabels(entry);
},
toggleBulkActions({hidden}) {
const tools = $('#tools-wrapper');
tools.classList.toggle('hidden', hidden);
$('.header-filter').classList.toggle('active', !tools.classList.contains('hidden'));
},
toggleExtraAppliesTo(event, entry) {
event.preventDefault();
entry.classList.toggle('hide-extra');
@ -230,6 +218,59 @@ Object.assign(handleEvent, {
}
},
resetFilters() {
$('#reset-filters').click();
// TODO: figure out why we need to press this twice
$('#reset-filters').click();
resetUpdates();
},
toggleFilter(el) {
if (el.classList.contains('reset-filters')) {
return handleEvent.resetFilters();
}
const target = (el.nodeName === 'LABEL') ? $('input', el) : el;
const type = Object.values(UI.searchFilters).find(filter => filter.id === target.id);
const filterQuery = type && type.query || '';
const remove = type && type.invert ? UI.searchFilters[type.invert].query : '';
const len = filterQuery.length + 1;
const search = $('#search');
let {selectionStart, selectionEnd, value} = search;
if (value.includes(filterQuery)) {
value = ` ${value} `.replace(` ${filterQuery} `, ' ').trim();
if (selectionEnd > value.length) {
selectionStart -= len;
selectionEnd -= len;
}
} else {
if (selectionEnd === value.length) {
selectionStart += len;
selectionEnd += len;
}
value = (` ${value} ${filterQuery} `.replace(` ${remove} `, ' ')).trim();
}
search.value = value;
search.selectionStart = selectionStart;
search.selectionEnd = selectionEnd;
search.focus();
router.updateSearch('search', value);
UI.updateFilterLabels();
// updates or issues (special case)
if (target.dataset.filterSelectors) {
handleEvent.checkFilterSelectors(target);
}
},
checkFilterSelectors(target) {
const selectors = target.dataset.filterSelectors;
const checked = target.classList.contains('checked');
$$('.entry').forEach(entry => {
entry.classList.toggle('hidden', checked && !entry.matches(selectors));
});
},
check(event, entry) {
event.preventDefault();
checkUpdate(entry, {single: true});

View File

@ -40,6 +40,11 @@ const UI = {
content: "${t('filteredStylesAllHidden')}";
}
`));
// remove update filter on init
const search = $('#search');
search.value = search.value.replace(UI.searchFilters.updatable.query, '');
// update filter labels to match location.search
UI.updateFilterLabels();
},
showStyles: (styles = [], matchUrlIds) => {
@ -178,6 +183,24 @@ const UI = {
: '';
},
updateFilterLabels: () => {
const filterLabels = $$('#filters-wrapper .search-filter input');
filterLabels.forEach(cb => {
cb.checked = false;
cb.parentElement.classList.remove('checked');
});
const filters = Object.values(UI.searchFilters);
$('#search').value.split(' ').forEach(part => {
const filter = filters.find(entry => entry.query === part);
if (filter) {
const button = filterLabels.filter(btn => btn.id === filter.id);
if (button.length) {
button[0].checked = true;
button[0].parentElement.classList.add('checked');
}
}
});
},
createStyleTargetsElement: ({entry, style}) => {
const parts = UI._parts;
@ -226,6 +249,48 @@ const UI = {
entry.classList.toggle('global', !numTargets);
},
// This order matters
searchFilters: {
enabled: {
id: 'manage.onlyEnabled',
query: 'is:enabled',
invert: 'disabled'
},
disabled: {
id: 'manage.onlyEnabled.invert',
query: 'is:disabled',
invert: 'enabled'
},
usercss: {
id: 'manage.onlyUsercss',
query: 'is:usercss',
invert: 'original'
},
original: {
id: 'manage.onlyUsercss.invert',
query: 'is:nonusercss',
invert: 'usercss'
},
local: {
id: 'manage.onlyLocal',
query: 'is:local',
invert: 'external'
},
external: {
id: 'manage.onlyLocal.invert',
query: 'is:external',
invert: 'local'
},
// only checkbox; all others are radio buttons
updatable: {
id: 'only-updates',
query: '', // 'has:updates',
},
reset: {
id: 'reset-filters',
query: ''
}
},
getFaviconImgSrc: (container = installed) => {
if (!UI.favicons) return;

View File

@ -4,7 +4,7 @@
--favicon-size: 16px;
--narrow-column: 60px;
--header-height: 40px;
--toolbar-height: 60px;
--toolbar-height: 40px;
--entry-height: 30px;
--onoffswitch-width: 60px;
@ -15,6 +15,7 @@
--header-icon-hover-color: #2afefe;
--tools-bg-color: #ccc;
--tools-bg-hover: #eee;
--entry-header-bg-color: #ddd;
--entry-header-text-color: #111;
@ -71,13 +72,17 @@ a:hover {
color: var(--entry-text-hover);
}
body a[disabled],
body button[disabled] {
cursor: default;
}
.invisible {
visibility: hidden;
pointer-events: none;
}
.svg-icon {
cursor: pointer;
width: var(--entry-icon-size);
height: var(--entry-icon-size);
vertical-align: middle;
@ -85,6 +90,10 @@ a:hover {
fill: var(--entry-icon-color);
}
.svg-icon:not(.no-pointer) {
cursor: pointer;
}
#main-header .svg-icon {
width: var(--header-icon-size);
height: var(--header-icon-size);
@ -122,6 +131,7 @@ a:hover {
#bulk-actions {
justify-content: flex-start;
align-items: center;
}
#bulk-info > span,
@ -147,9 +157,9 @@ a:hover {
100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); }
}
.entry-header a:hover .svg-icon,
.entry a:hover .svg-icon,
.svg-icon:hover {
.entry-header a:hover .svg-icon:not(.no-pointer),
.entry a:hover .svg-icon:not(.no-pointer),
.svg-icon:not(.no-pointer):hover {
fill: var(--entry-icon-hover-color);
}
@ -340,14 +350,6 @@ body.all-styles-hidden-by-filters #installed:after {
content: '▾';
}
.header-filter span:before {
content: '►';
color: var(--entry-icon-color);
position: relative;
left: 7px;
}
.header-filter:hover span:before,
.header-filter:hover .svg-icon,
.header-filter.active svg {
transition: all .5s;
@ -355,11 +357,6 @@ body.all-styles-hidden-by-filters #installed:after {
fill: var(--entry-text-hover);
}
.header-filter.active span:before {
content: '▲';
color: var(--entry-text-hover);
}
.targets {
flex-wrap: wrap;
}
@ -473,7 +470,8 @@ a svg, .svg-icon.sort {
}
/* Checkbox */
.checkmate input:checked + svg.checkbox .filled-circle {
.checkmate input:checked + svg.checkbox .filled-circle,
svg.checkbox-enabled .filled-circle {
fill: var(--checkbox-bg-color);
display: block;
}
@ -486,7 +484,8 @@ a svg, .svg-icon.sort {
display: none;
}
.checkmate input:checked + svg.checkbox .checkmark {
.checkmate input:checked + svg.checkbox .checkmark,
svg.checkbox-enabled .checkmark {
fill: var(--checkbox-icon-color);
}
@ -693,6 +692,9 @@ a svg, .svg-icon.sort {
height: var(--toolbar-height);
padding: 4px 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
}
#tools-wrapper:not(.hidden) + #installed {
@ -702,20 +704,85 @@ a svg, .svg-icon.sort {
top: calc(var(--header-height) + var(--toolbar-height));
}
#tools-wrapper .select-resizer {
background: #fff;
}
.manage-row {
padding: 2px 18px;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-end;
width: 50%;
}
#filters-wrapper,
#bulk-filter-count {
display: flex;
align-items: center;
margin-right: 20px;
padding-left: 5px;
}
#filters-wrapper input {
display: none;
}
#filters-wrapper svg {
height: 16px;
}
.manage-row label,
.manage-row button,
.manage-row select {
min-width: 2em;
height: 24px;
max-height: 24px;
line-height: 24px;
padding: 0 6px;
margin: 0 2px;
vertical-align: middle;
align-items: center;
display: inline-flex;
flex-shrink: 0;
cursor: pointer;
}
#filters-wrapper label:not(.checked):hover {
background-color: var(--tools-bg-hover);
}
.manage-row select {
padding-right: 20px;
}
.search-filter .svg-icon,
.search-filter span {
pointer-events: none;
fill: currentColor;
}
.button-group {
display: inline-flex;
}
.button-group label:first-child {
margin-right: 0;
border-right-width: 1px;
}
.button-group label:last-child {
margin-left: 0;
border-left-width: 0;
}
.manage-row button,
.search-filter {
background: #e0e1e2;
border: 1px #9e9e9e solid;
}
.search-filter.checked,
.search-filter.checked:hover {
background-color: var(--label-usercss-bg-color);
color: #fff;
fill: #fff;
}
#search-wrapper {
@ -723,7 +790,8 @@ a svg, .svg-icon.sort {
}
#search {
width: calc(100% - var(--entry-icon-size) * 1.4);
width: 100%;
height: 2.2em;
}
#search-help {
@ -751,7 +819,7 @@ a svg, .svg-icon.sort {
display: none !important;
}
.active #filters-stats,
#filters-stats,
#bulk-filter-count:not(:empty) {
background-color: var(--checked-count-bg-color);
border-color: var(--checked-count-bg-color);

View File

@ -1,6 +1,6 @@
/* global messageBox UI filtersSelector filterAndAppend
/* global messageBox UI handleEvent filtersSelector filterAndAppend
sorter $ $$ $create API onDOMready scrollElementIntoView t chromeLocal */
/* exported handleUpdateInstalled */
/* exported handleUpdateInstalled resetUpdates */
'use strict';
let updateTimer;
@ -27,6 +27,15 @@ function applyUpdateAll() {
});
}
function resetUpdates() {
$('#check-all-updates-force').classList.add('hidden');
$('#apply-all-updates').classList.add('hidden');
$('#update-history').classList.add('hidden');
const checkbox = $('#only-updates');
checkbox.checked = false;
checkbox.parentElement.classList.add('hidden');
}
function checkUpdateBulk() {
clearTimeout(updateTimer);
@ -120,6 +129,7 @@ function checkUpdate(entry, {single} = {}) {
function reportUpdateState({updated, style, error, STATES}) {
const isCheckAll = document.body.classList.contains('update-in-progress');
const entry = $(UI.ENTRY_ID_PREFIX + style.id);
if (!entry) return;
const newClasses = new Map([
/*
When a style is updated/installed, handleUpdateInstalled() clears "updatable"
@ -138,7 +148,10 @@ function reportUpdateState({updated, style, error, STATES}) {
if (updated) {
newClasses.set('can-update', true);
entry.updatedCode = style;
$('#only-updates').classList.remove('hidden');
const onlyUpdates = $('#only-updates');
onlyUpdates.parentElement.classList.remove('hidden');
onlyUpdates.checked = true;
onlyUpdates.change();
} else if (!entry.classList.contains('can-update')) {
const same = (
error === STATES.SAME_MD5 ||
@ -202,16 +215,15 @@ function reportUpdateState({updated, style, error, STATES}) {
}
}
function renderUpdatesOnlyFilter({show, check} = {}) {
const numUpdatable = $$('.can-update').length;
const mightUpdate = numUpdatable > 0 || $('.update-problem');
const checkbox = $('#only-updates input');
const checkbox = $('#only-updates');
show = show !== undefined ? show : mightUpdate;
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
$('#only-updates').classList.toggle('hidden', !show);
checkbox.checked = check && show;
checkbox.checked = check;
checkbox.parentElement.classList.toggle('hidden', !show);
checkbox.dispatchEvent(new Event('change'));
const btnApply = $('#apply-all-updates');