//= require govuk/vendor/polyfills/Element/prototype/classList.js
// This is a non-jQuery version of Magna Charta: https://github.com/alphagov/magna-charta
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};
(function (Modules) {
function MagnaCharta () { }
MagnaCharta.prototype.start = function ($module, options) {
this.$module = $module[0]
this.options = {
outOf: 65,
applyOnInit: true,
toggleText: 'Toggle between chart and table',
autoOutdent: false,
outdentAll: false,
toggleAfter: false, // BOOL set TRUE to append the toggle link
returnReference: false // for testing purposes
}
for (var k in options) this.options[k] = options[k]
this.detectIEVersion()
// Magna Charta doesn't work in IE7 or less - so plain tables are shown to those browsers
this.ENABLED = !(this.ie && this.ie < 8)
// store a reference to the table in the object
this.$table = $module
// lets make what will become the new graph
this.$graph = document.createElement('div')
// set the graph to aria-hidden, which isn't changed at any point
// the graph is totally inaccessible, so we let screen readers navigate the table
// and ignore the graph entirely
this.$graph.setAttribute('aria-hidden', 'true')
// copy over classes from the table, and add the extra one
this.$graph.setAttribute('class', this.$table.className)
this.$graph.classList.add('mc-chart')
// set the stacked option based on
// giving the table a class of mc-stacked
this.options.stacked = this.$table.classList.contains('mc-stacked')
// set the negative option based on
// giving the table a class of mc-negative
this.options.negative = this.$table.classList.contains('mc-negative')
// true if it's a 'multiple' table
// this means multiple bars per rows, but not stacked.
var moreThanTwoTDs = this.$table.querySelectorAll('tbody tr')[0].querySelectorAll('td').length > 2
this.options.multiple = !this.options.stacked && (this.$table.classList.contains('mc-multiple') || moreThanTwoTDs)
// set the outdent options
// which can be set via classes or overriden by setting the value to true
// in the initial options object that's passed in
this.options.autoOutdent = this.options.autoOutdent || this.$table.classList.contains('mc-auto-outdent')
this.options.outdentAll = this.options.outdentAll || this.$table.classList.contains('mc-outdented')
// add a mc-multiple class if it is
if (this.options.multiple) {
this.$graph.classList.add('mc-multiple')
}
this.options.hasCaption = !!this.$table.querySelectorAll('caption').length
if (this.ENABLED) {
this.apply()
// if applyOnInit is false, toggle immediately to show the table and hide the graph
if (!this.options.applyOnInit) {
this.toggleLink.click()
}
}
if (this.options.returnReference) {
return this
}
}
MagnaCharta.prototype.detectIEVersion = function () {
// detect IE version: James Padolsey, https://gist.github.com/527683
this.ie = (function () {
var undef
var v = 3
var div = document.createElement('div')
var all = div.getElementsByTagName('i')
do {
div.innerHTML = ''
} while (v < 10 && all[0])
return (v > 4) ? v : undef
})()
}
MagnaCharta.prototype.apply = function () {
if (this.ENABLED) {
this.constructChart()
this.addClassesToHeader()
this.applyWidths()
this.insert()
this.$table.classList.add('visually-hidden')
this.applyOutdent()
}
}
// methods for constructing the chart
MagnaCharta.prototype.construct = {}
// constructs the header
MagnaCharta.prototype.construct.thead = function () {
var thead = document.createElement('div')
thead.classList.add('mc-thead')
var tr = document.createElement('div')
tr.classList.add('mc-tr')
var output = ''
var allTheTHs = this.$table.querySelectorAll('th')
for (var i = 0; i < allTheTHs.length; i++) {
output += '
'
output += allTheTHs[i].innerHTML
output += '
'
}
tr.innerHTML = output
thead.appendChild(tr)
return thead
}
MagnaCharta.prototype.construct.tbody = function () {
var tbody = document.createElement('div')
tbody.classList.add('mc-tbody')
var allTheTbodyTrs = this.$table.querySelectorAll('tbody tr')
for (var i = 0; i < allTheTbodyTrs.length; i++) {
var tr = document.createElement('div')
tr.classList.add('mc-tr')
var cellsOutput = ''
var allTheTds = allTheTbodyTrs[i].querySelectorAll('td')
for (var j = 0; j < allTheTds.length; j++) {
cellsOutput += ''
cellsOutput += allTheTds[j].innerHTML
cellsOutput += '
'
}
tr.innerHTML = cellsOutput
tbody.appendChild(tr)
}
return tbody
}
MagnaCharta.prototype.construct.caption = function () {
var cap = this.$table.querySelector('caption')
return cap.cloneNode(true)
}
// construct a link to allow the user to toggle between chart and table
MagnaCharta.prototype.construct.toggleLink = function (toggleText) {
var link = document.createElement('a')
link.setAttribute('href', '#')
link.classList.add('mc-toggle-link')
link.innerHTML = toggleText
link.setAttribute('aria-hidden', 'true')
return link
}
// toggles between showing the table and showing the chart
MagnaCharta.prototype.addToggleClick = function () {
var that = this
this.toggleLink.addEventListener('click', function (e) {
e.preventDefault()
that.$graph.classList.toggle('visually-hidden')
that.$table.classList.toggle('visually-hidden')
})
}
MagnaCharta.prototype.constructChart = function () {
// turn every element in the table into divs with appropriate classes
// call them and define this as scope so it's easier to
// get at options and properties
var thead = this.construct.thead.call(this)
var tbody = this.construct.tbody.call(this)
this.toggleLink = this.construct.toggleLink(this.options.toggleText)
this.addToggleClick(this.toggleLink)
if (this.options.hasCaption) {
var caption = this.construct.caption.call(this)
this.$graph.appendChild(caption)
}
if (this.options.toggleAfter) {
this.$table.insertAdjacentElement('afterend', this.toggleLink)
} else {
this.$table.insertAdjacentElement('beforebegin', this.toggleLink)
}
this.$graph.appendChild(thead)
this.$graph.appendChild(tbody)
}
// some handy utility methods
MagnaCharta.prototype.utils = {
isFloat: function (val) {
return !isNaN(parseFloat(val))
},
stripValue: function (val) {
var re = new RegExp('\\,|£|%|[a-z]', 'gi')
return val.replace(re, '')
},
returnMax: function (values) {
var max = 0
for (var i = 0; i < values.length; i++) {
if (values[i] > max) { max = values[i] }
}
return max
},
isNegative: function (value) {
return (value < 0)
}
}
MagnaCharta.prototype.addClassesToHeader = function () {
var headerCells = this.$graph.querySelectorAll('.mc-th')
var looplength = headerCells.length
if (this.options.stacked) {
var last = looplength - 1
headerCells[last].classList.add('mc-stacked-header', 'mc-header-total')
looplength -= 1
}
// we deliberately don't apply this to the first cell
for (var i = 1; i < looplength; i++) {
headerCells[i].classList.add('mc-key-header')
if (!headerCells[i].classList.contains('mc-stacked-header')) {
headerCells[i].classList.add('mc-key-' + i)
}
}
}
MagnaCharta.prototype.calculateMaxWidth = function () {
// store the cell values in here so we can figure out the maximum value later
var values = []
// var to store the maximum negative value (used only for negative charts)
var maxNegativeValue = 0
// loop through every tr in the table
var trs = this.$graph.querySelectorAll('.mc-tr')
for (var i = 0; i < trs.length; i++) {
var $this = trs[i]
// the first td is going to be the key, so ignore it
// we'd use $this.querySelectorAll('.mc-td:not(:first-child)') but for IE8
var $bodyCellsOriginal = $this.querySelectorAll('.mc-td')
var $bodyCells = []
for (var k = 1; k < $bodyCellsOriginal.length; k++) {
$bodyCells.push($bodyCellsOriginal[k])
}
var bodyCellsLength = $bodyCells.length
// might be the row containing th elements, so we need to check
if (bodyCellsLength) {
// if it's stacked, the last column is a totals
// so we don't want that in our calculations
if (this.options.stacked) {
$bodyCells[bodyCellsLength - 1].classList.add('mc-stacked-total')
bodyCellsLength -= 1
}
// first td in each row is key
var firstCell = $this.querySelector('.mc-td')
if (firstCell) {
firstCell.classList.add('mc-key-cell')
}
// store the total value of the bar cells in a row
// for anything but stacked, this is just the value of one
var cellsTotalValue = 0
for (var j = 0; j < bodyCellsLength; j++) {
var $cell = $bodyCells[j]
$cell.classList.add('mc-bar-cell')
$cell.classList.add('mc-bar-' + (j + 1))
var cellVal = this.utils.stripValue($cell.innerText)
if (this.utils.isFloat(cellVal)) {
var parsedVal = parseFloat(cellVal, 10)
var absParsedVal = Math.abs(parsedVal)
if (parsedVal === 0) {
$cell.classList.add('mc-bar-zero')
}
if (this.options.negative) {
if (this.utils.isNegative(parsedVal)) {
$cell.classList.add('mc-bar-negative')
if (absParsedVal > maxNegativeValue) {
maxNegativeValue = absParsedVal
}
} else {
$cell.classList.add('mc-bar-positive')
}
}
// now we are done with our negative calculations
// set parsedVal to absParsedVal
parsedVal = absParsedVal
if (!this.options.stacked) {
cellsTotalValue = parsedVal
values.push(parsedVal)
} else {
cellsTotalValue += parsedVal
}
}
}
}
// if stacked, we need to push the total value of the row to the values array
if (this.options.stacked) { values.push(cellsTotalValue) }
}
var resp = {}
resp.max = parseFloat(this.utils.returnMax(values), 10)
resp.single = parseFloat(this.options.outOf / resp.max, 10)
if (this.options.negative) {
resp.marginLeft = parseFloat(maxNegativeValue, 10) * resp.single
resp.maxNegative = parseFloat(maxNegativeValue, 10)
}
return resp
}
MagnaCharta.prototype.applyWidths = function () {
this.dimensions = this.calculateMaxWidth()
var trs = this.$graph.querySelectorAll('.mc-tr')
for (var i = 0; i < trs.length; i++) {
var cells = trs[i].querySelectorAll('.mc-bar-cell')
for (var j = 0; j < cells.length; j++) {
var $cell = cells[j]
var parsedCellVal = parseFloat(this.utils.stripValue($cell.innerText), 10)
var parsedVal = parsedCellVal * this.dimensions.single
var absParsedCellVal = Math.abs(parsedCellVal)
var absParsedVal = Math.abs(parsedVal)
// apply the left margin to the positive bars
if (this.options.negative) {
if ($cell.classList.contains('mc-bar-positive')) {
$cell.style.marginLeft = this.dimensions.marginLeft + '%'
} else {
// if its negative but not the maximum negative
// we need to give it enough margin to push it further right to align
if (absParsedCellVal < this.dimensions.maxNegative) {
// left margin needs to be (largestNegVal - thisNegVal) * single
var leftMarg = (this.dimensions.maxNegative - absParsedCellVal) * this.dimensions.single
$cell.style.marginLeft = leftMarg + '%'
}
}
}
// wrap the cell value in a span tag
$cell.innerHTML = '' + $cell.innerHTML + ''
$cell.style.width = absParsedVal + '%'
}
}
}
MagnaCharta.prototype.insert = function () {
this.$table.insertAdjacentElement('afterend', this.$graph)
}
MagnaCharta.prototype.applyOutdent = function () {
/*
* this figures out if a cell needs an outdent and applies it
* it needs an outdent if the width of the text is greater than the width of the bar
* if this is the case, wrap the value in a span, and use absolute positioning
* to push it out (the bar is styled to be relative)
* unfortunately this has to be done once the chart has been inserted
*/
var cells = this.$graph.querySelectorAll('.mc-bar-cell')
for (var i = 0; i < cells.length; i++) {
var $cell = cells[i]
var cellVal = parseFloat(this.utils.stripValue($cell.innerText), 10)
var $cellSpan = $cell.querySelector('span')
var spanWidth = parseFloat(window.getComputedStyle($cellSpan, null).width.replace('px', '')) + 10 // +10 just for extra padding
var cellWidth = parseFloat(window.getComputedStyle($cell, null).width.replace('px', ''))
if (!this.options.stacked) {
// if it's 0, it is effectively outdented
if (cellVal === 0) { $cell.classList.add('mc-bar-outdented') }
if ((this.options.autoOutdent && spanWidth > cellWidth) || this.options.outdentAll) {
$cell.classList.add('mc-bar-outdented')
$cellSpan.style.marginLeft = '100%'
$cellSpan.style.display = 'inline-block'
} else {
$cell.classList.add('mc-bar-indented')
}
} else {
// if it's a stacked graph
if (spanWidth > cellWidth && cellVal > 0) {
$cell.classList.add('mc-value-overflow')
}
}
}
}
Modules.MagnaCharta = MagnaCharta
})(window.GOVUK.Modules)
|