//= 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 ($module, options) { this.$table = $module this.options = { outOf: 65, applyOnInit: true, autoOutdent: false, outdentAll: false, chartVisibleText: 'Change to table and accessible view', tableVisibleText: 'Change to chart view', chartAlertText: 'Chart visible', tableAlertText: 'Table visible', 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] } MagnaCharta.prototype.init = function () { 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) // a container around the graph element so that it can be targeted by screen readers, allowing us to inform screen reader users that the graph isn't accessible this.$graphContainer = document.createElement('div') this.$graphContainer.className = 'mc-chart-container' // 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') // get the id of the current chart within the page so that it can be used during the generation of the toggleLink this.chartId = this.getChartId() // 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 moreThanTwoCells = this.$table.querySelectorAll('tbody tr')[0].querySelectorAll('th, td').length > 2 this.options.multiple = !this.options.stacked && (this.$table.classList.contains('mc-multiple') || moreThanTwoCells) // 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('mc-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 allTheTHsInTableHead = this.$table.querySelectorAll('thead th') for (var i = 0; i < allTheTHsInTableHead.length; i++) { output += '
' output += allTheTHsInTableHead[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 allTheTableBodyCells = allTheTbodyTrs[i].querySelectorAll('th, td') for (var j = 0; j < allTheTableBodyCells.length; j++) { cellsOutput += '
' cellsOutput += allTheTableBodyCells[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 (chartVisibleText) { var link = document.createElement('button') // These spans are for managing the content within the button // toggleText is the public facing content whilst toggleStatus is visually hidden content that we use to let screen reader users know that the toggle link has been clicked var toggleText = document.createElement('span') var toggleStatus = document.createElement('span') toggleText.classList.add('mc-toggle-text') toggleText.innerHTML = chartVisibleText toggleStatus.classList.add('govuk-visually-hidden', 'mc-toggle-status') toggleStatus.setAttribute('role', 'alert') link.classList.add('govuk-body-s', 'mc-toggle-button') link.appendChild(toggleText) link.appendChild(toggleStatus) return link } // toggles between showing the table and showing the chart MagnaCharta.prototype.addToggleClick = function (chartVisible, tableVisible, chartAlert, tableAlert) { var that = this this.toggleLink.addEventListener('click', function (e) { e.preventDefault() var toggleText = that.toggleLink.querySelector('.mc-toggle-text') var toggleStatus = that.toggleLink.querySelector('.mc-toggle-status') that.$graphContainer.classList.toggle('mc-hidden') that.$table.classList.toggle('mc-hidden') toggleText.innerHTML = toggleText.innerHTML === tableVisible ? chartVisible : tableVisible toggleStatus.innerHTML = toggleStatus.innerHTML === tableAlert ? chartAlert : tableAlert }) } 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.chartVisibleText) this.addToggleClick(this.options.chartVisibleText, this.options.tableVisibleText, this.options.chartAlertText, this.options.tableAlertText) 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) { return val.replace(/,|£|%|[a-z]/gi, '') }, 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 () { var label = document.createElement('span') var labelId = 'mc-chart-not-accessible-' + this.chartId label.innerHTML = 'This content is not accessible - switch to table' label.className = 'mc-hidden' label.id = labelId this.$graphContainer.setAttribute('aria-labelledby', labelId) this.$graphContainer.appendChild(this.$graph) this.$graphContainer.appendChild(label) this.$table.insertAdjacentElement('afterend', this.$graphContainer) } 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 = $cell.querySelector('span').offsetWidth + 5 // +5 just for extra padding var cellWidth = $cell.offsetWidth 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) || (cellVal < 1)) { $cell.classList.add('mc-value-overflow') } } } } MagnaCharta.prototype.getChartId = function () { var allCharts = document.querySelectorAll('table.js-barchart-table') var id = null for (var i = 0; i < allCharts.length; i++) { if (allCharts[i] === this.$table) { id = i } } return id } Modules.MagnaCharta = MagnaCharta })(window.GOVUK.Modules)