Compare commits

...

67 Commits

Author SHA1 Message Date
Rob Garrison
5047ceb1fa Switch to filter text/buttons 2020-03-23 07:25:50 -05:00
Rob Garrison
2fd08cb041 Restore options popup 2020-03-11 22:10:31 -05:00
Rob Garrison
67e886bd45 Fix msgbox js error 2020-03-11 22:06:05 -05:00
Rob Garrison
9224c7ab6c Cleanup manage styles 2020-03-11 22:05:25 -05:00
Rob Garrison
48b1877b44 Tweaks 2020-03-10 20:26:44 -05:00
Rob Garrison
89644d0624 Replace home icon with external link icon 2020-03-10 19:37:55 -05:00
Rob Garrison
84163ba64f Change layout in lower resolutions 2020-03-10 19:37:55 -05:00
Rob Garrison
cd309aae03 Prevent JS error in Chrome while emulating mobile devices 2020-03-10 19:34:46 -05:00
Rob Garrison
6c747faccf Change new style into a dropdown 2020-03-10 19:33:15 -05:00
Rob Garrison
0dfd6680f8 Fix a few style issues 2020-03-10 19:33:14 -05:00
Rob Garrison
71baf445e0 Remove injection order code (for now) 2020-03-10 19:33:13 -05:00
Rob Garrison
7d7e7e9f72 Stop using detail summary as applies to expander 2020-03-10 19:33:13 -05:00
Rob Garrison
f9ccd3eeee Increase spacing between header icons 2020-03-10 19:33:12 -05:00
Rob Garrison
44df29613f Change favicon option to show by default 2020-03-10 19:33:12 -05:00
Rob Garrison
67682fa856 Fix favicon size 2020-03-10 19:33:12 -05:00
Rob Garrison
bd117b8fd7 Build & save injection order values 2020-03-10 19:33:11 -05:00
Rob Garrison
1bc84bbb49 Fix header backup dropdown position 2020-03-10 19:33:11 -05:00
Rob Garrison
5cc9e68d69 Increase tooltip contrast 2020-03-10 19:33:10 -05:00
Rob Garrison
99f8bbb48f Brighten checked checkbox color 2020-03-10 19:33:10 -05:00
Rob Garrison
3042054287 Fix entry height variable 2020-03-10 19:33:10 -05:00
Rob Garrison
67593416fb Fix hover highlight 2020-03-10 19:33:10 -05:00
Rob Garrison
ebeaba3478 Make entry name full height 2020-03-10 19:33:09 -05:00
Rob Garrison
ae6bace200 Reduce favicon size 2020-03-10 19:33:09 -05:00
Rob Garrison
7abc4f7fe6 Add export & update all buttons to header 2020-03-10 19:33:08 -05:00
Rob Garrison
30a780a44e Show a default of 6 applies to favicons 2020-03-10 19:32:29 -05:00
Rob Garrison
e9510c01b7 Only allow manual sort with ID column sort 2020-03-10 19:32:29 -05:00
Rob Garrison
9b408aaad4 Fix checked icon size 2020-03-10 19:32:28 -05:00
Rob Garrison
0d87689078 Make entire style name clickable 2020-03-10 19:32:28 -05:00
Rob Garrison
bc3f2e0fcf Add more css variables 2020-03-10 19:32:27 -05:00
Rob Garrison
e873ffd84e Move home & support buttons to the right 2020-03-10 19:32:27 -05:00
Rob Garrison
c966cfe17e Change active dragging color 2020-03-10 19:32:27 -05:00
Rob Garrison
e547d93fdc Only open bulk action with filter checkbox 2020-03-10 19:32:26 -05:00
Rob Garrison
9fd4e0f57d Fix styling for width < 1100px 2020-03-10 19:32:26 -05:00
Rob Garrison
b5387deb9a Fix label updates 2020-03-10 19:31:16 -05:00
Rob Garrison
403049692c Fix bulk updates 2020-03-10 19:31:16 -05:00
Rob Garrison
a0ba63bb19 Remove bulk reset 2020-03-10 19:31:15 -05:00
Rob Garrison
ead5e747b5 Fix import dropdown 2020-03-10 19:31:14 -05:00
Rob Garrison
44889f6158 Fix bulk export 2020-03-10 19:29:30 -05:00
Rob Garrison
a7026bdeee Wire up some bulk actions 2020-03-10 19:26:18 -05:00
Rob Garrison
30a69f5bea Fix bulk import 2020-03-10 19:25:02 -05:00
Rob Garrison
198315c626 Change default sort 2020-03-10 19:24:18 -05:00
Rob Garrison
218b6b41ec Add applies to config modal 2020-03-10 19:24:17 -05:00
Rob Garrison
42a75780d5 Fix style header localization 2020-03-10 19:23:08 -05:00
Rob Garrison
596d6a9ca9 Ditch tabs & add header icons 2020-03-10 19:23:07 -05:00
Rob Garrison
8f87494dec Add filter toolbar indicator 2020-03-10 19:23:07 -05:00
Rob Garrison
d2930e5e66 Add back update history button 2020-03-10 19:23:06 -05:00
Rob Garrison
331be7aa2b Add css tooltips 2020-03-10 19:23:06 -05:00
Rob Garrison
bc404e821f Fix enabled update when toggled from popup 2020-03-10 19:23:05 -05:00
Rob Garrison
529172de5b Fix tabbing to actions 2020-03-10 19:23:05 -05:00
Rob Garrison
cf4d4a2e91 Use semverCompare on version column 2020-03-10 19:23:05 -05:00
Rob Garrison
de7b0f44f1 WIP: sort injection order 2020-03-10 19:23:04 -05:00
Rob Garrison
e3be7bf18f Fix bulk actions markup 2020-03-10 19:23:04 -05:00
Rob Garrison
57c55896e8 Change header ID
Fix issue with StylusDeepDark
2020-03-10 19:23:04 -05:00
Rob Garrison
9a314523f6 Modify localization to allow including parameters in HTML
E.g i18n-title=key;param (single param only)
2020-03-10 19:23:03 -05:00
Rob Garrison
d379e5f34a Add multisort to header & labels 2020-03-10 19:23:03 -05:00
Rob Garrison
9368c27990 Cleanup updateDate code 2020-03-10 19:23:02 -05:00
Rob Garrison
52f012daf5 Fix action icon wrapping 2020-03-10 19:23:02 -05:00
Rob Garrison
0e7ff1c78f Add busy icon 2020-03-10 19:23:02 -05:00
Rob Garrison
7cd84038bf Update labels on style toggle 2020-03-10 19:23:02 -05:00
Rob Garrison
cf695c73d6 Cleanup UI 2020-03-10 19:23:01 -05:00
Rob Garrison
17e1860ba6 Wire up bulk action toggle 2020-03-10 19:23:01 -05:00
Rob Garrison
a1b78476bb Move style header & remove template 2020-03-10 19:23:00 -05:00
Rob Garrison
f57af7929f Fix filter, search & add bulk ui html 2020-03-10 19:23:00 -05:00
Rob Garrison
fbcc7aac08 Add bulk action panel 2020-03-10 19:22:59 -05:00
Rob Garrison
5c38441393 Delay loading of non-essential css/js 2020-03-10 19:22:58 -05:00
Rob Garrison
ed07cb8460 Make entries draggable 2020-03-10 19:22:57 -05:00
Rob Garrison
4475a8ad6a new manage layout 2020-03-10 19:22:56 -05:00
24 changed files with 3138 additions and 2286 deletions

View File

@ -3,6 +3,14 @@
"message": "Write new style",
"description": "Label for the button to go to the add style page"
},
"addPlainStyleLabel": {
"message": "New plain style",
"description": "Label for the button to go to the add style page"
},
"addUserCSSStyleLabel": {
"message": "New UserCSS style",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "Add Style",
"description": "Title of the page for adding styles"
@ -24,10 +32,6 @@
},
"description": "Text on the manage screen to describe what the style applies to"
},
"appliesDisplayTruncatedSuffix": {
"message": "and more",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "URLs on the domain",
"description": "Option to make the style apply to the entered string as a domain"
@ -88,12 +92,17 @@
"message": "Backup",
"description": "Heading for backup"
},
"backupImport": {
"message": "Import backup",
"description": "Tooltip for header import/restore backup icon"
},
"backupMessage": {
"message": "Select a file or drag and drop to this page.",
"description": "Message for backup"
},
"bckpInstStyles": {
"message": "Export styles"
"message": "Local device",
"description": "Selected option to backup indicated styles to local device/drive"
},
"checkAllUpdates": {
"message": "Check all styles for updates",
@ -335,7 +344,11 @@
},
"exportLabel": {
"message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
"description": "Label for the button to export a style ('edit' page) or bulk styles ('manage' page)"
},
"exportAllLabel": {
"message": "Export All",
"description": "Label for the button to export all styles ('manage' page)"
},
"externalFeedback": {
"message": "Feedback",
@ -405,6 +418,10 @@
"message": "Enabled",
"description": "Used in various lists/options to indicate that something is enabled"
},
"genericFilterLabel": {
"message": "Filter",
"description": "Used in various lists/options to indicate that something is or will be filtered"
},
"genericError": {
"message": "Error",
"description": "Used in various places to indicate some error occurred."
@ -413,6 +430,10 @@
"message": "History",
"description": "Used in various places to show a history log of something"
},
"genericName": {
"message": "Name",
"description": "Used in various places to indicate the style name"
},
"genericNext": {
"message": "Next",
"description": "Used in various places to select/perform the next step/action"
@ -554,6 +575,10 @@
"message": "Get help",
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
},
"linkChat": {
"message": "Chat with us",
"description": "Link to open a new browser tab to chat with users & developers on Discord"
},
"linkGetStyles": {
"message": "Get styles",
"description": "Help link text on the manage page e.g. https://userstyles.org"
@ -1164,7 +1189,8 @@
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"retrieveBckp": {
"message": "Import styles"
"message": "Local source",
"description": "Selected option to get a backup of styles from a local device/drive"
},
"search": {
"message": "Search",
@ -1214,6 +1240,26 @@
"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\">",
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page"
},
"bulkActions": {
"message": "Apply actions to selected styles",
"description": "Label for bulk actions select dropdown"
},
"bulkActionsSelect": {
"message": "Choose a bulk action",
"description": "Placeholder text in dropdown to tell the user to choose an action to apply to all selected styles"
},
"bulkActionsApply": {
"message": "Apply",
"description": "Text for button to apply the selected action"
},
"bulkActionsTooltip": {
"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",
"description": "Error displayed in a tooltip when the user attempts to apply an action with no styles selected"
},
"sectionAdd": {
"message": "Add another section",
"description": "Label for the button to add a section"
@ -1237,33 +1283,34 @@
"shortcutsNote": {
"message": "Define keyboard shortcuts"
},
"sortDateNewestFirst": {
"message": "newest first",
"description": "Text added to indicate that sorting a date would add the newest entries at the top"
"sortColumnEnabled": {
"message": "enabled styles",
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
},
"sortDateOldestFirst": {
"message": "oldest first",
"description": "Text added to indicate that sorting a date would add the oldest entries at the top"
"sortColumnName": {
"message": "style name",
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
},
"sortColumnVersion": {
"message": "style version",
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
},
"sortColumnLastUpdate": {
"message": "last updated",
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
},
"sortHeader": {
"message": "Sort",
"description": "Title of sort column, indicating that the style can be manually sorted by dragging & dropping"
},
"sortLabel": {
"message": "Select a sort to apply to the installed styles",
"description": "Title on the sort select to indicate it is used for sorting entries"
"message": "Click to sort the \"$name$\" column;\nUse shift + click to sort multiple columns",
"placeholders": {
"name": {
"content": "$1"
}
},
"sortLabelTitleAsc": {
"message": "Title Ascending",
"description": "Text added to option group to indicate a block of options that apply a title ascending (A to Z) sort"
},
"sortLabelTitleDesc": {
"message": "Title Descending",
"description": "Text added to option group to indicate a block of options that apply a title descending (Z to A) sort"
},
"sortStylesHelp": {
"message": "Choose the type of sort to apply to the installed entries from within the sort dropdown. The default setting applies an ascending sort (A to Z) to the entry titles. Sorts within the \"Title Descending\" group will apply a descending sort (Z to A) to the title.\nThere are other presets that will allow sorting the entries by multiple criteria. Think of this like sorting a table with multiple columns and each category in a select (between the plus signs) represents a column, or group.\nFor example, if the setting is \"Enabled (first) + Title\", the entries would sort so that all the enabled entries are sorted to the top of the list, then an entry title ascending sort (A to Z) is applied to both the enabled and disabled entries separately.",
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the sort input field on the Manage styles page"
},
"sortStylesHelpTitle": {
"message": "Sort contents",
"description": "Label for the sort info popup on the Manage styles page"
"description": "Title added to links in the manager page header to inform the user on how to sort the columns"
},
"styleBadRegexp": {
"message": "Regexp is invalid.",

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

@ -31,7 +31,7 @@
const retrying = new Set();
API_METHODS.updateCheckAll = checkAllStyles;
API_METHODS.updateCheckBulk = checkBulkStyles;
API_METHODS.updateCheck = checkStyle;
API_METHODS.getUpdaterStates = () => STATES;
@ -39,18 +39,22 @@
schedule();
chrome.alarms.onAlarm.addListener(onAlarm);
return {checkAllStyles, checkStyle, STATES};
return {checkBulkStyles, checkStyle, STATES};
function checkAllStyles({
function checkBulkStyles({
save = true,
ignoreDigest,
observe,
styleIds = [],
} = {}) {
resetInterval();
checkingAll = true;
retrying.clear();
const port = observe && chrome.runtime.connect({name: 'updater'});
return styleManager.getAllStyles().then(styles => {
if (styleIds.length) {
styles = styles.filter(style => styleIds.includes(style.id));
}
styles = styles.filter(style => style.updateUrl);
if (port) port.postMessage({count: styles.length});
log('');
@ -247,7 +251,7 @@
}
function onAlarm({name}) {
if (name === ALARM_NAME) checkAllStyles();
if (name === ALARM_NAME) checkBulkStyles();
}
function resetInterval() {

View File

@ -136,6 +136,7 @@ select {
color: #000;
background-color: transparent;
border: 1px solid hsl(0, 0%, 66%);
border-radius: 2px;
padding: 0 20px 0 6px;
transition: color .5s;
}

View File

@ -7,6 +7,11 @@ tDocLoader();
function t(key, params) {
if (!params && key.includes(';')) {
[key, params] = key.split(';');
// sometimes a param like "usercss" is passed; not defined in messages.json
params = params ? chrome.i18n.getMessage(params) || params : '';
}
const cache = !params && t.cache[key];
const s = cache || chrome.i18n.getMessage(key, params);
if (s === '') {

View File

@ -18,9 +18,9 @@ if (!CHROME && !chrome.browserAction.openPopup) {
// in FF pre-57 legacy addons can override useragent so we assume the worst
// until we know for sure in the async getBrowserInfo()
// (browserAction.openPopup was added in 57)
FIREFOX = browser.runtime.getBrowserInfo ? 51 : 50;
FIREFOX = browserApi.runtime.getBrowserInfo ? 51 : 50;
// getBrowserInfo was added in FF 51
Promise.resolve(FIREFOX >= 51 ? browser.runtime.getBrowserInfo() : {version: 50}).then(info => {
Promise.resolve(FIREFOX >= 51 ? browserApi.runtime.getBrowserInfo() : {version: 50}).then(info => {
FIREFOX = parseFloat(info.version);
document.documentElement.classList.add('moz-appearance-bug', FIREFOX && FIREFOX < 54);
});

View File

@ -27,15 +27,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
'manage.onlyEnabled.invert': false, // display only disabled styles
'manage.onlyLocal.invert': false, // display only externally installed styles
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
// UI element state: expanded/collapsed
'manage.backup.expanded': true,
'manage.filters.expanded': true,
'manage.options.expanded': true,
// the new compact layout doesn't look good on Android yet
'manage.newUI': !navigator.appVersion.includes('Android'),
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
'manage.export.destination': 'local', // default export destination (local or dropbox)
'manage.newUI.favicons': true, // show favicons for the sites in applies-to
'manage.newUI.faviconsGray': true, // gray out favicons
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
'manage.newUI.targets': 6, // max number of applies-to targets visible: 0 = none
'manage.newUI.sort': 'title,asc',
'editor.options': {}, // CodeMirror.defaults.*

View File

@ -1,4 +1,4 @@
<html id="stylus">
<html id="stylus" class="newUI">
<head>
<meta charset="UTF-8">
@ -6,12 +6,11 @@
<title i18n-text="manageTitle"></title>
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="manage/manage.css">
<link rel="stylesheet" href="manage/config-dialog.css">
<link rel="stylesheet" href="msgbox/msgbox.css">
<link rel="stylesheet" href="options/onoffswitch.css">
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css">
<style id="style-overrides"></style>
<link rel="stylesheet" data-href="manage/config-dialog.css">
<link rel="stylesheet" data-href="msgbox/msgbox.css">
<link rel="stylesheet" data-href="options/onoffswitch.css">
<link rel="stylesheet" data-href="vendor-overwrites/colorpicker/colorpicker.css">
<link rel="stylesheet" data-href="manage/tooltips.css">
<style id="firefox-transitions-bug-suppressor">
/* restrict to FF */
@ -30,77 +29,96 @@
-->
<template data-id="style">
<div class="entry">
<h2 class="style-name">
<a class="style-name-link"></a>
<a target="_blank" class="homepage"></a>
</h2>
<p class="applies-to">
<label i18n-text="appliesDisplay"></label>
<span class="targets"></span>
</p>
<p class="actions">
<a class="style-edit-link">
<button i18n-text="editStyleLabel" tabindex="-1"></button>
<div class="entry hide-extra">
<label class="entry-col entry-filter checkmate" tabindex="0">
<span class="col-label" i18n-text="genericFilterLabel"></span>
<input class="entry-filter-toggle" type="checkbox">
<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>
<label class="entry-col entry-state checkmate" tabindex="0">
<span class="col-label" i18n-text="genericEnabledLabel"></span>
<input class="entry-state-toggle" type="checkbox">
<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>
<a href="#" class="entry-col entry-name">
<span class="col-label" i18n-text="genericName"></span>
<span class="entry-name-text"></span>
<span class="entry-labels"></span>
</a>
<button class="enable" i18n-text="enableStyleLabel"></button>
<button class="disable" i18n-text="disableStyleLabel"></button>
<button class="delete" i18n-text="deleteStyleLabel"></button>
<button class="check-update" i18n-text="checkForUpdate"></button>
<button class="update" i18n-text="installUpdate"></button>
<button class="configure-usercss" i18n-text="configureStyle"></button>
<span class="update-note"></span>
</p>
</div>
</template>
<template data-id="styleCompact">
<div class="entry">
<h2 class="style-name">
<div class="checkmate">
<input class="checker" type="checkbox" i18n-title="toggleStyle">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
<a class="style-name-link"></a>
</h2>
<p class="actions">
<a target="_blank" class="homepage" tabindex="0"></a>
<a href="#" class="delete" i18n-title="deleteStyleLabel" tabindex="0">
<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 "/>
<div class="entry-col entry-actions">
<span class="col-label" i18n-text="optionsActions"></span>
<a href="#" class="entry-configure-usercss tt-e" i18n-data-title="configureStyle">
<svg class="svg-icon entry-config" viewBox="0 0 24 24">
<path d="M19.43 12.98a7.8 7.8 0 0 0 0-1.96l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5
0 0 0-.61-.22l-2.49 1a7.3 7.3 0 0 0-1.69-.98l-.38-2.65A.49.49 0 0 0 14 2h-4a.49.49
0 0 0-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.57.57 0 0 0-.18-.03.5.5 0 0
0-.43.25l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65a7.93 7.93 0 0 0 0 1.96l-2.11 1.65a.5.5
0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38
2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65a7.68 7.68 0 0 0 1.69-.98l2.49
1 .18.03a.5.5 0 0 0 .43-.25l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65zm-1.98-1.71a5.34
5.34 0 0 1 0 1.46l-.14 1.13.89.7 1.08.84-.7
1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2
1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43a5.67 5.67 0 0
1-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21
1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21
1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16
1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14
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"/>
</svg>
</a>
</p>
<div class="applies-to">
<a href="#" class="entry-edit tt-e" i18n-data-title="editStyleLabel">
<svg class="svg-icon edit" viewBox="0 0 24 24">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM5.92 19H5v-.92l9.06-9.06.92.92L5.92
19zM20.71 5.63l-2.34-2.34c-.2-.2-.45-.29-.71-.29s-.51.1-.7.29l-1.83 1.83 3.75 3.75
1.83-1.83a1 1 0 0 0 0-1.41z"/>
</svg>
</a>
<a href="#" class="entry-delete tt-e" i18n-data-title="deleteStyleLabel">
<svg class="svg-icon remove" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
</a>
<span class="entry-updater-placeholder"></span>
<a href="#" class="entry-homepage tt-w">
<svg class="svg-icon home" viewBox="0 0 24 24">
<path d="M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
</svg>
</a>
<a href="#" class="entry-support tt-w">
<svg class="svg-icon help" viewBox="0 0 24 24">
<path d="M11 18h2v-2h-2v2zm1-16a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1
0-16 8.01 8.01 0 0 1 0 16zm0-14a4 4 0 0 0-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3
5h2c0-2.25 3-2.5 3-5a4 4 0 0 0-4-4z"/>
</svg>
</a>
</div>
<div class="entry-col entry-version">
<span class="col-label">v#</span>
<span class="entry-version-value"></span>
</div>
<div class="entry-col entry-last-update tt-w">
<span class="col-label" i18n-text="searchResultUpdated"></span>
<span class="entry-last-update-value"></span>
</div>
<div class="entry-col entry-applies-to">
<span class="col-label" i18n-text="appliesLabel"></span>
<div class="targets"></div>
<a href="#" class="expander" tabindex="0">...</a>
</div>
</div>
</template>
<template data-id="homepageIconBig">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon shape-rendering="crispEdges" points="3,3 3,17 17,17 17,13 15,13 15,15 5,15 5,5 7,5 7,3 "/>
<polygon points="10,3 12.5,5.5 8,10 10,12 14.5,7.5 17,10 17,3 "/>
</svg>
</template>
<template data-id="homepageIconSmall">
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
</svg>
</template>
<template data-id="configureIcon">
<a href="#" class="configure-usercss" i18n-title="configureStyle" tabindex="0">
<svg class="svg-icon config"><use xlink:href="#svg-icon-config"></use></svg>
</a>
</template>
<template data-id="updaterIcons">
<span class="updater-icons">
<a href="#" class="check-update" i18n-title="checkForUpdate" tabindex="0">
<span class="updater-icons entry-col entry-update-state">
<a href="#" class="check-update tt-e" i18n-data-title="checkForUpdate" tabindex="0">
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M18,16.6l-3.1-3.1c0.5-0.7,0.9-1.5,1-2.5h-2.1c-0.4,1.7-2,3-3.9,3c-0.8,0-1.6-0.3-2.3-0.7
L10,11H6.1H4.1H4v6l2.3-2.3c1,0.8,2.3,1.3,3.7,1.3c1.3,0,2.5-0.4,3.5-1.1l3.1,3.1L18,16.6z"/>
@ -108,42 +126,46 @@
C7,4,4.6,6.2,4.1,9h2.1C6.6,7.3,8.1,6,10,6z"/>
</svg>
</a>
<a href="#" class="update" i18n-title="installUpdate" tabindex="0">
<a href="#" class="update tt-e" i18n-data-title="installUpdate" tabindex="0">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="16,8 12,8 12,3 8,3 8,8 4,8 10,14 "/>
<rect shape-rendering="crispEdges" x="4" y="15" width="12" height="2"/>
</svg>
</a>
<span class="up-to-date" i18n-title="updateCheckSucceededNoUpdate">
<span class="up-to-date tt-e" i18n-data-title="updateCheckSucceededNoUpdate">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
</svg>
</span>
<span class="updated" i18n-title="updateCompleted">
<span class="updated tt-e" i18n-data-title="updateCompleted">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
</svg>
</span>
<span class="update-note"></span>
</span>
</template>
<template data-id="appliesToTarget">
<span class="target"></span>
</template>
<template data-id="appliesToSeparator">
<span class="sep">, </span>
<span class="target tt-w">
<img async="true">
<svg class="svg-icon" viewBox="0 0 24 24">
<path fill="#666" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm7 6h-3l-1-4 4 4zm-7-4l2 4h-4l2-4zM4 14a8 8 0 0 1 0-4h4a17 17 0 0 0 0 4H4zm1 2h3l1 4-4-4zm3-8H5l4-4-1 4zm4 12l-2-4h4l-2 4zm2-6h-4a15 15 0 0 1 0-4h4a15 15 0 0 1 0 4zm1 6l1-4h3l-4 4zm1-6a17 17 0 0 0 0-4h4a8 8 0 0 1 0 4h-4z"/>
</svg>
</span>
</template>
<template data-id="appliesToEverything">
<span class="target" i18n-text="appliesToEverything"></span>
<span class="target tt-w" i18n-data-title="appliesToEverything">
<svg class="svg-icon world" viewBox="0 0 24 24">
<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM4 12c0-.61.08-1.21.21-1.78L8.99 15v1c0
1.1.9 2 2 2v1.93A8.01 8.01 0 0 1 4 12zm13.89 5.4a2 2 0 0 0-1.9-1.4h-1v-3a1 1 0 0
0-1-1h-6v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a8 8 0 0 1 2.9 12.81z"/>
</svg>
</span>
</template>
<template data-id="extraAppliesTo">
<details class="applies-to-extra">
<summary class="applies-to-extra-expander" i18n-text="appliesDisplayTruncatedSuffix"></summary>
</details>
<a href="#" class="applies-to-extra-expander" tabindex="0">...</a>
</template>
<script src="js/polyfill.js"></script>
@ -158,273 +180,423 @@
<script src="js/localization.js"></script>
<script src="manage/filters.js"></script>
<script src="manage/sort.js"></script>
<script src="manage/manage.js"></script>
<script src="vendor/semver-bundle/semver.js"></script>
<script src="manage/manage-ui.js"></script>
<script src="manage/manage-actions.js"></script>
<script src="manage/bulk-actions.js"></script>
<script src="vendor-overwrites/colorpicker/colorconverter.js"></script>
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
<script src="manage/config-dialog.js"></script>
<script data-src="vendor-overwrites/colorpicker/colorconverter.js"></script>
<script data-src="vendor-overwrites/colorpicker/colorpicker.js"></script>
<script data-src="manage/config-dialog.js"></script>
<script src="manage/updater-ui.js"></script>
<script src="manage/object-diff.js"></script>
<script src="manage/import-export.js"></script>
<script src="manage/incremental-search.js"></script>
<script src="msgbox/msgbox.js"></script>
<script data-src="manage/object-diff.js"></script>
<script data-src="manage/import-export.js"></script>
<script data-src="manage/incremental-search.js"></script>
<script data-src="msgbox/msgbox.js"></script>
<script src="js/sections-util.js"></script>
<script src="js/storage-util.js"></script>
</head>
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
<div id="header">
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
<div id="manage-settings">
<div class="settings-column">
<details id="filters" data-pref="manage.filters.expanded">
<summary>
<h2 i18n-text="manageFilters">:
<div class="filter-stats-wrapper">
<h1 id="main-header">
<div>
<svg id="stylus-logo" width="32" height="32" viewBox="0 0 48 48">
<path fill="#285959" d="M44.59 20.14c-.02-.86 0-13.6 0-13.6-.02-4.13-2.5-5.62-6.6-5.64H11.28C7.17.92
4.59 2.44 4.65 6.54v14.2c0 1.64-3.02 1.55-3.02 1.55v3.48s3.05.03 3.02 1.97v13.88c0 4.15 2.5 5.63
6.63 5.63h26.71c4.13.02 6.6-1.45 6.6-5.63V27.35c0-1.54 2.98-1.57 2.98-1.57l.04-3.84s-3-.07-3.02-1.8z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#28FEFE" d="M46 20.98V5.62A5.72 5.72 0 0 0
40.22.03H8.66a5.63 5.63 0 0 0-5.68 5.59v15.37H0v6h2.98v15.4a5.67 5.67 0 0 0 5.68 5.64h31.56A5.76
5.76 0 0 0 46 42.39V26.94h2.03v-5.98l-2.03.02zm-2 5.65v13.76c0 4.19-2.43 5.65-6.56 5.64H10.66c-4.13
0-5.69-1.48-5.69-5.64V27.1c.04-1.94-2.93-1.97-2.93-1.97v-2.21s2.93.09 2.93-1.54c.05-.52 0-13.84
0-13.84-.05-4.1 1.57-5.57 5.68-5.59h26.79c4.1.02 6.54 1.46 6.56 5.59 0 0 .08 12.87 0 13.4.02 1.73
1.98 1.94 1.98 1.94v2.19S44 25.09 44 26.63zm-16.76-6.2c-4.56-1.71-6.47-2.7-6.47-4.92 0-1.77 1.65-3.37
5.07-3.37 3.37 0 5.9.98 7.26 1.66l1.76-6.33c-2.08-.98-4.92-1.76-8.91-1.76-8.19 0-13.22 4.51-13.22
10.47 0 5.08 3.84 8.29 9.64 10.36 4.2 1.45 5.86 2.7 5.86 4.87 0 2.28-1.92 3.78-5.55 3.78-3.37
0-6.68-1.09-8.75-2.17l-1.61 6.47c1.97 1.1 5.9 2.18 9.9 2.18 9.58 0 14.04-4.98 14.04-10.83
0-4.92-2.85-8.13-9.02-10.41z"/>
</svg>
<span class="ext-name">Stylus</span>
<span class="ext-version"></span>
<span class="filter-stats-wrapper">
<span id="filters-stats"></span>
<a id="reset-filters" href="#" tabindex="0">
<svg class="svg-icon" viewBox="0 0 20 20">
<title i18n-text="genericResetLabel"></title>
<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 "/>
<a id="reset-filters" class="tt-e" href="#" tabindex="0" i18n-data-title="genericResetLabel">
<svg class="svg-icon" viewBox="0 0 20 20"><use xlink:href="#svg-icon-x"/></svg>
</a>
</span>
</div>
<div id="main-actions">
<div class="new-style tt-w">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0
16H5V5h14v14zm-8-2h2v-4h4v-2h-4V7h-2v4H7v2h4z"/>
</svg>
<div class="dropdown">
<a href="edit.html" id="add-usercss" i18n-text="addUserCSSStyleLabel"></a>
<a href="edit.html" id="add-reg-css" i18n-text="addPlainStyleLabel"></a>
</div>
</div>
<span class="spacer"></span>
<a href="#" id="manage-options-button" i18n-data-title="openOptions" class="tt-w">
<svg class="svg-icon ui-config" viewBox="0 0 24 24"><use xlink:href="#svg-icon-config"/></svg>
</a>
<a href="#" id="manage-shortcuts-button" class="chromium-only tt-w" i18n-data-title="shortcutsNote">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M20 7v10H4V7h16m0-2H4a2 2 0 0 0-1.99 2L2 17c0 1.1.9 2 2 2h16a2 2 0 0 0 2-2V7a2 2 0
0 0-2-2zm-9 3h2v2h-2zm0 3h2v2h-2zM8 8h2v2H8zm0 3h2v2H8zm-3 0h2v2H5zm0-3h2v2H5zm3
6h8v2H8zm6-3h2v2h-2zm0-3h2v2h-2zm3 3h2v2h-2zm0-3h2v2h-2z"/>
</svg>
</a>
<span class="spacer"></span>
<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"/>
</svg>
<div class="dropdown">
<a href="#" id="unfile-all-styles" i18n-text="bckpInstStyles"></a>
<a href="#" id="sync-dropbox-import" i18n-text="syncDropboxStyles"></a>
</div>
</div>
<div class="manage-backups tt-w" i18n-data-title="exportAllLabel">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M20.5 5.2l-1.4-1.6C19 3.2 18.5 3 18 3H6c-.5 0-.9.2-1.2.6L3.5 5.2A2 2 0 0 0 3 6.5V19c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.2-1-.5-1.3zM6.2 5h11.6l.8 1H5.4l.8-1zM5 19V8h14v11H5zm3-5h2.5v3h3v-3H16l-4-4z"/>
</svg>
<div class="dropdown">
<a href="#" id="file-all-styles" i18n-text="bckpInstStyles"></a>
<a href="#" id="sync-dropbox-export" i18n-text="syncDropboxStyles"></a>
</div>
</div>
<span class="spacer"></span>
<a href="https://add0n.com/stylus.html#features" target="_blank" i18n-data-title="linkGetHelp" class="tt-w">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M15 4v7H5.17L4 12.17V4h11m1-2H3a1 1 0 0 0-1 1v14l4-4h10a1 1 0 0 0 1-1V3a1 1 0 0
0-1-1zm5 4h-2v9H6v2a1 1 0 0 0 1 1h11l4 4V7a1 1 0 0 0-1-1z"/>
</svg>
</a>
<a href="https://discordapp.com/widget?id=379521691774353408" target="_blank" i18n-data-title="linkChat" class="tt-w">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M11 23.59v-3.6c-5.01-.26-9-4.42-9-9.49C2 5.26 6.26 1 11.5 1S21 5.26 21 10.5c0
4.95-3.44 9.93-8.57 12.4l-1.43.69zM11.5 3C7.36 3 4 6.36 4 10.5S7.36 18 11.5
18H13v2.3c3.64-2.3 6-6.08 6-9.8C19 6.36 15.64 3 11.5 3zm-1 11.5h2v2h-2zm2-1.5h-2c0-3.25
3-3 3-5 0-1.1-.9-2-2-2s-2 .9-2 2h-2c0-2.21 1.79-4 4-4s4 1.79 4 4c0 2.5-3 2.75-3 5z"/>
</svg>
</a>
<a href="https://github.com/openstyles/stylus/wiki" target="_blank" i18n-data-title="linkStylusWiki" class="tt-w">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M6 2c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6H6zm0
2h7v5h5v11H6V4zm2 8v2h8v-2H8zm0 4v2h8v-2H8z"/>
</svg>
</a>
<a href="https://www.transifex.com/github-7/Stylus" target="_blank" i18n-data-title="linkTranslate" class="tt-w">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M4 2a2 2 0 0 0-2 2v9c0 1.1.9 2 2 2h1v2l2 2h2v1c0 1.1.9 2 2 2h9a2 2 0 0 0 2-2v-9a2 2
0 0 0-2-2h-5V4a2 2 0 0 0-2-2zm0 2h9v5h-2c-.66 0-1.23.32-1.6.81-.15-.1-.3-.24-.43-.34C9.6
8.8 10.23 8 10.75 7H12V6H9V5H8v1H5v1h1.13a.5.5 0 0 0-.1.5s.17.5.69
1.19c.19.24.45.52.75.81-1.15.97-2.13 1.4-2.13 1.4a.5.5 0 0 0 .38.94s1.2-.48
2.53-1.65c.23.18.5.35.78.53-.01.1-.03.18-.03.28v2H4zm2.88 3h2.68A9.8 9.8 0 0 1 8.2
8.84a6.59 6.59 0 0 1-.69-.75c-.44-.57-.5-.87-.5-.87A.53.53 0 0 0 6.87 7zm7.96 5h1.32L19
20h-1.16l-.75-2.19h-3.25L13.13 20H12zm.6.9c-.13.48-1.28 4.1-1.28
4.1h2.65s-1.22-3.63-1.34-4.1zM7 15h2v2H7z"/>
</svg>
</a>
<a href="https://userstyles.org" target="_blank" i18n-data-title="linkGetStyles" class="tt-w">
<svg class="svg-icon" viewBox="0 0 24 24">
<path d="M2.53 19.65l1.34.56v-9.03l-2.43 5.86a2.02 2.02 0 0 0 1.09 2.61zm19.5-3.7L17.07
3.98a2.01 2.01 0 0 0-2.6-1.08L7.1 5.95a2 2 0 0 0-1.08 2.6l4.96 11.97a2 2 0 0 0 2.6
1.08l7.36-3.05a2 2 0 0 0 1.09-2.6zm-9.2 3.8L7.87 7.79l7.35-3.04h.01l4.95 11.95-7.35 3.05z"/>
<circle cx="11" cy="9" r="1"/>
<path d="M5.88 19.75c0 1.1.9 2 2 2h1.45l-3.45-8.34v6.34z"/>
</svg>
</a>
</div>
</h2>
</summary>
<div class="filter-selection">
<label>
<div class="checkmate">
<input id="manage.onlyEnabled" type="checkbox"
data-filter=".enabled"
data-filter-hide=".disabled">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</h1>
<div id="tools-wrapper">
<div id="bulk-actions" class="manage-row">
<label class="checkmate toggle-all" tabindex="0">
<input id="toggle-all-filters" type="checkbox">
<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"/>
<path class="indeterminate" d="M2.5 4.5h5v1h-5v-1z"/>
</svg>
</label>
<div class="select-resizer">
<select id="manage.onlyEnabled.invert">
<option i18n-text="manageOnlyEnabled" value="false"></option>
<option i18n-text="manageOnlyDisabled" value="true"></option>
<span id="bulk-filter-count"></span>
<span i18n-text="bulkActions"></span>
<span class="select-resizer bulk-actions-select-wrapper">
<select id="bulk-actions-select">
<option i18n-text="bulkActionsSelect" value=""></option>
<option i18n-text="enableStyleLabel" value="enable"></option>
<option i18n-text="disableStyleLabel" value="disable"></option>
<option i18n-text="exportLabel" value="export"></option>
<option i18n-text="checkForUpdate" value="update"></option>
<!-- Plan: Reset UserCSS variables -->
<!-- <option i18n-text="genericResetLabel" value="reset"></option> -->
<option i18n-text="deleteStyleLabel" value="delete"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
</span>
<button id="bulk-actions-apply" i18n-text="bulkActionsApply" class="tt-e" disabled>
<span id="update-progress"></span>
</button>
<div class="filter-selection">
<label>
<div class="checkmate">
<input id="manage.onlyLocal" type="checkbox"
data-filter=":not(.updatable):not(.update-done)"
data-filter-hide=".updatable, .update-done">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</label>
<div class="select-resizer">
<select id="manage.onlyLocal.invert" i18n-title="manageOnlyLocalTooltip">
<option i18n-text="manageOnlyLocal" value="false"></option>
<option i18n-text="manageOnlyExternal" value="true"></option>
<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">
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
<span id="update-all-no-updates" class="tt-e hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
<button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
</span>
<!-- Bulk export -->
<span data-bulk="export" class="dropdown export hidden">
Export to:
<span class="select-resizer">
<select id="manage.export.destination">
<option value="local" i18n-text="bckpInstStyles"></option>
<option value="dropbox" i18n-text="syncDropboxStyles"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</span>
</span>
</span>
<svg class="svg-icon busy hidden" viewBox="0 0 24 24">
<path d="M12 23h-1v-6.57A5.97 5.97 0 0 1 7 18c-3.25 0-6-2.75-6-6v-1h6.57A5.97 5.97 0 0 1 6
7c0-3.25 2.75-6 6-6h1v6.57A5.97 5.97 0 0 1 17 6c3.25 0 6 2.75 6 6v1h-6.57A5.97 5.97 0 0 1
18 17c0 3.25-2.75 6-6 6zm1-9.87v7.74c1.7-.46 3-2.04 3-3.87s-1.3-3.41-3-3.87zM3.13 13c.46
1.7 2.04 3 3.87 3s3.41-1.3 3.87-3H3.13zm10-2h7.74c-.46-1.7-2.05-3-3.87-3s-3.41 1.3-3.87
3zM11 3.13C9.3 3.59 8 5.18 8 7s1.3 3.41 3 3.87V3.13z"/>
<!-- supported everwhere?
<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 0 0"
to="360 0 0" dur="1s" repeatCount="indefinite"/>
-->
</svg>
</div>
<div class="filter-selection">
<label>
<div class="checkmate">
<input id="manage.onlyUsercss" type="checkbox"
data-filter=".usercss"
data-filter-hide=":not(.usercss)">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</label>
<div 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>
</div>
</div>
<label id="only-updates" class="hidden">
<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 checked"><use xlink:href="#svg-icon-checked"/></svg>
<span i18n-text="manageOnlyUpdates"></span>
</label>
<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">
<a href="#" id="search-help" tabindex="0">
</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>
</details>
<div id="sort-wrapper">
<div class="sorter-selection" i18n-title="sortLabel">
<select id="manage.newUI.sort"></select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<a href="#" id="sorter-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
<div id="style-actions">
<div id="update-check">
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
<a href="#" id="update-history" i18n-title="genericHistoryLabel" tabindex="0">
<svg class="svg-icon" viewBox="0 0 20 20" i18n-alt="helpAlt">
<path d="M13,7H7V6h6Zm6,6.5A5.5,5.5,0,0,1,8.61,16H4V3H16V8.61A5.5,5.5,0,0,1,19,13.5ZM8,14c0-.16,0-.84,0-1H7V12H8.21a5.46,5.46,0,0,1,.39-1H7V10H9.26a5.55,5.55,0,0,1,1.09-1H7V8h7V5H6v9Zm10-.5A4.5,4.5,0,1,0,13.5,18,4.5,4.5,0,0,0,18,13.5ZM14,13V10H13v4h4V13Z"/>
</svg>
</a>
</div>
<div id="update-all">
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
<button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
</div>
<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>
<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">
<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>
<a href="#" class="entry-col sortable header-state center-text tt-se" i18n-text="genericEnabledLabel" i18n-data-title="sortLabel;sortColumnEnabled" data-type="enabled">
<span></span>
</a>
<div class="entry-col header-name">
<a href="#" class="sortable tt-se" i18n-text="genericName" i18n-data-title="sortLabel;sortColumnName" data-type="title">
<span></span>
</a>
</div>
<div class="entry-col header-actions" i18n-text="optionsActions"></div>
<div class="entry-col header-version">
<a href="#" class="sortable tt-sw" i18n-data-title="sortLabel;sortColumnVersion" data-type="version">
v#<span></span>
</a>
</div>
<div class="entry-col header-last-update center-text">
<a href="#" class="sortable tt-sw" i18n-text="searchResultUpdated" i18n-data-title="sortLabel;sortColumnLastUpdate" data-type="dateUpdated">
<span></span>
</a>
<a href="#" id="update-history" class="tt-sw" i18n-data-title="genericHistoryLabel" tabindex="0">
<svg class="svg-icon update-history" viewBox="0 0 24 24" i18n-alt="updateCheckHistory">
<path d="M20.8 10.86a7 7 0 0 0-1.53-1.47V7.02L13.35 1H5.47c-1.08 0-1.96.9-1.96 2L3.5
19.05c0 1.1.88 2 1.96 2h5.94a6.86 6.86 0 0 0 8.2-.26 7.16 7.16 0 0 0 1.2-9.93zm-2.15
8.7a5.34 5.34 0 0 1-7.59-.96 5.53 5.53 0 0 1-1.1-4.05l-1.53-.2c-.2 1.6.14 3.26 1.05
4.7h-4V3h7.05l4.77 4.85v.59a6.84 6.84 0 0 0-6.26 1.2L9.62 7.78l-.52 4.3-.05.1 4.37.56L12
10.88a5.34 5.34 0 0 1 7.6.95 5.56 5.56 0 0 1-.94 7.72z"/>
<path d="M14.38 12.07V16l3.3 2 .56-.95-2.7-1.64v-3.34z"/>
</svg>
</a>
</div>
<div class="entry-col header-applies-to" i18n-text="appliesLabel">
<a href="#" id="applies-to-config" class="tt-sw" i18n-data-title="configureStyle" tabIndex="0">
<svg class="svg-icon applies-to-config" viewBox="0 0 24 24"><use xlink:href="#svg-icon-config"/></svg>
</a>
</div>
</header>
</div>
<!-- Applies to config, can't put this in a template because these inputs are bound to subscribed prefs -->
<div class="hidden">
<div id="appliesToConfig">
<label class="checkmate" tabindex="0">
<input id="manage.newUI.favicons" type="checkbox">
<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="manageFavicons"></span>
</label>
<div id="faviconsHelp" i18n-text="manageFaviconsHelp">
<p></p>
<label class="checkmate" tabindex="0">
<input id="manage.newUI.faviconsGray" type="checkbox">
<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="manageFaviconsGray"></span>
</label>
</div>
<label>
<input id="manage.newUI.targets" type="number" min="1" max="100" value="3">
<span i18n-text="manageMaxTargets"></span>
</label>
</div>
</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,6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>
<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,
6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,
6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,
15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>
</symbol>
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
<path fill-rule="evenodd" d="M1408 704q0 26-19 45l-448 448q-19 19-45 19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"/>
<path fill-rule="evenodd" d="M1408 704q0 26-19 45l-448 448q-19 19-45
19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"/>
</symbol>
<symbol id="svg-icon-help" viewBox="0 0 14 16">
<title i18n-text="helpAlt"></title>
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28
0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8
7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27
0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56
5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7
7-7-3.12-7-7 3.14-7 7-7z"></path>
</symbol>
<symbol id="svg-icon-config" viewBox="0 0 14 14">
<path d="M6.2,0C5.8,0,5.4,0.4,5.4,0.8v0.7C5,1.7,4.6,1.8,4.3,2L3.8,1.5C3.6,1.4,3.4,1.3,3.2,1.3S2.7,1.4,2.6,1.5L1.5,2.6c-0.3,0.3-0.3,0.9,0,1.2L2,4.3C1.8,4.6,1.7,5,1.5,5.4H0.8C0.4,5.4,0,5.8,0,6.2v1.5c0,0.5,0.4,0.8,0.8,0.8h0.7C1.7,9,1.8,9.4,2,9.7l-0.5,0.5c-0.3,0.3-0.3,0.8,0,1.2l1.1,1.1c0.3,0.3,0.9,0.3,1.2,0L4.3,12c0.4,0.2,0.8,0.4,1.2,0.5v0.7c0,0.5,0.4,0.8,0.8,0.8h1.5c0.5,0,0.8-0.4,0.8-0.8v-0.7C9,12.3,9.4,12.2,9.7,12l0.5,0.5c0.3,0.3,0.9,0.3,1.2,0l1.1-1.1c0.3-0.3,0.3-0.8,0-1.2L12,9.7c0.2-0.4,0.4-0.8,0.5-1.2h0.7c0.5,0,0.8-0.4,0.8-0.8V6.2c0-0.5-0.4-0.8-0.8-0.8h-0.7C12.3,5,12.2,4.6,12,4.3l0.5-0.5c0.3-0.3,0.3-0.9,0-1.2l-1.1-1.1c-0.2-0.2-0.4-0.2-0.6-0.2s-0.4,0.1-0.6,0.2L9.7,2C9.4,1.8,9,1.7,8.6,1.5V0.8C8.6,0.4,8.2,0,7.8,0L6.2,0z M6.8,0.8h0.4c0.2,0,0.4,0.2,0.4,0.4v1.2c0.8,0.1,1.6,0.4,2.3,0.9l0.8-0.8c0.2-0.2,0.4-0.2,0.6,0l0.3,0.3c0.2,0.2,0.2,0.4,0,0.6l-0.8,0.8c0.5,0.7,0.8,1.4,0.9,2.3h1.2c0.2,0,0.4,0.2,0.4,0.4v0.4c0,0.2-0.2,0.4-0.4,0.4h-1.2c-0.1,0.8-0.4,1.6-0.9,2.3l0.8,0.8c0.2,0.2,0.2,0.4,0,0.6l-0.3,0.3c-0.2,0.2-0.4,0.2-0.6,0l-0.8-0.8c-0.7,0.5-1.4,0.8-2.3,0.9v1.2c0,0.2-0.2,0.4-0.4,0.4H6.8c-0.2,0-0.4-0.2-0.4-0.4v-1.2c-0.8-0.1-1.6-0.4-2.3-0.9l-0.8,0.8c-0.2,0.2-0.4,0.2-0.6,0l-0.3-0.3c-0.2-0.2-0.2-0.4,0-0.6l0.8-0.8C2.8,9.2,2.5,8.4,2.4,7.6H1.2C1,7.6,0.8,7.4,0.8,7.2V6.8c0-0.2,0.2-0.4,0.4-0.4h1.2c0.1-0.8,0.4-1.6,0.9-2.3L2.5,3.3c-0.2-0.2-0.2-0.4,0-0.6l0.3-0.3c0.2-0.2,0.4-0.2,0.6,0l0.8,0.8c0.7-0.5,1.4-0.8,2.3-0.9V1.2C6.4,1,6.6,0.8,6.8,0.8L6.8,0.8z M7,3.6C5.1,3.6,3.6,5.1,3.6,7c0,0,0,0,0,0c0,1.9,1.5,3.4,3.4,3.4c1.9,0,3.4-1.5,3.4-3.4C10.4,5.1,8.9,3.6,7,3.6C7,3.6,7,3.6,7,3.6z M7,4.8c1.2,0,2.2,1,2.2,2.2c0,1.2-1,2.2-2.2,2.2c-1.2,0-2.2-1-2.2-2.2C4.8,5.8,5.8,4.8,7,4.8z"/>
<symbol id="svg-icon-config" viewBox="0 0 24 24">
<path d="M19.43 12.98a7.8 7.8 0 0 0 0-1.96l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5
0 0 0-.61-.22l-2.49 1a7.3 7.3 0 0 0-1.69-.98l-.38-2.65A.49.49 0 0 0 14 2h-4a.49.49
0 0 0-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.57.57 0 0 0-.18-.03.5.5 0 0
0-.43.25l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65a7.93 7.93 0 0 0 0 1.96l-2.11 1.65a.5.5
0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38
2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65a7.68 7.68 0 0 0 1.69-.98l2.49
1 .18.03a.5.5 0 0 0 .43-.25l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65zm-1.98-1.71a5.34
5.34 0 0 1 0 1.46l-.14 1.13.89.7 1.08.84-.7
1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2
1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43a5.67 5.67 0 0
1-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21
1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21
1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16
1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14
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>

145
manage/bulk-actions.js Normal file
View File

@ -0,0 +1,145 @@
/* global $ $$ API t prefs handleEvent installed exportToFile checkUpdateBulk exportDropbox
messageBox */
/* exported bulk */
'use strict';
const bulk = {
init: () => {
document.addEventListener('change', bulk.updateBulkFilters);
$('#bulk-actions-select').onchange = bulk.handleSelect;
$('#bulk-actions-apply').onclick = bulk.handleApply;
},
// Update all button in header
updateAll: () => {
const toggle = $('#toggle-all-filters');
toggle.checked = false; // ensure click will check all styles
toggle.click();
checkUpdateBulk();
},
checkApply: () => {
const checkedEntries = $$('.entry-filter-toggle').filter(entry => entry.checked);
if (checkedEntries.length > 0 && $('#bulk-actions-select').value !== '') {
$('#bulk-actions-apply').removeAttribute('disabled');
} else {
$('#bulk-actions-apply').setAttribute('disabled', true);
}
$('#bulk-filter-count').textContent = checkedEntries.length || '';
},
handleSelect: event => {
event.preventDefault();
$$('[data-bulk]').forEach(el => el.classList.add('hidden'));
switch (event.target.value) {
case 'enable':
break;
case 'disable':
break;
case 'export':
$('[data-bulk="export"]').classList.remove('hidden');
break;
case 'update':
$('[data-bulk="update"]').classList.remove('hidden');
break;
// case 'reset':
// break;
case 'delete':
break;
}
},
handleApply: event => {
event.preventDefault();
let styles;
const action = $('#bulk-actions-select').value;
const entries = $$('.entry-filter-toggle:checked').map(el => el.closest('.entry'));
switch (action) {
case 'enable':
case 'disable': {
const isEnabled = action === 'enable';
entries.forEach(entry => {
const box = $('.entry-state-toggle', entry);
entry.classList.toggle('enable', isEnabled);
box.checked = isEnabled;
handleEvent.toggle.call(box, event, entry);
});
break;
}
case 'export': {
styles = entries.map(entry => entry.styleMeta);
const destination = prefs.get('manage.export.destination');
if (destination === 'dropbox') {
return exportDropbox(styles);
}
return exportToFile(styles);
}
case 'update':
checkUpdateBulk();
break;
// case 'reset':
// break;
case 'delete': {
styles = entries.reduce((acc, entry) => {
const style = entry.styleMeta;
acc[style.id] = style.name;
return acc;
}, {});
bulk.deleteBulk(event, styles);
const toggle = $('#toggle-all-filters');
toggle.checked = false;
toggle.indeterminate = false;
break;
}
}
$('#bulk-actions-select').value = '';
$('#bulk-actions-apply').setAttribute('disabled', true);
},
updateBulkFilters: ({target}) => {
// total is undefined until initialized
if (installed.dataset.total) {
// ignore filter checkboxes
if (target.type === 'checkbox' && target.closest('.toggle-all, .entry-filter')) {
const bulk = $('#toggle-all-filters');
const state = target.checked;
const visibleEntries = $$('.entry-filter-toggle')
.filter(entry => !entry.closest('.entry').classList.contains('hidden'));
bulk.indeterminate = false;
if (target === bulk) {
visibleEntries.forEach(entry => {
entry.checked = state;
});
} else {
if (visibleEntries.length === visibleEntries.filter(entry => entry.checked === state).length) {
bulk.checked = state;
} else {
bulk.checked = false;
bulk.indeterminate = true;
}
}
}
bulk.checkApply();
}
},
deleteBulk: (event, styles) => {
messageBox({
title: t('deleteStyleConfirm'),
contents: Object.values(styles).join(', '),
className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')],
})
.then(({button}) => {
if (button === 0) {
Object.keys(styles).forEach(id => API.deleteStyle(Number(id)));
installed.dataset.total -= Object.keys(styles).length;
bulk.updateBulkFilters({target: $('#toggle-all-filters')});
}
});
}
};

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,10 +74,10 @@ function init() {
$('#reset-filters').onclick = event => {
event.preventDefault();
if (!filtersSelector.hide) {
return;
}
for (const el of $$('#filters [data-filter]')) {
// if (!filtersSelector.hide) {
// return;
// }
for (const el of $$('#tools-wrapper [data-filter]')) {
let value;
if (el.type === 'checkbox' && el.checked) {
value = el.checked = false;
@ -127,22 +93,16 @@ function init() {
}
filterOnChange({forceRefilter: true});
router.updateSearch('search', '');
resetUpdates();
UI.updateFilterLabels();
};
// Adjust width after selects are visible
prefs.subscribe(['manage.filters.expanded'], () => {
const el = $('#filters');
if (el.open) {
$$('select', el).forEach(select => select.adjustWidth());
}
});
filterOnChange({forceRefilter: true});
}
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) {
@ -150,7 +110,7 @@ function filterOnChange({target: el, forceRefilter}) {
}
el.lastValue = value;
}
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el));
const enabledFilters = $$('#tools-wrapper [data-filter]').filter(el => getValue(el));
const buildFilter = hide =>
(hide ? '' : '.entry.hidden') +
[...enabledFilters.map(el =>
@ -163,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);
}
@ -262,9 +223,9 @@ function reapplyFilter(container = installed, alreadySearched) {
function showFiltersStats() {
const active = filtersSelector.hide !== '';
$('#filters summary').classList.toggle('active', active);
$('.filter-stats-wrapper').classList.toggle('active', active);
$('#reset-filters').disabled = !active;
const numTotal = installed.children.length;
const numTotal = installed.children.length - 1; // Don't include the header
const numHidden = installed.getElementsByClassName('entry hidden').length;
const numShown = numTotal - numHidden;
if (filtersSelector.numShown !== numShown ||
@ -291,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]});
}
@ -294,8 +296,7 @@ function importFromString(jsonString) {
}
function exportToFile() {
API.getAllStyles().then(styles => {
function exportToFile(styles) {
// https://crbug.com/714373
document.documentElement.appendChild(
$create('iframe', {
@ -325,7 +326,6 @@ function exportToFile() {
// we don't remove the iframe or the object URL because the browser may show
// a download dialog and we don't know how long it'll take until the user confirms it
// (some browsers like Vivaldi can't download if we revoke the URL)
});
function generateFileName() {
const today = new Date();

View File

@ -38,6 +38,7 @@ onDOMready().then(() => {
let textAtPos = 1e6;
let rotated;
const entries = [...installed.children];
entries.shift(); // remove header
const focusedIndex = entries.indexOf(focusedEntry);
if (focusedIndex > 0) {
if (direction > 0) {
@ -63,7 +64,7 @@ onDOMready().then(() => {
}
if (found && found !== focusedEntry) {
focusedEntry = found;
focusedLink = $('.style-name-link', found);
focusedLink = $('.entry-name', found);
focusedName = found.styleNameLowerCase;
scrollElementIntoView(found, {invalidMarginRatio: .25});
animateElement(found, {className: 'highlight-quick'});
@ -82,6 +83,7 @@ onDOMready().then(() => {
// focus search field on "/" key
if (key === '/' || !key && k === 191 && !event.shiftKey) {
event.preventDefault();
$('#tools-wrapper').classList.remove('hidden');
$('#search').focus();
return;
}

591
manage/manage-actions.js Normal file
View File

@ -0,0 +1,591 @@
/*
global messageBox getStyleWithNoCode
filterAndAppend showFiltersStats
checkUpdate handleUpdateInstalled resetUpdates
objectDiff
configDialog
sorter msg prefs API onDOMready $ $$ setupLivePrefs
URLS enforceInputRange t
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI FIREFOX router
UI bulk
*/
'use strict';
let installed;
const handleEvent = {};
Promise.all([
API.getAllStyles(true),
// FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
Promise.all([
onDOMready(),
prefs.initializing,
])
.then(() => {
initGlobalEvents();
if (!VIVALDI) {
$$('#header select').forEach(el => el.adjustWidth());
}
if (FIREFOX && 'update' in (chrome.commands || {})) {
const btn = $('#manage-shortcuts-button');
btn.classList.remove('chromium-only');
btn.onclick = API.optionsCustomizeHotkeys;
}
}),
]).then(args => {
UI.init();
UI.showStyles(...args);
lazyLoad();
});
msg.onExtension(onRuntimeMessage);
function onRuntimeMessage(msg) {
switch (msg.method) {
case 'styleUpdated':
case 'styleAdded':
API.getStyle(msg.style.id, true)
.then(style => handleUpdate(style, msg));
break;
case 'styleDeleted':
handleDelete(msg.style.id);
break;
case 'styleApply':
case 'styleReplaceAll':
break;
default:
return;
}
setTimeout(() => {
sorter.updateStripes({onlyWhenColumnsChanged: true});
}, 0);
}
function initGlobalEvents() {
installed = $('#installed');
installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = event => {
event.preventDefault();
router.updateHash('#stylus-options');
};
$('#manage-shortcuts-button').onclick = event => {
event.preventDefault();
openURL({url: URLS.configureCommands});
};
$('#update-all').onclick = event => {
event.preventDefault();
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));
$$('#add-usercss, #add-reg-css').forEach(a => (a.onclick = handleEvent.newStyle));
document.addEventListener('visibilitychange', onVisibilityChange);
document.addEventListener('keydown', event => {
if (event.which === 27) {
// close all open "applies-to" details
$$('.applies-to-extra[open]').forEach(el => {
el.removeAttribute('open');
});
} else if (event.which === 32 && event.target.classList.contains('checkmate')) {
// pressing space toggles the containing checkbox
$('input[type="checkbox"]', event.target).click();
}
});
// triggered automatically by setupLivePrefs() below
enforceInputRange($('#manage.newUI.targets'));
// N.B. triggers existing onchange listeners
setupLivePrefs();
bulk.init();
sorter.init();
prefs.subscribe([
'manage.newUI.favicons',
'manage.newUI.faviconsGray',
'manage.newUI.targets',
], () => switchUI());
switchUI({styleOnly: true});
}
Object.assign(handleEvent, {
ENTRY_ROUTES: {
'.entry-state-toggle': 'toggle',
'.entry-style-name': 'name',
'.entry-homepage': 'external',
'.entry-support': 'external',
'.check-update': 'check',
'.update': 'update',
'.entry-delete': 'delete',
'.entry-configure-usercss': 'config',
'.sortable': 'updateSort',
'#applies-to-config': 'appliesConfig',
'.applies-to-extra-expander': 'toggleExtraAppliesTo'
},
entryClicked(event) {
const target = event.target;
const entry = target.closest('.entry');
for (const selector in handleEvent.ENTRY_ROUTES) {
for (let el = target; el && el !== entry; el = el.parentElement) {
if (el.matches(selector)) {
const handler = handleEvent.ENTRY_ROUTES[selector];
return handleEvent[handler].call(el, event, entry);
}
}
}
},
name(event) {
handleEvent.edit(event);
},
newStyle(event) {
event.preventDefault();
prefs.set('newStyleAsUsercss', event.target.id === 'add-usercss');
window.location.href = 'edit.html';
},
edit(event) {
if (event.altKey) {
return;
}
event.preventDefault();
event.stopPropagation();
const left = event.button === 0;
const middle = event.button === 1;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
const url = $('[href]', event.target.closest('.entry')).href;
if (openWindow || openBackgroundTab || openForegroundTab) {
if (chrome.windows && openWindow) {
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
} else {
getOwnTab().then(({index}) => {
openURL({
url,
index: index + 1,
active: openForegroundTab
});
});
}
} else {
onVisibilityChange();
getActiveTab().then(tab => {
sessionStorageHash('manageStylesHistory').set(tab.id, url);
location.href = url;
});
}
},
toggle(event, entry) {
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
UI.addLabels(entry);
},
toggleExtraAppliesTo(event, entry) {
event.preventDefault();
entry.classList.toggle('hide-extra');
if (event.shiftKey) {
const state = entry.classList.contains('hide-extra');
$$('.entry').forEach(entry => entry.classList.toggle('hide-extra', state));
}
},
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});
},
update(event, entry) {
event.preventDefault();
const json = entry.updatedCode;
json.id = entry.styleId;
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
},
updateSort(event) {
event.preventDefault();
sorter.updateSort(event);
removeSelection();
},
delete(event, entry) {
event.preventDefault();
const id = entry.styleId;
animateElement(entry);
messageBox({
title: t('deleteStyleConfirm'),
contents: entry.styleMeta.name,
className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')],
})
.then(({button}) => {
if (button === 0) {
API.deleteStyle(id);
}
});
},
external(event) {
if (event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
// Shift-click = the built-in 'open in a new window' action
return;
}
getOwnTab().then(({index}) => {
openURL({
url: event.target.closest('a').href,
index: index + 1,
active: !event.ctrlKey || event.shiftKey,
});
});
event.preventDefault();
},
loadFavicons({all = false} = {}) {
if (!installed.firstElementChild) return;
let favicons = [];
if (all) {
favicons = $$('img[data-src]', installed);
} else {
const {left, top} = installed.firstElementChild.getBoundingClientRect();
const x = Math.max(0, left);
const y = Math.max(0, top);
const first = document.elementFromPoint(x, y);
const lastOffset = first.offsetTop + window.innerHeight;
const numTargets = prefs.get('manage.newUI.targets');
let entry = first && first.closest('.entry') || installed.children[0];
while (entry && entry.offsetTop <= lastOffset) {
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
entry = entry.nextElementSibling;
}
}
let i = 0;
for (const img of favicons) {
img.src = img.dataset.src;
delete img.dataset.src;
// loading too many icons at once will block the page while the new layout is recalculated
if (++i > 100) break;
}
if ($('img[data-src]', installed)) {
debounce(handleEvent.loadFavicons, 1, {all: true});
}
},
config(event, {styleMeta}) {
event.preventDefault();
configDialog(styleMeta);
},
appliesConfig() {
messageBox({
title: t('configureStyle'),
className: 'config-dialog',
contents: [
$('#appliesToConfig').cloneNode(true)
],
buttons: [{
textContent: t('confirmClose'),
dataset: {cmd: 'close'},
}],
onshow: box => {
box.addEventListener('change', handleEvent.manageFavicons);
box.addEventListener('input', handleEvent.manageFavicons);
$$('input', box).forEach(el => {
el.dataset.id = el.id;
el.id = null;
});
}
}).then(() => {
const box = $('#message-box');
box.removeEventListener('change', handleEvent.manageFavicons);
box.removeEventListener('input', handleEvent.manageFavicons);
});
},
manageFavicons(event) {
event.stopPropagation();
const box = $('#message-box-contents');
let value = $('[data-id="manage.newUI.favicons"]', box).checked;
prefs.set('manage.newUI.favicons', value);
// Updating the hidden inputs; not the inputs in the message box
$('#manage.newUI.favicons').checked = value;
value = $('[data-id="manage.newUI.faviconsGray"]', box).checked;
prefs.set('manage.newUI.faviconsGray', value);
$('#manage.newUI.faviconsGray').checked = value;
value = $('[data-id="manage.newUI.targets"]', box).value;
prefs.set('manage.newUI.targets', value);
},
});
function handleUpdate(style, {reason, method} = {}) {
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
let entry;
let oldEntry = $(UI.ENTRY_ID_PREFIX + style.id);
if (oldEntry && method === 'styleUpdated') {
handleToggledOrCodeOnly();
}
entry = entry || UI.createStyleElement({style});
if (oldEntry) {
// Make sure to update the filter checkbox since it's state isn't saved to the style
$('.entry-filter-toggle', entry).checked = $('.entry-filter-toggle', oldEntry).checked;
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
installed.replaceChild(entry, oldEntry);
} else {
oldEntry.remove();
}
}
if ((reason === 'update' || reason === 'install') && entry.matches('.updatable')) {
handleUpdateInstalled(entry, reason);
}
filterAndAppend({entry}).then(sorter.update);
if (!entry.matches('.hidden') && reason !== 'import') {
animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
}
UI.getFaviconImgSrc(entry);
function handleToggledOrCodeOnly() {
const newStyleMeta = getStyleWithNoCode(style);
const diff = objectDiff(oldEntry.styleMeta, newStyleMeta)
.filter(({key, path}) => path || (!key.startsWith('original') && !key.endsWith('Date')));
if (diff.length === 0) {
// only code was modified
entry = oldEntry;
oldEntry = null;
}
if (diff.length === 1 && diff[0].key === 'enabled') {
oldEntry.classList.toggle('enabled', style.enabled);
oldEntry.classList.toggle('disabled', !style.enabled);
$$('.entry-state-toggle', oldEntry).forEach(el => (el.checked = style.enabled));
oldEntry.styleMeta = newStyleMeta;
entry = oldEntry;
UI.addLabels(entry);
oldEntry = null;
}
}
}
function handleDelete(id) {
const node = $(UI.ENTRY_ID_PREFIX + id);
if (node) {
node.remove();
if (node.matches('.can-update')) {
const btnApply = $('#apply-all-updates');
btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
}
showFiltersStats();
}
}
function switchUI({styleOnly} = {}) {
const current = {enabled: true};
const changed = {};
let someChanged = false;
// ensure the global option is processed first
for (const el of $$('[id^="manage.newUI."]')) {
const id = el.id.replace(/^manage\.newUI\.?/, '');
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
const valueChanged = value !== UI[id];
current[id] = value;
changed[id] = valueChanged;
someChanged |= valueChanged;
}
if (!styleOnly && !someChanged) {
return;
}
Object.assign(UI, current);
installed.classList.toggle('has-favicons', UI.favicons);
installed.classList.toggle('faviconsGray', UI.faviconsGray);
if (styleOnly) {
return;
}
const missingFavicons = UI.favicons && !$('.entry-applies-to img[src]');
if (changed.targets) {
for (const targetWrapper of $$('.entry .targets')) {
const targets = $$('.target', targetWrapper);
targets.forEach((target, indx) => {
target.classList.toggle('extra', indx >= UI.targets);
});
$('.applies-to-extra-expander', targetWrapper)
.classList.toggle('hidden', targets.length <= UI.targets);
}
return;
}
if (missingFavicons) {
debounce(UI.getFaviconImgSrc);
return;
}
}
function onVisibilityChange() {
switch (document.visibilityState) {
// page restored without reloading via history navigation (currently only in FF)
// the catch here is that DOM may be outdated so we'll at least refresh the just edited style
// assuming other changes aren't important enough to justify making a complicated DOM sync
case 'visible':
if (sessionStorage.justEditedStyleId) {
API.getStyle(Number(sessionStorage.justEditedStyleId), true)
.then(style => {
handleUpdate(style, {method: 'styleUpdated'});
});
delete sessionStorage.justEditedStyleId;
}
break;
// going away
case 'hidden':
history.replaceState({scrollY: window.scrollY}, document.title);
break;
}
}
function removeSelection() {
const sel = window.getSelection ? window.getSelection() : document.selection;
if (sel) {
if (sel.removeAllRanges) {
sel.removeAllRanges();
} else if (sel.empty) {
sel.empty();
}
}
}
function lazyLoad() {
setTimeout(() => {
$$('link[data-href]').forEach(link => {
link.href = link.dataset.href;
link.removeAttribute('data-href');
});
$$('script[data-src]').forEach(script => {
script.src = script.dataset.src;
script.removeAttribute('data-src');
});
}, 500);
}
function embedOptions() {
let options = $('#stylus-embedded-options');
if (!options) {
options = document.createElement('iframe');
options.id = 'stylus-embedded-options';
options.src = '/options.html';
document.documentElement.appendChild(options);
}
options.focus();
}
function unembedOptions() {
const options = $('#stylus-embedded-options');
if (options) {
options.contentWindow.document.body.classList.add('scaleout');
options.classList.add('fadeout');
animateElement(options, {
className: 'fadeout',
onComplete: () => options.remove(),
});
}
}
router.watch({hash: '#stylus-options'}, state => {
if (state) {
embedOptions();
} else {
unembedOptions();
}
});
window.addEventListener('closeOptions', () => {
router.updateHash('');
});

387
manage/manage-ui.js Normal file
View File

@ -0,0 +1,387 @@
/*
global prefs t $ $$ $create template tWordBreak
installed sorter filterAndAppend handleEvent
animateElement scrollElementIntoView formatDate
*/
'use strict';
const UI = {
ENTRY_ID_PREFIX_RAW: 'style-',
ENTRY_ID_PREFIX: '#style-',
TARGET_TYPES: ['domains', 'urls', 'urlPrefixes', 'regexps'],
GET_FAVICON_URL: 'https://www.google.com/s2/favicons?domain=',
OWN_ICON: chrome.runtime.getManifest().icons['16'],
favicons: prefs.get('manage.newUI.favicons'),
faviconsGray: prefs.get('manage.newUI.faviconsGray'),
targets: prefs.get('manage.newUI.targets'),
labels: {
'usercss': {
is: ({style}) => typeof style.usercssData !== 'undefined',
text: 'usercss'
},
'disabled': {
is: ({entry}) => !$('.entry-state-toggle', entry).checked,
text: t('genericDisabledLabel')
}
},
init: () => {
$('.ext-version').textContent = `v${chrome.runtime.getManifest().version}`;
// translate CSS manually
// #update-all-no-updates[data-skipped-edited="true"]::after {
// content: " ${t('updateAllCheckSucceededSomeEdited')}";
// }
document.head.appendChild($create('style', `
body.all-styles-hidden-by-filters #installed:after {
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) => {
UI.addHeaderLabels();
const sorted = sorter.sort({
styles: styles.map(style => ({
style,
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
})),
});
let index = 0;
let firstRun = true;
installed.dataset.total = styles.length;
const scrollY = (history.state || {}).scrollY;
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
const renderBin = document.createDocumentFragment();
if (scrollY) {
renderStyles();
} else {
requestAnimationFrame(renderStyles);
}
function renderStyles() {
const t0 = performance.now();
let rendered = 0;
while (
index < sorted.length &&
// eslint-disable-next-line no-unmodified-loop-condition
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10)
) {
const info = sorted[index++];
const entry = UI.createStyleElement(info);
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
entry.classList.add('not-matching');
rendered--;
}
renderBin.appendChild(entry);
}
filterAndAppend({container: renderBin}).then(sorter.updateStripes);
if (index < sorted.length) {
requestAnimationFrame(renderStyles);
if (firstRun) setTimeout(UI.getFaviconImgSrc);
firstRun = false;
return;
}
setTimeout(UI.getFaviconImgSrc);
if (sessionStorage.justEditedStyleId) {
UI.highlightEditedStyle();
} else if ('scrollY' in (history.state || {})) {
setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
}
}
},
createStyleElement: ({style, name}) => {
// query the sub-elements just once, then reuse the references
if (!(UI._parts || {}).UI) {
const entry = template['style'];
UI._parts = {
UI: true,
entry,
entryClassBase: entry.className,
checker: $('.entry-state-toggle', entry) || {},
nameLink: $('.entry-name', entry),
editLink: $('.entry-edit', entry) || {},
editHrefBase: 'edit.html?id=',
appliesTo: $('.entry-applies-to', entry),
targets: $('.targets', entry),
decorations: {
urlPrefixesAfter: '*',
regexpsBefore: '/',
regexpsAfter: '/',
},
};
}
const parts = UI._parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
parts.checker.checked = style.enabled;
$('.entry-name-text', parts.nameLink).textContent = tWordBreak(style.name);
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
// clear the code to free up some memory
// (note, style is already a deep copy)
style.sourceCode = null;
style.sections.forEach(section => (section.code = null));
const entry = parts.entry.cloneNode(true);
entry.id = UI.ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id;
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
entry.styleMeta = style;
entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : '') +
(style.usercssData ? ' usercss' : '');
let el = $('.entry-homepage', entry);
el.classList.toggle('invisible', !style.url);
el.href = style.url || '';
el.dataset.title = style.url ? `${t('externalHomepage')}: ${style.url}` : '';
const support = style.usercssData && style.usercssData.supportURL || '';
el = $('.entry-support', entry);
el.classList.toggle('invisible', !support);
el.href = support;
el.dataset.title = support ? `${t('externalSupport')}: ${support}` : '';
$('.entry-configure-usercss', entry).classList.toggle('invisible', !configurable);
if (style.updateUrl) {
$('.entry-updater-placeholder', entry).replaceWith(template.updaterIcons.cloneNode(true));
}
$('.entry-version-value', entry).textContent = style.usercssData && style.usercssData.version || '';
const lastUpdate = $('.entry-last-update-value', entry);
lastUpdate.textContent = UI.getDateString(style.updateDate);
// Show install & last update in title
lastUpdate.dataset.title = [
{prop: 'installDate', name: 'dateInstalled'},
{prop: 'updateDate', name: 'dateUpdated'},
].map(({prop, name}) => t(name) + ': ' + (formatDate(entry.styleMeta[prop]) || '—')).join('\n');
UI.createStyleTargetsElement({entry, style});
UI.addLabels(entry);
return entry;
},
getDateString: date => {
const newDate = new Date(date);
return newDate instanceof Date && isFinite(newDate)
? newDate.toISOString().split('T')[0].replace(/-/g, '.')
: '';
},
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;
const entryTargets = $('.targets', entry);
const targets = parts.targets.cloneNode(true);
let container = targets;
let numTargets = 0;
let extraClass = '';
const displayed = new Set();
for (const type of UI.TARGET_TYPES) {
for (const section of style.sections) {
for (const targetValue of section[type] || []) {
if (displayed.has(targetValue)) {
continue;
}
displayed.add(targetValue);
const element = template.appliesToTarget.cloneNode(true);
if (numTargets === UI.targets) {
extraClass = ' extra';
}
element.dataset.type = type;
element.dataset.index = numTargets;
element.dataset.title =
(parts.decorations[type + 'Before'] || '') +
targetValue +
(parts.decorations[type + 'After'] || '');
element.className += extraClass;
container.appendChild(element);
numTargets++;
}
}
}
// Include hidden expander in case user changes UI.targets
container.appendChild(template.extraAppliesTo.cloneNode(true));
if (numTargets <= UI.targets) {
$('.applies-to-extra-expander', container).classList.add('hidden');
}
if (numTargets) {
entryTargets.parentElement.replaceChild(targets, entryTargets);
} else if (!entry.classList.contains('global') || !entryTargets.firstElementChild) {
if (entryTargets.firstElementChild) {
entryTargets.textContent = '';
}
entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
}
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;
const regexpRemoveNegativeLookAhead = /(\?!([^)]+\))|\(\?![\w(]+[^)]+[\w|)]+)/g;
// replace extra characters & all but the first group entry "(abc|def|ghi)xyz" => abcxyz
const regexpReplaceExtraCharacters = /[\\(]|((\|\w+)+\))/g;
const regexpMatchRegExp = /[\w-]+[.(]+(com|org|co|net|im|io|edu|gov|biz|info|de|cn|uk|nl|eu|ru)\b/g;
const regexpMatchDomain = /^.*?:\/\/([^/]+)/;
for (const target of $$('.target', container)) {
const type = target.dataset.type;
const targetValue = target.dataset.title;
if (!targetValue) continue;
let favicon = '';
if (type === 'domains') {
favicon = UI.GET_FAVICON_URL + targetValue;
} else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) {
favicon = UI.OWN_ICON;
} else if (type === 'regexps') {
favicon = targetValue
.replace(regexpRemoveNegativeLookAhead, '')
.replace(regexpReplaceExtraCharacters, '')
.match(regexpMatchRegExp);
favicon = favicon ? UI.GET_FAVICON_URL + favicon.shift() : '';
} else {
favicon = targetValue.includes('://') && targetValue.match(regexpMatchDomain);
favicon = favicon ? UI.GET_FAVICON_URL + favicon[1] : '';
}
if (favicon) {
const el = $('img[src], svg', target);
if (!el || el.localName === 'svg') {
const img = $('img', target);
img.dataset.src = favicon;
} else if ((target.dataset.src || target.src) !== favicon) {
delete el.src;
el.dataset.src = favicon;
}
}
}
handleEvent.loadFavicons();
},
highlightEditedStyle: () => {
if (!sessionStorage.justEditedStyleId) return;
const entry = $(UI.ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId);
delete sessionStorage.justEditedStyleId;
if (entry) {
animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
}
},
addHeaderLabels: () => {
const header = $('.header-name');
const span = document.createElement('span');
let labels = $('.header-labels', header);
if (labels) {
labels.textContent = '';
} else {
labels = span.cloneNode();
}
const label = document.createElement('a');
labels.className = 'header-labels';
label.className = 'header-label sortable tt-s';
label.href = '#';
Object.keys(UI.labels).forEach(item => {
const newLabel = label.cloneNode(true);
const text = UI.labels[item].text;
newLabel.dataset.type = item;
newLabel.textContent = text;
newLabel.appendChild(span.cloneNode());
newLabel.dataset.title = t('sortLabel', text);
labels.appendChild(newLabel);
});
header.appendChild(labels);
},
addLabels: entry => {
const style = entry.styleMeta;
const container = $('.entry-labels', entry);
const label = document.createElement('span');
const labels = document.createElement('span');
labels.className = 'entry-labels';
label.className = 'entry-label';
Object.keys(UI.labels).forEach(item => {
if (UI.labels[item].is({entry, style})) {
const newLabel = label.cloneNode(true);
newLabel.dataset.type = item;
newLabel.textContent = UI.labels[item].text;
labels.appendChild(newLabel);
}
});
container.replaceWith(labels);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -1,741 +0,0 @@
/*
global messageBox getStyleWithNoCode
filterAndAppend showFiltersStats
checkUpdate handleUpdateInstalled
objectDiff
configDialog
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
URLS enforceInputRange t tWordBreak formatDate
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI FIREFOX router
*/
'use strict';
let installed;
const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
const newUI = {
enabled: prefs.get('manage.newUI'),
favicons: prefs.get('manage.newUI.favicons'),
faviconsGray: prefs.get('manage.newUI.faviconsGray'),
targets: prefs.get('manage.newUI.targets'),
renderClass() {
document.documentElement.classList.toggle('newUI', newUI.enabled);
},
};
newUI.renderClass();
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
const handleEvent = {};
Promise.all([
API.getAllStyles(true),
// FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
Promise.all([
onDOMready(),
prefs.initializing,
])
.then(() => {
initGlobalEvents();
if (!VIVALDI) {
$$('#header select').forEach(el => el.adjustWidth());
}
if (FIREFOX && 'update' in (chrome.commands || {})) {
const btn = $('#manage-shortcuts-button');
btn.classList.remove('chromium-only');
btn.onclick = API.optionsCustomizeHotkeys;
}
}),
]).then(args => {
showStyles(...args);
});
msg.onExtension(onRuntimeMessage);
function onRuntimeMessage(msg) {
switch (msg.method) {
case 'styleUpdated':
case 'styleAdded':
API.getStyle(msg.style.id, true)
.then(style => handleUpdate(style, msg));
break;
case 'styleDeleted':
handleDelete(msg.style.id);
break;
case 'styleApply':
case 'styleReplaceAll':
break;
default:
return;
}
setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true});
}
function initGlobalEvents() {
installed = $('#installed');
installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = () => {
router.updateHash('#stylus-options');
};
{
const btn = $('#manage-shortcuts-button');
btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands}));
}
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
// show date installed & last update on hover
installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
document.addEventListener('visibilitychange', onVisibilityChange);
$$('[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'));
// N.B. triggers existing onchange listeners
setupLivePrefs();
sorter.init();
prefs.subscribe([
'manage.newUI',
'manage.newUI.favicons',
'manage.newUI.faviconsGray',
'manage.newUI.targets',
], () => switchUI());
switchUI({styleOnly: true});
// translate CSS manually
document.head.appendChild($create('style', `
.disabled h2::after {
content: "${t('genericDisabledLabel')}";
}
#update-all-no-updates[data-skipped-edited="true"]::after {
content: " ${t('updateAllCheckSucceededSomeEdited')}";
}
body.all-styles-hidden-by-filters::after {
content: "${t('filteredStylesAllHidden')}";
}
`));
}
function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({
styles: styles.map(style => ({
style,
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
})),
});
let index = 0;
let firstRun = true;
installed.dataset.total = styles.length;
const scrollY = (history.state || {}).scrollY;
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
const renderBin = document.createDocumentFragment();
if (scrollY) {
renderStyles();
} else {
requestAnimationFrame(renderStyles);
}
function renderStyles() {
const t0 = performance.now();
let rendered = 0;
while (
index < sorted.length &&
// eslint-disable-next-line no-unmodified-loop-condition
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10)
) {
const info = sorted[index++];
const entry = createStyleElement(info);
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
entry.classList.add('not-matching');
rendered--;
}
renderBin.appendChild(entry);
}
filterAndAppend({container: renderBin}).then(sorter.updateStripes);
if (index < sorted.length) {
requestAnimationFrame(renderStyles);
if (firstRun) setTimeout(getFaviconImgSrc);
firstRun = false;
return;
}
setTimeout(getFaviconImgSrc);
if (sessionStorage.justEditedStyleId) {
highlightEditedStyle();
} else if ('scrollY' in (history.state || {})) {
setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
}
}
}
function createStyleElement({style, name}) {
// query the sub-elements just once, then reuse the references
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
createStyleElement.parts = {
newUI: newUI.enabled,
entry,
entryClassBase: entry.className,
checker: $('.checker', entry) || {},
nameLink: $('.style-name-link', entry),
editLink: $('.style-edit-link', entry) || {},
editHrefBase: 'edit.html?id=',
homepage: $('.homepage', entry),
homepageIcon: template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`],
appliesTo: $('.applies-to', entry),
targets: $('.targets', entry),
expander: $('.expander', entry),
decorations: {
urlPrefixesAfter: '*',
regexpsBefore: '/',
regexpsAfter: '/',
},
oldConfigure: !newUI.enabled && $('.configure-usercss', entry),
oldCheckUpdate: !newUI.enabled && $('.check-update', entry),
oldUpdate: !newUI.enabled && $('.update', entry),
};
}
const parts = createStyleElement.parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
parts.checker.checked = style.enabled;
parts.nameLink.textContent = tWordBreak(style.name);
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
parts.homepage.href = parts.homepage.title = style.url || '';
if (!newUI.enabled) {
parts.oldConfigure.classList.toggle('hidden', !configurable);
parts.oldCheckUpdate.classList.toggle('hidden', !style.updateUrl);
parts.oldUpdate.classList.toggle('hidden', !style.updateUrl);
}
// clear the code to free up some memory
// (note, style is already a deep copy)
style.sourceCode = null;
style.sections.forEach(section => (section.code = null));
const entry = parts.entry.cloneNode(true);
entry.id = ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id;
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
entry.styleMeta = style;
entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : '') +
(style.usercssData ? ' usercss' : '');
if (style.url) {
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
}
if (style.updateUrl && newUI.enabled) {
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
}
if (configurable && newUI.enabled) {
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
}
createStyleTargetsElement({entry, style});
return entry;
}
function createStyleTargetsElement({entry, style}) {
const parts = createStyleElement.parts;
const entryTargets = $('.targets', entry);
const targets = parts.targets.cloneNode(true);
let container = targets;
let numTargets = 0;
const displayed = new Set();
for (const type of TARGET_TYPES) {
for (const section of style.sections) {
for (const targetValue of section[type] || []) {
if (displayed.has(targetValue)) {
continue;
}
displayed.add(targetValue);
const element = template.appliesToTarget.cloneNode(true);
if (!newUI.enabled) {
if (numTargets === 10) {
container = container.appendChild(template.extraAppliesTo.cloneNode(true));
} else if (numTargets > 0) {
container.appendChild(template.appliesToSeparator.cloneNode(true));
}
}
element.dataset.type = type;
element.appendChild(
document.createTextNode(
(parts.decorations[type + 'Before'] || '') +
targetValue +
(parts.decorations[type + 'After'] || '')));
container.appendChild(element);
numTargets++;
}
}
}
if (newUI.enabled) {
if (numTargets > newUI.targets) {
$('.applies-to', entry).classList.add('has-more');
}
}
if (numTargets) {
entryTargets.parentElement.replaceChild(targets, entryTargets);
} else if (!entry.classList.contains('global') ||
!entryTargets.firstElementChild) {
if (entryTargets.firstElementChild) {
entryTargets.textContent = '';
}
entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
}
entry.classList.toggle('global', !numTargets);
}
function getFaviconImgSrc(container = installed) {
if (!newUI.enabled || !newUI.favicons) return;
const regexpRemoveNegativeLookAhead = /(\?!([^)]+\))|\(\?![\w(]+[^)]+[\w|)]+)/g;
// replace extra characters & all but the first group entry "(abc|def|ghi)xyz" => abcxyz
const regexpReplaceExtraCharacters = /[\\(]|((\|\w+)+\))/g;
const regexpMatchRegExp = /[\w-]+[.(]+(com|org|co|net|im|io|edu|gov|biz|info|de|cn|uk|nl|eu|ru)\b/g;
const regexpMatchDomain = /^.*?:\/\/([^/]+)/;
for (const target of $$('.target', container)) {
const type = target.dataset.type;
const targetValue = target.textContent;
if (!targetValue) continue;
let favicon = '';
if (type === 'domains') {
favicon = GET_FAVICON_URL + targetValue;
} else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) {
favicon = OWN_ICON;
} else if (type === 'regexps') {
favicon = targetValue
.replace(regexpRemoveNegativeLookAhead, '')
.replace(regexpReplaceExtraCharacters, '')
.match(regexpMatchRegExp);
favicon = favicon ? GET_FAVICON_URL + favicon.shift() : '';
} else {
favicon = targetValue.includes('://') && targetValue.match(regexpMatchDomain);
favicon = favicon ? GET_FAVICON_URL + favicon[1] : '';
}
if (favicon) {
const img = target.children[0];
if (!img || img.localName !== 'img') {
target.insertAdjacentElement('afterbegin', document.createElement('img'))
.dataset.src = favicon;
} else if ((img.dataset.src || img.src) !== favicon) {
img.src = '';
img.dataset.src = favicon;
}
}
}
handleEvent.loadFavicons();
}
Object.assign(handleEvent, {
ENTRY_ROUTES: {
'.checker, .enable, .disable': 'toggle',
'.style-name': 'name',
'.homepage': 'external',
'.check-update': 'check',
'.update': 'update',
'.delete': 'delete',
'.applies-to .expander': 'expandTargets',
'.configure-usercss': 'config'
},
entryClicked(event) {
const target = event.target;
const entry = target.closest('.entry');
for (const selector in handleEvent.ENTRY_ROUTES) {
for (let el = target; el && el !== entry; el = el.parentElement) {
if (el.matches(selector)) {
const handler = handleEvent.ENTRY_ROUTES[selector];
return handleEvent[handler].call(el, event, entry);
}
}
}
},
name(event) {
if (newUI.enabled) handleEvent.edit(event);
},
edit(event) {
if (event.altKey) {
return;
}
event.preventDefault();
event.stopPropagation();
const left = event.button === 0;
const middle = event.button === 1;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
const url = $('[href]', event.target.closest('.entry')).href;
if (openWindow || openBackgroundTab || openForegroundTab) {
if (chrome.windows && openWindow) {
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
} else {
getOwnTab().then(({index}) => {
openURL({
url,
index: index + 1,
active: openForegroundTab
});
});
}
} else {
onVisibilityChange();
getActiveTab().then(tab => {
sessionStorageHash('manageStylesHistory').set(tab.id, url);
location.href = url;
});
}
},
toggle(event, entry) {
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
},
check(event, entry) {
event.preventDefault();
checkUpdate(entry, {single: true});
},
update(event, entry) {
event.preventDefault();
const json = entry.updatedCode;
json.id = entry.styleId;
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
},
delete(event, entry) {
event.preventDefault();
const id = entry.styleId;
animateElement(entry);
messageBox({
title: t('deleteStyleConfirm'),
contents: entry.styleMeta.name,
className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')],
})
.then(({button}) => {
if (button === 0) {
API.deleteStyle(id);
}
});
},
external(event) {
if (event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
// Shift-click = the built-in 'open in a new window' action
return;
}
getOwnTab().then(({index}) => {
openURL({
url: event.target.closest('a').href,
index: index + 1,
active: !event.ctrlKey || event.shiftKey,
});
});
event.preventDefault();
},
expandTargets(event) {
event.preventDefault();
this.closest('.applies-to').classList.toggle('expanded');
},
loadFavicons({all = false} = {}) {
if (!installed.firstElementChild) return;
let favicons = [];
if (all) {
favicons = $$('img[data-src]', installed);
} else {
const {left, top} = installed.firstElementChild.getBoundingClientRect();
const x = Math.max(0, left);
const y = Math.max(0, top);
const first = document.elementFromPoint(x, y);
const lastOffset = first.offsetTop + window.innerHeight;
const numTargets = prefs.get('manage.newUI.targets');
let entry = first && first.closest('.entry') || installed.children[0];
while (entry && entry.offsetTop <= lastOffset) {
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
entry = entry.nextElementSibling;
}
}
let i = 0;
for (const img of favicons) {
img.src = img.dataset.src;
delete img.dataset.src;
// loading too many icons at once will block the page while the new layout is recalculated
if (++i > 100) break;
}
if ($('img[data-src]', installed)) {
debounce(handleEvent.loadFavicons, 1, {all: true});
}
},
config(event, {styleMeta}) {
event.preventDefault();
configDialog(styleMeta);
},
lazyAddEntryTitle({type, target}) {
const cell = target.closest('h2.style-name');
if (cell) {
const link = $('.style-name-link', cell);
if (type === 'mouseover' && !link.title) {
debounce(handleEvent.addEntryTitle, 50, link);
} else {
debounce.unregister(handleEvent.addEntryTitle);
}
}
},
addEntryTitle(link) {
const entry = link.closest('.entry');
link.title = [
{prop: 'installDate', name: 'dateInstalled'},
{prop: 'updateDate', name: 'dateUpdated'},
].map(({prop, name}) =>
t(name) + ': ' + (formatDate(entry.styleMeta[prop]) || '—')).join('\n');
}
});
function handleUpdate(style, {reason, method} = {}) {
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
let entry;
let oldEntry = $(ENTRY_ID_PREFIX + style.id);
if (oldEntry && method === 'styleUpdated') {
handleToggledOrCodeOnly();
}
entry = entry || createStyleElement({style});
if (oldEntry) {
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
installed.replaceChild(entry, oldEntry);
} else {
oldEntry.remove();
}
}
if ((reason === 'update' || reason === 'install') && entry.matches('.updatable')) {
handleUpdateInstalled(entry, reason);
}
filterAndAppend({entry}).then(sorter.update);
if (!entry.matches('.hidden') && reason !== 'import') {
animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
}
getFaviconImgSrc(entry);
function handleToggledOrCodeOnly() {
const newStyleMeta = getStyleWithNoCode(style);
const diff = objectDiff(oldEntry.styleMeta, newStyleMeta)
.filter(({key, path}) => path || (!key.startsWith('original') && !key.endsWith('Date')));
if (diff.length === 0) {
// only code was modified
entry = oldEntry;
oldEntry = null;
}
if (diff.length === 1 && diff[0].key === 'enabled') {
oldEntry.classList.toggle('enabled', style.enabled);
oldEntry.classList.toggle('disabled', !style.enabled);
$$('.checker', oldEntry).forEach(el => (el.checked = style.enabled));
oldEntry.styleMeta = newStyleMeta;
entry = oldEntry;
oldEntry = null;
}
}
}
function handleDelete(id) {
const node = $(ENTRY_ID_PREFIX + id);
if (node) {
node.remove();
if (node.matches('.can-update')) {
const btnApply = $('#apply-all-updates');
btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
}
showFiltersStats();
}
}
function switchUI({styleOnly} = {}) {
const current = {};
const changed = {};
let someChanged = false;
// ensure the global option is processed first
for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) {
const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled';
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
current[id] = value;
changed[id] = valueChanged;
someChanged |= valueChanged;
}
if (!styleOnly && !someChanged) {
return;
}
Object.assign(newUI, current);
newUI.renderClass();
installed.classList.toggle('has-favicons', newUI.favicons);
$('#style-overrides').textContent = `
.newUI .targets {
max-height: ${newUI.targets * 18}px;
}
` + (newUI.faviconsGray ? `
.newUI .target img {
-webkit-filter: grayscale(1);
filter: grayscale(1);
opacity: .25;
}
` : `
.newUI .target img {
-webkit-filter: none;
filter: none;
opacity: 1;
}
`) + (CHROME >= 3004 ? `
.newUI .entry {
contain: strict;
}
.newUI .entry > * {
contain: content;
}
.newUI .entry .actions {
contain: none;
}
.newUI .target {
contain: layout style;
}
.newUI .target img {
contain: layout style size;
}
.newUI .entry.can-update,
.newUI .entry.update-problem,
.newUI .entry.update-done {
contain: none;
}
` : '');
if (styleOnly) {
return;
}
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
installed.textContent = '';
API.getAllStyles(true).then(showStyles);
return;
}
if (changed.targets) {
for (const targets of $$('.entry .targets')) {
const hasMore = targets.children.length > newUI.targets;
targets.parentElement.classList.toggle('has-more', hasMore);
}
return;
}
if (missingFavicons) {
debounce(getFaviconImgSrc);
return;
}
}
function onVisibilityChange() {
switch (document.visibilityState) {
// page restored without reloading via history navigation (currently only in FF)
// the catch here is that DOM may be outdated so we'll at least refresh the just edited style
// assuming other changes aren't important enough to justify making a complicated DOM sync
case 'visible':
if (sessionStorage.justEditedStyleId) {
API.getStyle(Number(sessionStorage.justEditedStyleId), true)
.then(style => {
handleUpdate(style, {method: 'styleUpdated'});
});
delete sessionStorage.justEditedStyleId;
}
break;
// going away
case 'hidden':
history.replaceState({scrollY: window.scrollY}, document.title);
break;
}
}
function highlightEditedStyle() {
if (!sessionStorage.justEditedStyleId) return;
const entry = $(ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId);
delete sessionStorage.justEditedStyleId;
if (entry) {
animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
}
}
function embedOptions() {
let options = $('#stylus-embedded-options');
if (!options) {
options = document.createElement('iframe');
options.id = 'stylus-embedded-options';
options.src = '/options.html';
document.documentElement.appendChild(options);
}
options.focus();
}
function unembedOptions() {
const options = $('#stylus-embedded-options');
if (options) {
options.contentWindow.document.body.classList.add('scaleout');
options.classList.add('fadeout');
animateElement(options, {
className: 'fadeout',
onComplete: () => options.remove(),
});
}
}
router.watch({hash: '#stylus-options'}, state => {
if (state) {
embedOptions();
} else {
unembedOptions();
}
});
window.addEventListener('closeOptions', () => {
router.updateHash('');
});

View File

@ -1,12 +1,22 @@
/* global installed messageBox t $ $create prefs */
/* global installed t $ prefs semverCompare */
/* exported sorter */
'use strict';
const sorter = (() => {
// Set up for only one column
const defaultSort = 'title,asc';
const sortOrder = [
'asc',
'desc',
'' // unsorted
];
const sorterType = {
alpha: (a, b) => a < b ? -1 : a === b ? 0 : 1,
number: (a, b) => (a || 0) - (b || 0),
semver: (a, b) => semverCompare(a, b)
};
const tagData = {
@ -20,113 +30,129 @@ const sorter = (() => {
parse: ({style}) => style.usercssData ? 0 : 1,
sorter: sorterType.number
},
enabled: {
text: t('genericEnabledLabel'),
parse: ({style}) => style.enabled ? 0 : 1,
sorter: sorterType.number
},
disabled: {
text: '', // added as either "enabled" or "disabled" by the addOptions function
text: t('genericDisabledLabel'),
parse: ({style}) => style.enabled ? 1 : 0,
sorter: sorterType.number
},
version: {
text: '#',
parse: ({style}) => (style.usercssData && style.usercssData.version || ''),
sorter: sorterType.semver
},
dateInstalled: {
text: t('dateInstalled'),
parse: ({style}) => style.installDate,
parse: ({style}) => style.installDate || '',
sorter: sorterType.number
},
dateUpdated: {
text: t('dateUpdated'),
parse: ({style}) => style.updateDate,
parse: ({style}) => style.updateDate || '',
sorter: sorterType.number
}
};
// Adding (assumed) most commonly used ('title,asc' should always be first)
// whitespace before & after the comma is ignored
const selectOptions = [
'{groupAsc}',
'title,asc',
'dateInstalled,desc, title,asc',
'dateInstalled,asc, title,asc',
'dateUpdated,desc, title,asc',
'dateUpdated,asc, title,asc',
'usercss,asc, title,asc',
'usercss,desc, title,asc',
'disabled,asc, title,asc',
'disabled,desc, title,asc',
'disabled,desc, usercss,asc, title,asc',
'{groupDesc}',
'title,desc',
'usercss,asc, title,desc',
'usercss,desc, title,desc',
'disabled,desc, title,desc',
'disabled,desc, usercss,asc, title,desc'
];
const splitRegex = /\s*,\s*/;
const whitespace = /\s+/g;
let columns = 1;
function addOptions() {
let container;
const select = $('#manage.newUI.sort');
const renderBin = document.createDocumentFragment();
const option = $create('option');
const optgroup = $create('optgroup');
const meta = {
desc: ' \u21E9',
enabled: t('genericEnabledLabel'),
disabled: t('genericDisabledLabel'),
dateNew: ` (${t('sortDateNewestFirst')})`,
dateOld: ` (${t('sortDateOldestFirst')})`,
groupAsc: t('sortLabelTitleAsc'),
groupDesc: t('sortLabelTitleDesc')
};
const optgroupRegex = /\{\w+\}/;
selectOptions.forEach(sort => {
if (optgroupRegex.test(sort)) {
if (container) {
renderBin.appendChild(container);
}
container = optgroup.cloneNode();
container.label = meta[sort.substring(1, sort.length - 1)];
return;
}
let lastTag = '';
const opt = option.cloneNode();
opt.textContent = sort.split(splitRegex).reduce((acc, val) => {
if (tagData[val]) {
lastTag = val;
return acc + (acc !== '' ? ' + ' : '') + tagData[val].text;
}
if (lastTag.indexOf('date') > -1) return acc + meta[val === 'desc' ? 'dateNew' : 'dateOld'];
if (lastTag === 'disabled') return acc + meta[val === 'desc' ? 'enabled' : 'disabled'];
return acc + (meta[val] || '');
}, '');
opt.value = sort;
container.appendChild(opt);
});
renderBin.appendChild(container);
select.appendChild(renderBin);
select.value = prefs.get('manage.newUI.sort');
}
let lastSort;
function sort({styles}) {
const sortBy = prefs.get('manage.newUI.sort').split(splitRegex);
let sortBy = prefs.get('manage.newUI.sort').replace(whitespace, '');
if (lastSort === sortBy) {
return styles;
}
sortBy = sortBy.split(splitRegex);
updateHeaders(sortBy);
// Always append an ascending title (default) sort to keep sorts consistent; but don't
// show it in the header
sortBy = sortBy.concat(defaultSort.split(splitRegex));
const len = sortBy.length;
// Add first column sort to #installed; show sortable column when id column sorted
installed.dataset.sort = sortBy[0];
return styles.sort((a, b) => {
let types, direction;
let types, direction, x, y;
let result = 0;
let index = 0;
// multi-sort
while (result === 0 && index < len) {
types = tagData[sortBy[index++]];
direction = sortBy[index++] === 'asc' ? 1 : -1;
result = types.sorter(types.parse(a), types.parse(b)) * direction;
x = types.parse(a);
// sort empty values to the bottom
if (x === '') {
return 1;
}
y = types.parse(b);
if (y === '') {
return -1;
}
result = types.sorter(x, y) * direction;
}
return result;
});
}
// Update default sort on init & when all other columns are unsorted
function updateHeaders(sortBy) {
let header, sortDir;
let i = 0;
const len = sortBy.length;
while (i < len) {
header = $(`.entry-header [data-type="${sortBy[i++]}"]`);
sortDir = sortBy[i++];
if (header) {
header.dataset.sortDir = sortDir;
}
}
}
function updateSort(event) {
const sortables = $$('.entry-header .sortable');
const elm = event.target;
// default sort column only allows asc, desc; not unsorted
const len = sortOrder.length - (elm.dataset.type === defaultSort.split(splitRegex)[0] ? 1 : 0);
let index = (sortOrder.indexOf(elm.dataset.sortDir) + 1) % len;
// shift key for multi-column sorting
if (!event.shiftKey) {
sortables.forEach(el => {
el.dataset.sortDir = '';
el.dataset.timestamp = '';
});
}
elm.dataset.sortDir = sortOrder[index];
elm.dataset.timestamp = Date.now();
const newSort = sortables
.filter(el => el.dataset.sortDir !== '')
.reduce((acc, el) => {
const {sortDir, type, timestamp = new Date()} = el.dataset;
if (sortDir) {
acc.push({sortDir, type, timestamp: parseFloat(timestamp)});
}
return acc;
}, [])
.sort((a, b) => a.timestamp - b.timestamp)
.reduce((acc, item) => {
acc = acc.concat(item.type, item.sortDir);
return acc;
}, [])
.join(',');
prefs.set('manage.newUI.sort', newSort || defaultSort);
}
function update() {
if (!installed) return;
const current = [...installed.children];
current.shift(); // remove header
const sorted = sort({
styles: current.map(entry => ({
entry,
@ -148,7 +174,7 @@ const sorter = (() => {
let isOdd = false;
const flipRows = columns % 2 === 0;
for (const {classList} of installed.children) {
if (classList.contains('hidden')) continue;
if (classList.contains('hidden') || classList.contains('entry-header')) continue;
classList.toggle('odd', isOdd);
classList.toggle('even', !isOdd);
if (flipRows && ++index >= columns) {
@ -163,7 +189,8 @@ const sorter = (() => {
let newValue = 1;
for (let el = document.documentElement.lastElementChild;
el.localName === 'style';
el = el.previousElementSibling) {
el = el.previousElementSibling
) {
if (el.textContent.includes('--columns:')) {
newValue = Math.max(1, getComputedStyle(document.documentElement).getPropertyValue('--columns') | 0);
break;
@ -175,25 +202,10 @@ const sorter = (() => {
}
}
function showHelp(event) {
event.preventDefault();
messageBox({
className: 'help-text',
title: t('sortStylesHelpTitle'),
contents:
$create('div',
t('sortStylesHelp').split('\n').map(line =>
$create('p', line))),
buttons: [t('confirmOK')],
});
}
function init() {
prefs.subscribe(['manage.newUI.sort'], update);
$('#sorter-help').onclick = showHelp;
addOptions();
updateColumnCount();
}
return {init, update, sort, updateStripes};
return {init, update, sort, updateSort, updateStripes};
})();

187
manage/tooltips.css Normal file
View File

@ -0,0 +1,187 @@
:root {
--tooltip-bkgd: #555;
--tooltip-border: #777;
--tooltip-text: #fff;
--tooltip-error: #d40000;
--tooltip-warn: goldenrod;
}
[data-title] {
position: relative;
overflow: visible;
}
[data-title]:after {
-webkit-font-smoothing: subpixel-antialiased;
background: var(--tooltip-bkgd);
border: 1px solid var(--tooltip-border);
border-radius: 3px;
color: var(--tooltip-text);
content: attr(data-title);
font-size: 12px;
line-height: 1.5em;
letter-spacing: normal;
padding: .5em .75em;
text-align: center;
text-decoration: none;
text-shadow: none;
text-transform: none;
white-space: pre;
word-break: break-all;
overflow-wrap: break-word;
max-width: 50vw;
z-index: 1000000;
}
.targets [data-title]:after {
white-space: pre-wrap;
min-width: 240px;
}
.entry-last-update[data-title]:after {
text-align: left;
}
[data-title]:after,
[data-title]:before {
display: none;
pointer-events: none;
position: absolute;
}
[data-title]:before {
border: 6px solid transparent;
color: var(--tooltip-bkgd);
content: "";
height: 0;
width: 0;
z-index: 1000001;
}
[data-title]:hover:after,
[data-title]:hover:before {
display: inline-block;
text-decoration: none;
}
[data-title].tt-s:after,
[data-title].tt-se:after,
[data-title].tt-sw:after {
margin-top: 6px;
right: 50%;
top: 100%
}
[data-title].tt-s:before,
[data-title].tt-se:before,
[data-title].tt-sw:before {
border-bottom-color: var(--tooltip-bkgd);
bottom: -7px;
margin-right: -6px;
right: 50%;
top: auto
}
[data-title].tt-se:after {
left: 50%;
margin-left: -16px;
right: auto
}
[data-title].tt-sw:after {
margin-right: -16px
}
[data-title].tt-n:after,
[data-title].tt-ne:after,
[data-title].tt-nw:after {
bottom: 100%;
margin-bottom: 6px;
right: 50%
}
[data-title].tt-n:before,
[data-title].tt-ne:before,
[data-title].tt-nw:before {
border-top-color: var(--tooltip-bkgd);
bottom: auto;
margin-right: -6px;
right: 50%;
top: -7px
}
[data-title].tt-ne:after {
left: 50%;
margin-left: -16px;
right: auto
}
[data-title].tt-nw:after {
margin-right: -16px
}
[data-title].tt-n:after,
[data-title].tt-s:after {
transform: translateX(50%)
}
[data-title].tt-w:after {
bottom: 50%;
margin-right: 6px;
right: 100%;
transform: translateY(50%)
}
[data-title].tt-w:before {
border-left-color: var(--tooltip-bkgd);
bottom: 50%;
left: -7px;
margin-top: -6px;
top: 50%
}
[data-title].tt-e:after {
bottom: 50%;
left: 100%;
margin-left: 6px;
transform: translateY(50%)
}
[data-title].tt-e:before {
border-right-color: var(--tooltip-bkgd);
bottom: 50%;
margin-top: -6px;
right: -7px;
top: 50%
}
.can-update .updater-icons .update:before {
border-right-color: var(--tooltip-warn);
}
.entry-actions .entry-delete:before,
.update-problem .check-update:before {
border-right-color: var(--tooltip-error);
}
@media (max-width: 1100px) {
/* Action icons flip to left side; switch tooltip direction */
#main-actions [data-title].tt-w:after {
bottom: 50%;
left: 100%;
right: auto;
margin-left: 6px;
margin-right: 0;
transform: translateY(50%)
}
#main-actions [data-title].tt-w:before {
border-left-color: transparent;
border-right-color: var(--tooltip-bkgd);
bottom: 50%;
margin-top: -6px;
left: auto;
right: -7px;
top: 50%
}
}

View File

@ -1,11 +1,12 @@
/* global messageBox ENTRY_ID_PREFIX newUI filtersSelector filterAndAppend
/* global messageBox UI handleEvent filtersSelector filterAndAppend
sorter $ $$ $create API onDOMready scrollElementIntoView t chromeLocal */
/* exported handleUpdateInstalled */
/* exported handleUpdateInstalled resetUpdates */
'use strict';
let updateTimer;
onDOMready().then(() => {
$('#check-all-updates').onclick = checkUpdateAll;
$('#check-all-updates-force').onclick = checkUpdateAll;
$('#check-all-updates-force').onclick = checkUpdateBulk;
$('#apply-all-updates').onclick = applyUpdateAll;
$('#update-history').onclick = showUpdateHistory;
});
@ -26,18 +27,28 @@ 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 checkUpdateAll() {
function checkUpdateBulk() {
clearTimeout(updateTimer);
document.body.classList.add('update-in-progress');
const btnCheck = $('#check-all-updates');
// const btnCheck = $('#check-all-updates');
const btnCheckForce = $('#check-all-updates-force');
const btnApply = $('#apply-all-updates');
const noUpdates = $('#update-all-no-updates');
btnCheck.disabled = true;
const styleIds = $$('.entry-filter-toggle:checked').map(el => el.closest('.entry').styleMeta.id);
// btnCheck.disabled = true;
btnCheckForce.classList.add('hidden');
btnApply.classList.add('hidden');
noUpdates.classList.add('hidden');
const ignoreDigest = this && this.id === 'check-all-updates-force';
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
.map(checkUpdate);
@ -53,10 +64,11 @@ function checkUpdateAll() {
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updateCheckAll({
API.updateCheckBulk({
save: false,
observe: true,
ignoreDigest,
styleIds,
});
function observer(info, port) {
@ -82,21 +94,26 @@ function checkUpdateAll() {
port.onMessage.removeListener(observer);
document.body.classList.remove('update-in-progress');
btnCheck.disabled = total === 0;
// btnCheck.disabled = total === 0;
btnApply.disabled = false;
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
if (!updated) {
noUpdates.dataset.skippedEdited = skippedEdited > 0;
if (skippedEdited > 0) {
noUpdates.dataset.title = t('updateAllCheckSucceededSomeEdited');
}
noUpdates.classList.remove('hidden');
btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
updateTimer = setTimeout(() => {
noUpdates.classList.add('hidden');
noUpdates.dataset.title = '';
}, 1e4);
}
}
}
function checkUpdate(entry, {single} = {}) {
$('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', entry).title = '';
$('.check-update', entry).dataset.title = t('checkingForUpdate');
if (single) {
API.updateCheck({
save: false,
@ -111,7 +128,8 @@ function checkUpdate(entry, {single} = {}) {
function reportUpdateState({updated, style, error, STATES}) {
const isCheckAll = document.body.classList.contains('update-in-progress');
const entry = $(ENTRY_ID_PREFIX + style.id);
const entry = $(UI.ENTRY_ID_PREFIX + style.id);
if (!entry) return;
const newClasses = new Map([
/*
When a style is updated/installed, handleUpdateInstalled() clears "updatable"
@ -130,8 +148,10 @@ function reportUpdateState({updated, style, error, STATES}) {
if (updated) {
newClasses.set('can-update', true);
entry.updatedCode = style;
$('.update-note', entry).textContent = '';
$('#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 ||
@ -155,15 +175,14 @@ function reportUpdateState({updated, style, error, STATES}) {
const message = same ? t('updateCheckSucceededNoUpdate') : error;
newClasses.set('no-update', true);
newClasses.set('update-problem', !same);
$('.update-note', entry).textContent = message;
$('.check-update', entry).title = newUI.enabled ? message : '';
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
$('.check-update', entry).dataset.title = UI.enabled ? message : '';
$('.update', entry).dataset.title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
// digest may change silently when forcing an update of a locally edited style
// so we need to update it in entry's styleMeta in all open manager tabs
if (error === STATES.SAME_CODE) {
for (const view of chrome.extension.getViews({type: 'tab'})) {
if (view.location.pathname === location.pathname) {
const entry = view.$(ENTRY_ID_PREFIX + style.id);
const entry = view.$(UI.ENTRY_ID_PREFIX + style.id);
if (entry) entry.styleMeta.originalDigest = style.originalDigest;
}
}
@ -196,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');
@ -296,7 +314,6 @@ function handleUpdateInstalled(entry, reason) {
const note = t(isNew ? 'installButtonInstalled' : 'updateCompleted');
entry.classList.add('update-done', ...(isNew ? ['install-done'] : []));
entry.classList.remove('can-update', 'updatable');
$('.update-note', entry).textContent = note;
$('.updated', entry).title = note;
$('.updated', entry).dataset.title = note;
renderUpdatesOnlyFilter();
}

View File

@ -112,6 +112,5 @@
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
"strict_min_version": "53.0"
}
},
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ypG+Z/beZtoYrxxwXYhMwQiAiwRVnSHqdpOSzJdjsXVWdvJjlgEuZcU8kte75w58P45LsRUrwvU6N9x12S6eW84KNEBC8rlZj0RGNoxuhSAcdxneYzjJ9tBkZKOidVedYHHsi3LeaXiLuTNTBR+2lf3uCNcP0ebaFML9uDLdYTGEW4eL3hnEKYPSmT1/xkh4bSGTToCg4YNuWWWoTA0beEOpBWYkPVMarLDQgPzMN5Byu5w3lOS2zL0PPJcmdyk3ez/ZsB4PZKU+h17fVA6+YTvUfxUqLde5i2RiuZhEb6Coo5/W90ZW1yCDC9osjWrxMGYeUMQWHPIgFtDhk4K6QIDAQAB"
}
}

View File

@ -145,8 +145,10 @@ function messageBox({
}
function removeSelf() {
if (messageBox.element) {
messageBox.element.remove();
messageBox.element = null;
}
messageBox.resolve = null;
}
}

View File

@ -187,7 +187,7 @@ function checkUpdates() {
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updateCheckAll({observe: true});
API.updateCheckBulk({observe: true});
function observer(info) {
if ('count' in info) {

View File

@ -0,0 +1,177 @@
/* global messageBox Dropbox createZipFileFromText readZipFileFromBlob
launchWebAuthFlow getRedirectUrlAuthFlow importFromString resolve
$ $create t chromeLocal API getOwnTab */
/* exported exportDropbox */
'use strict';
const DROPBOX_API_KEY = 'zg52vphuapvpng9';
const FILENAME_ZIP_FILE = 'stylus.json';
const DROPBOX_FILE = 'stylus.zip';
const API_ERROR_STATUS_FILE_NOT_FOUND = 409;
const HTTP_STATUS_CANCEL = 499;
function messageProgressBar(data) {
return messageBox({
title: `${data.title}`,
className: 'config-dialog',
contents: [
$create('p', data.text)
],
buttons: [{
textContent: t('confirmClose'),
dataset: {cmd: 'close'},
}],
}).then(() => {
document.body.style.minWidth = '';
document.body.style.minHeight = '';
});
}
function hasDropboxAccessToken() {
return chromeLocal.getValue('dropbox_access_token');
}
function requestDropboxAccessToken() {
const client = new Dropbox.Dropbox({
clientId: DROPBOX_API_KEY,
fetch
});
const authUrl = client.getAuthenticationUrl(getRedirectUrlAuthFlow());
return launchWebAuthFlow({url: authUrl, interactive: true})
.then(urlReturned => {
const params = new URLSearchParams(new URL(urlReturned).hash.replace('#', ''));
chromeLocal.setValue('dropbox_access_token', params.get('access_token'));
return params.get('access_token');
});
}
function uploadFileDropbox(client, stylesText) {
return client.filesUpload({path: '/' + DROPBOX_FILE, contents: stylesText});
}
function exportDropbox(styles) {
const mode = localStorage.installType;
const title = t('syncDropboxStyles');
const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed');
messageProgressBar({title, text});
if (mode !== 'normal') return;
hasDropboxAccessToken()
.then(token => token || requestDropboxAccessToken())
.then(token => {
const client = new Dropbox.Dropbox({
clientId: DROPBOX_API_KEY,
accessToken: token,
fetch
});
return client.filesDownload({path: '/' + DROPBOX_FILE})
.then(() => messageBox.confirm(t('overwriteFileExport')))
.then(ok => {
// deletes file if user want to
if (!ok) {
return Promise.reject({status: HTTP_STATUS_CANCEL});
}
return client.filesDelete({path: '/' + DROPBOX_FILE});
})
// file deleted with success, process styles and create a file
.then(() => {
messageProgressBar({title: title, text: t('gettingStyles')});
return JSON.stringify(styles, null, '\t');
})
// create zip file
.then(stylesText => {
messageProgressBar({title: title, text: t('zipStyles')});
return createZipFileFromText(FILENAME_ZIP_FILE, stylesText);
})
// create file dropbox
.then(zipedText => {
messageProgressBar({title: title, text: t('uploadingFile')});
return uploadFileDropbox(client, zipedText);
})
// gives feedback to user
.then(() => messageProgressBar({title: title, text: t('exportSavedSuccess')}))
// handle not found cases and cancel action
.catch(error => {
console.log(error);
// saving file first time
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
API.getAllStyles()
.then(styles => {
messageProgressBar({title: title, text: t('gettingStyles')});
return JSON.stringify(styles, null, '\t');
})
.then(stylesText => {
messageProgressBar({title: title, text: t('zipStyles')});
return createZipFileFromText(FILENAME_ZIP_FILE, stylesText);
})
.then(zipedText => {
messageProgressBar({title: title, text: t('uploadingFile')});
return uploadFileDropbox(client, zipedText);
})
.then(() => messageProgressBar({title: title, text: t('exportSavedSuccess')}))
.catch(err => messageBox.alert(err));
return;
}
// user cancelled the flow
if (error.status === HTTP_STATUS_CANCEL) {
return;
}
console.error(error);
});
});
};
$('#sync-dropbox-import').onclick = () => {
const mode = localStorage.installType;
const title = t('syncDropboxStyles');
const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed');
messageProgressBar({title, text});
if (mode !== 'normal') return;
hasDropboxAccessToken()
.then(token => token || requestDropboxAccessToken())
.then(token => {
const client = new Dropbox.Dropbox({
clientId: DROPBOX_API_KEY,
accessToken: token,
fetch
});
return client.filesDownload({path: '/' + DROPBOX_FILE})
.then(response => {
messageProgressBar({title: title, text: t('unzipStyles')});
return readZipFileFromBlob(response.fileBlob);
})
.then(zipedFileBlob => {
messageProgressBar({title: title, text: t('readingStyles')});
document.body.style.cursor = 'wait';
const fReader = new FileReader();
fReader.onloadend = event => {
const text = event.target.result;
const maybeUsercss = !/^[\s\r\n]*\[/.test(text) &&
(text.includes('==UserStyle==') || /==UserStyle==/i.test(text));
(!maybeUsercss ?
importFromString(text) :
getOwnTab().then(tab => {
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
return API.openUsercssInstallPage({direct: true, tab})
.then(() => URL.revokeObjectURL(tab.url));
})
).then(numStyles => {
document.body.style.cursor = '';
resolve(numStyles);
});
};
fReader.readAsText(zipedFileBlob, 'utf-8');
})
.catch(error => {
// no file
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
messageBox.alert(t('noFileToImport'));
return;
}
messageBox.alert(error);
});
});
};