Add: live preview
This commit is contained in:
parent
a38558ef78
commit
15efafff3c
|
@ -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,
|
||||||
|
|
|
@ -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
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