editor: section labels, TOC, tweaks (#1086)
* section labels, TOC, speedups and fixes * show section numbers in widgets * debounce livePreview in usercss editor * better fixed header and compact layout compatibility * fix section sizing for compact layout + layout speedup * DocFuncMapper + cosmetics + fix Clone button * don't run linter during initSections * remove unused/unnecessary DOM polyfills * report invalid @document function as parser error * rewrite section finder * simplify focusedViaClick * simplify setPreprocessor and make it synchronous * throttle offscreen line widgets in usercss with lots of sections * add on, off aliases for add/removeEventListener + onOff * use on/off aliases in changed files * use getters in more places
This commit is contained in:
parent
71cabc2029
commit
5e5fecbcfe
|
@ -1267,6 +1267,10 @@
|
||||||
"message": "Restore removed section",
|
"message": "Restore removed section",
|
||||||
"description": "Label for the button to restore a removed section"
|
"description": "Label for the button to restore a removed section"
|
||||||
},
|
},
|
||||||
|
"sections": {
|
||||||
|
"message": "Sections",
|
||||||
|
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor"
|
||||||
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"message": "Shortcuts",
|
"message": "Shortcuts",
|
||||||
"description": "Go to shortcut configuration"
|
"description": "Go to shortcut configuration"
|
||||||
|
|
259
edit.html
259
edit.html
|
@ -32,12 +32,14 @@
|
||||||
<script src="content/style-injector.js"></script>
|
<script src="content/style-injector.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
|
|
||||||
|
<script src="edit/util.js"></script>
|
||||||
<script src="edit/edit.js"></script> <!-- run it ASAP to send a request for the style -->
|
<script src="edit/edit.js"></script> <!-- run it ASAP to send a request for the style -->
|
||||||
|
|
||||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
||||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/mode/css/css.js"></script>
|
<script src="vendor/codemirror/mode/css/css.js"></script>
|
||||||
|
<script src="vendor/codemirror/mode/stylus/stylus.js"></script>
|
||||||
|
|
||||||
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
||||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||||
|
@ -84,10 +86,10 @@
|
||||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<script src="edit/codemirror-default.js"></script>
|
||||||
<script src="edit/codemirror-factory.js"></script>
|
<script src="edit/codemirror-factory.js"></script>
|
||||||
<script src="edit/util.js"></script>
|
|
||||||
<script src="edit/regexp-tester.js"></script>
|
<script src="edit/regexp-tester.js"></script>
|
||||||
<script src="edit/live-preview.js"></script>
|
<script src="edit/live-preview.js"></script>
|
||||||
<script src="edit/applies-to-line-widget.js"></script>
|
<script src="edit/moz-section-finder.js"></script>
|
||||||
|
<script src="edit/moz-section-widget.js"></script>
|
||||||
<script src="edit/reroute-hotkeys.js"></script>
|
<script src="edit/reroute-hotkeys.js"></script>
|
||||||
<link href="edit/global-search.css" rel="stylesheet">
|
<link href="edit/global-search.css" rel="stylesheet">
|
||||||
<script src="edit/global-search.js"></script>
|
<script src="edit/global-search.js"></script>
|
||||||
|
@ -311,154 +313,147 @@
|
||||||
<button id="beautify" i18n-text="styleBeautify"></button>
|
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||||
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||||
</div>
|
</div>
|
||||||
<div id="mozilla-format-container">
|
<div id="mozilla-format-buttons" class="sectioned-only">
|
||||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading">
|
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||||
<a id="to-mozilla-help" class="svg-inline-wrapper" href="#" tabindex="0">
|
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<a id="to-mozilla-help" class="svg-inline-wrapper" href="#" tabindex="0"
|
||||||
</a>
|
i18n-title="styleMozillaFormatHeading">
|
||||||
</h2>
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
<div id="mozilla-format-buttons">
|
</a>
|
||||||
<button id="from-mozilla" i18n-text="importLabel"></button>
|
|
||||||
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<details id="options" data-pref="editor.options.expanded">
|
<div id="details-wrapper">
|
||||||
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
<details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
|
||||||
<div id="options-wrapper">
|
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
||||||
<div class="options-column">
|
<div id="options-wrapper">
|
||||||
<div class="option">
|
<div class="options-column">
|
||||||
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
|
<div class="option">
|
||||||
<input id="editor.lineWrapping" type="checkbox">
|
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<input id="editor.lineWrapping" type="checkbox">
|
||||||
</label>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
</div>
|
</label>
|
||||||
<div class="option">
|
|
||||||
<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">
|
|
||||||
<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">
|
|
||||||
<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">
|
|
||||||
<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">
|
|
||||||
<label i18n-text="cm_selectByTokens"
|
|
||||||
i18n-title="cm_selectByTokensTooltip">
|
|
||||||
<input id="editor.selectByTokens" type="checkbox">
|
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="option">
|
|
||||||
<label i18n-text="cm_colorpicker">
|
|
||||||
<input id="editor.colorpicker" type="checkbox">
|
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
|
||||||
</label>
|
|
||||||
<a id="colorpicker-settings" href="#" class="svg-inline-wrapper" i18n-title="shortcutsNote" tabindex="0">
|
|
||||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="option usercss-only">
|
|
||||||
<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>
|
|
||||||
<div class="options-column">
|
|
||||||
<div class="option aligned">
|
|
||||||
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
|
||||||
<input id="editor.tabSize" type="number" min="0">
|
|
||||||
</div>
|
|
||||||
<div class="option aligned">
|
|
||||||
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
|
||||||
<div class="select-resizer">
|
|
||||||
<select id="editor.keyMap"></select>
|
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
|
||||||
</div>
|
</div>
|
||||||
<a id="keyMap-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
<div class="option">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<label id="smartIndent-label" i18n-text="cm_smartIndent">
|
||||||
</a>
|
<input id="editor.smartIndent" type="checkbox">
|
||||||
</div>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<div class="option aligned">
|
</label>
|
||||||
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
</div>
|
||||||
<div class="select-resizer">
|
<div class="option">
|
||||||
<select id="editor.theme"></select>
|
<label id="indentWithTabs-label" i18n-text="cm_indentWithTabs">
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
<input id="editor.indentWithTabs" type="checkbox">
|
||||||
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="option">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<label i18n-text="cm_selectByTokens"
|
||||||
|
i18n-title="cm_selectByTokensTooltip">
|
||||||
|
<input id="editor.selectByTokens" type="checkbox">
|
||||||
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="option">
|
||||||
|
<label i18n-text="cm_colorpicker">
|
||||||
|
<input id="editor.colorpicker" type="checkbox">
|
||||||
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
|
</label>
|
||||||
|
<a id="colorpicker-settings" href="#" class="svg-inline-wrapper" i18n-title="shortcutsNote" tabindex="0">
|
||||||
|
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="option usercss-only">
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="option aligned">
|
<div class="options-column">
|
||||||
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
|
<div class="option aligned">
|
||||||
<div class="select-resizer">
|
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||||
<select id="editor.matchHighlight">
|
<input id="editor.tabSize" type="number" min="0">
|
||||||
<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>
|
<div class="option aligned">
|
||||||
<div class="option aligned">
|
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
||||||
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
|
|
||||||
<div class="select-resizer">
|
<div class="select-resizer">
|
||||||
<select id="editor.linter">
|
<select id="editor.keyMap"></select>
|
||||||
<option value="csslint" selected>CSSLint</option>
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
<option value="stylelint">Stylelint</option>
|
</div>
|
||||||
<option value="" i18n-text="genericDisabledLabel"></option>
|
<a id="keyMap-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
||||||
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="option aligned">
|
||||||
|
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
||||||
|
<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>
|
||||||
|
<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>
|
</select>
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<a id="linter-settings" href="#" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0">
|
</div>
|
||||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
|
<div class="option aligned">
|
||||||
</a>
|
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
|
||||||
|
<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>
|
||||||
|
<a id="linter-settings" href="#" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0">
|
||||||
|
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
</details>
|
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">
|
||||||
<details id="lint" class="hidden-unless-compact" data-pref="editor.lint.expanded">
|
<summary><h2 i18n-text="sections"></h2></summary>
|
||||||
<summary>
|
<ol id="toc"></ol>
|
||||||
<h2 i18n-text="linterIssues">: <span id="issue-count"></span>
|
</details>
|
||||||
<a id="lint-help" href="#" class="svg-inline-wrapper intercepts-click" tabindex="0">
|
<details id="lint" data-pref="editor.lint.expanded" class="hidden-unless-compact ignore-pref-if-compact">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<summary>
|
||||||
</a>
|
<h2 i18n-text="linterIssues">: <span id="issue-count"></span>
|
||||||
</h2>
|
<a id="lint-help" href="#" class="svg-inline-wrapper intercepts-click" tabindex="0">
|
||||||
</summary>
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
<div class="lint-scroll-container">
|
</a>
|
||||||
<div class="lint-report-container"></div>
|
</h2>
|
||||||
</div>
|
</summary>
|
||||||
</details>
|
<div class="lint-scroll-container">
|
||||||
|
<div class="lint-report-container"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
<div id="footer" class="hidden">
|
<div id="footer" class="hidden">
|
||||||
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||||
i18n-text="externalUsercssDocument"
|
i18n-text="externalUsercssDocument"
|
||||||
target="_blank"></a>
|
target="_blank"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section id="sections">
|
<section id="sections"></section>
|
||||||
<!--
|
|
||||||
It seems that we don't use these anymore
|
|
||||||
https://github.com/openstyles/stylus/blob/5cbe8a8d780a6eb9fce11d5846e92bf244c3a3f3/edit/sections.js#L18
|
|
||||||
-->
|
|
||||||
<!-- <h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span>
|
|
||||||
<a id="sections-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
|
||||||
</a>
|
|
||||||
</h2> -->
|
|
||||||
</section>
|
|
||||||
<div id="help-popup">
|
<div id="help-popup">
|
||||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||||
<div class="contents"></div>
|
<div class="contents"></div>
|
||||||
|
|
|
@ -1,590 +0,0 @@
|
||||||
/* global regExpTester debounce messageBox CodeMirror template colorMimicry msg
|
|
||||||
$ $create t prefs tryCatch deepEqual */
|
|
||||||
/* exported createAppliesToLineWidget */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
function createAppliesToLineWidget(cm) {
|
|
||||||
const THROTTLE_DELAY = 400;
|
|
||||||
const RX_SPACE = /(?:\s+|\/\*)+/y;
|
|
||||||
let TPL, EVENTS, CLICK_ROUTE;
|
|
||||||
let widgets = [];
|
|
||||||
let fromLine, toLine, actualStyle;
|
|
||||||
let initialized = false;
|
|
||||||
return {toggle};
|
|
||||||
|
|
||||||
function toggle(newState = !initialized) {
|
|
||||||
newState = Boolean(newState);
|
|
||||||
if (newState !== initialized) {
|
|
||||||
if (newState) {
|
|
||||||
init();
|
|
||||||
} else {
|
|
||||||
uninit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
TPL = {
|
|
||||||
container:
|
|
||||||
$create('div.applies-to', [
|
|
||||||
$create('label', t('appliesLabel')),
|
|
||||||
$create('ul.applies-to-list'),
|
|
||||||
]),
|
|
||||||
listItem: template.appliesTo.cloneNode(true),
|
|
||||||
appliesToEverything:
|
|
||||||
$create('li.applies-to-everything', t('appliesToEverything')),
|
|
||||||
};
|
|
||||||
|
|
||||||
$('.applies-value', TPL.listItem).insertAdjacentElement('afterend',
|
|
||||||
$create('button.test-regexp', t('styleRegexpTestButton')));
|
|
||||||
|
|
||||||
CLICK_ROUTE = {
|
|
||||||
'.test-regexp': showRegExpTester,
|
|
||||||
|
|
||||||
'.remove-applies-to': (item, apply, event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
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, event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const applies = item.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(item, apply, 'type', typeElement.value);
|
|
||||||
item.dataset.type = apply.type.text;
|
|
||||||
} else {
|
|
||||||
return EVENTS.oninput.apply(this, arguments);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
oninput({target}) {
|
|
||||||
if (target.matches('.applies-value')) {
|
|
||||||
const item = target.closest('.applies-to-item');
|
|
||||||
const apply = item.__apply;
|
|
||||||
changeItem(item, apply, 'value', target.value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclick(event) {
|
|
||||||
const {target} = event;
|
|
||||||
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, event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
actualStyle = $create('style');
|
|
||||||
fromLine = 0;
|
|
||||||
toLine = cm.doc.size;
|
|
||||||
|
|
||||||
cm.on('change', onChange);
|
|
||||||
cm.on('optionChange', onOptionChange);
|
|
||||||
|
|
||||||
msg.onExtension(onRuntimeMessage);
|
|
||||||
|
|
||||||
requestAnimationFrame(updateWidgetStyle);
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function uninit() {
|
|
||||||
initialized = false;
|
|
||||||
|
|
||||||
widgets.forEach(clearWidget);
|
|
||||||
widgets.length = 0;
|
|
||||||
cm.off('change', onChange);
|
|
||||||
cm.off('optionChange', onOptionChange);
|
|
||||||
msg.off(onRuntimeMessage);
|
|
||||||
actualStyle.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChange(cm, event) {
|
|
||||||
const {from, to, origin} = event;
|
|
||||||
if (origin === 'appliesTo') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lastChanged = CodeMirror.changeEnd(event).line;
|
|
||||||
fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line);
|
|
||||||
toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
|
|
||||||
if (origin === 'setValue') {
|
|
||||||
update();
|
|
||||||
} else {
|
|
||||||
debounce(update, THROTTLE_DELAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOptionChange(cm, option) {
|
|
||||||
if (option === 'theme') {
|
|
||||||
updateWidgetStyle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRuntimeMessage(msg) {
|
|
||||||
if (msg.reason === 'editPreview' && !$(`#stylus-${msg.style.id}`)) {
|
|
||||||
// no style element with this id means the style doesn't apply to the editor URL
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (msg.style || msg.styles ||
|
|
||||||
msg.prefs && 'disableAll' in msg.prefs ||
|
|
||||||
msg.method === 'styleDeleted') {
|
|
||||||
requestAnimationFrame(updateWidgetStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
const changed = {fromLine, toLine};
|
|
||||||
fromLine = Math.max(fromLine || 0, cm.display.viewFrom);
|
|
||||||
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo || toLine);
|
|
||||||
const visible = {fromLine, toLine};
|
|
||||||
const {curOp} = cm;
|
|
||||||
if (fromLine >= cm.display.viewFrom && toLine <= (cm.display.viewTo || toLine)) {
|
|
||||||
if (!curOp) cm.startOperation();
|
|
||||||
doUpdate();
|
|
||||||
if (!curOp) cm.endOperation();
|
|
||||||
}
|
|
||||||
if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {
|
|
||||||
setTimeout(updateInvisible, 0, changed, visible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateInvisible(changed, visible) {
|
|
||||||
let inOp = false;
|
|
||||||
if (changed.fromLine < visible.fromLine) {
|
|
||||||
fromLine = Math.min(fromLine, changed.fromLine);
|
|
||||||
toLine = Math.min(changed.toLine, visible.fromLine);
|
|
||||||
inOp = true;
|
|
||||||
cm.startOperation();
|
|
||||||
doUpdate();
|
|
||||||
}
|
|
||||||
if (changed.toLine > visible.toLine) {
|
|
||||||
fromLine = Math.max(fromLine, changed.toLine);
|
|
||||||
toLine = Math.max(changed.toLine, visible.toLine);
|
|
||||||
if (!inOp) {
|
|
||||||
inOp = true;
|
|
||||||
cm.startOperation();
|
|
||||||
}
|
|
||||||
doUpdate();
|
|
||||||
}
|
|
||||||
if (inOp) {
|
|
||||||
cm.endOperation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWidgetStyle() {
|
|
||||||
if (prefs.get('editor.theme') !== 'default' &&
|
|
||||||
!tryCatch(() => $('#cm-theme').sheet.cssRules)) {
|
|
||||||
requestAnimationFrame(updateWidgetStyle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const MIN_LUMA = .05;
|
|
||||||
const MIN_LUMA_DIFF = .4;
|
|
||||||
const color = {
|
|
||||||
wrapper: colorMimicry.get(cm.display.wrapper),
|
|
||||||
gutter: colorMimicry.get(cm.display.gutters, {
|
|
||||||
bg: 'backgroundColor',
|
|
||||||
border: 'borderRightColor',
|
|
||||||
}),
|
|
||||||
line: colorMimicry.get('.CodeMirror-linenumber', null, cm.display.lineDiv),
|
|
||||||
comment: colorMimicry.get('span.cm-comment', null, cm.display.lineDiv),
|
|
||||||
};
|
|
||||||
const hasBorder =
|
|
||||||
color.gutter.style.borderRightWidth !== '0px' &&
|
|
||||||
!/transparent|\b0\)/g.test(color.gutter.style.borderRightColor);
|
|
||||||
const diff = {
|
|
||||||
wrapper: Math.abs(color.gutter.bgLuma - color.wrapper.foreLuma),
|
|
||||||
border: hasBorder ? Math.abs(color.gutter.bgLuma - color.gutter.borderLuma) : 0,
|
|
||||||
line: Math.abs(color.gutter.bgLuma - color.line.foreLuma),
|
|
||||||
};
|
|
||||||
const preferLine = diff.line > diff.wrapper || diff.line > MIN_LUMA_DIFF;
|
|
||||||
const fore = preferLine ? color.line.fore : color.wrapper.fore;
|
|
||||||
|
|
||||||
const border = fore.replace(/[\d.]+(?=\))/, MIN_LUMA_DIFF / 2);
|
|
||||||
const borderStyleForced = `1px ${hasBorder ? color.gutter.style.borderRightStyle : 'solid'} ${border}`;
|
|
||||||
|
|
||||||
actualStyle.textContent = `
|
|
||||||
.applies-to {
|
|
||||||
background-color: ${color.gutter.bg};
|
|
||||||
border-top: ${borderStyleForced};
|
|
||||||
border-bottom: ${borderStyleForced};
|
|
||||||
}
|
|
||||||
.applies-to label {
|
|
||||||
color: ${fore};
|
|
||||||
}
|
|
||||||
.applies-to input,
|
|
||||||
.applies-to button,
|
|
||||||
.applies-to select {
|
|
||||||
background: rgba(255, 255, 255, ${
|
|
||||||
Math.max(MIN_LUMA, Math.pow(Math.max(0, color.gutter.bgLuma - MIN_LUMA * 2), 2)).toFixed(2)
|
|
||||||
});
|
|
||||||
border: ${borderStyleForced};
|
|
||||||
transition: none;
|
|
||||||
color: ${fore};
|
|
||||||
}
|
|
||||||
.applies-to .svg-icon.select-arrow {
|
|
||||||
fill: ${fore};
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.documentElement.appendChild(actualStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
function doUpdate() {
|
|
||||||
// find which widgets needs to be update
|
|
||||||
// some widgets (lines) might be deleted
|
|
||||||
widgets = widgets.filter(w => w.line.lineNo() !== null);
|
|
||||||
let i = widgets.findIndex(w => w.line.lineNo() > fromLine) - 1;
|
|
||||||
let j = widgets.findIndex(w => w.line.lineNo() > toLine);
|
|
||||||
if (i === -2) {
|
|
||||||
i = widgets.length - 1;
|
|
||||||
}
|
|
||||||
if (j < 0) {
|
|
||||||
j = widgets.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// decide search range
|
|
||||||
const fromPos = {line: widgets[i] ? widgets[i].line.lineNo() : 0, ch: 0};
|
|
||||||
const toPos = {line: widgets[j] ? widgets[j].line.lineNo() : toLine + 1, ch: 0};
|
|
||||||
|
|
||||||
// calc index->pos lookup table
|
|
||||||
let index = 0;
|
|
||||||
const lineIndexes = [0];
|
|
||||||
cm.doc.iter(0, toPos.line + 1, ({text}) => {
|
|
||||||
lineIndexes.push((index += text.length + 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
// splice
|
|
||||||
i = Math.max(0, i);
|
|
||||||
widgets.splice(i, 0, ...createWidgets(fromPos, toPos, widgets.splice(i, j - i), lineIndexes));
|
|
||||||
|
|
||||||
fromLine = null;
|
|
||||||
toLine = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function *createWidgets(start, end, removed, lineIndexes) {
|
|
||||||
let i = 0;
|
|
||||||
let itemHeight;
|
|
||||||
for (const section of findAppliesTo(start, end, lineIndexes)) {
|
|
||||||
let removedWidget = removed[i];
|
|
||||||
while (removedWidget && removedWidget.line.lineNo() < section.pos.line) {
|
|
||||||
clearWidget(removed[i]);
|
|
||||||
removedWidget = removed[++i];
|
|
||||||
}
|
|
||||||
if (removedWidget && deepEqual(removedWidget.node.__applies, section.applies, ['mark'])) {
|
|
||||||
yield removedWidget;
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const a of section.applies) {
|
|
||||||
setupApplyMarkers(a, lineIndexes);
|
|
||||||
}
|
|
||||||
if (removedWidget && removedWidget.line.lineNo() === section.pos.line) {
|
|
||||||
// reuse old widget
|
|
||||||
removedWidget.section.applies.forEach(apply => {
|
|
||||||
apply.type.mark.clear();
|
|
||||||
apply.value.mark.clear();
|
|
||||||
});
|
|
||||||
removedWidget.section = section;
|
|
||||||
const newNode = buildElement(section);
|
|
||||||
const removedNode = removedWidget.node;
|
|
||||||
if (removedNode.parentNode) {
|
|
||||||
removedNode.parentNode.replaceChild(newNode, removedNode);
|
|
||||||
}
|
|
||||||
removedWidget.node = newNode;
|
|
||||||
removedWidget.changed();
|
|
||||||
yield removedWidget;
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// new widget
|
|
||||||
const widget = cm.addLineWidget(section.pos.line, buildElement(section), {
|
|
||||||
coverGutter: true,
|
|
||||||
noHScroll: true,
|
|
||||||
above: true,
|
|
||||||
height: itemHeight ? section.applies.length * itemHeight : undefined,
|
|
||||||
});
|
|
||||||
widget.section = section;
|
|
||||||
itemHeight = itemHeight || widget.node.offsetHeight / (section.applies.length || 1);
|
|
||||||
yield widget;
|
|
||||||
}
|
|
||||||
removed.slice(i).forEach(clearWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearWidget(widget) {
|
|
||||||
widget.clear();
|
|
||||||
widget.section.applies.forEach(clearApply);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearApply(apply) {
|
|
||||||
apply.type.mark.clear();
|
|
||||||
apply.value.mark.clear();
|
|
||||||
apply.mark.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupApplyMarkers(apply, lineIndexes) {
|
|
||||||
apply.type.mark = cm.markText(
|
|
||||||
posFromIndex(cm, apply.type.start, lineIndexes),
|
|
||||||
posFromIndex(cm, apply.type.end, lineIndexes),
|
|
||||||
{clearWhenEmpty: false}
|
|
||||||
);
|
|
||||||
apply.value.mark = cm.markText(
|
|
||||||
posFromIndex(cm, apply.value.start, lineIndexes),
|
|
||||||
posFromIndex(cm, apply.value.end, lineIndexes),
|
|
||||||
{clearWhenEmpty: false}
|
|
||||||
);
|
|
||||||
apply.mark = cm.markText(
|
|
||||||
posFromIndex(cm, apply.start, lineIndexes),
|
|
||||||
posFromIndex(cm, apply.end, lineIndexes),
|
|
||||||
{clearWhenEmpty: false}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function posFromIndex(cm, index, lineIndexes) {
|
|
||||||
if (!lineIndexes) {
|
|
||||||
return cm.posFromIndex(index);
|
|
||||||
}
|
|
||||||
let line = lineIndexes.prev || 0;
|
|
||||||
const prev = lineIndexes[line];
|
|
||||||
const next = lineIndexes[line + 1];
|
|
||||||
if (prev <= index && index < next) {
|
|
||||||
return {line, ch: index - prev};
|
|
||||||
}
|
|
||||||
let a = index < prev ? 0 : line;
|
|
||||||
let b = index < next ? line + 1 : lineIndexes.length - 1;
|
|
||||||
while (a < b - 1) {
|
|
||||||
const mid = (a + b) >> 1;
|
|
||||||
if (lineIndexes[mid] < index) {
|
|
||||||
a = mid;
|
|
||||||
} else {
|
|
||||||
b = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line = lineIndexes[b] > index ? a : b;
|
|
||||||
Object.defineProperty(lineIndexes, 'prev', {value: line, configurable: true});
|
|
||||||
return {line, ch: index - lineIndexes[line]};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildElement({applies}) {
|
|
||||||
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 changeItem(itemElement, apply, part, newText) {
|
|
||||||
if (!apply) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
part = apply[part];
|
|
||||||
const range = part.mark.find();
|
|
||||||
part.mark.clear();
|
|
||||||
newText = unescapeDoubleslash(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;
|
|
||||||
|
|
||||||
if (part === apply.type) {
|
|
||||||
const range = apply.mark.find();
|
|
||||||
apply.mark.clear();
|
|
||||||
apply.mark = cm.markText(
|
|
||||||
part.mark.find().from,
|
|
||||||
range.to,
|
|
||||||
{clearWhenEmpty: false}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apply.type.text === 'regexp' && apply.value.text.trim()) {
|
|
||||||
showRegExpTester(itemElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
type: {
|
|
||||||
text: typeText,
|
|
||||||
start: typeStart,
|
|
||||||
end: typeEnd,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
text: unescapeDoubleslash(valueText),
|
|
||||||
start: valueStart,
|
|
||||||
end: valueEnd,
|
|
||||||
},
|
|
||||||
end
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function *findAppliesTo(posStart, posEnd, lineIndexes) {
|
|
||||||
const funcRe = /^(url|url-prefix|domain|regexp)$/i;
|
|
||||||
let pos;
|
|
||||||
const eatToken = sticky => {
|
|
||||||
if (!sticky) skipSpace(pos, posEnd);
|
|
||||||
pos.ch++;
|
|
||||||
const token = cm.getTokenAt(pos, true);
|
|
||||||
pos.ch = token.end;
|
|
||||||
return CodeMirror.cmpPos(pos, posEnd) <= 0 ? token : {};
|
|
||||||
};
|
|
||||||
const docCur = cm.getSearchCursor('@-moz-document', posStart);
|
|
||||||
while (docCur.findNext() &&
|
|
||||||
CodeMirror.cmpPos(docCur.pos.to, posEnd) <= 0) {
|
|
||||||
// CM can be nitpicky at token boundary so we'll check the next character
|
|
||||||
const safePos = {line: docCur.pos.from.line, ch: docCur.pos.from.ch + 1};
|
|
||||||
if (/\b(string|comment)\b/.test(cm.getTokenTypeAt(safePos))) continue;
|
|
||||||
const applies = [];
|
|
||||||
pos = docCur.pos.to;
|
|
||||||
do {
|
|
||||||
skipSpace(pos, posEnd);
|
|
||||||
const funcIndex = lineIndexes[pos.line] + pos.ch;
|
|
||||||
const func = eatToken().string;
|
|
||||||
// no space allowed before the opening parenthesis
|
|
||||||
if (!funcRe.test(func) || eatToken(true).string !== '(') break;
|
|
||||||
const url = eatToken();
|
|
||||||
if (url.type !== 'string' || eatToken().string !== ')') break;
|
|
||||||
const unquotedUrl = unquote(url.string);
|
|
||||||
const apply = createApply(
|
|
||||||
funcIndex,
|
|
||||||
func,
|
|
||||||
unquotedUrl,
|
|
||||||
unquotedUrl !== url.string
|
|
||||||
);
|
|
||||||
applies.push(apply);
|
|
||||||
} while (eatToken().string === ',');
|
|
||||||
yield {
|
|
||||||
pos: docCur.pos.from,
|
|
||||||
applies
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function skipSpace(pos, posEnd) {
|
|
||||||
let {ch, line} = pos;
|
|
||||||
let lookForEnd;
|
|
||||||
line--;
|
|
||||||
cm.doc.iter(pos.line, posEnd.line + 1, ({text}) => {
|
|
||||||
line++;
|
|
||||||
while (true) {
|
|
||||||
if (lookForEnd) {
|
|
||||||
ch = text.indexOf('*/', ch) + 1;
|
|
||||||
if (!ch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ch++;
|
|
||||||
lookForEnd = false;
|
|
||||||
}
|
|
||||||
// EOL is a whitespace so we'll check the next line
|
|
||||||
if (ch >= text.length) {
|
|
||||||
ch = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
RX_SPACE.lastIndex = ch;
|
|
||||||
const m = RX_SPACE.exec(text);
|
|
||||||
if (!m) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
ch += m[0].length;
|
|
||||||
lookForEnd = m[0].includes('/*');
|
|
||||||
if (ch < text.length && !lookForEnd) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pos.line = line;
|
|
||||||
pos.ch = ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unquote(s) {
|
|
||||||
const first = s.charAt(0);
|
|
||||||
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unescapeDoubleslash(s) {
|
|
||||||
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/.test(s);
|
|
||||||
return hasSingleEscapes ? s : s.replace(/\\\\/g, '\\');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showRegExpTester(item) {
|
|
||||||
regExpTester.toggle(true);
|
|
||||||
regExpTester.update(
|
|
||||||
item.closest('.applies-to').__applies
|
|
||||||
.filter(a => a.type.text === 'regexp')
|
|
||||||
.map(a => unescapeDoubleslash(a.value.text)));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global CodeMirror prefs loadScript editor $ template */
|
/* global CodeMirror prefs editor $ template */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -117,49 +117,24 @@
|
||||||
'lightslategrey': true,
|
'lightslategrey': true,
|
||||||
'slategrey': true,
|
'slategrey': true,
|
||||||
});
|
});
|
||||||
|
Object.assign(CodeMirror.prototype, {
|
||||||
const MODE = {
|
/**
|
||||||
less: {
|
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
|
||||||
family: 'css',
|
* @param {boolean} [force]
|
||||||
value: 'text/x-less',
|
*/
|
||||||
isActive: cm =>
|
setPreprocessor(pp, force) {
|
||||||
cm.doc.mode &&
|
const name = pp === 'less' ? 'text/x-less' : pp === 'stylus' ? pp : 'css';
|
||||||
cm.doc.mode.name === 'css' &&
|
const m = this.doc.mode;
|
||||||
cm.doc.mode.helperType === 'less',
|
if (force || (m.helperType ? m.helperType !== pp : m.name !== name)) {
|
||||||
},
|
this.setOption('mode', name);
|
||||||
stylus: 'stylus',
|
|
||||||
uso: 'css'
|
|
||||||
};
|
|
||||||
|
|
||||||
CodeMirror.defineExtension('setPreprocessor', function (preprocessor, force = false) {
|
|
||||||
const mode = MODE[preprocessor] || 'css';
|
|
||||||
const isActive = mode.isActive || (
|
|
||||||
cm => cm.doc.mode === mode ||
|
|
||||||
cm.doc.mode && (cm.doc.mode.name + (cm.doc.mode.helperType || '') === mode)
|
|
||||||
);
|
|
||||||
if (!force && isActive(this)) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
if ((mode.family || mode) === 'css') {
|
|
||||||
// css.js is always loaded via html
|
|
||||||
this.setOption('mode', mode.value || mode);
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return loadScript(`/vendor/codemirror/mode/${mode}/${mode}.js`).then(() => {
|
|
||||||
this.setOption('mode', mode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineExtension('isBlank', function () {
|
|
||||||
// superfast checking as it runs only until the first non-blank line
|
|
||||||
let isBlank = true;
|
|
||||||
this.doc.eachLine(line => {
|
|
||||||
if (line.text && line.text.trim()) {
|
|
||||||
isBlank = false;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
return isBlank;
|
/** Superfast GC-friendly check that runs until the first non-space line */
|
||||||
|
isBlank() {
|
||||||
|
let filled;
|
||||||
|
this.eachLine(({text}) => (filled = text && /\S/.test(text)));
|
||||||
|
return !filled;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// editor commands
|
// editor commands
|
||||||
|
|
124
edit/edit.css
124
edit/edit.css
|
@ -1,5 +1,6 @@
|
||||||
:root {
|
:root {
|
||||||
--header-narrow-min-height: 12em;
|
--header-narrow-min-height: 12em;
|
||||||
|
--fixed-padding: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -18,6 +19,7 @@ body {
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 2s;
|
transition: opacity 2s;
|
||||||
|
contain: strict;
|
||||||
}
|
}
|
||||||
#global-progress[title] {
|
#global-progress[title] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -146,9 +148,6 @@ label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
#mozilla-format-heading .svg-inline-wrapper {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
#colorpicker-settings.svg-inline-wrapper {
|
#colorpicker-settings.svg-inline-wrapper {
|
||||||
margin: -2px 0 0 .1rem;
|
margin: -2px 0 0 .1rem;
|
||||||
}
|
}
|
||||||
|
@ -190,8 +189,6 @@ input:invalid {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: -13px;
|
margin-left: -13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: .5rem;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#header summary h2 {
|
#header summary h2 {
|
||||||
|
@ -203,6 +200,9 @@ input:invalid {
|
||||||
padding-left: 13px; /* clicking directly on details-marker doesn't set pref so we cover it with h2 */
|
padding-left: 13px; /* clicking directly on details-marker doesn't set pref so we cover it with h2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#options-wrapper {
|
||||||
|
padding: .5rem 0;
|
||||||
|
}
|
||||||
#header summary:hover h2 {
|
#header summary:hover h2 {
|
||||||
border-color: #bbb;
|
border-color: #bbb;
|
||||||
}
|
}
|
||||||
|
@ -211,18 +211,25 @@ input:invalid {
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#details-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header details {
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
#actions > * {
|
#actions > * {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mozilla-format-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mozilla-format-buttons {
|
#mozilla-format-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#actions > div > a {
|
#actions > div > a {
|
||||||
|
@ -272,6 +279,7 @@ input:invalid {
|
||||||
/************ section editor ***********/
|
/************ section editor ***********/
|
||||||
.CodeMirror-vscrollbar,
|
.CodeMirror-vscrollbar,
|
||||||
.CodeMirror-hscrollbar {
|
.CodeMirror-hscrollbar {
|
||||||
|
box-shadow: none !important;
|
||||||
pointer-events: auto !important; /* FF bug */
|
pointer-events: auto !important; /* FF bug */
|
||||||
}
|
}
|
||||||
.section-editor .section {
|
.section-editor .section {
|
||||||
|
@ -305,6 +313,9 @@ input:invalid {
|
||||||
counter-reset: codebox;
|
counter-reset: codebox;
|
||||||
}
|
}
|
||||||
#sections > .section > label {
|
#sections > .section > label {
|
||||||
|
padding: 0 0 4px 0;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13px;
|
||||||
animation: 2s highlight;
|
animation: 2s highlight;
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
animation-direction: reverse;
|
animation-direction: reverse;
|
||||||
|
@ -312,9 +323,41 @@ input:invalid {
|
||||||
}
|
}
|
||||||
#sections > .section > label::after {
|
#sections > .section > label::after {
|
||||||
counter-increment: codebox;
|
counter-increment: codebox;
|
||||||
content: counter(codebox);
|
content: counter(codebox) ": " attr(data-text);
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
.single-editor .applies-to > label::before {
|
||||||
|
content: attr(data-index) ":";
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.code-label[data-text] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#toc {
|
||||||
|
counter-reset: codelabel;
|
||||||
|
margin: 0;
|
||||||
|
padding: .5rem 0;
|
||||||
|
}
|
||||||
|
#toc li {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#toc li.current:not(:only-child) {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#toc li[tabindex="-1"] {
|
||||||
|
opacity: .25;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#toc li:hover {
|
||||||
|
background-color: hsla(180, 50%, 36%, .2);
|
||||||
|
}
|
||||||
|
#toc li[tabindex="0"]::before {
|
||||||
|
counter-increment: codelabel;
|
||||||
|
content: counter(codelabel) ": ";
|
||||||
|
}
|
||||||
.section:only-of-type .move-section-up,
|
.section:only-of-type .move-section-up,
|
||||||
.section:only-of-type .move-section-down {
|
.section:only-of-type .move-section-down {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -438,6 +481,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.applies-to.error {
|
||||||
|
background-color: #f002;
|
||||||
|
border-color: #f008;
|
||||||
|
}
|
||||||
.applies-to label {
|
.applies-to label {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -617,9 +664,12 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
#help-popup .CodeMirror {
|
#help-popup .CodeMirror {
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
}
|
}
|
||||||
|
#help-popup .keymap-list input[type="search"] {
|
||||||
|
margin: 0 0 2px;
|
||||||
|
}
|
||||||
.keymap-list {
|
.keymap-list {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
padding: 0 3px 0 0;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
@ -677,13 +727,13 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
#lint[open]:not(.hidden-unless-compact) {
|
#lint[open]:not(.hidden-unless-compact) {
|
||||||
min-height: 130px;
|
min-height: 102px;
|
||||||
}
|
}
|
||||||
#lint summary h2 {
|
#lint summary h2 {
|
||||||
margin-left: -16px;
|
text-indent: -2px;
|
||||||
}
|
}
|
||||||
#lint > .lint-scroll-container {
|
#lint > .lint-scroll-container {
|
||||||
margin: 42px 1rem 0;
|
margin: 34px 10px 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -721,7 +771,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#lint tr:hover {
|
#lint tr:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: hsla(180, 50%, 36%, .2);
|
||||||
}
|
}
|
||||||
#lint td[role="severity"] {
|
#lint td[role="severity"] {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
|
@ -799,8 +849,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
}
|
}
|
||||||
|
|
||||||
html:not(.usercss) .usercss-only,
|
html:not(.usercss) .usercss-only,
|
||||||
.usercss #mozilla-format-container,
|
.usercss .sectioned-only {
|
||||||
.usercss #sections > h2 {
|
|
||||||
display: none !important; /* hide during page init */
|
display: none !important; /* hide during page init */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -877,7 +926,7 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
padding-top: 40px;
|
padding-top: var(--fixed-padding);
|
||||||
}
|
}
|
||||||
.fixed-header #header {
|
.fixed-header #header {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
@ -885,10 +934,11 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 8px 0 0;
|
padding: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
.fixed-header #header > *:not(#lint) {
|
.fixed-header #header > *:not(#details-wrapper),
|
||||||
|
.fixed-header #options {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
#actions {
|
#actions {
|
||||||
|
@ -925,9 +975,31 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
#options-wrapper {
|
#options-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 0 1rem .5rem;
|
padding: .5rem 1rem 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
#toc {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
}
|
||||||
|
#details-wrapper {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-bottom: .25rem;
|
||||||
|
}
|
||||||
|
#options {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#sections-list[open] {
|
||||||
|
height: 102px;
|
||||||
|
}
|
||||||
|
#sections-list[open] #toc {
|
||||||
|
max-height: 60px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#sections-list,
|
||||||
|
#lint {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
.options-column {
|
.options-column {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-right: .5rem;
|
padding-right: .5rem;
|
||||||
|
@ -947,7 +1019,7 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
position: static;
|
position: static;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
#options summary {
|
#header summary {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
@ -966,15 +1038,11 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
top: 0.2rem;
|
top: 0.2rem;
|
||||||
}
|
}
|
||||||
#lint > .lint-scroll-container {
|
#lint > .lint-scroll-container {
|
||||||
margin: 32px 1rem 0;
|
margin: 26px 1rem 0;
|
||||||
bottom: 6px;
|
|
||||||
}
|
}
|
||||||
#lint {
|
#lint {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 1rem 0 0;
|
margin: .5rem 0 0;
|
||||||
}
|
|
||||||
#lint > summary {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
}
|
||||||
#lint:not([open]) + #footer {
|
#lint:not([open]) + #footer {
|
||||||
margin: .25em 0 -1em .25em;
|
margin: .25em 0 -1em .25em;
|
||||||
|
|
362
edit/edit.js
362
edit/edit.js
|
@ -1,52 +1,72 @@
|
||||||
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
|
/* global
|
||||||
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
|
$
|
||||||
closeCurrentTab messageBox debounce tryJSONparse
|
$$
|
||||||
initBeautifyButton ignoreChromeError dirtyReporter linter
|
$create
|
||||||
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
|
API
|
||||||
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
|
clipString
|
||||||
|
closeCurrentTab
|
||||||
|
CodeMirror
|
||||||
|
CODEMIRROR_THEMES
|
||||||
|
debounce
|
||||||
|
deepEqual
|
||||||
|
DirtyReporter
|
||||||
|
DocFuncMapper
|
||||||
|
FIREFOX
|
||||||
|
getOwnTab
|
||||||
|
initBeautifyButton
|
||||||
|
linter
|
||||||
|
messageBox
|
||||||
|
moveFocus
|
||||||
|
msg
|
||||||
|
onDOMready
|
||||||
|
prefs
|
||||||
|
rerouteHotkeys
|
||||||
|
SectionsEditor
|
||||||
|
sessionStorageHash
|
||||||
|
setupLivePrefs
|
||||||
|
SourceEditor
|
||||||
|
t
|
||||||
|
tHTML
|
||||||
|
tryCatch
|
||||||
|
tryJSONparse
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// direct & reverse mapping of @-moz-document keywords and internal property names
|
/** @type {EditorBase|SourceEditor|SectionsEditor} */
|
||||||
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
const editor = {
|
||||||
const CssToProperty = Object.entries(propertyToCss)
|
isUsercss: false,
|
||||||
.reduce((o, v) => {
|
previewDelay: 200, // Chrome devtools uses 200
|
||||||
o[v[1]] = v[0];
|
};
|
||||||
return o;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
let editor;
|
|
||||||
let isWindowed;
|
let isWindowed;
|
||||||
let scrollPointTimer;
|
let headerHeight;
|
||||||
|
|
||||||
window.addEventListener('beforeunload', beforeUnload);
|
window.on('beforeunload', beforeUnload);
|
||||||
msg.onExtension(onRuntimeMessage);
|
msg.onExtension(onRuntimeMessage);
|
||||||
|
|
||||||
lazyInit();
|
lazyInit();
|
||||||
|
|
||||||
(async function init() {
|
(async function init() {
|
||||||
const [style] = await Promise.all([
|
let style;
|
||||||
initStyleData(),
|
|
||||||
onDOMready(),
|
|
||||||
prefs.initializing.then(() => new Promise(resolve => {
|
|
||||||
const theme = prefs.get('editor.theme');
|
|
||||||
const el = $('#cm-theme');
|
|
||||||
if (theme === 'default') {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
// preload the theme so CodeMirror can use the correct metrics
|
|
||||||
el.href = `vendor/codemirror/theme/${theme}.css`;
|
|
||||||
el.addEventListener('load', resolve, {once: true});
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
]);
|
|
||||||
const usercss = isUsercss(style);
|
|
||||||
const dirty = dirtyReporter();
|
|
||||||
let wasDirty = false;
|
|
||||||
let nameTarget;
|
let nameTarget;
|
||||||
|
let wasDirty = false;
|
||||||
prefs.subscribe(['editor.linter'], updateLinter);
|
const dirty = new DirtyReporter();
|
||||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
await Promise.all([
|
||||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
initStyle(),
|
||||||
|
prefs.initializing
|
||||||
|
.then(initTheme),
|
||||||
|
onDOMready(),
|
||||||
|
]);
|
||||||
|
/** @namespace EditorBase */
|
||||||
|
Object.assign(editor, {
|
||||||
|
style,
|
||||||
|
dirty,
|
||||||
|
updateName,
|
||||||
|
updateToc,
|
||||||
|
toggleStyle,
|
||||||
|
});
|
||||||
|
prefs.subscribe('editor.linter', updateLinter);
|
||||||
|
prefs.subscribe('editor.keyMap', showHotkeyInTooltip);
|
||||||
|
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||||
showHotkeyInTooltip();
|
showHotkeyInTooltip();
|
||||||
buildThemeElement();
|
buildThemeElement();
|
||||||
buildKeymapElement();
|
buildKeymapElement();
|
||||||
|
@ -55,32 +75,57 @@ lazyInit();
|
||||||
initBeautifyButton($('#beautify'), () => editor.getEditors());
|
initBeautifyButton($('#beautify'), () => editor.getEditors());
|
||||||
initResizeListener();
|
initResizeListener();
|
||||||
detectLayout();
|
detectLayout();
|
||||||
updateTitle();
|
|
||||||
|
|
||||||
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
||||||
$('#preview-label').classList.toggle('hidden', !style.id);
|
$('#preview-label').classList.toggle('hidden', !style.id);
|
||||||
|
|
||||||
editor = (usercss ? createSourceEditor : createSectionsEditor)({
|
const toc = [];
|
||||||
style,
|
const elToc = $('#toc');
|
||||||
dirty,
|
elToc.onclick = e => editor.jumpToEditor([...elToc.children].indexOf(e.target));
|
||||||
updateName,
|
|
||||||
toggleStyle,
|
(editor.isUsercss ? SourceEditor : SectionsEditor)();
|
||||||
});
|
|
||||||
|
prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {now: true});
|
||||||
dirty.onChange(updateDirty);
|
dirty.onChange(updateDirty);
|
||||||
await editor.ready;
|
await editor.ready;
|
||||||
|
|
||||||
// enabling after init to prevent flash of validation failure on an empty name
|
// enabling after init to prevent flash of validation failure on an empty name
|
||||||
$('#name').required = !usercss;
|
$('#name').required = !editor.isUsercss;
|
||||||
$('#save-button').onclick = editor.save;
|
$('#save-button').onclick = editor.save;
|
||||||
|
|
||||||
|
async function initStyle() {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const id = Number(params.get('id'));
|
||||||
|
style = id ? await API.getStyle(id) : initEmptyStyle(params);
|
||||||
|
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
|
||||||
|
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
|
||||||
|
document.documentElement.classList.toggle('usercss', editor.isUsercss);
|
||||||
|
sessionStorage.justEditedStyleId = style.id || '';
|
||||||
|
// no such style so let's clear the invalid URL parameters
|
||||||
|
if (!style.id) history.replaceState({}, '', location.pathname);
|
||||||
|
updateTitle(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEmptyStyle(params) {
|
||||||
|
return {
|
||||||
|
name: params.get('domain') ||
|
||||||
|
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
||||||
|
'',
|
||||||
|
enabled: true,
|
||||||
|
sections: [
|
||||||
|
DocFuncMapper.toSection([...params], {code: ''}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function initNameArea() {
|
function initNameArea() {
|
||||||
const nameEl = $('#name');
|
const nameEl = $('#name');
|
||||||
const resetEl = $('#reset-name');
|
const resetEl = $('#reset-name');
|
||||||
const isCustomName = style.updateUrl || usercss;
|
const isCustomName = style.updateUrl || editor.isUsercss;
|
||||||
nameTarget = isCustomName ? 'customName' : 'name';
|
nameTarget = isCustomName ? 'customName' : 'name';
|
||||||
nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||||
nameEl.title = isCustomName ? t('customNameHint') : '';
|
nameEl.title = isCustomName ? t('customNameHint') : '';
|
||||||
nameEl.addEventListener('input', () => {
|
nameEl.on('input', () => {
|
||||||
updateName(true);
|
updateName(true);
|
||||||
resetEl.hidden = false;
|
resetEl.hidden = false;
|
||||||
});
|
});
|
||||||
|
@ -101,6 +146,38 @@ lazyInit();
|
||||||
enabledEl.onchange = () => updateEnabledness(enabledEl.checked);
|
enabledEl.onchange = () => updateEnabledness(enabledEl.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initResizeListener() {
|
||||||
|
const {onBoundsChanged} = chrome.windows || {};
|
||||||
|
if (onBoundsChanged) {
|
||||||
|
// * movement is reported even if the window wasn't resized
|
||||||
|
// * fired just once when done so debounce is not needed
|
||||||
|
onBoundsChanged.addListener(wnd => {
|
||||||
|
// getting the current window id as it may change if the user attached/detached the tab
|
||||||
|
chrome.windows.getCurrent(ownWnd => {
|
||||||
|
if (wnd.id === ownWnd.id) saveWindowPos();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.on('resize', () => {
|
||||||
|
if (!onBoundsChanged) debounce(saveWindowPos, 100);
|
||||||
|
detectLayout();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const theme = prefs.get('editor.theme');
|
||||||
|
const el = $('#cm-theme');
|
||||||
|
if (theme === 'default') {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
// preload the theme so CodeMirror can use the correct metrics
|
||||||
|
el.href = `vendor/codemirror/theme/${theme}.css`;
|
||||||
|
el.on('load', resolve, {once: true});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function findKeyForCommand(command, map) {
|
function findKeyForCommand(command, map) {
|
||||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
||||||
let key = Object.keys(map).find(k => map[k] === command);
|
let key = Object.keys(map).find(k => map[k] === command);
|
||||||
|
@ -171,24 +248,6 @@ lazyInit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initResizeListener() {
|
|
||||||
const {onBoundsChanged} = chrome.windows || {};
|
|
||||||
if (onBoundsChanged) {
|
|
||||||
// * movement is reported even if the window wasn't resized
|
|
||||||
// * fired just once when done so debounce is not needed
|
|
||||||
onBoundsChanged.addListener(wnd => {
|
|
||||||
// getting the current window id as it may change if the user attached/detached the tab
|
|
||||||
chrome.windows.getCurrent(ownWnd => {
|
|
||||||
if (wnd.id === ownWnd.id) saveWindowPos();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
if (!onBoundsChanged) debounce(saveWindowPos, 100);
|
|
||||||
detectLayout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStyle() {
|
function toggleStyle() {
|
||||||
$('#enabled').checked = !style.enabled;
|
$('#enabled').checked = !style.enabled;
|
||||||
updateEnabledness(!style.enabled);
|
updateEnabledness(!style.enabled);
|
||||||
|
@ -217,17 +276,50 @@ lazyInit();
|
||||||
dirty.modify('name', style[nameTarget] || style.name, value);
|
dirty.modify('name', style[nameTarget] || style.name, value);
|
||||||
style[nameTarget] = value;
|
style[nameTarget] = value;
|
||||||
}
|
}
|
||||||
updateTitle({});
|
updateTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTitle() {
|
function updateTitle(isDirty = dirty.isDirty()) {
|
||||||
document.title = `${dirty.isDirty() ? '* ' : ''}${style.customName || style.name}`;
|
document.title = `${isDirty ? '* ' : ''}${style.customName || style.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLinter(key, value) {
|
function updateLinter(key, value) {
|
||||||
$('body').classList.toggle('linter-disabled', value === '');
|
$('body').classList.toggle('linter-disabled', value === '');
|
||||||
linter.run();
|
linter.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateToc(added = editor.sections) {
|
||||||
|
const {sections} = editor;
|
||||||
|
const first = sections.indexOf(added[0]);
|
||||||
|
let el = elToc.children[first];
|
||||||
|
if (added.focus) {
|
||||||
|
const cls = 'current';
|
||||||
|
const old = $('.' + cls, elToc);
|
||||||
|
if (old && old !== el) old.classList.remove(cls);
|
||||||
|
el.classList.add(cls);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (first >= 0) {
|
||||||
|
for (let i = first; i < sections.length; i++) {
|
||||||
|
const entry = sections[i].tocEntry;
|
||||||
|
if (!deepEqual(entry, toc[i])) {
|
||||||
|
if (!el) el = elToc.appendChild($create('li', {tabIndex: 0}));
|
||||||
|
el.tabIndex = entry.removed ? -1 : 0;
|
||||||
|
toc[i] = Object.assign({}, entry);
|
||||||
|
const s = el.textContent = clipString(entry.label) || (
|
||||||
|
entry.target == null
|
||||||
|
? t('appliesToEverything')
|
||||||
|
: clipString(entry.target) + (entry.numTargets > 1 ? ', ...' : ''));
|
||||||
|
if (s.length > 30) el.title = s;
|
||||||
|
}
|
||||||
|
el = el.nextElementSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (toc.length > sections.length) {
|
||||||
|
elToc.lastElementChild.remove();
|
||||||
|
toc.length--;
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* Stuff not needed for the main init so we can let it run at its own tempo */
|
/* Stuff not needed for the main init so we can let it run at its own tempo */
|
||||||
|
@ -330,53 +422,6 @@ function beforeUnload(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUsercss(style) {
|
|
||||||
return (
|
|
||||||
style.usercssData ||
|
|
||||||
!style.id && prefs.get('newStyleAsUsercss')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initStyleData() {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const id = Number(params.get('id'));
|
|
||||||
const createEmptyStyle = () => ({
|
|
||||||
name: params.get('domain') ||
|
|
||||||
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
|
||||||
'',
|
|
||||||
enabled: true,
|
|
||||||
sections: [
|
|
||||||
Object.assign({code: ''},
|
|
||||||
...Object.keys(CssToProperty)
|
|
||||||
.map(name => ({
|
|
||||||
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return fetchStyle()
|
|
||||||
.then(style => {
|
|
||||||
if (style.id) sessionStorage.justEditedStyleId = style.id;
|
|
||||||
// we set "usercss" class on <html> when <body> is empty
|
|
||||||
// so there'll be no flickering of the elements that depend on it
|
|
||||||
if (isUsercss(style)) {
|
|
||||||
document.documentElement.classList.add('usercss');
|
|
||||||
}
|
|
||||||
// strip URL parameters when invoked for a non-existent id
|
|
||||||
if (!style.id) {
|
|
||||||
history.replaceState({}, document.title, location.pathname);
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
});
|
|
||||||
|
|
||||||
function fetchStyle() {
|
|
||||||
if (id) {
|
|
||||||
return API.getStyle(id);
|
|
||||||
}
|
|
||||||
return Promise.resolve(createEmptyStyle());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHelp(title = '', body) {
|
function showHelp(title = '', body) {
|
||||||
const div = $('#help-popup');
|
const div = $('#help-popup');
|
||||||
div.className = '';
|
div.className = '';
|
||||||
|
@ -419,11 +464,11 @@ function showHelp(title = '', body) {
|
||||||
div.style.display = '';
|
div.style.display = '';
|
||||||
contents.textContent = '';
|
contents.textContent = '';
|
||||||
clearTimeout(contents.timer);
|
clearTimeout(contents.timer);
|
||||||
window.removeEventListener('keydown', showHelp.close, true);
|
window.off('keydown', showHelp.close, true);
|
||||||
window.dispatchEvent(new Event('closeHelp'));
|
window.dispatchEvent(new Event('closeHelp'));
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keydown', showHelp.close, true);
|
window.on('keydown', showHelp.close, true);
|
||||||
$('.dismiss', div).onclick = showHelp.close;
|
$('.dismiss', div).onclick = showHelp.close;
|
||||||
|
|
||||||
// reset any inline styles
|
// reset any inline styles
|
||||||
|
@ -433,6 +478,7 @@ function showHelp(title = '', body) {
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* exported showCodeMirrorPopup */
|
||||||
function showCodeMirrorPopup(title, html, options) {
|
function showCodeMirrorPopup(title, html, options) {
|
||||||
const popup = showHelp(title, html);
|
const popup = showHelp(title, html);
|
||||||
popup.classList.add('big');
|
popup.classList.add('big');
|
||||||
|
@ -462,10 +508,10 @@ function showCodeMirrorPopup(title, html, options) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKeyDown, true);
|
window.on('keydown', onKeyDown, true);
|
||||||
|
|
||||||
window.addEventListener('closeHelp', () => {
|
window.on('closeHelp', () => {
|
||||||
window.removeEventListener('keydown', onKeyDown, true);
|
window.off('keydown', onKeyDown, true);
|
||||||
document.documentElement.style.removeProperty('pointer-events');
|
document.documentElement.style.removeProperty('pointer-events');
|
||||||
rerouteHotkeys(true);
|
rerouteHotkeys(true);
|
||||||
cm = popup.codebox = null;
|
cm = popup.codebox = null;
|
||||||
|
@ -493,59 +539,32 @@ function saveWindowPos() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixedHeader() {
|
function fixedHeader() {
|
||||||
const scrollPoint = $('#header').clientHeight - 40;
|
const headerFixed = $('.fixed-header');
|
||||||
const linterEnabled = prefs.get('editor.linter') !== '';
|
if (!headerFixed) headerHeight = $('#header').clientHeight;
|
||||||
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
|
const scrollPoint = headerHeight - 43;
|
||||||
|
if (window.scrollY >= scrollPoint && !headerFixed) {
|
||||||
|
$('body').style.setProperty('--fixed-padding', ` ${headerHeight}px`);
|
||||||
$('body').classList.add('fixed-header');
|
$('body').classList.add('fixed-header');
|
||||||
} else if (window.scrollY < 40 && linterEnabled) {
|
} else if (window.scrollY < scrollPoint && headerFixed) {
|
||||||
$('body').classList.remove('fixed-header');
|
$('body').classList.remove('fixed-header');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectLayout() {
|
function detectLayout() {
|
||||||
const body = $('body');
|
|
||||||
const options = $('#options');
|
|
||||||
const lint = $('#lint');
|
|
||||||
const compact = window.innerWidth <= 850;
|
const compact = window.innerWidth <= 850;
|
||||||
const shortViewportLinter = window.innerHeight < 692;
|
|
||||||
const shortViewportNoLinter = window.innerHeight < 554;
|
|
||||||
const linterEnabled = prefs.get('editor.linter') !== '';
|
|
||||||
if (compact) {
|
if (compact) {
|
||||||
body.classList.add('compact-layout');
|
document.body.classList.add('compact-layout');
|
||||||
options.removeAttribute('open');
|
if (!editor.isUsercss) {
|
||||||
options.classList.add('ignore-pref');
|
debounce(fixedHeader, 250);
|
||||||
lint.removeAttribute('open');
|
window.on('scroll', fixedHeader, {passive: true});
|
||||||
lint.classList.add('ignore-pref');
|
|
||||||
if (!$('.usercss')) {
|
|
||||||
clearTimeout(scrollPointTimer);
|
|
||||||
scrollPointTimer = setTimeout(() => {
|
|
||||||
const scrollPoint = $('#header').clientHeight - 40;
|
|
||||||
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
|
|
||||||
body.classList.add('fixed-header');
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
window.addEventListener('scroll', fixedHeader, {passive: true});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
body.classList.remove('compact-layout');
|
document.body.classList.remove('compact-layout', 'fixed-header');
|
||||||
body.classList.remove('fixed-header');
|
window.off('scroll', fixedHeader);
|
||||||
window.removeEventListener('scroll', fixedHeader);
|
}
|
||||||
if (shortViewportLinter && linterEnabled || shortViewportNoLinter && !linterEnabled) {
|
for (const type of ['options', 'toc', 'lint']) {
|
||||||
options.removeAttribute('open');
|
const el = $(`details[data-pref="editor.${type}.expanded"]`);
|
||||||
options.classList.add('ignore-pref');
|
el.open = compact ? false : prefs.get(el.dataset.pref);
|
||||||
if (prefs.get('editor.lint.expanded')) {
|
|
||||||
lint.setAttribute('open', '');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options.classList.remove('ignore-pref');
|
|
||||||
lint.classList.remove('ignore-pref');
|
|
||||||
if (prefs.get('editor.options.expanded')) {
|
|
||||||
options.setAttribute('open', '');
|
|
||||||
}
|
|
||||||
if (prefs.get('editor.lint.expanded')) {
|
|
||||||
lint.setAttribute('open', '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,14 +581,3 @@ function isWindowMaximized() {
|
||||||
window.outerHeight < screen.availHeight + 10
|
window.outerHeight < screen.availHeight + 10
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleContextMenuDelete(event) {
|
|
||||||
if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
|
|
||||||
chrome.contextMenus.update('editor.contextDelete', {
|
|
||||||
enabled: Boolean(
|
|
||||||
this.selectionStart !== this.selectionEnd ||
|
|
||||||
this.somethingSelected && this.somethingSelected()
|
|
||||||
),
|
|
||||||
}, ignoreChromeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ onDOMready().then(() => {
|
||||||
doReplace();
|
doReplace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return !event.target.closest(focusAccessibility.ELEMENTS.join(','));
|
return !focusAccessibility.closest(event.target);
|
||||||
},
|
},
|
||||||
'Esc': () => {
|
'Esc': () => {
|
||||||
destroyDialog({restoreFocus: true});
|
destroyDialog({restoreFocus: true});
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
/* exported createMetaCompiler */
|
/* exported createMetaCompiler */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createMetaCompiler(cm) {
|
/**
|
||||||
const updateListeners = [];
|
* @param {CodeMirror} cm
|
||||||
|
* @param {function(meta:Object)} onUpdated
|
||||||
|
*/
|
||||||
|
function createMetaCompiler(cm, onUpdated) {
|
||||||
let meta = null;
|
let meta = null;
|
||||||
let metaIndex = null;
|
let metaIndex = null;
|
||||||
let cache = [];
|
let cache = [];
|
||||||
|
@ -22,9 +25,7 @@ function createMetaCompiler(cm) {
|
||||||
return editorWorker.metalint(match[0])
|
return editorWorker.metalint(match[0])
|
||||||
.then(({metadata, errors}) => {
|
.then(({metadata, errors}) => {
|
||||||
if (errors.every(err => err.code === 'unknownMeta')) {
|
if (errors.every(err => err.code === 'unknownMeta')) {
|
||||||
for (const cb of updateListeners) {
|
onUpdated(metadata);
|
||||||
cb(metadata);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cache = errors.map(err =>
|
cache = errors.map(err =>
|
||||||
({
|
({
|
||||||
|
@ -40,8 +41,4 @@ function createMetaCompiler(cm) {
|
||||||
return cache;
|
return cache;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
onUpdated: cb => updateListeners.push(cb)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/* exported createLivePreview */
|
/* exported createLivePreview */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createLivePreview(preprocess) {
|
function createLivePreview(preprocess, shouldShow) {
|
||||||
let data;
|
let data;
|
||||||
let previewer;
|
let previewer;
|
||||||
let enabled = prefs.get('editor.livePreview');
|
let enabled = prefs.get('editor.livePreview');
|
||||||
|
@ -20,6 +20,7 @@ function createLivePreview(preprocess) {
|
||||||
}
|
}
|
||||||
enabled = value;
|
enabled = value;
|
||||||
});
|
});
|
||||||
|
if (shouldShow != null) show(shouldShow);
|
||||||
return {update, show};
|
return {update, show};
|
||||||
|
|
||||||
function show(state) {
|
function show(state) {
|
||||||
|
|
386
edit/moz-section-finder.js
Normal file
386
edit/moz-section-finder.js
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
/* global
|
||||||
|
CodeMirror
|
||||||
|
debounce
|
||||||
|
deepEqual
|
||||||
|
trimCommentLabel
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* exported MozSectionFinder */
|
||||||
|
function MozSectionFinder(cm) {
|
||||||
|
const KEY = 'MozSectionFinder';
|
||||||
|
const MOZ_DOC_LEN = '@-moz-document'.length;
|
||||||
|
const rxDOC = /@-moz-document(\s+|$)/ig;
|
||||||
|
const rxFUNC = /(url|url-prefix|domain|regexp)\(/iy;
|
||||||
|
const rxQUOTE = /['"]/y;
|
||||||
|
const rxSPACE = /\s+/y;
|
||||||
|
const rxTokDOC = /^(?!comment|string)/;
|
||||||
|
const rxTokCOMMENT = /^comment(\s|$)/;
|
||||||
|
const rxTokSTRING = /^string(\s|$)/;
|
||||||
|
const {cmpPos} = CodeMirror;
|
||||||
|
const minPos = (a, b) => cmpPos(a, b) < 0 ? a : b;
|
||||||
|
const maxPos = (a, b) => cmpPos(a, b) > 0 ? a : b;
|
||||||
|
const keptAlive = new Map();
|
||||||
|
/** @type {CodeMirror.Pos} */
|
||||||
|
let updFrom;
|
||||||
|
/** @type {CodeMirror.Pos} */
|
||||||
|
let updTo;
|
||||||
|
|
||||||
|
const MozSectionFinder = {
|
||||||
|
IGNORE_ORIGIN: KEY,
|
||||||
|
EQ_SKIP_KEYS: [
|
||||||
|
'mark',
|
||||||
|
'valueStart',
|
||||||
|
'valueEnd',
|
||||||
|
'sticky', // added by TextMarker::find()
|
||||||
|
],
|
||||||
|
get sections() {
|
||||||
|
return getState().sections;
|
||||||
|
},
|
||||||
|
keepAliveFor(id, ms) {
|
||||||
|
let data = keptAlive.get(id);
|
||||||
|
if (data) {
|
||||||
|
clearTimeout(data.timer);
|
||||||
|
} else {
|
||||||
|
const NOP = () => 0;
|
||||||
|
data = {fn: NOP};
|
||||||
|
keptAlive.set(id, data);
|
||||||
|
MozSectionFinder.on(NOP);
|
||||||
|
}
|
||||||
|
data.timer = setTimeout(id => keptAlive.delete(id), ms, id);
|
||||||
|
},
|
||||||
|
on(fn) {
|
||||||
|
const {listeners} = getState();
|
||||||
|
const needsInit = !listeners.size;
|
||||||
|
listeners.add(fn);
|
||||||
|
if (needsInit) {
|
||||||
|
cm.on('changes', onCmChanges);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
off(fn) {
|
||||||
|
const {listeners, sections} = getState();
|
||||||
|
if (listeners.size) {
|
||||||
|
listeners.delete(fn);
|
||||||
|
if (!listeners.size) {
|
||||||
|
cm.off('changes', onCmChanges);
|
||||||
|
cm.operation(() => sections.forEach(sec => sec.mark.clear()));
|
||||||
|
sections.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOff(fn, enable) {
|
||||||
|
MozSectionFinder[enable ? 'on' : 'off'](fn);
|
||||||
|
},
|
||||||
|
/** @param {MozSection} [section] */
|
||||||
|
updatePositions(section) {
|
||||||
|
(section ? [section] : getState().sections).forEach(setPositionFromMark);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return MozSectionFinder;
|
||||||
|
|
||||||
|
/** @returns {MozSectionCmState} */
|
||||||
|
function getState() {
|
||||||
|
let state = cm.state[KEY];
|
||||||
|
if (!state) {
|
||||||
|
state = cm.state[KEY] = /** @namespace MozSectionCmState */ {
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
listeners: new Set(),
|
||||||
|
/** @type {MozSection[]} */
|
||||||
|
sections: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCmChanges(cm, changes) {
|
||||||
|
if (!updFrom) updFrom = {line: Infinity, ch: 0};
|
||||||
|
if (!updTo) updTo = {line: -1, ch: 0};
|
||||||
|
for (const c of changes) {
|
||||||
|
if (c.origin !== MozSectionFinder.IGNORE_ORIGIN) {
|
||||||
|
updFrom = minPos(c.from, updFrom);
|
||||||
|
updTo = maxPos(CodeMirror.changeEnd(c), updTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updTo.line >= 0) {
|
||||||
|
debounce(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(
|
||||||
|
from = updFrom || {line: 0, ch: 0},
|
||||||
|
to = updTo || {line: cm.doc.size, ch: 0}
|
||||||
|
) {
|
||||||
|
updFrom = updTo = null;
|
||||||
|
const {sections, listeners} = getState();
|
||||||
|
let cutAt = -1;
|
||||||
|
let cutTo = -1;
|
||||||
|
for (let i = 0, sec; (sec = sections[i]); i++) {
|
||||||
|
if (cmpPos(sec.end, from) >= 0) {
|
||||||
|
if (cutAt < 0) {
|
||||||
|
cutAt = i;
|
||||||
|
from = minPos(from, sec.start);
|
||||||
|
}
|
||||||
|
// Sections that start/end after `from` may have incorrect positions
|
||||||
|
if (setPositionFromMark(sec)) {
|
||||||
|
if (cmpPos(sec.start, to) > 0) {
|
||||||
|
cutTo = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
to = maxPos(sec.end, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cutAt < 0) from.ch = Math.max(0, from.ch - MOZ_DOC_LEN);
|
||||||
|
if (cutTo < 0) to.ch += MOZ_DOC_LEN;
|
||||||
|
const added = findSections(from, to);
|
||||||
|
if (!added.length && cutAt < 0 && cutTo < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cutTo < 0) {
|
||||||
|
cutTo = sections.length;
|
||||||
|
}
|
||||||
|
let op;
|
||||||
|
let reusedAtStart = 0;
|
||||||
|
const removed = sections.slice(cutAt, cutTo);
|
||||||
|
for (const sec of added) {
|
||||||
|
const i = removed.findIndex(isSameSection, sec);
|
||||||
|
if (i >= 0) {
|
||||||
|
removed[i].funcs = sec.funcs; // use the new valueStart, valueEnd
|
||||||
|
removed[i] = null;
|
||||||
|
if (!op) reusedAtStart++;
|
||||||
|
} else {
|
||||||
|
if (!op) op = cm.curOp || (cm.startOperation(), true);
|
||||||
|
sec.mark = cm.markText(sec.start, sec.end, {
|
||||||
|
clearWhenEmpty: false,
|
||||||
|
inclusiveRight: true,
|
||||||
|
[KEY]: sec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reusedAtStart) {
|
||||||
|
cutAt += reusedAtStart;
|
||||||
|
added.splice(0, reusedAtStart);
|
||||||
|
}
|
||||||
|
for (const sec of removed) {
|
||||||
|
if (sec) {
|
||||||
|
if (!op) op = cm.curOp || (cm.startOperation(), true);
|
||||||
|
sec.mark.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (op) {
|
||||||
|
sections.splice(cutAt, cutTo - cutAt, ...added);
|
||||||
|
listeners.forEach(fn => fn(added, removed, cutAt, cutTo));
|
||||||
|
}
|
||||||
|
if (op === true) {
|
||||||
|
cm.endOperation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CodeMirror.Pos} from
|
||||||
|
* @param {CodeMirror.Pos} to
|
||||||
|
* @returns MozSection[]
|
||||||
|
*/
|
||||||
|
function findSections(from, to) {
|
||||||
|
/** @type MozSection[] */
|
||||||
|
const found = [];
|
||||||
|
let line = from.line - 1;
|
||||||
|
let goal = '';
|
||||||
|
let section, func, funcPos, url;
|
||||||
|
/** @type {MozSectionFunc[]} */
|
||||||
|
let funcs;
|
||||||
|
// will stop after to.line if there's no goal anymore, see `return true` below
|
||||||
|
cm.eachLine(from.line, cm.doc.size, handle => {
|
||||||
|
++line;
|
||||||
|
const {text} = handle;
|
||||||
|
const len = text.length;
|
||||||
|
if (!len) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ch = line === from.line ? from.ch : 0;
|
||||||
|
while (true) {
|
||||||
|
let m;
|
||||||
|
if (!goal) {
|
||||||
|
// useful for minified styles with long lines
|
||||||
|
if ((line - to.line || ch - to.ch) >= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((ch = text.indexOf('@-', ch)) < 0 ||
|
||||||
|
!(rxDOC.lastIndex = ch, m = rxDOC.exec(text))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ch = m.index + m[0].length;
|
||||||
|
section = /** @namespace MozSection */ {
|
||||||
|
funcs: funcs = [],
|
||||||
|
start: {line, ch: m.index},
|
||||||
|
end: null,
|
||||||
|
mark: null,
|
||||||
|
tocEntry: {
|
||||||
|
label: '',
|
||||||
|
target: null,
|
||||||
|
numTargets: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (rxTokDOC.test(cm.getTokenTypeAt(section.start))) {
|
||||||
|
found.push(section);
|
||||||
|
goal = '_func';
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handle.styles) cm.getTokenTypeAt({line, ch: 0});
|
||||||
|
const {styles} = handle;
|
||||||
|
let j = 1;
|
||||||
|
if (ch) {
|
||||||
|
j += styles.length * ch / len & ~1;
|
||||||
|
while (styles[j - 2] >= ch) j -= 2;
|
||||||
|
while (styles[j] <= ch) j += 2;
|
||||||
|
}
|
||||||
|
let type;
|
||||||
|
for (; goal && j < styles.length; ch = styles[j], j += 2) {
|
||||||
|
let s;
|
||||||
|
type = styles[j + 1];
|
||||||
|
if (goal.startsWith('_')) {
|
||||||
|
if (!type && (rxSPACE.lastIndex = ch, rxSPACE.test(text))) {
|
||||||
|
ch = rxSPACE.lastIndex;
|
||||||
|
if (ch === styles[j]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isCmt = type && rxTokCOMMENT.test(type);
|
||||||
|
if (goal === '_cmt') {
|
||||||
|
const cmt = trimCommentLabel(text.slice(ch, styles[j]));
|
||||||
|
if (isCmt && cmt) section.tocEntry.label = cmt;
|
||||||
|
if (!isCmt || cmt) goal = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isCmt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
goal = goal.slice(1);
|
||||||
|
}
|
||||||
|
if (goal === 'func') {
|
||||||
|
if (!type || !(rxFUNC.lastIndex = ch, m = rxFUNC.exec(text))) {
|
||||||
|
goal = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
func = m[1];
|
||||||
|
funcPos = {line, ch};
|
||||||
|
url = null;
|
||||||
|
goal = '_str';
|
||||||
|
// Tokens in `styles` are split into multiple items due to `overlay`.
|
||||||
|
while (styles[j + 2] <= ch + func.length + 1) j += 2;
|
||||||
|
}
|
||||||
|
if (goal === 'str') {
|
||||||
|
if (!rxTokSTRING.test(type)) {
|
||||||
|
if (url && !url.quote && !type && text[ch] === ')') {
|
||||||
|
goal = ')';
|
||||||
|
} else {
|
||||||
|
goal = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!url) {
|
||||||
|
s = (rxQUOTE.lastIndex = ch, rxQUOTE.test(text)) && text[ch];
|
||||||
|
url = {
|
||||||
|
chunks: [],
|
||||||
|
start: {line, ch: s ? ch + 1 : ch},
|
||||||
|
end: null,
|
||||||
|
quote: s,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
s = text.slice(ch, styles[j]);
|
||||||
|
url.chunks.push(s);
|
||||||
|
url.end = {line, ch: styles[j]};
|
||||||
|
// CSS strings can span multiple lines.
|
||||||
|
// Tokens in `styles` are split into multiple items due to `overlay`.
|
||||||
|
if (url.quote && s.endsWith(url.quote) && s[s.length - 2] !== '\\') {
|
||||||
|
url.end.ch--;
|
||||||
|
goal = '_)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (goal === ')') {
|
||||||
|
if (text[ch] !== ')') {
|
||||||
|
goal = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ch++;
|
||||||
|
s = url.chunks.join('');
|
||||||
|
if (url.quote) s = s.slice(1, -1);
|
||||||
|
if (!funcs.length) section.tocEntry.target = s;
|
||||||
|
section.tocEntry.numTargets++;
|
||||||
|
funcs.push(/** @namespace MozSectionFunc */ {
|
||||||
|
type: func,
|
||||||
|
value: s,
|
||||||
|
isQuoted: url.quote,
|
||||||
|
start: funcPos,
|
||||||
|
end: {line, ch},
|
||||||
|
valueStart: url.start,
|
||||||
|
valueEnd: url.end,
|
||||||
|
});
|
||||||
|
s = text.slice(ch, styles[j]).trim();
|
||||||
|
goal = s.startsWith(',') ? '_func' :
|
||||||
|
s.startsWith('{') ? '_cmt' :
|
||||||
|
!s && '_,'; // non-space something at this place = syntax error
|
||||||
|
if (!goal) {
|
||||||
|
goal = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (goal === ',') {
|
||||||
|
goal = text[ch] === ',' ? '_func' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.end = {line, ch: styles[j + 2] || len};
|
||||||
|
// at this point it's either an error...
|
||||||
|
if (goal === 'error') {
|
||||||
|
goal = '';
|
||||||
|
section.funcs.length = 0;
|
||||||
|
}
|
||||||
|
// ...or a EOL, in which case we'll advance to the next line
|
||||||
|
if (goal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MozSection|MozSectionFunc} obj
|
||||||
|
* @returns {?{from:CodeMirror.Pos, to:CodeMirror.Pos}} falsy if marker was removed
|
||||||
|
*/
|
||||||
|
function setPositionFromMark(obj) {
|
||||||
|
const pos = obj.mark.find();
|
||||||
|
obj.start = pos && pos.from;
|
||||||
|
obj.end = pos && pos.to;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @this {MozSection} new section
|
||||||
|
* @param {MozSection} old
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isSameSection(old) {
|
||||||
|
return old &&
|
||||||
|
old.start &&
|
||||||
|
old.tocEntry.label === this.tocEntry.label &&
|
||||||
|
!cmpPos(old.start, this.start) &&
|
||||||
|
!cmpPos(old.end, this.end) &&
|
||||||
|
old.funcs.length === this.funcs.length &&
|
||||||
|
old.funcs.every(isSameFunc, this.funcs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @this {MozSectionFunc[]} new functions */
|
||||||
|
function isSameFunc(func, i) {
|
||||||
|
return deepEqual(func, this[i], MozSectionFinder.EQ_SKIP_KEYS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef CodeMirror.Pos
|
||||||
|
* @property {number} line
|
||||||
|
* @property {number} ch
|
||||||
|
*/
|
447
edit/moz-section-widget.js
Normal file
447
edit/moz-section-widget.js
Normal file
|
@ -0,0 +1,447 @@
|
||||||
|
/* global
|
||||||
|
$
|
||||||
|
$create
|
||||||
|
CodeMirror
|
||||||
|
colorMimicry
|
||||||
|
messageBox
|
||||||
|
MozSectionFinder
|
||||||
|
msg
|
||||||
|
prefs
|
||||||
|
regExpTester
|
||||||
|
t
|
||||||
|
template
|
||||||
|
tryCatch
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* exported MozSectionWidget */
|
||||||
|
function MozSectionWidget(
|
||||||
|
cm,
|
||||||
|
finder = MozSectionFinder(cm),
|
||||||
|
onDirectChange = () => 0
|
||||||
|
) {
|
||||||
|
let TPL, EVENTS, CLICK_ROUTE;
|
||||||
|
const KEY = 'MozSectionWidget';
|
||||||
|
const C_CONTAINER = '.applies-to';
|
||||||
|
const C_LABEL = 'label';
|
||||||
|
const C_LIST = '.applies-to-list';
|
||||||
|
const C_ITEM = '.applies-to-item';
|
||||||
|
const C_TYPE = '.applies-type';
|
||||||
|
const C_VALUE = '.applies-value';
|
||||||
|
/** @returns {MarkedFunc} */
|
||||||
|
const getFuncFor = el => el.closest(C_ITEM)[KEY];
|
||||||
|
/** @returns {MarkedFunc[]} */
|
||||||
|
const getFuncsFor = el => el.closest(C_LIST)[KEY];
|
||||||
|
/** @returns {MozSection} */
|
||||||
|
const getSectionFor = el => el.closest(C_CONTAINER)[KEY];
|
||||||
|
const {cmpPos} = CodeMirror;
|
||||||
|
let enabled = false;
|
||||||
|
let funcHeight = 0;
|
||||||
|
let actualStyle;
|
||||||
|
return {
|
||||||
|
toggle(enable) {
|
||||||
|
if (Boolean(enable) !== enabled) {
|
||||||
|
(enable ? init : destroy)();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
enabled = true;
|
||||||
|
TPL = {
|
||||||
|
container:
|
||||||
|
$create('div' + C_CONTAINER, [
|
||||||
|
$create(C_LABEL, t('appliesLabel')),
|
||||||
|
$create('ul' + C_LIST),
|
||||||
|
]),
|
||||||
|
listItem:
|
||||||
|
template.appliesTo.cloneNode(true),
|
||||||
|
appliesToEverything:
|
||||||
|
$create('li.applies-to-everything', t('appliesToEverything')),
|
||||||
|
};
|
||||||
|
|
||||||
|
$(C_VALUE, TPL.listItem).after(
|
||||||
|
$create('button.test-regexp', t('styleRegexpTestButton')));
|
||||||
|
|
||||||
|
CLICK_ROUTE = {
|
||||||
|
'.test-regexp': showRegExpTester,
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} elItem
|
||||||
|
* @param {MarkedFunc} func
|
||||||
|
*/
|
||||||
|
'.remove-applies-to'(elItem, func) {
|
||||||
|
const funcs = getFuncsFor(elItem);
|
||||||
|
if (funcs.length < 2) {
|
||||||
|
messageBox({
|
||||||
|
contents: t('appliesRemoveError'),
|
||||||
|
buttons: [t('confirmClose')]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const i = funcs.indexOf(func);
|
||||||
|
const next = funcs[i + 1];
|
||||||
|
const from = i ? funcs[i - 1].item.find(1) : func.item.find(-1);
|
||||||
|
const to = next ? next.item.find(-1) : func.item.find(1);
|
||||||
|
cm.replaceRange(i && next ? ', ' : '', from, to);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} elItem
|
||||||
|
* @param {MarkedFunc} func
|
||||||
|
*/
|
||||||
|
'.add-applies-to'(elItem, func) {
|
||||||
|
const pos = func.item.find(1);
|
||||||
|
cm.replaceRange(`, ${func.typeText}("")`, pos, pos);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
EVENTS = {
|
||||||
|
onchange({target: el}) {
|
||||||
|
EVENTS.oninput({target: el.closest(C_TYPE) || el});
|
||||||
|
},
|
||||||
|
oninput({target: el}) {
|
||||||
|
const part =
|
||||||
|
el.matches(C_VALUE) && 'value' ||
|
||||||
|
el.matches(C_TYPE) && 'type';
|
||||||
|
if (!part) return;
|
||||||
|
const func = getFuncFor(el);
|
||||||
|
const pos = func[part].find();
|
||||||
|
if (part === 'type' && el.value !== func.typeText) {
|
||||||
|
func.typeText = func.item[KEY].dataset.type = el.value;
|
||||||
|
}
|
||||||
|
if (part === 'value' && func === getFuncsFor(el)[0]) {
|
||||||
|
const sec = getSectionFor(el);
|
||||||
|
sec.tocEntry.target = el.value;
|
||||||
|
if (!sec.tocEntry.label) onDirectChange([sec]);
|
||||||
|
}
|
||||||
|
cm.replaceRange(toDoubleslash(el.value), pos.from, pos.to, finder.IGNORE_ORIGIN);
|
||||||
|
},
|
||||||
|
onclick(event) {
|
||||||
|
const {target} = event;
|
||||||
|
for (const selector in CLICK_ROUTE) {
|
||||||
|
const routed = target.closest(selector);
|
||||||
|
if (routed) {
|
||||||
|
const elItem = routed.closest(C_ITEM);
|
||||||
|
CLICK_ROUTE[selector](elItem, elItem[KEY], event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
actualStyle = $create('style');
|
||||||
|
|
||||||
|
cm.on('optionChange', onCmOption);
|
||||||
|
msg.onExtension(onRuntimeMessage);
|
||||||
|
if (finder.sections.length) {
|
||||||
|
update(finder.sections, []);
|
||||||
|
}
|
||||||
|
finder.on(update);
|
||||||
|
requestAnimationFrame(updateWidgetStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
enabled = false;
|
||||||
|
cm.off('optionChange', onCmOption);
|
||||||
|
msg.off(onRuntimeMessage);
|
||||||
|
actualStyle.remove();
|
||||||
|
actualStyle = null;
|
||||||
|
cm.operation(() => finder.sections.forEach(killWidget));
|
||||||
|
finder.off(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCmOption(cm, option) {
|
||||||
|
if (option === 'theme') {
|
||||||
|
updateWidgetStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRuntimeMessage(msg) {
|
||||||
|
if (msg.reason === 'editPreview' && !$(`#stylus-${msg.style.id}`)) {
|
||||||
|
// no style element with this id means the style doesn't apply to the editor URL
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.style || msg.styles ||
|
||||||
|
msg.prefs && 'disableAll' in msg.prefs ||
|
||||||
|
msg.method === 'styleDeleted') {
|
||||||
|
requestAnimationFrame(updateWidgetStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidgetStyle() {
|
||||||
|
funcHeight = 0;
|
||||||
|
if (prefs.get('editor.theme') !== 'default' &&
|
||||||
|
!tryCatch(() => $('#cm-theme').sheet.cssRules)) {
|
||||||
|
requestAnimationFrame(updateWidgetStyle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const MIN_LUMA = .05;
|
||||||
|
const MIN_LUMA_DIFF = .4;
|
||||||
|
const color = {
|
||||||
|
wrapper: colorMimicry.get(cm.display.wrapper),
|
||||||
|
gutter: colorMimicry.get(cm.display.gutters, {
|
||||||
|
bg: 'backgroundColor',
|
||||||
|
border: 'borderRightColor',
|
||||||
|
}),
|
||||||
|
line: colorMimicry.get('.CodeMirror-linenumber', null, cm.display.lineDiv),
|
||||||
|
comment: colorMimicry.get('span.cm-comment', null, cm.display.lineDiv),
|
||||||
|
};
|
||||||
|
const hasBorder =
|
||||||
|
color.gutter.style.borderRightWidth !== '0px' &&
|
||||||
|
!/transparent|\b0\)/g.test(color.gutter.style.borderRightColor);
|
||||||
|
const diff = {
|
||||||
|
wrapper: Math.abs(color.gutter.bgLuma - color.wrapper.foreLuma),
|
||||||
|
border: hasBorder ? Math.abs(color.gutter.bgLuma - color.gutter.borderLuma) : 0,
|
||||||
|
line: Math.abs(color.gutter.bgLuma - color.line.foreLuma),
|
||||||
|
};
|
||||||
|
const preferLine = diff.line > diff.wrapper || diff.line > MIN_LUMA_DIFF;
|
||||||
|
const fore = preferLine ? color.line.fore : color.wrapper.fore;
|
||||||
|
|
||||||
|
const border = fore.replace(/[\d.]+(?=\))/, MIN_LUMA_DIFF / 2);
|
||||||
|
const borderStyleForced = `1px ${hasBorder ? color.gutter.style.borderRightStyle : 'solid'} ${border}`;
|
||||||
|
|
||||||
|
actualStyle.textContent = `
|
||||||
|
${C_CONTAINER} {
|
||||||
|
background-color: ${color.gutter.bg};
|
||||||
|
border-top: ${borderStyleForced};
|
||||||
|
border-bottom: ${borderStyleForced};
|
||||||
|
}
|
||||||
|
${C_CONTAINER} ${C_LABEL} {
|
||||||
|
color: ${fore};
|
||||||
|
}
|
||||||
|
${C_CONTAINER} input,
|
||||||
|
${C_CONTAINER} button,
|
||||||
|
${C_CONTAINER} select {
|
||||||
|
background: rgba(255, 255, 255, ${
|
||||||
|
Math.max(MIN_LUMA, Math.pow(Math.max(0, color.gutter.bgLuma - MIN_LUMA * 2), 2)).toFixed(2)
|
||||||
|
});
|
||||||
|
border: ${borderStyleForced};
|
||||||
|
transition: none;
|
||||||
|
color: ${fore};
|
||||||
|
}
|
||||||
|
${C_CONTAINER} .svg-icon.select-arrow {
|
||||||
|
fill: ${fore};
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.documentElement.appendChild(actualStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MozSection[]} added
|
||||||
|
* @param {MozSection[]} removed
|
||||||
|
* @param {number} cutAt
|
||||||
|
*/
|
||||||
|
function update(added, removed, cutAt = finder.sections.indexOf(added[0])) {
|
||||||
|
const isDelayed = added.isDelayed && (cm.startOperation(), true);
|
||||||
|
const toDelay = [];
|
||||||
|
const t0 = performance.now();
|
||||||
|
let {viewFrom, viewTo} = cm.display;
|
||||||
|
for (const sec of added) {
|
||||||
|
const i = removed.findIndex(isReusableWidget, sec);
|
||||||
|
const old = removed[i];
|
||||||
|
if (isDelayed || old || sec.end.line >= viewFrom && sec.start.line < viewTo) {
|
||||||
|
renderWidget(sec, old);
|
||||||
|
viewTo -= (sec.funcs.length || 1) * 1.25;
|
||||||
|
if (old) removed[i] = null;
|
||||||
|
if (performance.now() - t0 > 50) {
|
||||||
|
toDelay.push(...added.slice(added.indexOf(sec) + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toDelay.push(sec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// renumber
|
||||||
|
for (let i = Math.max(0, cutAt), {sections} = finder, sec; (sec = sections[i++]);) {
|
||||||
|
if (!toDelay.includes(sec)) {
|
||||||
|
const data = $(C_LABEL, sec.widget.node).dataset;
|
||||||
|
if (data.index !== `${i}`) data.index = `${i}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toDelay.length) {
|
||||||
|
toDelay.isDelayed = true;
|
||||||
|
setTimeout(update, 0, toDelay, removed);
|
||||||
|
} else {
|
||||||
|
removed.forEach(killWidget);
|
||||||
|
}
|
||||||
|
if (isDelayed) cm.endOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @this {MozSection} */
|
||||||
|
function isReusableWidget(r) {
|
||||||
|
return r &&
|
||||||
|
r.widget &&
|
||||||
|
r.widget.line.parent &&
|
||||||
|
r.start &&
|
||||||
|
!cmpPos(r.start, this.start);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWidget(sec, old) {
|
||||||
|
let widget = old && old.widget;
|
||||||
|
const height = funcHeight * (sec.funcs.length || 1) || undefined;
|
||||||
|
const node = renderContainer(sec, widget);
|
||||||
|
if (widget) {
|
||||||
|
widget.node = node;
|
||||||
|
if (height && height !== widget.height) {
|
||||||
|
widget.height = height;
|
||||||
|
widget.changed();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widget = cm.addLineWidget(sec.start.line, node, {
|
||||||
|
coverGutter: true,
|
||||||
|
noHScroll: true,
|
||||||
|
above: true,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!funcHeight) {
|
||||||
|
funcHeight = node.offsetHeight / (sec.funcs.length || 1);
|
||||||
|
}
|
||||||
|
setProp(sec, 'widget', widget);
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MozSection} sec
|
||||||
|
* @param {LineWidget} oldWidget
|
||||||
|
* @returns {Node}
|
||||||
|
*/
|
||||||
|
function renderContainer(sec, oldWidget) {
|
||||||
|
const container = oldWidget ? oldWidget.node : TPL.container.cloneNode(true);
|
||||||
|
const elList = $(C_LIST, container);
|
||||||
|
const {funcs} = sec;
|
||||||
|
const oldItems = elList[KEY] || false;
|
||||||
|
const items = funcs.map((f, i) => renderFunc(f, oldItems[i]));
|
||||||
|
let slot = elList.firstChild;
|
||||||
|
for (const {item} of items) {
|
||||||
|
const el = item[KEY];
|
||||||
|
if (el !== slot) {
|
||||||
|
elList.insertBefore(el, slot);
|
||||||
|
if (slot) slot.remove();
|
||||||
|
slot = el;
|
||||||
|
}
|
||||||
|
slot = slot.nextSibling;
|
||||||
|
}
|
||||||
|
for (let i = funcs.length; oldItems && i < oldItems.length; i++) {
|
||||||
|
killFunc(oldItems[i]);
|
||||||
|
if (slot) {
|
||||||
|
const el = slot.nextSibling;
|
||||||
|
slot.remove();
|
||||||
|
slot = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!funcs.length && (!oldItems || oldItems.length)) {
|
||||||
|
TPL.appliesToEverything.cloneNode(true);
|
||||||
|
}
|
||||||
|
setProp(sec, 'widgetFuncs', items);
|
||||||
|
elList[KEY] = items;
|
||||||
|
container[KEY] = sec;
|
||||||
|
container.classList.toggle('error', !sec.funcs.length);
|
||||||
|
return Object.assign(container, EVENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MozSectionFunc} func
|
||||||
|
* @param {MarkedFunc} old
|
||||||
|
* @returns {MarkedFunc}
|
||||||
|
*/
|
||||||
|
function renderFunc(func, old = {}) {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
isQuoted = false,
|
||||||
|
start,
|
||||||
|
start: {line},
|
||||||
|
typeEnd = {line, ch: start.ch + type.length},
|
||||||
|
valuePos = {line, ch: typeEnd.ch + 1 + Boolean(isQuoted)},
|
||||||
|
valueEnd = {line, ch: valuePos.ch + value.length},
|
||||||
|
end = {line, ch: valueEnd.ch + Boolean(isQuoted) + 1},
|
||||||
|
} = func;
|
||||||
|
const el = (old.item || {})[KEY] || TPL.listItem.cloneNode(true);
|
||||||
|
/** @namespace MarkedFunc */
|
||||||
|
const res = el[KEY] = {
|
||||||
|
typeText: type,
|
||||||
|
item: markFuncPart(start, end, old.item, el),
|
||||||
|
type: markFuncPart(start, typeEnd, old.type, $(C_TYPE, el), type, toLowerCase),
|
||||||
|
value: markFuncPart(valuePos, valueEnd, old.value, $(C_VALUE, el), value, fromDoubleslash),
|
||||||
|
};
|
||||||
|
if (el.dataset.type !== type) {
|
||||||
|
el.dataset.type = type;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CodeMirror.Pos} start
|
||||||
|
* @param {CodeMirror.Pos} end
|
||||||
|
* @param {TextMarker} marker
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
* @param {string} [text]
|
||||||
|
* @param {function} [textTransform]
|
||||||
|
* @returns {TextMarker}
|
||||||
|
*/
|
||||||
|
function markFuncPart(start, end, marker, el, text, textTransform) {
|
||||||
|
if (marker) {
|
||||||
|
const pos = marker.find();
|
||||||
|
if (!pos ||
|
||||||
|
cmpPos(pos.from, start) ||
|
||||||
|
cmpPos(pos.to, end) ||
|
||||||
|
text != null && text !== cm.getRange(start, end)) {
|
||||||
|
marker.clear();
|
||||||
|
marker = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!marker) {
|
||||||
|
marker = cm.markText(start, end, {
|
||||||
|
clearWhenEmpty: false,
|
||||||
|
inclusiveLeft: true,
|
||||||
|
inclusiveRight: true,
|
||||||
|
[KEY]: el,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (text != null) {
|
||||||
|
text = textTransform(text);
|
||||||
|
if (el.value !== text) el.value = text;
|
||||||
|
}
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {MozSection} sec */
|
||||||
|
function killWidget(sec) {
|
||||||
|
const w = sec && sec.widget;
|
||||||
|
if (w) {
|
||||||
|
w.clear();
|
||||||
|
w.node[KEY].widgetFuncs.forEach(killFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {MarkedFunc} f */
|
||||||
|
function killFunc(f) {
|
||||||
|
f.item.clear();
|
||||||
|
f.type.clear();
|
||||||
|
f.value.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRegExpTester(el) {
|
||||||
|
const reFuncs = getFuncsFor(el).filter(f => f.typeText === 'regexp');
|
||||||
|
regExpTester.toggle(true);
|
||||||
|
regExpTester.update(reFuncs.map(f => fromDoubleslash(f.value[KEY].value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromDoubleslash(s) {
|
||||||
|
return /([^\\]|^)\\([^\\]|$)/.test(s) ? s : s.replace(/\\\\/g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDoubleslash(s) {
|
||||||
|
return fromDoubleslash(s).replace(/\\/g, '\\\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLowerCase(s) {
|
||||||
|
return s.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds a non-enumerable property so it won't be seen by deepEqual */
|
||||||
|
function setProp(obj, name, value) {
|
||||||
|
return Object.defineProperty(obj, name, {value, configurable: true});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,112 +1,43 @@
|
||||||
/* global template cmFactory $ propertyToCss CssToProperty linter regExpTester
|
/* global
|
||||||
FIREFOX toggleContextMenuDelete initBeautifyButton showHelp t tryRegExp */
|
$
|
||||||
/* exported createSection */
|
cmFactory
|
||||||
|
debounce
|
||||||
|
DocFuncMapper
|
||||||
|
editor
|
||||||
|
initBeautifyButton
|
||||||
|
linter
|
||||||
|
prefs
|
||||||
|
regExpTester
|
||||||
|
t
|
||||||
|
template
|
||||||
|
trimCommentLabel
|
||||||
|
tryRegExp
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createResizeGrip(cm) {
|
/* exported createSection */
|
||||||
const wrapper = cm.display.wrapper;
|
|
||||||
wrapper.classList.add('resize-grip-enabled');
|
|
||||||
const resizeGrip = template.resizeGrip.cloneNode(true);
|
|
||||||
wrapper.appendChild(resizeGrip);
|
|
||||||
let lastClickTime = 0;
|
|
||||||
let initHeight;
|
|
||||||
let initY;
|
|
||||||
resizeGrip.onmousedown = event => {
|
|
||||||
initHeight = wrapper.offsetHeight;
|
|
||||||
initY = event.pageY;
|
|
||||||
if (event.button !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
if (Date.now() - lastClickTime < 500) {
|
|
||||||
lastClickTime = 0;
|
|
||||||
toggleSectionHeight(cm);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastClickTime = Date.now();
|
|
||||||
const minHeight = cm.defaultTextHeight() +
|
|
||||||
/* .CodeMirror-lines padding */
|
|
||||||
cm.display.lineDiv.offsetParent.offsetTop +
|
|
||||||
/* borders */
|
|
||||||
wrapper.offsetHeight - wrapper.clientHeight;
|
|
||||||
wrapper.style.pointerEvents = 'none';
|
|
||||||
document.body.style.cursor = 's-resize';
|
|
||||||
document.addEventListener('mousemove', resize);
|
|
||||||
document.addEventListener('mouseup', resizeStop);
|
|
||||||
|
|
||||||
function resize(e) {
|
/** @returns {EditorSection} */
|
||||||
const height = Math.max(minHeight, initHeight + e.pageY - initY);
|
function createSection(originalSection, genId) {
|
||||||
if (height !== wrapper.offsetHeight) {
|
const {dirty} = editor;
|
||||||
cm.setSize(null, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeStop() {
|
|
||||||
document.removeEventListener('mouseup', resizeStop);
|
|
||||||
document.removeEventListener('mousemove', resize);
|
|
||||||
wrapper.style.pointerEvents = '';
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function toggleSectionHeight(cm) {
|
|
||||||
if (cm.state.toggleHeightSaved) {
|
|
||||||
// restore previous size
|
|
||||||
cm.setSize(null, cm.state.toggleHeightSaved);
|
|
||||||
cm.state.toggleHeightSaved = 0;
|
|
||||||
} else {
|
|
||||||
// maximize
|
|
||||||
const wrapper = cm.display.wrapper;
|
|
||||||
const allBounds = $('#sections').getBoundingClientRect();
|
|
||||||
const pageExtrasHeight = allBounds.top + window.scrollY +
|
|
||||||
parseFloat(getComputedStyle($('#sections')).paddingBottom);
|
|
||||||
const sectionEl = wrapper.parentNode;
|
|
||||||
const sectionExtrasHeight = sectionEl.clientHeight - wrapper.offsetHeight;
|
|
||||||
cm.state.toggleHeightSaved = wrapper.clientHeight;
|
|
||||||
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
|
|
||||||
const bounds = sectionEl.getBoundingClientRect();
|
|
||||||
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
|
|
||||||
window.scrollBy(0, bounds.top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSection({
|
|
||||||
// data model
|
|
||||||
originalSection,
|
|
||||||
dirty,
|
|
||||||
// util
|
|
||||||
nextEditor,
|
|
||||||
prevEditor,
|
|
||||||
genId,
|
|
||||||
// emit events
|
|
||||||
// TODO: better names like `onRemoved`? Or make a real event emitter.
|
|
||||||
showMozillaFormatImport,
|
|
||||||
removeSection,
|
|
||||||
insertSectionAfter,
|
|
||||||
moveSectionUp,
|
|
||||||
moveSectionDown,
|
|
||||||
restoreSection,
|
|
||||||
}) {
|
|
||||||
const sectionId = genId();
|
const sectionId = genId();
|
||||||
const el = template.section.cloneNode(true);
|
const el = template.section.cloneNode(true);
|
||||||
|
const elLabel = $('.code-label', el);
|
||||||
const cm = cmFactory.create(wrapper => {
|
const cm = cmFactory.create(wrapper => {
|
||||||
el.insertBefore(wrapper, $('.code-label', el).nextSibling);
|
// making it tall during initial load so IntersectionObserver sees only one adjacent CM
|
||||||
}, {value: originalSection.code});
|
wrapper.style.height = '100vh';
|
||||||
|
elLabel.after(wrapper);
|
||||||
|
}, {
|
||||||
|
value: originalSection.code,
|
||||||
|
});
|
||||||
el.CodeMirror = cm; // used by getAssociatedEditor
|
el.CodeMirror = cm; // used by getAssociatedEditor
|
||||||
|
|
||||||
const changeListeners = new Set();
|
const changeListeners = new Set();
|
||||||
|
|
||||||
const appliesToContainer = $('.applies-to-list', el);
|
const appliesToContainer = $('.applies-to-list', el);
|
||||||
const appliesTo = [];
|
const appliesTo = [];
|
||||||
for (const [key, fnName] of Object.entries(propertyToCss)) {
|
DocFuncMapper.forEachProp(originalSection, (type, value) =>
|
||||||
if (originalSection[key]) {
|
insertApplyAfter({type, value}));
|
||||||
originalSection[key].forEach(value =>
|
|
||||||
insertApplyAfter({type: fnName, value})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!appliesTo.length) {
|
if (!appliesTo.length) {
|
||||||
insertApplyAfter({all: true});
|
insertApplyAfter({all: true});
|
||||||
}
|
}
|
||||||
|
@ -118,89 +49,74 @@ function createSection({
|
||||||
updateRegexpTester();
|
updateRegexpTester();
|
||||||
createResizeGrip(cm);
|
createResizeGrip(cm);
|
||||||
|
|
||||||
linter.enableForEditor(cm);
|
/** @namespace EditorSection */
|
||||||
|
|
||||||
const section = {
|
const section = {
|
||||||
id: sectionId,
|
id: sectionId,
|
||||||
el,
|
el,
|
||||||
cm,
|
cm,
|
||||||
render,
|
appliesTo,
|
||||||
getModel,
|
getModel() {
|
||||||
remove,
|
const items = appliesTo.map(a => !a.all && [a.type, a.value]);
|
||||||
destroy,
|
return DocFuncMapper.toSection(items, {code: cm.getValue()});
|
||||||
restore,
|
},
|
||||||
isRemoved: () => removed,
|
remove() {
|
||||||
onChange,
|
linter.disableForEditor(cm);
|
||||||
off,
|
el.classList.add('removed');
|
||||||
appliesTo
|
removed = true;
|
||||||
|
appliesTo.forEach(a => a.remove());
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
cm.refresh();
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
cmFactory.destroy(cm);
|
||||||
|
},
|
||||||
|
restore() {
|
||||||
|
linter.enableForEditor(cm);
|
||||||
|
el.classList.remove('removed');
|
||||||
|
removed = false;
|
||||||
|
appliesTo.forEach(a => a.restore());
|
||||||
|
cm.refresh();
|
||||||
|
},
|
||||||
|
onChange(fn) {
|
||||||
|
changeListeners.add(fn);
|
||||||
|
},
|
||||||
|
off(fn) {
|
||||||
|
changeListeners.delete(fn);
|
||||||
|
},
|
||||||
|
get removed() {
|
||||||
|
return removed;
|
||||||
|
},
|
||||||
|
tocEntry: {
|
||||||
|
label: '',
|
||||||
|
get removed() {
|
||||||
|
return removed;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
prefs.subscribe('editor.toc.expanded', updateTocPrefToggled, {now: true});
|
||||||
|
|
||||||
return section;
|
return section;
|
||||||
|
|
||||||
function onChange(fn) {
|
function emitSectionChange(origin) {
|
||||||
changeListeners.add(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function off(fn) {
|
|
||||||
changeListeners.delete(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitSectionChange() {
|
|
||||||
for (const fn of changeListeners) {
|
for (const fn of changeListeners) {
|
||||||
fn();
|
fn(origin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModel() {
|
|
||||||
const section = {
|
|
||||||
code: cm.getValue()
|
|
||||||
};
|
|
||||||
for (const apply of appliesTo) {
|
|
||||||
if (apply.all) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const key = CssToProperty[apply.getType()];
|
|
||||||
if (!section[key]) {
|
|
||||||
section[key] = [];
|
|
||||||
}
|
|
||||||
section[key].push(apply.getValue());
|
|
||||||
}
|
|
||||||
return section;
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerEvents() {
|
function registerEvents() {
|
||||||
cm.on('changes', () => {
|
cm.on('changes', () => {
|
||||||
const newGeneration = cm.changeGeneration();
|
const newGeneration = cm.changeGeneration();
|
||||||
dirty.modify(`section.${sectionId}.code`, changeGeneration, newGeneration);
|
dirty.modify(`section.${sectionId}.code`, changeGeneration, newGeneration);
|
||||||
changeGeneration = newGeneration;
|
changeGeneration = newGeneration;
|
||||||
emitSectionChange();
|
emitSectionChange('code');
|
||||||
});
|
});
|
||||||
cm.on('paste', (cm, event) => {
|
cm.display.wrapper.on('keydown', event => handleKeydown(cm, event), true);
|
||||||
const text = event.clipboardData.getData('text') || '';
|
$('.test-regexp', el).onclick = () => {
|
||||||
if (/@-moz-document/i.test(text) &&
|
|
||||||
/@-moz-document\s+(url|url-prefix|domain|regexp)\(/i
|
|
||||||
.test(text.replace(/\/\*([^*]|\*(?!\/))*(\*\/|$)/g, ''))
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
showMozillaFormatImport(text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!FIREFOX) {
|
|
||||||
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
|
||||||
}
|
|
||||||
cm.display.wrapper.addEventListener('keydown', event =>
|
|
||||||
handleKeydown(cm, event), true);
|
|
||||||
|
|
||||||
$('.applies-to-help', el).addEventListener('click', showAppliesToHelp);
|
|
||||||
$('.remove-section', el).addEventListener('click', () => removeSection(section));
|
|
||||||
$('.add-section', el).addEventListener('click', () => insertSectionAfter(undefined, section));
|
|
||||||
$('.clone-section', el).addEventListener('click', () => insertSectionAfter(getModel(), section));
|
|
||||||
$('.move-section-up', el).addEventListener('click', () => moveSectionUp(section));
|
|
||||||
$('.move-section-down', el).addEventListener('click', () => moveSectionDown(section));
|
|
||||||
$('.restore-section', el).addEventListener('click', () => restoreSection(section));
|
|
||||||
$('.test-regexp', el).addEventListener('click', () => {
|
|
||||||
regExpTester.toggle();
|
regExpTester.toggle();
|
||||||
updateRegexpTester();
|
updateRegexpTester();
|
||||||
});
|
};
|
||||||
initBeautifyButton($('.beautify-section', el), () => [cm]);
|
initBeautifyButton($('.beautify-section', el), () => [cm]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,7 +133,7 @@ function createSection({
|
||||||
}
|
}
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
cm = line === 0 && prevEditor(cm, false);
|
cm = line === 0 && editor.prevEditor(cm, false);
|
||||||
if (!cm) {
|
if (!cm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -231,7 +147,7 @@ function createSection({
|
||||||
}
|
}
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
cm = line === cm.doc.size - 1 && nextEditor(cm, false);
|
cm = line === cm.doc.size - 1 && editor.nextEditor(cm, false);
|
||||||
if (!cm) {
|
if (!cm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -242,37 +158,9 @@ function createSection({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAppliesToHelp(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
showHelp(t('appliesLabel'), t('appliesHelp'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove() {
|
|
||||||
linter.disableForEditor(cm);
|
|
||||||
el.classList.add('removed');
|
|
||||||
removed = true;
|
|
||||||
appliesTo.forEach(a => a.remove());
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroy() {
|
|
||||||
cmFactory.destroy(cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function restore() {
|
|
||||||
linter.enableForEditor(cm);
|
|
||||||
el.classList.remove('removed');
|
|
||||||
removed = false;
|
|
||||||
appliesTo.forEach(a => a.restore());
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
cm.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRegexpTester() {
|
function updateRegexpTester() {
|
||||||
const regexps = appliesTo.filter(a => a.getType() === 'regexp')
|
const regexps = appliesTo.filter(a => a.type === 'regexp')
|
||||||
.map(a => a.getValue());
|
.map(a => a.value);
|
||||||
if (regexps.length) {
|
if (regexps.length) {
|
||||||
el.classList.add('has-regexp');
|
el.classList.add('has-regexp');
|
||||||
regExpTester.update(regexps);
|
regExpTester.update(regexps);
|
||||||
|
@ -282,6 +170,68 @@ function createSection({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTocEntry(origin) {
|
||||||
|
const te = section.tocEntry;
|
||||||
|
let changed;
|
||||||
|
if (origin === 'code' || !origin) {
|
||||||
|
const label = getLabelFromComment();
|
||||||
|
if (te.label !== label) {
|
||||||
|
te.label = elLabel.dataset.text = label;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!te.label) {
|
||||||
|
const target = appliesTo[0].all ? null : appliesTo[0].value;
|
||||||
|
if (te.target !== target) {
|
||||||
|
te.target = target;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (te.numTargets !== appliesTo.length) {
|
||||||
|
te.numTargets = appliesTo.length;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) editor.updateToc([section]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTocEntryLazy(...args) {
|
||||||
|
debounce(updateTocEntry, 0, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTocFocus() {
|
||||||
|
editor.updateToc({focus: true, 0: section});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTocPrefToggled(key, val) {
|
||||||
|
changeListeners[val ? 'add' : 'delete'](updateTocEntryLazy);
|
||||||
|
el.onOff(val, 'focusin', updateTocFocus);
|
||||||
|
if (val) {
|
||||||
|
updateTocEntry();
|
||||||
|
if (el.contains(document.activeElement)) {
|
||||||
|
updateTocFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelFromComment() {
|
||||||
|
let cmt = '';
|
||||||
|
let inCmt;
|
||||||
|
cm.eachLine(({text}) => {
|
||||||
|
let i = 0;
|
||||||
|
if (!inCmt) {
|
||||||
|
i = text.search(/\S/);
|
||||||
|
if (i < 0) return;
|
||||||
|
inCmt = text[i] === '/' && text[i + 1] === '*';
|
||||||
|
if (!inCmt) return true;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
const j = text.indexOf('*/', i);
|
||||||
|
cmt = trimCommentLabel(text.slice(i, j >= 0 ? j : text.length));
|
||||||
|
return j >= 0 || cmt;
|
||||||
|
});
|
||||||
|
return cmt;
|
||||||
|
}
|
||||||
|
|
||||||
function insertApplyAfter(init, base) {
|
function insertApplyAfter(init, base) {
|
||||||
const apply = createApply(init);
|
const apply = createApply(init);
|
||||||
appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
|
appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
|
||||||
|
@ -290,7 +240,7 @@ function createSection({
|
||||||
if (appliesTo.length > 1 && appliesTo[0].all) {
|
if (appliesTo.length > 1 && appliesTo[0].all) {
|
||||||
removeApply(appliesTo[0]);
|
removeApply(appliesTo[0]);
|
||||||
}
|
}
|
||||||
emitSectionChange();
|
emitSectionChange('apply');
|
||||||
return apply;
|
return apply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,7 +253,7 @@ function createSection({
|
||||||
if (!appliesTo.length) {
|
if (!appliesTo.length) {
|
||||||
insertApplyAfter({all: true});
|
insertApplyAfter({all: true});
|
||||||
}
|
}
|
||||||
emitSectionChange();
|
emitSectionChange('apply');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createApply({type = 'url', value, all = false}) {
|
function createApply({type = 'url', value, all = false}) {
|
||||||
|
@ -315,14 +265,14 @@ function createSection({
|
||||||
const selectEl = !all && $('.applies-type', el);
|
const selectEl = !all && $('.applies-type', el);
|
||||||
if (selectEl) {
|
if (selectEl) {
|
||||||
selectEl.value = type;
|
selectEl.value = type;
|
||||||
selectEl.addEventListener('change', () => {
|
selectEl.on('change', () => {
|
||||||
const oldType = type;
|
const oldType = type;
|
||||||
dirty.modify(`${dirtyPrefix}.type`, type, selectEl.value);
|
dirty.modify(`${dirtyPrefix}.type`, type, selectEl.value);
|
||||||
type = selectEl.value;
|
type = selectEl.value;
|
||||||
if (oldType === 'regexp' || type === 'regexp') {
|
if (oldType === 'regexp' || type === 'regexp') {
|
||||||
updateRegexpTester();
|
updateRegexpTester();
|
||||||
}
|
}
|
||||||
emitSectionChange();
|
emitSectionChange('apply');
|
||||||
validate();
|
validate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -330,15 +280,15 @@ function createSection({
|
||||||
const valueEl = !all && $('.applies-value', el);
|
const valueEl = !all && $('.applies-value', el);
|
||||||
if (valueEl) {
|
if (valueEl) {
|
||||||
valueEl.value = value;
|
valueEl.value = value;
|
||||||
valueEl.addEventListener('input', () => {
|
valueEl.on('input', () => {
|
||||||
dirty.modify(`${dirtyPrefix}.value`, value, valueEl.value);
|
dirty.modify(`${dirtyPrefix}.value`, value, valueEl.value);
|
||||||
value = valueEl.value;
|
value = valueEl.value;
|
||||||
if (type === 'regexp') {
|
if (type === 'regexp') {
|
||||||
updateRegexpTester();
|
updateRegexpTester();
|
||||||
}
|
}
|
||||||
emitSectionChange();
|
emitSectionChange('apply');
|
||||||
});
|
});
|
||||||
valueEl.addEventListener('change', validate);
|
valueEl.on('change', validate);
|
||||||
}
|
}
|
||||||
|
|
||||||
restore();
|
restore();
|
||||||
|
@ -349,19 +299,23 @@ function createSection({
|
||||||
remove,
|
remove,
|
||||||
restore,
|
restore,
|
||||||
el,
|
el,
|
||||||
getType: () => type,
|
valueEl, // used by validator
|
||||||
getValue: () => value,
|
get type() {
|
||||||
valueEl // used by validator
|
return type;
|
||||||
|
},
|
||||||
|
get value() {
|
||||||
|
return value;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeButton = $('.remove-applies-to', el);
|
const removeButton = $('.remove-applies-to', el);
|
||||||
if (removeButton) {
|
if (removeButton) {
|
||||||
removeButton.addEventListener('click', e => {
|
removeButton.on('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
removeApply(apply);
|
removeApply(apply);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$('.add-applies-to', el).addEventListener('click', e => {
|
$('.add-applies-to', el).on('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newApply = insertApplyAfter({type, value: ''}, apply);
|
const newApply = insertApplyAfter({type, value: ''}, apply);
|
||||||
$('input', newApply.el).focus();
|
$('input', newApply.el).focus();
|
||||||
|
@ -395,3 +349,72 @@ function createSection({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createResizeGrip(cm) {
|
||||||
|
const wrapper = cm.display.wrapper;
|
||||||
|
wrapper.classList.add('resize-grip-enabled');
|
||||||
|
const resizeGrip = template.resizeGrip.cloneNode(true);
|
||||||
|
wrapper.appendChild(resizeGrip);
|
||||||
|
let lastClickTime = 0;
|
||||||
|
let initHeight;
|
||||||
|
let initY;
|
||||||
|
resizeGrip.onmousedown = event => {
|
||||||
|
initHeight = wrapper.offsetHeight;
|
||||||
|
initY = event.pageY;
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
if (Date.now() - lastClickTime < 500) {
|
||||||
|
lastClickTime = 0;
|
||||||
|
toggleSectionHeight(cm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastClickTime = Date.now();
|
||||||
|
const minHeight = cm.defaultTextHeight() +
|
||||||
|
/* .CodeMirror-lines padding */
|
||||||
|
cm.display.lineDiv.offsetParent.offsetTop +
|
||||||
|
/* borders */
|
||||||
|
wrapper.offsetHeight - wrapper.clientHeight;
|
||||||
|
wrapper.style.pointerEvents = 'none';
|
||||||
|
document.body.style.cursor = 's-resize';
|
||||||
|
document.on('mousemove', resize);
|
||||||
|
document.on('mouseup', resizeStop);
|
||||||
|
|
||||||
|
function resize(e) {
|
||||||
|
const height = Math.max(minHeight, initHeight + e.pageY - initY);
|
||||||
|
if (height !== wrapper.offsetHeight) {
|
||||||
|
cm.setSize(null, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeStop() {
|
||||||
|
document.off('mouseup', resizeStop);
|
||||||
|
document.off('mousemove', resize);
|
||||||
|
wrapper.style.pointerEvents = '';
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleSectionHeight(cm) {
|
||||||
|
if (cm.state.toggleHeightSaved) {
|
||||||
|
// restore previous size
|
||||||
|
cm.setSize(null, cm.state.toggleHeightSaved);
|
||||||
|
cm.state.toggleHeightSaved = 0;
|
||||||
|
} else {
|
||||||
|
// maximize
|
||||||
|
const wrapper = cm.display.wrapper;
|
||||||
|
const allBounds = $('#sections').getBoundingClientRect();
|
||||||
|
const pageExtrasHeight = allBounds.top + window.scrollY +
|
||||||
|
parseFloat(getComputedStyle($('#sections')).paddingBottom);
|
||||||
|
const sectionEl = wrapper.parentNode;
|
||||||
|
const sectionExtrasHeight = sectionEl.clientHeight - wrapper.offsetHeight;
|
||||||
|
cm.state.toggleHeightSaved = wrapper.clientHeight;
|
||||||
|
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
|
||||||
|
const bounds = sectionEl.getBoundingClientRect();
|
||||||
|
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
|
||||||
|
window.scrollBy(0, bounds.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,75 +1,145 @@
|
||||||
/* global showHelp toggleContextMenuDelete createSection
|
/* global
|
||||||
CodeMirror linter createLivePreview showCodeMirrorPopup
|
$
|
||||||
sectionsToMozFormat messageBox clipString
|
$$
|
||||||
$ $$ $create t FIREFOX API
|
$create
|
||||||
debounce */
|
API
|
||||||
/* exported createSectionsEditor */
|
clipString
|
||||||
|
CodeMirror
|
||||||
|
createLivePreview
|
||||||
|
createSection
|
||||||
|
debounce
|
||||||
|
editor
|
||||||
|
FIREFOX
|
||||||
|
ignoreChromeError
|
||||||
|
linter
|
||||||
|
messageBox
|
||||||
|
prefs
|
||||||
|
sectionsToMozFormat
|
||||||
|
showCodeMirrorPopup
|
||||||
|
showHelp
|
||||||
|
t
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createSectionsEditor(editorBase) {
|
/* exported SectionsEditor */
|
||||||
const {style, dirty} = editorBase;
|
|
||||||
|
function SectionsEditor() {
|
||||||
|
const {style, dirty} = editor;
|
||||||
|
const container = $('#sections');
|
||||||
|
/** @type {EditorSection[]} */
|
||||||
|
const sections = [];
|
||||||
|
const xo = window.IntersectionObserver &&
|
||||||
|
new IntersectionObserver(refreshOnViewListener, {rootMargin: '100%'});
|
||||||
|
const livePreview = createLivePreview(null, style.id);
|
||||||
|
|
||||||
let INC_ID = 0; // an increment id that is used by various object to track the order
|
let INC_ID = 0; // an increment id that is used by various object to track the order
|
||||||
|
let sectionOrder = '';
|
||||||
const container = $('#sections');
|
let headerOffset; // in compact mode the header is at the top so it reduces the available height
|
||||||
const sections = [];
|
|
||||||
|
|
||||||
container.classList.add('section-editor');
|
container.classList.add('section-editor');
|
||||||
updateHeader();
|
updateHeader();
|
||||||
$('#to-mozilla').addEventListener('click', showMozillaFormat);
|
$('#to-mozilla').on('click', showMozillaFormat);
|
||||||
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
|
$('#to-mozilla-help').on('click', showToMozillaHelp);
|
||||||
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
|
$('#from-mozilla').on('click', () => showMozillaFormatImport());
|
||||||
|
document.on('wheel', scrollEntirePageOnCtrlShift, {passive: false});
|
||||||
document.addEventListener('wheel', scrollEntirePageOnCtrlShift, {passive: false});
|
|
||||||
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
|
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
|
||||||
|
|
||||||
if (!FIREFOX) {
|
if (!FIREFOX) {
|
||||||
$$([
|
$$('input:not([type]), input[type=text], input[type=search], input[type=number]')
|
||||||
'input:not([type])',
|
.forEach(e => e.on('mousedown', toggleContextMenuDelete));
|
||||||
'input[type="text"]',
|
|
||||||
'input[type="search"]',
|
|
||||||
'input[type="number"]',
|
|
||||||
].join(','))
|
|
||||||
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const xo = window.IntersectionObserver && new IntersectionObserver(entries => {
|
/** @namespace SectionsEditor */
|
||||||
for (const {isIntersecting, target} of entries) {
|
Object.assign(editor, {
|
||||||
if (isIntersecting) {
|
|
||||||
target.CodeMirror.refresh();
|
|
||||||
xo.unobserve(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {rootMargin: '100%'});
|
|
||||||
const refreshOnView = (cm, force) =>
|
|
||||||
force || !xo ?
|
|
||||||
cm.refresh() :
|
|
||||||
xo.observe(cm.display.wrapper);
|
|
||||||
|
|
||||||
let sectionOrder = '';
|
sections,
|
||||||
let headerOffset; // in compact mode the header is at the top so it reduces the available height
|
|
||||||
const ready = initSections(style.sections, {pristine: true});
|
|
||||||
|
|
||||||
const livePreview = createLivePreview();
|
closestVisible,
|
||||||
livePreview.show(Boolean(style.id));
|
updateLivePreview,
|
||||||
|
|
||||||
return Object.assign({}, editorBase, {
|
getEditors() {
|
||||||
ready,
|
return sections.filter(s => !s.removed).map(s => s.cm);
|
||||||
replaceStyle,
|
},
|
||||||
getEditors,
|
|
||||||
scrollToEditor,
|
getEditorTitle(cm) {
|
||||||
getEditorTitle: cm => {
|
const index = editor.getEditors().indexOf(cm);
|
||||||
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm);
|
|
||||||
return `${t('sectionCode')} ${index + 1}`;
|
return `${t('sectionCode')} ${index + 1}`;
|
||||||
},
|
},
|
||||||
save,
|
|
||||||
nextEditor,
|
getSearchableInputs(cm) {
|
||||||
prevEditor,
|
return sections.find(s => s.cm === cm).appliesTo.map(a => a.valueEl).filter(Boolean);
|
||||||
closestVisible,
|
},
|
||||||
getSearchableInputs,
|
|
||||||
updateLivePreview,
|
jumpToEditor(i) {
|
||||||
|
const {cm} = sections[i] || {};
|
||||||
|
if (cm) {
|
||||||
|
editor.scrollToEditor(cm);
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
nextEditor(cm, cycle = true) {
|
||||||
|
return cycle || cm !== findLast(sections, s => !s.removed).cm
|
||||||
|
? nextPrevEditor(cm, 1)
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
|
||||||
|
prevEditor(cm, cycle = true) {
|
||||||
|
return cycle || cm !== sections.find(s => !s.removed).cm
|
||||||
|
? nextPrevEditor(cm, -1)
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async replaceStyle(newStyle, codeIsUpdated) {
|
||||||
|
dirty.clear('name');
|
||||||
|
// FIXME: avoid recreating all editors?
|
||||||
|
if (codeIsUpdated !== false) {
|
||||||
|
await initSections(newStyle.sections, {replace: true, pristine: true});
|
||||||
|
}
|
||||||
|
Object.assign(style, newStyle);
|
||||||
|
updateHeader();
|
||||||
|
dirty.clear();
|
||||||
|
// Go from new style URL to edit style URL
|
||||||
|
if (location.href.indexOf('id=') === -1 && style.id) {
|
||||||
|
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
|
||||||
|
$('#heading').textContent = t('editStyleHeading');
|
||||||
|
}
|
||||||
|
livePreview.show(Boolean(style.id));
|
||||||
|
updateLivePreview();
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
if (!dirty.isDirty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let newStyle = getModel();
|
||||||
|
if (!validate(newStyle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newStyle = await API.editSave(newStyle);
|
||||||
|
destroyRemovedSections();
|
||||||
|
sessionStorage.justEditedStyleId = newStyle.id;
|
||||||
|
editor.replaceStyle(newStyle, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollToEditor(cm) {
|
||||||
|
const section = sections.find(s => s.cm === cm).el;
|
||||||
|
const bounds = section.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
(bounds.bottom > window.innerHeight && bounds.top > 0) ||
|
||||||
|
(bounds.top < 0 && bounds.bottom < window.innerHeight)
|
||||||
|
) {
|
||||||
|
if (bounds.top < 0) {
|
||||||
|
window.scrollBy(0, bounds.top - 1);
|
||||||
|
} else {
|
||||||
|
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
editor.ready = initSections(style.sections, {pristine: true});
|
||||||
|
|
||||||
|
/** @param {EditorSection} section */
|
||||||
function fitToContent(section) {
|
function fitToContent(section) {
|
||||||
const {el, cm, cm: {display: {wrapper, sizer}}} = section;
|
const {el, cm, cm: {display: {wrapper, sizer}}} = section;
|
||||||
if (cm.display.renderedView) {
|
if (cm.display.renderedView) {
|
||||||
|
@ -90,18 +160,19 @@ function createSectionsEditor(editorBase) {
|
||||||
cm.off('update', resize);
|
cm.off('update', resize);
|
||||||
const cmHeight = wrapper.offsetHeight;
|
const cmHeight = wrapper.offsetHeight;
|
||||||
const maxHeight = (window.innerHeight - headerOffset) - (section.el.offsetHeight - cmHeight);
|
const maxHeight = (window.innerHeight - headerOffset) - (section.el.offsetHeight - cmHeight);
|
||||||
cm.setSize(null, Math.min(contentHeight, maxHeight));
|
const fit = Math.min(contentHeight, maxHeight);
|
||||||
|
if (Math.abs(fit - cmHeight) > 1) {
|
||||||
|
cm.setSize(null, fit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitToAvailableSpace() {
|
function fitToAvailableSpace() {
|
||||||
const ch = container.offsetHeight;
|
const lastSectionBottom = sections[sections.length - 1].el.getBoundingClientRect().bottom;
|
||||||
let available = ch - sections[sections.length - 1].el.getBoundingClientRect().bottom + headerOffset;
|
const delta = Math.floor((window.innerHeight - lastSectionBottom) / sections.length);
|
||||||
if (available <= 1) available = window.innerHeight - ch - headerOffset;
|
|
||||||
const delta = Math.floor(available / sections.length);
|
|
||||||
if (delta > 1) {
|
if (delta > 1) {
|
||||||
sections.forEach(({cm}) => {
|
sections.forEach(({cm}) => {
|
||||||
cm.setSize(null, cm.display.wrapper.offsetHeight + delta);
|
cm.setSize(null, cm.display.lastWrapHeight + delta);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,14 +200,12 @@ function createSectionsEditor(editorBase) {
|
||||||
showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
|
showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSearchableInputs(cm) {
|
/**
|
||||||
return sections.find(s => s.cm === cm).appliesTo.map(a => a.valueEl).filter(Boolean);
|
priority:
|
||||||
}
|
1. associated CM for applies-to element
|
||||||
|
2. last active if visible
|
||||||
// priority:
|
3. first visible
|
||||||
// 1. associated CM for applies-to element
|
*/
|
||||||
// 2. last active if visible
|
|
||||||
// 3. first visible
|
|
||||||
function closestVisible(nearbyElement) {
|
function closestVisible(nearbyElement) {
|
||||||
const cm =
|
const cm =
|
||||||
nearbyElement instanceof CodeMirror ? nearbyElement :
|
nearbyElement instanceof CodeMirror ? nearbyElement :
|
||||||
|
@ -181,7 +250,7 @@ function createSectionsEditor(editorBase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findClosest() {
|
function findClosest() {
|
||||||
const editors = getEditors();
|
const editors = editor.getEditors();
|
||||||
const last = editors.length - 1;
|
const last = editors.length - 1;
|
||||||
let a = 0;
|
let a = 0;
|
||||||
let b = last;
|
let b = last;
|
||||||
|
@ -206,7 +275,7 @@ function createSectionsEditor(editorBase) {
|
||||||
}
|
}
|
||||||
const cm = editors[b];
|
const cm = editors[b];
|
||||||
if (distances[b] > 0) {
|
if (distances[b] > 0) {
|
||||||
scrollToEditor(cm);
|
editor.scrollToEditor(cm);
|
||||||
}
|
}
|
||||||
return cm;
|
return cm;
|
||||||
}
|
}
|
||||||
|
@ -221,24 +290,6 @@ function createSectionsEditor(editorBase) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditors() {
|
|
||||||
return sections.filter(s => !s.isRemoved()).map(s => s.cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextEditor(cm, cycle = true) {
|
|
||||||
if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return nextPrevEditor(cm, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevEditor(cm, cycle = true) {
|
|
||||||
if (!cycle && sections.find(s => !s.isRemoved()).cm === cm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return nextPrevEditor(cm, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findLast(arr, match) {
|
function findLast(arr, match) {
|
||||||
for (let i = arr.length - 1; i >= 0; i--) {
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
if (match(arr[i])) {
|
if (match(arr[i])) {
|
||||||
|
@ -248,32 +299,17 @@ function createSectionsEditor(editorBase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPrevEditor(cm, direction) {
|
function nextPrevEditor(cm, direction) {
|
||||||
const editors = getEditors();
|
const editors = editor.getEditors();
|
||||||
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
|
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
|
||||||
scrollToEditor(cm);
|
editor.scrollToEditor(cm);
|
||||||
cm.focus();
|
cm.focus();
|
||||||
return cm;
|
return cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToEditor(cm) {
|
|
||||||
const section = sections.find(s => s.cm === cm).el;
|
|
||||||
const bounds = section.getBoundingClientRect();
|
|
||||||
if (
|
|
||||||
(bounds.bottom > window.innerHeight && bounds.top > 0) ||
|
|
||||||
(bounds.top < 0 && bounds.bottom < window.innerHeight)
|
|
||||||
) {
|
|
||||||
if (bounds.top < 0) {
|
|
||||||
window.scrollBy(0, bounds.top - 1);
|
|
||||||
} else {
|
|
||||||
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastActivatedEditor() {
|
function getLastActivatedEditor() {
|
||||||
let result;
|
let result;
|
||||||
for (const section of sections) {
|
for (const section of sections) {
|
||||||
if (section.isRemoved()) {
|
if (section.removed) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// .lastActive is initiated by codemirror-factory
|
// .lastActive is initiated by codemirror-factory
|
||||||
|
@ -387,16 +423,18 @@ function createSectionsEditor(editorBase) {
|
||||||
|
|
||||||
function updateSectionOrder() {
|
function updateSectionOrder() {
|
||||||
const oldOrder = sectionOrder;
|
const oldOrder = sectionOrder;
|
||||||
const validSections = sections.filter(s => !s.isRemoved());
|
const validSections = sections.filter(s => !s.removed);
|
||||||
sectionOrder = validSections.map(s => s.id).join(',');
|
sectionOrder = validSections.map(s => s.id).join(',');
|
||||||
dirty.modify('sectionOrder', oldOrder, sectionOrder);
|
dirty.modify('sectionOrder', oldOrder, sectionOrder);
|
||||||
container.dataset.sectionCount = validSections.length;
|
container.dataset.sectionCount = validSections.length;
|
||||||
linter.refreshReport();
|
linter.refreshReport();
|
||||||
|
editor.updateToc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @returns {Style} */
|
||||||
function getModel() {
|
function getModel() {
|
||||||
return Object.assign({}, style, {
|
return Object.assign({}, style, {
|
||||||
sections: sections.filter(s => !s.isRemoved()).map(s => s.getModel())
|
sections: sections.filter(s => !s.removed).map(s => s.getModel())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,7 +445,7 @@ function createSectionsEditor(editorBase) {
|
||||||
}
|
}
|
||||||
for (const section of sections) {
|
for (const section of sections) {
|
||||||
for (const apply of section.appliesTo) {
|
for (const apply of section.appliesTo) {
|
||||||
if (apply.getType() !== 'regexp') {
|
if (apply.type !== 'regexp') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!apply.valueEl.reportValidity()) {
|
if (!apply.valueEl.reportValidity()) {
|
||||||
|
@ -419,25 +457,9 @@ function createSectionsEditor(editorBase) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
|
||||||
if (!dirty.isDirty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newStyle = getModel();
|
|
||||||
if (!validate(newStyle)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
API.editSave(newStyle)
|
|
||||||
.then(newStyle => {
|
|
||||||
destroyRemovedSections();
|
|
||||||
sessionStorage.justEditedStyleId = newStyle.id;
|
|
||||||
replaceStyle(newStyle, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroyRemovedSections() {
|
function destroyRemovedSections() {
|
||||||
for (let i = 0; i < sections.length;) {
|
for (let i = 0; i < sections.length;) {
|
||||||
if (!sections[i].isRemoved()) {
|
if (!sections[i].removed) {
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -451,14 +473,14 @@ function createSectionsEditor(editorBase) {
|
||||||
$('#name').value = style.customName || style.name || '';
|
$('#name').value = style.customName || style.name || '';
|
||||||
$('#enabled').checked = style.enabled !== false;
|
$('#enabled').checked = style.enabled !== false;
|
||||||
$('#url').href = style.url || '';
|
$('#url').href = style.url || '';
|
||||||
editorBase.updateName();
|
editor.updateName();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLivePreview() {
|
function updateLivePreview() {
|
||||||
debounce(_updateLivePreview, 200);
|
debounce(updateLivePreviewNow, editor.previewDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _updateLivePreview() {
|
function updateLivePreviewNow() {
|
||||||
livePreview.update(getModel());
|
livePreview.update(getModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,7 +514,8 @@ function createSectionsEditor(editorBase) {
|
||||||
setGlobalProgress(total - originalSections.length, total);
|
setGlobalProgress(total - originalSections.length, total);
|
||||||
if (!originalSections.length) {
|
if (!originalSections.length) {
|
||||||
setGlobalProgress();
|
setGlobalProgress();
|
||||||
fitToAvailableSpace();
|
requestAnimationFrame(fitToAvailableSpace);
|
||||||
|
sections.forEach(({cm}) => setTimeout(linter.enableForEditor, 0, cm));
|
||||||
done();
|
done();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(chunk);
|
setTimeout(chunk);
|
||||||
|
@ -500,8 +523,9 @@ function createSectionsEditor(editorBase) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {EditorSection} section */
|
||||||
function removeSection(section) {
|
function removeSection(section) {
|
||||||
if (sections.every(s => s.isRemoved() || s === section)) {
|
if (sections.every(s => s.removed || s === section)) {
|
||||||
// TODO: hide remove button when `#sections[data-section-count=1]`
|
// TODO: hide remove button when `#sections[data-section-count=1]`
|
||||||
throw new Error('Cannot remove last section');
|
throw new Error('Cannot remove last section');
|
||||||
}
|
}
|
||||||
|
@ -528,6 +552,7 @@ function createSectionsEditor(editorBase) {
|
||||||
updateLivePreview();
|
updateLivePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {EditorSection} section */
|
||||||
function restoreSection(section) {
|
function restoreSection(section) {
|
||||||
section.restore();
|
section.restore();
|
||||||
updateSectionOrder();
|
updateSectionOrder();
|
||||||
|
@ -535,40 +560,36 @@ function createSectionsEditor(editorBase) {
|
||||||
updateLivePreview();
|
updateLivePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StyleSection} [init]
|
||||||
|
* @param {EditorSection} [base]
|
||||||
|
* @param {boolean} [forceRefresh]
|
||||||
|
*/
|
||||||
function insertSectionAfter(init, base, forceRefresh) {
|
function insertSectionAfter(init, base, forceRefresh) {
|
||||||
if (!init) {
|
if (!init) {
|
||||||
init = {code: '', urlPrefixes: ['http://example.com']};
|
init = {code: '', urlPrefixes: ['http://example.com']};
|
||||||
}
|
}
|
||||||
const section = createSection({
|
const section = createSection(init, genId);
|
||||||
originalSection: init,
|
|
||||||
genId,
|
|
||||||
dirty,
|
|
||||||
showMozillaFormatImport,
|
|
||||||
removeSection,
|
|
||||||
restoreSection,
|
|
||||||
insertSectionAfter,
|
|
||||||
moveSectionUp,
|
|
||||||
moveSectionDown,
|
|
||||||
prevEditor,
|
|
||||||
nextEditor
|
|
||||||
});
|
|
||||||
const {cm} = section;
|
const {cm} = section;
|
||||||
sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section);
|
sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section);
|
||||||
container.insertBefore(section.el, base ? base.el.nextSibling : null);
|
container.insertBefore(section.el, base ? base.el.nextSibling : null);
|
||||||
refreshOnView(cm, forceRefresh);
|
refreshOnView(cm, forceRefresh);
|
||||||
|
registerEvents(section);
|
||||||
if (!base || init.code) {
|
if (!base || init.code) {
|
||||||
// Fit a) during startup or b) when the clone button is clicked on a section with some code
|
// Fit a) during startup or b) when the clone button is clicked on a section with some code
|
||||||
fitToContent(section);
|
fitToContent(section);
|
||||||
}
|
}
|
||||||
if (base) {
|
if (base) {
|
||||||
cm.focus();
|
cm.focus();
|
||||||
setTimeout(scrollToEditor, 0, cm);
|
setTimeout(editor.scrollToEditor, 0, cm);
|
||||||
|
linter.enableForEditor(cm);
|
||||||
}
|
}
|
||||||
updateSectionOrder();
|
updateSectionOrder();
|
||||||
section.onChange(updateLivePreview);
|
section.onChange(updateLivePreview);
|
||||||
updateLivePreview();
|
updateLivePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {EditorSection} section */
|
||||||
function moveSectionUp(section) {
|
function moveSectionUp(section) {
|
||||||
const index = sections.indexOf(section);
|
const index = sections.indexOf(section);
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
|
@ -580,6 +601,7 @@ function createSectionsEditor(editorBase) {
|
||||||
updateSectionOrder();
|
updateSectionOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {EditorSection} section */
|
||||||
function moveSectionDown(section) {
|
function moveSectionDown(section) {
|
||||||
const index = sections.indexOf(section);
|
const index = sections.indexOf(section);
|
||||||
if (index === sections.length - 1) {
|
if (index === sections.length - 1) {
|
||||||
|
@ -591,21 +613,56 @@ function createSectionsEditor(editorBase) {
|
||||||
updateSectionOrder();
|
updateSectionOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replaceStyle(newStyle, codeIsUpdated) {
|
/** @param {EditorSection} section */
|
||||||
dirty.clear('name');
|
function registerEvents(section) {
|
||||||
// FIXME: avoid recreating all editors?
|
const {el, cm} = section;
|
||||||
if (codeIsUpdated !== false) {
|
$('.applies-to-help', el).onclick = () => showHelp(t('appliesLabel'), t('appliesHelp'));
|
||||||
await initSections(newStyle.sections, {replace: true, pristine: true});
|
$('.remove-section', el).onclick = () => removeSection(section);
|
||||||
|
$('.add-section', el).onclick = () => insertSectionAfter(undefined, section);
|
||||||
|
$('.clone-section', el).onclick = () => insertSectionAfter(section.getModel(), section);
|
||||||
|
$('.move-section-up', el).onclick = () => moveSectionUp(section);
|
||||||
|
$('.move-section-down', el).onclick = () => moveSectionDown(section);
|
||||||
|
$('.restore-section', el).onclick = () => restoreSection(section);
|
||||||
|
cm.on('paste', maybeImportOnPaste);
|
||||||
|
if (!FIREFOX) {
|
||||||
|
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
||||||
}
|
}
|
||||||
Object.assign(style, newStyle);
|
}
|
||||||
updateHeader();
|
|
||||||
dirty.clear();
|
function maybeImportOnPaste(cm, event) {
|
||||||
// Go from new style URL to edit style URL
|
const text = event.clipboardData.getData('text') || '';
|
||||||
if (location.href.indexOf('id=') === -1 && style.id) {
|
if (/@-moz-document/i.test(text) &&
|
||||||
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
|
/@-moz-document\s+(url|url-prefix|domain|regexp)\(/i
|
||||||
$('#heading').textContent = t('editStyleHeading');
|
.test(text.replace(/\/\*([^*]|\*(?!\/))*(\*\/|$)/g, ''))
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
showMozillaFormatImport(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshOnView(cm, force) {
|
||||||
|
return force || !xo ?
|
||||||
|
cm.refresh() :
|
||||||
|
xo.observe(cm.display.wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshOnViewListener(entries) {
|
||||||
|
for (const {isIntersecting, target} of entries) {
|
||||||
|
if (isIntersecting) {
|
||||||
|
target.CodeMirror.refresh();
|
||||||
|
xo.unobserve(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleContextMenuDelete(event) {
|
||||||
|
if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
|
||||||
|
chrome.contextMenus.update('editor.contextDelete', {
|
||||||
|
enabled: Boolean(
|
||||||
|
this.selectionStart !== this.selectionEnd ||
|
||||||
|
this.somethingSelected && this.somethingSelected()
|
||||||
|
),
|
||||||
|
}, ignoreChromeError);
|
||||||
}
|
}
|
||||||
livePreview.show(Boolean(style.id));
|
|
||||||
updateLivePreview();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,98 @@
|
||||||
/* global
|
/* global
|
||||||
createAppliesToLineWidget messageBox
|
$
|
||||||
|
$$
|
||||||
|
$create
|
||||||
|
API
|
||||||
|
chromeSync
|
||||||
|
cmFactory
|
||||||
|
CodeMirror
|
||||||
|
createLivePreview
|
||||||
|
createMetaCompiler
|
||||||
|
debounce
|
||||||
|
editor
|
||||||
|
linter
|
||||||
|
messageBox
|
||||||
|
MozSectionFinder
|
||||||
|
MozSectionWidget
|
||||||
|
prefs
|
||||||
sectionsToMozFormat
|
sectionsToMozFormat
|
||||||
createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t
|
t
|
||||||
chromeSync */
|
*/
|
||||||
/* exported createSourceEditor */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createSourceEditor(editorBase) {
|
/* exported SourceEditor */
|
||||||
const {style, dirty} = editorBase;
|
|
||||||
|
|
||||||
|
function SourceEditor() {
|
||||||
|
const {style, dirty} = editor;
|
||||||
|
let savedGeneration;
|
||||||
let placeholderName = '';
|
let placeholderName = '';
|
||||||
|
let prevMode = NaN;
|
||||||
|
|
||||||
$('#mozilla-format-container').remove();
|
$$.remove('.sectioned-only');
|
||||||
$('#header').addEventListener('wheel', headerOnScroll);
|
$('#header').on('wheel', headerOnScroll);
|
||||||
$('#sections').textContent = '';
|
$('#sections').textContent = '';
|
||||||
$('#sections').appendChild($create('.single-editor'));
|
$('#sections').appendChild($create('.single-editor'));
|
||||||
|
|
||||||
// normalize style
|
|
||||||
if (!style.id) setupNewStyle(style);
|
if (!style.id) setupNewStyle(style);
|
||||||
|
|
||||||
const cm = cmFactory.create($('.single-editor'), {
|
const cm = cmFactory.create($('.single-editor'));
|
||||||
value: style.sourceCode,
|
const sectionFinder = MozSectionFinder(cm);
|
||||||
|
const sectionWidget = MozSectionWidget(cm, sectionFinder, editor.updateToc);
|
||||||
|
const livePreview = createLivePreview(preprocess, style.id);
|
||||||
|
/** @namespace SourceEditor */
|
||||||
|
Object.assign(editor, {
|
||||||
|
sections: sectionFinder.sections,
|
||||||
|
replaceStyle,
|
||||||
|
getEditors: () => [cm],
|
||||||
|
scrollToEditor: () => {},
|
||||||
|
getEditorTitle: () => '',
|
||||||
|
save,
|
||||||
|
prevEditor: nextPrevSection.bind(null, -1),
|
||||||
|
nextEditor: nextPrevSection.bind(null, 1),
|
||||||
|
jumpToEditor(i) {
|
||||||
|
const sec = sectionFinder.sections[i];
|
||||||
|
if (sec) {
|
||||||
|
sectionFinder.updatePositions(sec);
|
||||||
|
jumpToPos(sec.start);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closestVisible: () => cm,
|
||||||
|
getSearchableInputs: () => [],
|
||||||
|
updateLivePreview,
|
||||||
});
|
});
|
||||||
let savedGeneration = cm.changeGeneration();
|
createMetaCompiler(cm, meta => {
|
||||||
|
|
||||||
const livePreview = createLivePreview(preprocess);
|
|
||||||
livePreview.show(Boolean(style.id));
|
|
||||||
|
|
||||||
cm.on('changes', () => {
|
|
||||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
|
||||||
updateLivePreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
cm.operation(initAppliesToLineWidget);
|
|
||||||
|
|
||||||
const metaCompiler = createMetaCompiler(cm);
|
|
||||||
metaCompiler.onUpdated(meta => {
|
|
||||||
style.usercssData = meta;
|
style.usercssData = meta;
|
||||||
style.name = meta.name;
|
style.name = meta.name;
|
||||||
style.url = meta.homepageURL || style.installationUrl;
|
style.url = meta.homepageURL || style.installationUrl;
|
||||||
updateMeta();
|
updateMeta();
|
||||||
});
|
});
|
||||||
|
updateMeta();
|
||||||
updateMeta().then(() => {
|
cm.setValue(style.sourceCode);
|
||||||
|
prefs.subscribeMany({
|
||||||
linter.enableForEditor(cm);
|
'editor.linter': updateLinterSwitch,
|
||||||
|
'editor.appliesToLineWidget': (k, val) => sectionWidget.toggle(val),
|
||||||
let prevMode = NaN;
|
'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val),
|
||||||
cm.on('optionChange', (cm, option) => {
|
}, {now: true});
|
||||||
if (option !== 'mode') return;
|
cm.clearHistory();
|
||||||
const mode = getModeName();
|
cm.markClean();
|
||||||
if (mode === prevMode) return;
|
savedGeneration = cm.changeGeneration();
|
||||||
prevMode = mode;
|
cm.on('changes', () => {
|
||||||
linter.run();
|
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||||
updateLinterSwitch();
|
debounce(updateLivePreview, editor.previewDelay);
|
||||||
});
|
|
||||||
|
|
||||||
$('#editor.linter').addEventListener('change', updateLinterSwitch);
|
|
||||||
updateLinterSwitch();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if ((document.activeElement || {}).localName !== 'input') {
|
|
||||||
cm.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
cm.on('optionChange', (cm, option) => {
|
||||||
|
if (option !== 'mode') return;
|
||||||
|
const mode = getModeName();
|
||||||
|
if (mode === prevMode) return;
|
||||||
|
prevMode = mode;
|
||||||
|
linter.run();
|
||||||
|
updateLinterSwitch();
|
||||||
|
});
|
||||||
|
debounce(linter.enableForEditor, 0, cm);
|
||||||
|
if (!$.isTextInput(document.activeElement)) {
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
|
||||||
function preprocess(style) {
|
function preprocess(style) {
|
||||||
return API.buildUsercss({
|
return API.buildUsercss({
|
||||||
|
@ -85,13 +113,6 @@ function createSourceEditor(editorBase) {
|
||||||
livePreview.update(Object.assign({}, style, {sourceCode: cm.getValue()}));
|
livePreview.update(Object.assign({}, style, {sourceCode: cm.getValue()}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAppliesToLineWidget() {
|
|
||||||
const PREF_NAME = 'editor.appliesToLineWidget';
|
|
||||||
const widget = createAppliesToLineWidget(cm);
|
|
||||||
widget.toggle(prefs.get(PREF_NAME));
|
|
||||||
prefs.subscribe([PREF_NAME], (key, value) => widget.toggle(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLinterSwitch() {
|
function updateLinterSwitch() {
|
||||||
const el = $('#editor.linter');
|
const el = $('#editor.linter');
|
||||||
el.value = getCurrentLinter();
|
el.value = getCurrentLinter();
|
||||||
|
@ -158,8 +179,8 @@ function createSourceEditor(editorBase) {
|
||||||
}
|
}
|
||||||
$('#enabled').checked = style.enabled;
|
$('#enabled').checked = style.enabled;
|
||||||
$('#url').href = style.url;
|
$('#url').href = style.url;
|
||||||
editorBase.updateName();
|
editor.updateName();
|
||||||
return cm.setPreprocessor((style.usercssData || {}).preprocessor);
|
cm.setPreprocessor((style.usercssData || {}).preprocessor);
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceStyle(newStyle, codeIsUpdated) {
|
function replaceStyle(newStyle, codeIsUpdated) {
|
||||||
|
@ -272,68 +293,30 @@ function createSourceEditor(editorBase) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPrevMozDocument(cm, dir) {
|
function nextPrevSection(dir) {
|
||||||
const MOZ_DOC = '@-moz-document';
|
// ensure the data is ready in case the user wants to jump around a lot in a large style
|
||||||
const cursor = cm.getCursor();
|
sectionFinder.keepAliveFor(nextPrevSection, 10e3);
|
||||||
const usePrevLine = dir < 0 && cursor.ch <= MOZ_DOC.length;
|
sectionFinder.updatePositions();
|
||||||
let line = cursor.line + (usePrevLine ? -1 : 0);
|
const {sections} = sectionFinder;
|
||||||
let start = usePrevLine ? 1e9 : cursor.ch + (dir > 0 ? 1 : -MOZ_DOC.length);
|
const num = sections.length;
|
||||||
let found;
|
if (!num) return;
|
||||||
if (dir > 0) {
|
dir = dir < 0 ? -1 : 0;
|
||||||
cm.doc.iter(cursor.line, cm.doc.size, goFind);
|
const pos = cm.getCursor();
|
||||||
if (!found && cursor.line > 0) {
|
let i = sections.findIndex(sec => CodeMirror.cmpPos(sec.start, pos) > Math.min(dir, 0));
|
||||||
line = 0;
|
if (i < 0 && (!dir || CodeMirror.cmpPos(sections[num - 1].start, pos) < 0)) {
|
||||||
cm.doc.iter(0, cursor.line + 1, goFind);
|
i = 0;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let handle, parentLines;
|
|
||||||
let passesRemain = line < cm.doc.size - 1 ? 2 : 1;
|
|
||||||
let stopAtLine = 0;
|
|
||||||
while (passesRemain--) {
|
|
||||||
let indexInParent = 0;
|
|
||||||
while (line >= stopAtLine) {
|
|
||||||
if (!indexInParent--) {
|
|
||||||
handle = cm.getLineHandle(line);
|
|
||||||
parentLines = handle.parent.lines;
|
|
||||||
indexInParent = parentLines.indexOf(handle);
|
|
||||||
} else {
|
|
||||||
handle = parentLines[indexInParent];
|
|
||||||
}
|
|
||||||
if (goFind(handle)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line = cm.doc.size - 1;
|
|
||||||
stopAtLine = cursor.line;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function goFind({text}) {
|
jumpToPos(sections[(i + dir + num) % num].start);
|
||||||
// use the initial 'start' on cursor row...
|
}
|
||||||
let ch = start;
|
|
||||||
// ...and reset it for the rest
|
function jumpToPos(pos) {
|
||||||
start = dir > 0 ? 0 : 1e9;
|
const coords = cm.cursorCoords(pos, 'page');
|
||||||
while (true) {
|
const b = cm.display.wrapper.getBoundingClientRect();
|
||||||
// indexOf is 1000x faster than toLowerCase().indexOf() so we're trying it first
|
if (coords.top < b.top + cm.defaultTextHeight() * 2 ||
|
||||||
ch = dir > 0 ? text.indexOf('@-', ch) : text.lastIndexOf('@-', ch);
|
coords.bottom > b.bottom - 100) {
|
||||||
if (ch < 0) {
|
cm.scrollIntoView(pos, b.height / 2);
|
||||||
line += dir;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (text.substr(ch, MOZ_DOC.length).toLowerCase() === MOZ_DOC &&
|
|
||||||
cm.getTokenTypeAt({line, ch: ch + 1}) === 'def') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ch += dir * 3;
|
|
||||||
}
|
|
||||||
cm.setCursor(line, ch);
|
|
||||||
if (cm.cursorCoords().bottom > cm.display.scroller.clientHeight - 100) {
|
|
||||||
const margin = Math.min(100, cm.display.scroller.clientHeight / 4);
|
|
||||||
line += prefs.get('editor.appliesToLineWidget') ? 1 : 0;
|
|
||||||
cm.scrollIntoView({line, ch}, margin);
|
|
||||||
}
|
|
||||||
found = true;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
cm.setCursor(pos, null, {scroll: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
function headerOnScroll({target, deltaY, deltaMode, shiftKey}) {
|
function headerOnScroll({target, deltaY, deltaMode, shiftKey}) {
|
||||||
|
@ -358,18 +341,4 @@ function createSourceEditor(editorBase) {
|
||||||
return (mode.name || mode || '') +
|
return (mode.name || mode || '') +
|
||||||
(mode.helperType || '');
|
(mode.helperType || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign({}, editorBase, {
|
|
||||||
ready: Promise.resolve(),
|
|
||||||
replaceStyle,
|
|
||||||
getEditors: () => [cm],
|
|
||||||
scrollToEditor: () => {},
|
|
||||||
getEditorTitle: () => '',
|
|
||||||
save,
|
|
||||||
prevEditor: cm => nextPrevMozDocument(cm, -1),
|
|
||||||
nextEditor: cm => nextPrevMozDocument(cm, 1),
|
|
||||||
closestVisible: () => cm,
|
|
||||||
getSearchableInputs: () => [],
|
|
||||||
updateLivePreview,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
180
edit/util.js
180
edit/util.js
|
@ -1,130 +1,161 @@
|
||||||
/* global CodeMirror $create prefs */
|
/* global
|
||||||
/* exported dirtyReporter memoize clipString sectionsToMozFormat createHotkeyInput */
|
$create
|
||||||
|
CodeMirror
|
||||||
|
prefs
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function dirtyReporter() {
|
/* exported DirtyReporter */
|
||||||
const dirty = new Map();
|
class DirtyReporter {
|
||||||
const onchanges = [];
|
constructor() {
|
||||||
|
this._dirty = new Map();
|
||||||
|
this._onchange = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
function add(obj, value) {
|
add(obj, value) {
|
||||||
const saved = dirty.get(obj);
|
const wasDirty = this.isDirty();
|
||||||
|
const saved = this._dirty.get(obj);
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
dirty.set(obj, {type: 'add', newValue: value});
|
this._dirty.set(obj, {type: 'add', newValue: value});
|
||||||
} else if (saved.type === 'remove') {
|
} else if (saved.type === 'remove') {
|
||||||
if (saved.savedValue === value) {
|
if (saved.savedValue === value) {
|
||||||
dirty.delete(obj);
|
this._dirty.delete(obj);
|
||||||
} else {
|
} else {
|
||||||
saved.newValue = value;
|
saved.newValue = value;
|
||||||
saved.type = 'modify';
|
saved.type = 'modify';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.notifyChange(wasDirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(obj, value) {
|
remove(obj, value) {
|
||||||
const saved = dirty.get(obj);
|
const wasDirty = this.isDirty();
|
||||||
|
const saved = this._dirty.get(obj);
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
dirty.set(obj, {type: 'remove', savedValue: value});
|
this._dirty.set(obj, {type: 'remove', savedValue: value});
|
||||||
} else if (saved.type === 'add') {
|
} else if (saved.type === 'add') {
|
||||||
dirty.delete(obj);
|
this._dirty.delete(obj);
|
||||||
} else if (saved.type === 'modify') {
|
} else if (saved.type === 'modify') {
|
||||||
saved.type = 'remove';
|
saved.type = 'remove';
|
||||||
}
|
}
|
||||||
|
this.notifyChange(wasDirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modify(obj, oldValue, newValue) {
|
modify(obj, oldValue, newValue) {
|
||||||
const saved = dirty.get(obj);
|
const wasDirty = this.isDirty();
|
||||||
|
const saved = this._dirty.get(obj);
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
if (oldValue !== newValue) {
|
if (oldValue !== newValue) {
|
||||||
dirty.set(obj, {type: 'modify', savedValue: oldValue, newValue});
|
this._dirty.set(obj, {type: 'modify', savedValue: oldValue, newValue});
|
||||||
}
|
}
|
||||||
} else if (saved.type === 'modify') {
|
} else if (saved.type === 'modify') {
|
||||||
if (saved.savedValue === newValue) {
|
if (saved.savedValue === newValue) {
|
||||||
dirty.delete(obj);
|
this._dirty.delete(obj);
|
||||||
} else {
|
} else {
|
||||||
saved.newValue = newValue;
|
saved.newValue = newValue;
|
||||||
}
|
}
|
||||||
} else if (saved.type === 'add') {
|
} else if (saved.type === 'add') {
|
||||||
saved.newValue = newValue;
|
saved.newValue = newValue;
|
||||||
}
|
}
|
||||||
|
this.notifyChange(wasDirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(obj) {
|
clear(obj) {
|
||||||
|
const wasDirty = this.isDirty();
|
||||||
if (obj === undefined) {
|
if (obj === undefined) {
|
||||||
dirty.clear();
|
this._dirty.clear();
|
||||||
} else {
|
} else {
|
||||||
dirty.delete(obj);
|
this._dirty.delete(obj);
|
||||||
|
}
|
||||||
|
this.notifyChange(wasDirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirty() {
|
||||||
|
return this._dirty.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(cb, add = true) {
|
||||||
|
this._onchange[add ? 'add' : 'delete'](cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyChange(wasDirty) {
|
||||||
|
if (wasDirty !== this.isDirty()) {
|
||||||
|
this._onchange.forEach(cb => cb());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDirty() {
|
has(key) {
|
||||||
return dirty.size > 0;
|
return this._dirty.has(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChange(cb) {
|
|
||||||
// make sure the callback doesn't throw
|
|
||||||
onchanges.push(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrap(obj) {
|
|
||||||
for (const key of ['add', 'remove', 'modify', 'clear']) {
|
|
||||||
obj[key] = trackChange(obj[key]);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitChange() {
|
|
||||||
for (const cb of onchanges) {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function trackChange(fn) {
|
|
||||||
return function () {
|
|
||||||
const dirty = isDirty();
|
|
||||||
const result = fn.apply(null, arguments);
|
|
||||||
if (dirty !== isDirty()) {
|
|
||||||
emitChange();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function has(key) {
|
|
||||||
return dirty.has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrap({add, remove, modify, clear, isDirty, onChange, has});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* exported DocFuncMapper */
|
||||||
function sectionsToMozFormat(style) {
|
const DocFuncMapper = {
|
||||||
const propertyToCss = {
|
TO_CSS: {
|
||||||
urls: 'url',
|
urls: 'url',
|
||||||
urlPrefixes: 'url-prefix',
|
urlPrefixes: 'url-prefix',
|
||||||
domains: 'domain',
|
domains: 'domain',
|
||||||
regexps: 'regexp',
|
regexps: 'regexp',
|
||||||
};
|
},
|
||||||
return style.sections.map(section => {
|
FROM_CSS: {
|
||||||
let cssMds = [];
|
'url': 'urls',
|
||||||
for (const i in propertyToCss) {
|
'url-prefix': 'urlPrefixes',
|
||||||
if (section[i]) {
|
'domain': 'domains',
|
||||||
cssMds = cssMds.concat(section[i].map(v =>
|
'regexp': 'regexps',
|
||||||
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
|
},
|
||||||
));
|
/**
|
||||||
|
* @param {Object} section
|
||||||
|
* @param {function(func:string, value:string)} fn
|
||||||
|
*/
|
||||||
|
forEachProp(section, fn) {
|
||||||
|
for (const [propName, func] of Object.entries(DocFuncMapper.TO_CSS)) {
|
||||||
|
const props = section[propName];
|
||||||
|
if (props) props.forEach(value => fn(func, value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {Array<?[type,value]>} funcItems
|
||||||
|
* @param {?Object} [section]
|
||||||
|
* @returns {Object} section
|
||||||
|
*/
|
||||||
|
toSection(funcItems, section = {}) {
|
||||||
|
for (const item of funcItems) {
|
||||||
|
const [func, value] = item || [];
|
||||||
|
const propName = DocFuncMapper.FROM_CSS[func];
|
||||||
|
if (propName) {
|
||||||
|
const props = section[propName] || (section[propName] = []);
|
||||||
|
if (Array.isArray(value)) props.push(...value);
|
||||||
|
else props.push(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cssMds.length ?
|
return section;
|
||||||
'@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' :
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* exported sectionsToMozFormat */
|
||||||
|
function sectionsToMozFormat(style) {
|
||||||
|
return style.sections.map(section => {
|
||||||
|
const cssFuncs = [];
|
||||||
|
DocFuncMapper.forEachProp(section, (type, value) =>
|
||||||
|
cssFuncs.push(`${type}("${value.replace(/\\/g, '\\\\')}")`));
|
||||||
|
return cssFuncs.length ?
|
||||||
|
`@-moz-document ${cssFuncs.join(', ')} {\n${section.code}\n}` :
|
||||||
section.code;
|
section.code;
|
||||||
}).join('\n\n');
|
}).join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* exported trimCommentLabel */
|
||||||
|
function trimCommentLabel(str, limit = 1000) {
|
||||||
|
// stripping /*** foo ***/ to foo
|
||||||
|
return clipString(str.replace(/^[!-/:;=\s]*|[-#$&(+,./:;<=>\s*]*$/g, ''), limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* exported clipString */
|
||||||
function clipString(str, limit = 100) {
|
function clipString(str, limit = 100) {
|
||||||
return str.length <= limit ? str : str.substr(0, limit) + '...';
|
return str.length <= limit ? str : str.substr(0, limit) + '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a decorator. Cache the first call
|
/* exported memoize */
|
||||||
function memoize(fn) {
|
function memoize(fn) {
|
||||||
let cached = false;
|
let cached = false;
|
||||||
let result;
|
let result;
|
||||||
|
@ -137,6 +168,7 @@ function memoize(fn) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* exported createHotkeyInput */
|
||||||
/**
|
/**
|
||||||
* @param {!string} prefId
|
* @param {!string} prefId
|
||||||
* @param {?function(isEnter:boolean)} onDone
|
* @param {?function(isEnter:boolean)} onDone
|
||||||
|
|
|
@ -212,8 +212,12 @@ select[disabled] + .select-arrow {
|
||||||
|
|
||||||
:focus,
|
:focus,
|
||||||
.CodeMirror-focused,
|
.CodeMirror-focused,
|
||||||
[data-focused-via-click] input[type="text"]:focus,
|
/* Allowing click outline on text/search inputs and textareas */
|
||||||
[data-focused-via-click] input[type="number"]:focus {
|
textarea[data-focused-via-click]:focus,
|
||||||
|
input:not([type])[data-focused-via-click]:focus, /* same as "text" */
|
||||||
|
input[type="text"][data-focused-via-click]:focus,
|
||||||
|
input[type="search"][data-focused-via-click]:focus,
|
||||||
|
input[type="number"][data-focused-via-click]:focus {
|
||||||
/* Using box-shadow instead of the ugly outline in new Chrome */
|
/* Using box-shadow instead of the ugly outline in new Chrome */
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 1px hsl(180, 100%, 38%), 0 0 3px hsla(180, 100%, 60%, .5);
|
box-shadow: 0 0 0 1px hsl(180, 100%, 38%), 0 0 3px hsla(180, 100%, 60%, .5);
|
||||||
|
|
124
js/dom.js
124
js/dom.js
|
@ -7,21 +7,18 @@ if (!/^Win\d+/.test(navigator.platform)) {
|
||||||
document.documentElement.classList.add('non-windows');
|
document.documentElement.classList.add('non-windows');
|
||||||
}
|
}
|
||||||
|
|
||||||
// make querySelectorAll enumeration code readable
|
Object.assign(EventTarget.prototype, {
|
||||||
// FIXME: avoid extending native?
|
on: addEventListener,
|
||||||
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
|
off: removeEventListener,
|
||||||
NodeList.prototype[method] = Array.prototype[method];
|
/** args: [el:EventTarget, type:string, fn:function, ?opts] */
|
||||||
|
onOff(enable, ...args) {
|
||||||
|
(enable ? addEventListener : removeEventListener).apply(this, args);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// polyfill for old browsers to enable [...results] and for-of
|
$.isTextInput = (el = {}) =>
|
||||||
for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) {
|
el.localName === 'textarea' ||
|
||||||
if (!type.prototype[Symbol.iterator]) {
|
el.localName === 'input' && /^(text|search|number)$/.test(el.type);
|
||||||
type.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$.isTextLikeInput = el =>
|
|
||||||
el.localName === 'input' && /^(text|search|number)$/.test(el.type);
|
|
||||||
|
|
||||||
$.remove = (selector, base = document) => {
|
$.remove = (selector, base = document) => {
|
||||||
const el = selector && typeof selector === 'string' ? $(selector, base) : selector;
|
const el = selector && typeof selector === 'string' ? $(selector, base) : selector;
|
||||||
|
@ -61,7 +58,7 @@ $$.remove = (selector, base = document) => {
|
||||||
setTimeout(addTooltipsToEllipsized, 500);
|
setTimeout(addTooltipsToEllipsized, 500);
|
||||||
// throttle on continuous resizing
|
// throttle on continuous resizing
|
||||||
let timer;
|
let timer;
|
||||||
window.addEventListener('resize', () => {
|
window.on('resize', () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = setTimeout(addTooltipsToEllipsized, 100);
|
timer = setTimeout(addTooltipsToEllipsized, 100);
|
||||||
});
|
});
|
||||||
|
@ -89,13 +86,13 @@ onDOMready().then(() => {
|
||||||
// set language for CSS :lang and [FF-only] hyphenation
|
// set language for CSS :lang and [FF-only] hyphenation
|
||||||
document.documentElement.setAttribute('lang', chrome.i18n.getUILanguage());
|
document.documentElement.setAttribute('lang', chrome.i18n.getUILanguage());
|
||||||
// avoid adding # to the page URL when clicking dummy links
|
// avoid adding # to the page URL when clicking dummy links
|
||||||
document.addEventListener('click', e => {
|
document.on('click', e => {
|
||||||
if (e.target.closest('a[href="#"]')) {
|
if (e.target.closest('a[href="#"]')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// update inputs on mousewheel when focused
|
// update inputs on mousewheel when focused
|
||||||
document.addEventListener('wheel', event => {
|
document.on('wheel', event => {
|
||||||
const el = document.activeElement;
|
const el = document.activeElement;
|
||||||
if (!el || el !== event.target && !el.contains(event.target)) {
|
if (!el || el !== event.target && !el.contains(event.target)) {
|
||||||
return;
|
return;
|
||||||
|
@ -117,7 +114,7 @@ document.addEventListener('wheel', event => {
|
||||||
function onDOMready() {
|
function onDOMready() {
|
||||||
return document.readyState !== 'loading'
|
return document.readyState !== 'loading'
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
|
: new Promise(resolve => document.on('DOMContentLoaded', resolve, {once: true}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,12 +149,12 @@ function animateElement(el, cls = 'highlight', ...removeExtraClasses) {
|
||||||
if (onDone) {
|
if (onDone) {
|
||||||
const style = getComputedStyle(el);
|
const style = getComputedStyle(el);
|
||||||
if (style.animationName === 'none' || !parseFloat(style.animationDuration)) {
|
if (style.animationName === 'none' || !parseFloat(style.animationDuration)) {
|
||||||
el.removeEventListener('animationend', onDone);
|
el.off('animationend', onDone);
|
||||||
onDone();
|
onDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
el.addEventListener('animationend', onDone, {once: true});
|
el.on('animationend', onDone, {once: true});
|
||||||
el.classList.add(cls);
|
el.classList.add(cls);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -175,8 +172,8 @@ function enforceInputRange(element) {
|
||||||
doNotify();
|
doNotify();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
element.addEventListener('change', onChange);
|
element.on('change', onChange);
|
||||||
element.addEventListener('input', onChange);
|
element.on('input', onChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -320,7 +317,7 @@ function initCollapsibles({bindClickOn = 'h2'} = {}) {
|
||||||
const key = el.dataset.pref;
|
const key = el.dataset.pref;
|
||||||
prefMap[key] = el;
|
prefMap[key] = el;
|
||||||
el.open = prefs.get(key);
|
el.open = prefs.get(key);
|
||||||
(bindClickOn && $(bindClickOn, el) || el).addEventListener('click', onClick);
|
(bindClickOn && $(bindClickOn, el) || el).on('click', onClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.subscribe(Object.keys(prefMap), (key, value) => {
|
prefs.subscribe(Object.keys(prefMap), (key, value) => {
|
||||||
|
@ -339,7 +336,7 @@ function initCollapsibles({bindClickOn = 'h2'} = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveState(el) {
|
function saveState(el) {
|
||||||
if (!el.classList.contains('ignore-pref')) {
|
if (!el.matches('.compact-layout .ignore-pref-if-compact')) {
|
||||||
prefs.set(el.dataset.pref, el.open);
|
prefs.set(el.dataset.pref, el.open);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -349,58 +346,41 @@ function initCollapsibles({bindClickOn = 'h2'} = {}) {
|
||||||
function focusAccessibility() {
|
function focusAccessibility() {
|
||||||
// last event's focusedViaClick
|
// last event's focusedViaClick
|
||||||
focusAccessibility.lastFocusedViaClick = false;
|
focusAccessibility.lastFocusedViaClick = false;
|
||||||
// tags of focusable elements;
|
// to avoid a full layout recalc due to changes on body/root
|
||||||
// to avoid a full layout recalc we modify the closest one
|
// we modify the closest focusable element (like input or button or anything with tabindex=0)
|
||||||
focusAccessibility.ELEMENTS = [
|
focusAccessibility.closest = el => {
|
||||||
'a',
|
let labelSeen;
|
||||||
'button',
|
for (; el; el = el.parentElement) {
|
||||||
'input',
|
if (el.localName === 'label' && el.control && !labelSeen) {
|
||||||
'label',
|
el = el.control;
|
||||||
'select',
|
labelSeen = true;
|
||||||
'summary',
|
}
|
||||||
];
|
if (el.tabIndex >= 0) return el;
|
||||||
// try to find a focusable parent for this many parentElement jumps:
|
}
|
||||||
const GIVE_UP_DEPTH = 4;
|
};
|
||||||
// allow outline on text/search inputs in addition to textareas
|
// suppress outline on click
|
||||||
const isOutlineAllowed = el =>
|
window.on('mousedown', ({target}) => {
|
||||||
!focusAccessibility.ELEMENTS.includes(el.localName) ||
|
const el = focusAccessibility.closest(target);
|
||||||
$.isTextLikeInput(el);
|
if (el) {
|
||||||
|
focusAccessibility.lastFocusedViaClick = true;
|
||||||
addEventListener('mousedown', suppressOutlineOnClick, {passive: true});
|
if (el.dataset.focusedViaClick === undefined) {
|
||||||
addEventListener('keydown', keepOutlineOnTab, {passive: true});
|
el.dataset.focusedViaClick = '';
|
||||||
|
|
||||||
function suppressOutlineOnClick({target}) {
|
|
||||||
for (let el = target, i = 0; el && i++ < GIVE_UP_DEPTH; el = el.parentElement) {
|
|
||||||
if (!isOutlineAllowed(el)) {
|
|
||||||
focusAccessibility.lastFocusedViaClick = true;
|
|
||||||
if (el.dataset.focusedViaClick === undefined) {
|
|
||||||
el.dataset.focusedViaClick = '';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, {passive: true});
|
||||||
|
// keep outline on Tab or Shift-Tab key
|
||||||
function keepOutlineOnTab(event) {
|
window.on('keydown', event => {
|
||||||
if (event.key === 'Tab') {
|
if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||||
focusAccessibility.lastFocusedViaClick = false;
|
focusAccessibility.lastFocusedViaClick = false;
|
||||||
setTimeout(keepOutlineOnTab, 0, true);
|
setTimeout(() => {
|
||||||
return;
|
let el = document.activeElement;
|
||||||
} else if (event !== true) {
|
if (el) {
|
||||||
return;
|
el = el.closest('[data-focused-via-click]');
|
||||||
|
if (el) delete el.dataset.focusedViaClick;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let el = document.activeElement;
|
}, {passive: true});
|
||||||
if (!el || isOutlineAllowed(el)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (el.dataset.focusedViaClick !== undefined) {
|
|
||||||
delete el.dataset.focusedViaClick;
|
|
||||||
}
|
|
||||||
el = el.closest('[data-focused-via-click]');
|
|
||||||
if (el) {
|
|
||||||
delete el.dataset.focusedViaClick;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -437,7 +417,7 @@ function setupLivePrefs(
|
||||||
for (const id of IDs) {
|
for (const id of IDs) {
|
||||||
const element = $('#' + id);
|
const element = $('#' + id);
|
||||||
updateElement({id, element, force: true});
|
updateElement({id, element, force: true});
|
||||||
element.addEventListener('change', onChange);
|
element.on('change', onChange);
|
||||||
}
|
}
|
||||||
prefs.subscribe(IDs, (id, value) => updateElement({id, value}));
|
prefs.subscribe(IDs, (id, value) => updateElement({id, value}));
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ window.INJECTED !== 1 && (() => {
|
||||||
'manage.newUI.sort': 'title,asc',
|
'manage.newUI.sort': 'title,asc',
|
||||||
|
|
||||||
'editor.options': {}, // CodeMirror.defaults.*
|
'editor.options': {}, // CodeMirror.defaults.*
|
||||||
|
'editor.toc.expanded': true, // UI element state: expanded/collapsed
|
||||||
'editor.options.expanded': true, // UI element state: expanded/collapsed
|
'editor.options.expanded': true, // UI element state: expanded/collapsed
|
||||||
'editor.lint.expanded': true, // UI element state: expanded/collapsed
|
'editor.lint.expanded': true, // UI element state: expanded/collapsed
|
||||||
'editor.lineWrapping': true, // word wrap
|
'editor.lineWrapping': true, // word wrap
|
||||||
|
@ -180,6 +181,11 @@ window.INJECTED !== 1 && (() => {
|
||||||
if (now) fn();
|
if (now) fn();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
subscribeMany(data, opts) {
|
||||||
|
for (const [k, fn] of Object.entries(data)) {
|
||||||
|
prefs.subscribe(k, fn, opts);
|
||||||
|
}
|
||||||
|
},
|
||||||
unsubscribe(keys, fn) {
|
unsubscribe(keys, fn) {
|
||||||
if (keys) {
|
if (keys) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
|
|
|
@ -79,7 +79,7 @@ function styleSectionsEqual(a, b, {ignoreCode, checkSource} = {}) {
|
||||||
|
|
||||||
function normalizeStyleSections({sections}) {
|
function normalizeStyleSections({sections}) {
|
||||||
// retain known properties in an arbitrarily predefined order
|
// retain known properties in an arbitrarily predefined order
|
||||||
return (sections || []).map(section => ({
|
return (sections || []).map(section => /** @namespace StyleSection */({
|
||||||
code: section.code || '',
|
code: section.code || '',
|
||||||
urls: section.urls || [],
|
urls: section.urls || [],
|
||||||
urlPrefixes: section.urlPrefixes || [],
|
urlPrefixes: section.urlPrefixes || [],
|
||||||
|
|
|
@ -84,7 +84,7 @@ onDOMready().then(() => {
|
||||||
if (event.altKey || event.metaKey || $('#message-box')) {
|
if (event.altKey || event.metaKey || $('#message-box')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const inTextInput = $.isTextLikeInput(event.target);
|
const inTextInput = $.isTextInput(event.target);
|
||||||
const {key, code, ctrlKey: ctrl} = event;
|
const {key, code, ctrlKey: ctrl} = event;
|
||||||
// `code` is independent of the current keyboard language
|
// `code` is independent of the current keyboard language
|
||||||
if ((code === 'KeyF' && ctrl && !event.shiftKey) ||
|
if ((code === 'KeyF' && ctrl && !event.shiftKey) ||
|
||||||
|
@ -94,17 +94,21 @@ onDOMready().then(() => {
|
||||||
$('#search').focus();
|
$('#search').focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ctrl || inTextInput ||
|
if (ctrl || inTextInput && event.target !== input) {
|
||||||
key === ' ' && !input.value /* Space or Shift-Space is for page down/up */) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const time = performance.now();
|
const time = performance.now();
|
||||||
if (key.length === 1) {
|
if (key.length === 1) {
|
||||||
input.focus();
|
|
||||||
if (time - prevTime > 1000) {
|
if (time - prevTime > 1000) {
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
prevTime = time;
|
// Space or Shift-Space is for page down/up
|
||||||
|
if (key === ' ' && !input.value) {
|
||||||
|
input.blur();
|
||||||
|
} else {
|
||||||
|
input.focus();
|
||||||
|
prevTime = time;
|
||||||
|
}
|
||||||
} else
|
} else
|
||||||
if (key === 'Enter' && focusedLink) {
|
if (key === 'Enter' && focusedLink) {
|
||||||
focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||||
|
|
|
@ -68,7 +68,7 @@ function messageBox({
|
||||||
}
|
}
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
if (target.closest(focusAccessibility.ELEMENTS.join(','))) {
|
if (focusAccessibility.closest(target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -4435,14 +4435,23 @@ self.parserlib = (() => {
|
||||||
const prefix = start.value.split('-')[1] || '';
|
const prefix = start.value.split('-')[1] || '';
|
||||||
do {
|
do {
|
||||||
this._ws();
|
this._ws();
|
||||||
functions.push(this._documentFunction());
|
functions.push(this._documentFunction() || stream.LT(1));
|
||||||
} while (stream.match(Tokens.COMMA));
|
} while (stream.match(Tokens.COMMA));
|
||||||
|
|
||||||
this._ws();
|
this._ws();
|
||||||
if (this.options.emptyDocument && stream.peek() !== Tokens.LBRACE) {
|
if (this.options.emptyDocument && stream.peek() !== Tokens.LBRACE) {
|
||||||
this.fire({type: 'emptydocument', functions, prefix}, start);
|
this.fire({type: 'emptydocument', functions, prefix}, start);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
for (const fn of functions) {
|
||||||
|
if ((fn.type !== 'function' || !/^(url(-prefix)?|domain|regexp)$/i.test(fn.name)) &&
|
||||||
|
fn.type !== 'uri') {
|
||||||
|
this.fire({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Expected url( or url-prefix( or domain( or regexp(, instead saw ' +
|
||||||
|
Tokens.name(fn.tokenType || fn.type) + ' ' + (fn.text || fn.value),
|
||||||
|
}, fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
stream.mustMatch(Tokens.LBRACE);
|
stream.mustMatch(Tokens.LBRACE);
|
||||||
|
|
||||||
this.fire({
|
this.fire({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user