From 15efafff3c55fbd5e08693849b370a1f8fa1ac38 Mon Sep 17 00:00:00 2001 From: eight Date: Mon, 8 Oct 2018 17:49:57 +0800 Subject: [PATCH] Add: live preview --- background/style-manager.js | 86 +++++++++++++++---------- edit/codemirror-editing-hooks.js | 107 ------------------------------- edit/live-preview.js | 31 +++++++++ 3 files changed, 84 insertions(+), 140 deletions(-) create mode 100644 edit/live-preview.js diff --git a/background/style-manager.js b/background/style-manager.js index c70e4b75..35ac165b 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -13,6 +13,30 @@ const styleManager = (() => { const compiledExclusion = createCache(); const BAD_MATCHER = {test: () => false}; + // setup live preview + chrome.runtime.onConnect(port => { + if (port.name !== 'livePreview') { + return; + } + let id; + port.onMessage.addListener(data => { + if (!id) { + id = data.id; + } + const style = styles.get(id); + style.preview = data; + broadcastStyleUpdated(data, 'editPreview'); + }); + port.onDisconnect.addListener(() => { + port = null; + if (id) { + const style = styles.get(id); + style.preview = null; + broadcastStyleUpdated(style.data, 'editPreview'); + } + }); + }); + return ensurePrepared({ get, getStylesInfo, @@ -111,10 +135,11 @@ const styleManager = (() => { data.originalDigest = digest; return saveStyle(data); }) - .then(newData => - broadcastStyleUpdated(newData, style ? 'update' : 'install') - .then(() => newData) - ); + .then(newData => handleSave( + newData, + style ? 'update' : 'install', + style ? 'styleUpdated' : 'styleAdded' + )); } function editSave(data) { @@ -125,19 +150,17 @@ const styleManager = (() => { data = Object.assign(createNewStyle(), data); } return saveStyle(data) - .then(newData => - broadcastStyleUpdated(newData, 'editSave') - .then(() => newData) - ); + .then(newData => handleSave( + newData, + 'editSave', + style ? 'styleUpdated' : 'styleAdded' + )); } function setStyleExclusions(id, exclusions) { const data = Object.assign({}, styles.get(id), {exclusions}); return saveStyle(data) - .then(newData => - broadcastStyleUpdated(newData, 'exclusions') - .then(() => newData) - ); + .then(newData => handleSave(newData, 'exclusions', 'styleUpdated')); } function deleteStyle(id) { @@ -179,24 +202,8 @@ const styleManager = (() => { }; } - function broadcastStyleUpdated(data, reason) { + function broadcastStyleUpdated(data, reason, method = 'styleUpdated') { const style = styles.get(data.id); - if (!style) { - // new style - const appliesTo = new Set(); - styles.set(data.id, { - appliesTo, - data - }); - for (const cache of cachedStyleForUrl.values()) { - cache.maybeMatch.add(data.id); - } - return msg.broadcast({ - method: 'styleAdded', - style: {id: data.id, enabled: data.enabled}, - reason - }); - } const excluded = new Set(); const updated = new Set(); for (const [url, cache] of cachedStyleForUrl.entries()) { @@ -217,10 +224,9 @@ const styleManager = (() => { }; } } - style.data = data; style.appliesTo = updated; return msg.broadcast({ - method: 'styleUpdated', + method, style: { id: data.id, enabled: data.enabled @@ -251,6 +257,20 @@ const styleManager = (() => { }); } + function handleSave(data, reason, method) { + const style = styles.get(data.id); + if (!style) { + styles.set(data.id, { + appliesTo: new Set(), + data + }); + } else { + style.data = data; + } + return broadcastStyleUpdated(data, reason, method) + .then(() => data); + } + function getStylesInfoByUrl(url) { const sections = getSectionsByUrl(url); return Object.keys(sections) @@ -295,8 +315,8 @@ const styleManager = (() => { return cache.sections; function buildCache(styleList) { - for (const {appliesTo, data} of styleList) { - const code = getAppliedCode(url, data); + for (const {appliesTo, data, preview} of styleList) { + const code = getAppliedCode(url, preview || data); if (code) { cache.sections[data.id] = { id: data.id, diff --git a/edit/codemirror-editing-hooks.js b/edit/codemirror-editing-hooks.js index 01cd79c4..44c2778d 100644 --- a/edit/codemirror-editing-hooks.js +++ b/edit/codemirror-editing-hooks.js @@ -43,9 +43,6 @@ onDOMscriptReady('/codemirror.js').then(() => { if (!cm.display.wrapper.closest('#sections')) { return; } - if (prefs.get('editor.livePreview') && styleId) { - cm.on('changes', updatePreview); - } if (prefs.get('editor.autocompleteOnTyping')) { setupAutocomplete(cm); } @@ -75,7 +72,6 @@ onDOMscriptReady('/codemirror.js').then(() => { // N.B. the onchange event listeners should be registered before setupLivePrefs() $('#options').addEventListener('change', onOptionElementChanged); - setupLivePreview(); buildThemeElement(); buildKeymapElement(); setupLivePrefs(); @@ -592,107 +588,4 @@ onDOMscriptReady('/codemirror.js').then(() => { } return ''; } - - function setupLivePreview() { - if (!prefs.get('editor.livePreview') && !editors.length) { - setTimeout(setupLivePreview); - return; - } - if (styleId) { - $('#editor.livePreview').onchange = livePreviewToggled; - return; - } - // wait for #preview-label's class to lose 'hidden' after the first save - new MutationObserver((_, observer) => { - if (!styleId) return; - observer.disconnect(); - setupLivePreview(); - livePreviewToggled(); - }).observe($('#preview-label'), { - attributes: true, - attributeFilter: ['class'], - }); - } - - function livePreviewToggled() { - const me = this instanceof Node ? this : $('#editor.livePreview'); - const previewing = me.checked; - editors.forEach(cm => cm[previewing ? 'on' : 'off']('changes', updatePreview)); - const addRemove = EventTarget.prototype[previewing ? 'addEventListener' : 'removeEventListener']; - addRemove.call($('#enabled'), 'change', updatePreview); - if (!editor) { - for (const el of $$('#sections .applies-to')) { - addRemove.call(el, 'input', updatePreview); - } - toggleLivePreviewSectionsObserver(previewing); - } - if (!previewing || document.body.classList.contains('dirty')) { - updatePreview(null, previewing); - } - } - - /** - * Observes newly added section elements, and sets these event listeners: - * 1. 'changes' on CodeMirror inside - * 2. 'input' on .applies-to inside - * The goal is to avoid listening to 'input' on the entire #sections tree, - * which would trigger updatePreview() twice on any keystroke - - * both for the synthetic event from CodeMirror and the original event. - * Side effects: - * two expando properties on #sections - * 1. __livePreviewObserver - * 2. __livePreviewObserverEnabled - * @param {Boolean} enable - */ - function toggleLivePreviewSectionsObserver(enable) { - const sections = $('#sections'); - const observing = sections.__livePreviewObserverEnabled; - let mo = sections.__livePreviewObserver; - if (enable && !mo) { - sections.__livePreviewObserver = mo = new MutationObserver(mutations => { - for (const {addedNodes} of mutations) { - for (const node of addedNodes) { - const el = node.children && $('.applies-to', node); - if (el) el.addEventListener('input', updatePreview); - if (node.CodeMirror) node.CodeMirror.on('changes', updatePreview); - } - } - }); - } - if (enable && !observing) { - mo.observe(sections, {childList: true}); - sections.__livePreviewObserverEnabled = true; - } else if (!enable && observing) { - mo.disconnect(); - sections.__livePreviewObserverEnabled = false; - } - } - - function updatePreview(data, previewing) { - if (previewing !== true && previewing !== false) { - if (data instanceof Event && !data.target.matches('.style-contributor')) return; - debounce(updatePreview, data && data.id === 'enabled' ? 0 : 400, null, true); - return; - } - const errors = $('#preview-errors'); - API.refreshAllTabs({ - reason: 'editPreview', - tabId: ownTabId, - style: { - id: styleId, - enabled: $('#enabled').checked, - sections: previewing && (editor ? editors[0].getValue() : getSectionsHashes()), - }, - }).then(() => { - errors.classList.add('hidden'); - }).catch(err => { - if (Array.isArray(err)) err = err.join('\n'); - if (err && editor && !isNaN(err.index)) { - const pos = editors[0].posFromIndex(err.index); - err = `${pos.line}:${pos.ch} ${err}`; - } - errors.classList.remove('hidden'); - errors.onclick = () => messageBox.alert(String(err), 'pre'); - }); - } }); diff --git a/edit/live-preview.js b/edit/live-preview.js new file mode 100644 index 00000000..fe81de0b --- /dev/null +++ b/edit/live-preview.js @@ -0,0 +1,31 @@ +'use strict'; + +function createLivePreview() { + let previewer; + return {update}; + + // {id, enabled, sourceCode, sections} + function update(data) { + if (!previewer) { + if (!data.id || !data.enabled) { + return; + } + previewer = createPreviewer(); + } + previewer.update(data); + } + + function createPreviewer() { + const port = chrome.runtime.connect({ + name: 'livePreview' + }); + port.onDisconnet.addListener(err => { + throw err; + }); + return {update}; + + function update(data) { + port.postMessage(data); + } + } +}