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
# dom.js
onDOMready: false
onDOMscriptReady: false
scrollElementIntoView: false
enforceInputRange: false
animateElement: false

View File

@ -33,6 +33,7 @@
<script src="edit/beautify.js"></script>
<script src="edit/sections.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="vendor/codemirror/lib/codemirror.js"></script>
@ -67,8 +68,11 @@
<script src="vendor/codemirror/keymap/emacs.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/codemirror-editing-hooks.js"></script>
<script src="edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css">

View File

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

View File

@ -5,8 +5,7 @@ global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
*/
'use strict';
addEventListener('init:allDone', function _() {
removeEventListener('init:allDone', _);
onDOMscriptReady('/codemirror.js').then(() => {
CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
@ -48,11 +47,16 @@ addEventListener('init:allDone', function _() {
// cm.state.search for last used 'find'
let searchState;
onDOMready().then(() => {
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
showKeyInSaveButtonTooltip();
// N.B. the event listener should be registered before setupLivePrefs()
$('#options').addEventListener('change', onOptionElementChanged);
buildOptionsElements();
setupLivePrefs();
rerouteHotkeys(true);
});
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 */
'use strict';
// eslint-disable-next-line no-var
var initColorpicker = () => {
onDOMscriptReady('/colorview.js').then(() => {
initOverlayHooks();
onDOMready().then(() => {
$('#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'], colorpickerOnDemand);
return prefs.get('editor.colorpicker') && colorpickerOnDemand(null, true);
function colorpickerOnDemand(id, enabled) {
return loadScript(enabled && scripts)
.then(() => setColorpickerOption(id, enabled));
}
prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
setColorpickerOption(null, prefs.get('editor.colorpicker'));
function setColorpickerOption(id, enabled) {
const defaults = CodeMirror.defaults;
const keyName = prefs.get('editor.colorpicker.hotkey');
delete defaults.extraKeys[keyName];
defaults.colorpicker = enabled;
if (enabled) {
if (keyName) {
CodeMirror.commands.colorpicker = invokeColorpicker;
defaults.extraKeys = defaults.extraKeys || {};
defaults.extraKeys[keyName] = '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
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
@ -162,4 +156,4 @@ var initColorpicker = () => {
return style;
}
}
};
});

View File

@ -3,8 +3,6 @@ global CodeMirror parserlib loadScript
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
global mozParser createSourceEditor
global closeCurrentTab regExpTester messageBox
global initColorpicker
global initCollapsibles
global setupCodeMirror
global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes
@ -17,8 +15,6 @@ let dirty = {};
// array of all CodeMirror instances
const editors = [];
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
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
@ -35,14 +31,25 @@ Promise.all([
initStyleData(),
onDOMready(),
])
.then(([style]) => Promise.all([
style,
initColorpicker(),
initCollapsibles(),
initHooksCommon(),
dispatchEvent(new Event('init:allDone')),
]))
.then(createEditor);
.then(([style]) => {
setupLivePrefs();
const usercss = isUsercss(style);
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
$('#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() {
// make querySelectorAll enumeration code readable
@ -103,7 +110,18 @@ function preinit() {
getOwnTab().then(tab => {
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) {
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) {
switch (request.method) {
case 'styleUpdated':
@ -270,40 +274,6 @@ function initHooks() {
}
// 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) {
const node = event.target;

View File

@ -55,6 +55,7 @@ $$.remove = (selector, base = document) => {
onDOMready().then(() => {
$.remove('#firefox-transitions-bug-suppressor');
initCollapsibles();
});
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'} = {}) {
const prefMap = {};
const elements = $$('details[data-pref]');
if (!elements.length) {
return;
}
for (const el of elements) {
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)));
};
})();
(() => {
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 objectDiff */
/* global configDialog */
/* global initCollapsibles */
'use strict';
let installed;
@ -83,9 +82,6 @@ function initGlobalEvents() {
// N.B. triggers existing onchange listeners
setupLivePrefs();
// the options block
initCollapsibles();
$$('[id^="manage.newUI"]')
.forEach(el => (el.oninput = (el.onchange = switchUI)));

View File

@ -59,6 +59,8 @@
if (!mx || mx.token !== colorizeToken) {
CodeMirror.extendMode('css', {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();
}
if (value) {
registerHooks();
cm.state.colorpicker = new ColorMarker(cm, value);
}
});
// 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();
})();