mobile table rendering
This commit is contained in:
+142
-2
@@ -14,7 +14,146 @@
|
||||
});
|
||||
*/
|
||||
|
||||
$(document).ready(function () {
|
||||
(function () {
|
||||
const WG_TABLE_SCROLL_CLASS = 'wg-mobile-table-scroll';
|
||||
const WIKI_TABLE_SELECTOR = '.westgate-wiki .wiki-page-content.wiki-article-prose > .card-body table';
|
||||
const WIKI_TABLE_MIN_COLUMN_WIDTH = 40;
|
||||
const WIKI_TABLE_DESKTOP_MIN_WIDTH = 720;
|
||||
const SKIP_TABLE_SELECTOR = [
|
||||
`.${WG_TABLE_SCROLL_CLASS}`,
|
||||
'.tableWrapper',
|
||||
'.table-responsive',
|
||||
'.table-responsive-sm',
|
||||
'.table-responsive-md',
|
||||
'figure.table',
|
||||
'.wiki-infobox',
|
||||
'.wiki-alignment-table',
|
||||
].join(', ');
|
||||
|
||||
function toArray(list) {
|
||||
return Array.prototype.slice.call(list || []);
|
||||
}
|
||||
|
||||
function parsePixelLength(value) {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const match = String(value).trim().match(/^([0-9]+(?:\.[0-9]+)?)px$/i);
|
||||
return match ? parseFloat(match[1]) : 0;
|
||||
}
|
||||
|
||||
function getInlinePixelWidth(element) {
|
||||
const styledWidth = element && element.style ? parsePixelLength(element.style.width) : 0;
|
||||
if (styledWidth) {
|
||||
return styledWidth;
|
||||
}
|
||||
|
||||
if (!element || typeof element.getAttribute !== 'function') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const styleAttribute = element.getAttribute('style') || '';
|
||||
const widthMatch = styleAttribute.match(/(?:^|;)\s*width\s*:\s*([0-9]+(?:\.[0-9]+)?)px(?:\s*;|$)/i);
|
||||
return widthMatch ? parseFloat(widthMatch[1]) : 0;
|
||||
}
|
||||
|
||||
function tableUsesFluidWidth(table) {
|
||||
const styledWidth = table && table.style ? String(table.style.width || '') : '';
|
||||
if (/%$/.test(styledWidth.trim())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!table || typeof table.getAttribute !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const styleAttribute = table.getAttribute('style') || '';
|
||||
return /(?:^|;)\s*width\s*:\s*[0-9]+(?:\.[0-9]+)?%(?:\s*;|$)/i.test(styleAttribute);
|
||||
}
|
||||
|
||||
function getTableColumnCount(table) {
|
||||
let columnCount = 0;
|
||||
|
||||
if (table && typeof table.querySelectorAll === 'function') {
|
||||
columnCount = Math.max(columnCount, table.querySelectorAll('colgroup col').length);
|
||||
|
||||
toArray(table.querySelectorAll('tr')).forEach((row) => {
|
||||
const rowColumnCount = toArray(row.children).reduce((count, cell) => {
|
||||
const span = parseInt(cell.getAttribute && cell.getAttribute('colspan'), 10) || cell.colSpan || 1;
|
||||
return count + Math.max(1, span);
|
||||
}, 0);
|
||||
columnCount = Math.max(columnCount, rowColumnCount);
|
||||
});
|
||||
}
|
||||
|
||||
return columnCount;
|
||||
}
|
||||
|
||||
function getTableColumnWidthFloor(table, columnCount) {
|
||||
if (!table || typeof table.querySelectorAll !== 'function' || !columnCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const explicitColumns = toArray(table.querySelectorAll('colgroup col'))
|
||||
.map(getInlinePixelWidth)
|
||||
.filter(width => width > 0);
|
||||
const explicitWidth = explicitColumns.reduce((total, width) => total + width, 0);
|
||||
const inferredColumns = Math.max(0, columnCount - explicitColumns.length);
|
||||
|
||||
return explicitWidth + (inferredColumns * WIKI_TABLE_MIN_COLUMN_WIDTH);
|
||||
}
|
||||
|
||||
function getRenderedWidth(element) {
|
||||
if (element && typeof element.getBoundingClientRect === 'function') {
|
||||
return element.getBoundingClientRect().width || 0;
|
||||
}
|
||||
|
||||
return element && element.offsetWidth ? element.offsetWidth : 0;
|
||||
}
|
||||
|
||||
function setMobileTableMinWidth(table, wrapper) {
|
||||
const columnCount = getTableColumnCount(table);
|
||||
const measuredWidth = getRenderedWidth(table);
|
||||
const columnWidthFloor = getTableColumnWidthFloor(table, columnCount);
|
||||
const desktopWidthFloor = tableUsesFluidWidth(table) && columnCount >= 8 ? WIKI_TABLE_DESKTOP_MIN_WIDTH : 0;
|
||||
const minWidth = Math.ceil(Math.max(measuredWidth, columnWidthFloor, desktopWidthFloor));
|
||||
|
||||
if (minWidth > 0 && wrapper && wrapper.style && typeof wrapper.style.setProperty === 'function') {
|
||||
wrapper.style.setProperty('--wg-mobile-table-min-width', `${minWidth}px`);
|
||||
}
|
||||
}
|
||||
|
||||
function wrapWestgateWikiTables(root) {
|
||||
const scope = root && typeof root.querySelectorAll === 'function' ? root : document;
|
||||
if (!scope || typeof scope.querySelectorAll !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
toArray(scope.querySelectorAll(WIKI_TABLE_SELECTOR)).forEach((table) => {
|
||||
if (!table || !table.parentNode || table.closest(SKIP_TABLE_SELECTOR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = WG_TABLE_SCROLL_CLASS;
|
||||
wrapper.setAttribute('tabindex', '0');
|
||||
wrapper.setAttribute('aria-label', 'Scrollable table');
|
||||
table.parentNode.insertBefore(wrapper, table);
|
||||
wrapper.appendChild(table);
|
||||
setMobileTableMinWidth(table, wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
window.westgateTheme = window.westgateTheme || {};
|
||||
window.westgateTheme.wrapWikiTables = wrapWestgateWikiTables;
|
||||
|
||||
$(document).ready(function () {
|
||||
wrapWestgateWikiTables(document);
|
||||
$(window).on('action:ajaxify.end', function () {
|
||||
wrapWestgateWikiTables(document);
|
||||
});
|
||||
|
||||
require(['api'], function (api) {
|
||||
const originalGet = api.get.bind(api);
|
||||
let categoryClassMapPromise;
|
||||
@@ -62,4 +201,5 @@ $(document).ready(function () {
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
}());
|
||||
|
||||
@@ -1167,6 +1167,75 @@
|
||||
font-family: var(--wg-font-code) !important;
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll,
|
||||
.westgate-wiki .wiki-article-prose :where(.tableWrapper, .table-responsive, .table-responsive-sm, .table-responsive-md, figure.table),
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content :where(.tableWrapper, .table-responsive, .table-responsive-sm, .table-responsive-md, figure.table),
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content :where(.tableWrapper, .table-responsive, .table-responsive-sm, .table-responsive-md, figure.table) {
|
||||
border: 1px solid rgba(194, 163, 90, 0.14);
|
||||
border-radius: 7px;
|
||||
box-shadow:
|
||||
inset -1.25rem 0 1rem -1rem rgba(194, 163, 90, 0.16),
|
||||
0 0.6rem 1.4rem rgba(0, 0, 0, 0.16);
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
max-inline-size: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-color: rgba(194, 163, 90, 0.5) rgba(8, 7, 10, 0.48);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll:focus {
|
||||
outline: 2px solid rgba(194, 163, 90, 0.42);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table,
|
||||
.westgate-wiki .wiki-article-prose :where(.tableWrapper, .table-responsive, .table-responsive-sm, .table-responsive-md, figure.table) > table,
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content :where(.tableWrapper, .table-responsive, .table-responsive-sm, .table-responsive-md, figure.table) > table,
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content :where(.tableWrapper, .table-responsive, .table-responsive-sm, .table-responsive-md, figure.table) > table {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table,
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content table,
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content table {
|
||||
max-inline-size: none;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table {
|
||||
min-inline-size: var(--wg-mobile-table-min-width, max-content);
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table.wiki-table-layout-fixed[style*="width:100%"],
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table.wiki-table-layout-fixed[style*="width: 100%"],
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content table.wiki-table-layout-fixed[style*="width:100%"],
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content table.wiki-table-layout-fixed[style*="width: 100%"],
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content table.wiki-table-layout-fixed[style*="width:100%"],
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content table.wiki-table-layout-fixed[style*="width: 100%"] {
|
||||
min-width: var(--wg-mobile-table-min-width, max-content);
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table:not([style*="width"]),
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content table:not([style*="width"]),
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content table:not([style*="width"]) {
|
||||
min-inline-size: var(--wg-mobile-table-min-width, max-content);
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose :where(th, td),
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content :where(th, td),
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content :where(th, td) {
|
||||
padding: 0.48rem 0.62rem;
|
||||
}
|
||||
}
|
||||
|
||||
.westgate-wiki-compose .wiki-article-prose .ck.ck-editor__editable_inline.ck-content pre[data-language]::after,
|
||||
.westgate-wiki-compose .ck.ck-editor__editable_inline.wiki-article-prose.ck-content pre[data-language]::after,
|
||||
.westgate-wiki-compose .ck-code-block-language-label {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'public', 'client.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const readyCallbacks = [];
|
||||
const eventHandlers = [];
|
||||
const fakeApi = {
|
||||
get: function () {},
|
||||
};
|
||||
const fakeDocument = {
|
||||
querySelectorAll: function () {
|
||||
return [];
|
||||
},
|
||||
createElement: function () {
|
||||
throw new Error('createElement should not be called during initialisation');
|
||||
},
|
||||
};
|
||||
|
||||
function fakeJquery() {
|
||||
return {
|
||||
ready: function (callback) {
|
||||
readyCallbacks.push(callback);
|
||||
},
|
||||
on: function (eventName, callback) {
|
||||
eventHandlers.push({ eventName, callback });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const context = {
|
||||
console,
|
||||
document: fakeDocument,
|
||||
require: function (deps, callback) {
|
||||
assert.equal(deps.length, 1);
|
||||
assert.equal(deps[0], 'api');
|
||||
callback(fakeApi);
|
||||
},
|
||||
window: {},
|
||||
$: fakeJquery,
|
||||
};
|
||||
|
||||
vm.runInNewContext(source, context, { filename: 'public/client.js' });
|
||||
readyCallbacks.forEach(callback => callback());
|
||||
|
||||
assert.equal(
|
||||
typeof (context.window.westgateTheme && context.window.westgateTheme.wrapWikiTables),
|
||||
'function',
|
||||
'client should expose a testable wiki table wrapper helper'
|
||||
);
|
||||
|
||||
assert(
|
||||
eventHandlers.some(handler => handler.eventName === 'action:ajaxify.end'),
|
||||
'client should rerun wiki table wrapping after ajaxify navigation'
|
||||
);
|
||||
|
||||
let createdWrapper;
|
||||
let insertedNode;
|
||||
const parent = {
|
||||
insertBefore: function (node, before) {
|
||||
insertedNode = { node, before };
|
||||
node.parentNode = this;
|
||||
},
|
||||
};
|
||||
const table = {
|
||||
style: {},
|
||||
parentNode: parent,
|
||||
closest: function (selector) {
|
||||
assert(
|
||||
selector.includes('wg-mobile-table-scroll') ||
|
||||
selector.includes('tableWrapper') ||
|
||||
selector.includes('wiki-infobox'),
|
||||
`unexpected closest selector: ${selector}`
|
||||
);
|
||||
return null;
|
||||
},
|
||||
getAttribute: function (name) {
|
||||
return name === 'style' ? 'width:100%' : null;
|
||||
},
|
||||
getBoundingClientRect: function () {
|
||||
return { width: 320 };
|
||||
},
|
||||
querySelectorAll: function (selector) {
|
||||
if (selector === 'colgroup col') {
|
||||
return [
|
||||
{ style: { width: '61px' }, getAttribute: function () { return 'width:61px'; } },
|
||||
{ style: { width: '80px' }, getAttribute: function () { return 'width:80px'; } },
|
||||
];
|
||||
}
|
||||
|
||||
if (selector === 'tr') {
|
||||
return [
|
||||
{
|
||||
children: Array.from({ length: 10 }, () => ({
|
||||
getAttribute: function () {
|
||||
return null;
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const root = {
|
||||
querySelectorAll: function (selector) {
|
||||
assert.equal(
|
||||
selector,
|
||||
'.westgate-wiki .wiki-page-content.wiki-article-prose > .card-body table'
|
||||
);
|
||||
return [table];
|
||||
},
|
||||
};
|
||||
|
||||
context.document.createElement = function (tagName) {
|
||||
assert.equal(tagName, 'div');
|
||||
createdWrapper = {
|
||||
className: '',
|
||||
child: null,
|
||||
attributes: {},
|
||||
style: {
|
||||
values: {},
|
||||
setProperty: function (name, value) {
|
||||
this.values[name] = value;
|
||||
},
|
||||
},
|
||||
appendChild: function (child) {
|
||||
this.child = child;
|
||||
child.parentNode = this;
|
||||
},
|
||||
setAttribute: function (name, value) {
|
||||
this.attributes[name] = value;
|
||||
},
|
||||
};
|
||||
return createdWrapper;
|
||||
};
|
||||
|
||||
context.window.westgateTheme.wrapWikiTables(root);
|
||||
|
||||
assert.equal(createdWrapper.className, 'wg-mobile-table-scroll');
|
||||
assert.equal(createdWrapper.attributes.tabindex, '0');
|
||||
assert.equal(insertedNode.node, createdWrapper);
|
||||
assert.equal(insertedNode.before, table);
|
||||
assert.equal(createdWrapper.child, table);
|
||||
assert.equal(createdWrapper.style.values['--wg-mobile-table-min-width'], '720px');
|
||||
@@ -0,0 +1,56 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const stylesheet = fs.readFileSync(
|
||||
path.join(__dirname, "..", "scss", "westgate", "_wiki-prose.scss"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const mobileMediaMatch = stylesheet.match(/@media\s*\(max-width:\s*767\.98px\)\s*\{[\s\S]*\n\}/);
|
||||
assert(mobileMediaMatch, "Expected a mobile-width table rendering media query");
|
||||
|
||||
const mobileStyles = mobileMediaMatch[0];
|
||||
|
||||
[
|
||||
".westgate-wiki .wiki-article-prose .wg-mobile-table-scroll",
|
||||
".westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table",
|
||||
".westgate-wiki .wiki-article-prose .wg-mobile-table-scroll > table.wiki-table-layout-fixed[style*=\"width:100%\"]",
|
||||
].forEach(selector => {
|
||||
assert(
|
||||
mobileStyles.includes(selector),
|
||||
`${selector} should receive mobile table handling without changing article HTML`
|
||||
);
|
||||
});
|
||||
|
||||
[
|
||||
"overflow-x: auto",
|
||||
"-webkit-overflow-scrolling: touch",
|
||||
"max-inline-size: 100%",
|
||||
"max-inline-size: none",
|
||||
"min-inline-size: var(--wg-mobile-table-min-width, max-content)",
|
||||
"min-width: var(--wg-mobile-table-min-width, max-content)",
|
||||
].forEach(declaration => {
|
||||
assert(
|
||||
mobileStyles.includes(declaration),
|
||||
`Mobile wiki tables should include ${declaration}`
|
||||
);
|
||||
});
|
||||
|
||||
[
|
||||
"attr(data-label)",
|
||||
"wiki-mobile-table",
|
||||
".wiki-article-prose tr {\n\t\tdisplay: block",
|
||||
".wiki-article-prose td::before",
|
||||
".wiki-article-prose th::before",
|
||||
].forEach(fragment => {
|
||||
assert(
|
||||
!mobileStyles.includes(fragment),
|
||||
`Mobile table rendering must not use stacked/card conversion: ${fragment}`
|
||||
);
|
||||
});
|
||||
|
||||
assert(
|
||||
!mobileStyles.includes(".westgate-wiki .wiki-page-content.wiki-article-prose > .card-body {\n\t\tmax-inline-size: 100%;\n\t\toverflow-x: auto"),
|
||||
"Mobile table scrolling should be scoped to table wrappers, not the whole article body"
|
||||
);
|
||||
Reference in New Issue
Block a user