diff --git a/edit.html b/edit.html
index 2826b6bf..a5e2286f 100644
--- a/edit.html
+++ b/edit.html
@@ -28,6 +28,9 @@
+
+
+
@@ -62,8 +65,10 @@
-
-
+
+
+
+
diff --git a/edit/beautify.js b/edit/beautify.js
new file mode 100644
index 00000000..c3dd0723
--- /dev/null
+++ b/edit/beautify.js
@@ -0,0 +1,96 @@
+/*
+global CodeMirror loadScript css_beautify
+global editors getSectionForChild showHelp
+*/
+'use strict';
+
+function beautify(event) {
+ loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
+ .then(() => {
+ if (!window.css_beautify && window.exports) {
+ window.css_beautify = window.exports.css_beautify;
+ }
+ })
+ .then(doBeautify);
+
+ function doBeautify() {
+ const tabs = prefs.get('editor.indentWithTabs');
+ const options = prefs.get('editor.beautify');
+ options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
+ options.indent_char = tabs ? '\t' : ' ';
+
+ const section = getSectionForChild(event.target);
+ const scope = section ? [section.CodeMirror] : editors;
+
+ showHelp(t('styleBeautify'), '
' +
+ optionHtml('.selector1,', 'selector_separator_newline') +
+ optionHtml('.selector2,', 'newline_before_open_brace') +
+ optionHtml('{', 'newline_after_open_brace') +
+ optionHtml('border: none;', 'newline_between_properties', true) +
+ optionHtml('display: block;', 'newline_before_close_brace', true) +
+ optionHtml('}', 'newline_between_rules') +
+ `' +
+ '
' +
+ '');
+
+ const undoButton = $('#help-popup button[role="undo"]');
+ undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal');
+ undoButton.addEventListener('click', () => {
+ let undoable = false;
+ scope.forEach(cm => {
+ if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
+ delete cm.beautifyChange[cm.changeGeneration()];
+ cm.undo();
+ cm.scrollIntoView(cm.getCursor());
+ undoable |= cm.beautifyChange[cm.changeGeneration()];
+ }
+ });
+ undoButton.disabled = !undoable;
+ });
+
+ scope.forEach(cm => {
+ setTimeout(() => {
+ const pos = options.translate_positions =
+ [].concat.apply([], cm.doc.sel.ranges.map(r =>
+ [Object.assign({}, r.anchor), Object.assign({}, r.head)]));
+ const text = cm.getValue();
+ const newText = css_beautify(text, options);
+ if (newText !== text) {
+ if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
+ // clear the list if last change wasn't a css-beautify
+ cm.beautifyChange = {};
+ }
+ cm.setValue(newText);
+ const selections = [];
+ for (let i = 0; i < pos.length; i += 2) {
+ selections.push({anchor: pos[i], head: pos[i + 1]});
+ }
+ cm.setSelections(selections);
+ cm.beautifyChange[cm.changeGeneration()] = true;
+ undoButton.disabled = false;
+ }
+ }, 0);
+ });
+
+ $('.beautify-options').onchange = ({target}) => {
+ const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
+ prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
+ if (target.parentNode.hasAttribute('newline')) {
+ target.parentNode.setAttribute('newline', value.toString());
+ }
+ doBeautify();
+ };
+
+ function optionHtml(label, optionName, indent) {
+ const value = options[optionName];
+ return '' +
+ '' + label + '' +
+ '
';
+ }
+ }
+}
diff --git a/edit/codemirror-editing-hooks.js b/edit/codemirror-editing-hooks.js
new file mode 100644
index 00000000..6e4fdea4
--- /dev/null
+++ b/edit/codemirror-editing-hooks.js
@@ -0,0 +1,687 @@
+/*
+global CodeMirror linterConfig loadScript
+global editors editor styleId
+global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
+*/
+'use strict';
+
+onDOMready().then(() => {
+ CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
+
+ const COMMANDS = {
+ save,
+ toggleStyle,
+ jumpToLine,
+ nextEditor, prevEditor,
+ find, findNext, findPrev, replace, replaceAll,
+ };
+ // reroute handling to nearest editor when keypress resolves to one of these commands
+ const REROUTED = new Set([
+ ...Object.keys(COMMANDS),
+ 'colorpicker',
+ ]);
+
+ const ORIGINAL_COMMAND = {
+ find: CodeMirror.commands.find,
+ findNext: CodeMirror.commands.findNext,
+ findPrev: CodeMirror.commands.findPrev,
+ replace: CodeMirror.commands.replace
+ };
+ const ORIGINAL_METHOD = {
+ openDialog: CodeMirror.prototype.openDialog,
+ openConfirm: CodeMirror.prototype.openConfirm,
+ };
+
+ Object.assign(CodeMirror, {
+ getOption,
+ setOption,
+ });
+ Object.assign(CodeMirror.commands,
+ COMMANDS);
+ Object.assign(CodeMirror.prototype, {
+ getSection,
+ rerouteHotkeys,
+ });
+
+ // cm.state.search for last used 'find'
+ let searchState;
+
+ buildOptionsElements();
+ setupLivePrefs();
+ rerouteHotkeys(true);
+ $('#options').addEventListener('change', onOptionElementChanged);
+
+ return;
+
+ ////////////////////////////////////////////////
+
+ function getOption(o) {
+ return CodeMirror.defaults[o];
+ }
+
+ function setOption(o, v) {
+ CodeMirror.defaults[o] = v;
+ if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
+ throttleSetOption({key: o, value: v, index: 0});
+ return;
+ }
+ editors.forEach(editor => {
+ editor.setOption(o, v);
+ });
+ }
+
+ function throttleSetOption({
+ key,
+ value,
+ index,
+ timeStart = performance.now(),
+ cmStart = editors.lastActive || editors[0],
+ editorsCopy = editors.slice(),
+ progress,
+ }) {
+ if (index === 0) {
+ if (!cmStart) {
+ return;
+ }
+ cmStart.setOption(key, value);
+ }
+
+ const THROTTLE_AFTER_MS = 100;
+ const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
+
+ const t0 = performance.now();
+ const total = editorsCopy.length;
+ while (index < total) {
+ const cm = editorsCopy[index++];
+ if (cm === cmStart ||
+ cm !== editors[index] && !editors.includes(cm)) {
+ continue;
+ }
+ cm.setOption(key, value);
+ if (performance.now() - t0 > THROTTLE_AFTER_MS) {
+ break;
+ }
+ }
+ if (index >= total) {
+ $.remove(progress);
+ return;
+ }
+ if (!progress &&
+ index < total / 2 &&
+ t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
+ let option = $('#editor.' + key);
+ if (option) {
+ if (option.type === 'checkbox') {
+ option = (option.labels || [])[0] || option.nextElementSibling || option;
+ }
+ progress = document.body.appendChild($element({
+ className: 'set-option-progress',
+ targetElement: option,
+ }));
+ }
+ }
+ if (progress) {
+ const optionBounds = progress.targetElement.getBoundingClientRect();
+ const bounds = {
+ top: optionBounds.top + window.scrollY + 1,
+ left: optionBounds.left + window.scrollX + 1,
+ width: (optionBounds.width - 2) * index / total | 0,
+ height: optionBounds.height - 2,
+ };
+ const style = progress.style;
+ for (const prop in bounds) {
+ if (bounds[prop] !== parseFloat(style[prop])) {
+ style[prop] = bounds[prop] + 'px';
+ }
+ }
+ }
+ setTimeout(throttleSetOption, 0, {
+ key,
+ value,
+ index,
+ timeStart,
+ cmStart,
+ editorsCopy,
+ progress,
+ });
+ }
+
+ function getSection() {
+ return this.display.wrapper.parentNode;
+ }
+
+ function nextEditor(cm) {
+ nextPrevEditor(cm, 1);
+ }
+
+ function prevEditor(cm) {
+ nextPrevEditor(cm, -1);
+ }
+
+ function nextPrevEditor(cm, direction) {
+ cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
+ makeSectionVisible(cm);
+ cm.focus();
+ return cm;
+ }
+
+ function jumpToLine(cm) {
+ const cur = cm.getCursor();
+ refocusMinidialog(cm);
+ cm.openDialog(template.jumpToLine.cloneNode(true), str => {
+ const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
+ if (m) {
+ cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
+ }
+ }, {value: cur.line + 1});
+ }
+
+ function refocusMinidialog(cm) {
+ const section = cm.getSection();
+ if (!$('.CodeMirror-dialog', section)) {
+ return;
+ }
+ // close the currently opened minidialog
+ cm.focus();
+ // make sure to focus the input in newly opened minidialog
+ setTimeout(() => {
+ $('.CodeMirror-dialog', section).focus();
+ });
+ }
+
+ function onOptionElementChanged(event) {
+ const el = event.target;
+ let option = el.id.replace(/^editor\./, '');
+ //console.log('acmeEventListener heard %s on %s', event.type, el.id);
+ if (!option) {
+ console.error('acmeEventListener: no "cm_option" %O', el);
+ return;
+ }
+ let value = el.type === 'checkbox' ? el.checked : el.value;
+ switch (option) {
+ case 'tabSize':
+ value = Number(value);
+ CodeMirror.setOption('indentUnit', value);
+ break;
+
+ case 'theme': {
+ const themeLink = $('#cm-theme');
+ // use non-localized 'default' internally
+ if (!value || value === 'default' || value === t('defaultTheme')) {
+ value = 'default';
+ if (prefs.get(el.id) !== value) {
+ prefs.set(el.id, value);
+ }
+ themeLink.href = '';
+ el.selectedIndex = 0;
+ break;
+ }
+ const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
+ if (themeLink.href === url) {
+ // preloaded in initCodeMirror()
+ break;
+ }
+ // avoid flicker: wait for the second stylesheet to load, then apply the theme
+ document.head.appendChild($element({
+ tag: 'link',
+ id: 'cm-theme2',
+ rel: 'stylesheet',
+ href: url
+ }));
+ setTimeout(() => {
+ CodeMirror.setOption(option, value);
+ themeLink.remove();
+ $('#cm-theme2').id = 'cm-theme';
+ }, 100);
+ return;
+ }
+
+ case 'autocompleteOnTyping':
+ editors.forEach(cm => setupAutocomplete(cm, el.checked));
+ return;
+
+ case 'autoCloseBrackets':
+ Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
+ CodeMirror.setOption(option, value);
+ });
+ return;
+
+ case 'matchHighlight':
+ switch (value) {
+ case 'token':
+ case 'selection':
+ document.body.dataset[option] = value;
+ value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
+ break;
+ default:
+ value = null;
+ }
+ option = 'highlightSelectionMatches';
+ break;
+
+ case 'colorpicker':
+ return;
+ }
+ CodeMirror.setOption(option, value);
+ }
+
+ function buildOptionsElements() {
+ // no need to escape the period in the id
+ const themeControl = $('#editor.theme');
+ const themeList = localStorage.codeMirrorThemes;
+ if (themeList) {
+ optionsFromArray(themeControl, themeList.split(/\s+/));
+ } else {
+ // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
+ const theme = prefs.get('editor.theme');
+ optionsFromArray(themeControl, [theme === 'default' ? t('defaultTheme') : theme]);
+ getCodeMirrorThemes().then(() => {
+ const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
+ optionsFromArray(themeControl, themes);
+ themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
+ });
+ }
+ optionsFromArray($('#editor.keyMap'), Object.keys(CodeMirror.keyMap).sort());
+ }
+
+ function optionsFromArray(parent, options) {
+ const fragment = document.createDocumentFragment();
+ for (const opt of options) {
+ fragment.appendChild($element({tag: 'option', textContent: opt}));
+ }
+ parent.appendChild(fragment);
+ }
+
+ /////////////////////
+
+ function shouldIgnoreCase(query) {
+ // treat all-lowercase non-regexp queries as case-insensitive
+ return typeof query === 'string' && query === query.toLowerCase();
+ }
+
+ function updateState(cm, newState) {
+ if (!newState) {
+ if (cm.state.search) {
+ return cm.state.search;
+ }
+ if (!searchState) {
+ return null;
+ }
+ newState = searchState;
+ }
+ cm.state.search = {
+ query: newState.query,
+ overlay: newState.overlay,
+ annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
+ };
+ cm.addOverlay(newState.overlay);
+ return cm.state.search;
+ }
+
+ // overrides the original openDialog with a clone of the provided template
+ function customizeOpenDialog(cm, template, callback) {
+ cm.openDialog = (tmpl, cb, opt) => {
+ // invoke 'callback' and bind 'this' to the original callback
+ ORIGINAL_METHOD.openDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt);
+ };
+ setTimeout(() => (cm.openDialog = ORIGINAL_METHOD.openDialog));
+ refocusMinidialog(cm);
+ }
+
+ function focusClosestCM(activeCM) {
+ editors.lastActive = activeCM;
+ const cm = getEditorInSight();
+ if (cm !== activeCM) {
+ cm.focus();
+ }
+ return cm;
+ }
+
+ function find(activeCM) {
+ activeCM = focusClosestCM(activeCM);
+ customizeOpenDialog(activeCM, template.find, function (query) {
+ this(query);
+ searchState = activeCM.state.search;
+ if (editors.length === 1 || !searchState.query) {
+ return;
+ }
+ editors.forEach(cm => {
+ if (cm !== activeCM) {
+ cm.execCommand('clearSearch');
+ updateState(cm, searchState);
+ }
+ });
+ if (CodeMirror.cmpPos(searchState.posFrom, searchState.posTo) === 0) {
+ findNext(activeCM);
+ }
+ });
+ ORIGINAL_COMMAND.find(activeCM);
+ }
+
+ function findNext(activeCM, reverse) {
+ let state = updateState(activeCM);
+ if (!state || !state.query) {
+ find(activeCM);
+ return;
+ }
+ let pos = activeCM.getCursor(reverse ? 'from' : 'to');
+ // clear the selection, don't move the cursor
+ activeCM.setSelection(activeCM.getCursor());
+
+ const rxQuery = typeof state.query === 'object'
+ ? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : '');
+
+ if (
+ document.activeElement &&
+ document.activeElement.name === 'applies-value' &&
+ searchAppliesTo(activeCM)
+ ) {
+ return;
+ }
+ let cm = activeCM;
+ for (let i = 0; i < editors.length; i++) {
+ state = updateState(cm);
+ if (!cm.hasFocus()) {
+ pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
+ }
+ const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query));
+ if (searchCursor.find(reverse)) {
+ if (editors.length > 1) {
+ makeSectionVisible(cm);
+ cm.focus();
+ }
+ // speedup the original findNext
+ state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
+ state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch);
+ ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'](cm);
+ return;
+ } else if (!reverse && searchAppliesTo(cm)) {
+ return;
+ }
+ cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length];
+ if (reverse && searchAppliesTo(cm)) {
+ return;
+ }
+ }
+ // nothing found so far, so call the original search with wrap-around
+ ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'](activeCM);
+
+ function searchAppliesTo(cm) {
+ let inputs = $$('.applies-value', cm.getSection());
+ if (reverse) {
+ inputs = inputs.reverse();
+ }
+ inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
+ return inputs.some(input => {
+ const match = rxQuery.exec(input.value);
+ if (match) {
+ input.focus();
+ const end = match.index + match[0].length;
+ // scroll selected part into view in long inputs,
+ // works only outside of current event handlers chain, hence timeout=0
+ setTimeout(() => {
+ input.setSelectionRange(end, end);
+ input.setSelectionRange(match.index, end);
+ }, 0);
+ return true;
+ }
+ });
+ }
+ }
+
+ function findPrev(cm) {
+ findNext(cm, true);
+ }
+
+ function replace(activeCM, all) {
+ let queue;
+ let query;
+ let replacement;
+ activeCM = focusClosestCM(activeCM);
+ customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], function (txt) {
+ query = txt;
+ customizeOpenDialog(activeCM, template.replaceWith, txt => {
+ replacement = txt;
+ queue = editors.rotate(-editors.indexOf(activeCM));
+ if (all) {
+ editors.forEach(doReplace);
+ } else {
+ doReplace();
+ }
+ });
+ this(query);
+ });
+ ORIGINAL_COMMAND.replace(activeCM, all);
+
+ function doReplace() {
+ const cm = queue.shift();
+ if (!cm) {
+ if (!all) {
+ editors.lastActive.focus();
+ }
+ return;
+ }
+ // hide the first two dialogs (replace, replaceWith)
+ cm.openDialog = (tmpl, callback) => {
+ cm.openDialog = (tmpl, callback) => {
+ cm.openDialog = ORIGINAL_METHOD.openDialog;
+ if (all) {
+ callback(replacement);
+ } else {
+ doConfirm(cm);
+ callback(replacement);
+ if (!$('.CodeMirror-dialog', cm.getWrapperElement())) {
+ // no dialog == nothing found in the current CM, move to the next
+ doReplace();
+ }
+ }
+ };
+ callback(query);
+ };
+ ORIGINAL_COMMAND.replace(cm, all);
+ }
+ function doConfirm(cm) {
+ let wrapAround = false;
+ const origPos = cm.getCursor();
+ cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
+ const ovrCallbacks = callbacks.map(callback => () => {
+ makeSectionVisible(cm);
+ cm.openConfirm = overrideConfirm;
+ setTimeout(() => (cm.openConfirm = ORIGINAL_METHOD.openConfirm));
+
+ const pos = cm.getCursor();
+ callback();
+ const cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
+ wrapAround |= cmp <= 0;
+
+ const dlg = $('.CodeMirror-dialog', cm.getWrapperElement());
+ if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
+ $.remove(dlg);
+ doReplace();
+ }
+ });
+ ORIGINAL_METHOD.openConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt);
+ };
+ }
+ }
+
+ function replaceAll(cm) {
+ replace(cm, true);
+ }
+
+ ////////////////////////////////////////////////
+
+ function rerouteHotkeys(enable, immediately) {
+ if (!immediately) {
+ debounce(rerouteHotkeys, 0, enable, true);
+ } else if (enable) {
+ document.addEventListener('keydown', rerouteHandler);
+ } else {
+ document.removeEventListener('keydown', rerouteHandler);
+ }
+ }
+
+ function rerouteHandler(event) {
+ const keyName = CodeMirror.keyName(event);
+ if (!keyName) {
+ return;
+ }
+ const rerouteCommand = name => {
+ if (REROUTED.has(name)) {
+ CodeMirror.commands[name](getEditorInSight(event.target));
+ return true;
+ }
+ };
+ if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
+ CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ ////////////////////////////////////////////////
+
+ function getEditorInSight(nearbyElement) {
+ // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
+ let cm;
+ if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) {
+ cm = getSectionForChild(nearbyElement).CodeMirror;
+ } else {
+ cm = editors.lastActive;
+ }
+ // closest editor should have at least 2 lines visible
+ const lineHeight = editors[0].defaultTextHeight();
+ const scrollY = window.scrollY;
+ const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
+ const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
+ const distances = [];
+ const alreadyInView = cm && offscreenDistance(null, cm) === 0;
+ return alreadyInView ? cm : findClosest();
+
+ function offscreenDistance(index, cm) {
+ if (index >= 0 && distances[index] !== undefined) {
+ return distances[index];
+ }
+ const section = (cm || editors[index]).getSection();
+ const top = allSectionsContainerTop + section.offsetTop;
+ if (top < scrollY + lineHeight) {
+ return Math.max(0, scrollY - top - lineHeight);
+ }
+ if (top < windowBottom) {
+ return 0;
+ }
+ const distance = top - windowBottom + section.offsetHeight;
+ if (index >= 0) {
+ distances[index] = distance;
+ }
+ return distance;
+ }
+
+ function findClosest() {
+ const last = editors.length - 1;
+ let a = 0;
+ let b = last;
+ let c;
+ let distance;
+ while (a < b - 1) {
+ c = (a + b) / 2 | 0;
+ distance = offscreenDistance(c);
+ if (!distance || !c) {
+ break;
+ }
+ const distancePrev = offscreenDistance(c - 1);
+ const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
+ if (distancePrev <= distance && distance <= distanceNext) {
+ b = c;
+ } else {
+ a = c;
+ }
+ }
+ while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
+ b--;
+ }
+ const cm = editors[b];
+ if (distances[b] > 0) {
+ makeSectionVisible(cm);
+ }
+ return cm;
+ }
+ }
+
+ ////////////////////////////////////////////////
+
+ function getCodeMirrorThemes() {
+ if (!chrome.runtime.getPackageDirectoryEntry) {
+ const themes = [
+ chrome.i18n.getMessage('defaultTheme'),
+ '3024-day',
+ '3024-night',
+ 'abcdef',
+ 'ambiance',
+ 'ambiance-mobile',
+ 'base16-dark',
+ 'base16-light',
+ 'bespin',
+ 'blackboard',
+ 'cobalt',
+ 'colorforth',
+ 'dracula',
+ 'duotone-dark',
+ 'duotone-light',
+ 'eclipse',
+ 'elegant',
+ 'erlang-dark',
+ 'hopscotch',
+ 'icecoder',
+ 'isotope',
+ 'lesser-dark',
+ 'liquibyte',
+ 'material',
+ 'mbo',
+ 'mdn-like',
+ 'midnight',
+ 'monokai',
+ 'neat',
+ 'neo',
+ 'night',
+ 'panda-syntax',
+ 'paraiso-dark',
+ 'paraiso-light',
+ 'pastel-on-dark',
+ 'railscasts',
+ 'rubyblue',
+ 'seti',
+ 'solarized',
+ 'the-matrix',
+ 'tomorrow-night-bright',
+ 'tomorrow-night-eighties',
+ 'ttcn',
+ 'twilight',
+ 'vibrant-ink',
+ 'xq-dark',
+ 'xq-light',
+ 'yeti',
+ 'zenburn',
+ ];
+ localStorage.codeMirrorThemes = themes.join(' ');
+ return Promise.resolve(themes);
+ }
+ return new Promise(resolve => {
+ chrome.runtime.getPackageDirectoryEntry(rootDir => {
+ rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
+ themeDir.createReader().readEntries(entries => {
+ const themes = [
+ chrome.i18n.getMessage('defaultTheme')
+ ].concat(
+ entries.filter(entry => entry.isFile)
+ .sort((a, b) => (a.name < b.name ? -1 : 1))
+ .map(entry => entry.name.replace(/\.css$/, ''))
+ );
+ localStorage.codeMirrorThemes = themes.join(' ');
+ resolve(themes);
+ });
+ });
+ });
+ });
+ }
+});
diff --git a/edit/edit.js b/edit/edit.js
index a7d5c526..23d02412 100644
--- a/edit/edit.js
+++ b/edit/edit.js
@@ -1,12 +1,14 @@
-/* eslint brace-style: 0, operator-linebreak: 0 */
-/* global CodeMirror parserlib */
-/* global loadScript */
-/* global css_beautify */
-/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
-/* global mozParser createSourceEditor */
-/* global closeCurrentTab regExpTester messageBox */
-/* global initColorpicker */
-/* global initCollapsibles */
+/*
+global CodeMirror parserlib loadScript
+global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
+global mozParser createSourceEditor
+global closeCurrentTab regExpTester messageBox
+global initColorpicker
+global initCollapsibles
+global setupCodeMirror
+global beautify
+global initWithSectionStyle addSections removeSection getSectionsHashes
+*/
'use strict';
let styleId = null;
@@ -24,6 +26,10 @@ const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'do
let editor;
+preinit();
+window.onbeforeunload = beforeUnload;
+chrome.runtime.onMessage.addListener(onRuntimeMessage);
+
Promise.all([
initStyleData().then(style => {
styleId = style.id;
@@ -49,87 +55,256 @@ Promise.all([
initHooksCommon(),
]))
.then(([style]) => {
- initCodeMirror();
-
const usercss = isUsercss(style);
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
-
+ $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
if (usercss) {
editor = createSourceEditor(style);
} else {
initWithSectionStyle({style});
+ document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
}
+});
- // workaround part2 for the not showing its toggle icon: hide on scroll
- $('#lint').addEventListener('scroll', function () {
- const newOpacity = this.scrollTop === 0 ? '' : '0';
- const style = this.firstElementChild.style;
- if (style.opacity !== newOpacity) {
- style.opacity = newOpacity;
+function preinit() {
+ // make querySelectorAll enumeration code readable
+ ['forEach', 'some', 'indexOf', 'map'].forEach(method => {
+ NodeList.prototype[method] = Array.prototype[method];
+ });
+
+ // eslint-disable-next-line no-extend-native
+ Object.defineProperties(Array.prototype, {
+ last: {
+ get() {
+ return this[this.length - 1];
+ },
+ },
+ rotate: {
+ value: function (amount) {
+ // negative amount == rotate left
+ const r = this.slice(-amount, this.length);
+ Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
+ return r;
+ },
+ },
+ });
+
+ // preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
+ new MutationObserver((mutations, observer) => {
+ const themeElement = $('#cm-theme');
+ if (themeElement) {
+ themeElement.href = prefs.get('editor.theme') === 'default' ? ''
+ : 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
+ observer.disconnect();
}
- }, {passive: true});
-});
+ }).observe(document, {subtree: true, childList: true});
-// make querySelectorAll enumeration code readable
-['forEach', 'some', 'indexOf', 'map'].forEach(method => {
- NodeList.prototype[method] = Array.prototype[method];
-});
-
-// eslint-disable-next-line no-extend-native
-Array.prototype.rotate = function (amount) {
- // negative amount == rotate left
- const r = this.slice(-amount, this.length);
- Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
- return r;
-};
-
-// eslint-disable-next-line no-extend-native
-Object.defineProperty(Array.prototype, 'last', {get: function () { return this[this.length - 1]; }});
-
-// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
-new MutationObserver((mutations, observer) => {
- const themeElement = $('#cm-theme');
- if (themeElement) {
- themeElement.href = prefs.get('editor.theme') === 'default' ? ''
- : 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
- observer.disconnect();
+ if (chrome.windows) {
+ queryTabs({currentWindow: true}).then(tabs => {
+ const windowId = tabs[0].windowId;
+ if (prefs.get('openEditInWindow')) {
+ if (
+ /true/.test(sessionStorage.saveSizeOnClose) &&
+ 'left' in prefs.get('windowPosition', {}) &&
+ !isWindowMaximized()
+ ) {
+ // window was reopened via Ctrl-Shift-T etc.
+ chrome.windows.update(windowId, prefs.get('windowPosition'));
+ }
+ if (tabs.length === 1 && window.history.length === 1) {
+ chrome.windows.getAll(windows => {
+ if (windows.length > 1) {
+ sessionStorageHash('saveSizeOnClose').set(windowId, true);
+ saveSizeOnClose = true;
+ }
+ });
+ } else {
+ saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
+ }
+ }
+ });
}
-}).observe(document, {subtree: true, childList: true});
-getCodeMirrorThemes();
+ getOwnTab().then(tab => {
+ const ownTabId = tab.id;
+ useHistoryBack = sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href;
+ if (!chrome.windows) {
+ return;
+ }
+ // When an edit page gets attached or detached, remember its state
+ // so we can do the same to the next one to open.
+ chrome.tabs.onAttached.addListener((tabId, info) => {
+ if (tabId !== ownTabId) {
+ return;
+ }
+ if (info.newPosition !== 0) {
+ prefs.set('openEditInWindow', false);
+ return;
+ }
+ chrome.windows.get(info.newWindowId, {populate: true}, win => {
+ // If there's only one tab in this window, it's been dragged to new window
+ const openEditInWindow = win.tabs.length === 1;
+ if (openEditInWindow && FIREFOX) {
+ // FF-only because Chrome retardedly resets the size during dragging
+ chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
+ }
+ prefs.set('openEditInWindow', openEditInWindow);
+ });
+ });
+ });
+}
-// reroute handling to nearest editor when keypress resolves to one of these commands
-const hotkeyRerouter = {
- commands: {
- save: true, jumpToLine: true, nextEditor: true, prevEditor: true,
- find: true, findNext: true, findPrev: true, replace: true, replaceAll: true,
- toggleStyle: true,
- colorpicker: true,
- },
- setState: enable => {
- setTimeout(() => {
- document[(enable ? 'add' : 'remove') + 'EventListener']('keydown', hotkeyRerouter.eventHandler);
- }, 0);
- },
- eventHandler: event => {
- const keyName = CodeMirror.keyName(event);
- if (
- CodeMirror.lookupKey(keyName, CodeMirror.getOption('keyMap'), handleCommand) === 'handled' ||
- CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand) === 'handled'
- ) {
- event.preventDefault();
+function onRuntimeMessage(request) {
+ switch (request.method) {
+ case 'styleUpdated':
+ if (styleId && styleId === request.style.id &&
+ request.reason !== 'editSave' &&
+ request.reason !== 'config') {
+ // code-less style from notifyAllTabs
+ if ((request.style.sections[0] || {}).code === null) {
+ request.style = BG.cachedStyles.byId.get(request.style.id);
+ }
+ if (isUsercss(request.style)) {
+ editor.replaceStyle(request.style, request.codeIsUpdated);
+ } else {
+ initWithSectionStyle(request);
+ }
+ }
+ break;
+ case 'styleDeleted':
+ if (styleId === request.id || editor && editor.getStyle().id === request.id) {
+ window.onbeforeunload = () => {};
+ closeCurrentTab();
+ break;
+ }
+ break;
+ case 'prefChanged':
+ if ('editor.smartIndent' in request.prefs) {
+ CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
+ }
+ break;
+ case 'editDeleteText':
+ document.execCommand('delete');
+ break;
+ }
+}
+
+function beforeUnload() {
+ if (saveSizeOnClose) {
+ rememberWindowSize();
+ }
+ document.activeElement.blur();
+ if (isClean()) {
+ return;
+ }
+ updateLintReportIfEnabled(null, 0);
+ // neither confirm() nor custom messages work in modern browsers but just in case
+ return t('styleChangesNotSaved');
+
+ function isClean() {
+ if (editor) {
+ return !editor.isDirty();
+ } else {
+ return isCleanGlobal();
+ }
+ }
+}
+
+function isUsercss(style) {
+ return (
+ style.usercssData ||
+ !style.id && prefs.get('newStyleAsUsercss')
+ );
+}
+
+function initStyleData() {
+ // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
+ const params = new URLSearchParams(location.search.replace(/^\?/, ''));
+ const id = params.get('id');
+ const createEmptyStyle = () => ({
+ id: null,
+ name: '',
+ enabled: true,
+ sections: [
+ Object.assign({code: ''},
+ ...Object.keys(CssToProperty)
+ .map(name => ({
+ [CssToProperty[name]]: params.get(name) && [params.get(name)] || []
+ }))
+ )
+ ],
+ });
+ return !id ?
+ Promise.resolve(createEmptyStyle()) :
+ getStylesSafe({id}).then(([style]) => style || createEmptyStyle());
+}
+
+function initHooks() {
+ if (initHooks.alreadyDone) {
+ return;
+ }
+ initHooks.alreadyDone = true;
+ $$('#header .style-contributor').forEach(node => {
+ node.addEventListener('change', onChange);
+ node.addEventListener('input', onChange);
+ });
+ $('#toggle-style-help').addEventListener('click', showToggleStyleHelp);
+ $('#to-mozilla').addEventListener('click', showMozillaFormat, false);
+ $('#to-mozilla-help').addEventListener('click', showToMozillaHelp, false);
+ $('#from-mozilla').addEventListener('click', fromMozillaFormat);
+ $('#save-button').addEventListener('click', save, false);
+ $('#sections-help').addEventListener('click', showSectionHelp, false);
+
+ initLint();
+
+ if (!FIREFOX) {
+ $$([
+ 'input:not([type])',
+ 'input[type="text"]',
+ 'input[type="search"]',
+ 'input[type="number"]',
+ ].join(','))
+ .forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
+ }
+}
+
+// common for usercss and classic
+function initHooksCommon() {
+ $('#cancel-button').addEventListener('click', goBackToManage);
+ $('#beautify').addEventListener('click', beautify);
+
+ prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
+ showKeyInSaveButtonTooltip();
+
+ window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
+
+ function goBackToManage(event) {
+ if (useHistoryBack) {
event.stopPropagation();
+ event.preventDefault();
+ history.back();
}
- function handleCommand(command) {
- if (hotkeyRerouter.commands[command] === true) {
- CodeMirror.commands[command](getEditorInSight(event.target));
- return true;
+ }
+ function showKeyInSaveButtonTooltip(prefName, value) {
+ $('#save-button').title = findKeyForCommand('save', value);
+ }
+ function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
+ const map = CodeMirror.keyMap[mapName];
+ let key = Object.keys(map).find(k => map[k] === command);
+ if (key) {
+ return key;
+ }
+ for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
+ key = ft && findKeyForCommand(command, ft);
+ if (key) {
+ return key;
}
}
+ return '';
}
-};
+}
function onChange(event) {
const node = event.target;
@@ -189,7 +364,7 @@ function setCleanGlobal() {
}
function setCleanSection(section) {
- $$('.style-contributor', section).forEach(node => { setCleanItem(node, true); });
+ $$('.style-contributor', section).forEach(node => setCleanItem(node, true));
// #header section has no codemirror
const cm = section.CodeMirror;
@@ -199,1476 +374,77 @@ function setCleanSection(section) {
}
}
-function initCodeMirror() {
- const CM = CodeMirror;
- // lint.js is not loaded initially
- CM.defaults.lint = linterConfig.getForCodeMirror();
-
- // additional commands
- CM.commands.jumpToLine = jumpToLine;
- CM.commands.nextEditor = cm => nextPrevEditor(cm, 1);
- CM.commands.prevEditor = cm => nextPrevEditor(cm, -1);
- CM.commands.save = save;
- CM.commands.toggleStyle = toggleStyle;
-
- // user option values
- CM.getOption = o => CodeMirror.defaults[o];
- CM.setOption = (o, v) => {
- CodeMirror.defaults[o] = v;
- if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
- throttleSetOption({key: o, value: v, index: 0});
- return;
- }
- editors.forEach(editor => {
- editor.setOption(o, v);
- });
- };
-
- CM.prototype.getSection = function () {
- return this.display.wrapper.parentNode;
- };
-
- // initialize global editor controls
- function optionsFromArray(parent, options) {
- const fragment = document.createDocumentFragment();
- for (const opt of options) {
- fragment.appendChild($element({tag: 'option', textContent: opt}));
- }
- parent.appendChild(fragment);
- }
- // no need to escape the period in the id
- const themeControl = $('#editor.theme');
- const themeList = localStorage.codeMirrorThemes;
- if (themeList) {
- optionsFromArray(themeControl, themeList.split(/\s+/));
- } else {
- // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
- const theme = prefs.get('editor.theme');
- optionsFromArray(themeControl, [theme === 'default' ? t('defaultTheme') : theme]);
- getCodeMirrorThemes().then(() => {
- const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
- optionsFromArray(themeControl, themes);
- themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
- });
- }
- optionsFromArray($('#editor.keyMap'), Object.keys(CM.keyMap).sort());
- $('#options').addEventListener('change', acmeEventListener, false);
- setupLivePrefs();
-
- hotkeyRerouter.setState(true);
-
- const THROTTLE_AFTER_MS = 100;
- const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
-
- function throttleSetOption({
- key,
- value,
- index,
- timeStart = performance.now(),
- cmStart = editors.lastActive || editors[0],
- editorsCopy = editors.slice(),
- progress,
- }) {
- if (index === 0) {
- if (!cmStart) {
- return;
- }
- cmStart.setOption(key, value);
- }
- const t0 = performance.now();
- const total = editorsCopy.length;
- while (index < total) {
- const cm = editorsCopy[index++];
- if (cm === cmStart ||
- cm !== editors[index] && !editors.includes(cm)) {
- continue;
- }
- cm.setOption(key, value);
- if (performance.now() - t0 > THROTTLE_AFTER_MS) {
- break;
- }
- }
- if (index >= total) {
- $.remove(progress);
- return;
- }
- if (!progress &&
- index < total / 2 &&
- t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
- let option = $('#editor.' + key);
- if (option) {
- if (option.type === 'checkbox') {
- option = (option.labels || [])[0] || option.nextElementSibling || option;
- }
- progress = document.body.appendChild($element({
- className: 'set-option-progress',
- targetElement: option,
- }));
- }
- }
- if (progress) {
- const optionBounds = progress.targetElement.getBoundingClientRect();
- const bounds = {
- top: optionBounds.top + window.scrollY + 1,
- left: optionBounds.left + window.scrollX + 1,
- width: (optionBounds.width - 2) * index / total | 0,
- height: optionBounds.height - 2,
- };
- const style = progress.style;
- for (const prop in bounds) {
- if (bounds[prop] !== parseFloat(style[prop])) {
- style[prop] = bounds[prop] + 'px';
- }
- }
- }
- setTimeout(throttleSetOption, 0, {
- key,
- value,
- index,
- timeStart,
- cmStart,
- editorsCopy,
- progress,
- });
- }
-}
-
-function acmeEventListener(event) {
- const el = event.target;
- let option = el.id.replace(/^editor\./, '');
- //console.log('acmeEventListener heard %s on %s', event.type, el.id);
- if (!option) {
- console.error('acmeEventListener: no "cm_option" %O', el);
- return;
- }
- let value = el.type === 'checkbox' ? el.checked : el.value;
- switch (option) {
- case 'tabSize':
- value = Number(value);
- CodeMirror.setOption('indentUnit', value);
- break;
- case 'theme': {
- const themeLink = $('#cm-theme');
- // use non-localized 'default' internally
- if (!value || value === 'default' || value === t('defaultTheme')) {
- value = 'default';
- if (prefs.get(el.id) !== value) {
- prefs.set(el.id, value);
- }
- themeLink.href = '';
- el.selectedIndex = 0;
- break;
- }
- const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
- if (themeLink.href === url) {
- // preloaded in initCodeMirror()
- break;
- }
- // avoid flicker: wait for the second stylesheet to load, then apply the theme
- document.head.appendChild($element({
- tag: 'link',
- id: 'cm-theme2',
- rel: 'stylesheet',
- href: url
- }));
- setTimeout(() => {
- CodeMirror.setOption(option, value);
- themeLink.remove();
- $('#cm-theme2').id = 'cm-theme';
- }, 100);
- return;
- }
- case 'autocompleteOnTyping':
- editors.forEach(cm => setupAutocomplete(cm, el.checked));
- return;
- case 'autoCloseBrackets':
- Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
- CodeMirror.setOption(option, value);
- });
- return;
- case 'matchHighlight':
- switch (value) {
- case 'token':
- case 'selection':
- document.body.dataset[option] = value;
- value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
- break;
- default:
- value = null;
- }
- option = 'highlightSelectionMatches';
- break;
- case 'colorpicker':
- return;
- }
- CodeMirror.setOption(option, value);
-}
-
-function setupCodeMirror(sectionDiv, code, index) {
- const cm = CodeMirror(wrapper => {
- $('.code-label', sectionDiv).insertAdjacentElement('afterend', wrapper);
- }, {
- value: code,
- });
- const wrapper = cm.display.wrapper;
-
- cm.on('changes', indicateCodeChangeDebounced);
- if (prefs.get('editor.autocompleteOnTyping')) {
- setupAutocomplete(cm);
- }
- wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
- cm.on('blur', () => {
- editors.lastActive = cm;
- hotkeyRerouter.setState(true);
- setTimeout(() => {
- wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
- });
- });
- cm.on('focus', () => {
- hotkeyRerouter.setState(false);
- wrapper.classList.add('CodeMirror-active');
- });
- cm.on('paste', (cm, event) => {
- const text = event.clipboardData.getData('text') || '';
- if (
- text.includes('@-moz-document') &&
- text.replace(/\/\*[\s\S]*?\*\//g, '')
- .match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/)
- ) {
- event.preventDefault();
- fromMozillaFormat();
- $('#help-popup').codebox.setValue(text);
- $('#help-popup').codebox.clearHistory();
- $('#help-popup').codebox.markClean();
- }
- if (editors.length === 1) {
- setTimeout(() => {
- if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) {
- maximizeCodeHeight.stats = null;
- maximizeCodeHeight(cm.getSection(), true);
- }
- });
- }
- });
- if (!FIREFOX) {
- cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
- }
-
- wrapper.classList.add('resize-grip-enabled');
- let lastClickTime = 0;
- const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
- resizeGrip.onmousedown = event => {
- 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';
- function resize(e) {
- const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
- const height = Math.max(minHeight, e.pageY - cmPageY);
- if (height !== wrapper.clientHeight) {
- cm.setSize(null, height);
- }
- }
- document.addEventListener('mousemove', resize);
- document.addEventListener('mouseup', function resizeStop() {
- document.removeEventListener('mouseup', resizeStop);
- document.removeEventListener('mousemove', resize);
- wrapper.style.pointerEvents = '';
- document.body.style.cursor = '';
- });
- };
-
- editors.splice(index || editors.length, 0, cm);
- return cm;
-}
-
-function indicateCodeChange(cm) {
- const section = cm.getSection();
- setCleanItem(section, cm.isClean(section.savedValue));
- updateTitle();
- updateLintReportIfEnabled(cm);
-}
-
-function indicateCodeChangeDebounced(cm, ...args) {
- clearTimeout(cm.state.stylusOnChangeTimer);
- cm.state.stylusOnChangeTimer = setTimeout(indicateCodeChange, 200, cm, ...args);
-}
-
-function getSectionForChild(e) {
- return e.closest('#sections > div');
-}
-
-function getSections() {
- return $$('#sections > div');
-}
-
-// remind Chrome to repaint a previously invisible editor box by toggling any element's transform
-// this bug is present in some versions of Chrome (v37-40 or something)
-document.addEventListener('scroll', () => {
- const style = $('#name').style;
- style.webkitTransform = style.webkitTransform ? '' : 'scale(1)';
-});
-
-// Shift-Ctrl-Wheel scrolls entire page even when mouse is over a code editor
-document.addEventListener('wheel', event => {
- if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
- // Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
- window.scrollBy(0, event.deltaX || event.deltaY);
- event.preventDefault();
- }
-});
-
-chrome.windows && queryTabs({currentWindow: true}).then(tabs => {
- const windowId = tabs[0].windowId;
- if (prefs.get('openEditInWindow')) {
- if (
- /true/.test(sessionStorage.saveSizeOnClose) &&
- 'left' in prefs.get('windowPosition', {}) &&
- !isWindowMaximized()
- ) {
- // window was reopened via Ctrl-Shift-T etc.
- chrome.windows.update(windowId, prefs.get('windowPosition'));
- }
- if (tabs.length === 1 && window.history.length === 1) {
- chrome.windows.getAll(windows => {
- if (windows.length > 1) {
- sessionStorageHash('saveSizeOnClose').set(windowId, true);
- saveSizeOnClose = true;
- }
- });
- } else {
- saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
- }
- }
-});
-
-getOwnTab().then(tab => {
- const ownTabId = tab.id;
- useHistoryBack = sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href;
- if (!chrome.windows) {
- return;
- }
- // When an edit page gets attached or detached, remember its state
- // so we can do the same to the next one to open.
- chrome.tabs.onAttached.addListener((tabId, info) => {
- if (tabId !== ownTabId) {
- return;
- }
- if (info.newPosition !== 0) {
- prefs.set('openEditInWindow', false);
- return;
- }
- chrome.windows.get(info.newWindowId, {populate: true}, win => {
- // If there's only one tab in this window, it's been dragged to new window
- const openEditInWindow = win.tabs.length === 1;
- if (openEditInWindow && FIREFOX) {
- // FF-only because Chrome retardedly resets the size during dragging
- chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
- }
- prefs.set('openEditInWindow', openEditInWindow);
- });
- });
-});
-
-function goBackToManage(event) {
- if (useHistoryBack) {
- event.stopPropagation();
- event.preventDefault();
- history.back();
- }
-}
-
-function isWindowMaximized() {
- return (
- window.screenX <= 0 &&
- window.screenY <= 0 &&
- window.outerWidth >= screen.availWidth &&
- window.outerHeight >= screen.availHeight &&
-
- window.screenX > -10 &&
- window.screenY > -10 &&
- window.outerWidth < screen.availWidth + 10 &&
- window.outerHeight < screen.availHeight + 10
- );
-}
-
-function rememberWindowSize() {
- if (
- document.visibilityState === 'visible' &&
- prefs.get('openEditInWindow') &&
- !isWindowMaximized()
- ) {
- prefs.set('windowPosition', {
- left: window.screenX,
- top: window.screenY,
- width: window.outerWidth,
- height: window.outerHeight,
- });
- }
-}
-
-window.onbeforeunload = () => {
- if (saveSizeOnClose) {
- rememberWindowSize();
- }
- document.activeElement.blur();
- if (isClean()) {
- return;
- }
- updateLintReportIfEnabled(null, 0);
- // neither confirm() nor custom messages work in modern browsers but just in case
- return t('styleChangesNotSaved');
-
- function isClean() {
- if (editor) {
- return !editor.isDirty();
- } else {
- return isCleanGlobal();
- }
- }
-};
-
-function addAppliesTo(list, name, value) {
- const showingEverything = $('.applies-to-everything', list) !== null;
- // blow away 'Everything' if it's there
- if (showingEverything) {
- list.removeChild(list.firstChild);
- }
- let e;
- if (name) {
- e = template.appliesTo.cloneNode(true);
- $('[name=applies-type]', e).value = name;
- $('[name=applies-value]', e).value = value;
- $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false);
- } else if (showingEverything || list.hasChildNodes()) {
- e = template.appliesTo.cloneNode(true);
- if (list.hasChildNodes()) {
- $('[name=applies-type]', e).value = $('li:last-child [name="applies-type"]', list).value;
- }
- $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false);
- } else {
- e = template.appliesToEverything.cloneNode(true);
- }
- $('.add-applies-to', e).addEventListener('click', function () {
- addAppliesTo(this.parentNode.parentNode);
- }, false);
- list.appendChild(e);
-}
-
-function addSection(event, section) {
- const div = template.section.cloneNode(true);
- $('.applies-to-help', div).addEventListener('click', showAppliesToHelp, false);
- $('.remove-section', div).addEventListener('click', removeSection, false);
- $('.add-section', div).addEventListener('click', addSection, false);
- $('.beautify-section', div).addEventListener('click', beautify);
-
- const code = (section || {}).code || '';
-
- const appliesTo = $('.applies-to-list', div);
- let appliesToAdded = false;
-
- if (section) {
- for (const i in propertyToCss) {
- if (section[i]) {
- section[i].forEach(url => {
- addAppliesTo(appliesTo, propertyToCss[i], url);
- appliesToAdded = true;
- });
- }
- }
- }
- if (!appliesToAdded) {
- addAppliesTo(appliesTo);
- }
-
- appliesTo.addEventListener('change', onChange);
- appliesTo.addEventListener('input', onChange);
-
- toggleTestRegExpVisibility();
- appliesTo.addEventListener('change', toggleTestRegExpVisibility);
- $('.test-regexp', div).onclick = () => {
- regExpTester.toggle();
- regExpTester.update(getRegExps());
- };
-
- function getRegExps() {
- return [...appliesTo.children]
- .map(item =>
- !item.matches('.applies-to-everything') &&
- $('.applies-type', item).value === 'regexp' &&
- $('.applies-value', item).value.trim()
- )
- .filter(item => item);
- }
-
- function toggleTestRegExpVisibility() {
- const show = getRegExps().length > 0;
- div.classList.toggle('has-regexp', show);
- appliesTo.oninput = appliesTo.oninput || show && (event => {
- if (
- event.target.matches('.applies-value') &&
- $('.applies-type', event.target.parentElement).value === 'regexp'
- ) {
- regExpTester.update(getRegExps());
- }
- });
- }
-
- const sections = $('#sections');
- let cm;
- if (event) {
- const clickedSection = getSectionForChild(event.target);
- sections.insertBefore(div, clickedSection.nextElementSibling);
- const newIndex = getSections().indexOf(clickedSection) + 1;
- cm = setupCodeMirror(div, code, newIndex);
- makeSectionVisible(cm);
- renderLintReport();
- cm.focus();
- } else {
- sections.appendChild(div);
- cm = setupCodeMirror(div, code);
- }
- div.CodeMirror = cm;
- setCleanSection(div);
- return div;
-}
-
-function removeAppliesTo(event) {
- const appliesTo = event.target.parentNode;
- const appliesToList = appliesTo.parentNode;
- removeAreaAndSetDirty(appliesTo);
- if (!appliesToList.hasChildNodes()) {
- addAppliesTo(appliesToList);
- }
-}
-
-function removeSection(event) {
- const section = getSectionForChild(event.target);
- const cm = section.CodeMirror;
- removeAreaAndSetDirty(section);
- editors.splice(editors.indexOf(cm), 1);
- renderLintReport();
-}
-
-function removeAreaAndSetDirty(area) {
- const contributors = $$('.style-contributor', area);
- if (!contributors.length) {
- setCleanItem(area, false);
- }
- contributors.some(node => {
- if (node.savedValue) {
- // it's a saved section, so make it dirty and stop the enumeration
- setCleanItem(area, false);
- return true;
- } else {
- // it's an empty section, so undirty the applies-to items,
- // otherwise orphaned ids would keep the style dirty
- setCleanItem(node, true);
- }
- });
- updateTitle();
- area.parentNode.removeChild(area);
-}
-
-function makeSectionVisible(cm) {
- const section = cm.getSection();
- 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 setupGlobalSearch() {
- const originalCommand = {
- find: CodeMirror.commands.find,
- findNext: CodeMirror.commands.findNext,
- findPrev: CodeMirror.commands.findPrev,
- replace: CodeMirror.commands.replace
- };
- const originalOpenDialog = CodeMirror.prototype.openDialog;
- const originalOpenConfirm = CodeMirror.prototype.openConfirm;
-
- // cm.state.search for last used 'find'
- let curState;
-
- function shouldIgnoreCase(query) {
- // treat all-lowercase non-regexp queries as case-insensitive
- return typeof query === 'string' && query === query.toLowerCase();
- }
-
- function updateState(cm, newState) {
- if (!newState) {
- if (cm.state.search) {
- return cm.state.search;
- }
- if (!curState) {
- return null;
- }
- newState = curState;
- }
- cm.state.search = {
- query: newState.query,
- overlay: newState.overlay,
- annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
- };
- cm.addOverlay(newState.overlay);
- return cm.state.search;
- }
-
- // overrides the original openDialog with a clone of the provided template
- function customizeOpenDialog(cm, template, callback) {
- cm.openDialog = (tmpl, cb, opt) => {
- // invoke 'callback' and bind 'this' to the original callback
- originalOpenDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt);
- };
- setTimeout(() => { cm.openDialog = originalOpenDialog; }, 0);
- refocusMinidialog(cm);
- }
-
- function focusClosestCM(activeCM) {
- editors.lastActive = activeCM;
- const cm = getEditorInSight();
- if (cm !== activeCM) {
- cm.focus();
- }
- return cm;
- }
-
- function find(activeCM) {
- activeCM = focusClosestCM(activeCM);
- customizeOpenDialog(activeCM, template.find, function (query) {
- this(query);
- curState = activeCM.state.search;
- if (editors.length === 1 || !curState.query) {
- return;
- }
- editors.forEach(cm => {
- if (cm !== activeCM) {
- cm.execCommand('clearSearch');
- updateState(cm, curState);
- }
- });
- if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) === 0) {
- findNext(activeCM);
- }
- });
- originalCommand.find(activeCM);
- }
-
- function findNext(activeCM, reverse) {
- let state = updateState(activeCM);
- if (!state || !state.query) {
- find(activeCM);
- return;
- }
- let pos = activeCM.getCursor(reverse ? 'from' : 'to');
- // clear the selection, don't move the cursor
- activeCM.setSelection(activeCM.getCursor());
-
- const rxQuery = typeof state.query === 'object'
- ? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : '');
-
- if (
- document.activeElement &&
- document.activeElement.name === 'applies-value' &&
- searchAppliesTo(activeCM)
- ) {
- return;
- }
- let cm = activeCM;
- for (let i = 0; i < editors.length; i++) {
- state = updateState(cm);
- if (!cm.hasFocus()) {
- pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
- }
- const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query));
- if (searchCursor.find(reverse)) {
- if (editors.length > 1) {
- makeSectionVisible(cm);
- cm.focus();
- }
- // speedup the original findNext
- state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
- state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch);
- originalCommand[reverse ? 'findPrev' : 'findNext'](cm);
- return;
- } else if (!reverse && searchAppliesTo(cm)) {
- return;
- }
- cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length];
- if (reverse && searchAppliesTo(cm)) {
- return;
- }
- }
- // nothing found so far, so call the original search with wrap-around
- originalCommand[reverse ? 'findPrev' : 'findNext'](activeCM);
-
- function searchAppliesTo(cm) {
- let inputs = $$('.applies-value', cm.getSection());
- if (reverse) {
- inputs = inputs.reverse();
- }
- inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
- return inputs.some(input => {
- const match = rxQuery.exec(input.value);
- if (match) {
- input.focus();
- const end = match.index + match[0].length;
- // scroll selected part into view in long inputs,
- // works only outside of current event handlers chain, hence timeout=0
- setTimeout(() => {
- input.setSelectionRange(end, end);
- input.setSelectionRange(match.index, end);
- }, 0);
- return true;
- }
- });
- }
- }
-
- function findPrev(cm) {
- findNext(cm, true);
- }
-
- function replace(activeCM, all) {
- let queue;
- let query;
- let replacement;
- activeCM = focusClosestCM(activeCM);
- customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], function (txt) {
- query = txt;
- customizeOpenDialog(activeCM, template.replaceWith, function (txt) {
- replacement = txt;
- queue = editors.rotate(-editors.indexOf(activeCM));
- if (all) {
- editors.forEach(doReplace);
- } else {
- doReplace();
- }
- });
- this(query);
- });
- originalCommand.replace(activeCM, all);
-
- function doReplace() {
- const cm = queue.shift();
- if (!cm) {
- if (!all) {
- editors.lastActive.focus();
- }
- return;
- }
- // hide the first two dialogs (replace, replaceWith)
- cm.openDialog = (tmpl, callback) => {
- cm.openDialog = (tmpl, callback) => {
- cm.openDialog = originalOpenDialog;
- if (all) {
- callback(replacement);
- } else {
- doConfirm(cm);
- callback(replacement);
- if (!$('.CodeMirror-dialog', cm.getWrapperElement())) {
- // no dialog == nothing found in the current CM, move to the next
- doReplace();
- }
- }
- };
- callback(query);
- };
- originalCommand.replace(cm, all);
- }
- function doConfirm(cm) {
- let wrapAround = false;
- const origPos = cm.getCursor();
- cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
- const ovrCallbacks = callbacks.map(callback => () => {
- makeSectionVisible(cm);
- cm.openConfirm = overrideConfirm;
- setTimeout(() => { cm.openConfirm = originalOpenConfirm; }, 0);
-
- const pos = cm.getCursor();
- callback();
- const cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
- wrapAround |= cmp <= 0;
-
- const dlg = $('.CodeMirror-dialog', cm.getWrapperElement());
- if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
- $.remove(dlg);
- doReplace();
- }
- });
- originalOpenConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt);
- };
- }
- }
-
- function replaceAll(cm) {
- replace(cm, true);
- }
-
- CodeMirror.commands.find = find;
- CodeMirror.commands.findNext = findNext;
- CodeMirror.commands.findPrev = findPrev;
- CodeMirror.commands.replace = replace;
- CodeMirror.commands.replaceAll = replaceAll;
-}
-
-function jumpToLine(cm) {
- const cur = cm.getCursor();
- refocusMinidialog(cm);
- cm.openDialog(template.jumpToLine.cloneNode(true), str => {
- const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
- if (m) {
- cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
- }
- }, {value: cur.line + 1});
-}
-
function toggleStyle() {
- if (editor) {
- editor.toggleStyle();
- } else {
- toggleSectionStyle();
- }
-}
-
-function toggleSectionStyle() {
$('#enabled').checked = !$('#enabled').checked;
save();
}
-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 sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight;
- cm.state.toggleHeightSaved = wrapper.clientHeight;
- cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
- const bounds = cm.getSection().getBoundingClientRect();
- if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
- window.scrollBy(0, bounds.top);
- }
- }
-}
-
-function setupAutocomplete(cm, enable = true) {
- const onOff = enable ? 'on' : 'off';
- cm[onOff]('changes', autocompleteOnTyping);
- cm[onOff]('pick', autocompletePicked);
-}
-
-function autocompleteOnTyping(cm, [info], debounced) {
- if (
- cm.state.completionActive ||
- info.origin && !info.origin.includes('input') ||
- !info.text.last
- ) {
+function save() {
+ updateLintReportIfEnabled(null, 0);
+ if (!validate()) {
return;
}
- if (cm.state.autocompletePicked) {
- cm.state.autocompletePicked = false;
- return;
- }
- if (!debounced) {
- debounce(autocompleteOnTyping, 100, cm, [info], true);
- return;
- }
- if (info.text.last.match(/[-a-z!]+$/i)) {
- cm.state.autocompletePicked = false;
- cm.options.hintOptions.completeSingle = false;
- cm.execCommand('autocomplete');
- setTimeout(() => {
- cm.options.hintOptions.completeSingle = true;
- });
- }
-}
-function autocompletePicked(cm) {
- cm.state.autocompletePicked = true;
-}
-
-function refocusMinidialog(cm) {
- const section = cm.getSection();
- if (!$('.CodeMirror-dialog', section)) {
- return;
- }
- // close the currently opened minidialog
- cm.focus();
- // make sure to focus the input in newly opened minidialog
- setTimeout(() => {
- $('.CodeMirror-dialog', section).focus();
- }, 0);
-}
-
-function nextPrevEditor(cm, direction) {
- cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
- makeSectionVisible(cm);
- cm.focus();
- return cm;
-}
-
-function nextPrevEditorOnKeydown(cm, event) {
- const key = event.which;
- if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
- return;
- }
- const {line, ch} = cm.getCursor();
- switch (key) {
- case 37:
- // arrow Left
- if (line || ch) {
- return;
- }
- // fallthrough to arrow Up
- case 38:
- // arrow Up
- if (line > 0 || cm === editors[0]) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- cm = nextPrevEditor(cm, -1);
- cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
- break;
- case 39:
- // arrow Right
- if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
- return;
- }
- // fallthrough to arrow Down
- case 40:
- // arrow Down
- if (line < cm.doc.size - 1 || cm === editors.last) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- cm = nextPrevEditor(cm, 1);
- cm.setCursor(0, 0);
- break;
- }
- const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
- if (animation) {
- animation.playbackRate = -1;
- animation.currentTime = 2000;
- animation.play();
- }
-}
-
-function getEditorInSight(nearbyElement) {
- // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
- let cm;
- if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) {
- cm = getSectionForChild(nearbyElement).CodeMirror;
- } else {
- cm = editors.lastActive;
- }
- // closest editor should have at least 2 lines visible
- const lineHeight = editors[0].defaultTextHeight();
- const scrollY = window.scrollY;
- const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
- const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
- const distances = [];
- const alreadyInView = cm && offscreenDistance(null, cm) === 0;
- return alreadyInView ? cm : findClosest();
-
- function offscreenDistance(index, cm) {
- if (index >= 0 && distances[index] !== undefined) {
- return distances[index];
+ saveStyleSafe({
+ id: styleId,
+ name: name,
+ enabled: $('#enabled').checked,
+ reason: 'editSave',
+ sections: getSectionsHashes()
+ })
+ .then(style => {
+ styleId = style.id;
+ sessionStorage.justEditedStyleId = styleId;
+ setCleanGlobal();
+ // Go from new style URL to edit style URL
+ if (location.href.indexOf('id=') === -1) {
+ history.replaceState({}, document.title, 'edit.html?id=' + style.id);
+ $('#heading').textContent = t('editStyleHeading');
}
- const section = (cm || editors[index]).getSection();
- const top = allSectionsContainerTop + section.offsetTop;
- if (top < scrollY + lineHeight) {
- return Math.max(0, scrollY - top - lineHeight);
- }
- if (top < windowBottom) {
- return 0;
- }
- const distance = top - windowBottom + section.offsetHeight;
- if (index >= 0) {
- distances[index] = distance;
- }
- return distance;
- }
-
- function findClosest() {
- const last = editors.length - 1;
- let a = 0;
- let b = last;
- let c;
- let cm, distance;
- while (a < b - 1) {
- c = (a + b) / 2 | 0;
- distance = offscreenDistance(c);
- if (!distance || !c) {
- break;
- }
- const distancePrev = offscreenDistance(c - 1);
- const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
- if (distancePrev <= distance && distance <= distanceNext) {
- b = c;
- } else {
- a = c;
- }
- }
- while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
- b--;
- }
- cm = editors[b];
- if (distances[b] > 0) {
- makeSectionVisible(cm);
- }
- return cm;
- }
-}
-
-function beautify(event) {
- loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
- .then(() => {
- if (!window.css_beautify && window.exports) {
- window.css_beautify = window.exports.css_beautify;
- }
- })
- .then(doBeautify);
-
- function doBeautify() {
- const tabs = prefs.get('editor.indentWithTabs');
- const options = prefs.get('editor.beautify');
- options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
- options.indent_char = tabs ? '\t' : ' ';
-
- const section = getSectionForChild(event.target);
- const scope = section ? [section.CodeMirror] : editors;
-
- showHelp(t('styleBeautify'), '' +
- optionHtml('.selector1,', 'selector_separator_newline') +
- optionHtml('.selector2,', 'newline_before_open_brace') +
- optionHtml('{', 'newline_after_open_brace') +
- optionHtml('border: none;', 'newline_between_properties', true) +
- optionHtml('display: block;', 'newline_before_close_brace', true) +
- optionHtml('}', 'newline_between_rules') +
- `' +
- '
' +
- '');
-
- const undoButton = $('#help-popup button[role="undo"]');
- undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal');
- undoButton.addEventListener('click', () => {
- let undoable = false;
- scope.forEach(cm => {
- if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
- delete cm.beautifyChange[cm.changeGeneration()];
- cm.undo();
- cm.scrollIntoView(cm.getCursor());
- undoable |= cm.beautifyChange[cm.changeGeneration()];
- }
- });
- undoButton.disabled = !undoable;
- });
-
- scope.forEach(cm => {
- setTimeout(() => {
- const pos = options.translate_positions =
- [].concat.apply([], cm.doc.sel.ranges.map(r =>
- [Object.assign({}, r.anchor), Object.assign({}, r.head)]));
- const text = cm.getValue();
- const newText = css_beautify(text, options);
- if (newText !== text) {
- if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
- // clear the list if last change wasn't a css-beautify
- cm.beautifyChange = {};
- }
- cm.setValue(newText);
- const selections = [];
- for (let i = 0; i < pos.length; i += 2) {
- selections.push({anchor: pos[i], head: pos[i + 1]});
- }
- cm.setSelections(selections);
- cm.beautifyChange[cm.changeGeneration()] = true;
- undoButton.disabled = false;
- }
- }, 0);
- });
-
- $('.beautify-options').onchange = ({target}) => {
- const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
- prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
- if (target.parentNode.hasAttribute('newline')) {
- target.parentNode.setAttribute('newline', value.toString());
- }
- doBeautify();
- };
-
- function optionHtml(label, optionName, indent) {
- const value = options[optionName];
- return '' +
- '' + label + '' +
- '
';
- }
- }
-}
-
-function initStyleData() {
- // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
- const params = new URLSearchParams(location.search.replace(/^\?/, ''));
- const id = params.get('id');
- const createEmptyStyle = () => ({
- id: null,
- name: '',
- enabled: true,
- sections: [
- Object.assign({code: ''},
- ...Object.keys(CssToProperty)
- .map(name => ({
- [CssToProperty[name]]: params.get(name) && [params.get(name)] || []
- }))
- )
- ],
+ updateTitle();
});
- return !id ?
- Promise.resolve(createEmptyStyle()) :
- getStylesSafe({id}).then(([style]) => style || createEmptyStyle());
}
-function setStyleMeta(style) {
- $('#name').value = style.name || '';
- $('#enabled').checked = style.enabled !== false;
- $('#url').href = style.url || '';
-}
-
-function isUsercss(style) {
- return (
- style.usercssData ||
- !style.id && prefs.get('newStyleAsUsercss')
- );
-}
-
-function initWithSectionStyle({style, codeIsUpdated}) {
- setStyleMeta(style);
- if (codeIsUpdated !== false) {
- editors.length = 0;
- getSections().forEach(div => div.remove());
- addSections(style.sections.length ? style.sections : [{code: ''}]);
- initHooks();
+function validate() {
+ const name = $('#name').value.trim();
+ if (!name) {
+ $('#name').focus();
+ messageBox.alert(t('styleMissingName'));
+ return false;
}
- setCleanGlobal();
- updateTitle();
-}
-function addSections(sections, onAdded = () => {}) {
- if (addSections.running) {
- console.error('addSections cannot be re-entered: please report to the developers');
- // TODO: handle this properly e.g. on update/import
- return;
- }
- addSections.running = true;
- maximizeCodeHeight.stats = null;
- // make a shallow copy since we might run asynchronously
- // and the original array might get modified
- sections = sections.slice();
- const t0 = performance.now();
- const divs = [];
- let index = 0;
-
- return new Promise(function run(resolve) {
- while (index < sections.length) {
- const div = addSection(null, sections[index]);
- maximizeCodeHeight(div, index === sections.length - 1);
- onAdded(div, index);
- divs.push(div);
- maybeFocusFirstCM();
- index++;
- const elapsed = performance.now() - t0;
- if (elapsed > 500) {
- setGlobalProgress(index, sections.length);
+ if ($$('.applies-to-list li:not(.applies-to-everything)')
+ .some(li => {
+ const type = $('[name=applies-type]', li).value;
+ const value = $('[name=applies-value]', li);
+ const rx = value.value.trim();
+ if (type === 'regexp' && rx && !tryRegExp(rx)) {
+ value.focus();
+ value.select();
+ return true;
}
- if (elapsed > 100) {
- // after 100ms the sections are added asynchronously
- setTimeout(run, 0, resolve);
- return;
- }
- }
- editors.last.state.renderLintReportNow = true;
- addSections.running = false;
- setGlobalProgress();
- resolve(divs);
- });
-
- function maybeFocusFirstCM() {
- const isPageLocked = document.documentElement.style.pointerEvents;
- if (divs[0] && (isPageLocked ? divs.length === sections.length : index === 0)) {
- makeSectionVisible(divs[0].CodeMirror);
- divs[0].CodeMirror.focus();
- }
- }
-}
-
-function initHooks() {
- if (initHooks.alreadyDone) {
- return;
- }
- initHooks.alreadyDone = true;
- $$('#header .style-contributor').forEach(node => {
- node.addEventListener('change', onChange);
- node.addEventListener('input', onChange);
- });
- $('#toggle-style-help').addEventListener('click', showToggleStyleHelp);
- $('#to-mozilla').addEventListener('click', showMozillaFormat, false);
- $('#to-mozilla-help').addEventListener('click', showToMozillaHelp, false);
- $('#from-mozilla').addEventListener('click', fromMozillaFormat);
- $('#beautify').addEventListener('click', beautify);
- $('#save-button').addEventListener('click', save, false);
- $('#sections-help').addEventListener('click', showSectionHelp, false);
- $('#keyMap-help').addEventListener('click', showKeyMapHelp, false);
- $('#cancel-button').addEventListener('click', goBackToManage);
-
- initLint();
-
- if (!FIREFOX) {
- $$([
- 'input:not([type])',
- 'input[type="text"]',
- 'input[type="search"]',
- 'input[type="number"]',
- ].join(',')
- ).forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
+ })) {
+ messageBox.alert(t('styleBadRegexp'));
+ return false;
}
- setupGlobalSearch();
-}
-
-// common for usercss and classic
-function initHooksCommon() {
- showKeyInSaveButtonTooltip();
- prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
- window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
-
- function showKeyInSaveButtonTooltip(prefName, value) {
- $('#save-button').title = findKeyForCommand('save', value);
- }
- function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
- const map = CodeMirror.keyMap[mapName];
- let key = Object.keys(map).find(k => map[k] === command);
- if (key) {
- return key;
- }
- for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
- key = ft && findKeyForCommand(command, ft);
- if (key) {
- return key;
- }
- }
- return '';
- }
-}
-
-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);
- }
-}
-
-
-function maximizeCodeHeight(sectionDiv, isLast) {
- const cm = sectionDiv.CodeMirror;
- const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
- if (!stats.cmActualHeight) {
- stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
- }
- if (!stats.sectionMarginTop) {
- stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
- }
- const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
- if (!stats.firstSectionTop) {
- stats.firstSectionTop = sectionTop;
- }
- const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
- const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
- const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight();
- const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
- stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
- stats.totalHeight += cmGrantableHeight + extrasHeight;
- if (!isLast) {
- return;
- }
- stats.totalHeight += stats.firstSectionTop;
- if (stats.totalHeight <= window.innerHeight) {
- editors.forEach((cm, index) => {
- cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
- });
- return;
- }
- // scale heights to fill the gap between last section and bottom edge of the window
- const sections = $('#sections');
- const available = window.innerHeight - sections.getBoundingClientRect().bottom -
- parseFloat(getComputedStyle(sections).marginBottom);
- if (available <= 0) {
- return;
- }
- const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0);
- const q = available / totalDelta;
- const baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
- stats.deltas.forEach((delta, index) => {
- editors[index].setSize(null, baseHeight + Math.floor(q * delta));
- });
+ return true;
}
function updateTitle() {
const DIRTY_TITLE = '* $';
-
const name = $('#name').savedValue;
const clean = isCleanGlobal();
const title = styleId === null ? t('addStyleTitle') : t('editStyleTitle', [name]);
document.title = clean ? title : DIRTY_TITLE.replace('$', title);
}
-function validate() {
- const name = $('#name').value;
- if (name === '') {
- $('#name').focus();
- return t('styleMissingName');
- }
- // validate the regexps
- if ($$('.applies-to-list').some(list => {
- list.childNodes.some(li => {
- if (li.className === template.appliesToEverything.className) {
- return false;
- }
- const valueElement = $('[name=applies-value]', li);
- const type = $('[name=applies-type]', li).value;
- const value = valueElement.value;
- if (type && value) {
- if (type === 'regexp') {
- try {
- new RegExp(value);
- } catch (ex) {
- valueElement.focus();
- return true;
- }
- }
- }
- return false;
- });
- })) {
- return t('styleBadRegexp');
- }
- return null;
-}
-
function updateLintReportIfEnabled(...args) {
if (CodeMirror.defaults.lint) {
updateLintReport(...args);
}
}
-function save() {
- if (editor) {
- editor.save();
- } else {
- saveSectionStyle();
- }
-}
-
-function saveSectionStyle() {
- updateLintReportIfEnabled(null, 0);
-
- const error = validate();
- if (error) {
- alert(error);
- return;
- }
- const name = $('#name').value;
- const enabled = $('#enabled').checked;
- saveStyleSafe({
- id: styleId,
- name: name,
- enabled: enabled,
- reason: 'editSave',
- sections: getSectionsHashes()
- })
- .then(saveComplete);
-}
-
-function getSectionsHashes() {
- const sections = [];
- getSections().forEach(div => {
- const meta = getMeta(div);
- const code = div.CodeMirror.getValue();
- if (/^\s*$/.test(code) && Object.keys(meta).length === 0) {
- return;
- }
- meta.code = code;
- sections.push(meta);
- });
- return sections;
-}
-
-function getMeta(e) {
- const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
- $('.applies-to-list', e).childNodes.forEach(li => {
- if (li.className === template.appliesToEverything.className) {
- return;
- }
- const type = $('[name=applies-type]', li).value;
- const value = $('[name=applies-value]', li).value;
- if (type && value) {
- const property = CssToProperty[type];
- meta[property].push(value);
- }
- });
- return meta;
-}
-
-function saveComplete(style) {
- styleId = style.id;
- sessionStorage.justEditedStyleId = styleId;
- setCleanGlobal();
-
- // Go from new style URL to edit style URL
- if (location.href.indexOf('id=') === -1) {
- history.replaceState({}, document.title, 'edit.html?id=' + style.id);
- $('#heading').textContent = t('editStyleHeading');
- }
- updateTitle();
-}
-
function showMozillaFormat() {
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
popup.codebox.setValue(toMozillaFormat());
@@ -1769,107 +545,6 @@ function showToggleStyleHelp() {
showHelp(t('helpAlt'), t('styleEnabledToggleHint'));
}
-function showKeyMapHelp() {
- const keyMap = mergeKeyMaps({}, prefs.get('editor.keyMap'), CodeMirror.defaults.extraKeys);
- const keyMapSorted = Object.keys(keyMap)
- .map(key => ({key: key, cmd: keyMap[key]}))
- .concat([{key: 'Shift-Ctrl-Wheel', cmd: 'scrollWindow'}])
- .sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1));
- showHelp(t('cm_keyMap') + ': ' + prefs.get('editor.keyMap'),
- '');
-
- const table = $('#help-popup table');
- table.addEventListener('input', filterTable);
-
- const inputs = $$('input', table);
- inputs[0].addEventListener('keydown', hotkeyHandler);
- inputs[1].focus();
-
- function hotkeyHandler(event) {
- const keyName = CodeMirror.keyName(event);
- if (keyName === 'Esc' || keyName === 'Tab' || keyName === 'Shift-Tab') {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- // normalize order of modifiers,
- // for modifier-only keys ('Ctrl-Shift') a dummy main key has to be temporarily added
- const keyMap = {};
- keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, '$&-dummy')] = '';
- const normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0];
- this.value = normalizedKey.replace('-dummy', '');
- filterTable(event);
- }
-
- function filterTable(event) {
- const input = event.target;
- const col = input.parentNode.cellIndex;
- inputs[1 - col].value = '';
- table.tBodies[0].childNodes.forEach(row => {
- const cell = row.children[col];
- const text = cell.textContent;
- const query = stringAsRegExp(input.value, 'gi');
- const test = query.test(text);
- row.style.display = input.value && test === false ? 'none' : '';
- if (input.value && test) {
- cell.textContent = '';
- let offset = 0;
- text.replace(query, (match, index) => {
- if (index > offset) {
- cell.appendChild(document.createTextNode(text.substring(offset, index)));
- }
- cell.appendChild($element({tag: 'mark', textContent: match}));
- offset = index + match.length;
- });
- if (offset + 1 !== text.length) {
- cell.appendChild(document.createTextNode(text.substring(offset)));
- }
- }
- else {
- cell.textContent = text;
- }
- // clear highlight from the other column
- const otherCell = row.children[1 - col];
- if (otherCell.children.length) {
- const text = otherCell.textContent;
- otherCell.textContent = text;
- }
- });
- }
- function mergeKeyMaps(merged, ...more) {
- more.forEach(keyMap => {
- if (typeof keyMap === 'string') {
- keyMap = CodeMirror.keyMap[keyMap];
- }
- Object.keys(keyMap).forEach(key => {
- let cmd = keyMap[key];
- // filter out '...', 'attach', etc. (hotkeys start with an uppercase letter)
- if (!merged[key] && !key.match(/^[a-z]/) && cmd !== '...') {
- if (typeof cmd === 'function') {
- // for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body)
- // for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism
- cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, '$1');
- merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + '...';
- } else {
- merged[key] = cmd;
- }
- }
- });
- if (keyMap.fallthrough) {
- merged = mergeKeyMaps(merged, keyMap.fallthrough);
- }
- });
- return merged;
- }
-}
-
function showHelp(title = '', body) {
const div = $('#help-popup');
div.classList.remove('big');
@@ -1911,7 +586,7 @@ function showCodeMirrorPopup(title, html, options) {
const popup = showHelp(title, html);
popup.classList.add('big');
- popup.codebox = CodeMirror($('.contents', popup), Object.assign({
+ const cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
mode: 'css',
lineNumbers: true,
lineWrapping: true,
@@ -1923,131 +598,12 @@ function showCodeMirrorPopup(title, html, options) {
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap')
}, options));
- popup.codebox.focus();
- popup.codebox.on('focus', () => { hotkeyRerouter.setState(false); });
- popup.codebox.on('blur', () => { hotkeyRerouter.setState(true); });
+ cm.focus();
+ cm.on('focus', () => cm.rerouteHotkeys(false));
+ cm.on('blur', () => cm.rerouteHotkeys(true));
return popup;
}
-chrome.runtime.onMessage.addListener(onRuntimeMessage);
-
-function onRuntimeMessage(request) {
- switch (request.method) {
- case 'styleUpdated':
- if (styleId && styleId === request.style.id &&
- request.reason !== 'editSave' &&
- request.reason !== 'config') {
- // code-less style from notifyAllTabs
- if ((request.style.sections[0] || {}).code === null) {
- request.style = BG.cachedStyles.byId.get(request.style.id);
- }
- if (isUsercss(request.style)) {
- editor.replaceStyle(request.style, request.codeIsUpdated);
- } else {
- initWithSectionStyle(request);
- }
- }
- break;
- case 'styleDeleted':
- if (styleId === request.id || editor && editor.getStyle().id === request.id) {
- window.onbeforeunload = () => {};
- closeCurrentTab();
- break;
- }
- break;
- case 'prefChanged':
- if ('editor.smartIndent' in request.prefs) {
- CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
- }
- break;
- case 'editDeleteText':
- document.execCommand('delete');
- break;
- }
-}
-
-function getComputedHeight(el) {
- const compStyle = getComputedStyle(el);
- return el.getBoundingClientRect().height +
- parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
-}
-
-
-function getCodeMirrorThemes() {
- if (!chrome.runtime.getPackageDirectoryEntry) {
- const themes = [
- chrome.i18n.getMessage('defaultTheme'),
- '3024-day',
- '3024-night',
- 'abcdef',
- 'ambiance',
- 'ambiance-mobile',
- 'base16-dark',
- 'base16-light',
- 'bespin',
- 'blackboard',
- 'cobalt',
- 'colorforth',
- 'dracula',
- 'duotone-dark',
- 'duotone-light',
- 'eclipse',
- 'elegant',
- 'erlang-dark',
- 'hopscotch',
- 'icecoder',
- 'isotope',
- 'lesser-dark',
- 'liquibyte',
- 'material',
- 'mbo',
- 'mdn-like',
- 'midnight',
- 'monokai',
- 'neat',
- 'neo',
- 'night',
- 'panda-syntax',
- 'paraiso-dark',
- 'paraiso-light',
- 'pastel-on-dark',
- 'railscasts',
- 'rubyblue',
- 'seti',
- 'solarized',
- 'the-matrix',
- 'tomorrow-night-bright',
- 'tomorrow-night-eighties',
- 'ttcn',
- 'twilight',
- 'vibrant-ink',
- 'xq-dark',
- 'xq-light',
- 'yeti',
- 'zenburn',
- ];
- localStorage.codeMirrorThemes = themes.join(' ');
- return Promise.resolve(themes);
- }
- return new Promise(resolve => {
- chrome.runtime.getPackageDirectoryEntry(rootDir => {
- rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
- themeDir.createReader().readEntries(entries => {
- const themes = [
- chrome.i18n.getMessage('defaultTheme')
- ].concat(
- entries.filter(entry => entry.isFile)
- .sort((a, b) => (a.name < b.name ? -1 : 1))
- .map(entry => entry.name.replace(/\.css$/, ''))
- );
- localStorage.codeMirrorThemes = themes.join(' ');
- resolve(themes);
- });
- });
- });
- });
-}
-
function setGlobalProgress(done, total) {
const progressElement = $('#global-progress') ||
total && document.body.appendChild($element({id: 'global-progress'}));
@@ -2061,3 +617,61 @@ function setGlobalProgress(done, total) {
$.remove(progressElement);
}
}
+
+function scrollEntirePageOnCtrlShift(event) {
+ // make Shift-Ctrl-Wheel scroll entire page even when mouse is over a code editor
+ if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
+ // Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
+ window.scrollBy(0, event.deltaX || event.deltaY);
+ event.preventDefault();
+ }
+}
+
+function hideLintHeaderOnScroll() {
+ // workaround part2 for the not showing its toggle icon: hide on scroll
+ const newOpacity = this.scrollTop === 0 ? '' : '0';
+ const style = this.firstElementChild.style;
+ if (style.opacity !== newOpacity) {
+ style.opacity = newOpacity;
+ }
+}
+
+function rememberWindowSize() {
+ if (
+ document.visibilityState === 'visible' &&
+ prefs.get('openEditInWindow') &&
+ !isWindowMaximized()
+ ) {
+ prefs.set('windowPosition', {
+ left: window.screenX,
+ top: window.screenY,
+ width: window.outerWidth,
+ height: window.outerHeight,
+ });
+ }
+}
+
+function isWindowMaximized() {
+ return (
+ window.screenX <= 0 &&
+ window.screenY <= 0 &&
+ window.outerWidth >= screen.availWidth &&
+ window.outerHeight >= screen.availHeight &&
+
+ window.screenX > -10 &&
+ window.screenY > -10 &&
+ window.outerWidth < screen.availWidth + 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);
+ }
+}
diff --git a/edit/lint.js b/edit/lint.js
index abed7661..f39ec685 100644
--- a/edit/lint.js
+++ b/edit/lint.js
@@ -1,5 +1,5 @@
/* global CodeMirror messageBox */
-/* global editors makeSectionVisible showCodeMirrorPopup showHelp hotkeyRerouter */
+/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
/* global loadScript require CSSLint stylelint */
/* global makeLink */
'use strict';
@@ -449,10 +449,10 @@ function setupLinterPopup(config) {
cm.on('changes', updateButtonState);
updateButtonState();
- hotkeyRerouter.setState(false);
+ cm.rerouteHotkeys(false);
window.addEventListener('closeHelp', function _() {
window.removeEventListener('closeHelp', _);
- hotkeyRerouter.setState(true);
+ cm.rerouteHotkeys(true);
});
loadScript([
diff --git a/edit/sections.js b/edit/sections.js
new file mode 100644
index 00000000..696c3fea
--- /dev/null
+++ b/edit/sections.js
@@ -0,0 +1,515 @@
+/*
+global CodeMirror
+global editors propertyToCss CssToProperty
+global onChange indicateCodeChange initHooks setCleanGlobal
+global fromMozillaFormat maximizeCodeHeight toggleContextMenuDelete
+global setCleanItem updateTitle updateLintReportIfEnabled renderLintReport
+global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
+*/
+'use strict';
+
+function initWithSectionStyle({style, codeIsUpdated}) {
+ $('#name').value = style.name || '';
+ $('#enabled').checked = style.enabled !== false;
+ $('#url').href = style.url || '';
+ if (codeIsUpdated !== false) {
+ editors.length = 0;
+ getSections().forEach(div => div.remove());
+ addSections(style.sections.length ? style.sections : [{code: ''}]);
+ initHooks();
+ }
+ setCleanGlobal();
+ updateTitle();
+}
+
+function addSections(sections, onAdded = () => {}) {
+ if (addSections.running) {
+ console.error('addSections cannot be re-entered: please report to the developers');
+ // TODO: handle this properly e.g. on update/import
+ return;
+ }
+ addSections.running = true;
+ maximizeCodeHeight.stats = null;
+ // make a shallow copy since we might run asynchronously
+ // and the original array might get modified
+ sections = sections.slice();
+ const t0 = performance.now();
+ const divs = [];
+ let index = 0;
+
+ return new Promise(function run(resolve) {
+ while (index < sections.length) {
+ const div = addSection(null, sections[index]);
+ maximizeCodeHeight(div, index === sections.length - 1);
+ onAdded(div, index);
+ divs.push(div);
+ maybeFocusFirstCM();
+ index++;
+ const elapsed = performance.now() - t0;
+ if (elapsed > 500) {
+ setGlobalProgress(index, sections.length);
+ }
+ if (elapsed > 100) {
+ // after 100ms the sections are added asynchronously
+ setTimeout(run, 0, resolve);
+ return;
+ }
+ }
+ editors.last.state.renderLintReportNow = true;
+ addSections.running = false;
+ setGlobalProgress();
+ resolve(divs);
+ });
+
+ function maybeFocusFirstCM() {
+ const isPageLocked = document.documentElement.style.pointerEvents;
+ if (divs[0] && (isPageLocked ? divs.length === sections.length : index === 0)) {
+ makeSectionVisible(divs[0].CodeMirror);
+ divs[0].CodeMirror.focus();
+ }
+ }
+}
+
+function addSection(event, section) {
+ const div = template.section.cloneNode(true);
+ $('.applies-to-help', div).addEventListener('click', showAppliesToHelp, false);
+ $('.remove-section', div).addEventListener('click', removeSection, false);
+ $('.add-section', div).addEventListener('click', addSection, false);
+ $('.beautify-section', div).addEventListener('click', beautify);
+
+ const code = (section || {}).code || '';
+
+ const appliesTo = $('.applies-to-list', div);
+ let appliesToAdded = false;
+
+ if (section) {
+ for (const i in propertyToCss) {
+ if (section[i]) {
+ section[i].forEach(url => {
+ addAppliesTo(appliesTo, propertyToCss[i], url);
+ appliesToAdded = true;
+ });
+ }
+ }
+ }
+ if (!appliesToAdded) {
+ addAppliesTo(appliesTo);
+ }
+
+ appliesTo.addEventListener('change', onChange);
+ appliesTo.addEventListener('input', onChange);
+
+ toggleTestRegExpVisibility();
+ appliesTo.addEventListener('change', toggleTestRegExpVisibility);
+ $('.test-regexp', div).onclick = () => {
+ regExpTester.toggle();
+ regExpTester.update(getRegExps());
+ };
+
+ function getRegExps() {
+ return [...appliesTo.children]
+ .map(item =>
+ !item.matches('.applies-to-everything') &&
+ $('.applies-type', item).value === 'regexp' &&
+ $('.applies-value', item).value.trim()
+ )
+ .filter(item => item);
+ }
+
+ function toggleTestRegExpVisibility() {
+ const show = getRegExps().length > 0;
+ div.classList.toggle('has-regexp', show);
+ appliesTo.oninput = appliesTo.oninput || show && (event => {
+ if (event.target.matches('.applies-value') &&
+ $('.applies-type', event.target.parentElement).value === 'regexp') {
+ regExpTester.update(getRegExps());
+ }
+ });
+ }
+
+ const sections = $('#sections');
+ let cm;
+ if (event) {
+ const clickedSection = getSectionForChild(event.target);
+ sections.insertBefore(div, clickedSection.nextElementSibling);
+ const newIndex = getSections().indexOf(clickedSection) + 1;
+ cm = setupCodeMirror(div, code, newIndex);
+ makeSectionVisible(cm);
+ renderLintReport();
+ cm.focus();
+ } else {
+ sections.appendChild(div);
+ cm = setupCodeMirror(div, code);
+ }
+ div.CodeMirror = cm;
+ setCleanSection(div);
+ return div;
+}
+
+function addAppliesTo(list, name, value) {
+ const showingEverything = $('.applies-to-everything', list) !== null;
+ // blow away 'Everything' if it's there
+ if (showingEverything) {
+ list.removeChild(list.firstChild);
+ }
+ let e;
+ if (name) {
+ e = template.appliesTo.cloneNode(true);
+ $('[name=applies-type]', e).value = name;
+ $('[name=applies-value]', e).value = value;
+ $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false);
+ } else if (showingEverything || list.hasChildNodes()) {
+ e = template.appliesTo.cloneNode(true);
+ if (list.hasChildNodes()) {
+ $('[name=applies-type]', e).value = $('li:last-child [name="applies-type"]', list).value;
+ }
+ $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false);
+ } else {
+ e = template.appliesToEverything.cloneNode(true);
+ }
+ $('.add-applies-to', e).addEventListener('click', function () {
+ addAppliesTo(this.parentNode.parentNode);
+ }, false);
+ list.appendChild(e);
+}
+
+function setupCodeMirror(sectionDiv, code, index) {
+ const cm = CodeMirror(wrapper => {
+ $('.code-label', sectionDiv).insertAdjacentElement('afterend', wrapper);
+ }, {
+ value: code,
+ });
+ const wrapper = cm.display.wrapper;
+
+ let onChangeTimer;
+ cm.on('changes', (cm, changes) => {
+ clearTimeout(onChangeTimer);
+ onChangeTimer = setTimeout(indicateCodeChange, 200, cm, changes);
+ });
+ if (prefs.get('editor.autocompleteOnTyping')) {
+ setupAutocomplete(cm);
+ }
+ wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
+ cm.on('blur', () => {
+ editors.lastActive = cm;
+ cm.rerouteHotkeys(true);
+ setTimeout(() => {
+ wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
+ });
+ });
+ cm.on('focus', () => {
+ cm.rerouteHotkeys(false);
+ wrapper.classList.add('CodeMirror-active');
+ });
+ cm.on('paste', (cm, event) => {
+ const text = event.clipboardData.getData('text') || '';
+ if (
+ text.includes('@-moz-document') &&
+ text.replace(/\/\*[\s\S]*?\*\//g, '')
+ .match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/)
+ ) {
+ event.preventDefault();
+ fromMozillaFormat();
+ $('#help-popup').codebox.setValue(text);
+ $('#help-popup').codebox.clearHistory();
+ $('#help-popup').codebox.markClean();
+ }
+ if (editors.length === 1) {
+ setTimeout(() => {
+ if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) {
+ maximizeCodeHeight.stats = null;
+ maximizeCodeHeight(cm.getSection(), true);
+ }
+ });
+ }
+ });
+ if (!FIREFOX) {
+ cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
+ }
+
+ wrapper.classList.add('resize-grip-enabled');
+ let lastClickTime = 0;
+ const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
+ resizeGrip.onmousedown = event => {
+ 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';
+ function resize(e) {
+ const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
+ const height = Math.max(minHeight, e.pageY - cmPageY);
+ if (height !== wrapper.clientHeight) {
+ cm.setSize(null, height);
+ }
+ }
+ document.addEventListener('mousemove', resize);
+ document.addEventListener('mouseup', function resizeStop() {
+ document.removeEventListener('mouseup', resizeStop);
+ document.removeEventListener('mousemove', resize);
+ wrapper.style.pointerEvents = '';
+ document.body.style.cursor = '';
+ });
+ };
+
+ editors.splice(index || editors.length, 0, cm);
+ return cm;
+}
+
+function indicateCodeChange(cm) {
+ const section = cm.getSection();
+ setCleanItem(section, cm.isClean(section.savedValue));
+ updateTitle();
+ updateLintReportIfEnabled(cm);
+}
+
+function setupAutocomplete(cm, enable = true) {
+ const onOff = enable ? 'on' : 'off';
+ cm[onOff]('changes', autocompleteOnTyping);
+ cm[onOff]('pick', autocompletePicked);
+}
+
+function autocompleteOnTyping(cm, [info], debounced) {
+ if (
+ cm.state.completionActive ||
+ info.origin && !info.origin.includes('input') ||
+ !info.text.last
+ ) {
+ return;
+ }
+ if (cm.state.autocompletePicked) {
+ cm.state.autocompletePicked = false;
+ return;
+ }
+ if (!debounced) {
+ debounce(autocompleteOnTyping, 100, cm, [info], true);
+ return;
+ }
+ if (info.text.last.match(/[-a-z!]+$/i)) {
+ cm.state.autocompletePicked = false;
+ cm.options.hintOptions.completeSingle = false;
+ cm.execCommand('autocomplete');
+ setTimeout(() => {
+ cm.options.hintOptions.completeSingle = true;
+ });
+ }
+}
+
+function autocompletePicked(cm) {
+ cm.state.autocompletePicked = true;
+}
+
+function nextPrevEditorOnKeydown(cm, event) {
+ const key = event.which;
+ if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
+ return;
+ }
+ const {line, ch} = cm.getCursor();
+ switch (key) {
+ case 37:
+ // arrow Left
+ if (line || ch) {
+ return;
+ }
+ // fallthrough to arrow Up
+ case 38:
+ // arrow Up
+ if (line > 0 || cm === editors[0]) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ cm = CodeMirror.commands.prevEditor.call(cm);
+ cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
+ break;
+ case 39:
+ // arrow Right
+ if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
+ return;
+ }
+ // fallthrough to arrow Down
+ case 40:
+ // arrow Down
+ if (line < cm.doc.size - 1 || cm === editors.last) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ cm = CodeMirror.commands.nextEditor.call(cm);
+ cm.setCursor(0, 0);
+ break;
+ }
+ const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
+ if (animation) {
+ animation.playbackRate = -1;
+ animation.currentTime = 2000;
+ animation.play();
+ }
+}
+
+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 sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight;
+ cm.state.toggleHeightSaved = wrapper.clientHeight;
+ cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
+ const bounds = cm.getSection().getBoundingClientRect();
+ if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
+ window.scrollBy(0, bounds.top);
+ }
+ }
+}
+
+function getSectionForChild(e) {
+ return e.closest('#sections > div');
+}
+
+function getSections() {
+ return $$('#sections > div');
+}
+
+function getSectionsHashes() {
+ const sections = [];
+ getSections().forEach(div => {
+ const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
+ for (const li of $('.applies-to-list', div).childNodes) {
+ if (li.className === template.appliesToEverything.className) {
+ return;
+ }
+ const type = $('[name=applies-type]', li).value;
+ const value = $('[name=applies-value]', li).value;
+ if (type && value) {
+ meta[CssToProperty[type]].push(value);
+ }
+ }
+ const code = div.CodeMirror.getValue();
+ if (/^\s*$/.test(code) && Object.keys(meta).length === 0) {
+ return;
+ }
+ meta.code = code;
+ sections.push(meta);
+ });
+ return sections;
+}
+
+function removeAppliesTo(event) {
+ const appliesTo = event.target.parentNode;
+ const appliesToList = appliesTo.parentNode;
+ removeAreaAndSetDirty(appliesTo);
+ if (!appliesToList.hasChildNodes()) {
+ addAppliesTo(appliesToList);
+ }
+}
+
+function removeSection(event) {
+ const section = getSectionForChild(event.target);
+ const cm = section.CodeMirror;
+ removeAreaAndSetDirty(section);
+ editors.splice(editors.indexOf(cm), 1);
+ renderLintReport();
+}
+
+function removeAreaAndSetDirty(area) {
+ const contributors = $$('.style-contributor', area);
+ if (!contributors.length) {
+ setCleanItem(area, false);
+ }
+ contributors.some(node => {
+ if (node.savedValue) {
+ // it's a saved section, so make it dirty and stop the enumeration
+ setCleanItem(area, false);
+ return true;
+ } else {
+ // it's an empty section, so undirty the applies-to items,
+ // otherwise orphaned ids would keep the style dirty
+ setCleanItem(node, true);
+ }
+ });
+ updateTitle();
+ area.parentNode.removeChild(area);
+}
+
+function makeSectionVisible(cm) {
+ const section = cm.getSection();
+ 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 maximizeCodeHeight(sectionDiv, isLast) {
+ const cm = sectionDiv.CodeMirror;
+ const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
+ if (!stats.cmActualHeight) {
+ stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
+ }
+ if (!stats.sectionMarginTop) {
+ stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
+ }
+ const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
+ if (!stats.firstSectionTop) {
+ stats.firstSectionTop = sectionTop;
+ }
+ const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
+ const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
+ const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight();
+ const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
+ stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
+ stats.totalHeight += cmGrantableHeight + extrasHeight;
+ if (!isLast) {
+ return;
+ }
+ stats.totalHeight += stats.firstSectionTop;
+ if (stats.totalHeight <= window.innerHeight) {
+ editors.forEach((cm, index) => {
+ cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
+ });
+ return;
+ }
+ // scale heights to fill the gap between last section and bottom edge of the window
+ const sections = $('#sections');
+ const available = window.innerHeight - sections.getBoundingClientRect().bottom -
+ parseFloat(getComputedStyle(sections).marginBottom);
+ if (available <= 0) {
+ return;
+ }
+ const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0);
+ const q = available / totalDelta;
+ const baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
+ stats.deltas.forEach((delta, index) => {
+ editors[index].setSize(null, baseHeight + Math.floor(q * delta));
+ });
+
+ function getComputedHeight(el) {
+ const compStyle = getComputedStyle(el);
+ return el.getBoundingClientRect().height +
+ parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
+ }
+}
diff --git a/edit/show-keymap-help.js b/edit/show-keymap-help.js
new file mode 100644
index 00000000..7326784c
--- /dev/null
+++ b/edit/show-keymap-help.js
@@ -0,0 +1,106 @@
+/* global CodeMirror showHelp */
+'use strict';
+
+onDOMready().then(() => {
+ $('#keyMap-help').addEventListener('click', showKeyMapHelp);
+});
+
+function showKeyMapHelp() {
+ const keyMap = mergeKeyMaps({}, prefs.get('editor.keyMap'), CodeMirror.defaults.extraKeys);
+ const keyMapSorted = Object.keys(keyMap)
+ .map(key => ({key: key, cmd: keyMap[key]}))
+ .concat([{key: 'Shift-Ctrl-Wheel', cmd: 'scrollWindow'}])
+ .sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1));
+ showHelp(t('cm_keyMap') + ': ' + prefs.get('editor.keyMap'),
+ '');
+
+ const table = $('#help-popup table');
+ table.addEventListener('input', filterTable);
+
+ const inputs = $$('input', table);
+ inputs[0].addEventListener('keydown', hotkeyHandler);
+ inputs[1].focus();
+
+ function hotkeyHandler(event) {
+ const keyName = CodeMirror.keyName(event);
+ if (keyName === 'Esc' || keyName === 'Tab' || keyName === 'Shift-Tab') {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ // normalize order of modifiers,
+ // for modifier-only keys ('Ctrl-Shift') a dummy main key has to be temporarily added
+ const keyMap = {};
+ keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, '$&-dummy')] = '';
+ const normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0];
+ this.value = normalizedKey.replace('-dummy', '');
+ filterTable(event);
+ }
+
+ function filterTable(event) {
+ const input = event.target;
+ const col = input.parentNode.cellIndex;
+ inputs[1 - col].value = '';
+ table.tBodies[0].childNodes.forEach(row => {
+ const cell = row.children[col];
+ const text = cell.textContent;
+ const query = stringAsRegExp(input.value, 'gi');
+ const test = query.test(text);
+ row.style.display = input.value && test === false ? 'none' : '';
+ if (input.value && test) {
+ cell.textContent = '';
+ let offset = 0;
+ text.replace(query, (match, index) => {
+ if (index > offset) {
+ cell.appendChild(document.createTextNode(text.substring(offset, index)));
+ }
+ cell.appendChild($element({tag: 'mark', textContent: match}));
+ offset = index + match.length;
+ });
+ if (offset + 1 !== text.length) {
+ cell.appendChild(document.createTextNode(text.substring(offset)));
+ }
+ } else {
+ cell.textContent = text;
+ }
+ // clear highlight from the other column
+ const otherCell = row.children[1 - col];
+ if (otherCell.children.length) {
+ const text = otherCell.textContent;
+ otherCell.textContent = text;
+ }
+ });
+ }
+ function mergeKeyMaps(merged, ...more) {
+ more.forEach(keyMap => {
+ if (typeof keyMap === 'string') {
+ keyMap = CodeMirror.keyMap[keyMap];
+ }
+ Object.keys(keyMap).forEach(key => {
+ let cmd = keyMap[key];
+ // filter out '...', 'attach', etc. (hotkeys start with an uppercase letter)
+ if (!merged[key] && !key.match(/^[a-z]/) && cmd !== '...') {
+ if (typeof cmd === 'function') {
+ // for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body)
+ // for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism
+ cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, '$1');
+ merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + '...';
+ } else {
+ merged[key] = cmd;
+ }
+ }
+ });
+ if (keyMap.fallthrough) {
+ merged = mergeKeyMaps(merged, keyMap.fallthrough);
+ }
+ });
+ return merged;
+ }
+}
diff --git a/edit/source-editor.js b/edit/source-editor.js
index b6090e32..d73373e4 100644
--- a/edit/source-editor.js
+++ b/edit/source-editor.js
@@ -1,6 +1,5 @@
-/* global CodeMirror dirtyReporter initLint beautify showKeyMapHelp */
+/* global CodeMirror dirtyReporter initLint */
/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
-/* global hotkeyRerouter setupAutocomplete */
/* global editors linterConfig updateLinter regExpTester mozParser */
/* global makeLink createAppliesToLineWidget messageBox */
'use strict';
@@ -124,10 +123,7 @@ function createSourceEditor(style) {
function initHooks() {
$('#save-button').onclick = save;
- $('#beautify').onclick = beautify;
- $('#keyMap-help').onclick = showKeyMapHelp;
$('#toggle-style-help').onclick = showToggleStyleHelp;
- $('#cancel-button').onclick = goBackToManage;
$('#enabled').onchange = function () {
const value = this.checked;
@@ -140,11 +136,13 @@ function createSourceEditor(style) {
updateLintReportIfEnabled(cm);
});
- cm.on('focus', () => hotkeyRerouter.setState(false));
- cm.on('blur', () => hotkeyRerouter.setState(true));
+ cm.on('focus', () => cm.rerouteHotkeys(false));
+ cm.on('blur', () => cm.rerouteHotkeys(true));
CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1);
CodeMirror.commands.nextEditor = cm => nextPrevMozDocument(cm, 1);
+ CodeMirror.commands.toggleStyle = toggleStyle;
+ CodeMirror.commands.save = save;
}
function updateMeta() {