Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tasklist component #1173

Merged
merged 2 commits into from
Nov 1, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added app/assets/images/icon-plus-minus-small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/icon-plus-minus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/images/icon-plus-minus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/icon-plus-minus_orig.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions app/assets/javascripts/current-location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// used by the tasklist component

(function(root) {
"use strict";
window.GOVUK = window.GOVUK || {};

GOVUK.getCurrentLocation = function(){
return root.location;
};
}(window));
333 changes: 333 additions & 0 deletions app/assets/javascripts/govuk-component/tasklist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
// Most of this is originally from the service manual but has changed considerably since then

(function (Modules) {
"use strict";
window.GOVUK = window.GOVUK || {};

Modules.Tasklist = function () {

var bulkActions = {
openAll: {
buttonText: "Open all",
eventLabel: "Open All"
},
closeAll: {
buttonText: "Close all",
eventLabel: "Close All"
}
};

var rememberOpenSection = false;

this.start = function ($element) {

$(window).unload(storeScrollPosition);

// Indicate that js has worked
$element.addClass('pub-c-task-list--active');

// Prevent FOUC, remove class hiding content
$element.removeClass('js-hidden');

rememberOpenSection = !!$element.filter('[data-remember]').length;
var $sections = $element.find('.js-section');
var $sectionHeaders = $element.find('.js-toggle-panel');
var totalSections = $element.find('.js-panel').length;

var $openOrCloseAllButton;

var tasklistTracker = new TasklistTracker(totalSections);

addButtonstoSections();
addOpenCloseAllButton();
addIconsToSections();
addAriaControlsAttrForOpenCloseAllButton();

closeAllSections();
openLinkedSection();

bindToggleForSections(tasklistTracker);
bindToggleOpenCloseAllButton(tasklistTracker);

// When navigating back in browser history to the tasklist, the browser will try to be "clever" and return
// the user to their previous scroll position. However, since we collapse all but the currently-anchored
// section, the content length changes and the user is returned to the wrong position (often the footer).
// In order to correct this behaviour, as the user leaves the page, we anticipate the correct height we wish the
// user to return to by forcibly scrolling them to that height, which becomes the height the browser will return
// them to.
// If we can't find an element to return them to, then reset the scroll to the top of the page. This handles
// the case where the user has expanded all sections, so they are not returned to a particular section, but
// still could have scrolled a long way down the page.
function storeScrollPosition() {
closeAllSections();
var $section = getSectionForAnchor();

document.body.scrollTop = $section && $section.length
? $section.offset().top
: 0;
}

function addOpenCloseAllButton() {
$element.prepend('<div class="pub-c-task-list__controls"><button aria-expanded="false" class="pub-c-task-list__button pub-c-task-list__button--controls js-section-controls-button">' + bulkActions.openAll.buttonText + '</button></div>');
}

function addIconsToSections() {
$sectionHeaders.append('<span class="pub-c-task-list__icon pub-c-task-list__icon--plus"></span>');
$sectionHeaders.append('<span class="pub-c-task-list__icon pub-c-task-list__icon--minus"></span>');
}

function addAriaControlsAttrForOpenCloseAllButton() {
var ariaControlsValue = "";
var $sectionPanels = $element.find('.js-panel')
for (var i = 0; i < totalSections; i++) {
ariaControlsValue += $sectionPanels[i].id + " "
}

$openOrCloseAllButton = $element.find('.js-section-controls-button');
$openOrCloseAllButton.attr('aria-controls', ariaControlsValue);
}

function closeAllSections() {
setAllSectionsOpenState(false);
}

function setAllSectionsOpenState(isOpen) {
$.each($sections, function () {
var sectionView = new SectionView($(this));
sectionView.preventHashUpdate();
sectionView.setIsOpen(isOpen);
});
}

function openLinkedSection() {
var $section;
if (rememberOpenSection) {
$section = getSectionForAnchor();
}
else {
$section = $sections.filter('[data-open]');
}

if ($section && $section.length) {
var sectionView = new SectionView($section);
sectionView.open();
}
}

function getSectionForAnchor() {
var anchor = getActiveAnchor();

return anchor.length
? $element.find('#' + escapeSelector(anchor.substr(1)))
: null;
}

function getActiveAnchor() {
return GOVUK.getCurrentLocation().hash;
}

function addButtonstoSections() {
$.each($sections, function () {
var $section = $(this);
var $title = $section.find('.js-section-title');
var contentId = $section.find('.js-panel').first().attr('id');

$title.wrapInner(
'<button ' +
'class="pub-c-task-list__button pub-c-task-list__button--title js-section-title-button" ' +
'aria-expanded="false" aria-controls="' + contentId + '">' +
'</button>' );
});
}

function bindToggleForSections(tasklistTracker) {
$element.find('.js-toggle-panel').click(function (event) {
preventLinkFollowingForCurrentTab(event);

var sectionView = new SectionView($(this).closest('.js-section'));
sectionView.toggle();

var toggleClick = new SectionToggleClick(sectionView, $sections, tasklistTracker);
toggleClick.track();

setOpenCloseAllText();
});
}

function preventLinkFollowingForCurrentTab(event) {
// If the user is holding the ⌘ or Ctrl key, they're trying
// to open the link in a new window, so let the click happen
if (event.metaKey || event.ctrlKey) {
return;
}

event.preventDefault();
}

function bindToggleOpenCloseAllButton(tasklistTracker) {
$openOrCloseAllButton = $element.find('.js-section-controls-button');
$openOrCloseAllButton.on('click', function () {
var shouldOpenAll;

if ($openOrCloseAllButton.text() == bulkActions.openAll.buttonText) {
$openOrCloseAllButton.text(bulkActions.closeAll.buttonText);
shouldOpenAll = true;

tasklistTracker.track('pageElementInteraction', 'tasklistAllOpened', {
label: bulkActions.openAll.eventLabel
});
} else {
$openOrCloseAllButton.text(bulkActions.openAll.buttonText);
shouldOpenAll = false;

tasklistTracker.track('pageElementInteraction', 'tasklistAllClosed', {
label: bulkActions.closeAll.eventLabel
});
}

setAllSectionsOpenState(shouldOpenAll);
$openOrCloseAllButton.attr('aria-expanded', shouldOpenAll);
setOpenCloseAllText();
setHash(null);

return false;
});
}

function setOpenCloseAllText() {
var openSections = $element.find('.section-is-open').length;
// Find out if the number of is-opens == total number of sections
if (openSections === totalSections) {
$openOrCloseAllButton.text(bulkActions.closeAll.buttonText);
} else {
$openOrCloseAllButton.text(bulkActions.openAll.buttonText);
}
}

// Ideally we'd use jQuery.escapeSelector, but this is only available from v3
// See https://github.com/jquery/jquery/blob/2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/selector-native.js#L46
function escapeSelector(s) {
var cssMatcher = /([\x00-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;
return s.replace(cssMatcher, "\\$&");
}
};

function SectionView($sectionElement) {
var $titleLink = $sectionElement.find('.js-section-title-button');
var $sectionContent = $sectionElement.find('.js-panel');
var shouldUpdateHash = rememberOpenSection;

this.title = $sectionElement.find('.js-section-title').text();
this.href = $titleLink.attr('href');
this.element = $sectionElement;

this.open = open;
this.close = close;
this.toggle = toggle;
this.setIsOpen = setIsOpen;
this.isOpen = isOpen;
this.isClosed = isClosed;
this.preventHashUpdate = preventHashUpdate;
this.numberOfContentItems = numberOfContentItems;

function open() {
setIsOpen(true);
}

function close() {
setIsOpen(false);
}

function toggle() {
setIsOpen(isClosed());
}

function setIsOpen(isOpen) {
$sectionElement.toggleClass('section-is-open', isOpen);
$sectionContent.toggleClass('js-hidden', !isOpen);
$titleLink.attr("aria-expanded", isOpen);

if (shouldUpdateHash) {
updateHash($sectionElement);
}
}

function isOpen() {
return $sectionElement.hasClass('section-is-open');
}

function isClosed() {
return !isOpen();
}

function preventHashUpdate() {
shouldUpdateHash = false;
}

function numberOfContentItems() {
return $sectionContent.find('li').length;
}
}

function updateHash($sectionElement) {
var sectionView = new SectionView($sectionElement);
var hash = sectionView.isOpen() && '#' + $sectionElement.attr('id');
setHash(hash)
}

// Sets the hash for the page. If a falsy value is provided, the hash is cleared.
function setHash(hash) {
if (!GOVUK.support.history()) {
return;
}

var newLocation = hash || GOVUK.getCurrentLocation().pathname;
history.replaceState({}, '', newLocation);
}

function SectionToggleClick(sectionView, $sections, tasklistTracker) {
this.track = trackClick;

function trackClick() {
var tracking_options = {label: trackingLabel(), dimension28: sectionView.numberOfContentItems().toString()}
tasklistTracker.track('pageElementInteraction', trackingAction(), tracking_options);

if (!sectionView.isClosed()) {
tasklistTracker.track(
'navtasklistLinkClicked',
String(sectionIndex()),
{
label: sectionView.href,
dimension28: String(sectionView.numberOfContentItems()),
dimension29: sectionView.title
}
)
}
}

function trackingLabel() {
return sectionIndex() + '. ' + sectionView.title;
}

function sectionIndex() {
return $sections.index(sectionView.element) + 1;
}

function trackingAction() {
return (sectionView.isClosed() ? 'tasklistClosed' : 'tasklistOpened');
}
}

// A helper that sends a custom event request to Google Analytics if
// the GOVUK module is setup
function TasklistTracker(totalSections) {
this.track = function(category, action, options) {
if (GOVUK.analytics && GOVUK.analytics.trackEvent) {
options = options || {};
options["dimension28"] = options["dimension28"] || totalSections.toString();
GOVUK.analytics.trackEvent(category, action, options);
}
}
}
};
})(window.GOVUK.Modules);
2 changes: 2 additions & 0 deletions app/assets/javascripts/header-footer-only.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
//= require govuk-component/govspeak-convert-html-pub-charts
//= require govuk-component/option-select
//= require govuk/shim-links-with-button-role
//= require history-support
//= require current-location
8 changes: 8 additions & 0 deletions app/assets/javascripts/history-support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// used by the tasklist component

if(typeof window.GOVUK === 'undefined'){ window.GOVUK = {}; }
if(typeof window.GOVUK.support === 'undefined'){ window.GOVUK.support = {}; }

window.GOVUK.support.history = function() {
return window.history && window.history.pushState && window.history.replaceState;
}
1 change: 1 addition & 0 deletions app/assets/javascripts/start-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// = require modules/toggle
// = require modules/toggle-input-class-on-focus
// = require modules/track-click
// = require govuk-component/tasklist

$(document).ready(function () {
GOVUK.modules.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
@import "metadata-print";
@import "related-items-print";
@import "title-print";
@import "task-list-print";
1 change: 1 addition & 0 deletions app/assets/stylesheets/govuk-component/_component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
@import "search";
@import "button";
@import "lead-paragraph";
@import "task-list";
Loading