Removed all govuk css (#2814)

* Removed all govuk css

* Updated reference files

* Removing govuk js

* Fixed casing for modules, removed unused page

* Got more reference images

* Updated template page

* Removed govuk padding util

* Updated hint to uswds hint

* More govuk cleanup

* Commiting backstopjs ref files

* Fixed all unit tests that broke due to brittleness around govuk styling

* Added new ref images

* Final removal of govuk

* Officially removed all govuk references

* Updated reference file

* Updated link to button

* UI modernization

* Cleanup

* removed govuk escaping tests since they are no longer needed

* Fix CodeQL security issue in escapeElementName function

- Escape backslashes first before other special characters
- Prevents potential double-escaping vulnerability
- Addresses CodeQL alert about improper string escaping

* Found more govuk removal. Fixed unit tests

* Add missing pipeline check to pre-commit

* updated test

* Updated e2e test

* More update to e2e test

* Fixed another e2e test

* Simple PR comments addressed

* More updates

* Updated backstop ref files

* Refactored folder selection for non-admins

* Updated redundant line

* Updated tests to include correct mocks

* Added more ref files

* Addressing carlos comments

* Addressing Carlo comments, cleanup of window initing

* More cleanup and addressing carlo comments

* Fixing a11 scan

* Fixed a few issues with javascript

* Fixed for pr

* Fixing e2e tests

* Tweaking e2e test

* Added more ref files and cleaned up urls.js

* Fixed bug with creating new template

* Removed brittle test - addressed code ql comment

* e2e race condition fix

* More e2e test fixes

* Updated e2e tests to not wait for text sent

* Updated test to not wait for button click response

* Made tear down more resilent if staging is down

* reverted e2e test to what was working before main merge

* Updated backstopRef images

* Updated gulp to include job-polling differently
This commit is contained in:
Alex Janousek
2025-10-06 09:38:54 -04:00
committed by GitHub
parent a366a10865
commit 6f5750f095
323 changed files with 2731 additions and 2796 deletions

View File

@@ -399,19 +399,19 @@
dropdown.addEventListener('change', handleDropdownChange);
});
// Resize chart on window resize
window.addEventListener('resize', function() {
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0 && pendingData.length > 0) {
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
}
});
// Resize chart on window resize
window.addEventListener('resize', function() {
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0 && pendingData.length > 0) {
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
}
});
// Export functions for testing
window.createChart = createChart;
window.createTable = createTable;
window.handleDropdownChange = handleDropdownChange;
window.fetchData = fetchData;
}
// Export functions for testing
window.createChart = createChart;
window.createTable = createTable;
window.handleDropdownChange = handleDropdownChange;
window.fetchData = fetchData;
}
})(window);

View File

@@ -1,7 +1,7 @@
(function (global) {
(function (window) {
"use strict";
const GOVUK = global.GOVUK;
const USWDS = window.USWDS || {};
function Summary (module) {
this.module = module;
@@ -222,6 +222,8 @@
this.summary.bindEvents(this);
};
GOVUK.Modules.CollapsibleCheckboxes = CollapsibleCheckboxes;
NotifyModules['collapsible-checkboxes'] = function() {
return new CollapsibleCheckboxes();
};
}(window));

View File

@@ -1,9 +1,9 @@
(function(Modules) {
(function(window) {
"use strict";
if (!document.queryCommandSupported('copy')) return;
Modules.CopyToClipboard = function() {
window.NotifyModules['copy-to-clipboard'] = function() {
const states = {
'valueVisible': (options) => `
@@ -90,11 +90,11 @@
.find('.usa-button').focus()
);
if ('stickAtBottomWhenScrolling' in GOVUK) {
GOVUK.stickAtBottomWhenScrolling.recalculate();
if ('stickAtBottomWhenScrolling' in window.NotifyModules) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
};
};
})(window.GOVUK.Modules);
})(window);

View File

@@ -1,4 +1,4 @@
(function(Modules) {
(function(window) {
"use strict";
if (
@@ -7,10 +7,11 @@
const tagPattern = /\(\(([^\)\((\?)]+)(\?\?)?([^\)\(]*)\)\)/g;
Modules.EnhancedTextbox = function() {
window.NotifyModules['enhanced-textbox'] = function() {
this.start = function(textarea) {
this.start = function(element) {
let textarea = $(element);
let visibleTextbox;
this.highlightPlaceholders = (
@@ -18,7 +19,7 @@
!!textarea.data('highlightPlaceholders')
);
this.$textbox = $(textarea)
this.$textbox = textarea
.wrap(`
<div class='textbox-highlight-wrapper' />
`)
@@ -58,8 +59,8 @@
)
);
if ('stickAtBottomWhenScrolling' in GOVUK) {
GOVUK.stickAtBottomWhenScrolling.recalculate();
if ('stickAtBottomWhenScrolling' in window.NotifyModules) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
};
@@ -84,4 +85,4 @@
};
})(window.GOVUK.Modules);
})(window);

View File

@@ -8,7 +8,7 @@
This may behave in unexpected ways if you have more than one element with the `banner-dangerous` class on your page.
*/
window.GOVUK.ErrorBanner = {
window.NotifyModules.ErrorBanner = {
hideBanner: () => $('.banner-dangerous').addClass('display-none'),
showBanner: () => $('.banner-dangerous')
.removeClass('display-none')

View File

@@ -1,19 +1,21 @@
(function(window) {
"use strict";
window.GOVUK.Modules.TrackError = function() {
window.NotifyModules['track-error'] = function() {
this.start = function(component) {
this.start = function(element) {
var component = $(element);
if (!('analytics' in window.GOVUK)) return;
window.GOVUK.analytics.trackEvent(
'Error',
$(component).data('error-type'),
{
'label': $(component).data('error-label')
}
);
// Track error to analytics if available
if (window.NotifyModules && window.NotifyModules.analytics && window.NotifyModules.analytics.trackEvent) {
window.NotifyModules.analytics.trackEvent(
'Error',
component.data('error-type'),
{
'label': component.data('error-label')
}
);
}
};

View File

@@ -24,16 +24,16 @@ function initUploadStatusAnnouncer() {
});
}
(function(Modules) {
(function(window) {
"use strict";
Modules.FileUpload = function() {
window.NotifyModules['file-upload'] = function() {
this.submit = () => this.$form.trigger('submit');
this.showCancelButton = () => {
$('.file-upload-button', this.$form).replaceWith(`
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
Uploading<span class="dot-anim" aria-hidden="true"></span>
Uploading<span class="loading-spinner" role="status" aria-label="Uploading"></span>
</button>
`);
};
@@ -55,7 +55,7 @@ function initUploadStatusAnnouncer() {
});
};
};
})(window.GOVUK.Modules);
})(window);
if (typeof module !== 'undefined' && module.exports) {
module.exports = {

View File

@@ -1,7 +1,7 @@
(function(Modules) {
(function(window) {
"use strict";
Modules.FullscreenTable = function() {
window.NotifyModules['fullscreen-table'] = function() {
this.start = function(component) {
@@ -24,10 +24,10 @@
.on('scroll', this.maintainHeight)
if (
window.GOVUK.stickAtBottomWhenScrolling &&
window.GOVUK.stickAtBottomWhenScrolling.recalculate
window.NotifyModules.stickAtBottomWhenScrolling &&
window.NotifyModules.stickAtBottomWhenScrolling.recalculate
) {
window.GOVUK.stickAtBottomWhenScrolling.recalculate();
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
this.maintainWidth();
@@ -120,4 +120,4 @@
};
})(window.GOVUK.Modules);
})(window);

View File

@@ -0,0 +1,7 @@
// Expose jQuery and other libraries to window for legacy code
if (typeof jQuery !== 'undefined') {
window.jQuery = window.$ = jQuery;
}
if (typeof getCaretCoordinates !== 'undefined') {
window.getCaretCoordinates = getCaretCoordinates;
}

View File

@@ -1,6 +1,7 @@
(function (Modules) {
(function (window) {
'use strict';
var Modules = window.NotifyModules;
var lists = [],
listEntry,
ListEntry;
@@ -216,10 +217,10 @@
}
};
Modules.ListEntry = function () {
Modules['list-entry'] = function () {
this.start = component => lists.push(new ListEntry($(component)));
};
})(window.GOVUK.Modules);
})(window);

View File

@@ -1,6 +1,7 @@
(function(Modules) {
(function(window) {
"use strict";
var Modules = window.NotifyModules;
let state;
let normalize = (string) => string.toLowerCase().replace(/ /g,'');
let resultsSummary = (num) => {
@@ -61,14 +62,14 @@
// make sticky JS recalculate its cache of the element's position
// because live search can change the height document
if ('stickAtBottomWhenScrolling' in GOVUK) {
GOVUK.stickAtBottomWhenScrolling.recalculate();
if (window.NotifyModules && 'stickAtBottomWhenScrolling' in window.NotifyModules) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
};
Modules.LiveSearch = function() {
Modules['live-search'] = function() {
this.start = function(component) {
@@ -95,4 +96,4 @@
};
})(window.GOVUK.Modules);
})(window);

View File

@@ -1,22 +1,48 @@
window.GOVUK.Frontend.initAll();
// Initialize USWDS components
if (window.uswds) {
window.uswds.init();
}
var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init();
// Initialize custom modules
window.NotifyModules.start = function() {
var modules = document.querySelectorAll('[data-module]');
modules.forEach(function(element) {
var moduleName = element.getAttribute('data-module');
var moduleStarted = element.getAttribute('data-module-started');
$(() => GOVUK.modules.start());
$(() => $('.error-message, .usa-error-message').eq(0).parent('label').next('input').trigger('focus'));
if (!moduleStarted && window.NotifyModules[moduleName]) {
var module = new window.NotifyModules[moduleName]();
if (module.start) {
module.start(element);
}
element.setAttribute('data-module-started', 'true');
}
});
};
document.addEventListener('DOMContentLoaded', function() {
window.NotifyModules.start();
const errorElement = document.querySelector('.error-message, .usa-error-message');
if (errorElement) {
const label = errorElement.closest('label');
if (label && label.nextElementSibling && label.nextElementSibling.tagName === 'INPUT') {
label.nextElementSibling.focus();
}
}
});
// Applies our expanded focus style to the siblings of links when that link is wrapped in a heading.
//
// This will be possible in CSS in the future, using the :has pseudo-class. When :has is available
// in the browsers we support, this code can be replaced with a CSS-only solution.
$('.js-mark-focus-on-parent').on('focus blur', '*', e => {
$target = $(e.target);
if (e.type === 'focusin') {
$target.parent().addClass('js-child-has-focus');
} else {
$target.parent().removeClass('js-child-has-focus');
}
document.addEventListener('DOMContentLoaded', function() {
const markFocusElements = document.querySelectorAll('.js-mark-focus-on-parent');
markFocusElements.forEach(element => {
element.addEventListener('focusin', function(e) {
e.target.parentElement.classList.add('js-child-has-focus');
});
element.addEventListener('focusout', function(e) {
e.target.parentElement.classList.remove('js-child-has-focus');
});
});
});

View File

@@ -1,73 +0,0 @@
// JS Module used to combine all the JS modules used in the application into a single entry point,
// a bit like `app/__init__` in the Flask app.
//
// When processed by a bundler, this is turned into a Immediately Invoked Function Expression (IIFE)
// The IIFE format allows it to run in browsers that don't support JS Modules.
//
// Exported items will be added to the window.GOVUK namespace.
// For example, `export { Frontend }` will assign `Frontend` to `window.Frontend`
// GOVUK Frontend modules
import Header from 'govuk-frontend/components/header/header';
import Details from 'govuk-frontend/components/details/details';
import Button from 'govuk-frontend/components/button/button';
import Radios from 'govuk-frontend/components/radios/radios';
// Modules from 3rd party vendors
/**
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
* This seems to fail in IE8, requires more investigation.
* See: https://github.com/imagitama/nodelist-foreach-polyfill
*/
function nodeListForEach(nodes, callback) {
if (window.NodeList.prototype.forEach) {
return nodes.forEach(callback)
}
for (var i = 0; i < nodes.length; i++) {
callback.call(window, nodes[i], i, nodes);
}
}
// Copy of the initAll function from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/all.js
// except it only includes, and initialises, the components used by this application.
function initAll(options) {
// Set the options to an empty object by default if no options are passed.
options = typeof options !== 'undefined' ? options : {}
// Allow the user to initialise US Frontend in only certain sections of the page
// Defaults to the entire document if nothing is set.
var scope = typeof options.scope !== 'undefined' ? options.scope : document
// Find all buttons with [role=button] on the scope to enhance.
new Button(scope).init()
// Find all global details elements to enhance.
var $details = scope.querySelectorAll('details')
nodeListForEach($details, function ($detail) {
new Details($detail).init()
})
// Find first header module to enhance.
var $toggleButton = scope.querySelector('[data-module="header"]')
new Header($toggleButton).init()
var $radios = scope.querySelectorAll('[data-module="radios"]')
nodeListForEach($radios, function ($radio) {
new Radios($radio).init()
})
}
// Create separate namespace for GOVUK Frontend.
var Frontend = {
"Header": Header,
"Details": Details,
"Button": Button,
"initAll": initAll
}
// The exported object will be assigned to window.GOVUK in our production code
// (bundled into an IIFE by RollupJS)
export {
Frontend
}

View File

@@ -0,0 +1,4 @@
// Initialize the NotifyModules namespace
// This file MUST be loaded first in the build to ensure window.NotifyModules exists
// before any other modules try to use it
window.NotifyModules = window.NotifyModules || {};

View File

@@ -0,0 +1,154 @@
(function (window) {
'use strict';
var $ = window.jQuery;
function ShowHideContent () {
var self = this;
var selectors = {
namespace: 'ShowHideContent',
radio: '[data-target] > input[type="radio"]',
checkbox: '[data-target] > input[type="checkbox"]'
};
function initToggledContent () {
var $control = $(this);
var $content = getToggledContent($control);
if ($content.length) {
$control.attr('aria-controls', $content.attr('id'));
$control.attr('aria-expanded', 'false');
$content.attr('aria-hidden', 'true');
}
}
function getToggledContent ($control) {
try {
var id = $control.attr('aria-controls');
if (!id) {
id = $control.closest('[data-target]').data('target');
}
if (!id || !/^[\w-]+$/.test(id)) {
console.warn('Invalid element ID:', id);
return $();
}
return $('#' + id);
} catch (error) {
console.error('Error getting toggled content:', error);
return $();
}
}
function showToggledContent ($control, $content) {
if ($content.hasClass('display-none')) {
$content.removeClass('display-none');
$content.attr('aria-hidden', 'false');
if ($control.attr('aria-controls')) {
$control.attr('aria-expanded', 'true');
}
}
}
function hideToggledContent ($control, $content) {
$content.addClass('display-none');
$content.attr('aria-hidden', 'true');
if ($control.attr('aria-controls')) {
$control.attr('aria-expanded', 'false');
}
}
function handleRadioContent ($control, $content) {
var selector = selectors.radio + '[name=' + escapeElementName($control.attr('name')) + ']';
var $radios = $(selector);
$radios.each(function () {
hideToggledContent($(this), getToggledContent($(this)));
});
showToggledContent($control, $content);
}
function handleCheckboxContent ($control, $content) {
if ($control.is(':checked')) {
showToggledContent($control, $content);
} else {
hideToggledContent($control, $content);
}
}
function escapeElementName (str) {
// First escape backslashes, then escape other special characters
// This prevents double-escaping issues identified by CodeQL
return str
? str.replace(/\\/g, '\\\\').replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\\$1')
: str;
}
function setupHandlers () {
var $controls = $(selectors.radio + ', ' + selectors.checkbox);
$(selectors.radio).on('click.' + selectors.namespace, function () {
handleRadioContent($(this), getToggledContent($(this)));
});
$(selectors.checkbox).on('click.' + selectors.namespace, function () {
handleCheckboxContent($(this), getToggledContent($(this)));
});
if ($controls.filter(':checked').length) {
$controls.filter(':checked').each(function () {
var $control = $(this);
var $content = getToggledContent($control);
if ($control.is('[type=radio]')) {
handleRadioContent($control, $content);
} else {
handleCheckboxContent($control, $content);
}
});
}
}
self.destroy = function () {
var $controls = $(selectors.radio + ', ' + selectors.checkbox);
$controls.each(function () {
var $control = $(this);
var $content = getToggledContent($control);
$control.removeAttr('aria-controls aria-expanded');
$content.removeAttr('aria-hidden');
});
$(selectors.radio).off('.' + selectors.namespace);
$(selectors.checkbox).off('.' + selectors.namespace);
};
self.init = function () {
try {
$(selectors.radio + ', ' + selectors.checkbox).each(initToggledContent);
setupHandlers();
} catch (error) {
console.error('Error initializing show-hide content:', error);
}
};
}
ShowHideContent.prototype.start = function (element) {
try {
var instance = new ShowHideContent();
instance.init(element);
} catch (error) {
console.error('Failed to start show-hide content module:', error);
}
};
window.NotifyModules.ShowHideContent = ShowHideContent;
})(window);

View File

@@ -0,0 +1,60 @@
(function (window) {
'use strict';
var $ = window.jQuery;
window.NotifyModules.moduleSystem = {
find: function (container) {
container = container || $('body');
var modules;
var moduleSelector = '[data-module]';
modules = container.find(moduleSelector);
if (container.is(moduleSelector)) {
modules = modules.add(container);
}
return modules;
},
start: function (container) {
var modules = this.find(container);
for (var i = 0, l = modules.length; i < l; i++) {
try {
var module;
var element = $(modules[i]);
var type = this.camelCaseAndCapitalise(element.data('module'));
var started = element.data('module-started');
if (typeof window.NotifyModules[type] === 'function' && !started) {
module = new window.NotifyModules[type]();
if (module.start) {
module.start(element);
}
element.data('module-started', true);
}
} catch (error) {
console.error('Failed to initialize module:', type || 'unknown', error);
}
}
},
camelCaseAndCapitalise: function (string) {
return this.capitaliseFirstLetter(this.camelCase(string));
},
camelCase: function (string) {
return string.replace(/-([a-z])/g, function (g) {
return g.charAt(1).toUpperCase();
});
},
capitaliseFirstLetter: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
};
})(window);

View File

@@ -11,34 +11,39 @@
$submitButton.data('clicked', 'true');
// Add dot animation for Send/Schedule/Cancel buttons
// Add loading spinner for Send/Schedule/Cancel buttons
const buttonName = $submitButton.attr('name')?.toLowerCase();
if (["send", "schedule", "cancel"].includes(buttonName)) {
$submitButton.prop('disabled', true);
// Use setTimeout with minimal delay to allow form submission to proceed first
setTimeout(() => {
$submitButton.prop('disabled', true);
// Inject dot animation span if not already present
if ($submitButton.find('.dot-anim').length === 0) {
$submitButton.append('<span class="dot-anim" aria-hidden="true"></span>');
}
// Add loading spinner and aria-busy attribute for accessibility
if ($submitButton.find('.loading-spinner').length === 0) {
$submitButton.attr('aria-busy', 'true');
$submitButton.append('<span class="loading-spinner" role="status" aria-label="Sending"></span>');
}
// Disable Cancel button too
const $cancelButton = $('button[name]').filter(function () {
return $(this).attr('name')?.toLowerCase() === 'cancel';
});
$cancelButton.prop('disabled', true);
// Disable Cancel button too
const $cancelButton = $('button[name]').filter(function () {
return $(this).attr('name')?.toLowerCase() === 'cancel';
});
$cancelButton.prop('disabled', true);
}, 50); // Small delay to ensure form submits first
setTimeout(() => {
renableSubmitButton($submitButton);
renableSubmitButton($submitButton)();
}, 10000); // fallback safety
} else {
setTimeout(renableSubmitButton($submitButton), 1500);
setTimeout(() => renableSubmitButton($submitButton)(), 1500);
}
};
const renableSubmitButton = ($submitButton) => () => {
$submitButton.data('clicked', '');
$submitButton.prop('disabled', false);
$submitButton.find('.dot-anim').remove(); // clean up if needed
$submitButton.attr('aria-busy', 'false');
$submitButton.find('.loading-spinner').remove(); // clean up spinner
};
$('form').on('submit', disableSubmitButtons);

View File

@@ -1,8 +1,8 @@
(function(global) {
(function(window) {
"use strict";
var Modules = global.GOVUK.Modules;
var Modules = window.NotifyModules;
// Template functions for rendering component states
let renderStates = {
@@ -79,7 +79,7 @@
}
};
Modules.RadioSelect = function() {
Modules['radio-select'] = function() {
this.start = function(component) {

View File

@@ -1,8 +1,8 @@
(function(global) {
(function(window) {
"use strict";
global.GOVUK.Modules.RadioSlider = function() {
window.NotifyModules['radio-slider'] = function() {
this.start = function(component) {

View File

@@ -1,13 +1,26 @@
document.querySelectorAll('.usa-button-group a, .usa-pagination a').forEach(function(button) {
button.addEventListener('click', function() {
sessionStorage.setItem('scrollPosition', window.pageYOffset);
button.addEventListener('click', function(e) {
// Add scroll position to URL as fragment
var scrollPos = window.pageYOffset;
if (scrollPos > 0) {
var url = new URL(this.href, window.location.href);
url.hash = 'scroll=' + scrollPos;
this.href = url.toString();
}
});
});
document.addEventListener('DOMContentLoaded', function() {
var scrollPosition = sessionStorage.getItem('scrollPosition');
if (scrollPosition !== null) {
window.scrollTo(0, parseInt(scrollPosition));
sessionStorage.removeItem('scrollPosition');
var hash = window.location.hash;
if (hash && hash.startsWith('#scroll=')) {
var scrollPosition = parseInt(hash.substring(8), 10);
if (!isNaN(scrollPosition) && scrollPosition > 0) {
window.scrollTo(0, scrollPosition);
// Clean up the URL
if (window.history && window.history.replaceState) {
var cleanUrl = window.location.pathname + window.location.search;
window.history.replaceState(null, '', cleanUrl);
}
}
}
});

View File

@@ -1,8 +1,8 @@
;(function (global) {
;(function (window) {
'use strict';
var $ = global.jQuery;
var GOVUK = global.GOVUK || {};
var $ = window.jQuery;
var NotifyModules = window.NotifyModules || {};
var _mode = 'default';
// Constructor to make objects representing the area sticky elements can scroll in
@@ -630,7 +630,7 @@
};
if ((!el.hasLoaded()) && ($img.length > 0)) {
var image = new global.Image();
var image = new window.Image();
image.onload = function () {
onload();
};
@@ -677,7 +677,7 @@
}
self.setElementDimensions(elObj, onDimensionsSet);
};
};
Sticky.prototype.remove = function (el) {
if ($.inArray(el, this._els) !== -1) {
@@ -734,25 +734,25 @@
// window position
if (this._scrollTimeout === false) {
$(global).scroll(this._scrollEvent);
this._scrollTimeout = global.setInterval(this.checkScroll.bind(this), 50);
this._scrollTimeout = window.setInterval(this.checkScroll.bind(this), 50);
}
// Recalculate all dimensions when the window resizes
if (this._resizeTimeout === false) {
$(global).resize(this._resizeEvent);
this._resizeTimeout = global.setInterval(this.checkResize.bind(this), 50);
this._resizeTimeout = window.setInterval(this.checkResize.bind(this), 50);
}
};
Sticky.prototype.clearEvents = function () {
if (this._scrollTimeout !== false) {
$(global).off('scroll', this._scrollEvent);
global.clearInterval(this._scrollTimeout);
window.clearInterval(this._scrollTimeout);
this._scrollTimeout = false;
}
if (this._resizeTimeout !== false) {
$(global).off('resize', this._resizeEvent);
global.clearInterval(this._resizeTimeout);
window.clearInterval(this._resizeTimeout);
this._resizeTimeout = false;
}
@@ -989,7 +989,7 @@
el.unstop();
};
GOVUK.stickAtTopWhenScrolling = stickAtTop;
GOVUK.stickAtBottomWhenScrolling = stickAtBottom;
global.GOVUK = GOVUK;
NotifyModules.stickAtTopWhenScrolling = stickAtTop;
NotifyModules.stickAtBottomWhenScrolling = stickAtBottom;
window.NotifyModules = NotifyModules;
})(window);

View File

@@ -1,7 +1,7 @@
(function(Modules) {
(function(window) {
"use strict";
Modules.TemplateFolderForm = function() {
window.NotifyModules['template-folder-form'] = function() {
this.start = function(templateFolderForm) {
this.$form = $(templateFolderForm);
@@ -82,14 +82,15 @@
this.render();
}
this.$form.on('click', 'button.usa-button--event', (event) => this.actionButtonClicked(event));
this.$form.on('click', 'button.usa-button', (event) => this.actionButtonClicked(event));
this.$form.on('change', 'input[type=checkbox]', () => this.templateFolderCheckboxChanged());
this.$form.on('change', 'input[name="add_template_by_template_type"]', () => this.templateTypeChanged());
};
this.addDescriptionsToStates = function () {
let id, description;
$.each(this.states.filter(state => 'description' in state), (idx, state) => {
$.each(this.states.filter(state => 'description' in state), (_, state) => {
id = `${state.key}__description`;
description = `<p class="usa-sr-only" id="${id}">${state.description}</p>`;
state.$el
@@ -191,17 +192,30 @@
this.$singleChannelService = (document.querySelector('div[id=add_new_template_form]')).getAttribute("data-service");
this.actionButtonClicked = function(event) {
event.preventDefault();
this.currentState = $(event.currentTarget).val();
if (event.currentTarget.value === 'add-new-template' && this.$singleNotificationChannel) {
event.preventDefault();
window.location = "/services/" + this.$singleChannelService + "/templates/add-" + this.$singleNotificationChannel;
} else {
if (this.currentState === 'add-new-template') {
} else if (this.currentState === 'add-new-template') {
// Check if a template type is selected
const selectedTemplateType = this.$form.find('input[name="add_template_by_template_type"]:checked').val();
if (selectedTemplateType) {
// Template type is selected, let the form submit normally
return true;
} else {
// No template type selected, show the selection UI
event.preventDefault();
this.$form.find('input[type=checkbox]').prop('checked', false);
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
}
if (this.stateChanged()) {
this.render();
}
}
} else {
event.preventDefault();
if (this.stateChanged()) {
this.render();
}
@@ -260,14 +274,29 @@
};
this.templateTypeChanged = function() {
this.updateContinueButtonState();
};
this.updateContinueButtonState = function() {
const selectedTemplateType = this.$form.find('input[name="add_template_by_template_type"]:checked').val();
const continueButton = this.$form.find('#add_new_template_form button[value="add-new-template"]');
if (selectedTemplateType) {
continueButton.prop('disabled', false);
} else {
continueButton.prop('disabled', true);
}
};
this.hasCheckboxes = function() {
return !!this.$form.find('input:checkbox').length;
};
this.countSelectedCheckboxes = function() {
const allSelected = this.$form.find('input:checkbox:checked');
const templates = allSelected.filter((idx, el) => $(el).siblings('.template-list-template').length > 0).length;
const folders = allSelected.filter((idx, el) => $(el).siblings('.template-list-folder').length > 0).length;
const templates = allSelected.filter((_, el) => $(el).siblings('.template-list-template').length > 0).length;
const folders = allSelected.filter((_, el) => $(el).siblings('.template-list-folder').length > 0).length;
const results = {
'templates': templates,
'folders': folders,
@@ -277,7 +306,6 @@
};
this.render = function() {
let mode = 'default';
let currentStateObj = this.states.filter(state => { return (state.key === this.currentState); })[0];
let scrollTop;
@@ -286,11 +314,6 @@
state => (state.key === this.currentState ? this.$liveRegionCounter.before(state.$el) : state.$el.detach())
);
// use dialog mode for states which contain more than one form control
if (['move-to-existing-folder', 'add-new-template'].indexOf(this.currentState) !== -1) {
mode = 'dialog';
}
if (this.currentState === 'add-new-template') {
this.$form.find('.template-list-item').addClass('js-hidden');
$('.live-search').addClass('js-hidden');
@@ -302,6 +325,9 @@
$('#page-title').text('New Template');
$('#page-description').text('Every message starts with a template. Choose to start with a blank template or copy an existing template.');
document.title = 'New Templates';
// Disable Continue button initially and update based on selection
this.updateContinueButtonState();
} else {
this.$form.find('.template-list-item').removeClass('js-hidden');
$('.live-search').removeClass('js-hidden');
@@ -324,10 +350,12 @@
<div id="nothing_selected">
<div class="js-stick-at-bottom-when-scrolling">
<div class="usa-button-group">
<button class="usa-button usa-button--event" value="add-new-template" aria-expanded="false">
<button class="usa-button" value="add-new-template" aria-expanded="false" role="button">
New template
</button>
<button class="usa-button usa-button--event" value="add-new-folder" aria-expanded="false">New folder</button>
<button class="usa-button usa-button--outline" value="add-new-folder" aria-expanded="false" role="button">
New folder
</button>
</div>
<div class="template-list-selected-counter">
<span class="template-list-selected-counter__count" aria-hidden="true">
@@ -342,13 +370,15 @@
<div id="items_selected">
<div class="js-stick-at-bottom-when-scrolling">
<div class="usa-button-group">
<button class="usa-button usa-button--event" value="move-to-existing-folder" aria-expanded="false">
<button class="usa-button" value="move-to-existing-folder" aria-expanded="false" role="button">
Move<span class="usa-sr-only"> selection to folder</span>
</button>
<button class="usa-button usa-button--event" value="move-to-new-folder" aria-expanded="false">Add to new folder</button>
<button class="usa-button usa-button--outline" value="move-to-new-folder" aria-expanded="false" role="button">
Add to new folder
</button>
</div>
<div class="template-list-selected-counter" aria-hidden="true">
<span class="template-list-selected-counter__count" aria-hidden="true">
<span class="template-list-selected-counter__count text-base" aria-hidden="true">
${this.selectionStatus.selected(1)}
</span>
</div>
@@ -357,4 +387,4 @@
`).get(0);
};
})(window.GOVUK.Modules);
})(window);

View File

@@ -1,8 +1,6 @@
window.GOVUK = window.GOVUK || {};
window.GOVUK.Modules = window.GOVUK.Modules || {};
window.GOVUK.Modules.TimeoutPopup = window.GOVUK.Modules.TimeoutPopup || {};
window.NotifyModules.TimeoutPopup = window.NotifyModules.TimeoutPopup || {};
(function(global) {
(function(window) {
"use strict";
const sessionTimer = document.getElementById("sessionTimer");
@@ -56,10 +54,10 @@ window.GOVUK.Modules.TimeoutPopup = window.GOVUK.Modules.TimeoutPopup || {};
setTimeout(setSessionTimer, 25 * 60 * 1000);
}
global.GOVUK.Modules.TimeoutPopup.checkTimer = checkTimer;
global.GOVUK.Modules.TimeoutPopup.expireUserSession = expireUserSession;
global.GOVUK.Modules.TimeoutPopup.signoutUser = signoutUser;
global.GOVUK.Modules.TimeoutPopup.extendSession = extendSession;
global.GOVUK.Modules.TimeoutPopup.showTimer = showTimer;
global.GOVUK.Modules.TimeoutPopup.closeTimer = closeTimer;
window.NotifyModules.TimeoutPopup.checkTimer = checkTimer;
window.NotifyModules.TimeoutPopup.expireUserSession = expireUserSession;
window.NotifyModules.TimeoutPopup.signoutUser = signoutUser;
window.NotifyModules.TimeoutPopup.extendSession = extendSession;
window.NotifyModules.TimeoutPopup.showTimer = showTimer;
window.NotifyModules.TimeoutPopup.closeTimer = closeTimer;
})(window);

View File

@@ -1,150 +1,144 @@
(function (window) {
function createTotalMessagesChart() {
var chartContainer = document.getElementById('totalMessageChartContainer');
if (!chartContainer) return;
function createTotalMessagesChart() {
var chartContainer = document.getElementById('totalMessageChartContainer');
if (!chartContainer) return;
var chartTitle = document.getElementById('chartTitle').textContent;
var chartTitle = document.getElementById('chartTitle').textContent;
// Access data attributes from the HTML
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
var totalMessages = messagesSent + messagesRemaining;
// Access data attributes from the HTML
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
var totalMessages = messagesSent + messagesRemaining;
// Update the message below the chart
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
// Update the message below the chart
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
var minSentValue = totalMessages * minSentPercentage;
var displaySent = Math.max(messagesSent, minSentValue);
var displayRemaining = totalMessages - displaySent;
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
var minSentValue = totalMessages * minSentPercentage;
var displaySent = Math.max(messagesSent, minSentValue);
var displayRemaining = totalMessages - displaySent;
var svg = d3.select("#totalMessageChart");
var width = chartContainer.clientWidth;
var height = 48;
var svg = d3.select("#totalMessageChart");
var width = chartContainer.clientWidth;
var height = 48;
// Ensure the width is set correctly
if (width === 0) {
console.error('Chart container width is 0, cannot set SVG width.');
return;
}
svg.attr("width", width).attr("height", height);
var x = d3.scaleLinear()
.domain([0, totalMessages])
.range([0, width]);
// Create tooltip dynamically
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip");
// Create the initial bars
var sentBar = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("fill", '#0076d6')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Messages Sent: ${messagesSent.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
var remainingBar = svg.append("rect")
.attr("x", 0) // Initially set to 0, will be updated during animation
.attr("y", 0)
.attr("height", height)
.attr("fill", '#C7CACE')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Remaining: ${messagesRemaining.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
// Animate the bars together as a single cohesive line
svg.transition()
.duration(1000) // Total animation duration
.attr("width", width)
.tween("resize", function() {
var interpolator = d3.interpolate(0, width);
return function(t) {
var newWidth = interpolator(t);
var sentWidth = x(displaySent) / width * newWidth;
var remainingWidth = x(displayRemaining) / width * newWidth;
sentBar.attr("width", sentWidth);
remainingBar.attr("x", sentWidth).attr("width", remainingWidth);
};
});
// Create and populate the accessible table
var tableContainer = document.getElementById('totalMessageTable');
var table = document.createElement('table');
table.className = 'usa-sr-only usa-table';
var caption = document.createElement('caption');
caption.textContent = chartTitle;
table.appendChild(caption);
var thead = document.createElement('thead'); // Ensure thead is created
var theadRow = document.createElement('tr');
var thMessagesSent = document.createElement('th');
thMessagesSent.textContent = 'Messages Sent'; // First column header
var thRemaining = document.createElement('th');
thRemaining.textContent = 'Remaining'; // Second column header
theadRow.appendChild(thMessagesSent);
theadRow.appendChild(thRemaining);
thead.appendChild(theadRow); // Append theadRow to the thead
table.appendChild(thead);
var tbody = document.createElement('tbody');
var tbodyRow = document.createElement('tr');
var tdMessagesSent = document.createElement('td');
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
var tdRemaining = document.createElement('td');
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
tbodyRow.appendChild(tdMessagesSent);
tbodyRow.appendChild(tdRemaining);
tbody.appendChild(tbodyRow);
table.appendChild(tbody);
tableContainer.appendChild(table);
table.appendChild(tbody);
tableContainer.appendChild(table);
// Ensure the chart resizes correctly on window resize
window.addEventListener('resize', function () {
width = chartContainer.clientWidth;
x.range([0, width]);
svg.attr("width", width);
sentBar.attr("width", x(displaySent));
remainingBar.attr("x", x(displaySent)).attr("width", x(displayRemaining));
});
// Ensure the width is set correctly
if (width === 0) {
console.error('Chart container width is 0, cannot set SVG width.');
return;
}
// Initialize total messages chart if the container exists
document.addEventListener('DOMContentLoaded', function() {
createTotalMessagesChart();
svg.attr("width", width).attr("height", height);
var x = d3.scaleLinear()
.domain([0, totalMessages])
.range([0, width]);
// Create tooltip dynamically
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip");
// Create the initial bars
var sentBar = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("fill", '#0076d6')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Messages Sent: ${messagesSent.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
var remainingBar = svg.append("rect")
.attr("x", 0) // Initially set to 0, will be updated during animation
.attr("y", 0)
.attr("height", height)
.attr("fill", '#C7CACE')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Remaining: ${messagesRemaining.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
// Animate the bars together as a single cohesive line
svg.transition()
.duration(1000) // Total animation duration
.attr("width", width)
.tween("resize", function() {
var interpolator = d3.interpolate(0, width);
return function(t) {
var newWidth = interpolator(t);
var sentWidth = x(displaySent) / width * newWidth;
var remainingWidth = x(displayRemaining) / width * newWidth;
sentBar.attr("width", sentWidth);
remainingBar.attr("x", sentWidth).attr("width", remainingWidth);
};
});
// Create and populate the accessible table
var tableContainer = document.getElementById('totalMessageTable');
var table = document.createElement('table');
table.className = 'usa-sr-only usa-table';
var caption = document.createElement('caption');
caption.textContent = chartTitle;
table.appendChild(caption);
var thead = document.createElement('thead'); // Ensure thead is created
var theadRow = document.createElement('tr');
var thMessagesSent = document.createElement('th');
thMessagesSent.textContent = 'Messages Sent'; // First column header
var thRemaining = document.createElement('th');
thRemaining.textContent = 'Remaining'; // Second column header
theadRow.appendChild(thMessagesSent);
theadRow.appendChild(thRemaining);
thead.appendChild(theadRow); // Append theadRow to the thead
table.appendChild(thead);
var tbody = document.createElement('tbody');
var tbodyRow = document.createElement('tr');
var tdMessagesSent = document.createElement('td');
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
var tdRemaining = document.createElement('td');
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
tbodyRow.appendChild(tdMessagesSent);
tbodyRow.appendChild(tdRemaining);
tbody.appendChild(tbodyRow);
table.appendChild(tbody);
tableContainer.appendChild(table);
// Ensure the chart resizes correctly on window resize
window.addEventListener('resize', function () {
width = chartContainer.clientWidth;
x.range([0, width]);
svg.attr("width", width);
sentBar.attr("width", x(displaySent));
remainingBar.attr("x", x(displaySent)).attr("width", x(displayRemaining));
});
}
// Export function for testing
window.createTotalMessagesChart = createTotalMessagesChart;
// Initialize total messages chart if the container exists
document.addEventListener('DOMContentLoaded', function() {
createTotalMessagesChart();
});
})(window);
// Export function for testing
window.createTotalMessagesChart = createTotalMessagesChart;

View File

@@ -1,7 +1,7 @@
(function(window) {
"use strict";
window.GOVUK.Modules.UpdateStatus = function() {
window.NotifyModules['update-status'] = function() {
const getRenderer = $component => response => $component.html(
response.html