diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b199d941..0ba6530beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ## Unreleased * Add public/frontend layout component ([PR #1265](https://github.com/alphagov/govuk_publishing_components/pull/1265)) +* Replace jQuery in checkboxes.js ([PR #1620](https://github.com/alphagov/govuk_publishing_components/pull/1620)) ## 21.60.3 diff --git a/app/assets/javascripts/govuk_publishing_components/components/checkboxes.js b/app/assets/javascripts/govuk_publishing_components/components/checkboxes.js index 5e794bfaf7..b1d520b779 100644 --- a/app/assets/javascripts/govuk_publishing_components/components/checkboxes.js +++ b/app/assets/javascripts/govuk_publishing_components/components/checkboxes.js @@ -1,94 +1,123 @@ /* eslint-env jquery */ +// = require govuk/vendor/polyfills/Element/prototype/closest.js // = require govuk/components/checkboxes/checkboxes.js window.GOVUK = window.GOVUK || {} window.GOVUK.Modules = window.GOVUK.Modules || {} window.GOVUK.Modules.Checkboxes = window.GOVUKFrontend; (function (Modules) { - 'use strict' - - Modules.GovukCheckboxes = function () { - this.start = function (scope) { - var _this = this - this.applyAriaControlsAttributes(scope) - - $(scope).on('change', '[data-nested=true] input[type=checkbox]', function (e) { - var checkbox = e.target - var isNested = $(checkbox).closest('.govuk-checkboxes--nested') - var hasNested = $('.govuk-checkboxes--nested[data-parent=' + checkbox.id + ']') - - if (hasNested.length) { - _this.toggleNestedCheckboxes(hasNested, checkbox) - } else if (isNested.length) { - _this.toggleParentCheckbox(isNested, checkbox) - } - }) - - $(scope).on('change', 'input[type=checkbox]', function (e) { - if (window.GOVUK.analytics && window.GOVUK.analytics.trackEvent) { - // where checkboxes are manipulated externally in finders, suppressAnalytics - // is passed to prevent duplicate GA events - if (typeof e.suppressAnalytics === 'undefined' || e.suppressAnalytics !== true) { - var $checkbox = $(e.target) - var category = $checkbox.data('track-category') - if (typeof category !== 'undefined') { - var isChecked = $checkbox.is(':checked') - var uncheckTrackCategory = $checkbox.data('uncheck-track-category') - if (!isChecked && typeof uncheckTrackCategory !== 'undefined') { - category = uncheckTrackCategory - } - var action = $checkbox.data('track-action') - var options = $checkbox.data('track-options') - if (typeof options !== 'object' || options === null) { - options = {} - } - options.value = $checkbox.data('track-value') - options.label = $checkbox.data('track-label') - window.GOVUK.analytics.trackEvent(category, action, options) - } + function GovukCheckboxes () { } + + GovukCheckboxes.prototype.start = function ($module) { + this.$module = $module[0] + this.$checkboxes = this.$module.querySelectorAll('input[type=checkbox]') + this.$nestedCheckboxes = this.$module.querySelectorAll('[data-nested=true] input[type=checkbox]') + this.$exclusiveCheckboxes = this.$module.querySelectorAll('[data-exclusive=true] input[type=checkbox]') + + this.applyAriaControlsAttributes(this.$module) + + for (var i = 0; i < this.$checkboxes.length; i++) { + this.$checkboxes[i].addEventListener('change', this.handleCheckboxChange) + } + + for (i = 0; i < this.$nestedCheckboxes.length; i++) { + this.$nestedCheckboxes[i].addEventListener('change', this.handleNestedCheckboxChange.bind(this)) + } + + for (i = 0; i < this.$exclusiveCheckboxes.length; i++) { + this.$exclusiveCheckboxes[i].addEventListener('change', this.handleExclusiveCheckboxChange) + } + } + + GovukCheckboxes.prototype.handleCheckboxChange = function (event) { + if (window.GOVUK.analytics && window.GOVUK.analytics.trackEvent) { + // Where checkboxes are manipulated externally in finders, `suppressAnalytics` + // is passed to prevent duplicate GA events. + if (!event.detail || (event.detail && event.detail.suppressAnalytics !== true)) { + var $checkbox = event.target + var category = $checkbox.getAttribute('data-track-category') + if (category) { + var uncheckTrackCategory = $checkbox.getAttribute('data-uncheck-track-category') + if (!$checkbox.checked && uncheckTrackCategory) { + category = uncheckTrackCategory } + var action = $checkbox.getAttribute('data-track-action') + var options = $checkbox.getAttribute('data-track-options') + if (options) { + options = JSON.parse(options) + } else { + options = {} + } + options.value = $checkbox.getAttribute('data-track-value') + options.label = $checkbox.getAttribute('data-track-label') + window.GOVUK.analytics.trackEvent(category, action, options) } - }) - - $(scope).on('change', '[data-exclusive=true] input[type=checkbox]', function (e) { - var currentCheckbox = e.target - var checkboxes = currentCheckbox.closest('.govuk-checkboxes') - var exclusiveOption = $(checkboxes).find('input[type=checkbox][data-exclusive]') - var nonExclusiveOptions = $(checkboxes).find('input[type=checkbox]:not([data-exclusive])') - - if (currentCheckbox.dataset.exclusive === 'true' && currentCheckbox.checked === true) { - nonExclusiveOptions.each(function () { - $(this).prop('checked', false) - }) - } else if (currentCheckbox.dataset.exclusive !== 'true' && currentCheckbox.checked === true) { - exclusiveOption.prop('checked', false) - } - }) + } } + } - this.toggleNestedCheckboxes = function (scope, checkbox) { - if (checkbox.checked) { - scope.find('input[type=checkbox]').prop('checked', true) - } else { - scope.find('input[type=checkbox]').prop('checked', false) + GovukCheckboxes.prototype.handleNestedCheckboxChange = function (event) { + var $checkbox = event.target + var $isNested = $checkbox.closest('.govuk-checkboxes--nested') + var $hasNested = this.$module.querySelector('.govuk-checkboxes--nested[data-parent=' + $checkbox.id + ']') + + if ($hasNested) { + this.toggleNestedCheckboxes($hasNested, $checkbox) + } else if ($isNested) { + this.toggleParentCheckbox($isNested, $checkbox) + } + } + + GovukCheckboxes.prototype.toggleNestedCheckboxes = function ($scope, $checkbox) { + var $nestedCheckboxes = $scope.querySelectorAll('input[type=checkbox]') + if ($checkbox.checked) { + for (var i = 0; i < $nestedCheckboxes.length; i++) { + $nestedCheckboxes[i].checked = true } + } else { + for (i = 0; i < $nestedCheckboxes.length; i++) { + $nestedCheckboxes[i].checked = false + } + } + } + + GovukCheckboxes.prototype.toggleParentCheckbox = function ($scope, $checkbox) { + var $inputs = $scope.querySelectorAll('input') + var $checkedInputs = $scope.querySelectorAll('input:checked') + var parentId = $scope.getAttribute('data-parent') + var $parent = document.getElementById(parentId) + + if ($checkbox.checked && $inputs.length === $checkedInputs.length) { + $parent.checked = true + } else { + $parent.checked = false } + } - this.toggleParentCheckbox = function (scope, checkbox) { - var siblings = $(checkbox).closest('.govuk-checkboxes__item').siblings() - var parentId = scope.data('parent') + GovukCheckboxes.prototype.handleExclusiveCheckboxChange = function (event) { + var $currentCheckbox = event.target + var $checkboxes = $currentCheckbox.closest('.govuk-checkboxes') + var $exclusiveOption = $checkboxes.querySelector('input[type=checkbox][data-exclusive]') + var $nonExclusiveOptions = $checkboxes.querySelectorAll('input[type=checkbox]:not([data-exclusive])') - if (checkbox.checked && siblings.length === siblings.find(':checked').length) { - $('#' + parentId).prop('checked', true) - } else { - $('#' + parentId).prop('checked', false) + if ($currentCheckbox.dataset.exclusive === 'true' && $currentCheckbox.checked === true) { + for (var i = 0; i < $nonExclusiveOptions.length; i++) { + $nonExclusiveOptions[i].checked = false + } + } else if ($currentCheckbox.dataset.exclusive !== 'true' && $currentCheckbox.checked === true) { + if ($exclusiveOption) { + $exclusiveOption.checked = false } } + } + + GovukCheckboxes.prototype.applyAriaControlsAttributes = function ($scope) { + var $inputs = $scope.querySelectorAll('[data-controls]') - this.applyAriaControlsAttributes = function (scope) { - $(scope).find('[data-controls]').each(function () { - $(this).attr('aria-controls', $(this).attr('data-controls')) - }) + for (var i = 0; i < $inputs.length; i++) { + $inputs[i].setAttribute('aria-controls', $inputs[i].getAttribute('data-controls')) } } + + Modules.GovukCheckboxes = GovukCheckboxes })(window.GOVUK.Modules) diff --git a/spec/javascripts/components/checkboxes-spec.js b/spec/javascripts/components/checkboxes-spec.js index 1347454392..0f12415ebe 100644 --- a/spec/javascripts/components/checkboxes-spec.js +++ b/spec/javascripts/components/checkboxes-spec.js @@ -70,8 +70,8 @@ describe('Checkboxes component', function () { $checkboxesWrapper = $('.gem-c-checkboxes') $exclusiveOption = $checkboxesWrapper.find('input[type=checkbox][data-exclusive]') $nonExclusiveOptions = $checkboxesWrapper.find('input[type=checkbox]:not([data-exclusive])') - expectedRedOptions = { label: 'red', value: 1, dimension28: 'wubbalubbadubdub', dimension29: 'Pickle Rick' } - expectedBlueOptions = { label: 'blue', value: 2, dimension28: 'Get schwifty', dimension29: 'Squanch' } + expectedRedOptions = { label: 'red', value: '1', dimension28: 'wubbalubbadubdub', dimension29: 'Pickle Rick' } + expectedBlueOptions = { label: 'blue', value: '2', dimension28: 'Get schwifty', dimension29: 'Squanch' } GOVUK.analytics = { trackEvent: function () {} @@ -144,21 +144,22 @@ describe('Checkboxes component', function () { describe('controlling Google analytics track event when a checkbox is changed', function () { it('fires a Google analytics event if suppressAnalytics not passed to the change event', function () { var $checkbox = $checkboxesWrapper.find(":input[value='blue']") - $checkbox.trigger('change') + var fakeOnChangeEvent = new window.CustomEvent('change') + $checkbox[0].dispatchEvent(fakeOnChangeEvent) expect(GOVUK.analytics.trackEvent).toHaveBeenCalled() }) it('fires a Google analytics event if suppressAnalytics is set to false and passed to the change event', function () { var $checkbox = $checkboxesWrapper.find(":input[value='blue']") - var fakeOnChangeEvent = { type: 'change', suppressAnalytics: false } - $checkbox.trigger(fakeOnChangeEvent) + var fakeOnChangeEvent = new window.CustomEvent('change', { detail: { suppressAnalytics: false } }) + $checkbox[0].dispatchEvent(fakeOnChangeEvent) expect(GOVUK.analytics.trackEvent).toHaveBeenCalled() }) it('does not fire a Google analytics event if suppressAnalytics is passed to the change event', function () { var $checkbox = $checkboxesWrapper.find(":input[value='blue']") - var fakeOnChangeEvent = { type: 'change', suppressAnalytics: true } - $checkbox.trigger(fakeOnChangeEvent) + var fakeOnChangeEvent = new window.CustomEvent('change', { detail: { suppressAnalytics: true } }) + $checkbox[0].dispatchEvent(fakeOnChangeEvent) expect(GOVUK.analytics.trackEvent).not.toHaveBeenCalled() }) })