Add: live preview

This commit is contained in:
eight 2018-10-08 17:49:57 +08:00
parent a38558ef78
commit 15efafff3c
3 changed files with 84 additions and 140 deletions

View File

@ -13,6 +13,30 @@ const styleManager = (() => {
const compiledExclusion = createCache(); const compiledExclusion = createCache();
const BAD_MATCHER = {test: () => false}; 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({ return ensurePrepared({
get, get,
getStylesInfo, getStylesInfo,
@ -111,10 +135,11 @@ const styleManager = (() => {
data.originalDigest = digest; data.originalDigest = digest;
return saveStyle(data); return saveStyle(data);
}) })
.then(newData => .then(newData => handleSave(
broadcastStyleUpdated(newData, style ? 'update' : 'install') newData,
.then(() => newData) style ? 'update' : 'install',
); style ? 'styleUpdated' : 'styleAdded'
));
} }
function editSave(data) { function editSave(data) {
@ -125,19 +150,17 @@ const styleManager = (() => {
data = Object.assign(createNewStyle(), data); data = Object.assign(createNewStyle(), data);
} }
return saveStyle(data) return saveStyle(data)
.then(newData => .then(newData => handleSave(
broadcastStyleUpdated(newData, 'editSave') newData,
.then(() => newData) 'editSave',
); style ? 'styleUpdated' : 'styleAdded'
));
} }
function setStyleExclusions(id, exclusions) { function setStyleExclusions(id, exclusions) {
const data = Object.assign({}, styles.get(id), {exclusions}); const data = Object.assign({}, styles.get(id), {exclusions});
return saveStyle(data) return saveStyle(data)
.then(newData => .then(newData => handleSave(newData, 'exclusions', 'styleUpdated'));
broadcastStyleUpdated(newData, 'exclusions')
.then(() => newData)
);
} }
function deleteStyle(id) { 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); 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 excluded = new Set();
const updated = new Set(); const updated = new Set();
for (const [url, cache] of cachedStyleForUrl.entries()) { for (const [url, cache] of cachedStyleForUrl.entries()) {
@ -217,10 +224,9 @@ const styleManager = (() => {
}; };
} }
} }
style.data = data;
style.appliesTo = updated; style.appliesTo = updated;
return msg.broadcast({ return msg.broadcast({
method: 'styleUpdated', method,
style: { style: {
id: data.id, id: data.id,
enabled: data.enabled 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) { function getStylesInfoByUrl(url) {
const sections = getSectionsByUrl(url); const sections = getSectionsByUrl(url);
return Object.keys(sections) return Object.keys(sections)
@ -295,8 +315,8 @@ const styleManager = (() => {
return cache.sections; return cache.sections;
function buildCache(styleList) { function buildCache(styleList) {
for (const {appliesTo, data} of styleList) { for (const {appliesTo, data, preview} of styleList) {
const code = getAppliedCode(url, data); const code = getAppliedCode(url, preview || data);
if (code) { if (code) {
cache.sections[data.id] = { cache.sections[data.id] = {
id: data.id, id: data.id,

View File

@ -43,9 +43,6 @@ onDOMscriptReady('/codemirror.js').then(() => {
if (!cm.display.wrapper.closest('#sections')) { if (!cm.display.wrapper.closest('#sections')) {
return; return;
} }
if (prefs.get('editor.livePreview') && styleId) {
cm.on('changes', updatePreview);
}
if (prefs.get('editor.autocompleteOnTyping')) { if (prefs.get('editor.autocompleteOnTyping')) {
setupAutocomplete(cm); setupAutocomplete(cm);
} }
@ -75,7 +72,6 @@ onDOMscriptReady('/codemirror.js').then(() => {
// N.B. the onchange event listeners should be registered before setupLivePrefs() // N.B. the onchange event listeners should be registered before setupLivePrefs()
$('#options').addEventListener('change', onOptionElementChanged); $('#options').addEventListener('change', onOptionElementChanged);
setupLivePreview();
buildThemeElement(); buildThemeElement();
buildKeymapElement(); buildKeymapElement();
setupLivePrefs(); setupLivePrefs();
@ -592,107 +588,4 @@ onDOMscriptReady('/codemirror.js').then(() => {
} }
return ''; 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');
});
}
}); });

31
edit/live-preview.js Normal file
View File

@ -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);
}
}
}