live preview in editor

* refreshAllTabs was extracted
* ...and fixed to use each frame's url when getting the styles
This commit is contained in:
tophf 2018-01-10 21:56:14 +03:00
parent 0c2c86a8de
commit 989df35b05
15 changed files with 385 additions and 84 deletions

View File

@ -749,6 +749,14 @@
"message": "Number of styles active for the current site", "message": "Number of styles active for the current site",
"description": "Label for the checkbox controlling toolbar badge text." "description": "Label for the checkbox controlling toolbar badge text."
}, },
"previewLabel": {
"message": "Live preview",
"description": "Label for the checkbox in style editor to enable live preview while editing."
},
"previewTooltip": {
"message": "Temporarily applies the changes without saving.\nSave the style to make the changes permanent.",
"description": "Tooltip for the checkbox in style editor to enable live preview while editing."
},
"replace": { "replace": {
"message": "Replace", "message": "Replace",
"description": "Label before the replace input field in the editor shown on Ctrl-H" "description": "Label before the replace input field in the editor shown on Ctrl-H"
@ -927,7 +935,7 @@
"description": "Label for the enabled state of styles" "description": "Label for the enabled state of styles"
}, },
"styleEnabledToggleHint": { "styleEnabledToggleHint": {
"message": "Press Alt-Enter to toggle enabled/disabled state and save the style", "message": "Press Alt-Enter to toggle the enabled/disabled state",
"description": "Help text for the '[x] enable' checkbox in the editor" "description": "Help text for the '[x] enable' checkbox in the editor"
}, },
"styleInstall": { "styleInstall": {

View File

@ -21,7 +21,6 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
detectSloppyRegexps, detectSloppyRegexps,
openEditor, openEditor,
updateIcon, updateIcon,
refreshAllTabs,
closeTab: (msg, sender, respond) => { closeTab: (msg, sender, respond) => {
chrome.tabs.remove(msg.tabId || sender.tab.id, () => { chrome.tabs.remove(msg.tabId || sender.tab.id, () => {
@ -306,40 +305,6 @@ function webNavUsercssInstallerFF(data) {
} }
function refreshAllTabs(msg, sender = {}) {
return Promise.all([
sender.tab || getActiveTab(),
queryTabs(),
]).then(([ownTab, tabs]) => new Promise(resolve => {
if (FIREFOX) tabs = tabs.filter(tab => tab.width);
const last = tabs.length - 1;
for (let i = 0; i < last; i++) {
refreshTab(tabs[i], ownTab);
}
if (tabs.length) {
refreshTab(tabs[last], ownTab, resolve);
} else {
resolve();
}
}));
function refreshTab(tab, ownTab, resolve) {
const {id: tabId, url: matchUrl} = tab;
chrome.webNavigation.getAllFrames({tabId}, (frames = []) => {
ignoreChromeError();
for (const {frameId} of frames[0] ? frames : [{frameId: 0}]) {
getStyles({matchUrl, enabled: true, asHash: true}).then(styles => {
const message = {method: 'styleReplaceAll', tabId, frameId, styles};
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
if (!frameId) setTimeout(updateIcon, 0, {tab, styles});
if (resolve) resolve();
});
}
});
}
}
function updateIcon({tab, styles}) { function updateIcon({tab, styles}) {
if (tab.id < 0) { if (tab.id < 0) {
return; return;

View File

@ -0,0 +1,228 @@
/*
global API_METHODS cachedStyles
global getStyles filterStyles invalidateCache normalizeStyleSections
global updateIcon
*/
'use strict';
(() => {
const previewFromTabs = new Map();
/**
* When style id and state is provided, only that style is propagated.
* Otherwise all styles are replaced and the toolbar icon is updated.
* @param {Object} [msg]
* @param {{id:Number, enabled?:Boolean, sections?: (Array|String)}} [msg.style] -
* style to propagate
* @param {Boolean} [msg.codeIsUpdated]
* @returns {Promise<void>}
*/
API_METHODS.refreshAllTabs = (msg = {}) =>
Promise.all([
queryTabs(),
maybeParseUsercss(msg),
getStyles(),
]).then(([tabs, style]) =>
new Promise(resolve => {
if (style) msg.style.sections = normalizeStyleSections(style);
run(tabs, msg, resolve);
}));
function run(tabs, msg, resolve) {
const {style, codeIsUpdated} = msg;
// the style was updated/saved so we need to remove the old copy of the original style
if (msg.method === 'styleUpdated' && msg.reason !== 'editPreview') {
for (const [tabId, original] of previewFromTabs.entries()) {
if (style.id === original.id) {
previewFromTabs.delete(tabId);
}
}
if (!previewFromTabs.size) {
unregisterTabListeners();
}
}
if (!style) {
msg = {method: 'styleReplaceAll'};
// simple style update:
// * if disabled, apply.js will remove the element
// * if toggled and code is unchanged, apply.js will toggle the element
} else if (!style.enabled || codeIsUpdated === false) {
msg = {
method: 'styleUpdated',
reason: msg.reason,
style: {
id: style.id,
enabled: style.enabled,
},
codeIsUpdated,
};
// live preview puts the code in cachedStyles, saves the original in previewFromTabs,
// and if preview is being disabled, but the style is already deleted, we bail out
} else if (msg.reason === 'editPreview' && !updateCache(msg)) {
return;
// live preview normal operation, the new code is already in cachedStyles
} else {
msg.method = 'styleApply';
msg.style = {id: msg.style.id};
}
if (!tabs || !tabs.length) {
resolve();
return;
}
const last = tabs[tabs.length - 1];
for (const tab of tabs) {
if (FIREFOX && !tab.width) continue;
chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
refreshFrame(tab, frames, msg, tab === last && resolve));
}
}
function refreshFrame(tab, frames, msg, resolve) {
ignoreChromeError();
if (!frames || !frames.length) {
frames = [{
frameId: 0,
url: tab.url,
}];
}
msg.tabId = tab.id;
const styleId = msg.style && msg.style.id;
for (const frame of frames) {
const styles = filterStyles({
matchUrl: getFrameUrl(frame, frames),
asHash: true,
id: styleId,
});
msg = Object.assign({}, msg);
msg.frameId = frame.frameId;
if (msg.method !== 'styleUpdated') {
msg.styles = styles;
}
if (msg.method === 'styleApply' && !styles.length) {
// remove the style from a previously matching frame
invokeOrPostpone(tab.active, sendMessage, {
method: 'styleUpdated',
reason: 'editPreview',
style: {
id: styleId,
enabled: false,
},
tabId: tab.id,
frameId: frame.frameId,
}, ignoreChromeError);
} else {
invokeOrPostpone(tab.active, sendMessage, msg, ignoreChromeError);
}
if (msg.method === 'styleReplaceAll' && !frame.frameId) {
setTimeout(updateIcon, 0, {
tab,
styles,
});
}
}
if (resolve) resolve();
}
function getFrameUrl(frame, frames) {
while (frame.url === 'about:blank' && frame.frameId > 0) {
for (const f of frames) {
if (f.frameId === frame.parentFrameId) {
frame.url = f.url;
frame = f;
break;
}
}
}
return (frame || frames[0]).url;
}
function maybeParseUsercss({style}) {
if (style && typeof style.sections === 'string') {
return API_METHODS.parseUsercss({sourceCode: style.sections});
}
}
function updateCache(msg) {
const {style, tabId, restoring} = msg;
const spoofed = !restoring && previewFromTabs.get(tabId);
const original = cachedStyles.byId.get(style.id);
if (style.sections && !restoring) {
if (!previewFromTabs.size) {
registerTabListeners();
}
if (!spoofed) {
previewFromTabs.set(tabId, Object.assign({}, original));
}
} else {
previewFromTabs.delete(tabId);
if (!previewFromTabs.size) {
unregisterTabListeners();
}
if (!original) {
return;
}
if (!restoring) {
msg.style = spoofed || original;
}
}
invalidateCache({updated: msg.style});
return true;
}
function registerTabListeners() {
chrome.tabs.onRemoved.addListener(onTabRemoved);
chrome.tabs.onReplaced.addListener(onTabReplaced);
chrome.webNavigation.onCommitted.addListener(onTabNavigated);
}
function unregisterTabListeners() {
chrome.tabs.onRemoved.removeListener(onTabRemoved);
chrome.tabs.onReplaced.removeListener(onTabReplaced);
chrome.webNavigation.onCommitted.removeListener(onTabNavigated);
}
function onTabRemoved(tabId) {
const style = previewFromTabs.get(tabId);
if (style) {
API_METHODS.refreshAllTabs({
style,
tabId,
reason: 'editPreview',
restoring: true,
});
}
}
function onTabReplaced(addedTabId, removedTabId) {
onTabRemoved(removedTabId);
}
function onTabNavigated({tabId}) {
onTabRemoved(tabId);
}
})();

View File

@ -7,7 +7,8 @@
API_METHODS.saveUsercssUnsafe = style => save(style, true); API_METHODS.saveUsercssUnsafe = style => save(style, true);
API_METHODS.buildUsercss = build; API_METHODS.buildUsercss = build;
API_METHODS.installUsercss = install; API_METHODS.installUsercss = install;
API_METHODS.findUsercss = findUsercss; API_METHODS.parseUsercss = parse;
API_METHODS.findUsercss = find;
const TEMP_CODE_PREFIX = 'tempUsercssCode'; const TEMP_CODE_PREFIX = 'tempUsercssCode';
const TEMP_CODE_CLEANUP_DELAY = 60e3; const TEMP_CODE_CLEANUP_DELAY = 60e3;
@ -49,50 +50,55 @@
} }
} }
function assignVars(style) {
if (style.reason === 'config' && style.id) {
return style;
}
const dup = find(style);
if (dup) {
style.id = dup.id;
if (style.reason !== 'config') {
// preserve style.vars during update
usercss.assignVars(style, dup);
}
}
return style;
}
// Parse the source and find the duplication // Parse the source and find the duplication
function build({sourceCode, checkDup = false}) { function build({sourceCode, checkDup = false}) {
return buildMeta({sourceCode}) return buildMeta({sourceCode})
.then(usercss.buildCode) .then(usercss.buildCode)
.then(style => ({ .then(style => ({
style, style,
dup: checkDup && findUsercss(style), dup: checkDup && find(style),
})); }));
} }
function save(style, allowErrors = false) { // Parse the source, apply customizations, report fatal/syntax errors
function parse(style, allowErrors = false) {
// restore if stripped by getStyleWithNoCode // restore if stripped by getStyleWithNoCode
if (typeof style.sourceCode !== 'string') { if (typeof style.sourceCode !== 'string') {
style.sourceCode = cachedStyles.byId.get(style.id).sourceCode; style.sourceCode = cachedStyles.byId.get(style.id).sourceCode;
} }
return buildMeta(style) return buildMeta(style)
.then(assignVars) .then(assignVars)
.then(style => usercss.buildCode(style, allowErrors)) .then(style => usercss.buildCode(style, allowErrors));
}
function save(style, allowErrors = false) {
return parse(style, allowErrors)
.then(result => .then(result =>
allowErrors ? allowErrors ?
saveStyle(result.style).then(style => ({style, errors: result.errors})) : saveStyle(result.style).then(style => ({style, errors: result.errors})) :
saveStyle(result)); saveStyle(result));
function assignVars(style) {
if (style.reason === 'config' && style.id) {
return style;
}
const dup = findUsercss(style);
if (dup) {
style.id = dup.id;
if (style.reason !== 'config') {
// preserve style.vars during update
usercss.assignVars(style, dup);
}
}
return style;
}
} }
/** /**
* @param {Style|{name:string, namespace:string}} styleOrData * @param {Style|{name:string, namespace:string}} styleOrData
* @returns {Style} * @returns {Style}
*/ */
function findUsercss(styleOrData) { function find(styleOrData) {
if (styleOrData.id) return cachedStyles.byId.get(styleOrData.id); if (styleOrData.id) return cachedStyles.byId.get(styleOrData.id);
const {name, namespace} = styleOrData.usercssData || styleOrData; const {name, namespace} = styleOrData.usercssData || styleOrData;
for (const dup of cachedStyles.list) { for (const dup of cachedStyles.list) {

View File

@ -519,7 +519,7 @@
function moveAfter(el, expected) { function moveAfter(el, expected) {
if (!sorting) { if (!sorting) {
sorting = true; sorting = true;
if (observer) observer.stop(); stop();
} }
expected.insertAdjacentElement('afterend', el); expected.insertAdjacentElement('afterend', el);
if (el.disabled !== disableAll) { if (el.disabled !== disableAll) {

View File

@ -249,13 +249,15 @@
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a> <a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div> </div>
<div id="basic-info-enabled"> <div id="basic-info-enabled">
<label id="enabled-label" i18n-text="styleEnabledLabel"> <label id="enabled-label" i18n-text="styleEnabledLabel" i18n-title="styleEnabledToggleHint">
<input type="checkbox" id="enabled" class="style-contributor"> <input type="checkbox" id="enabled" class="style-contributor">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label> </label>
<a href="#" id="toggle-style-help" class="svg-inline-wrapper"> <label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip" class="hidden">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <input type="checkbox" id="editor.livePreview">
</a> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<span id="preview-errors" class="hidden">!</span>
</div> </div>
</section> </section>
<section id="actions"> <section id="actions">

View File

@ -1,7 +1,9 @@
/* /*
global CodeMirror linterConfig loadScript global CodeMirror linterConfig loadScript
global editors editor styleId global editors editor styleId ownTabId
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
global getSectionsHashes
global messageBox
*/ */
'use strict'; 'use strict';
@ -41,8 +43,9 @@ onDOMscriptReady('/codemirror.js').then(() => {
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip); addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
showHotkeyInTooltip(); showHotkeyInTooltip();
// N.B. the event listener 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();
@ -531,4 +534,49 @@ onDOMscriptReady('/codemirror.js').then(() => {
} }
return ''; return '';
} }
function setupLivePreview() {
if (!prefs.get('editor.livePreview') && !editors.length) {
setTimeout(setupLivePreview);
return;
}
$('#editor.livePreview').onchange = function () {
const previewing = this.checked;
editors.forEach(cm => cm[previewing ? 'on' : 'off']('changes', updatePreview));
const addRemove = previewing ? 'addEventListener' : 'removeEventListener';
$('#enabled')[addRemove]('change', updatePreview);
$('#sections')[addRemove]('change', updatePreview);
if (!previewing || document.body.classList.contains('dirty')) {
updatePreview(null, previewing);
}
};
CodeMirror.defineInitHook(cm => {
if (prefs.get('editor.livePreview')) {
cm.on('changes', updatePreview);
}
});
}
function updatePreview(data, previewing) {
if (previewing !== true && previewing !== false) {
if (data instanceof Event && !event.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 => {
errors.classList.remove('hidden');
errors.onclick = () => messageBox.alert(String(err));
});
}
}); });

View File

@ -41,10 +41,6 @@ body {
margin-bottom: 12px; margin-bottom: 12px;
} }
#basic-info-enabled {
margin-top: 2px;
}
label { label {
padding-left: 16px; padding-left: 16px;
position: relative; position: relative;
@ -102,6 +98,43 @@ label {
#url:not([href^="http"]) { #url:not([href^="http"]) {
display: none; display: none;
} }
#basic-info-enabled {
margin-top: 2px;
display: flex;
align-items: center;
line-height: 16px;
}
#basic-info-enabled > * {
margin-right: 1em;
margin-left: 0;
}
#basic-info-enabled > :last-child {
margin-right: 0;
}
#basic-info-enabled input,
#basic-info-enabled svg {
margin: auto 0;
bottom: 0;
}
#basic-info-enabled svg {
left: 2px;
}
#preview-errors {
background-color: red;
color: white;
padding: 0 6px;
border-radius: 9px;
margin-left: -.5em;
font-weight: bold;
cursor: pointer;
}
.svg-icon { .svg-icon {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
@ -117,9 +150,6 @@ label {
#mozilla-format-heading .svg-inline-wrapper { #mozilla-format-heading .svg-inline-wrapper {
margin-left: 0; margin-left: 0;
} }
#basic-info-enabled .svg-inline-wrapper {
margin-left: .1rem;
}
#colorpicker-settings.svg-inline-wrapper { #colorpicker-settings.svg-inline-wrapper {
margin: -2px 0 0 .1rem; margin: -2px 0 0 .1rem;
} }
@ -155,10 +185,6 @@ input:invalid {
} }
#enabled { #enabled {
margin-left: 0; margin-left: 0;
vertical-align: middle;
}
#enabled-label {
vertical-align: middle;
} }
/* collapsibles */ /* collapsibles */
#header summary { #header summary {

View File

@ -16,6 +16,7 @@ let dirty = {};
// array of all CodeMirror instances // array of all CodeMirror instances
const editors = []; const editors = [];
let saveSizeOnClose; let saveSizeOnClose;
let ownTabId;
// direct & reverse mapping of @-moz-document keywords and internal property names // direct & reverse mapping of @-moz-document keywords and internal property names
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'}; const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
@ -38,6 +39,8 @@ Promise.all([
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : ''; $('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
$('#preview-label').classList.toggle('hidden', !styleId);
$('#beautify').onclick = beautify; $('#beautify').onclick = beautify;
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
@ -110,7 +113,7 @@ function preinit() {
} }
getOwnTab().then(tab => { getOwnTab().then(tab => {
const ownTabId = tab.id; ownTabId = tab.id;
// use browser history back when 'back to manage' is clicked // use browser history back when 'back to manage' is clicked
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) { if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
@ -153,6 +156,7 @@ function onRuntimeMessage(request) {
switch (request.method) { switch (request.method) {
case 'styleUpdated': case 'styleUpdated':
if (styleId && styleId === request.style.id && if (styleId && styleId === request.style.id &&
request.reason !== 'editPreview' &&
request.reason !== 'editSave' && request.reason !== 'editSave' &&
request.reason !== 'config') { request.reason !== 'config') {
// code-less style from notifyAllTabs // code-less style from notifyAllTabs
@ -258,7 +262,6 @@ function initHooks() {
node.addEventListener('change', onChange); node.addEventListener('change', onChange);
node.addEventListener('input', onChange); node.addEventListener('input', onChange);
}); });
$('#toggle-style-help').addEventListener('click', showToggleStyleHelp);
$('#to-mozilla').addEventListener('click', showMozillaFormat, false); $('#to-mozilla').addEventListener('click', showMozillaFormat, false);
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp, false); $('#to-mozilla-help').addEventListener('click', showToMozillaHelp, false);
$('#from-mozilla').addEventListener('click', fromMozillaFormat); $('#from-mozilla').addEventListener('click', fromMozillaFormat);
@ -365,6 +368,7 @@ function save() {
$('#heading').textContent = t('editStyleHeading'); $('#heading').textContent = t('editStyleHeading');
} }
updateTitle(); updateTitle();
$('#preview-label').classList.remove('hidden');
}); });
} }

View File

@ -1,7 +1,10 @@
/* global CodeMirror dirtyReporter initLint */ /*
/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */ global editors styleId: true
/* global editors linterConfig updateLinter regExpTester sectionsToMozFormat */ global CodeMirror dirtyReporter
/* global createAppliesToLineWidget messageBox */ global updateLintReportIfEnabled initLint linterConfig updateLinter
global createAppliesToLineWidget messageBox
global sectionsToMozFormat
*/
'use strict'; 'use strict';
function createSourceEditor(style) { function createSourceEditor(style) {
@ -9,7 +12,6 @@ function createSourceEditor(style) {
$('#save-button').disabled = true; $('#save-button').disabled = true;
$('#mozilla-format-container').remove(); $('#mozilla-format-container').remove();
$('#save-button').onclick = save; $('#save-button').onclick = save;
$('#toggle-style-help').onclick = showToggleStyleHelp;
$('#header').addEventListener('wheel', headerOnScroll, {passive: true}); $('#header').addEventListener('wheel', headerOnScroll, {passive: true});
$('#sections').textContent = ''; $('#sections').textContent = '';
$('#sections').appendChild($create('.single-editor')); $('#sections').appendChild($create('.single-editor'));
@ -176,6 +178,8 @@ function createSourceEditor(style) {
} }
sessionStorage.justEditedStyleId = newStyle.id; sessionStorage.justEditedStyleId = newStyle.id;
style = newStyle; style = newStyle;
styleId = style.id;
$('#preview-label').classList.remove('hidden');
updateMeta(); updateMeta();
} }
} }

View File

@ -144,7 +144,8 @@ var API = (() => {
function notifyAllTabs(msg) { function notifyAllTabs(msg) {
const originalMessage = msg; const originalMessage = msg;
if (msg.method === 'styleUpdated' || msg.method === 'styleAdded') { const styleUpdated = msg.method === 'styleUpdated';
if (styleUpdated || msg.method === 'styleAdded') {
// apply/popup/manage use only meta for these two methods, // apply/popup/manage use only meta for these two methods,
// editor may need the full code but can fetch it directly, // editor may need the full code but can fetch it directly,
// so we send just the meta to avoid spamming lots of tabs with huge styles // so we send just the meta to avoid spamming lots of tabs with huge styles
@ -167,7 +168,8 @@ function notifyAllTabs(msg) {
if (affectsTabs || affectsIcon) { if (affectsTabs || affectsIcon) {
const notifyTab = tab => { const notifyTab = tab => {
// own pages will be notified via runtime.sendMessage later // own pages will be notified via runtime.sendMessage later
if ((affectsTabs || URLS.optionsUI.includes(tab.url)) if (!styleUpdated
&& (affectsTabs || URLS.optionsUI.includes(tab.url))
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin)) && !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
&& (!FIREFOX || tab.width)) { && (!FIREFOX || tab.width)) {
@ -198,6 +200,10 @@ function notifyAllTabs(msg) {
if (typeof applyOnMessage !== 'undefined') { if (typeof applyOnMessage !== 'undefined') {
applyOnMessage(originalMessage); applyOnMessage(originalMessage);
} }
// propagate saved style state/code efficiently
if (styleUpdated) {
API.refreshAllTabs(msg);
}
} }
@ -404,7 +410,7 @@ const debounce = Object.assign((fn, delay, ...args) => {
function deepCopy(obj) { function deepCopy(obj) {
if (!obj || typeof obj !== 'object') return obj; if (!obj || typeof obj !== 'object') return obj;
// N.B. a copy should be an explicitly literal // N.B. the copy should be an explicit literal
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
const copy = []; const copy = [];
for (const v of obj) { for (const v of obj) {

View File

@ -68,6 +68,7 @@ var prefs = new function Prefs() {
'editor.contextDelete': contextDeleteMissing(), // "Delete" item in context menu 'editor.contextDelete': contextDeleteMissing(), // "Delete" item in context menu
'editor.appliesToLineWidget': true, // show applies-to line widget on the editor 'editor.appliesToLineWidget': true, // show applies-to line widget on the editor
'editor.livePreview': true,
// show CSS colors as clickable colored rectangles // show CSS colors as clickable colored rectangles
'editor.colorpicker': true, 'editor.colorpicker': true,

View File

@ -515,6 +515,7 @@ Object.assign(handleEvent, {
function handleUpdate(style, {reason, method} = {}) { function handleUpdate(style, {reason, method} = {}) {
if (reason === 'editPreview') return;
let entry; let entry;
let oldEntry = $(ENTRY_ID_PREFIX + style.id); let oldEntry = $(ENTRY_ID_PREFIX + style.id);
if (oldEntry && method === 'styleUpdated') { if (oldEntry && method === 'styleUpdated') {

View File

@ -33,6 +33,7 @@
"background/style-via-api.js", "background/style-via-api.js",
"background/search-db.js", "background/search-db.js",
"background/update.js", "background/update.js",
"background/refresh-all-tabs.js",
"vendor/node-semver/semver.js", "vendor/node-semver/semver.js",
"vendor-overwrites/colorpicker/colorconverter.js" "vendor-overwrites/colorpicker/colorconverter.js"
] ]

View File

@ -32,6 +32,7 @@ function onRuntimeMessage(msg) {
switch (msg.method) { switch (msg.method) {
case 'styleAdded': case 'styleAdded':
case 'styleUpdated': case 'styleUpdated':
if (msg.reason === 'editPreview') return;
handleUpdate(msg.style); handleUpdate(msg.style);
break; break;
case 'styleDeleted': case 'styleDeleted':