initialize editor page fully in First Meaningful Paint frame

* previously it wasn't the case when colorpicker option was enabled
* the cost of always loading colorview is ~1ms for >200ms here
This commit is contained in:
tophf 2017-12-08 05:45:27 +03:00
parent 0413736a29
commit 1c68ac1a3a
10 changed files with 145 additions and 92 deletions

View File

@ -51,6 +51,7 @@ globals:
tWordBreak: false tWordBreak: false
# dom.js # dom.js
onDOMready: false onDOMready: false
onDOMscriptReady: false
scrollElementIntoView: false scrollElementIntoView: false
enforceInputRange: false enforceInputRange: false
animateElement: false animateElement: false

View File

@ -33,6 +33,7 @@
<script src="edit/beautify.js"></script> <script src="edit/beautify.js"></script>
<script src="edit/sections.js"></script> <script src="edit/sections.js"></script>
<script src="edit/show-keymap-help.js"></script> <script src="edit/show-keymap-help.js"></script>
<script src="edit/codemirror-editing-hooks.js"></script>
<script src="edit/edit.js"></script> <script src="edit/edit.js"></script>
<script src="vendor/codemirror/lib/codemirror.js"></script> <script src="vendor/codemirror/lib/codemirror.js"></script>
@ -67,8 +68,11 @@
<script src="vendor/codemirror/keymap/emacs.js"></script> <script src="vendor/codemirror/keymap/emacs.js"></script>
<script src="vendor/codemirror/keymap/vim.js"></script> <script src="vendor/codemirror/keymap/vim.js"></script>
<link href="/vendor-overwrites/colorpicker/colorpicker.css" rel="stylesheet">
<script src="/vendor-overwrites/colorpicker/colorpicker.js"></script>
<script src="/vendor-overwrites/colorpicker/colorview.js"></script>
<script src="edit/match-highlighter-helper.js"></script> <script src="edit/match-highlighter-helper.js"></script>
<script src="edit/codemirror-editing-hooks.js"></script>
<script src="edit/codemirror-default.js"></script> <script src="edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css"> <link rel="stylesheet" href="/edit/codemirror-default.css">

View File

@ -25,12 +25,12 @@
styleActiveLine: true, styleActiveLine: true,
theme: 'default', theme: 'default',
keyMap: prefs.get('editor.keyMap'), keyMap: prefs.get('editor.keyMap'),
extraKeys: { extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
// independent of current keyMap // independent of current keyMap
'Alt-Enter': 'toggleStyle', 'Alt-Enter': 'toggleStyle',
'Alt-PageDown': 'nextEditor', 'Alt-PageDown': 'nextEditor',
'Alt-PageUp': 'prevEditor' 'Alt-PageUp': 'prevEditor'
}, }),
maxHighlightLength: 100e3, maxHighlightLength: 100e3,
}; };

View File

@ -5,8 +5,7 @@ global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
*/ */
'use strict'; 'use strict';
addEventListener('init:allDone', function _() { onDOMscriptReady('/codemirror.js').then(() => {
removeEventListener('init:allDone', _);
CodeMirror.defaults.lint = linterConfig.getForCodeMirror(); CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
@ -48,11 +47,16 @@ addEventListener('init:allDone', function _() {
// cm.state.search for last used 'find' // cm.state.search for last used 'find'
let searchState; let searchState;
// N.B. the event listener should be registered before setupLivePrefs() onDOMready().then(() => {
$('#options').addEventListener('change', onOptionElementChanged); prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
buildOptionsElements(); showKeyInSaveButtonTooltip();
setupLivePrefs();
rerouteHotkeys(true); // N.B. the event listener should be registered before setupLivePrefs()
$('#options').addEventListener('change', onOptionElementChanged);
buildOptionsElements();
rerouteHotkeys(true);
});
return; return;
@ -679,4 +683,23 @@ addEventListener('init:allDone', function _() {
}); });
}); });
} }
function showKeyInSaveButtonTooltip(prefName, value) {
$('#save-button').title = findKeyForCommand('save', value);
}
function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
const map = CodeMirror.keyMap[mapName];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
}); });

View File

@ -1,34 +1,23 @@
/* global CodeMirror loadScript editors showHelp */ /* global CodeMirror loadScript editors showHelp */
'use strict'; 'use strict';
// eslint-disable-next-line no-var onDOMscriptReady('/colorview.js').then(() => {
var initColorpicker = () => {
initOverlayHooks(); initOverlayHooks();
onDOMready().then(() => { onDOMready().then(() => {
$('#colorpicker-settings').onclick = configureColorpicker; $('#colorpicker-settings').onclick = configureColorpicker;
}); });
const scripts = [
'/vendor-overwrites/colorpicker/colorpicker.css',
'/vendor-overwrites/colorpicker/colorpicker.js',
'/vendor-overwrites/colorpicker/colorview.js',
];
prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey); prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey);
prefs.subscribe(['editor.colorpicker'], colorpickerOnDemand); prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
return prefs.get('editor.colorpicker') && colorpickerOnDemand(null, true); setColorpickerOption(null, prefs.get('editor.colorpicker'));
function colorpickerOnDemand(id, enabled) {
return loadScript(enabled && scripts)
.then(() => setColorpickerOption(id, enabled));
}
function setColorpickerOption(id, enabled) { function setColorpickerOption(id, enabled) {
const defaults = CodeMirror.defaults; const defaults = CodeMirror.defaults;
const keyName = prefs.get('editor.colorpicker.hotkey'); const keyName = prefs.get('editor.colorpicker.hotkey');
delete defaults.extraKeys[keyName];
defaults.colorpicker = enabled; defaults.colorpicker = enabled;
if (enabled) { if (enabled) {
if (keyName) { if (keyName) {
CodeMirror.commands.colorpicker = invokeColorpicker; CodeMirror.commands.colorpicker = invokeColorpicker;
defaults.extraKeys = defaults.extraKeys || {};
defaults.extraKeys[keyName] = 'colorpicker'; defaults.extraKeys[keyName] = 'colorpicker';
} }
defaults.colorpicker = { defaults.colorpicker = {
@ -45,6 +34,11 @@ var initColorpicker = () => {
}, },
}, },
}; };
} else {
CodeMirror.modeExtensions.css.unregisterColorviewHooks();
if (defaults.extraKeys) {
delete defaults.extraKeys[keyName];
}
} }
// on page load runs before CodeMirror.setOption is defined // on page load runs before CodeMirror.setOption is defined
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker)); editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
@ -162,4 +156,4 @@ var initColorpicker = () => {
return style; return style;
} }
} }
}; });

View File

@ -3,8 +3,6 @@ global CodeMirror parserlib loadScript
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
global mozParser createSourceEditor global mozParser createSourceEditor
global closeCurrentTab regExpTester messageBox global closeCurrentTab regExpTester messageBox
global initColorpicker
global initCollapsibles
global setupCodeMirror global setupCodeMirror
global beautify global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes global initWithSectionStyle addSections removeSection getSectionsHashes
@ -17,8 +15,6 @@ let dirty = {};
// array of all CodeMirror instances // array of all CodeMirror instances
const editors = []; const editors = [];
let saveSizeOnClose; let saveSizeOnClose;
// use browser history back when 'back to manage' is clicked
let useHistoryBack;
// 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'};
@ -35,14 +31,25 @@ Promise.all([
initStyleData(), initStyleData(),
onDOMready(), onDOMready(),
]) ])
.then(([style]) => Promise.all([ .then(([style]) => {
style, setupLivePrefs();
initColorpicker(),
initCollapsibles(), const usercss = isUsercss(style);
initHooksCommon(), $('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
dispatchEvent(new Event('init:allDone')), $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
])) $('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
.then(createEditor);
$('#beautify').onclick = beautify;
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
if (usercss) {
editor = createSourceEditor(style);
} else {
initWithSectionStyle({style});
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
}
});
function preinit() { function preinit() {
// make querySelectorAll enumeration code readable // make querySelectorAll enumeration code readable
@ -103,7 +110,18 @@ function preinit() {
getOwnTab().then(tab => { getOwnTab().then(tab => {
const ownTabId = tab.id; const ownTabId = tab.id;
useHistoryBack = sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href;
// use browser history back when 'back to manage' is clicked
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
onDOMready().then(() => {
$('#cancel-button').onclick = event => {
event.stopPropagation();
event.preventDefault();
history.back();
};
});
}
// no windows on android
if (!chrome.windows) { if (!chrome.windows) {
return; return;
} }
@ -130,20 +148,6 @@ function preinit() {
}); });
} }
function createEditor([style]) {
const usercss = isUsercss(style);
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
if (usercss) {
editor = createSourceEditor(style);
} else {
initWithSectionStyle({style});
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
}
}
function onRuntimeMessage(request) { function onRuntimeMessage(request) {
switch (request.method) { switch (request.method) {
case 'styleUpdated': case 'styleUpdated':
@ -270,40 +274,6 @@ function initHooks() {
} }
// common for usercss and classic // common for usercss and classic
function initHooksCommon() {
$('#cancel-button').addEventListener('click', goBackToManage);
$('#beautify').addEventListener('click', beautify);
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
showKeyInSaveButtonTooltip();
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
function goBackToManage(event) {
if (useHistoryBack) {
event.stopPropagation();
event.preventDefault();
history.back();
}
}
function showKeyInSaveButtonTooltip(prefName, value) {
$('#save-button').title = findKeyForCommand('save', value);
}
function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
const map = CodeMirror.keyMap[mapName];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
}
function onChange(event) { function onChange(event) {
const node = event.target; const node = event.target;

View File

@ -55,6 +55,7 @@ $$.remove = (selector, base = document) => {
onDOMready().then(() => { onDOMready().then(() => {
$.remove('#firefox-transitions-bug-suppressor'); $.remove('#firefox-transitions-bug-suppressor');
initCollapsibles();
}); });
if (!chrome.app && chrome.windows) { if (!chrome.app && chrome.windows) {
@ -278,9 +279,13 @@ function $createLink(href = '', content) {
} }
// makes <details> with [data-pref] save/restore their state
function initCollapsibles({bindClickOn = 'h2'} = {}) { function initCollapsibles({bindClickOn = 'h2'} = {}) {
const prefMap = {}; const prefMap = {};
const elements = $$('details[data-pref]'); const elements = $$('details[data-pref]');
if (!elements.length) {
return;
}
for (const el of elements) { for (const el of elements) {
const key = el.dataset.pref; const key = el.dataset.pref;

View File

@ -44,3 +44,60 @@ var loadScript = (() => {
return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f))); return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f)));
}; };
})(); })();
(() => {
let subscribers, observer;
// natively declared <script> elements in html can't have onload= attribute
// due to the default extension CSP that forbids inline code (and we don't want to relax it),
// so we're using MutationObserver to add onload event listener to the script element to be loaded
window.onDOMscriptReady = (src, timeout = 1000) => {
if (!subscribers) {
subscribers = new Map();
observer = new MutationObserver(observe);
observer.observe(document.head, {childList: true});
}
return new Promise((resolve, reject) => {
const listeners = subscribers.get(src);
if (listeners) {
listeners.push(resolve);
} else {
subscribers.set(src, [resolve]);
}
// no need to clear the timer since a resolved Promise won't reject anymore
setTimeout(reject, timeout);
});
};
return;
function observe(mutations) {
for (const {addedNodes} of mutations) {
for (const n of addedNodes) {
if (n.src && getSubscribersForSrc(n.src)) {
n.addEventListener('load', notifySubscribers);
}
}
}
}
function getSubscribersForSrc(src) {
for (const [subscribedSrc, listeners] of subscribers.entries()) {
if (src.endsWith(subscribedSrc)) {
return {subscribedSrc, listeners};
}
}
}
function notifySubscribers(event) {
this.removeEventListener('load', notifySubscribers);
const {subscribedSrc, listeners = []} = getSubscribersForSrc(this.src) || {};
listeners.forEach(fn => fn(event));
subscribers.delete(subscribedSrc);
if (!subscribers.size) {
observer.disconnect();
observer = null;
subscribers = null;
}
}
})();

View File

@ -3,7 +3,6 @@
/* global checkUpdate, handleUpdateInstalled */ /* global checkUpdate, handleUpdateInstalled */
/* global objectDiff */ /* global objectDiff */
/* global configDialog */ /* global configDialog */
/* global initCollapsibles */
'use strict'; 'use strict';
let installed; let installed;
@ -83,9 +82,6 @@ function initGlobalEvents() {
// N.B. triggers existing onchange listeners // N.B. triggers existing onchange listeners
setupLivePrefs(); setupLivePrefs();
// the options block
initCollapsibles();
$$('[id^="manage.newUI"]') $$('[id^="manage.newUI"]')
.forEach(el => (el.oninput = (el.onchange = switchUI))); .forEach(el => (el.oninput = (el.onchange = switchUI)));

View File

@ -59,6 +59,8 @@
if (!mx || mx.token !== colorizeToken) { if (!mx || mx.token !== colorizeToken) {
CodeMirror.extendMode('css', {token: colorizeToken}); CodeMirror.extendMode('css', {token: colorizeToken});
CodeMirror.extendMode('stylus', {token: colorizeToken}); CodeMirror.extendMode('stylus', {token: colorizeToken});
CodeMirror.modeExtensions.css.registerColorviewHooks = registerHooks;
CodeMirror.modeExtensions.css.unregisterColorviewHooks = unregisterHooks;
} }
} }
@ -498,11 +500,12 @@
cm.state.colorpicker.destroy(); cm.state.colorpicker.destroy();
} }
if (value) { if (value) {
registerHooks();
cm.state.colorpicker = new ColorMarker(cm, value); cm.state.colorpicker = new ColorMarker(cm, value);
} }
}); });
// initial runMode is performed by CodeMirror before setting our option // initial runMode is performed by CodeMirror before setting our option
// so we register the hooks right away - not a problem as our js is loaded on demand // so we register the hooks right away (the cost of always loading colorview is ~1ms for >200ms)
registerHooks(); registerHooks();
})(); })();