From a15493bfb93120508a1ec57be650fac710fab2ae Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 00:09:25 +0800 Subject: [PATCH] Add: source editor --- edit.html | 4 ++ edit/edit.css | 11 ++++ edit/edit.js | 58 ++++++++++++++---- edit/lint.js | 6 +- edit/source-editor.js | 133 ++++++++++++++++++++++++++++++++++++++++++ edit/util.js | 90 ++++++++++++++++++++++++++++ 6 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 edit/source-editor.js create mode 100644 edit/util.js 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}); +}