Add: live preview
This commit is contained in:
parent
a38558ef78
commit
15efafff3c
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
31
edit/live-preview.js
Normal file
31
edit/live-preview.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user