This commit is contained in:
derv82 2017-12-04 20:10:52 -08:00
commit f580c10e85
42 changed files with 1209 additions and 886 deletions

View File

@ -55,7 +55,8 @@ globals:
animateElement: false
$: false
$$: false
$element: false
$create: false
$createLink: false
# prefs.js
prefs: false
setupLivePrefs: false
@ -236,7 +237,7 @@ rules:
one-var: [0]
operator-assignment: [2, always]
operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
padded-blocks: [2, never]
padded-blocks: [0]
prefer-numeric-literals: [2]
prefer-rest-params: [0]
prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}]

View File

@ -377,6 +377,10 @@
"message": "Open this style on userstyles.org to customize via 'Advanced Style Settings'",
"description": "Tooltip for a button that opens style on userstyles.org for customizing"
},
"filteredStylesAllHidden": {
"message": "Currently applied filters match no styles",
"description": "Text shown when no styles match currently applied filter in the style manager"
},
"findStylesForSite": {
"message": "Find styles",
"description": "Text for a link that gets a list of styles for the current site"
@ -774,6 +778,10 @@
"message": "Invalid regexps skipped",
"description": "RegExp test report: label for the invalid expressions"
},
"styleRegexpTestNote": {
"message": "Note: use a single \\ for escaping in the regexp input field, which will be automatically converted to \\\\ in the style code as per specification for quoted strings in CSS.",
"description": "RegExp test report: a note displayed at the bottom of the dialog"
},
"styleRegexpPartialExplanation": {
"message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome which incorrectly checks 'regexp()' rules since the very first version (known bug)."
},

View File

@ -36,7 +36,7 @@
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "安裝",
"message": "安裝樣式",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
@ -47,6 +47,10 @@
"message": "無效的 JSON 格式",
"description": "Setting linter config with invalid JSON"
},
"popupHotkeysTooltip": {
"message": "點選以檢視可用的快捷鍵",
"description": "Tooltip displayed when hovering the right edge of the extension popup"
},
"optionsBadgeNormal": {
"message": "背景顏色",
"description": ""
@ -330,6 +334,10 @@
"message": "顯示生效的樣式數目",
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
},
"popupHotkeysInfo": {
"message": "<1>-<9>、<0>,數字鍵盤也可以 — 切換第 N 個樣式0 是切換到第 10 個)\n<A>-<Z> 切換以該字母為名稱第一個字的樣式\n<Shift> 開啟編輯器而非切換\n<Numpad +> 啟用列出的樣式\n<Numpad > 停用列出的樣式\n<Numpad *> 與 <`> (倒引號)— 切換初始啟用的樣式;不要在彈出式視窗開啟時套用到後來啟用的樣式,這樣您就可以在測試完後復原初始選擇:僅停用全部,然後切換。<Numpad > <Numpad *>\n更多資訊請見 wiki",
"description": "NOTE1: preserve < and > symbols so that <hotkey> is styled as a key.\nNOTE2: the last line is displayed as a text of the link to the wiki page.\nNOTE3: this is the list of hotkeys displayed after clicking the right edge of the extension popup."
},
"cm_lineWrapping": {
"message": "自動換行",
"description": "Label for the checkbox controlling word wrap option for the style editor."
@ -934,7 +942,7 @@
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "重新安裝",
"message": "重新安裝樣式",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
@ -1036,7 +1044,7 @@
"description": ""
},
"installButtonUpdate": {
"message": "更新",
"message": "更新樣式",
"description": "Label for update button"
},
"backupButtons": {
@ -1052,7 +1060,7 @@
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "已安裝",
"message": "樣式已安裝",
"description": "Text displayed when the style is successfully installed"
},
"author": {

View File

@ -238,6 +238,9 @@ function getStyles(options) {
cachedStyles.byId.clear();
for (const style of cachedStyles.list) {
cachedStyles.byId.set(style.id, style);
if (!style.name) {
style.name = 'ID: ' + style.id;
}
}
cachedStyles.mutex.inProgress = false;

View File

@ -56,26 +56,26 @@ var updater = {
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
const maybeUpdate = style.usercssData ? maybeUpdateUsercss : maybeUpdateUSO;
return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
.then(checkIfEdited)
.then(maybeUpdate)
.then(maybeValidate)
return Promise.resolve(style)
.then([calcStyleDigest][!ignoreDigest ? 0 : 'skip'])
.then([checkIfEdited][!ignoreDigest ? 0 : 'skip'])
.then([maybeUpdateUSO, maybeUpdateUsercss][style.usercssData ? 1 : 0])
.then(maybeSave)
.then(saved => {
observer(updater.UPDATED, saved);
updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`);
})
.catch(err => {
observer(updater.SKIPPED, style, err);
err = err === 0 ? 'server unreachable' : err;
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
});
.then(reportSuccess)
.catch(reportFailure);
function reportSuccess(saved) {
observer(updater.UPDATED, saved);
updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`);
}
function reportFailure(err) {
observer(updater.SKIPPED, style, err);
err = err === 0 ? 'server unreachable' : err;
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
}
function checkIfEdited(digest) {
if (ignoreDigest) {
return;
}
if (style.originalDigest && style.originalDigest !== digest) {
return Promise.reject(updater.EDITED);
}
@ -95,6 +95,7 @@ var updater = {
}
function maybeUpdateUsercss() {
// TODO: when sourceCode is > 100kB use http range request(s) for version check
return download(style.updateUrl).then(text => {
const json = usercss.buildMeta(text);
const {usercssData: {version}} = style;
@ -104,6 +105,8 @@ var updater = {
// re-install is invalid in a soft upgrade
if (!ignoreDigest) {
return Promise.reject(updater.SAME_VERSION);
} else if (text === style.sourceCode) {
return Promise.reject(updater.SAME_CODE);
}
break;
case 1:
@ -114,38 +117,31 @@ var updater = {
});
}
function maybeValidate(json) {
if (json.usercssData) {
// usercss is already validated while building
return json;
}
if (!styleJSONseemsValid(json)) {
function maybeSave(json = {}) {
// usercss is already validated while building
if (!json.usercssData && !styleJSONseemsValid(json)) {
return Promise.reject(updater.ERROR_JSON);
}
return json;
}
function maybeSave(json) {
json.id = style.id;
json.updateDate = Date.now();
json.reason = 'update';
// keep current state
delete json.enabled;
// keep local name customizations
delete json.name;
if (styleSectionsEqual(json, style)) {
// JSONs may have different order of items even if sections are effectively equal
// so we'll update the digest anyway
// always update digest even if (save === false)
// update digest even if save === false as there might be just a space added etc.
saveStyle(Object.assign(json, {reason: 'update-digest'}));
return Promise.reject(updater.SAME_CODE);
} else if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(updater.MAYBE_EDITED);
}
if (!save) {
return json;
}
json.reason = 'update';
if (json.usercssData) {
return usercssHelper.save(json);
}
json.name = null; // keep local name customizations
return saveStyle(json);
return !save ? json :
json.usercssData
? usercssHelper.save(json)
: saveStyle(json);
}
function styleJSONseemsValid(json) {

View File

@ -386,6 +386,7 @@
let restorationCounter = 0;
let observing = false;
let sorting = false;
let timer;
// allow any types of elements between ours, except for the following:
const ORDERED_TAGS = ['head', 'body', 'style', 'link'];
@ -395,6 +396,10 @@
function init() {
docRootObserver = new MutationObserver(sortStyleElements);
Object.assign(docRootObserver, {start, stop});
if (!chrome.app) {
// compensate for Firefox missing a step when loading the tab in background
setTimeout(sortStyleElements);
}
}
function start({sort = false} = {}) {
if (sort && sortStyleMap()) {
@ -453,8 +458,8 @@
if (sorting) {
sorting = false;
docRootObserver.takeRecords();
setTimeout(start);
//docRootObserver.start();
clearTimeout(timer);
timer = setTimeout(start);
}
}
function isMovable(el) {

125
edit.html
View File

@ -3,6 +3,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="edit/edit.css">
<style id="firefox-transitions-bug-suppressor">
/* restrict to FF */
@supports (-moz-appearance:none) {
@ -21,7 +24,6 @@
<script src="js/script-loader.js"></script>
<script src="js/moz-parser.js"></script>
<script src="content/apply.js"></script>
<link rel="stylesheet" href="edit/edit.css">
<script src="edit/lint.js"></script>
<script src="edit/util.js"></script>
<script src="edit/regexp-tester.js"></script>
@ -70,17 +72,19 @@
<script src="edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css">
<link id="cm-theme" rel="stylesheet">
<template data-id="appliesTo">
<li>
<select name="applies-type" class="applies-type style-contributor">
<option value="url" i18n-text="appliesUrlOption"></option>
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
<option value="domain" i18n-text="appliesDomainOption"></option>
<option value="regexp" i18n-text="appliesRegexpOption"></option>
</select>
<input name="applies-value" class="applies-value style-contributor">
<div class="select-resizer">
<select name="applies-type" class="applies-type style-contributor">
<option value="url" i18n-text="appliesUrlOption"></option>
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
<option value="domain" i18n-text="appliesDomainOption"></option>
<option value="regexp" i18n-text="appliesRegexpOption"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
<button class="remove-applies-to" i18n-text="appliesRemove"></button>
<button class="add-applies-to" i18n-text="appliesAdd"></button>
</li>
@ -157,7 +161,7 @@
<thead>
<tr>
<th><input i18n-placeholder="helpKeyMapHotkey" type="search" class="can-close-on-esc"></th>
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc"></th>
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc" spellcheck="false"></th>
</tr>
</thead>
<tbody>
@ -179,8 +183,10 @@
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div>
<div id="basic-info-enabled">
<input type="checkbox" id="enabled" class="style-contributor">
<label for="enabled" id="enabled-label" i18n-text="styleEnabledLabel"></label><!--
<label id="enabled-label" i18n-text="styleEnabledLabel">
<input type="checkbox" id="enabled" class="style-contributor">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label><!--
--><svg id="toggle-style-help" class="svg-icon info">
<use xlink:href="#svg-icon-help"/>
</svg>
@ -201,30 +207,40 @@
<details id="options" data-pref="editor.options.expanded">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
<div class="option">
<input id="editor.lineWrapping" type="checkbox">
<label id="lineWrapping-label" for="editor.lineWrapping" i18n-text="cm_lineWrapping"></label>
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
<input id="editor.lineWrapping" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<input id="editor.smartIndent" type="checkbox">
<label id="smartIndent-label" for="editor.smartIndent" i18n-text="cm_smartIndent"></label>
<label id="smartIndent-label" i18n-text="cm_smartIndent">
<input id="editor.smartIndent" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<input id="editor.indentWithTabs" type="checkbox">
<label id="indentWithTabs-label" for="editor.indentWithTabs" i18n-text="cm_indentWithTabs"></label>
<label id="indentWithTabs-label" i18n-text="cm_indentWithTabs">
<input id="editor.indentWithTabs" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<input id="editor.autoCloseBrackets" type="checkbox">
<label for="editor.autoCloseBrackets"
i18n-text="cm_autoCloseBrackets"
i18n-title="cm_autoCloseBracketsTooltip"></label>
<label i18n-text="cm_autoCloseBrackets" i18n-title="cm_autoCloseBracketsTooltip">
<input id="editor.autoCloseBrackets" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<input id="editor.autocompleteOnTyping" type="checkbox">
<label for="editor.autocompleteOnTyping" i18n-text="cm_autocompleteOnTyping"></label>
<label i18n-text="cm_autocompleteOnTyping">
<input id="editor.autocompleteOnTyping" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<input id="editor.colorpicker" type="checkbox">
<label for="editor.colorpicker" i18n-text="cm_colorpicker"></label>
<label i18n-text="cm_colorpicker">
<input id="editor.colorpicker" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<span class="svg-inline-wrapper" i18n-title="shortcutsNote">
<svg id="colorpicker-settings" class="svg-icon settings">
<use xlink:href="#svg-icon-settings"/>
@ -232,10 +248,10 @@
</span>
</div>
<div class="option usercss-only">
<input id="editor.appliesToLineWidget" type="checkbox">
<label for="editor.appliesToLineWidget"
i18n-text="appliesLineWidgetLabel"
i18n-title="appliesLineWidgetWarning"></label>
<label i18n-text="appliesLineWidgetLabel" i18n-title="appliesLineWidgetWarning">
<input id="editor.appliesToLineWidget" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option aligned">
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
@ -243,30 +259,42 @@
</div>
<div class="option aligned">
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
<select id="editor.keyMap"></select>
<div class="select-resizer">
<select id="editor.keyMap"></select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<span class="svg-inline-wrapper">
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</span>
</div>
<div class="option aligned">
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
<select id="editor.theme"></select>
<div class="select-resizer">
<select id="editor.theme"></select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<div class="option aligned">
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
<select id="editor.matchHighlight">
<option i18n-text="cm_matchHighlightToken" value="token">
<option i18n-text="cm_matchHighlightSelection" value="selection">
<option i18n-text="genericDisabledLabel" value="">
</select>
<div class="select-resizer">
<select id="editor.matchHighlight">
<option i18n-text="cm_matchHighlightToken" value="token">
<option i18n-text="cm_matchHighlightSelection" value="selection">
<option i18n-text="genericDisabledLabel" value="">
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<div class="option aligned">
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
<select id="editor.linter">
<option value="csslint" selected>CSSLint</option>
<option value="stylelint">Stylelint</option>
<option value="" i18n-text="genericDisabledLabel"></option>
</select>
<div class="select-resizer">
<select id="editor.linter">
<option value="csslint" selected>CSSLint</option>
<option value="stylelint">Stylelint</option>
<option value="" i18n-text="genericDisabledLabel"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<span class="svg-inline-wrapper" i18n-title="linterConfigTooltip">
<svg id="linter-settings" class="svg-icon settings">
<use xlink:href="#svg-icon-settings"/>
@ -299,18 +327,31 @@
</div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
<symbol id="svg-icon-external-link" viewBox="0 0 8 8">
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
</symbol>
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n-alt="helpAlt">
<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-close" viewBox="0 0 12 16">
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
</symbol>
<symbol id="svg-icon-settings" viewBox="0 0 16 16">
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.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"/>
</symbol>
<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"/>
</symbol>
</svg>
</body>

View File

@ -1,79 +1,12 @@
/* global regExpTester debounce messageBox CodeMirror */
/* global regExpTester debounce messageBox CodeMirror template */
'use strict';
function templateCache(cache) {
function clone(id) {
if (typeof cache[id] === 'function') {
cache[id] = cache[id]();
}
return cache[id].cloneNode(true);
}
return {clone};
}
function createAppliesToLineWidget(cm) {
const APPLIES_TYPE = [
[t('appliesUrlOption'), 'url'],
[t('appliesUrlPrefixOption'), 'url-prefix'],
[t('appliesDomainOption'), 'domain'],
[t('appliesRegexpOption'), 'regexp']
];
const THROTTLE_DELAY = 400;
let TPL, EVENTS, CLICK_ROUTE;
let widgets = [];
let fromLine, toLine, styleVariables;
let initialized = false;
const template = templateCache({
container: () =>
$element({className: 'applies-to', appendChild: [
$element({tag: 'label', appendChild: t('appliesLabel')}),
$element({
tag: 'ul',
className: 'applies-to-list'
})
]}),
listItem: () =>
$element({tag: 'li', appendChild: [
$element({
tag: 'select',
className: 'applies-type',
appendChild: APPLIES_TYPE.map(([label, value]) => $element({
tag: 'option',
value: value,
textContent: label
}))
}),
$element({
tag: 'input',
className: 'applies-value'
}),
$element({
tag: 'button',
type: 'button',
className: 'applies-to-regexp-test',
textContent: t('styleRegexpTestButton')
}),
$element({
tag: 'button',
type: 'button',
className: 'applies-to-remove',
textContent: t('appliesRemove')
}),
$element({
tag: 'button',
type: 'button',
className: 'applies-to-add',
textContent: t('appliesAdd')
})
]}),
appliesToEverything: () =>
$element({
tag: 'li',
className: 'applies-to-everything',
textContent: t('appliesToEverything')
})
});
return {toggle};
function toggle(newState = !initialized) {
@ -90,7 +23,120 @@ function createAppliesToLineWidget(cm) {
function init() {
initialized = true;
styleVariables = $element({tag: 'style'});
TPL = {
container:
$create('div.applies-to', [
$create('label', t('appliesLabel')),
$create('ul.applies-to-list'),
]),
listItem:
$create('li.applies-to-item', [
$create('select.applies-type', [
$create('option', {value: 'url'}, t('appliesUrlOption')),
$create('option', {value: 'url-prefix'}, t('appliesUrlPrefixOption')),
$create('option', {value: 'domain'}, t('appliesDomainOption')),
$create('option', {value: 'regexp'}, t('appliesRegexpOption')),
]),
$create('input.applies-value', {spellcheck: false}),
$create('button.test-regexp', t('styleRegexpTestButton')),
$create('button.remove-applies-to', t('appliesRemove')),
$create('button.add-applies-to', t('appliesAdd')),
]),
appliesToEverything:
$create('li.applies-to-everything', t('appliesToEverything')),
};
CLICK_ROUTE = {
'.test-regexp': (item, apply) => {
regExpTester.toggle();
regExpTester.update([apply.value.text]);
},
'.remove-applies-to': (item, apply) => {
const applies = item.closest('.applies-to').__applies;
const i = applies.indexOf(apply);
let repl;
let from;
let to;
if (applies.length < 2) {
messageBox({
contents: t('appliesRemoveError'),
buttons: [t('confirmClose')]
});
return;
}
if (i === 0) {
from = apply.mark.find().from;
to = applies[i + 1].mark.find().from;
repl = '';
} else if (i === applies.length - 1) {
from = applies[i - 1].mark.find().to;
to = apply.mark.find().to;
repl = '';
} else {
from = applies[i - 1].mark.find().to;
to = applies[i + 1].mark.find().from;
repl = ', ';
}
cm.replaceRange(repl, from, to, 'appliesTo');
clearApply(apply);
item.remove();
applies.splice(i, 1);
},
'.add-applies-to': (item, apply) => {
const applies = this.closest('.applies-to').__applies;
const i = applies.indexOf(apply);
const pos = apply.mark.find().to;
const text = `, ${apply.type.text}("")`;
cm.replaceRange(text, pos, pos, 'appliesTo');
const newApply = createApply(
cm.indexFromPos(pos) + 2,
apply.type.text,
'',
true
);
setupApplyMarkers(newApply);
applies.splice(i + 1, 0, newApply);
item.insertAdjacentElement('afterend', buildChildren(applies, newApply));
},
};
EVENTS = {
onchange({target}) {
const typeElement = target.closest('.applies-type');
if (typeElement) {
const item = target.closest('.applies-to-item');
const apply = item.__apply;
changeItem(apply, 'type', typeElement.value);
item.dataset.type = apply.type.text;
}
},
oninput({target}) {
if (target.matches('.applies-value')) {
const apply = target.closest('.applies-to-item').__apply;
debounce(changeItem, THROTTLE_DELAY, apply, 'value', target.value);
}
},
onfocus({target}) {
if (target.matches('.test-regexp')) {
const apply = target.closest('.applies-to-item').__apply;
updateRegexpTest(apply);
}
},
onclick({target}) {
for (const selector in CLICK_ROUTE) {
const routed = target.closest(selector);
if (routed) {
const item = routed.closest('.applies-to-item');
CLICK_ROUTE[selector].call(routed, item, item.__apply);
return;
}
}
}
};
styleVariables = $create('style');
fromLine = 0;
toLine = cm.doc.size;
@ -175,7 +221,7 @@ function createAppliesToLineWidget(cm) {
inOp = true;
cm.startOperation();
}
cm.operation(doUpdate);
doUpdate();
}
if (inOp) {
cm.endOperation();
@ -245,26 +291,29 @@ function createAppliesToLineWidget(cm) {
let i = 0;
let itemHeight;
for (const section of findAppliesTo(start, end)) {
while (removed[i] && removed[i].line.lineNo() < section.pos.line) {
clearWidget(removed[i++]);
let removedWidget = removed[i];
while (removedWidget && removedWidget.line.lineNo() < section.pos.line) {
clearWidget(removed[i]);
removedWidget = removed[++i];
}
for (const a of section.applies) {
setupApplyMarkers(a, lineIndexes);
}
if (removed[i] && removed[i].line.lineNo() === section.pos.line) {
if (removedWidget && removedWidget.line.lineNo() === section.pos.line) {
// reuse old widget
removed[i].section.applies.forEach(apply => {
removedWidget.section.applies.forEach(apply => {
apply.type.mark.clear();
apply.value.mark.clear();
});
removed[i].section = section;
removedWidget.section = section;
const newNode = buildElement(section);
if (removed[i].node.parentNode) {
removed[i].node.parentNode.replaceChild(newNode, removed[i].node);
const removedNode = removedWidget.node;
if (removedNode.parentNode) {
removedNode.parentNode.replaceChild(newNode, removedNode);
}
removed[i].node = newNode;
removed[i].changed();
yield removed[i];
removedWidget.node = newNode;
removedWidget.changed();
yield removedWidget;
i++;
continue;
}
@ -337,138 +386,71 @@ function createAppliesToLineWidget(cm) {
}
function buildElement({applies}) {
const el = template.clone('container');
const appliesToList = $('.applies-to-list', el);
applies.map(makeLi)
.forEach(item => appliesToList.appendChild(item));
if (!appliesToList.childNodes.length) {
appliesToList.appendChild(template.clone('appliesToEverything'));
const container = TPL.container.cloneNode(true);
const list = $('.applies-to-list', container);
for (const apply of applies) {
list.appendChild(buildChildren(applies, apply));
}
if (!list.children[0]) {
list.appendChild(TPL.appliesToEverything.cloneNode(true));
}
return Object.assign(container, EVENTS, {__applies: applies});
}
function buildChildren(applies, apply) {
const el = TPL.listItem.cloneNode(true);
el.dataset.type = apply.type.text;
el.__apply = apply;
$('.applies-type', el).value = apply.type.text;
$('.applies-value', el).value = apply.value.text;
return el;
}
function makeLi(apply) {
const el = template.clone('listItem');
el.dataset.type = apply.type.text;
el.addEventListener('change', e => {
if (e.target.classList.contains('applies-type')) {
el.dataset.type = apply.type.text;
}
});
function changeItem(apply, part, newText) {
if (!apply) {
return;
}
part = apply[part];
const range = part.mark.find();
part.mark.clear();
newText = newText.replace(/\\/g, '\\\\');
cm.replaceRange(newText, range.from, range.to, 'appliesTo');
part.mark = cm.markText(
range.from,
cm.findPosH(range.from, newText.length, 'char'),
{clearWhenEmpty: false}
);
part.text = newText;
const typeInput = $('.applies-type', el);
typeInput.value = apply.type.text;
typeInput.onchange = function () {
applyChange(apply.type, this.value);
};
if (part === apply.type) {
const range = apply.mark.find();
apply.mark.clear();
apply.mark = cm.markText(
part.mark.find().from,
range.to,
{clearWhenEmpty: false}
);
}
const valueInput = $('.applies-value', el);
valueInput.value = apply.value.text;
valueInput.oninput = function () {
debounce(applyChange, THROTTLE_DELAY, apply.value, this.value);
};
valueInput.onfocus = updateRegexpTest;
updateRegexpTest(apply);
}
const regexpTestButton = $('.applies-to-regexp-test', el);
regexpTestButton.onclick = () => {
regExpTester.toggle();
regExpTester.update([apply.value.text]);
};
const removeButton = $('.applies-to-remove', el);
removeButton.onclick = function () {
const i = applies.indexOf(apply);
let repl;
let from;
let to;
if (applies.length < 2) {
messageBox({
contents: chrome.i18n.getMessage('appliesRemoveError'),
buttons: [t('confirmClose')]
});
return;
}
if (i === 0) {
from = apply.mark.find().from;
to = applies[i + 1].mark.find().from;
repl = '';
} else if (i === applies.length - 1) {
from = applies[i - 1].mark.find().to;
to = apply.mark.find().to;
repl = '';
} else {
from = applies[i - 1].mark.find().to;
to = applies[i + 1].mark.find().from;
repl = ', ';
}
cm.replaceRange(repl, from, to, 'appliesTo');
clearApply(apply);
this.closest('li').remove();
applies.splice(i, 1);
};
const addButton = $('.applies-to-add', el);
addButton.onclick = function () {
const i = applies.indexOf(apply);
const pos = apply.mark.find().to;
const text = `, ${apply.type.text}("")`;
cm.replaceRange(text, pos, pos, 'appliesTo');
const newApply = createApply(
cm.indexFromPos(pos) + 2,
apply.type.text,
'',
true
);
setupApplyMarkers(newApply);
applies.splice(i + 1, 0, newApply);
this.closest('li').insertAdjacentElement('afterend', makeLi(newApply));
};
return el;
function updateRegexpTest() {
if (apply.type.text === 'regexp') {
const re = apply.value.text.trim();
if (re) {
regExpTester.update([re]);
} else {
regExpTester.update([]);
}
}
}
function applyChange(input, newText) {
const range = input.mark.find();
input.mark.clear();
cm.replaceRange(newText, range.from, range.to, 'appliesTo');
input.mark = cm.markText(
range.from,
cm.findPosH(range.from, newText.length, 'char'),
{clearWhenEmpty: false}
);
input.text = newText;
if (input === apply.type) {
const range = apply.mark.find();
apply.mark.clear();
apply.mark = cm.markText(
input.mark.find().from,
range.to,
{clearWhenEmpty: false}
);
}
updateRegexpTest();
}
function updateRegexpTest(apply) {
if (apply.type.text === 'regexp') {
const rx = apply.value.text.trim();
regExpTester.update(rx ? [rx] : {});
}
}
function createApply(pos, typeText, valueText, isQuoted = false) {
typeText = typeText.toLowerCase();
const start = pos;
const typeStart = start;
const typeEnd = typeStart + typeText.length;
const valueStart = typeEnd + 1 + Number(isQuoted);
const valueEnd = valueStart + valueText.length;
const end = valueEnd + Number(isQuoted) + 1;
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/.test(valueText);
return {
start,
type: {
@ -477,7 +459,7 @@ function createAppliesToLineWidget(cm) {
end: typeEnd,
},
value: {
text: valueText,
text: hasSingleEscapes ? valueText : valueText.replace(/\\\\/g, '\\'),
start: valueStart,
end: valueEnd,
},
@ -487,8 +469,12 @@ function createAppliesToLineWidget(cm) {
function *findAppliesTo(posStart, posEnd) {
const text = cm.getValue();
const re = /^[\t ]*@-moz-document\s+/mg;
const applyRe = /(url|url-prefix|domain|regexp)\(((['"])(?:\\\\|\\\n|\\\3|[^\n])*?\3|[^)\n]*)\)[\s,]*/iyg;
const re = /^[\t ]*@-moz-document[\s\n]+/gm;
const applyRe = new RegExp([
/(?:\/\*[^*]*\*\/[\s\n]*)*/,
/(url|url-prefix|domain|regexp)/,
/\(((['"])(?:\\\\|\\\n|\\\3|[^\n])*?\3|[^)\n]*)\)\s*(,\s*)?/,
].map(rx => rx.source).join(''), 'giy');
let match;
re.lastIndex = posStart;
while ((match = re.exec(text))) {

View File

@ -29,12 +29,16 @@ function beautify(event) {
optionHtml('border: none;', 'newline_between_properties', true) +
optionHtml('display: block;', 'newline_before_close_brace', true) +
optionHtml('}', 'newline_between_rules') +
`<label style="display: block; clear: both;"><input data-option="indent_conditional" type="checkbox"
${options.indent_conditional !== false ? 'checked' : ''}>` +
`<label style="display: block; clear: both;">
<input data-option="indent_conditional" type="checkbox"
${options.indent_conditional !== false ? 'checked' : ''}>
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>` +
t('styleBeautifyIndentConditional') + '</label>' +
'</div>' +
'<div><button role="undo"></button></div>');
$('#help-popup').className = 'wide';
const undoButton = $('#help-popup button[role="undo"]');
undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal');
undoButton.addEventListener('click', () => {
@ -87,10 +91,17 @@ function beautify(event) {
const value = options[optionName];
return '<div newline="' + value.toString() + '">' +
'<span' + (indent ? ' indent' : '') + '>' + label + '</span>' +
'<select data-option="' + optionName + '">' +
'<option' + (value ? '' : ' selected') + '>&nbsp;</option>' +
'<option' + (value ? ' selected' : '') + '>\\n</option>' +
'</select></div>';
'<div class="select-resizer">' +
'<select data-option="' + optionName + '">' +
'<option' + (value ? '' : ' selected') + '>&nbsp;</option>' +
'<option' + (value ? ' selected' : '') + '>\\n</option>' +
'</select>' +
'<svg class="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"/>' +
'</svg>' +
'</div>' +
'</div>';
}
}
}

View File

@ -115,10 +115,8 @@ onDOMready().then(() => {
if (option.type === 'checkbox') {
option = (option.labels || [])[0] || option.nextElementSibling || option;
}
progress = document.body.appendChild($element({
className: 'set-option-progress',
targetElement: option,
}));
progress = document.body.appendChild(
$create('.set-option-progress', {targetElement: option}));
}
}
if (progress) {
@ -222,12 +220,7 @@ onDOMready().then(() => {
break;
}
// avoid flicker: wait for the second stylesheet to load, then apply the theme
document.head.appendChild($element({
tag: 'link',
id: 'cm-theme2',
rel: 'stylesheet',
href: url
}));
document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url}));
setTimeout(() => {
CodeMirror.setOption(option, value);
themeLink.remove();
@ -287,7 +280,7 @@ onDOMready().then(() => {
function optionsFromArray(parent, options) {
const fragment = document.createDocumentFragment();
for (const opt of options) {
fragment.appendChild($element({tag: 'option', textContent: opt}));
fragment.appendChild($create('option', opt));
}
parent.appendChild(fragment);
}
@ -342,16 +335,16 @@ onDOMready().then(() => {
customizeOpenDialog(activeCM, template.find, function (query) {
this(query);
searchState = activeCM.state.search;
if (editors.length === 1 || !searchState.query) {
return;
}
const searchOthers = editors.length > 1 && searchState.query;
editors.forEach(cm => {
if (cm !== activeCM) {
cm.execCommand('clearSearch');
updateState(cm, searchState);
if (searchOthers) {
updateState(cm, searchState);
}
}
});
if (CodeMirror.cmpPos(searchState.posFrom, searchState.posTo) === 0) {
if (searchOthers && CodeMirror.cmpPos(searchState.posFrom, searchState.posTo) === 0) {
findNext(activeCM);
}
});

View File

@ -69,8 +69,7 @@ var initColorpicker = () => {
}
function configureColorpicker() {
const input = $element({
tag: 'input',
const input = $create('input', {
type: 'search',
spellcheck: false,
value: prefs.get('editor.colorpicker.hotkey'),

View File

@ -27,6 +27,20 @@ body {
display: none !important;
}
/************ checkbox & select************/
#options > div[class="option"] {
margin-bottom: 4px;
}
#basic-info-enabled {
margin-top: 2px;
}
label {
padding-left: 16px;
position: relative;
}
/************ header ************/
#header {
width: 280px;
@ -61,9 +75,7 @@ body {
margin-top: 0.1rem;
min-height: 1.4rem;
}
input[type="checkbox"] {
margin-left: 0.1rem;
}
/* basic info */
#basic-info {
margin-bottom: 1rem;
@ -136,14 +148,21 @@ input:invalid {
margin-left: -13px;
cursor: pointer;
outline: none;
margin-top: 8px;
margin-bottom: 8px;
}
#header summary h2 {
display: inline-block;
border-bottom: 1px dotted transparent;
margin-top: .1em;
margin-bottom: .1em;
}
#header summary h2:hover {
#header summary:hover h2 {
border-color: #bbb;
}
#header summary svg {
margin-top: -3px;
}
@ -159,8 +178,9 @@ input:invalid {
text-align: left;
padding-left: .25em;
}
#options .option.aligned > * {
padding-right: 0.25rem;
#options .option.aligned > label {
padding: .1rem .25rem 0 0;
vertical-align: middle;
}
.set-option-progress {
position: absolute;
@ -306,7 +326,7 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
align-items: center;
margin-bottom: 0.35rem;
}
.applies-to li > *:not(button) {
.applies-to li > *:not(button):not(.select-resizer) {
flex: auto;
min-height: 1.4rem;
margin-left: 0.35rem;
@ -396,9 +416,12 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
padding: 0.5rem;
z-index: 99;
}
#help-popup.big,
#help-popup.wide {
max-width: 100%;
}
#help-popup.big {
box-shadow: rgba(0, 0, 0, 0.45) 0px 0px 0px 100000px !important;
max-width: 100%;
left: calc(280px - 3rem);
}
#help-popup.big .CodeMirror {
@ -526,10 +549,11 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
/************ CSS beautifier ************/
.beautify-options {
white-space: nowrap;
font-family: monospace;
}
.beautify-options div {
float: left;
display: flex;
align-items: center;
}
.beautify-options div[newline="true"] + div {
clear: left;
@ -537,18 +561,30 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
.beautify-options div[newline="true"] + div span[indent] {
padding-left: 2rem;
}
.beautify-options > label {
top: 1em;
margin: 1ex 0;
}
.firefox .beautify-options > label input {
top: 1px;
}
.beautify-options:after {
clear: both;
display: block;
content: " ";
content: "";
height: 1rem;
}
.beautify-options span {
font-weight: bold;
font-family: monospace;
}
.beautify-options select {
border: none;
background-color: rgba(0, 0, 0, 0.05);
background: linear-gradient(90deg, rgba(0, 0, 0, .05) 18px, rgba(0, 0, 0, .02) 24px);
font-family: monospace;
font-weight: bold;
padding-left: 4px;
margin-left: 4px;
}
/************ single editor **************/
@ -671,12 +707,6 @@ html:not(.usercss) .usercss-only,
#options h2 {
margin: 0 0 .5em;
}
#options .aligned > *:not(.svg-inline-wrapper) {
margin: 1px 0 0 0; /* workaround the flowing-padding column bug in webkit */
padding-right: 0.4rem;
vertical-align: baseline;
min-height: 1.4rem;
}
.option label {
line-height: 1.25rem;
margin: 0;

View File

@ -92,14 +92,36 @@ function preinit() {
});
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
new MutationObserver((mutations, observer) => {
const themeElement = $('#cm-theme');
if (themeElement) {
themeElement.href = prefs.get('editor.theme') === 'default' ? ''
: 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
observer.disconnect();
document.head.appendChild(
$create('link#cm-theme', {
rel: 'stylesheet',
href: prefs.get('editor.theme') === 'default' ? '' :
'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css'
}));
// forcefully break long labels in aligned options to prevent the entire block layout from breaking
onDOMready().then(() => new Promise(requestAnimationFrame)).then(() => {
const maxWidth2ndChild = $$('#options .aligned > :nth-child(2)')
.sort((a, b) => b.offsetWidth - a.offsetWidth)[0].offsetWidth;
const widthFor1stChild = $('#options').offsetWidth - maxWidth2ndChild;
if (widthFor1stChild > 50) {
for (const el of $$('#options .aligned > :nth-child(1)')) {
if (el.offsetWidth > widthFor1stChild) {
el.style.cssText = 'word-break: break-all; hyphens: auto;';
}
}
} else {
const width = $('#options').clientWidth;
document.head.appendChild($create('style', `
#options .aligned > nth-child(1) {
max-width: 70px;
}
#options .aligned > nth-child(2) {
max-width: ${width - 70}px;
}
`));
}
}).observe(document, {subtree: true, childList: true});
});
if (chrome.windows) {
queryTabs({currentWindow: true}).then(tabs => {
@ -387,7 +409,7 @@ function save() {
saveStyleSafe({
id: styleId,
name: name,
name: $('#name').value.trim(),
enabled: $('#enabled').checked,
reason: 'editSave',
sections: getSectionsHashes()
@ -457,22 +479,20 @@ function toMozillaFormat() {
function fromMozillaFormat() {
const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'),
$element({appendChild: [
$element({
tag: 'button',
$create([
$create('button', {
name: 'import-append',
textContent: t('importAppendLabel'),
title: 'Ctrl-Enter:\n' + t('importAppendTooltip'),
onclick: doImport,
}),
$element({
tag: 'button',
$create('button', {
name: 'import-replace',
textContent: t('importReplaceLabel'),
title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'),
onclick: () => doImport({replaceOldStyle: true}),
}),
]}));
]));
const contents = $('.contents', popup);
contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
popup.codebox.focus();
@ -522,10 +542,8 @@ function fromMozillaFormat() {
}
function showError(errors) {
showHelp(t('styleFromMozillaFormatError'), $element({
tag: 'pre',
textContent: Array.isArray(errors) ? errors.join('\n') : errors,
}));
showHelp(t('styleFromMozillaFormatError'),
$create('pre', Array.isArray(errors) ? errors.join('\n') : errors));
}
}
@ -547,7 +565,7 @@ function showToggleStyleHelp() {
function showHelp(title = '', body) {
const div = $('#help-popup');
div.classList.remove('big');
div.className = '';
const contents = $('.contents', div);
contents.textContent = '';
if (body) {
@ -617,7 +635,7 @@ function showCodeMirrorPopup(title, html, options) {
function setGlobalProgress(done, total) {
const progressElement = $('#global-progress') ||
total && document.body.appendChild($element({id: 'global-progress'}));
total && document.body.appendChild($create('#global-progress'));
if (total) {
const progress = (done / Math.max(done, total) * 100).toFixed(1);
progressElement.style.borderLeftWidth = progress + 'vw';

View File

@ -1,7 +1,6 @@
/* global CodeMirror messageBox */
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
/* global loadScript require CSSLint stylelint */
/* global makeLink */
'use strict';
onDOMready().then(loadLinterAssets);
@ -231,9 +230,7 @@ function updateLinter({immediately, linter = linterConfig.getName()} = {}) {
cm.options.gutters = guttersOption;
const el = $('.' + GUTTERS_CLASS, cm.display.gutters);
if (linter && !el) {
cm.display.gutters.appendChild($element({
className: 'CodeMirror-gutter ' + GUTTERS_CLASS
}));
cm.display.gutters.appendChild($create('.CodeMirror-gutter ' + GUTTERS_CLASS));
} else if (!linter && el) {
el.remove();
}
@ -281,9 +278,8 @@ function updateLintReportInternal(scope, {postponeNewIssues} = {}) {
const newMarkers = lintState.stylusMarkers = new Map();
const oldText = (lintState.body || {}).textContentCached || '';
const activeLine = cm.getCursor().line;
const body = !(lintState.marked || {}).length ? {} : $element({
tag: 'tbody',
appendChild: lintState.marked.map(mark => {
const body = !(lintState.marked || {}).length ? {} :
$create('tbody', lintState.marked.map(mark => {
const info = mark.__annotation;
const {line, ch} = info.from;
const isActiveLine = line === activeLine;
@ -294,27 +290,15 @@ function updateLintReportInternal(scope, {postponeNewIssues} = {}) {
oldMarkers.delete(pos);
}
newMarkers.set(pos, message);
return $element({
tag: 'tr',
className: info.severity,
appendChild: [
$element({
tag: 'td',
attributes: {role: 'severity'},
dataset: {rule: info.rule},
appendChild: $element({
className: 'CodeMirror-lint-marker-' + info.severity,
textContent: info.severity,
}),
}),
$element({tag: 'td', attributes: {role: 'line'}, textContent: line + 1}),
$element({tag: 'td', attributes: {role: 'sep'}, textContent: ':'}),
$element({tag: 'td', attributes: {role: 'col'}, textContent: ch + 1}),
$element({tag: 'td', attributes: {role: 'message'}, textContent: message, title}),
],
});
})
});
return $create(`tr.${info.severity}`, [
$create('td', {attributes: {role: 'severity'}, dataset: {rule: info.rule}},
$create('.CodeMirror-lint-marker-' + info.severity, info.severity)),
$create('td', {attributes: {role: 'line'}}, line + 1),
$create('td', {attributes: {role: 'sep'}}, ':'),
$create('td', {attributes: {role: 'col'}}, ch + 1),
$create('td', {attributes: {role: 'message'}, title}, message),
]);
}));
body.textContentCached = body.textContent || '';
lintState.body = body.textContentCached && body;
result.changed |= oldText !== body.textContentCached;
@ -340,14 +324,10 @@ function renderLintReport(someBlockChanged) {
if (!body) {
return;
}
const newBlock = $element({
tag: 'table',
appendChild: [
$element({tag: 'caption', textContent: label + ' ' + (index + 1)}),
body,
],
cm,
});
const newBlock = $create('table', {cm}, [
$create('caption', label + ' ' + (index + 1)),
body,
]);
newContent.appendChild(newBlock);
issueCount += newBlock.rows.length;
@ -386,38 +366,36 @@ function showLintHelp() {
? 'https://stylelint.io/user-guide/rules/'
// some CSSLint rules do not have a url
: 'https://github.com/CSSLint/csslint/issues/535';
let headerLink, template;
let headerLink, template, csslintRules;
if (linter === 'csslint') {
headerLink = makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint');
headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint');
template = ruleID => {
const rule = linterConfig.allRuleIds.csslint.find(rule => rule.id === ruleID);
const rule = csslintRules.find(rule => rule.id === ruleID);
return rule &&
$element({tag: 'li', appendChild: [
$element({tag: 'b', appendChild: makeLink(rule.url || baseUrl, rule.name)}),
$element({tag: 'br'}),
$create('li', [
$create('b', $createLink(rule.url || baseUrl, rule.name)),
$create('br'),
rule.desc,
]});
]);
};
} else {
headerLink = makeLink(baseUrl, 'stylelint');
headerLink = $createLink(baseUrl, 'stylelint');
template = rule =>
$element({
tag: 'li',
appendChild: makeLink(baseUrl + rule, rule),
});
$create('li',
rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule));
}
const header = t('linterIssuesHelp', '\x01').split('\x01');
const activeRules = new Set($$('#lint td[role="severity"]').map(el => el.dataset.rule));
return showHelp(t('linterIssues'),
$element({appendChild: [
header[0], headerLink, header[1],
$element({
tag: 'ul',
className: 'rules',
appendChild: [...activeRules.values()].map(template),
}),
]})
);
Promise.resolve(linter !== 'csslint' || linterConfig.invokeWorker({action: 'getAllRuleInfos'}))
.then(data => {
csslintRules = data;
showHelp(t('linterIssues'),
$create([
header[0], headerLink, header[1],
$create('ul.rules', [...activeRules.values()].map(template)),
])
);
});
}
function showLinterErrorMessage(title, contents, popup) {
@ -465,41 +443,21 @@ function setupLinterPopup(config) {
});
function makeFooter() {
const makeButton = (className, onclick, text, options = {}) =>
$element(Object.assign(options, {
className,
onclick,
tag: 'button',
type: 'button',
textContent: t(text),
}));
return $element({
appendChild: [
$element({
tag: 'p',
appendChild: [
t('linterRulesLink') + ' ',
$element({
tag: 'a',
target: '_blank',
href: linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
textContent: linterTitle
}),
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : ''
]
}),
makeButton('save', save, 'styleSaveLabel', {title: 'Ctrl-Enter'}),
makeButton('cancel', cancel, 'confirmClose'),
makeButton('reset', reset, 'genericResetLabel', {title: t('linterResetMessage')}),
$element({
tag: 'span',
className: 'saved-message',
textContent: t('genericSavedMessage')
})
]
});
return $create('div', [
$create('p', [
t('linterRulesLink') + ' ',
$createLink(
linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
linterTitle),
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '',
]),
$create('button.save', {onclick: save, title: 'Ctrl-Enter'}, t('styleSaveLabel')),
$create('button.cancel', {onclick: cancel}, t('confirmClose')),
$create('button.reset', {onclick: reset, title: t('linterResetMessage')}, t('genericResetLabel')),
$create('span.saved-message', t('genericSavedMessage')),
]);
}
function save(event) {
@ -516,9 +474,7 @@ function setupLinterPopup(config) {
if (invalid.length) {
showLinterErrorMessage(linter, [
t('linterInvalidConfigError'),
$element({tag: 'ul', appendChild: invalid.map(name =>
$element({tag: 'li', textContent: name})),
}),
$create('ul', invalid.map(name => $create('li', name))),
], popup);
return;
}

View File

@ -123,7 +123,8 @@
const rx = query instanceof RegExp && query;
const sel = this.getSelection();
// current query differs from the selected text => remove the overlay
if (sel && (rx && !rx.test(sel) || sel.toLowerCase() !== query)) {
if (sel && rx && !rx.test(sel) || sel.toLowerCase() !== originalToken.toLowerCase()) {
helper.query = helper.originalToken = sel;
return;
}
// if token under cursor has changed => remove the overlay
@ -160,6 +161,8 @@
// in case the original addon won't highlight anything we need to actually remove the overlays
// by setting a timer that runs in the next event loop cycle and can be canceled in this cycle
hookTimer: setTimeout(removeOverlayIfExpired, 0, this, state),
originalToken,
query,
};
// fool the original addon so it won't invoke state.matchesonscroll.clear()
state.matchesonscroll = null;

View File

@ -25,17 +25,17 @@ var regExpTester = (() => {
}
}
function isShowed() {
function isShown() {
return Boolean($('.regexp-report'));
}
function toggle(state = !isShowed()) {
if (state && !isShowed()) {
function toggle(state = !isShown()) {
if (state && !isShown()) {
if (!isInit) {
init();
}
showHelp('', $element({className: 'regexp-report'}));
} else if (!state && isShowed()) {
showHelp('', $create('.regexp-report'));
} else if (!state && isShown()) {
if (isInit) {
uninit();
}
@ -45,7 +45,7 @@ var regExpTester = (() => {
}
function update(newRegexps) {
if (!isShowed()) {
if (!isShown()) {
if (isInit) {
uninit();
}
@ -108,19 +108,19 @@ var regExpTester = (() => {
const faviconUrl = url.startsWith(URLS.ownOrigin)
? OWN_ICON
: GET_FAVICON_URL + new URL(url).hostname;
const icon = $element({tag: 'img', src: faviconUrl});
const icon = $create('img', {src: faviconUrl});
if (match.text.length === url.length) {
full.push($element({appendChild: [
full.push($create('div', [
icon,
url,
]}));
]));
} else {
partial.push($element({appendChild: [
partial.push($create('div', [
icon,
url.substr(0, match.pos),
$element({tag: 'mark', textContent: match.text}),
$create('mark', match.text),
url.substr(match.pos + match.text.length),
]}));
]));
}
}
if (full.length) {
@ -131,33 +131,28 @@ var regExpTester = (() => {
}
}
// render stats
const report = $element({className: 'regexp-report'});
const br = $element({tag: 'br'});
const report = $create('.regexp-report');
const br = $create('br');
for (const type in stats) {
// top level groups: full, partial, none, invalid
const {label, data} = stats[type];
if (!data.length) {
continue;
}
const block = report.appendChild($element({
tag: 'details',
open: true,
dataset: {type},
appendChild: $element({tag: 'summary', appendChild: label}),
}));
const block = report.appendChild(
$create('details', {open: true, dataset: {type}}, [
$create('summary', label),
]));
// 2nd level: regexp text
for (const {text, urls} of data) {
if (urls) {
// type is partial or full
block.appendChild($element({
tag: 'details',
open: true,
appendChild: [
$element({tag: 'summary', textContent: text}),
block.appendChild(
$create('details', {open: true}, [
$create('summary', text),
// 3rd level: tab urls
...urls,
],
}));
]));
} else {
// type is none or invalid
block.appendChild(document.createTextNode(text));
@ -165,9 +160,14 @@ var regExpTester = (() => {
}
}
}
report.appendChild(
$create('p.regexp-report-note',
t('styleRegexpTestNote')
.split(/(\\+)/)
.map(s => (s.startsWith('\\') ? $create('code', s) : s))));
showHelp(t('styleRegexpTestTitle'), report);
$('.regexp-report').onclick = event => {
report.onclick = event => {
const target = event.target.closest('a, .regexp-report div');
if (target) {
openURL({

View File

@ -390,11 +390,11 @@ function getSections() {
function getSectionsHashes() {
const sections = [];
getSections().forEach(div => {
for (const div of getSections()) {
const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
for (const li of $('.applies-to-list', div).childNodes) {
if (li.className === template.appliesToEverything.className) {
return;
break;
}
const type = $('[name=applies-type]', li).value;
const value = $('[name=applies-value]', li).value;
@ -404,11 +404,11 @@ function getSectionsHashes() {
}
const code = div.CodeMirror.getValue();
if (/^\s*$/.test(code) && Object.keys(meta).length === 0) {
return;
continue;
}
meta.code = code;
sections.push(meta);
});
}
return sections;
}

View File

@ -64,7 +64,7 @@ function showKeyMapHelp() {
if (index > offset) {
cell.appendChild(document.createTextNode(text.substring(offset, index)));
}
cell.appendChild($element({tag: 'mark', textContent: match}));
cell.appendChild($create('mark', match));
offset = index + match.length;
});
if (offset + 1 !== text.length) {

View File

@ -1,7 +1,7 @@
/* global CodeMirror dirtyReporter initLint */
/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
/* global editors linterConfig updateLinter regExpTester mozParser */
/* global makeLink createAppliesToLineWidget messageBox */
/* global createAppliesToLineWidget messageBox */
'use strict';
function createSourceEditor(style) {
@ -12,9 +12,7 @@ function createSourceEditor(style) {
$('#name').disabled = true;
$('#mozilla-format-container').remove();
$('#sections').textContent = '';
$('#sections').appendChild(
$element({className: 'single-editor'})
);
$('#sections').appendChild($create('.single-editor'));
const dirty = dirtyReporter();
dirty.onChange(() => {
@ -236,15 +234,12 @@ function createSourceEditor(style) {
return;
}
const contents = Array.isArray(err) ?
$element({tag: 'pre', textContent: err.join('\n')}) :
$create('pre', err.join('\n')) :
[String(err)];
if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index);
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
contents.push($element({
tag: 'pre',
textContent: drawLinePointer(pos)
}));
contents.push($create('pre', drawLinePointer(pos)));
}
messageBox.alert(contents);
});

95
global.css Normal file
View File

@ -0,0 +1,95 @@
.svg-icon.checked {
position: absolute;
height: 8px;
width: 8px;
display: none;
fill: #000;
margin: 2px 0 0 2px;
}
input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
display: inline-flex;
transition: fill .1s;
}
input[type="checkbox"]:not(.slider) {
position: absolute;
left: 0;
top: 0;
-moz-appearance: none;
-webkit-appearance: none;
pointer-events: none;
border: 1px solid hsl(0, 0%, 46%);
height: 12px;
width: 12px;
display: inline-flex;
border-radius: 2px;
background-color: hsla(0, 0%, 0%, .1);
outline: none;
margin: 0;
transition: background-color .1s, border-color .1s;
}
input[type="checkbox"]:not(.slider):hover {
border-color: hsl(0, 0%, 32%);
background-color: hsl(0, 0%, 82%);
}
input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
label {
transition: color .1s;
}
select {
-moz-appearance: none;
-webkit-appearance: none;
height: 22px;
background-color: transparent;
border: 1px solid hsl(0, 0%, 66%);
padding: 0 20px 0 6px;
transition: color .5s;
}
.firefox select {
padding: 0 20px 0 2px;
}
.select-resizer {
display: inline-flex!important;
cursor: default;
position: relative;
}
.svg-icon.select-arrow {
pointer-events: none;
cursor: default;
display: inline-flex;
height: 14px;
width: 14px;
fill: #000;
position: absolute;
top: 4px;
right: 4px;
transition: fill .5s;
}
@supports (-moz-appearance: none) {
.moz-appearance-bug .svg-icon.checked {
display: none !important;
}
.moz-appearance-bug input[type="checkbox"] {
-moz-appearance: checkbox !important;
}
.moz-appearance-bug button {
padding-left: .75ex;
padding-right: .75ex;
}
}

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Loading...</title>
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="/install-usercss/install-usercss.css">
<script src="/js/messaging.js"></script>
<script src="/js/prefs.js"></script>
@ -61,10 +62,12 @@
<button class="install" i18n-text="installButton"></button>
<label class="set-update-url">
<input type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<span></span>
</label>
<label class="live-reload">
<input type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<span i18n-text="liveReloadLabel"></span>
</label>
</div>
@ -86,6 +89,14 @@
<div class="warnings"></div>
</div>
</div>
<script src="/install-usercss/install-usercss.js"></script>
<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"/>
</symbol>
</svg>
</body>
</html>

View File

@ -239,6 +239,10 @@ li {
user-select: auto;
}
label {
padding-left: 16px;
position: relative;
}
/* spinner: https://github.com/loadingio/css-spinner */
@keyframes lds-spinner {
@ -353,58 +357,6 @@ li {
-webkit-animation-delay: 0s;
animation-delay: 0s;
}
/* https://github.com/lukehaas/css-loaders */
/*
.spinner {
--color: currentColor;
--background-color: currentColor;
font-size: 10px;
margin: 50px auto;
text-indent: -9999em;
width: 200px;
height: 200px;
border-radius: 50%;
background: #ffffff;
background: -moz-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: -webkit-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: -o-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: -ms-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
position: relative;
-webkit-animation: load3 1.4s infinite linear;
animation: load3 1.4s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before {
width: 50%;
height: 50%;
background: #ffffff;
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
.spinner:after {
background: #0dc5c1;
width: 75%;
height: 75%;
border-radius: 50%;
content: '';
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
*/
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);

View File

@ -1,4 +1,4 @@
/* global CodeMirror semverCompare makeLink closeCurrentTab */
/* global CodeMirror semverCompare closeCurrentTab */
/* global messageBox download chromeLocal */
'use strict';
@ -44,11 +44,8 @@
setTimeout(() => {
if (!installed) {
const div = $element({});
$('.header').appendChild($element({
className: 'lds-spinner',
appendChild: new Array(12).fill(div).map(e => e.cloneNode()),
}));
$('.header').appendChild($create('.lds-spinner',
new Array(12).fill($create('div')).map(e => e.cloneNode())));
}
}, 200);
@ -101,8 +98,7 @@
$('.applies-to').textContent = '';
getAppliesTo(style).forEach(pattern =>
$('.applies-to').appendChild($element({tag: 'li', textContent: pattern}))
);
$('.applies-to').appendChild($create('li', pattern)));
$('.external-link').textContent = '';
const externalLink = makeExternalLink();
@ -125,46 +121,35 @@
const [, name, email, url] = match;
const frag = document.createDocumentFragment();
if (email) {
frag.appendChild(makeLink(`mailto:${email}`, name));
frag.appendChild($createLink(`mailto:${email}`, name));
} else {
frag.appendChild($element({
tag: 'span',
textContent: name
}));
frag.appendChild($create('span', name));
}
if (url) {
frag.appendChild(makeLink(
url,
$element({
tag: 'svg#svg',
viewBox: '0 0 20 20',
class: 'svg-icon',
appendChild: $element({
tag: 'svg#path',
frag.appendChild($createLink(url,
$create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'},
$create('SVG:path', {
d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z'
})
})
}))
));
}
return frag;
}
function makeExternalLink() {
const urls = [];
if (data.homepageURL) {
urls.push([data.homepageURL, t('externalHomepage')]);
}
if (data.supportURL) {
urls.push([data.supportURL, t('externalSupport')]);
}
if (urls.length) {
return $element({appendChild: [
$element({tag: 'h3', textContent: t('externalLink')}),
$element({tag: 'ul', appendChild: urls.map(args =>
$element({tag: 'li', appendChild: makeLink(...args)})
)})
]});
}
const urls = [
data.homepageURL && [data.homepageURL, t('externalHomepage')],
data.supportURL && [data.supportURL, t('externalSupport')],
];
return (data.homepageURL || data.supportURL) && (
$create('div', [
$create('h3', t('externalLink')),
$create('ul', urls.map(args => args &&
$create('li',
$createLink(...args)
)
))
]));
}
function installButtonClass() {
@ -220,8 +205,8 @@
function initSourceCode(sourceCode) {
cm.setValue(sourceCode);
cm.refresh();
sendMessage({method: 'buildUsercss', sourceCode, checkDup: true})
.then(init)
BG.usercssHelper.build(BG.deepCopy({sourceCode, checkDup: true}))
.then(r => init(deepCopy(r)))
.catch(err => {
$('.header').classList.add('meta-init-error');
showError(err);
@ -229,10 +214,43 @@
}
function buildWarning(err) {
return $element({className: 'warning', appendChild: [
const contents = Array.isArray(err) ?
$create('pre', err.join('\n')) :
[err && err.message || err || 'Unknown error'];
if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index);
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
contents.push($create('pre', drawLinePointer(pos)));
setTimeout(() => {
cm.scrollIntoView({line: pos.line + 1, ch: pos.ch}, window.innerHeight / 4);
cm.setCursor(pos.line, pos.ch + 1);
cm.focus();
});
}
return $create('.warning', [
t('parseUsercssError'),
$element({tag: 'pre', textContent: String(err)})
]});
'\n',
...contents,
]);
}
function drawLinePointer(pos) {
const SIZE = 60;
const line = cm.getLine(pos.line);
const numTabs = pos.ch + 1 - line.slice(0, pos.ch + 1).replace(/\t/g, '').length;
const pointer = ' '.repeat(pos.ch) + '^';
const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0);
const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length);
const leftPad = start !== 0 ? '...' : '';
const rightPad = end !== line.length ? '...' : '';
return (
leftPad +
line.slice(start, end).replace(/\t/g, ' '.repeat(cm.options.tabSize)) +
rightPad +
'\n' +
' '.repeat(leftPad.length + numTabs * cm.options.tabSize) +
pointer.slice(start, end)
);
}
function init({style, dup}) {
@ -245,7 +263,7 @@
// update UI
if (versionTest < 0) {
$('.actions').parentNode.insertBefore(
$element({className: 'warning', textContent: t('versionInvalidOlder')}),
$create('.warning', t('versionInvalidOlder')),
$('.actions')
);
}
@ -258,9 +276,10 @@
data.version,
]))
).then(ok => ok &&
sendMessage(Object.assign(style, {method: 'saveUsercss', reason: 'update'}))
.then(install)
.catch(err => messageBox.alert(t('styleInstallFailed', err))));
BG.usercssHelper.save(BG.deepCopy(Object.assign(style, {reason: 'update'})))
.then(r => install(deepCopy(r)))
.catch(err => messageBox.alert(t('styleInstallFailed', err)))
);
};
// set updateUrl

View File

@ -73,8 +73,7 @@ if (!chrome.app && chrome.windows) {
}
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
for (const size of [38, 32, 19, 16]) {
document.head.appendChild($element({
tag: 'link',
document.head.appendChild($create('link', {
rel: 'icon',
href: `/images/icon/${iconset}${size}.png`,
sizes: size + 'x' + size,
@ -167,47 +166,103 @@ function $$(selector, base = document) {
}
function $element(opt) {
// tag: string, default 'div', may include namespace like 'ns#tag'
// appendChild: element/string or an array of elements/strings
// dataset: object
// any DOM property: assigned as is
const [ns, tag] = opt.tag && opt.tag.includes('#')
? opt.tag.split('#')
: [null, opt.tag];
function $create(selector = 'div', properties, children) {
/*
$create('tag#id.class.class', ?[children])
$create('tag#id.class.class', ?textContentOrChildNode)
$create('tag#id.class.class', {properties}, ?[children])
$create('tag#id.class.class', {properties}, ?textContentOrChildNode)
tag is 'div' by default, #id and .class are optional
$create([children])
$create({propertiesAndOptions})
$create({propertiesAndOptions}, ?[children])
tag: string, default 'div'
appendChild: element/string or an array of elements/strings
dataset: object
any DOM property: assigned as is
tag may include namespace like 'ns:tag'
*/
let ns, tag, opt;
if (typeof selector === 'string') {
if (Array.isArray(properties) ||
properties instanceof Node ||
typeof properties !== 'object') {
opt = {};
children = properties;
} else {
opt = properties || {};
}
const idStart = (selector.indexOf('#') + 1 || selector.length + 1) - 1;
const classStart = (selector.indexOf('.') + 1 || selector.length + 1) - 1;
const id = selector.slice(idStart + 1, classStart);
if (id) {
opt.id = id;
}
const cls = selector.slice(classStart + 1);
if (cls) {
opt[selector.includes(':') ? 'class' : 'className'] =
cls.includes('.') ? cls.replace(/\./g, ' ') : cls;
}
tag = selector.slice(0, Math.min(idStart, classStart));
} else if (Array.isArray(selector)) {
tag = 'div';
opt = {};
children = selector;
} else {
opt = selector;
tag = opt.tag;
delete opt.tag;
children = opt.appendChild || properties;
delete opt.appendChild;
}
if (tag && tag.includes(':')) {
([ns, tag] = tag.split(':'));
}
const element = ns
? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag)
: document.createElement(tag || 'div');
const children = Array.isArray(opt.appendChild) ? opt.appendChild : [opt.appendChild];
for (const child of children) {
for (const child of Array.isArray(children) ? children : [children]) {
if (child) {
element.appendChild(child instanceof Node ? child : document.createTextNode(child));
}
}
delete opt.appendChild;
delete opt.tag;
if (opt.dataset) {
Object.assign(element.dataset, opt.dataset);
delete opt.dataset;
}
if (opt.attributes) {
for (const attr in opt.attributes) {
element.setAttribute(attr, opt.attributes[attr]);
}
delete opt.attributes;
}
if (ns) {
for (const attr in opt) {
element.setAttributeNS(null, attr, opt[attr]);
const i = attr.indexOf(':') + 1;
const attrNS = i && `http://www.w3.org/1999/${attr.slice(0, i - 1)}`;
element.setAttributeNS(attrNS || null, attr, opt[attr]);
}
} else {
Object.assign(element, opt);
}
return element;
}
function makeLink(href = '', content) {
function $createLink(href = '', content) {
const opt = {
tag: 'a',
target: '_blank',
@ -217,9 +272,9 @@ function makeLink(href = '', content) {
Object.assign(opt, href);
} else {
opt.href = href;
opt.appendChild = content;
}
return $element(opt);
opt.appendChild = opt.appendChild || content;
return $create(opt);
}

View File

@ -17,7 +17,7 @@ if (!CHROME && !chrome.browserAction.openPopup) {
FIREFOX = 50;
browser.runtime.getBrowserInfo().then(info => {
FIREFOX = parseFloat(info.version);
document.documentElement.classList.toggle('moz-appearance-bug', FIREFOX && FIREFOX < 54);
document.documentElement.classList.add('moz-appearance-bug', FIREFOX && FIREFOX < 54);
});
}
@ -62,7 +62,6 @@ if (BG && !BG.getStyles && BG !== window) {
}
if (!BG || BG !== window) {
document.documentElement.classList.toggle('firefox', FIREFOX);
document.documentElement.classList.toggle('moz-appearance-bug', FIREFOX && FIREFOX < 54);
document.documentElement.classList.toggle('opera', OPERA);
// TODO: remove once our manifest's minimum_chrome_version is 50+
// Chrome 49 doesn't report own extension pages in webNavigation apparently

View File

@ -23,10 +23,11 @@ var mozParser = (() => {
const section = {code: '', start: {line, col}};
// move last comment before @-moz-document inside the section
if (!/\/\*[\s\n]*AGENT_SHEET[\s\n]*\*\//.test(lastCmt)) {
section.code = lastCmt + '\n';
const indent = outerText.match(/^\s*/)[0];
outerText = outerText.slice(0, -lastCmt.length);
outerText = indent + outerText.trim();
if (lastCmt) {
section.code = lastCmt + '\n';
outerText = outerText.slice(0, -lastCmt.length);
}
outerText = outerText.match(/^\s*/)[0] + outerText.trim();
}
if (outerText.trim()) {
lastSection.code = outerText;

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title i18n-text="manageTitle"></title>
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="manage/manage.css">
<link rel="stylesheet" href="msgbox/msgbox.css">
<link rel="stylesheet" href="options/onoffswitch.css">
@ -54,7 +55,10 @@
<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">
@ -156,50 +160,85 @@
</head>
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
<div id="header">
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
<fieldset>
<legend id="filters">
<span i18n-text="manageFilters"></span><span id="filters-stats"></span>
</legend>
<label>
<input id="manage.onlyEnabled" type="checkbox"
data-filter=".enabled"
data-filter-hide=".disabled">
<select id="manage.onlyEnabled.invert">
<option i18n-text="manageOnlyEnabled" value="false"></option>
<option i18n-text="manageOnlyDisabled" value="true"></option>
</select>
</label>
<label>
<input id="manage.onlyLocal" type="checkbox"
data-filter=":not(.updatable):not(.update-done)"
data-filter-hide=".updatable, .update-done">
<select id="manage.onlyLocal.invert" i18n-title="manageOnlyLocalTooltip">
<option i18n-text="manageOnlyLocal" value="false"></option>
<option i18n-text="manageOnlyExternal" value="true"></option>
</select>
</label>
<label>
<input id="manage.onlyUsercss" type="checkbox"
data-filter=".usercss"
data-filter-hide=":not(.usercss)">
<select id="manage.onlyUsercss.invert">
<option i18n-text="manageOnlyUsercss" value="false"></option>
<option i18n-text="manageOnlyNonUsercss" value="true"></option>
</select>
</label>
<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>
</label>
<div class="select-resizer">
<select id="manage.onlyEnabled.invert">
<option i18n-text="manageOnlyEnabled" value="false"></option>
<option i18n-text="manageOnlyDisabled" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<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>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</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="onlyUpdates" 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)">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<span i18n-text="manageOnlyUpdates"></span>
</label>
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false"
i18n-title="searchStylesTooltip"
data-filter=":not(.not-matching)"
data-filter-hide=".not-matching">
</fieldset>
<p class="nowrap">
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
<span id="update-history" i18n-title="genericHistoryLabel">
@ -208,17 +247,20 @@
</svg>
</span>
</p>
<p>
<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>
</p>
<p>
<a href="edit.html">
<button id="add-style-label" i18n-text="addStyleLabel"></button>
</a>
<label id="newStyleAsUsercss-wrapper" class="nobreak">
<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"
@ -229,25 +271,38 @@
</a>
</label>
</p>
<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"><span i18n-text="manageNewUI"></span></label>
<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>
<input id="manage.newUI.favicons" type="checkbox">
<label for="manage.newUI.favicons" i18n-text="manageFavicons"></label>
<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>
</label>
<svg class="svg-icon info" viewBox="0 0 14 16" i18n-alt="helpAlt" data-toggle-on-click="#faviconsHelp">
<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>
</svg>
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
<div>
<input id="manage.newUI.faviconsGray" type="checkbox">
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray"></label>
<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>
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
</div>
<p>
<button id="manage-options-button" i18n-text="openOptionsManage"></button>
<button id="manage-shortcuts-button" class="chromium-only"
@ -258,7 +313,9 @@
i18n-title="editorStylesButton"
target="_blank"><button i18n-text="cm_theme"></button></a>
</p>
</details>
<div id="backup">
<h2 id="backup-title" i18n-text="backupButtons"></h2>
<span id="backup-message" i18n-text="backupMessage"></span>
@ -267,13 +324,26 @@
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
</p>
</div>
<p id="manage-text" i18n-html="manageText"></p>
</div>
<div id="installed"></div>
<script src="manage/import-export.js"></script>
<script src="msgbox/msgbox.js"></script>
<script src="manage/incremental-search.js" async></script>
<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"/>
</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"/>
</symbol>
</svg>
</body>
</html>

View File

@ -1,8 +1,9 @@
/* global messageBox makeLink */
/* global messageBox */
'use strict';
function configDialog(style) {
const varsHash = deepCopy(style.usercssData.vars) || {};
const data = style.usercssData;
const varsHash = deepCopy(data.vars) || {};
const varNames = Object.keys(varsHash);
const vars = varNames.map(name => varsHash[name]);
const elements = [];
@ -12,21 +13,12 @@ function configDialog(style) {
renderValues();
return messageBox({
title: `${style.name} v${style.usercssData.version}`,
title: `${style.name} v${data.version}`,
className: 'config-dialog',
contents: [
$element({
className: 'config-heading',
appendChild: style.usercssData.supportURL && makeLink({
className: 'external-support',
href: style.usercssData.supportURL,
textContent: t('externalFeedback')
})
}),
$element({
className: 'config-body',
appendChild: elements
})
$create('.config-heading', data.supportURL &&
$createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))),
$create('.config-body', elements)
],
buttons: [
t('confirmSave'),
@ -71,19 +63,17 @@ function configDialog(style) {
continue;
}
invalid.push(['*' + va.name, ': ', ...error].map(e =>
e[0] === '*' && $element({tag: 'b', textContent: e.slice(1)}) || e));
e[0] === '*' && $create('b', e.slice(1)) || e));
if (bgva) {
styleVars[va.name].value = deepCopy(bgva);
}
}
if (invalid.length) {
messageBox.alert([
$element({textContent: t('usercssConfigIncomplete'), style: 'max-width: 34em'}),
$element({
tag: 'ol',
style: 'text-align: left',
appendChild: invalid.map(msg => $element({tag: 'li', appendChild: msg})),
}),
$create('div', {style: 'max-width: 34em'}, t('usercssConfigIncomplete')),
$create('ol', {style: 'text-align: left'},
invalid.map(msg =>
$create({tag: 'li', appendChild: msg}))),
]);
}
return numValid && BG.usercssHelper.save(style);
@ -91,30 +81,28 @@ function configDialog(style) {
function buildConfigForm() {
for (const va of vars) {
let appendChild;
let children;
switch (va.type) {
case 'color':
appendChild = [$element({
className: 'cm-colorview',
appendChild: va.inputColor = $element({
va,
className: 'color-swatch',
onclick: showColorpicker,
})
})];
va.inputColor = $create('.color-swatch', {va, onclick: showColorpicker});
children = [
$create('.cm-colorview', [
va.inputColor,
]),
];
break;
case 'checkbox':
va.input = $element({tag: 'input', type: 'checkbox', className: 'slider'});
va.input = $create('input.slider', {type: 'checkbox'});
va.input.onchange = () => {
va.dirty = true;
va.value = String(Number(va.input.checked));
};
appendChild = [
$element({tag: 'span', className: 'onoffswitch', appendChild: [
children = [
$create('span.onoffswitch', [
va.input,
$element({tag: 'span'})
]})
$create('span'),
])
];
break;
@ -122,36 +110,33 @@ function configDialog(style) {
case 'dropdown':
case 'image':
// TODO: a image picker input?
va.input = $element({
tag: 'select',
appendChild: va.options.map(o => $element({
tag: 'option', value: o.name, textContent: o.label
}))
});
va.input = $create('.select-resizer', [
$create('select', va.options.map(o =>
$create('option', {value: o.name}, o.label))),
$create('SVG:svg.svg-icon.select-arrow',
$create('SVG:use', {'xlink:href': '#svg-icon-select-arrow'})),
]);
va.input.onchange = () => {
va.dirty = true;
va.value = va.input.value;
};
appendChild = [va.input];
children = [va.input];
break;
default:
va.input = $element({tag: 'input', type: 'text'});
va.input = $create('input', {type: 'text'});
va.input.oninput = () => {
va.dirty = true;
va.value = va.input.value;
};
appendChild = [va.input];
children = [va.input];
break;
}
elements.push($element({
tag: 'label',
className: `config-${va.type}`,
appendChild: [
$element({tag: 'span', appendChild: va.label}),
...appendChild,
],
}));
elements.push(
$create(`label.config-${va.type}`, [
$create('span', va.label),
...children,
]));
}
}

View File

@ -260,6 +260,9 @@ function reapplyFilter(container = installed) {
};
}
}
if (fullPass) {
showFiltersStats({immediately: true});
}
}
function findInsertionPoint(entry) {
@ -295,9 +298,9 @@ function reapplyFilter(container = installed) {
}
function showFiltersStats({immediately} = {}) {
if (!immediately) {
debounce(showFiltersStats, 100, {immediately: true});
function showFiltersStats() {
if (!BG.cachedStyles.list) {
debounce(showFiltersStats, 100);
return;
}
$('#filters').classList.toggle('active', filtersSelector.hide !== '');
@ -309,6 +312,8 @@ function showFiltersStats({immediately} = {}) {
filtersSelector.numShown = numShown;
filtersSelector.numTotal = numTotal;
$('#filters-stats').textContent = t('filteredStyles', [numShown, numTotal]);
document.body.classList.toggle('all-styles-hidden-by-filters',
!numShown && numTotal && filtersSelector.hide);
}
}

View File

@ -175,18 +175,16 @@ function importFromString(jsonString) {
.map(kind => {
const {ids, names, legend} = stats[kind];
const listItemsWithId = (name, i) =>
$element({dataset: {id: ids[i]}, textContent: name});
$create('div', {dataset: {id: ids[i]}}, name);
const listItems = name =>
$element({textContent: name});
$create('div', name);
const block =
$element({tag: 'details', dataset: {id: kind}, appendChild: [
$element({tag: 'summary', appendChild:
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
}),
$element({tag: 'small', appendChild:
names.map(ids ? listItemsWithId : listItems)
}),
]});
$create('details', {dataset: {id: kind}}, [
$create('summary',
$create('b', names.length + ' ' + t(legend))),
$create('small',
names.map(ids ? listItemsWithId : listItems)),
]);
return block;
});
scrollTo(0, 0);
@ -308,8 +306,7 @@ $('#file-all-styles').onclick = () => {
const text = JSON.stringify(styles, null, '\t');
const blob = new Blob([text], {type: 'application/json'});
const objectURL = URL.createObjectURL(blob);
let link = $element({
tag:'a',
let link = $create('a', {
href: objectURL,
type: 'application/json',
download: generateFileName(),
@ -319,8 +316,7 @@ $('#file-all-styles').onclick = () => {
link.dispatchEvent(new MouseEvent('click'));
setTimeout(() => URL.revokeObjectURL(objectURL));
} else {
const iframe = document.body.appendChild($element({
tag: 'iframe',
const iframe = document.body.appendChild($create('iframe', {
style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'),
}));
doTimeout()

View File

@ -5,8 +5,7 @@ onDOMready().then(() => {
let prevText, focusedLink, focusedEntry;
let prevTime = performance.now();
let focusedName = '';
const input = $element({
tag: 'textarea',
const input = $create('textarea', {
spellcheck: false,
oninput: incrementalSearch,
});

View File

@ -15,6 +15,26 @@ body {
height: 100%;
}
body.all-styles-hidden-by-filters:before,
body.all-styles-hidden-by-filters:after {
position: absolute;
left: calc(3rem + var(--header-width));
color: hsla(180, 40%, 45%, .3);
animation: fadein 3s;
}
body.all-styles-hidden-by-filters:before {
content: "\2190"; /* left arrow */
font-size: 3rem;
top: 3.5rem;
}
body.all-styles-hidden-by-filters:after {
font-size: 1.5rem;
position: absolute;
top: 3rem;
}
a, .disabled a:hover {
color: #000;
transition: color .5s;
@ -74,6 +94,10 @@ select {
fill: #aaa;
}
#usercss-wiki svg {
margin-top: -2px;
}
.nobreak {
white-space: nowrap;
}
@ -250,16 +274,17 @@ label.nobreak input {
margin-left: -13px;
cursor: pointer;
outline: none;
margin-bottom: 8px;
}
#header summary h2 {
display: inline-block;
border-bottom: 1px dotted transparent;
margin-top: .2em;
margin-bottom: .4em;
margin-top: .1em;
margin-bottom: .1em;
}
#header summary h2:hover {
#header summary:hover h2 {
border-color: #bbb;
}
@ -289,11 +314,107 @@ label.nobreak input {
display: table-cell;
vertical-align: middle;
}
/************ checkbox & select************/
.newUI .checker {
margin: 0;
}
#newUIoptions > div {
margin: 4px 0;
}
.filter-selection {
position: relative;
top: 1px;
margin: 0 1ex 0 0;
}
#header label {
padding-left: 16px;
position: relative;
}
#header .filter-selection label {
display: flex;
align-items: center;
height: 18px;
width: 100%;
padding: 0 0 0 4px;
}
#header .filter-selection label .checkmate {
vertical-align: middle;
margin-top: -2px;
}
.firefox #header .filter-selection label .checkmate {
margin: 0;
}
.newUI #newUIoptions > label {
padding-left: 0;
}
.filter-selection select {
height: 18px;
outline: none;
border: none;
max-width: 100%;
padding-left: 4px;
padding-right: 14px;
}
.firefox .filter-selection select {
padding-left: 0;
}
.filter-selection .select-arrow {
margin-top: 2px;
top: 0;
right: 0;
}
.select-resizer {
cursor: default;
border: none;
max-width: calc(100% - 2em);
position: absolute;
top: 0;
left: 16px;
}
.firefox .select-resizer {
left: 16px;
}
fieldset > label,
fieldset > .filter-selection {
transition: background-color .25s;
}
fieldset > label:hover,
fieldset > .filter-selection:hover {
background-color: hsla(0, 0%, 50%, .2);
}
.checkmate {
position: relative;
height: 12px;
width: 12px;
display: inline-flex;
}
.entry .checkmate {
vertical-align: middle;
margin: -2px 1ex 0 0;
}
.newUI .entry .svg-icon.checked,
.newUI .entry:hover .svg-icon.checked {
fill: #000;
}
.newUI .entry input[type="checkbox"]:not(.slider) {
pointer-events: all;
}
.newUI .style-name {
@ -638,13 +759,19 @@ fieldset {
max-width: 250px;
}
fieldset > *:not(legend) {
fieldset > input,
fieldset > label {
display: flex;
align-items: center;
}
fieldset > label:hover {
background-color: hsla(0, 0%, 50%, .1);
#header fieldset > label {
padding-left: 20px;
}
#header fieldset > label input[type="checkbox"]:not(.slider),
#header fieldset > label input[type="checkbox"]:not(.slider):checked + .svg-icon.checked{
left: 4px;
}
#filters {
@ -665,12 +792,6 @@ fieldset > label:hover {
content: ": ";
}
fieldset select {
border: none;
max-width: calc(100% - 2em);
background-color: transparent;
}
#search {
width: calc(100% - 4px);
margin: 0.25rem 4px 0;
@ -752,6 +873,10 @@ fieldset select {
align-items: center;
}
.config-dialog .select-resizer {
position: static;
}
.config-dialog label:first-child {
padding-top: 0;
}
@ -788,10 +913,13 @@ fieldset select {
vertical-align: middle;
}
.config-dialog .select-resizer,
.config-dialog select {
width: auto;
min-width: var(--onoffswitch-width);
max-width: 124px;
left: auto;
position: relative;
}
.config-dialog .onoffswitch {
@ -808,7 +936,7 @@ fieldset select {
flex-shrink: 0;
}
.config-dialog label > :last-child:not(.onoffswitch) > :not(:last-child) {
.config-dialog label > :last-child:not(.onoffswitch):not(.select-resizer) > :not(:last-child) {
margin-right: 4px;
}
@ -898,6 +1026,17 @@ fieldset select {
flex-direction: column;
}
body.all-styles-hidden-by-filters:before {
content: "\2191"; /* up arrow */
top: calc(50% + 2.75rem);
left: 2rem;
}
body.all-styles-hidden-by-filters:after {
top: calc(50% + 4rem);
left: 3.75rem;
}
#header {
height: auto;
position: static;
@ -972,12 +1111,3 @@ fieldset select {
word-break: break-all;
}
}
@supports (-moz-appearance:none) {
/* fixes for Firefox quirks */
#newStyleAsUsercss-wrapper svg {
margin-top: -2px;
}
}

View File

@ -84,14 +84,17 @@ function initGlobalEvents() {
switchUI({styleOnly: true});
// translate CSS manually
document.head.appendChild($element({tag: 'style', textContent: `
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')}";
}
`));
}

View File

@ -122,9 +122,9 @@ function reportUpdateState(state, style, details) {
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
entry.dataset.details = details;
if (!details) {
details = t('updateCheckFailServerUnreachable');
details = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl;
} else if (typeof details === 'number') {
details = t('updateCheckFailBadResponseCode', [details]);
details = t('updateCheckFailBadResponseCode', [details]) + '\n' + style.updateUrl;
} else if (details === BG.updater.EDITED) {
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} else if (details === BG.updater.MAYBE_EDITED) {
@ -174,10 +174,7 @@ function showUpdateHistory() {
BG.chromeLocal.getValue('updateLog').then((lines = []) => {
messageBox({
title: t('updateCheckHistory'),
contents: $element({
className: 'update-history-log',
textContent: lines.join('\n'),
}),
contents: $create('.update-history-log', lines.join('\n')),
buttons: [t('confirmOK')],
onshow: () => ($('#message-box-contents').scrollTop = 1e9),
});

View File

@ -1,6 +1,6 @@
{
"name": "Stylus",
"version": "1.1.7",
"version": "1.1.7.2",
"minimum_chrome_version": "49",
"description": "__MSG_description__",
"homepage_url": "http://add0n.com/stylus.html",

View File

@ -28,7 +28,7 @@
}
#message-box.fadeout {
animation: fadeout .5s ease-in-out;
animation: fadeout .25s ease-in-out;
}
#message-box.center {

View File

@ -58,27 +58,25 @@ function messageBox({
removeSelf();
}
const id = 'message-box';
messageBox.element = $element({id, className, appendChild: [
$element({appendChild: [
$element({id: `${id}-title`, textContent: title}),
$element({id: `${id}-close-icon`, appendChild:
$element({tag: 'SVG#svg', class: 'svg-icon', viewBox: '0 0 20 20', appendChild:
$element({tag: 'SVG#path', d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
'5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z',
})
}),
onclick: messageBox.listeners.closeIcon}),
$element({id: `${id}-contents`, appendChild: tHTML(contents)}),
$element({id: `${id}-buttons`, appendChild:
buttons.map((content, buttonIndex) => content && $element({
tag: 'button',
buttonIndex,
textContent: content.textContent || content,
onclick: content.onclick || messageBox.listeners.button,
}))
}),
]}),
]});
messageBox.element =
$create({id, className}, [
$create([
$create(`#${id}-title`, title),
$create(`#${id}-close-icon`, {onclick: messageBox.listeners.closeIcon},
$create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'},
$create('SVG:path', {d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
'5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z',
}))),
$create(`#${id}-contents`, tHTML(contents)),
$create(`#${id}-buttons`,
buttons.map((content, buttonIndex) => content &&
$create('button', {
buttonIndex,
textContent: content.textContent || content,
onclick: content.onclick || messageBox.listeners.button,
}))),
]),
]);
}
function bindGlobalListeners() {

View File

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="popup/popup.css">
<link rel="stylesheet" href="popup/search-results.css">
@ -26,11 +27,9 @@
<div class="entry">
<div class="main-controls">
<label class="style-name">
<input class="checker" type="checkbox">
<svg class="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"/>
</svg>
</label>
<input class="checker" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="actions">
<a href="#" class="enable" i18n-text="enableStyleLabel"></a>
@ -141,11 +140,10 @@
<div class="actions">
<div id="disable-all-wrapper">
<div class="main-controls">
<input id="disableAll" type="checkbox">
<svg class="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"/>
</svg>
<label id="disableAll-label" for="disableAll" i18n-text="disableAllStyles"></label>
<label id="disableAll-label" i18n-text="disableAllStyles">
<input id="disableAll" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
</div>
<div id="searchResults" class="hidden">
@ -193,5 +191,12 @@
i18n-title="shortcutsNote"></button>
</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"/>
</symbol>
</svg>
</body>
</html>

View File

@ -142,13 +142,13 @@ window.addEventListener('showStyles:done', function _() {
line
.split(/(<.*?>)/)
.map(s => (!s.startsWith('<') ? s :
$element({tag: 'mark', textContent: s.slice(1, -1)})));
$create('mark', s.slice(1, -1))));
const linesToElements = text =>
text
.trim()
.split('\n')
.map((line, i, array) =>
$element(i < array.length - 1 ? {
$create(i < array.length - 1 ? {
tag: 'p',
appendChild: keysToElements(line),
} : {
@ -159,9 +159,9 @@ window.addEventListener('showStyles:done', function _() {
}));
[
linesToElements(t('popupHotkeysInfo')),
$element({tag: 'button', textContent: t('confirmOK')}),
$create('button', t('confirmOK')),
].forEach(child => {
container.appendChild($element({appendChild: child}));
container.appendChild($create('div', child));
});
}
}

View File

@ -22,112 +22,57 @@ body > div:not(#installed) {
.firefox .chromium-only {
display: none;
}
/************ checkbox ************/
/* there's a bug in FF 52 ESR so until we find a better workaround we won't style checkboxes in FF */
html.moz-appearance-bug .svg-icon.checked {
display: none;
}
html.moz-appearance-bug input[type="checkbox"] {
position: absolute;
left: 8px;
top: 0;
bottom: 0;
margin: auto;
}
html.moz-appearance-bug #disableAll {
left: -1px;
}
html.moz-appearance-bug #disableAll-label {
padding-left: 16px !important;
}
html:not(.moz-appearance-bug) .svg-icon.checked {
position: absolute;
height: 8px;
width: 8px;
display: none;
fill: #000;
margin: 2px 0 0 2px;
}
html:not(.moz-appearance-bug) input[type="checkbox"]:checked + .svg-icon.checked {
display: inline-flex;
transition: fill .25s;
}
html:not(.moz-appearance-bug) input[type="checkbox"] {
-moz-appearance: none;
-webkit-appearance: none;
border: 1px solid hsl(0, 0%, 46%);
height: 12px;
width: 12px;
display: inline-flex;
border-radius: 2px;
background-color: hsl(0, 0%, 94%);
outline: none;
margin: 0;
transition: background-color .25s, border-color .25s;
}
html:not(.moz-appearance-bug) input[type="checkbox"]:checked:hover,
html:not(.moz-appearance-bug) .style-name:hover input[type="checkbox"]:checked {
.style-name:hover input[type="checkbox"]:checked {
border-color: hsl(0, 0%, 32%);
background-color: hsl(0, 0%, 82%);
}
html:not(.moz-appearance-bug) input[type="checkbox"]:hover,
html:not(.moz-appearance-bug) .style-name:hover input[type="checkbox"],
html:not(.moz-appearance-bug) .entry.disabled .style-name:hover input[type="checkbox"] {
.style-name:hover input[type="checkbox"] {
border-color: hsl(0, 0%, 32%);
background-color: hsl(0, 0%, 82%);
}
html:not(.moz-appearance-bug) #installed.disabled + .actions #disableAll:checked + .svg-icon.checked {
fill: hsl(0, 68%, 42%);
}
html:not(.moz-appearance-bug) #installed.disabled + .actions #disableAll:checked {
border-color: hsl(0, 68%, 50%);
}
html:not(.moz-appearance-bug) #installed.disabled + .actions #disableAll:checked:hover + .svg-icon.checked {
fill: #fff;
}
html:not(.moz-appearance-bug) #disableAll:hover {
border-color: hsl(0, 68%, 50%);
background-color: hsl(20, 70%, 75%);
}
html:not(.moz-appearance-bug) #disableAll:hover + .svg-icon + label {
color: hsl(0, 68%, 42%);
}
html:not(.moz-appearance-bug) #installed.disabled + .actions #disableAll:checked:hover {
border-color: hsl(0, 50%, 56%);
background-color: hsl(0, 50%, 56%);
}
html:not(.moz-appearance-bug) .style-name .checker,
html:not(.moz-appearance-bug) .style-name .svg-icon.checked {
position: absolute;
top: 7px;
left: 9px;
pointer-events: none;
}
html:not(.moz-appearance-bug) #disable-all-wrapper .main-controls .svg-icon.checked {
#disable-all-wrapper input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
label {
transition: color .25s;
#installed.disabled + .actions #disableAll:checked + .svg-icon.checked {
fill: hsl(0, 68%, 42%);
}
#installed.disabled + .actions #disableAll:checked {
border-color: hsl(0, 68%, 50%);
}
#installed.disabled + .actions #disableAll:checked:hover + .svg-icon.checked {
fill: #fff;
}
#disableAll:hover {
border-color: hsl(0, 68%, 50%);
background-color: hsl(20, 70%, 75%);
}
#disableAll-label:hover {
color: hsl(0, 68%, 42%);
}
#installed.disabled + .actions #disableAll:checked:hover {
border-color: hsl(0, 50%, 56%);
background-color: hsl(0, 50%, 56%);
}
#installed .style-name .checker,
#installed .style-name .svg-icon.checked {
position: absolute;
top: 7px;
left: 9px;
pointer-events: none;
}
#disable-all-wrapper {
@ -140,18 +85,14 @@ label {
}
#disable-all-wrapper .main-controls label {
padding-left: 16px;
position: relative;
transition: color .25s;
font-size: 12px;
padding-left: 4px;
}
#no-styles {
margin: 0 .75em;
font-weight: bold;
font-style: normal;
}
#no-styles:before {
content: none;
font-style: italic;
}
.checker {
@ -222,6 +163,10 @@ html[style] .entry {
padding: 0 16px 0 0;
}
#no-styles.entry {
padding: 0 14px;
}
html[style] .entry:nth-child(-n+10):before,
html[style] .entry:nth-child(10):before {
right: 7px;

View File

@ -929,7 +929,7 @@
((cm.display.renderedView || [])[0] || {}).text || cm.display.lineDiv;
for (let el = start; el; el = el.parentElement) {
const bgColor = getComputedStyle(el).backgroundColor;
const [r, g, b, a = 255] = bgColor.match(/\d+/g).map(Number);
const [r, g, b, a = 255] = (bgColor.match(/\d+/g) || []).map(Number);
if (!a) {
continue;
}

View File

@ -10994,6 +10994,11 @@ self.onmessage = ({data: {action = 'run', code, config}}) => {
self.postMessage(CSSLint.getRules().map(rule => rule.id));
return;
case 'getAllRuleInfos':
// the functions are non-tranferable
self.postMessage(CSSLint.getRules().map(rule => JSON.parse(JSON.stringify(rule))));
return;
case 'run':
Object.defineProperty(config, 'errors', {get: () => 0, set: () => 0});
config['uso-vars'] = 1;