Index tables allow you to present structured data. This is the type of data that typically comes from JSON or databases where the data structure is predictable.
This differs from the @uol-rich-text-table for use in Rich Text Areas where authors can edit the table content.
If the data can be presented with a single heading row and does not rely on column headings there is the option to present a “stacked” view for smaller screens. See Options for tables.
Name | Type | Description |
---|---|---|
tables | array | An array of tables. |
Name | Type | Description |
---|---|---|
stackable | boolean | Provides a “stacked” view for smaller screens. Only use when date has a single heading row. |
caption | string | Provides a title for the table, renders as a <h2> . Recommended unless preceded by an alternative semantic title. |
headings | array | The table headings, includes a “label: string that provides the cell data and boolean options for ‘image_column’ ‘sortable’ and ‘numeric’ which affect layout and functionality. |
rows | array | Each table row contains an array of ‘cells’ with the following required or optional fields: Required: ‘content’ string. Optional: ‘type’ that currently accepts a value of ‘numeric’. Optional: ‘img’ object that can have a ‘src’, ‘alt’ and ‘type’ (currently accepts ‘profile’). Optional: ‘link’ URL which acts as an active link. Optional: ‘sortBy’ string can be added for when the data is to be sorted by a value other than the displayed content (eg. ‘content’: ‘Dr Richard Ansell’, ‘sortBy’: ‘Ansell Richard Dr’). This should also be used when the displayed content is a mixture of text and numerical data and the content is to be sorted by the numerical value (eg. ‘content’: ‘£8’, ‘sortBy’: ‘8’). |
{% for table in tables %}
{% if table.caption %}<h2 class="uol-index-table-caption">{{ table.caption }}</h2>{% endif %}
{% if table.search_results and table.search_results.term %}
<div class="uol-index-table--results-container">
<h2 class="uol-index-table--results">{{ table.rows.length }} results for <strong>'{{ table.search_results.term }}'</strong></h2>
</div>
{% endif %}
<table class="uol-index-table{{ ' js-uolTableStackable' if table.stackable}} {{ ' uol-index-table--search-results' if table.search_results}}">
{# Currently unused due to Safari position: sticky bug #}
{# {% if table.caption %}<caption>{{ table.caption }}</caption>{% endif %} #}
{% if table.headings.length > 0 %}
<thead>
<tr>
{% for heading in table.headings%}
<th
class="uol-index-table__th {{ 'uol-index-table__th--numeric' if heading.numeric }} {{ 'uol-index-table__th--image-column' if heading.image_column }}"
{{ ' data-sortable=true' if heading.sortable }}>
{{ heading.label }}
</th>
{% endfor %}
</tr>
</thead>
{% endif %}
{% if table.rows.length > 0%}
<tbody>
{% for row in table.rows %}
<tr>
{% for cell in row.cells %}
<td
class="uol-index-table__td {{ 'uol-index-table__td--type-img' if cell.img }} {{ 'uol-index-table__td--type-numeric' if cell.type=='numeric' }}"
data-value="{{ cell.content }}"
{% if cell.sortBy %}
data-sortby="{{ cell.sortBy }}"
{% endif%}
>
{% if cell.link %}<a href="{{ cell.link }}">{% endif %}
{{ cell.content }}
{% if cell.img %}
<span
class="uol-index-table__td__img uol-index-table__td__img--profile"
{% if cell.img.src %}
role="img"
aria-label="{{ cell.img.alt }}"
style="background-image: url({{ cell.img.src }})"
{% endif%}
{% if cell.sortBy %}
data-sortkey="{{ cell.sortBy }}"
{% endif%}
>
</span>
{% endif %}
{% if cell.link %}</a>{% endif %}
{% if cell.email %}
<a href="mailto:{{cell.email}}">{{cell.email}}</a>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
{% endif %}
</table>
{% endfor %}
<h2 class="uol-index-table-caption">Multisort</h2>
<table class="uol-index-table js-uolTableStackable ">
<thead>
<tr>
<th class="uol-index-table__th " data-sortable=true>
Name (Sort by last name)
</th>
<th class="uol-index-table__th " data-sortable=true>
Role
</th>
<th class="uol-index-table__th uol-index-table__th--numeric " data-sortable=true>
Integer
</th>
<th class="uol-index-table__th uol-index-table__th--numeric " data-sortable=true>
Real
</th>
<th class="uol-index-table__th uol-index-table__th--numeric " data-sortable=true>
Currency
</th>
<th class="uol-index-table__th " data-sortable=true>
Email address
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="uol-index-table__td " data-value="Dr Paolo Actis" data-sortby="Actis Paolo Dr">
<a href="/paolo-actis">
Dr Paolo Actis
</a>
</td>
<td class="uol-index-table__td " data-value="Honorary Senior Lecturer">
Honorary Senior Lecturer
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="8">
8
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="8.2">
8.2
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="£8" data-sortby="8">
£8
</td>
<td class="uol-index-table__td " data-value="pactis@leeds.ac.uk">
<a href="mailto:pactis@leeds.ac.uk">
pactis@leeds.ac.uk
</a>
</td>
</tr>
<tr>
<td class="uol-index-table__td " data-value="Mr Azim Abadi" data-sortby="Abadi Azim Mr">
<a href="/profile-azim-abadi">
Mr Azim Abadi
</a>
</td>
<td class="uol-index-table__td " data-value="Visiting Research Fellow">
Visiting Research Fellow
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="2">
2
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="2.5">
2.5
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="£2.50" data-sortby="2.50">
£2.50
</td>
<td class="uol-index-table__td " data-value="aabadi@leeds.ac.uk">
<a href="mailto:aabadi@leeds.ac.uk">
aabadi@leeds.ac.uk
</a>
</td>
</tr>
<tr>
<td class="uol-index-table__td " data-value="Dr Mahmood Zinn" data-sortby="Zinn Mahmood Dr">
<a href="/mahmood-zinn-profile">
Dr Mahmood Zinn
</a>
</td>
<td class="uol-index-table__td " data-value="Lecturer">
Lecturer
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="22">
22
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="22">
22
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="£22" data-sortby="22">
£22
</td>
<td class="uol-index-table__td " data-value="mzinn@leeds.ac.uk">
<a href="mailto:azinn@leeds.ac.uk">
mzinn@leeds.ac.uk
</a>
</td>
</tr>
<tr>
<td class="uol-index-table__td " data-value="Dr Richard Newman" data-sortby="Newman Richard Dr">
<a href="/richard-newman">
Dr Richard Newman
</a>
</td>
<td class="uol-index-table__td " data-value="Associate Professor">
Associate Professor
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="3">
3
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="3.1">
3.1
</td>
<td class="uol-index-table__td uol-index-table__td--type-numeric" data-value="£3.10" data-sortby="3.10">
£3.10
</td>
<td class="uol-index-table__td " data-value="rnewman@leeds.ac.uk">
<a href="mailto:rnewman@leeds.ac.uk">
rnewman@leeds.ac.uk
</a>
</td>
</tr>
</tbody>
</table>
.uol-index-table-caption {
@extend %text-size-heading-4;
color: $color-font--dark;
margin: $spacing-4 0 0;
padding: $spacing-4;
@include media("<uol-media-m") {
border: 1px solid $color-border--light;
border-radius: 6px 6px 0 0;
}
}
.uol-index-table--results-container {
padding: $spacing-4;
@include media(">uol-media-m") {
text-align: right;
}
}
.uol-index-table--results {
@include font-size-responsive(1rem);
}
.uol-index-table {
@extend %text-size-paragraph--small;
border-collapse: unset;
margin-bottom: $spacing-4;
border: 1px solid $color-border--light;
font-variant-numeric: lining-nums;
@include media(">=uol-media-m") {
margin: $spacing-4;
position: relative;
border-radius: 12px;
}
// Currently unused due to Safari position: sticky bug
// caption {
// font-size: 18px;
// font-weight: 600;
// color: $color-font--dark;
// }
thead {
border-radius: 6px 6px 0 0;
@include media(">=uol-media-m") {
z-index: 2;
border-radius: 0;
}
}
th,
td {
vertical-align: top;
@include media(">=uol-media-m") {
padding: $spacing-4;
}
}
th {
font-weight: $font-weight-medium--sans-serif;
@include media(">=uol-media-m") {
text-align: left;
position: sticky;
top: 0;
padding-bottom: 20px;
background: $color-white;
z-index: 2;
// transform: translateZ(0) scaleX(1.01);
&:first-of-type {
border-radius: 12px 0 0 0;
}
&:last-of-type {
border-radius: 0 12px 0 0;
}
.csspositionsticky & {
&::before {
content: "";
display: block;
position: absolute;
top: 0;
bottom: -4px;
left: 0;
right: 0;
background: linear-gradient(0deg, rgba(#000, 0) 0, rgba(#000, 0.4) 4px, rgba(#000, 0) 4px);
opacity: 0;
transition: opacity 0.7s ease;
}
}
&.uol-index-table__th--numeric {
text-align: right;
}
&.uol-index-table__th--image-column {
@include media(">=uol-media-m") {
padding-left: 6.5rem;
}
}
}
}
.thead--stuck th {
@include media(">=uol-media-m") {
&::before {
opacity: 1;
}
&:first-of-type,
&:last-of-type {
border-radius: 0;
}
}
}
td {
color: $color-font;
a {
color: $color-font;
text-decoration: underline;
&:hover,
&:focus {
text-decoration-color: $color-brand;
}
}
}
tbody {
line-height: 1.5;
@include media(">=uol-media-m") {
z-index: 1;
}
tr {
transition: all 0.3s ease;
@include media(">=uol-media-m") {
&:nth-child(odd) {
background: $color-grey--faded;
}
&:last-of-type {
td {
&:first-of-type {
border-radius: 0 0 0 12px;
}
&:last-of-type {
border-radius: 0 0 12px 0;
}
}
}
}
}
td {
@include media(">=uol-media-m") {
border-bottom: 1px solid $color-border--light;
}
}
tr:last-of-type td {
border-bottom: none;
}
}
.uol-index-table__td--type-numeric {
@include media(">=uol-media-m") {
text-align: right;
}
}
.uol-index-table__td--type-img {
position: relative;
margin-top: -#{$spacing-4};
margin-left: -#{$spacing-4};
margin-right: -#{$spacing-4};
padding: $spacing-4 $spacing-4 $spacing-4 6.5rem;
&::before {
content: "";
float: left;
overflow: hidden;
height: $spacing-8;
font-size: 0;
}
}
.uol-index-table__td__img {
position: absolute;
top: 8px;
left: 8px;
display: block;
width: 80px;
height: 80px;
background-color: $color-white;
border-radius: 50%;
overflow: hidden;
@include media(">=uol-media-m") {
width: 64px;
height: 64px;
top: 16px;
}
&::before,
&::after {
content: "";
display: block;
position: absolute;
top: 0;
right: 0;
width: inherit;
height: inherit;
border-radius: inherit;
}
&::before {
background: 50% / cover;
background-image: inherit;
}
&::after {
box-shadow: inset 0 2px 5px rgba($color-black, 0.16);
}
}
.uol-index-table__td__img--profile {
background: $color-white url("../img/avatar-missing.png");
}
.uol-index-table__sort-select {
display: block;
font-weight: $font-weight-bold--sans-serif;
width: 100%;
margin: 0;
padding: $spacing-4;
border-color: $color-border;
border-radius: 0;
@supports (-webkit-appearance:none) {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: url("../img/sort.svg") no-repeat right $spacing-4 top 50%;
}
@media (-ms-high-contrast: active) {
-webkit-appearance: menulist;
-moz-appearance: menulist;
appearance: menulist;
background: initial;
}
@include media(">=uol-media-m") {
display: none;
}
}
.uol-index-table__th-inner {
display: flex;
}
.uol-index-table__btn-sort {
position: relative;
width: 25px;
height: 24px;
background: transparent;
border: none;
@include media("<uol-media-m") {
display: none;
}
&::before,
&::after {
content: "";
display: block;
position: absolute;
width: 0;
height: 0;
left: 8px;
border: 0.35rem solid transparent;
border-bottom: none;
border-top: 0.4rem solid $color-black--dark;
@media (forced-colors: active) {
border: 0.35rem solid canvas;
border-top: 0.4rem solid $color-black--dark;
}
@media (-ms-high-contrast: active) {
border: 0.35rem solid window;
border-top: 0.4rem solid windowText;
}
}
&::before {
transform: rotate(180deg) translateY(120%);
}
&::after {
transform: translateY(20%);
}
&.sorted--asc {
&::after {
opacity: 0;
}
}
&.sorted--desc {
&::before {
opacity: 0;
}
}
}
}
// uol-index-table--stacked class added with javascript
.uol-index-table--stacked {
margin: 0 0 $spacing-4;
border-radius: 12px;
@include media("<uol-media-m") {
display: block;
border-collapse: separate;
border-radius: 0 0 6px 6px;
// Currently unused due to Safari position: sticky bug
// caption {
// padding: 16px;
// line-height: 1.333;
// background-color: $color-cream--dark;
// border-bottom: 4px solid hotpink;
// }
thead {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px);
height: 1px;
width: 1px;
overflow: hidden;
left: -9999px;
}
tbody {
display: block;
tr {
display: block;
margin-bottom: 0;
}
td {
display: block;
padding: $spacing-4;
color: $color-font--dark;
&:first-of-type {
background: $color-grey--faded;
}
&:not(:first-child):not(:last-child) {
border-bottom: 1px solid $color-border--light;
}
}
.uol-index-table__td--type-img {
position: relative;
background: $color-cream--dark;
padding: $spacing-5 $spacing-4 $spacing-5 7rem;
margin: 0;
}
.uol-index-table__td__img {
position: absolute;
top: $spacing-4;
left: $spacing-4;
display: block;
width: $spacing-9;
height: $spacing-9;
background-color: $color-white;
border-radius: 50%;
overflow: hidden;
&::before,
&::after {
content: "";
display: block;
position: absolute;
top: 0;
right: 0;
width: inherit;
height: inherit;
border-radius: inherit;
}
&::before {
background: 50% / cover;
background-image: inherit;
}
&::after {
box-shadow: inset 0 2px 5px rgba($color-black, 0.16);
}
img {
width: 100%;
height: auto;
}
}
}
}
}
.uol-index-table--search-results {
@include media("<uol-media-m") {
border-radius: 6px;
.uol-index-table__sort-select {
border-radius: 6px 6px 0 0;
}
}
}
.uol-index-table__pseudo-th {
display: block;
font-weight: $font-weight-bold--sans-serif;
color: $color-font;
margin-bottom: $spacing-1;
@include media(">=uol-media-m") {
display: none;
}
}
// TODO: Add colspan support
export const uolTableStackable = () => {
// Config
const
stackableTableClass = '.js-uolTableStackable',
stackableTablePseudoThClass = 'uol-index-table__pseudo-th',
stackedTableClass = 'uol-index-table--stacked';
// Get tables
const tables = document.querySelectorAll(stackableTableClass);
// Process tables
tables.forEach( (table) => {
const ths = table.querySelectorAll('thead th');
const rows = table.querySelectorAll('tbody tr');
rows.forEach( (row) => {
const cells = row.querySelectorAll('td')
cells.forEach( (cell, i) => {
cell.innerHTML = `
<span
class="${stackableTablePseudoThClass}"
aria-hidden="true">
${ths.item(i).innerText}
</span>
${cell.innerHTML}
`;
})
})
table.classList.add(stackedTableClass);
})
}
export const uolTablesSortable = () => {
let ascending;
const sortRowsByIndex = (table, colNumber) => {
const tBody = table.querySelector('tbody');
const rows = tBody.rows;
let newTableArray = [];
for (let i = 0; i < (rows.length); i++) {
newTableArray[i] = rows[i];
}
/**
* Sort function - switches through adjacent table elements
* Sorts in array, no swapping in DOM
* @param {element} a - first element to compare
* @param {element} b - second element to compare
* @returns {number} if -ve a is sorted before b, +ve b before a, 0 no change
*/
newTableArray.sort(function(a, b){
let aElement = a.getElementsByTagName("TD")[colNumber];
let bElement = b.getElementsByTagName("TD")[colNumber];
let aItem = aElement.dataset.sortby ?
aElement.dataset.sortby :
aElement.innerText;
let bItem = bElement.dataset.sortby ?
bElement.dataset.sortby :
bElement.innerText;
/**
* Nested ternary return statement
* Outer checks if both values are number or not
* Inner checks ascending boolean
* Returns -ve, +ve or 0
*/
return (!isNaN(parseFloat(aItem)) && !isNaN(parseFloat(bItem)) ?
ascending ? aItem - bItem : bItem - aItem :
ascending ? ('' + aItem).localeCompare(bItem) : ('' + bItem).localeCompare(aItem)
);
});
/** after sort completed build new table body from sorted array */
let newTBody = '';
for (let i = 0; i < (newTableArray.length); i++) {
newTBody += '<tr>' + newTableArray[i].innerHTML + '</tr>';
}
/** update DOM with new sorted table body content */
tBody.innerHTML = newTBody;
}
const tables = document.querySelectorAll('.uol-index-table');
tables.forEach( (table) => {
const tableHeadings = table.querySelectorAll('thead th');
/** Create map for mobile dropdown */
let mobileSortableColumns = new Map();
tableHeadings.forEach( (th, index) => {
if (th.dataset.sortable) {
const thValue = th.innerText;
th.innerHTML = `<span class="uol-index-table__th-inner"><span class="uol-index-table__sortable-label">${thValue}</span></span>`
// Add value to mobile dropdown map
mobileSortableColumns.set(index, thValue);
const sortButton = document.createElement('button');
sortButton.classList.add('uol-index-table__btn-sort');
sortButton.type = 'button';
sortButton.setAttribute('aria-label', 'Sort column');
sortButton.onclick = () => {
const allTh = table.querySelectorAll('thead th');
const thisBtn = allTh[index].querySelector('button');
if (thisBtn.classList.contains('sorted--asc')) {
ascending = false;
thisBtn.classList.add('sorted');
thisBtn.classList.add('sorted--desc');
thisBtn.classList.remove('sorted--asc');
} else {
ascending = true;
thisBtn.classList.add('sorted');
thisBtn.classList.remove('sorted--desc');
thisBtn.classList.add('sorted--asc');
}
// Reset all the other buttons
allTh.forEach( (th, i) => {
// Don't reset the button just clicked
if (i != index) {
// Get the button
const otherBtn = th.querySelector('button');
// Not all th have buttons
if (otherBtn) {
otherBtn.classList.remove('sorted');
otherBtn.classList.remove('sorted--asc');
otherBtn.classList.remove('sorted--desc');
}
}
})
sortRowsByIndex(table, index);
}
th.querySelector('.uol-index-table__th-inner').appendChild(sortButton);
}
})
if (mobileSortableColumns.size > 0) {
let selectSort = document.createElement('select');
selectSort.classList.add('uol-index-table__sort-select');
selectSort.setAttribute('aria-label', 'Sort table columns');
let selectSortOptions = `<option value="">Sort by</option>`;
mobileSortableColumns.forEach( (value, key) => {
selectSortOptions += `
<option data-sort_index="${key}" data-sort_dir="asc" value="${key}">${value} - ascending</option>
<option data-sort_index="${key}" data-sort_dir="desc" value="${key}">${value} - descending</option>
`;
})
selectSort.innerHTML = selectSortOptions;
selectSort.addEventListener('change', (e) => {
const selected = e.target.options[e.target.selectedIndex],
selectedIndex = selected.dataset.sort_index,
selectedDir = selected.dataset.sort_dir;
ascending = (selectedDir == "asc") ? true: false;
sortRowsByIndex(table, selectedIndex);
})
// Insert select
const thead = table.querySelector('thead');
thead.parentNode.insertBefore(selectSort, thead);
}
})
}
export const uolTableSticky = () => {
require('intersection-observer');
const tHeads = document.querySelectorAll('.uol-index-table thead')
tHeads.forEach( (thead) => {
(new IntersectionObserver(function(e,options){
if(e[0].intersectionRatio>0){
thead.classList.remove('thead--stuck');
} else {
thead.classList.add('thead--stuck');
};
})).observe(thead);
})
}
{
"tables": [
{
"stackable": true,
"caption": "Multisort",
"search_term": "ab",
"headings": [
{
"sortable": true,
"label": "Name (Sort by last name)"
},
{
"sortable": true,
"label": "Role"
},
{
"sortable": true,
"numeric": true,
"label": "Integer"
},
{
"sortable": true,
"numeric": true,
"label": "Real"
},
{
"sortable": true,
"numeric": true,
"label": "Currency"
},
{
"sortable": true,
"label": "Email address"
}
],
"rows": [
{
"cells": [
{
"content": "Dr Paolo Actis",
"type": "text",
"link": "/paolo-actis",
"sortBy": "Actis Paolo Dr"
},
{
"type": "text",
"content": "Honorary Senior Lecturer"
},
{
"type": "numeric",
"content": "8"
},
{
"type": "numeric",
"content": "8.2"
},
{
"type": "numeric",
"content": "£8",
"sortBy": "8"
},
{
"type": "text",
"content": "pactis@leeds.ac.uk",
"link": "mailto:pactis@leeds.ac.uk"
}
]
},
{
"cells": [
{
"content": "Mr Azim Abadi",
"type": "text",
"link": "/profile-azim-abadi",
"sortBy": "Abadi Azim Mr"
},
{
"type": "text",
"content": "Visiting Research Fellow"
},
{
"type": "numeric",
"content": "2"
},
{
"type": "numeric",
"content": "2.5"
},
{
"type": "numeric",
"content": "£2.50",
"sortBy": "2.50"
},
{
"type": "text",
"content": "aabadi@leeds.ac.uk",
"link": "mailto:aabadi@leeds.ac.uk"
}
]
},
{
"cells": [
{
"content": "Dr Mahmood Zinn",
"type": "text",
"link": "/mahmood-zinn-profile",
"sortBy": "Zinn Mahmood Dr"
},
{
"type": "text",
"content": "Lecturer"
},
{
"type": "numeric",
"content": "22"
},
{
"type": "numeric",
"content": "22"
},
{
"type": "numeric",
"content": "£22",
"sortBy": "22"
},
{
"type": "text",
"content": "mzinn@leeds.ac.uk",
"link": "mailto:azinn@leeds.ac.uk"
}
]
},
{
"cells": [
{
"content": "Dr Richard Newman",
"type": "text",
"link": "/richard-newman",
"sortBy": "Newman Richard Dr"
},
{
"type": "text",
"content": "Associate Professor"
},
{
"type": "numeric",
"content": "3"
},
{
"type": "numeric",
"content": "3.1"
},
{
"type": "numeric",
"content": "£3.10",
"sortBy": "3.10"
},
{
"type": "text",
"content": "rnewman@leeds.ac.uk",
"link": "mailto:rnewman@leeds.ac.uk"
}
]
}
]
}
]
}