diff --git a/edit.html b/edit.html
index 0d51cc3b..678a6e98 100644
--- a/edit.html
+++ b/edit.html
@@ -11,6 +11,8 @@
+
+
@@ -41,6 +43,8 @@
+
+
diff --git a/edit/edit.css b/edit/edit.css
index 5e54a2b1..a3ac2d43 100644
--- a/edit/edit.css
+++ b/edit/edit.css
@@ -496,6 +496,17 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
background-color: rgba(0, 0, 0, 0.05);
}
+/************ single editor **************/
+#sections .single-editor {
+ margin: 0;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.single-editor .CodeMirror {
+ height: 100%;
+}
+
/************ reponsive layouts ************/
@media(max-width:737px) {
#header {
diff --git a/edit/edit.js b/edit/edit.js
index fef86a84..35ebe1b7 100644
--- a/edit/edit.js
+++ b/edit/edit.js
@@ -3,7 +3,7 @@
/* global onDOMscripted */
/* global css_beautify */
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
-/* global mozParser */
+/* global mozParser createSourceEditor */
'use strict';
@@ -20,6 +20,8 @@ let useHistoryBack;
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
+let editor;
+
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
onBackgroundReady();
@@ -271,11 +273,13 @@ function initCodeMirror() {
CM.getOption = o => CodeMirror.defaults[o];
CM.setOption = (o, v) => {
CodeMirror.defaults[o] = v;
- editors.forEach(editor => {
+ $$('.CodeMirror').map(e => e.CodeMirror).forEach(editor => {
editor.setOption(o, v);
});
};
+ CM.modeURL = '/vendor/codemirror/mode/%N/%N.js';
+
CM.prototype.getSection = function () {
return this.display.wrapper.parentNode;
};
@@ -355,11 +359,9 @@ function acmeEventListener(event) {
return;
}
case 'autocompleteOnTyping':
- editors.forEach(cm => {
- const onOff = el.checked ? 'on' : 'off';
- cm[onOff]('change', autocompleteOnTyping);
- cm[onOff]('pick', autocompletePicked);
- });
+ $$('.CodeMirror')
+ .map(e => e.CodeMirror)
+ .forEach(cm => setupAutocomplete(cm, el.checked));
return;
case 'matchHighlight':
switch (value) {
@@ -384,8 +386,7 @@ function setupCodeMirror(textarea, index) {
cm.on('change', indicateCodeChange);
if (prefs.get('editor.autocompleteOnTyping')) {
- cm.on('change', autocompleteOnTyping);
- cm.on('pick', autocompletePicked);
+ setupAutocomplete(cm);
}
cm.on('blur', () => {
editors.lastActive = cm;
@@ -996,6 +997,13 @@ function jumpToLine(cm) {
}
function toggleStyle() {
+ if (!editor) {
+ return _toggleStyle();
+ }
+ editor.toggleStyle();
+}
+
+function _toggleStyle() {
$('#enabled').checked = !$('#enabled').checked;
save();
}
@@ -1021,6 +1029,12 @@ function toggleSectionHeight(cm) {
}
}
+function setupAutocomplete(cm, enable = true) {
+ const onOff = enable ? 'on' : 'off';
+ cm[onOff]('change', autocompleteOnTyping);
+ cm[onOff]('pick', autocompletePicked);
+}
+
function autocompleteOnTyping(cm, info, debounced) {
if (
cm.state.completionActive ||
@@ -1079,7 +1093,7 @@ function getEditorInSight(nearbyElement) {
cm = editors.lastActive;
}
if (!cm || offscreenDistance(cm) > 0) {
- const sorted = editors
+ const sorted = $$('#sections .CodeMirror').map(e => e.CodeMirror)
.map((cm, index) => ({cm: cm, distance: offscreenDistance(cm), index: index}))
.sort((a, b) => a.distance - b.distance || a.index - b.index);
cm = sorted[0].cm;
@@ -1120,7 +1134,7 @@ function beautify(event) {
options.indent_char = tabs ? '\t' : ' ';
const section = getSectionForChild(event.target);
- const scope = section ? [section.CodeMirror] : editors;
+ const scope = section ? [section.CodeMirror] : $$('#sections .CodeMirror').map(e => e.CodeMirror);
showHelp(t('styleBeautify'), '
' +
optionHtml('.selector1,', 'selector_separator_newline') +
@@ -1261,7 +1275,20 @@ function setStyleMeta(style) {
$('#url').href = style.url;
}
-function initWithStyle({style, codeIsUpdated}) {
+function initWithStyle({style}) {
+ // FIXME: what does codeIsUpdated do?
+ if (!style.usercss) {
+ return _initWithStyle({style});
+ }
+
+ if (editor) {
+ editor.replaceStyle(style);
+ } else {
+ editor = createSourceEditor(style);
+ }
+}
+
+function _initWithStyle({style, codeIsUpdated}) {
setStyleMeta(style);
if (codeIsUpdated === false) {
@@ -1440,6 +1467,13 @@ function updateLintReportIfEnabled(cm, time) {
}
function save() {
+ if (!editor) {
+ return _save();
+ }
+ editor.save();
+}
+
+function _save() {
updateLintReportIfEnabled(null, 0);
// save the contents of the CodeMirror editors back into the textareas
diff --git a/edit/lint.js b/edit/lint.js
index e8cc1fc9..df103833 100644
--- a/edit/lint.js
+++ b/edit/lint.js
@@ -147,7 +147,7 @@ function updateLinter({immediately} = {}) {
function updateEditors() {
CodeMirror.defaults.lint = linterConfig.getForCodeMirror(linter);
const guttersOption = prepareGuttersOption();
- editors.forEach(cm => {
+ $$('#sections .CodeMirror').map(e => e.CodeMirror).forEach(cm => {
cm.setOption('lint', CodeMirror.defaults.lint);
if (guttersOption) {
cm.setOption('guttersOption', guttersOption);
@@ -217,7 +217,7 @@ function updateLintReport(cm, delay) {
state.postponeNewIssues = delay === undefined || delay === null;
function update(cm) {
- const scope = cm ? [cm] : editors;
+ const scope = cm ? [cm] : $$('#sections .CodeMirror').map(e => e.CodeMirror);
let changed = false;
let fixedOldIssues = false;
scope.forEach(cm => {
@@ -284,7 +284,7 @@ function renderLintReport(someBlockChanged) {
const label = t('sectionCode');
const newContent = content.cloneNode(false);
let issueCount = 0;
- editors.forEach((cm, index) => {
+ $$('#sections .CodeMirror').map(e => e.CodeMirror).forEach((cm, index) => {
if (cm.state.lint && cm.state.lint.html) {
const html = '
' + label + ' ' + (index + 1) + '' + cm.state.lint.html;
const newBlock = newContent.appendChild(tHTML(html, 'table'));
diff --git a/edit/source-editor.js b/edit/source-editor.js
new file mode 100644
index 00000000..2fbc2188
--- /dev/null
+++ b/edit/source-editor.js
@@ -0,0 +1,133 @@
+/* global CodeMirror dirtyReporter initLint beautify showKeyMapHelp */
+/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
+/* global hotkeyRerouter setupAutocomplete */
+
+'use strict';
+
+function createSourceEditor(style) {
+ // draw HTML
+ $('#sections').innerHTML = '';
+ $('#name').disabled = true;
+ $('#mozilla-format-heading').parentNode.remove();
+
+ $('#sections').appendChild(tHTML(`
+
+
+
+ `));
+
+ // draw CodeMirror
+ $('#sections textarea').value = style.source;
+ const cm = CodeMirror.fromTextArea($('#sections textarea'));
+
+ // dirty reporter
+ const dirty = dirtyReporter();
+ dirty.onChange(() => {
+ const DIRTY = dirty.isDirty();
+ document.title = (DIRTY ? '* ' : '') + t('editStyleTitle', [style.name]);
+ document.body.classList.toggle('dirty', DIRTY);
+ $('#save-button').disabled = !DIRTY;
+ });
+
+ // draw metas info
+ updateMetas();
+ initHooks();
+ initLint();
+
+ function initHooks() {
+ // sidebar commands
+ $('#save-button').onclick = save;
+ $('#beautify').onclick = beautify;
+ $('#keyMap-help').onclick = showKeyMapHelp;
+ $('#toggle-style-help').onclick = showToggleStyleHelp;
+ $('#cancel-button').onclick = goBackToManage;
+
+ // enable
+ $('#enabled').onchange = e => {
+ const value = e.target.checked;
+ dirty.modify('enabled', style.enabled, value);
+ style.enabled = value;
+ };
+
+ // source
+ cm.on('change', () => {
+ const value = cm.getValue();
+ dirty.modify('source', style.source, value);
+ style.source = value;
+
+ updateLintReportIfEnabled(cm);
+ });
+
+ // hotkeyRerouter
+ cm.on('focus', () => {
+ hotkeyRerouter.setState(false);
+ });
+ cm.on('blur', () => {
+ hotkeyRerouter.setState(true);
+ });
+
+ // autocomplete
+ if (prefs.get('editor.autocompleteOnTyping')) {
+ setupAutocomplete(cm);
+ }
+ }
+
+ function updateMetas() {
+ $('#name').value = style.name;
+ $('#enabled').checked = style.enabled;
+ $('#url').href = style.url;
+ cm.setOption('mode', style.preprocessor || 'css');
+ CodeMirror.autoLoadMode(cm, style.preprocessor || 'css');
+ // beautify only works with regular CSS
+ $('#beautify').disabled = Boolean(style.preprocessor);
+ }
+
+ function replaceStyle(_style) {
+ style = _style;
+ updateMetas();
+ if (style.source !== cm.getValue()) {
+ const cursor = cm.getCursor();
+ cm.setValue(style.source);
+ cm.setCursor(cursor);
+ }
+ dirty.clear();
+ }
+
+ function toggleStyle() {
+ const value = !style.enabled;
+ dirty.modify('enabled', style.enabled, value);
+ style.enabled = value;
+ updateMetas();
+ // save when toggle enable state?
+ save();
+ }
+
+ function save() {
+ if (!dirty.isDirty()) {
+ return;
+ }
+ const req = {
+ method: 'saveUsercss',
+ reason: 'editSave',
+ id: style.id,
+ enabled: style.enabled,
+ source: style.source
+ };
+ return onBackgroundReady().then(() => BG.saveUsercss(req))
+ .then(result => {
+ if (result.status === 'error') {
+ throw new Error(result.error);
+ }
+ return result;
+ })
+ .then(({style}) => {
+ replaceStyle(style);
+ })
+ .catch(err => {
+ console.error(err);
+ alert(err);
+ });
+ }
+
+ return {replaceStyle, save, toggleStyle};
+}
diff --git a/edit/util.js b/edit/util.js
new file mode 100644
index 00000000..e76e289e
--- /dev/null
+++ b/edit/util.js
@@ -0,0 +1,90 @@
+'use strict';
+
+function dirtyReporter() {
+ const dirty = new Map();
+ const onchanges = [];
+
+ function add(obj, value) {
+ const saved = dirty.get(obj);
+ if (!saved) {
+ dirty.set(obj, {type: 'add', newValue: value});
+ } else if (saved.type === 'remove') {
+ if (saved.savedValue === value) {
+ dirty.delete(obj);
+ } else {
+ saved.newValue = value;
+ saved.type = 'modify';
+ }
+ }
+ }
+
+ function remove(obj, value) {
+ const saved = dirty.get(obj);
+ if (!saved) {
+ dirty.set(obj, {type: 'remove', savedValue: value});
+ } else if (saved.type === 'add') {
+ dirty.delete(obj);
+ } else if (saved.type === 'modify') {
+ saved.type = 'remove';
+ }
+ }
+
+ function modify(obj, oldValue, newValue) {
+ const saved = dirty.get(obj);
+ if (!saved) {
+ if (oldValue !== newValue) {
+ dirty.set(obj, {type: 'modify', savedValue: oldValue, newValue});
+ }
+ } else if (saved.type === 'modify') {
+ if (saved.savedValue === newValue) {
+ dirty.delete(obj);
+ } else {
+ saved.newValue = newValue;
+ }
+ } else if (saved.type === 'add') {
+ saved.newValue = newValue;
+ }
+ }
+
+ function clear() {
+ dirty.clear();
+ }
+
+ function isDirty() {
+ return dirty.size > 0;
+ }
+
+ function onChange(cb) {
+ onchanges.push(cb);
+ }
+
+ function wrap(obj) {
+ for (const key of ['add', 'remove', 'modify', 'clear']) {
+ obj[key] = trackChange(obj[key]);
+ }
+ return obj;
+ }
+
+ function emitChange() {
+ for (const cb of onchanges) {
+ try {
+ cb();
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ function trackChange(fn) {
+ return function () {
+ const dirty = isDirty();
+ const result = fn.apply(null, arguments);
+ if (dirty !== isDirty()) {
+ emitChange();
+ }
+ return result;
+ };
+ }
+
+ return wrap({add, remove, modify, clear, isDirty, onChange});
+}