MediaWiki:Common.js: Difference between revisions

Rewrite sticky headers code - Corrects issue with sticky headers within overflown tables, and only updates the relevant tables' headers upon resize
(Sticky headers: Try ResizeObserver instead of MutationObserver (cannot observe a computed style change))
(Rewrite sticky headers code - Corrects issue with sticky headers within overflown tables, and only updates the relevant tables' headers upon resize)
Line 521: Line 521:
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]


/* Sets the top property for stickyHeader tables */
// Sticky headers for tables
function setStickyHeaderTop(addObservers) {
 
var observer = null;
// Returns a list of header rows within a sticky table
if ((addObservers) && (ResizeObserver !== undefined)) {
function getStickyTableHeaders(element) {
observer = new ResizeObserver(
var rv = [];
function(entries) {
for (var rowIdx = 0; rowIdx < 10; rowIdx++) {
setStickyHeaderTop(false);
var rowElem = element.getElementsByClassName('headerRow-' + rowIdx.toString());
});
if (rowElem.length === 0) {
break;
}
rv.push(rowElem[0]);
}
}
var stickyTables = document.getElementsByClassName('stickyHeader');
return rv;
var headStyles = getComputedStyle(document.getElementById('mw-header-container'));
}
var headHeight = document.getElementById('mw-header-container').offsetHeight;
 
if (headStyles !== undefined && headStyles.position === 'static') {
// Given a table element, sets the headers' 'top' property as required
headHeight = 0;
function setStickyHeaderTop(element) {
var isOverflown = false;
var parentElem = element.parentElement;
if (parentElem !== undefined) {
isOverflown = (parentElem.scrollHeight > parentElem.clientHeight || parentElem.scrollWidth > parentElem.clientWidth);
}
}
for (var i = 0; i < stickyTables.length; i++) {
 
var firstRow = stickyTables[i].getElementsByClassName('headerRow-0');
// Determine the height of the MediWiki header, if it is always visible at the top of the page.
var secondRow = stickyTables[i].getElementsByClassName('headerRow-1');
// If the parent div to the table is overflowing, then the header's top position is set in
var firstHeight = 0;
// relation to that parent element and this can be skipped
if (firstRow.length > 0) {
var headHeight = 0;
firstHeight = firstRow[0].offsetHeight;
if (!isOverflown) {
if (addObservers) {
var headElem = document.getElementById('mw-header-container');
observer.observe(firstRow[0]);
if (headElem !== undefined) {
var headStyles = getComputedStyle(headElem);
if ((headStyles !== undefined) && (headStyles.position !== 'static')) {
headHeight = headElem.offsetHeight;
}
}
var firstHeaders = firstRow[0].getElementsByTagName('th');
}
for (var j = 0; j < firstHeaders.length; j++) {
}
firstHeaders[j].style.top = headHeight + 'px';
 
var cumulativeRowHeight = 0;
var headElems = getStickyTableHeaders(element);
for (var rowIdx = 0; rowIdx < headElems.length; rowIdx++) {
// Find each header row in sequence. When found, set or remove the 'top' attribute as
// required. If not found, then break
var headElem = headElems[rowIdx];
var cellElems = headElem.getElementsByTagName('th');
var topPos = headHeight + cumulativeRowHeight;
// Iterate over all header cells for the current header row
for (var cellIdx = 0; cellIdx < cellElems.length; cellIdx++) {
var cell = cellElems[cellIdx];
if ((isOverflown) && (cell.style.top !== undefined)) {
// If the table has overflown, then unset the 'top' attribute
cell.removeAttribute('top');
}
}
if (secondRow.length > 0) {
else {
var secondHeaders = secondRow[0].getElementsByTagName('th');
// Otherwise, set the 'top' attribute with the appropriate position
var secondHeight = headHeight + firstHeight - 1;
cellElems[cellIdx].style.top = topPos.toString() + 'px';
for (var j = 0; j < secondHeaders.length; j++) {
}
secondHeaders[j].style.top = secondHeight + 'px';
}
cumulativeRowHeight += headElem.offsetHeight - 1;
}
}
 
// Initialize observers for stickyHeader tables. These enable attributes of table headers to be
// adjusted as required when various elements are resized
function initStickyObservers() {
if (ResizeObserver !== undefined) {
// If the headers are resized, then the header's top position (particularly the second
// header) may need to be set again
var obvHeaderResize = new ResizeObserver(
function(entries) {
var st = [];
for (var i = 0; i < entries.length; i++) {
var headerRow = entries[i].target;
var stickyTable = headerRow.parentElement.parentElement;
if (!st.includes(stickyTable)) {
st.push(stickyTable);
}
}
for (var j = 0; j < st.length; j++) {
setStickyHeaderTop(st[j]);
}
}
}
);
// If the parent div to a table is overflowing, then the header's top position needs to
// be set in relation to the top of that parent element
var obvOverflow = new ResizeObserver(
function(entries) {
for (var i = 0; i < entries.length; i++) {
var tableParent = entries[i].target;
// The child elements will contain the table we want to set sticky headers for
var stickyTables = tableParent.children;
for (var j = 0; j < stickyTables.length; j++) {
var stickyTable = stickyTables[j];
if (stickyTable.classList.contains('stickyHeader')) {
setStickyHeaderTop(stickyTable);
}
}
}
}
);
var stickyTables = document.getElementsByClassName('stickyHeader');
for (var i = 0; i < stickyTables.length; i++) {
var stickyTable = stickyTables[i];
// Observe the table's parent for content overflows
obvOverflow.observe(stickyTable.parentElement);
var headElems = getStickyTableHeaders(stickyTable);
for (var j = 0; j < headElems.length; j++) {
// Observe the table's header rows for resizing
obvHeaderResize.observe(headElems[j]);
}
}
}
}
Line 560: Line 636:
}
}


$(document).ready(function () {
function initStickyHeaders() {
// Table sticky headers
var stickyTables = document.getElementsByClassName('stickyHeader');
var elemSticky = document.getElementsByClassName('stickyHeader');
if (stickyTables.length > 0) {
if (elemSticky.length > 0) {
// Sticky headers do not function well when Tabber containers/article tags.
// Therefore identify any stickyHeader tables within these containers
//  and remove the stickyHeader class
var elemArticle = document.getElementsByTagName('article');
var elemArticle = document.getElementsByTagName('article');
if (elemArticle.length > 0) {
for (i = 0; i < stickyTables.length; i++) {
for (var kS = 0; kS < elemSticky.length; kS++) {
var stickyTable = stickyTables[i];
for (var kA = 0; kA < elemArticle.length; kA++) {
 
var eSticky = elemSticky[kS];
// Sticky headers do not function well when Tabber containers/article tags.
var eArticle = elemArticle[kA];
// Therefore identify any stickyHeader tables within these containers
if (eArticle.contains(eSticky)) {
//  and remove the stickyHeader class
eSticky.classList.remove('stickyHeader');
for (j = 0; j < elemArticle.length; j++) {
}
if (elemArticle[j].contains(stickyTable)) {
stickyTable.classList.remove('stickyHeader');
}
}
}
if (stickyTable.classList.contains('stickyHeader')) {
// If the table is still sticky, initialize header positions
setStickyHeaderTop(stickyTable);
}
}
}
}
// Initialize sticky header positions
 
setStickyHeaderTop(true);
// Initialize observers
initStickyObservers();
 
// Reset sticky header positions when the window resizes, as this may
// Reset sticky header positions when the window resizes, as this may
// affect visibility of fixed elements at the top of the page
// affect visibility of fixed elements at the top of the page
$(window).resize(function() { setStickyHeaderTop(false); });
$(window).resize(
// Reset sticky header positions if the offsetHeight attribute changes
function() {
// Without this, headers may have an incorrect offset
var stickyTables = document.getElementsByClassName('stickyHeader');
for (i = 0; i < stickyTables.length; i++) {
setStickyHeaderTop(stickyTables[i]);
}
});
}
}
}
$(document).ready(function () {
// Table sticky headers
initStickyHeaders();
});
});