-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Line highlight: Batching DOM read/writes to avoid reflows (#1865)
This batches DOM read and write operation to avoid reflows resulting for better performance.
- Loading branch information
1 parent
4faa331
commit 632ce00
Showing
2 changed files
with
179 additions
and
147 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,181 +1,213 @@ | ||
(function(){ | ||
|
||
if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) { | ||
return; | ||
} | ||
|
||
function $$(expr, con) { | ||
return Array.prototype.slice.call((con || document).querySelectorAll(expr)); | ||
} | ||
|
||
function hasClass(element, className) { | ||
className = " " + className + " "; | ||
return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1 | ||
} | ||
|
||
// Some browsers round the line-height, others don't. | ||
// We need to test for it to position the elements properly. | ||
var isLineHeightRounded = (function() { | ||
var res; | ||
return function() { | ||
if(typeof res === 'undefined') { | ||
var d = document.createElement('div'); | ||
d.style.fontSize = '13px'; | ||
d.style.lineHeight = '1.5'; | ||
d.style.padding = 0; | ||
d.style.border = 0; | ||
d.innerHTML = ' <br /> '; | ||
document.body.appendChild(d); | ||
// Browsers that round the line-height should have offsetHeight === 38 | ||
// The others should have 39. | ||
res = d.offsetHeight === 38; | ||
document.body.removeChild(d); | ||
} | ||
return res; | ||
} | ||
}()); | ||
|
||
function highlightLines(pre, lines, classes) { | ||
lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line'); | ||
|
||
var ranges = lines.replace(/\s+/g, '').split(','), | ||
offset = +pre.getAttribute('data-line-offset') || 0; | ||
(function () { | ||
|
||
var parseMethod = isLineHeightRounded() ? parseInt : parseFloat; | ||
var lineHeight = parseMethod(getComputedStyle(pre).lineHeight); | ||
var hasLineNumbers = hasClass(pre, 'line-numbers'); | ||
|
||
for (var i=0, currentRange; currentRange = ranges[i++];) { | ||
var range = currentRange.split('-'); | ||
if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) { | ||
return; | ||
} | ||
|
||
var start = +range[0], | ||
end = +range[1] || start; | ||
function $$(expr, con) { | ||
return Array.prototype.slice.call((con || document).querySelectorAll(expr)); | ||
} | ||
|
||
var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div'); | ||
function hasClass(element, className) { | ||
className = " " + className + " "; | ||
return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1 | ||
} | ||
|
||
line.setAttribute('aria-hidden', 'true'); | ||
line.setAttribute('data-range', currentRange); | ||
line.className = (classes || '') + ' line-highlight'; | ||
function callFunction(func) { | ||
func(); | ||
} | ||
|
||
//if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers | ||
if(hasLineNumbers && Prism.plugins.lineNumbers) { | ||
var startNode = Prism.plugins.lineNumbers.getLine(pre, start); | ||
var endNode = Prism.plugins.lineNumbers.getLine(pre, end); | ||
|
||
if (startNode) { | ||
line.style.top = startNode.offsetTop + 'px'; | ||
// Some browsers round the line-height, others don't. | ||
// We need to test for it to position the elements properly. | ||
var isLineHeightRounded = (function () { | ||
var res; | ||
return function () { | ||
if (typeof res === 'undefined') { | ||
var d = document.createElement('div'); | ||
d.style.fontSize = '13px'; | ||
d.style.lineHeight = '1.5'; | ||
d.style.padding = 0; | ||
d.style.border = 0; | ||
d.innerHTML = ' <br /> '; | ||
document.body.appendChild(d); | ||
// Browsers that round the line-height should have offsetHeight === 38 | ||
// The others should have 39. | ||
res = d.offsetHeight === 38; | ||
document.body.removeChild(d); | ||
} | ||
|
||
if (endNode) { | ||
line.style.height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px'; | ||
} | ||
} else { | ||
line.setAttribute('data-start', start); | ||
return res; | ||
} | ||
}()); | ||
|
||
if(end > start) { | ||
line.setAttribute('data-end', end); | ||
/** | ||
* Highlights the lines of the given pre. | ||
* | ||
* This function is split into a DOM measuring and mutate phase to improve performance. | ||
* The returned function mutates the DOM when called. | ||
* | ||
* @param {HTMLElement} pre | ||
* @param {string} [lines] | ||
* @param {string} [classes=''] | ||
* @returns {() => void} | ||
*/ | ||
function highlightLines(pre, lines, classes) { | ||
lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line'); | ||
|
||
var ranges = lines.replace(/\s+/g, '').split(','); | ||
var offset = +pre.getAttribute('data-line-offset') || 0; | ||
|
||
var parseMethod = isLineHeightRounded() ? parseInt : parseFloat; | ||
var lineHeight = parseMethod(getComputedStyle(pre).lineHeight); | ||
var hasLineNumbers = hasClass(pre, 'line-numbers'); | ||
var parentElement = hasLineNumbers ? pre : pre.querySelector('code') || pre; | ||
var mutateActions = /** @type {(() => void)[]} */ ([]); | ||
|
||
ranges.forEach(function (currentRange) { | ||
var range = currentRange.split('-'); | ||
|
||
var start = +range[0]; | ||
var end = +range[1] || start; | ||
|
||
var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div'); | ||
|
||
mutateActions.push(function () { | ||
line.setAttribute('aria-hidden', 'true'); | ||
line.setAttribute('data-range', currentRange); | ||
line.className = (classes || '') + ' line-highlight'; | ||
}); | ||
|
||
// if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers | ||
if (hasLineNumbers && Prism.plugins.lineNumbers) { | ||
var startNode = Prism.plugins.lineNumbers.getLine(pre, start); | ||
var endNode = Prism.plugins.lineNumbers.getLine(pre, end); | ||
|
||
if (startNode) { | ||
var top = startNode.offsetTop + 'px'; | ||
mutateActions.push(function () { | ||
line.style.top = top; | ||
}); | ||
} | ||
|
||
if (endNode) { | ||
var height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px'; | ||
mutateActions.push(function () { | ||
line.style.height = height; | ||
}); | ||
} | ||
} else { | ||
mutateActions.push(function () { | ||
line.setAttribute('data-start', start); | ||
|
||
if (end > start) { | ||
line.setAttribute('data-end', end); | ||
} | ||
|
||
line.style.top = (start - offset - 1) * lineHeight + 'px'; | ||
|
||
line.textContent = new Array(end - start + 2).join(' \n'); | ||
}); | ||
} | ||
|
||
line.style.top = (start - offset - 1) * lineHeight + 'px'; | ||
|
||
line.textContent = new Array(end - start + 2).join(' \n'); | ||
} | ||
mutateActions.push(function () { | ||
// allow this to play nicely with the line-numbers plugin | ||
// need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning | ||
parentElement.appendChild(line); | ||
}); | ||
}); | ||
|
||
//allow this to play nicely with the line-numbers plugin | ||
if(hasLineNumbers) { | ||
//need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning | ||
pre.appendChild(line); | ||
} else { | ||
(pre.querySelector('code') || pre).appendChild(line); | ||
} | ||
return function () { | ||
mutateActions.forEach(callFunction); | ||
}; | ||
} | ||
} | ||
|
||
function applyHash() { | ||
var hash = location.hash.slice(1); | ||
function applyHash() { | ||
var hash = location.hash.slice(1); | ||
|
||
// Remove pre-existing temporary lines | ||
$$('.temporary.line-highlight').forEach(function (line) { | ||
line.parentNode.removeChild(line); | ||
}); | ||
// Remove pre-existing temporary lines | ||
$$('.temporary.line-highlight').forEach(function (line) { | ||
line.parentNode.removeChild(line); | ||
}); | ||
|
||
var range = (hash.match(/\.([\d,-]+)$/) || [,''])[1]; | ||
var range = (hash.match(/\.([\d,-]+)$/) || [, ''])[1]; | ||
|
||
if (!range || document.getElementById(hash)) { | ||
return; | ||
} | ||
if (!range || document.getElementById(hash)) { | ||
return; | ||
} | ||
|
||
var id = hash.slice(0, hash.lastIndexOf('.')), | ||
pre = document.getElementById(id); | ||
var id = hash.slice(0, hash.lastIndexOf('.')), | ||
pre = document.getElementById(id); | ||
|
||
if (!pre) { | ||
return; | ||
} | ||
if (!pre) { | ||
return; | ||
} | ||
|
||
if (!pre.hasAttribute('data-line')) { | ||
pre.setAttribute('data-line', ''); | ||
} | ||
if (!pre.hasAttribute('data-line')) { | ||
pre.setAttribute('data-line', ''); | ||
} | ||
|
||
highlightLines(pre, range, 'temporary '); | ||
var mutateDom = highlightLines(pre, range, 'temporary '); | ||
mutateDom(); | ||
|
||
document.querySelector('.temporary.line-highlight').scrollIntoView(); | ||
} | ||
document.querySelector('.temporary.line-highlight').scrollIntoView(); | ||
} | ||
|
||
var fakeTimer = 0; // Hack to limit the number of times applyHash() runs | ||
var fakeTimer = 0; // Hack to limit the number of times applyHash() runs | ||
|
||
Prism.hooks.add('before-sanity-check', function(env) { | ||
var pre = env.element.parentNode; | ||
var lines = pre && pre.getAttribute('data-line'); | ||
Prism.hooks.add('before-sanity-check', function (env) { | ||
var pre = env.element.parentNode; | ||
var lines = pre && pre.getAttribute('data-line'); | ||
|
||
if (!pre || !lines || !/pre/i.test(pre.nodeName)) { | ||
return; | ||
} | ||
|
||
/* | ||
* Cleanup for other plugins (e.g. autoloader). | ||
* | ||
* Sometimes <code> blocks are highlighted multiple times. It is necessary | ||
* to cleanup any left-over tags, because the whitespace inside of the <div> | ||
* tags change the content of the <code> tag. | ||
*/ | ||
var num = 0; | ||
$$('.line-highlight', pre).forEach(function (line) { | ||
num += line.textContent.length; | ||
line.parentNode.removeChild(line); | ||
if (!pre || !lines || !/pre/i.test(pre.nodeName)) { | ||
return; | ||
} | ||
|
||
/* | ||
* Cleanup for other plugins (e.g. autoloader). | ||
* | ||
* Sometimes <code> blocks are highlighted multiple times. It is necessary | ||
* to cleanup any left-over tags, because the whitespace inside of the <div> | ||
* tags change the content of the <code> tag. | ||
*/ | ||
var num = 0; | ||
$$('.line-highlight', pre).forEach(function (line) { | ||
num += line.textContent.length; | ||
line.parentNode.removeChild(line); | ||
}); | ||
// Remove extra whitespace | ||
if (num && /^( \n)+$/.test(env.code.slice(-num))) { | ||
env.code = env.code.slice(0, -num); | ||
} | ||
}); | ||
// Remove extra whitespace | ||
if (num && /^( \n)+$/.test(env.code.slice(-num))) { | ||
env.code = env.code.slice(0, -num); | ||
} | ||
}); | ||
|
||
Prism.hooks.add('complete', function completeHook(env) { | ||
var pre = env.element.parentNode; | ||
var lines = pre && pre.getAttribute('data-line'); | ||
Prism.hooks.add('complete', function completeHook(env) { | ||
var pre = env.element.parentNode; | ||
var lines = pre && pre.getAttribute('data-line'); | ||
|
||
if (!pre || !lines || !/pre/i.test(pre.nodeName)) { | ||
return; | ||
} | ||
if (!pre || !lines || !/pre/i.test(pre.nodeName)) { | ||
return; | ||
} | ||
|
||
clearTimeout(fakeTimer); | ||
clearTimeout(fakeTimer); | ||
|
||
var hasLineNumbers = Prism.plugins.lineNumbers; | ||
var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers; | ||
var hasLineNumbers = Prism.plugins.lineNumbers; | ||
var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers; | ||
|
||
if (hasClass(pre, 'line-numbers') && hasLineNumbers && !isLineNumbersLoaded) { | ||
Prism.hooks.add('line-numbers', completeHook); | ||
} else { | ||
highlightLines(pre, lines); | ||
fakeTimer = setTimeout(applyHash, 1); | ||
} | ||
}); | ||
if (hasClass(pre, 'line-numbers') && hasLineNumbers && !isLineNumbersLoaded) { | ||
Prism.hooks.add('line-numbers', completeHook); | ||
} else { | ||
var mutateDom = highlightLines(pre, lines); | ||
mutateDom(); | ||
fakeTimer = setTimeout(applyHash, 1); | ||
} | ||
}); | ||
|
||
window.addEventListener('hashchange', applyHash); | ||
window.addEventListener('resize', function () { | ||
var preElements = document.querySelectorAll('pre[data-line]'); | ||
Array.prototype.forEach.call(preElements, function (pre) { | ||
highlightLines(pre); | ||
var actions = []; | ||
$$('pre[data-line]').forEach(function (pre) { | ||
actions.push(highlightLines(pre)); | ||
}); | ||
actions.forEach(callFunction); | ||
}); | ||
|
||
})(); | ||
})(); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.