//= require govuk/vendor/polyfills/Element/prototype/classList.js
//= require ../vendor/polyfills/closest.js
//= require ../vendor/polyfills/indexOf.js
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};
(function (Modules) {
function Gemstepnav ($module) {
this.$module = $module
this.$module.actions = {} // stores text for JS appended elements 'show' and 'hide' on steps, and 'show/hide all' button
this.$module.rememberShownStep = false
this.$module.stepNavSize = false
this.$module.sessionStoreLink = 'govuk-step-nav-active-link'
this.$module.activeLinkClass = 'gem-c-step-nav__list-item--active'
this.$module.activeStepClass = 'gem-c-step-nav__step--active'
this.$module.activeLinkHref = '#content'
this.$module.uniqueId = false
}
Gemstepnav.prototype.init = function () {
// Indicate that js has worked
this.$module.classList.add('gem-c-step-nav--active')
// Prevent FOUC, remove class hiding content
this.$module.classList.remove('js-hidden')
this.$module.stepNavSize = this.$module.classList.contains('gem-c-step-nav--large') ? 'Big' : 'Small'
this.$module.rememberShownStep = !!this.$module.hasAttribute('data-remember') && this.$module.stepNavSize === 'Big'
this.$module.steps = this.$module.querySelectorAll('.js-step')
this.$module.stepHeaders = this.$module.querySelectorAll('.js-toggle-panel')
this.$module.totalSteps = this.$module.querySelectorAll('.js-panel').length
this.$module.totalLinks = this.$module.querySelectorAll('.gem-c-step-nav__link').length
this.$module.showOrHideAllButton = false
this.$module.uniqueId = this.$module.getAttribute('data-id') || false
this.$module.dataModule = this.$module.getAttribute('data-module')
this.$module.isGa4Enabled = this.$module.dataModule ? this.$module.dataModule.indexOf('ga4-event-tracker') !== -1 : false
if (this.$module.uniqueId) {
this.$module.sessionStoreLink = this.$module.sessionStoreLink + '_' + this.$module.uniqueId
}
var stepNavTracker = new this.StepNavTracker(this.$module.uniqueId, this.$module.totalSteps, this.$module.totalLinks)
this.getTextForInsertedElements()
this.addButtonstoSteps()
this.addShowHideAllButton()
this.addShowHideToggle()
this.addAriaControlsAttrForShowHideAllButton()
this.ensureOnlyOneActiveLink()
this.showPreviouslyOpenedSteps()
this.bindToggleForSteps(stepNavTracker)
this.bindToggleShowHideAllButton(stepNavTracker)
this.bindComponentLinkClicks(stepNavTracker)
}
Gemstepnav.prototype.getTextForInsertedElements = function () {
this.$module.actions.showText = this.$module.getAttribute('data-show-text')
this.$module.actions.hideText = this.$module.getAttribute('data-hide-text')
this.$module.actions.showAllText = this.$module.getAttribute('data-show-all-text')
this.$module.actions.hideAllText = this.$module.getAttribute('data-hide-all-text')
}
Gemstepnav.prototype.addShowHideAllButton = function () {
var showAll = document.createElement('div')
var steps = this.$module.querySelectorAll('.gem-c-step-nav__steps')[0]
showAll.className = 'gem-c-step-nav__controls govuk-!-display-none-print'
showAll.innerHTML =
''
this.$module.insertBefore(showAll, steps)
this.$module.showOrHideAllButton = this.$module.querySelectorAll('.js-step-controls-button')[0]
// if GA4 is enabled, set attributes on 'show all sections' for tracking using ga4-event-tracker
if (this.$module.isGa4Enabled) {
var showAllAttributesGa4 = { event_name: 'select_content', type: 'step by step', index: { index_section: 0, index_section_count: this.$module.totalSteps } }
this.$module.showOrHideAllButton.setAttribute('data-ga4-event', JSON.stringify(showAllAttributesGa4))
}
}
Gemstepnav.prototype.addShowHideToggle = function () {
for (var i = 0; i < this.$module.stepHeaders.length; i++) {
var thisel = this.$module.stepHeaders[i]
if (!thisel.querySelectorAll('.js-toggle-link').length) {
var showHideSpan = document.createElement('span')
var showHideSpanText = document.createElement('span')
var showHideSpanIcon = document.createElement('span')
var showHideSpanFocus = document.createElement('span')
var thisSectionSpan = document.createElement('span')
showHideSpan.className = 'gem-c-step-nav__toggle-link js-toggle-link govuk-!-display-none-print'
showHideSpanText.className = 'gem-c-step-nav__button-text js-toggle-link-text'
showHideSpanIcon.className = 'gem-c-step-nav__chevron js-toggle-link-icon'
showHideSpanFocus.className = 'gem-c-step-nav__toggle-link-focus'
thisSectionSpan.className = 'govuk-visually-hidden'
showHideSpan.appendChild(showHideSpanFocus)
showHideSpanFocus.appendChild(showHideSpanIcon)
showHideSpanFocus.appendChild(showHideSpanText)
thisSectionSpan.innerHTML = ' this section'
showHideSpan.appendChild(thisSectionSpan)
thisel.querySelectorAll('.js-step-title-button')[0].appendChild(showHideSpan)
}
}
}
Gemstepnav.prototype.headerIsOpen = function (stepHeader) {
return (typeof stepHeader.parentNode.getAttribute('show') !== 'undefined')
}
Gemstepnav.prototype.addAriaControlsAttrForShowHideAllButton = function () {
var ariaControlsValue = this.$module.querySelectorAll('.js-panel')[0].getAttribute('id')
this.$module.showOrHideAllButton.setAttribute('aria-controls', ariaControlsValue)
}
// called by show all/hide all, sets all steps accordingly
Gemstepnav.prototype.setAllStepsShownState = function (isShown) {
var data = []
for (var i = 0; i < this.$module.steps.length; i++) {
var stepView = new this.StepView(this.$module.steps[i], this.$module)
stepView.setIsShown(isShown)
if (isShown) {
data.push(this.$module.steps[i].getAttribute('id'))
}
}
if (isShown) {
this.saveToSessionStorage(this.$module.uniqueId, JSON.stringify(data))
} else {
this.removeFromSessionStorage(this.$module.uniqueId)
}
}
// called on load, determines whether each step should be open or closed
Gemstepnav.prototype.showPreviouslyOpenedSteps = function () {
var data = this.loadFromSessionStorage(this.$module.uniqueId) || []
for (var i = 0; i < this.$module.steps.length; i++) {
var thisel = this.$module.steps[i]
var id = thisel.getAttribute('id')
var stepView = new this.StepView(thisel, this.$module)
var shouldBeShown = thisel.hasAttribute('data-show')
// show the step if it has been remembered or if it has the 'data-show' attribute
if ((this.$module.rememberShownStep && data.indexOf(id) > -1) || (shouldBeShown && shouldBeShown !== 'undefined')) {
stepView.setIsShown(true)
} else {
stepView.setIsShown(false)
}
}
if (data.length > 0) {
this.$module.showOrHideAllButton.setAttribute('aria-expanded', true)
this.setShowHideAllText()
}
}
Gemstepnav.prototype.addButtonstoSteps = function () {
for (var i = 0; i < this.$module.steps.length; i++) {
var thisel = this.$module.steps[i]
var title = thisel.querySelectorAll('.js-step-title')[0]
var contentId = thisel.querySelectorAll('.js-panel')[0].getAttribute('id')
var titleText = title.textContent || title.innerText // IE8 fallback
title.outerHTML =
'' +
'' +
''
if (this.$module.isGa4Enabled) {
var ga4JSON = {
event_name: 'select_content',
type: 'step by step',
text: titleText.trim(),
index: {
index_section: i + 1,
index_section_count: this.$module.totalSteps
},
index_total: thisel.querySelectorAll('a').length
}
var button = thisel.querySelector('.js-step-title-button')
button.setAttribute('data-ga4-event', JSON.stringify(ga4JSON))
}
}
}
Gemstepnav.prototype.bindToggleForSteps = function (stepNavTracker) {
var that = this
var togglePanels = this.$module.querySelectorAll('.js-toggle-panel')
for (var i = 0; i < togglePanels.length; i++) {
togglePanels[i].addEventListener('click', function (event) {
var stepView = new that.StepView(this.parentNode, that.$module)
stepView.toggle()
var stepIsOptional = this.parentNode.hasAttribute('data-optional')
var toggleClick = new that.StepToggleClick(event, stepView, stepNavTracker, stepIsOptional, that.$module.stepNavSize)
toggleClick.trackClick()
that.setShowHideAllText()
that.rememberStepState(this.parentNode)
})
}
}
// if the step is open, store its id in session store
// if the step is closed, remove its id from session store
Gemstepnav.prototype.rememberStepState = function (step) {
if (this.$module.rememberShownStep) {
var data = JSON.parse(this.loadFromSessionStorage(this.$module.uniqueId)) || []
var thisstep = step.getAttribute('id')
var shown = step.classList.contains('step-is-shown')
if (shown) {
data.push(thisstep)
} else {
var i = data.indexOf(thisstep)
if (i > -1) {
data.splice(i, 1)
}
}
this.saveToSessionStorage(this.$module.uniqueId, JSON.stringify(data))
}
}
// tracking click events on links in step content
Gemstepnav.prototype.bindComponentLinkClicks = function (stepNavTracker) {
var jsLinks = this.$module.querySelectorAll('.js-link')
var that = this
for (var i = 0; i < jsLinks.length; i++) {
jsLinks[i].addEventListener('click', function (event) {
var dataPosition = this.getAttribute('data-position')
var linkClick = new that.ComponentLinkClick(event, stepNavTracker, dataPosition, that.$module.stepNavSize)
linkClick.trackClick()
if (this.getAttribute('rel') !== 'external') {
that.saveToSessionStorage(that.$module.sessionStoreLink, dataPosition)
}
if (this.getAttribute('href') === that.$module.activeLinkHref) {
that.setOnlyThisLinkActive(this)
that.setActiveStepClass()
}
})
}
}
Gemstepnav.prototype.saveToSessionStorage = function (key, value) {
window.sessionStorage.setItem(key, value)
}
Gemstepnav.prototype.loadFromSessionStorage = function (key, value) {
return window.sessionStorage.getItem(key)
}
Gemstepnav.prototype.removeFromSessionStorage = function (key) {
window.sessionStorage.removeItem(key)
}
Gemstepnav.prototype.setOnlyThisLinkActive = function (clicked) {
var allActiveLinks = this.$module.querySelectorAll('.' + this.$module.activeLinkClass)
for (var i = 0; i < allActiveLinks.length; i++) {
allActiveLinks[i].classList.remove(this.$module.activeLinkClass)
}
clicked.parentNode.classList.add(this.$module.activeLinkClass)
}
// if a link occurs more than once in a step nav, the backend doesn't know which one to highlight
// so it gives all those links the 'active' attribute and highlights the last step containing that link
// if the user clicked on one of those links previously, it will be in the session store
// this code ensures only that link and its corresponding step have the highlighting
// otherwise it accepts what the backend has already passed to the component
Gemstepnav.prototype.ensureOnlyOneActiveLink = function () {
var activeLinks = this.$module.querySelectorAll('.js-list-item.' + this.$module.activeLinkClass)
if (activeLinks.length <= 1) {
return
}
var loaded = this.loadFromSessionStorage(this.$module.sessionStoreLink)
var activeParent = this.$module.querySelectorAll('.' + this.$module.activeLinkClass)[0]
var activeChild = activeParent.firstChild
var foundLink = activeChild.getAttribute('data-position')
var lastClicked = loaded || foundLink // the value saved has priority
// it's possible for the saved link position value to not match any of the currently duplicate highlighted links
// so check this otherwise it'll take the highlighting off all of them
var checkLink = this.$module.querySelectorAll('[data-position="' + lastClicked + '"]')[0]
if (checkLink) {
if (!checkLink.parentNode.classList.contains(this.$module.activeLinkClass)) {
lastClicked = checkLink
}
} else {
lastClicked = foundLink
}
this.removeActiveStateFromAllButCurrent(activeLinks, lastClicked)
this.setActiveStepClass()
}
Gemstepnav.prototype.removeActiveStateFromAllButCurrent = function (activeLinks, current) {
for (var i = 0; i < activeLinks.length; i++) {
var thisel = activeLinks[i]
if (thisel.querySelectorAll('.js-link')[0].getAttribute('data-position').toString() !== current.toString()) {
thisel.classList.remove(this.$module.activeLinkClass)
var visuallyHidden = thisel.querySelectorAll('.visuallyhidden')
if (visuallyHidden.length) {
visuallyHidden[0].parentNode.removeChild(visuallyHidden[0])
}
}
}
}
Gemstepnav.prototype.setActiveStepClass = function () {
// remove the 'active/open' state from all steps
var allActiveSteps = this.$module.querySelectorAll('.' + this.$module.activeStepClass)
for (var i = 0; i < allActiveSteps.length; i++) {
allActiveSteps[i].classList.remove(this.$module.activeStepClass)
allActiveSteps[i].removeAttribute('data-show')
}
// find the current page link and apply 'active/open' state to parent step
var activeLink = this.$module.querySelectorAll('.' + this.$module.activeLinkClass)[0]
if (activeLink) {
var activeStep = activeLink.closest('.gem-c-step-nav__step')
activeStep.classList.add(this.$module.activeStepClass)
activeStep.setAttribute('data-show', '')
}
}
Gemstepnav.prototype.bindToggleShowHideAllButton = function (stepNavTracker) {
var that = this
this.$module.showOrHideAllButton.addEventListener('click', function (event) {
var textContent = this.textContent || this.innerText
var shouldShowAll = textContent === that.$module.actions.showAllText
// Fire GA click tracking
stepNavTracker.trackClick('pageElementInteraction', (shouldShowAll ? 'stepNavAllShown' : 'stepNavAllHidden'), {
label: (shouldShowAll ? that.$module.actions.showAllText : that.$module.actions.hideAllText) + ': ' + that.$module.stepNavSize
})
that.setAllStepsShownState(shouldShowAll)
that.$module.showOrHideAllButton.setAttribute('aria-expanded', shouldShowAll)
that.setShowHideAllText()
return false
})
}
Gemstepnav.prototype.setShowHideAllText = function () {
var shownSteps = this.$module.querySelectorAll('.step-is-shown').length
var showAllChevon = this.$module.showOrHideAllButton.querySelector('.js-step-controls-button-icon')
var showAllButtonText = this.$module.showOrHideAllButton.querySelector('.js-step-controls-button-text')
// Find out if the number of is-opens == total number of steps
var shownStepsIsTotalSteps = shownSteps === this.$module.totalSteps
if (shownStepsIsTotalSteps) {
showAllButtonText.innerHTML = this.$module.actions.hideAllText
showAllChevon.classList.remove('gem-c-step-nav__chevron--down')
} else {
showAllButtonText.innerHTML = this.$module.actions.showAllText
showAllChevon.classList.add('gem-c-step-nav__chevron--down')
}
}
Gemstepnav.prototype.StepView = function (stepElement, $module) {
this.stepElement = stepElement
this.stepContent = this.stepElement.querySelectorAll('.js-panel')[0]
this.titleButton = this.stepElement.querySelectorAll('.js-step-title-button')[0]
var textElement = this.stepElement.querySelectorAll('.js-step-title-text')[0]
this.title = textElement.textContent || textElement.innerText
this.title = this.title.replace(/^\s+|\s+$/g, '') // this is 'trim' but supporting IE8
this.showText = $module.actions.showText
this.hideText = $module.actions.hideText
this.upChevronSvg = $module.upChevronSvg
this.downChevronSvg = $module.downChevronSvg
this.show = function () {
this.setIsShown(true)
}
this.hide = function () {
this.setIsShown(false)
}
this.toggle = function () {
this.setIsShown(this.isHidden())
}
this.setIsShown = function (isShown) {
var toggleLink = this.stepElement.querySelectorAll('.js-toggle-link')[0]
var toggleLinkText = toggleLink.querySelector('.js-toggle-link-text')
var stepChevron = toggleLink.querySelector('.js-toggle-link-icon')
if (isShown) {
this.stepElement.classList.add('step-is-shown')
this.stepContent.classList.remove('js-hidden')
toggleLinkText.innerHTML = this.hideText
stepChevron.classList.remove('gem-c-step-nav__chevron--down')
} else {
this.stepElement.classList.remove('step-is-shown')
this.stepContent.classList.add('js-hidden')
toggleLinkText.innerHTML = this.showText
stepChevron.classList.add('gem-c-step-nav__chevron--down')
}
this.titleButton.setAttribute('aria-expanded', isShown)
}
this.isShown = function () {
return this.stepElement.classList.contains('step-is-shown')
}
this.isHidden = function () {
return !this.isShown()
}
this.numberOfContentItems = function () {
return this.stepContent.querySelectorAll('.js-link').length
}
}
Gemstepnav.prototype.StepToggleClick = function (event, stepView, stepNavTracker, stepIsOptional, stepNavSize) {
this.target = event.target
this.stepIsOptional = stepIsOptional
this.stepNavSize = stepNavSize
this.trackClick = function () {
var trackingOptions = { label: this.trackingLabel(), dimension28: stepView.numberOfContentItems().toString() }
stepNavTracker.trackClick('pageElementInteraction', this.trackingAction(), trackingOptions)
}
this.trackingLabel = function () {
var clickedNearbyToggle = this.target.closest('.js-step').querySelectorAll('.js-toggle-panel')[0]
return clickedNearbyToggle.getAttribute('data-position') + ' - ' + stepView.title + ' - ' + this.locateClickElement() + ': ' + this.stepNavSize + this.isOptional()
}
// returns index of the clicked step in the overall number of steps
this.stepIndex = function () { // eslint-disable-line no-unused-vars
return this.$module.steps.index(stepView.element) + 1
}
this.trackingAction = function () {
return (stepView.isHidden() ? 'stepNavHidden' : 'stepNavShown')
}
this.locateClickElement = function () {
if (this.clickedOnIcon()) {
return this.iconType() + ' click'
} else if (this.clickedOnHeading()) {
return 'Heading click'
} else {
return 'Elsewhere click'
}
}
this.clickedOnIcon = function () {
return this.target.classList.contains('js-toggle-link')
}
this.clickedOnHeading = function () {
return this.target.classList.contains('js-step-title-text')
}
this.iconType = function () {
return (stepView.isHidden() ? 'Minus' : 'Plus')
}
this.isOptional = function () {
return (this.stepIsOptional ? ' ; optional' : '')
}
}
Gemstepnav.prototype.ComponentLinkClick = function (event, stepNavTracker, linkPosition, size) {
this.size = size
this.target = event.target
this.trackClick = function () {
var trackingOptions = { label: this.target.getAttribute('href') + ' : ' + this.size }
var dimension28 = this.target.closest('.gem-c-step-nav__list').getAttribute('data-length')
if (dimension28) {
trackingOptions.dimension28 = dimension28
}
stepNavTracker.trackClick('stepNavLinkClicked', linkPosition, trackingOptions)
}
}
// A helper that sends a custom event request to Google Analytics if
// the GOVUK module is setup
Gemstepnav.prototype.StepNavTracker = function (uniqueId, totalSteps, totalLinks) {
this.totalSteps = totalSteps
this.totalLinks = totalLinks
this.uniqueId = uniqueId
this.trackClick = function (category, action, options) {
// dimension26 records the total number of expand/collapse steps in this step nav
// dimension27 records the total number of links in this step nav
// dimension28 records the number of links in the step that was shown/hidden (handled in click event)
if (window.GOVUK.analytics && window.GOVUK.analytics.trackEvent) {
options = options || {}
options.dimension26 = options.dimension26 || this.totalSteps.toString()
options.dimension27 = options.dimension27 || this.totalLinks.toString()
options.dimension96 = options.dimension96 || this.uniqueId
window.GOVUK.analytics.trackEvent(category, action, options)
}
}
}
Modules.Gemstepnav = Gemstepnav
})(window.GOVUK.Modules)