Merge pull request #3025 from GSA/fix/frontend-optimization

Optimizing FE further with dead code cleanup, documentation updates, removal of more convoluting dependencies
This commit is contained in:
ccostino
2025-10-29 11:00:09 -04:00
committed by GitHub
64 changed files with 2147 additions and 3685 deletions

View File

@@ -1,11 +1,9 @@
(function (window) {
"use strict";
const USWDS = window.USWDS || {};
function Summary (module) {
this.module = module;
this.$el = module.$formGroup.find('.selection-summary').first();
this.el = module.formGroup.querySelector('.selection-summary');
this.fieldLabel = module.fieldLabel;
this.total = module.total;
this.addContent();
@@ -20,14 +18,17 @@
}[field] || `No ${field}s`)
};
Summary.prototype.addContent = function() {
const $hint = this.module.$formGroup.find('.usa-hint');
this.$text = $(`<p class="selection-summary__text" />`);
const hint = this.module.formGroup.querySelector('.usa-hint');
this.text = document.createElement('p');
this.text.className = 'selection-summary__text';
if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); }
if (this.fieldLabel === 'folder') { this.text.classList.add('selection-summary__text--folders'); }
this.$el.attr('id', $hint.attr('id'));
this.$el.append(this.$text);
$hint.remove();
if (hint) {
this.el.setAttribute('id', hint.getAttribute('id'));
hint.remove();
}
this.el.appendChild(this.text);
};
Summary.prototype.update = function(selection) {
let template;
@@ -40,19 +41,19 @@
template = 'none';
}
this.$text.html(this.templates[template](selection, this.total, this.fieldLabel));
this.text.innerHTML = this.templates[template](selection, this.total, this.fieldLabel);
};
Summary.prototype.bindEvents = function () {
// take summary out of tab order when focus moves
this.$el.on('blur', (e) => $(this).attr('tabindex', '-1'));
this.el.addEventListener('blur', (e) => e.target.setAttribute('tabindex', '-1'));
};
function Footer (module) {
this.module = module;
this.fieldLabel = module.fieldLabel;
this.fieldsetId = module.$fieldset.attr('id');
this.$el = this.getEl(this.module.expanded);
this.module.$formGroup.append(this.$el);
this.fieldsetId = module.fieldset.getAttribute('id');
this.el = this.getEl(this.module.expanded);
this.module.formGroup.appendChild(this.el);
}
Footer.prototype.buttonContent = {
change: (fieldLabel) => `Choose ${fieldLabel}s`,
@@ -63,37 +64,37 @@
const buttonContent = this.buttonContent[buttonState](this.fieldLabel);
const stickyClass = expanded ? ' js-stick-at-bottom-when-scrolling' : '';
return $(`<div class="selection-footer${stickyClass} margin-top-2">
<button
const div = document.createElement('div');
div.className = `selection-footer${stickyClass} margin-top-2`;
div.innerHTML = `<button
class="usa-button usa-button--outline selection-footer__button"
aria-expanded="${expanded ? 'true' : 'false'}"
aria-controls="${this.fieldsetId}">
${buttonContent}
</button>
</div>`);
</button>`;
return div;
};
Footer.prototype.update = function (expanded) {
this.$el.remove();
this.$el = this.getEl(expanded);
this.el.remove();
this.el = this.getEl(expanded);
this.module.$formGroup.append(this.$el);
this.module.formGroup.appendChild(this.el);
};
function CollapsibleCheckboxes () {}
CollapsibleCheckboxes.prototype._focusTextElement = ($el) => {
$el
.attr('tabindex', '-1')
.focus();
CollapsibleCheckboxes.prototype._focusTextElement = (el) => {
el.setAttribute('tabindex', '-1');
el.focus();
};
CollapsibleCheckboxes.prototype.start = function(component) {
this.$component = $(component);
this.$formGroup = this.$component.find('.usa-form-group').first();
this.$fieldset = this.$formGroup.find('fieldset').first();
this.$checkboxes = this.$fieldset.find('input[type=checkbox]');
this.fieldLabel = this.$component.data('fieldLabel');
this.total = this.$checkboxes.length;
this.legendText = this.$fieldset.find('legend').first().text().trim();
this.component = component;
this.formGroup = component.querySelector('.usa-form-group');
this.fieldset = this.formGroup.querySelector('fieldset');
this.checkboxes = this.fieldset.querySelectorAll('input[type=checkbox]');
this.fieldLabel = component.dataset.fieldLabel;
this.total = this.checkboxes.length;
this.legendText = this.fieldset.querySelector('legend').textContent.trim();
this.expanded = false;
this.checkUncheckButtonsAdded = false;
@@ -103,71 +104,78 @@
this.footer = new Footer(this);
this.summary = new Summary(this);
this.$fieldset.before(this.summary.$el);
this.fieldset.insertAdjacentElement('beforebegin', this.summary.el);
// add custom classes
this.$formGroup.addClass('selection-wrapper');
this.$fieldset.addClass('selection-content');
this.formGroup.classList.add('selection-wrapper');
this.fieldset.classList.add('selection-content');
// hide checkboxes
this.$fieldset.hide();
this.fieldset.style.display = 'none';
this.bindEvents();
};
CollapsibleCheckboxes.prototype.getSelection = function() { return this.$checkboxes.filter(':checked').length; };
CollapsibleCheckboxes.prototype.getSelection = function() {
return Array.from(this.checkboxes).filter(cb => cb.checked).length;
};
CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() {
const headingLevel = this.$component.data('heading-level') || '2';
const headingLevel = this.component.dataset.headingLevel || '2';
this.$heading = $(`<h${headingLevel} class="heading-small"></h${headingLevel}>`);
this.$heading.text(this.legendText);
this.$fieldset.before(this.$heading);
this.heading = document.createElement(`h${headingLevel}`);
this.heading.className = 'heading-small';
this.heading.textContent = this.legendText;
this.fieldset.insertAdjacentElement('beforebegin', this.heading);
this.$fieldset.find('legend').addClass('usa-sr-only');
this.fieldset.querySelector('legend').classList.add('usa-sr-only');
};
CollapsibleCheckboxes.prototype.addCheckUncheckAllButtons = function() {
const $buttonsContainer = $('<div class="check-uncheck-all-buttons margin-bottom-2"></div>');
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'check-uncheck-all-buttons margin-bottom-2';
this.$toggleAllButton = $('<button type="button" class="usa-button usa-button--outline usa-button--small">Select all</button>');
this.toggleAllButton = document.createElement('button');
this.toggleAllButton.type = 'button';
this.toggleAllButton.className = 'usa-button usa-button--outline usa-button--small';
this.toggleAllButton.textContent = 'Select all';
$buttonsContainer.append(this.$toggleAllButton);
buttonsContainer.appendChild(this.toggleAllButton);
this.summary.$el.after($buttonsContainer);
this.summary.el.insertAdjacentElement('afterend', buttonsContainer);
this.$toggleAllButton.on('click', this.toggleAll.bind(this));
this.toggleAllButton.addEventListener('click', this.toggleAll.bind(this));
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.toggleAll = function(e) {
e.preventDefault();
e.stopPropagation();
const allChecked = this.$checkboxes.filter(':checked').length === this.$checkboxes.length;
const allChecked = Array.from(this.checkboxes).filter(cb => cb.checked).length === this.checkboxes.length;
if (allChecked) {
this.$checkboxes.prop('checked', false);
this.checkboxes.forEach(cb => cb.checked = false);
} else {
this.$checkboxes.prop('checked', true);
this.checkboxes.forEach(cb => cb.checked = true);
}
this.handleSelection();
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.updateToggleButtonText = function() {
if (!this.$toggleAllButton) return;
if (!this.toggleAllButton) return;
const checkedCount = this.$checkboxes.filter(':checked').length;
const allChecked = checkedCount === this.$checkboxes.length;
const checkedCount = Array.from(this.checkboxes).filter(cb => cb.checked).length;
const allChecked = checkedCount === this.checkboxes.length;
if (allChecked) {
this.$toggleAllButton.text('Deselect all');
this.toggleAllButton.textContent = 'Deselect all';
} else {
this.$toggleAllButton.text('Select all');
this.toggleAllButton.textContent = 'Select all';
}
};
CollapsibleCheckboxes.prototype.expand = function(e) {
if (e !== undefined) { e.preventDefault(); }
if (!this.expanded) {
this.$fieldset.show();
this.fieldset.style.display = '';
this.expanded = true;
this.summary.update(this.getSelection());
this.footer.update(this.expanded);
@@ -176,31 +184,31 @@
this.addCheckUncheckAllButtons();
this.checkUncheckButtonsAdded = true;
} else {
if (this.$toggleAllButton) {
this.$toggleAllButton.parent().show();
if (this.toggleAllButton) {
this.toggleAllButton.parentElement.style.display = '';
}
}
}
// shift focus whether expanded or not
this._focusTextElement(this.$fieldset);
this._focusTextElement(this.fieldset);
};
CollapsibleCheckboxes.prototype.collapse = function(e) {
if (e !== undefined) { e.preventDefault(); }
if (this.expanded) {
this.$fieldset.hide();
this.fieldset.style.display = 'none';
this.expanded = false;
this.summary.update(this.getSelection());
this.footer.update(this.expanded);
if (this.$toggleAllButton) {
this.$toggleAllButton.parent().hide();
if (this.toggleAllButton) {
this.toggleAllButton.parentElement.style.display = 'none';
}
}
// shift focus whether expanded or not
this._focusTextElement(this.summary.$text);
this._focusTextElement(this.summary.text);
};
CollapsibleCheckboxes.prototype.handleClick = function(e) {
if (this.expanded) {
@@ -214,10 +222,15 @@
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.bindEvents = function() {
const self = this;
this.formGroup.addEventListener('click', (e) => {
if (e.target.closest('.usa-button')) {
this.handleClick.call(this, e);
}
});
this.$formGroup.on('click', '.usa-button', this.handleClick.bind(this));
this.$checkboxes.on('click', this.handleSelection.bind(this));
this.checkboxes.forEach(cb => {
cb.addEventListener('click', this.handleSelection.bind(this));
});
this.summary.bindEvents(this);
};

View File

@@ -1,98 +1,70 @@
(function(window) {
"use strict";
if (!document.queryCommandSupported('copy')) return;
// Only initialize if modern Clipboard API is available
if (!navigator.clipboard) return;
window.NotifyModules['copy-to-clipboard'] = function() {
const states = {
'valueVisible': (options) => `
<span class="copy-to-clipboard__value margin-bottom-1">${options.valueLabel ? '<span class="usa-sr-only">' + options.thing + ': </span>' : ''}${options.value}</span>
<span class="copy-to-clipboard__notice" aria-live="assertive">
${options.onload ? '' : options.thing + ' returned to page, press button to copy to clipboard'}
</span>
<button class="usa-button usa-button--outline copy-to-clipboard__button--copy">
Copy ${options.thing} to clipboard${options.name ? '<span class="usa-sr-only"> for ' + options.name + '</span>' : ''}
</button>
`,
'valueCopied': (options) => `
<span class="copy-to-clipboard__notice" aria-live="assertive">
<span class="usa-sr-only">${options.thing} </span>Copied to clipboard<span class="usa-sr-only">, press button to show in page</span>
</span>
<button class="usa-button copy-to-clipboard__button--show">
Show ${options.thing}${options.name ? '<span class="usa-sr-only"> for ' + options.name + '</span>' : ''}
</button>
`
};
this.getRangeFromElement = function (copyableElement) {
const range = document.createRange();
const childNodes = Array.prototype.slice.call(copyableElement.childNodes);
let prefixIndex = -1;
childNodes.forEach((el, idx) => {
if ((el.nodeType === 1) && el.classList.contains('usa-sr-only')) {
prefixIndex = idx;
}
});
range.selectNodeContents(copyableElement);
if (prefixIndex !== -1) { range.setStart(copyableElement, prefixIndex + 1); }
return range;
};
this.copyValueToClipboard = function(copyableElement, callback) {
var selection = window.getSelection ? window.getSelection() : document.selection,
range = this.getRangeFromElement(copyableElement);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
selection.removeAllRanges();
callback();
/**
* Copy text to clipboard using modern Clipboard API
* @param {string} text - The text to copy
* @param {Function} callback - Called after successful copy
*/
this.copyValueToClipboard = async function(text, callback) {
try {
await navigator.clipboard.writeText(text);
callback();
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
this.start = function(component) {
const $component = $(component),
stateOptions = {
value: $component.data('value'),
thing: $component.data('thing')
},
name = $component.data('name');
const value = component.dataset.value;
const thing = component.dataset.thing;
const name = component.dataset.name;
// if the name is distinct from the thing:
// - it will be used in the rendering
// - the value won't be identified by a heading so needs its own label
if (name !== stateOptions.thing) {
stateOptions.name = name;
stateOptions.valueLabel = true;
}
// Determine button label
const isMultiple = name !== thing;
const buttonLabel = isMultiple
? `Copy ${thing}`
: `Copy ${thing} to clipboard`;
const srSuffix = isMultiple ? ` for ${name}` : '';
$component
.addClass('copy-to-clipboard')
.css('min-height', $component.height())
.html(states.valueVisible($.extend({ 'onload': true }, stateOptions)))
.on(
'click', '.copy-to-clipboard__button--copy', () =>
this.copyValueToClipboard(
$('.copy-to-clipboard__value', component)[0], () =>
$component
.html(states.valueCopied(stateOptions))
.find('.usa-button').focus()
)
)
.on(
'click', '.copy-to-clipboard__button--show', () =>
$component
.html(states.valueVisible(stateOptions))
.find('.usa-button').focus()
);
// Create simple HTML structure
component.classList.add('copy-to-clipboard');
component.innerHTML = `
<div class="copy-to-clipboard__value">${value}</div>
<button class="usa-button usa-button--outline copy-to-clipboard__button" type="button">
${buttonLabel}<span class="usa-sr-only">${srSuffix}</span>
</button>
<span class="usa-sr-only" aria-live="polite" aria-atomic="true"></span>
`;
if ('stickAtBottomWhenScrolling' in window.NotifyModules) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
const button = component.querySelector('.copy-to-clipboard__button');
const srAnnouncement = component.querySelector('[aria-live]');
// Handle copy button click
button.addEventListener('click', () => {
this.copyValueToClipboard(value, () => {
// Change button text to "Copied!"
const originalText = button.innerHTML;
button.innerHTML = `Copied!<span class="usa-sr-only">${srSuffix}</span>`;
button.disabled = true;
// Announce to screen readers
srAnnouncement.textContent = `${thing} copied to clipboard`;
// Reset button after 2 seconds
setTimeout(() => {
button.innerHTML = originalText;
button.disabled = false;
srAnnouncement.textContent = '';
}, 2000);
});
});
};
};

View File

@@ -11,61 +11,71 @@
this.start = function(element) {
let textarea = $(element);
let textarea = element;
let visibleTextbox;
this.highlightPlaceholders = (
typeof textarea.data('highlightPlaceholders') === 'undefined' ||
!!textarea.data('highlightPlaceholders')
typeof textarea.dataset.highlightPlaceholders === 'undefined' ||
textarea.dataset.highlightPlaceholders !== 'false'
);
this.$textbox = textarea
.wrap(`
<div class='textbox-highlight-wrapper' />
`)
.after(this.$background = $(`
<div class="textbox-highlight-background" aria-hidden="true" />
`))
.on("input", this.update);
// Create wrapper div
const wrapper = document.createElement('div');
wrapper.className = 'textbox-highlight-wrapper';
$(window).on("resize", this.resize);
// Insert wrapper before textarea and move textarea into it
textarea.parentNode.insertBefore(wrapper, textarea);
wrapper.appendChild(textarea);
visibleTextbox = this.$textbox.clone().appendTo("body").css({
position: 'absolute',
visibility: 'hidden',
display: 'block'
});
this.initialHeight = visibleTextbox.height();
// Create background div
this.background = document.createElement('div');
this.background.className = 'textbox-highlight-background';
this.background.setAttribute('aria-hidden', 'true');
this.$background.css({
'border-width': this.$textbox.css('border-width')
});
// Insert background after textarea
textarea.parentNode.insertBefore(this.background, textarea.nextSibling);
this.textbox = textarea;
this.textbox.addEventListener("input", this.update);
window.addEventListener("resize", this.resize);
// Clone textbox to measure initial height
visibleTextbox = this.textbox.cloneNode(true);
visibleTextbox.style.position = 'absolute';
visibleTextbox.style.visibility = 'hidden';
visibleTextbox.style.display = 'block';
document.body.appendChild(visibleTextbox);
this.initialHeight = visibleTextbox.offsetHeight;
const borderWidth = window.getComputedStyle(this.textbox).borderWidth;
this.background.style.borderWidth = borderWidth;
visibleTextbox.remove();
this.$textbox
.trigger("input");
this.textbox.dispatchEvent(new Event("input"));
};
this.resize = () => {
this.$background.width(this.$textbox.width());
const computedStyle = window.getComputedStyle(this.textbox);
const width = parseFloat(computedStyle.width);
this.background.style.width = width + 'px';
this.$textbox.height(
Math.max(
this.initialHeight,
this.$background.outerHeight()
)
);
const backgroundHeight = this.background.offsetHeight;
this.textbox.style.height = Math.max(this.initialHeight, backgroundHeight) + 'px';
if ('stickAtBottomWhenScrolling' in window.NotifyModules) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
};
this.contentEscaped = () => $('<div/>').text(this.$textbox.val()).html();
this.contentEscaped = () => {
const div = document.createElement('div');
div.textContent = this.textbox.value;
return div.innerHTML;
};
this.contentReplaced = () => this.contentEscaped().replace(
tagPattern, (match, name, separator, value) => value && separator ?
@@ -75,9 +85,8 @@
this.update = () => {
this.$background.html(
this.highlightPlaceholders ? this.contentReplaced() : this.contentEscaped()
);
this.background.innerHTML =
this.highlightPlaceholders ? this.contentReplaced() : this.contentEscaped();
this.resize();

View File

@@ -1,6 +1,10 @@
export const ErrorBanner = {
hideBanner: () => $('.banner-dangerous').addClass('display-none'),
showBanner: () => $('.banner-dangerous').removeClass('display-none')
hideBanner: () => {
document.querySelectorAll('.banner-dangerous').forEach(el => el.classList.add('display-none'));
},
showBanner: () => {
document.querySelectorAll('.banner-dangerous').forEach(el => el.classList.remove('display-none'));
}
};
window.NotifyModules = window.NotifyModules || {};

View File

@@ -4,15 +4,13 @@
window.NotifyModules['track-error'] = function() {
this.start = function(element) {
var component = $(element);
// 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'),
element.dataset.errorType,
{
'label': component.data('error-label')
'label': element.dataset.errorLabel
}
);
}

View File

@@ -28,30 +28,38 @@ function initUploadStatusAnnouncer() {
"use strict";
window.NotifyModules['file-upload'] = function() {
this.submit = () => this.$form.trigger('submit');
this.submit = () => this.form.submit();
this.showCancelButton = () => {
$('.file-upload-button', this.$form).replaceWith(`
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
Uploading<span class="loading-spinner" role="status" aria-label="Uploading"></span>
</button>
`);
const uploadButton = this.form.querySelector('.file-upload-button');
if (uploadButton) {
uploadButton.outerHTML = `
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
Uploading<span class="loading-spinner" role="status" aria-label="Uploading"></span>
</button>
`;
}
};
this.start = function(component) {
this.$form = $(component);
this.form = component;
this.$form.on('click', '[data-module="upload-trigger"]', function () {
const inputId = $(this).data('file-input-id');
const fileInput = document.getElementById(inputId);
if (fileInput) fileInput.click();
this.form.addEventListener('click', (event) => {
const trigger = event.target.closest('[data-module="upload-trigger"]');
if (trigger) {
const inputId = trigger.dataset.fileInputId;
const fileInput = document.getElementById(inputId);
if (fileInput) fileInput.click();
}
});
$(window).on("pageshow", () => this.$form[0].reset());
window.addEventListener("pageshow", () => this.form.reset());
this.$form.on('change', '.file-upload-field', () => {
this.submit();
this.showCancelButton();
this.form.addEventListener('change', (event) => {
if (event.target.closest('.file-upload-field')) {
this.submit();
this.showCancelButton();
}
});
};
};

View File

@@ -1,10 +1,6 @@
// Webpack Entry Point
// Vendor libraries
import $ from 'jquery';
window.jQuery = window.$ = $;
import './jquery-expose.js';
import 'query-command-supported';
import 'textarea-caret';
import * as cbor from 'cbor-js';
@@ -74,11 +70,9 @@ import './fileUpload.js';
import './errorTracking.js';
import './templateFolderForm.js';
import './collapsibleCheckboxes.js';
import './radioSlider.js';
import './updateStatus.js';
import './main.js';
import './listEntry.js';
import './stick-to-window-when-scrolling.js';
import './totalMessagesChart.js';
import './activityChart.js';
import './job-polling.js';

View File

@@ -1,7 +0,0 @@
// 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,22 +1,28 @@
(function (window) {
'use strict';
// This module creates dynamic add/remove input lists. It lets users add multiple entries
// (like email addresses or domains) with "Add another" and "Remove" buttons. It handles
// min/max entry limits, focus management, and preserves input attributes.
// We're keeping this for now but it's not currently being used in the application.
var Modules = window.NotifyModules;
var lists = [],
listEntry,
ListEntry;
ListEntry = function (elm) {
var $elm = $(elm),
idPattern = $elm.prop('id');
var idPattern = elm.id;
if (!idPattern) { return false; }
if (!idPattern) {
return false;
}
this.idPattern = idPattern;
this.elementSelector = '.list-entry, .input-list__button--remove, .input-list__button--add';
this.entries = [];
this.$wrapper = $elm;
this.wrapper = elm;
this.minEntries = 2;
this.listItemName = this.$wrapper.data('listItemName');
this.listItemName = this.wrapper.dataset.listItemName;
this.getSharedAttributes();
this.getOriginalClasses();
@@ -26,33 +32,43 @@
this.render();
this.bindEvents();
};
ListEntry.optionalAttributes = ['aria-describedby'];
ListEntry.escapeHtml = function(unsafe) {
if (!unsafe) return '';
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
ListEntry.prototype.renderEntry = function(data) {
var escapeHtml = ListEntry.escapeHtml;
return `
<div class="list-entry">
<label for="${data.id}" class="usa-label">
<span class="usa-sr-only">${data.listItemName} number </span>${data.number}.
<label for="${escapeHtml(data.id)}" class="usa-label">
<span class="usa-sr-only">${escapeHtml(data.listItemName)} number </span>${escapeHtml(data.number)}.
</label>
<input
class="usa-input ${data.classes || ''}"
name="${data.name}"
id="${data.id}"
${data.value ? `value="${data.value}"` : ''}
class="usa-input ${escapeHtml(data.classes || '')}"
name="${escapeHtml(data.name)}"
id="${escapeHtml(data.id)}"
${data.value ? `value="${escapeHtml(data.value)}"` : ''}
${data.sharedAttributes}
/>
${data.button ? `
<button type="button" class="usa-button usa-button--unstyled input-list__button--remove">
Remove<span class="usa-sr-only"> ${data.listItemName} number ${data.number}</span>
Remove<span class="usa-sr-only"> ${escapeHtml(data.listItemName)} number ${escapeHtml(data.number)}</span>
</button>
` : ''}
</div>
`;
};
ListEntry.prototype.renderAddButton = function(data) {
return `<button type="button" class="usa-button usa-button--outline input-list__button--add margin-top-4">Add another ${data.listItemName} (${data.entriesLeft} remaining)</button>`;
var escapeHtml = ListEntry.escapeHtml;
return `<button type="button" class="usa-button usa-button--outline input-list__button--add margin-top-4">Add another ${escapeHtml(data.listItemName)} (${escapeHtml(data.entriesLeft)} remaining)</button>`;
};
ListEntry.prototype.getSharedAttributes = function () {
var $inputs = this.$wrapper.find('input'),
var inputs = this.wrapper.querySelectorAll('input'),
generatedAttributes = ['id', 'name', 'value', 'class'],
attributes = [],
attrIdx,
@@ -64,15 +80,16 @@
elmIdx = attrsByElm.length,
existingAttributes = [],
elmAttrs,
attrIdx;
attrIdx,
escapeHtml = ListEntry.escapeHtml;
while (elmIdx--) {
elmAttrs = attrsByElm[elmIdx];
attrIdx = elmAttrs.length;
while (attrIdx--) {
// prevent duplicates
if ($.inArray(elmAttrs[attrIdx].name, existingAttributes) === -1) {
attrStr += ` ${elmAttrs[attrIdx].name}="${elmAttrs[attrIdx].value}"`;
if (existingAttributes.indexOf(elmAttrs[attrIdx].name) === -1) {
attrStr += ` ${escapeHtml(elmAttrs[attrIdx].name)}="${escapeHtml(elmAttrs[attrIdx].value)}"`;
existingAttributes.push(elmAttrs[attrIdx].name);
}
}
@@ -80,11 +97,11 @@
return attrStr;
};
$inputs.each(function (idx, elm) {
inputs.forEach(function (elm) {
attrIdx = elm.attributes.length;
elmAttributes = [];
while(attrIdx--) {
if ($.inArray(elm.attributes[attrIdx].name, generatedAttributes) === -1) {
if (generatedAttributes.indexOf(elm.attributes[attrIdx].name) === -1) {
elmAttributes.push({
'name': elm.attributes[attrIdx].name,
'value': elm.attributes[attrIdx].value
@@ -99,9 +116,9 @@
this.sharedAttributes = (attributes.length) ? getAttributesHTML(attributes) : '';
};
ListEntry.prototype.getOriginalClasses = function () {
var $firstInput = this.$wrapper.find('input').first();
if ($firstInput.length) {
var classList = $firstInput.attr('class');
var firstInput = this.wrapper.querySelector('input');
if (firstInput) {
var classList = firstInput.getAttribute('class');
if (classList) {
// Preserve any additional classes from the original input
this.additionalClasses = classList;
@@ -114,8 +131,8 @@
};
ListEntry.prototype.getValues = function () {
this.entries = [];
this.$wrapper.find('input').each(function (idx, elm) {
var val = $(elm).val();
this.wrapper.querySelectorAll('input').forEach(function (elm) {
var val = elm.value;
this.entries.push(val);
}.bind(this));
@@ -144,11 +161,12 @@
}
};
ListEntry.prototype.bindEvents = function () {
this.$wrapper.on('click', '.input-list__button--remove', function (e) {
this.removeEntry($(e.target));
}.bind(this));
this.$wrapper.on('click', '.input-list__button--add', function (e) {
this.addEntry();
this.wrapper.addEventListener('click', function (e) {
if (e.target.closest('.input-list__button--remove')) {
this.removeEntry(e.target);
} else if (e.target.closest('.input-list__button--add')) {
this.addEntry();
}
}.bind(this));
};
ListEntry.prototype.shiftFocus = function (opts) {
@@ -159,7 +177,13 @@
} else { // opts.action === 'add'
numberTargeted = opts.entryNumberFocused + 1;
}
this.$wrapper.find('.list-entry').eq(numberTargeted - 1).find('input').focus();
var entries = this.wrapper.querySelectorAll('.list-entry');
if (entries[numberTargeted - 1]) {
var input = entries[numberTargeted - 1].querySelector('input');
if (input) {
input.focus();
}
}
};
ListEntry.prototype.removeEntryFromEntries = function (entryNumber) {
var idx,
@@ -173,7 +197,7 @@
}
this.entries = newEntries;
};
ListEntry.prototype.addEntry = function ($removeButton) {
ListEntry.prototype.addEntry = function () {
var currentLastEntryNumber = this.entries.length;
this.getValues();
@@ -181,8 +205,23 @@
this.render();
this.shiftFocus({ 'action' : 'add', 'entryNumberFocused' : currentLastEntryNumber });
};
ListEntry.prototype.removeEntry = function ($removeButton) {
var entryNumber = parseInt($removeButton.find('span').text().match(/\d+/)[0], 10);
ListEntry.prototype.removeEntry = function (removeButton) {
var button = removeButton.closest('.input-list__button--remove');
if (!button) {
console.error('ListEntry: Remove button not found');
return;
}
var span = button.querySelector('span');
if (!span) {
console.error('ListEntry: Entry number span not found in remove button');
return;
}
var match = span.textContent.match(/\d+/);
if (!match) {
console.error('ListEntry: Could not find entry number in remove button');
return;
}
var entryNumber = parseInt(match[0], 10);
this.getValues();
this.removeEntryFromEntries(entryNumber);
@@ -190,8 +229,10 @@
this.shiftFocus({ 'action' : 'remove', 'entryNumberFocused' : entryNumber });
};
ListEntry.prototype.render = function () {
this.$wrapper.find(this.elementSelector).remove();
$.each(this.entries, function (idx, entry) {
this.wrapper.querySelectorAll(this.elementSelector).forEach(function(el) {
el.remove();
});
this.entries.forEach(function (entry, idx) {
var entryNumber = idx + 1,
dataObj = {
'id' : this.getId(entryNumber),
@@ -207,10 +248,10 @@
if (entryNumber > 1) {
dataObj.button = true;
}
this.$wrapper.append(this.renderEntry(dataObj));
this.wrapper.insertAdjacentHTML('beforeend', this.renderEntry(dataObj));
}.bind(this));
if (this.entries.length < this.maxEntries) {
this.$wrapper.append(this.renderAddButton({
this.wrapper.insertAdjacentHTML('beforeend', this.renderAddButton({
'listItemName' : this.listItemName,
'entriesLeft' : (this.maxEntries - this.entries.length)
}));
@@ -219,7 +260,7 @@
Modules['list-entry'] = function () {
this.start = component => lists.push(new ListEntry($(component)));
this.start = component => lists.push(new ListEntry(component));
};

View File

@@ -12,24 +12,25 @@
}
};
let filter = ($searchBox, $searchLabel, $liveRegion, $targets) => () => {
let filter = (searchBox, searchLabel, liveRegion, targets) => () => {
let query = normalize($searchBox.val());
let query = normalize(searchBox.value);
let results = 0;
let $noResultsMessage = $('.js-live-search-no-results');
let noResultsMessage = document.querySelector('.js-live-search-no-results');
$targets.each(function() {
targets.forEach(function(target) {
let content = $('.live-search-relevant', this).text() || $(this).text();
let relevantElement = target.querySelector('.live-search-relevant');
let content = relevantElement ? relevantElement.textContent : target.textContent;
if ($(this).has(':checked').length) {
$(this).show();
if (target.querySelector(':checked')) {
target.classList.remove('js-hidden');
results++;
return;
}
if (query == '') {
$(this).removeClass('js-hidden');
target.classList.remove('js-hidden');
results++;
return;
}
@@ -37,34 +38,31 @@
let isMatch = normalize(content).includes(normalize(query));
if (isMatch) {
$(this).removeClass('js-hidden');
target.classList.remove('js-hidden');
results++;
} else {
$(this).addClass('js-hidden');
target.classList.add('js-hidden');
}
});
if (query !== '' && results === 0) {
$noResultsMessage.removeClass('js-hidden');
} else {
$noResultsMessage.addClass('js-hidden');
if (noResultsMessage) {
if (query !== '' && results === 0) {
noResultsMessage.classList.remove('js-hidden');
} else {
noResultsMessage.classList.add('js-hidden');
}
}
if (state === 'loaded') {
if (query !== '') {
$searchBox.attr('aria-label', $searchLabel.text().trim() + ', ' + resultsSummary(results));
searchBox.setAttribute('aria-label', searchLabel.textContent.trim() + ', ' + resultsSummary(results));
}
state = 'active';
} else {
$searchBox.removeAttr('aria-label');
$liveRegion.text(resultsSummary(results));
searchBox.removeAttribute('aria-label');
liveRegion.textContent = resultsSummary(results);
}
// make sticky JS recalculate its cache of the element's position
// because live search can change the height document
if (window.NotifyModules && 'stickAtBottomWhenScrolling' in window.NotifyModules) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
};
@@ -73,22 +71,24 @@
this.start = function(component) {
let $component = $(component);
let searchBox = component.querySelector('input');
let searchLabel = component.querySelector('label');
let liveRegion = component.querySelector('.live-search__status');
let $searchBox = $('input', $component);
let $searchLabel = $('label', $component);
let $liveRegion = $('.live-search__status', $component);
let targetsSelector = component.dataset.targets;
let targets = Array.from(document.querySelectorAll(targetsSelector));
let filterFunc = filter(
$searchBox,
$searchLabel,
$liveRegion,
$($component.data('targets'))
searchBox,
searchLabel,
liveRegion,
targets
);
state = 'loaded';
$searchBox.on('keyup input', filterFunc);
searchBox.addEventListener('keyup', filterFunc);
searchBox.addEventListener('input', filterFunc);
filterFunc();

View File

@@ -1,10 +1,11 @@
(function (window) {
'use strict';
var $ = window.jQuery;
// this javascript could be removed in the future since it is only being used
// on the page "send-files-by-email". which is not currently in use.
function ShowHideContent () {
var self = this;
var eventHandlers = new Map();
var selectors = {
namespace: 'ShowHideContent',
@@ -12,126 +13,159 @@
checkbox: '[data-target] > input[type="checkbox"]'
};
function initToggledContent () {
var $control = $(this);
var $content = getToggledContent($control);
function initToggledContent (control) {
var content = getToggledContent(control);
if ($content.length) {
$control.attr('aria-controls', $content.attr('id'));
$control.attr('aria-expanded', 'false');
$content.attr('aria-hidden', 'true');
if (content) {
control.setAttribute('aria-controls', content.getAttribute('id'));
control.setAttribute('aria-expanded', 'false');
content.setAttribute('aria-hidden', 'true');
}
}
function getToggledContent ($control) {
function getToggledContent (control) {
try {
var id = $control.attr('aria-controls');
var id = control.getAttribute('aria-controls');
if (!id) {
id = $control.closest('[data-target]').data('target');
var parent = control.closest('[data-target]');
id = parent ? parent.dataset.target : null;
}
if (!id || !/^[\w-]+$/.test(id)) {
console.warn('Invalid element ID:', id);
return $();
return null;
}
return $('#' + id);
return document.getElementById(id);
} catch (error) {
console.error('Error getting toggled content:', error);
return $();
return null;
}
}
function showToggledContent ($control, $content) {
if ($content.hasClass('display-none')) {
$content.removeClass('display-none');
$content.attr('aria-hidden', 'false');
function showToggledContent (control, content) {
if (content.classList.contains('display-none')) {
content.classList.remove('display-none');
content.setAttribute('aria-hidden', 'false');
if ($control.attr('aria-controls')) {
$control.attr('aria-expanded', 'true');
if (control.getAttribute('aria-controls')) {
control.setAttribute('aria-expanded', 'true');
}
}
}
function hideToggledContent ($control, $content) {
$content.addClass('display-none');
$content.attr('aria-hidden', 'true');
function hideToggledContent (control, content) {
content.classList.add('display-none');
content.setAttribute('aria-hidden', 'true');
if ($control.attr('aria-controls')) {
$control.attr('aria-expanded', 'false');
if (control.getAttribute('aria-controls')) {
control.setAttribute('aria-expanded', 'false');
}
}
function handleRadioContent ($control, $content) {
var selector = selectors.radio + '[name=' + escapeElementName($control.attr('name')) + ']';
var $radios = $(selector);
function handleRadioContent (control, content) {
var selector = selectors.radio + '[name=' + escapeElementName(control.getAttribute('name')) + ']';
var radios = document.querySelectorAll(selector);
$radios.each(function () {
hideToggledContent($(this), getToggledContent($(this)));
radios.forEach(function (radio) {
var radioContent = getToggledContent(radio);
if (radioContent) {
hideToggledContent(radio, radioContent);
}
});
showToggledContent($control, $content);
if (content) {
showToggledContent(control, content);
}
}
function handleCheckboxContent ($control, $content) {
if ($control.is(':checked')) {
showToggledContent($control, $content);
function handleCheckboxContent (control, content) {
if (!content) {
return;
}
if (control.checked) {
showToggledContent(control, content);
} else {
hideToggledContent($control, $content);
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;
return str ? str.replace(/\\/g, '\\\\').replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\\$1') : str;
}
function setupHandlers () {
var $controls = $(selectors.radio + ', ' + selectors.checkbox);
var radios = document.querySelectorAll(selectors.radio);
var checkboxes = document.querySelectorAll(selectors.checkbox);
$(selectors.radio).on('click.' + selectors.namespace, function () {
handleRadioContent($(this), getToggledContent($(this)));
radios.forEach(function (radio) {
var handler = function () {
handleRadioContent(radio, getToggledContent(radio));
};
radio.addEventListener('click', handler);
eventHandlers.set(radio, handler);
});
$(selectors.checkbox).on('click.' + selectors.namespace, function () {
handleCheckboxContent($(this), getToggledContent($(this)));
checkboxes.forEach(function (checkbox) {
var handler = function () {
handleCheckboxContent(checkbox, getToggledContent(checkbox));
};
checkbox.addEventListener('click', handler);
eventHandlers.set(checkbox, handler);
});
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);
var allControls = Array.from(radios).concat(Array.from(checkboxes));
var checkedControls = allControls.filter(function (control) {
return control.checked;
});
if (checkedControls.length) {
checkedControls.forEach(function (control) {
var content = getToggledContent(control);
if (content) {
if (control.type === 'radio') {
handleRadioContent(control, content);
} else {
handleCheckboxContent(control, content);
}
}
});
}
}
self.destroy = function () {
var $controls = $(selectors.radio + ', ' + selectors.checkbox);
var radios = document.querySelectorAll(selectors.radio);
var checkboxes = document.querySelectorAll(selectors.checkbox);
var allControls = Array.from(radios).concat(Array.from(checkboxes));
$controls.each(function () {
var $control = $(this);
var $content = getToggledContent($control);
allControls.forEach(function (control) {
var content = getToggledContent(control);
$control.removeAttr('aria-controls aria-expanded');
$content.removeAttr('aria-hidden');
control.removeAttribute('aria-controls');
control.removeAttribute('aria-expanded');
if (content) {
content.removeAttribute('aria-hidden');
}
var handler = eventHandlers.get(control);
if (handler) {
control.removeEventListener('click', handler);
eventHandlers.delete(control);
}
});
$(selectors.radio).off('.' + selectors.namespace);
$(selectors.checkbox).off('.' + selectors.namespace);
};
self.init = function () {
try {
$(selectors.radio + ', ' + selectors.checkbox).each(initToggledContent);
var radios = document.querySelectorAll(selectors.radio);
var checkboxes = document.querySelectorAll(selectors.checkbox);
var allControls = Array.from(radios).concat(Array.from(checkboxes));
allControls.forEach(initToggledContent);
setupHandlers();
} catch (error) {

View File

@@ -1,19 +1,23 @@
(function (window) {
'use strict';
var $ = window.jQuery;
window.NotifyModules.moduleSystem = {
find: function (container) {
container = container || $('body');
container = container || document.body;
var modules;
var moduleSelector = '[data-module]';
modules = container.find(moduleSelector);
// If container is already an element, use it directly
if (container instanceof Element) {
modules = Array.from(container.querySelectorAll(moduleSelector));
if (container.is(moduleSelector)) {
modules = modules.add(container);
// If the container itself is a module, include it
if (container.matches && container.matches(moduleSelector)) {
modules.unshift(container);
}
} else {
modules = Array.from(document.querySelectorAll(moduleSelector));
}
return modules;
@@ -25,16 +29,16 @@
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');
var element = modules[i];
var type = this.camelCaseAndCapitalise(element.dataset.module);
var started = element.dataset.moduleStarted;
if (typeof window.NotifyModules[type] === 'function' && !started) {
module = new window.NotifyModules[type]();
if (module.start) {
module.start(element);
}
element.data('module-started', true);
element.dataset.moduleStarted = 'true';
}
} catch (error) {
console.error('Failed to initialize module:', type || 'unknown', error);

View File

@@ -2,45 +2,51 @@
"use strict";
const disableSubmitButtons = function (event) {
const $submitButton = $(this).find(':submit');
const form = event.currentTarget;
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
if ($submitButton.data('clicked') === 'true') {
if (!submitButton) return;
if (submitButton.dataset.clicked === 'true') {
event.preventDefault();
return;
}
$submitButton.data('clicked', 'true');
submitButton.dataset.clicked = 'true';
// Add loading spinner for Send/Schedule/Cancel buttons
const buttonName = $submitButton.attr('name')?.toLowerCase();
const buttonName = submitButton.getAttribute('name')?.toLowerCase();
if (["send", "schedule", "cancel"].includes(buttonName)) {
// Use setTimeout with minimal delay to allow form submission to proceed first
setTimeout(() => {
$submitButton.prop('disabled', true);
submitButton.disabled = true;
// 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>');
if (submitButton.querySelector('.loading-spinner') === null) {
submitButton.setAttribute('aria-busy', 'true');
submitButton.insertAdjacentHTML('beforeend', '<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';
const cancelButtons = Array.from(document.querySelectorAll('button[name]')).filter(button => {
return button.getAttribute('name')?.toLowerCase() === 'cancel';
});
$cancelButton.prop('disabled', true);
cancelButtons.forEach(button => button.disabled = true);
}, 50); // Small delay to ensure form submits first
} else {
setTimeout(() => renableSubmitButton($submitButton)(), 1500);
setTimeout(() => renableSubmitButton(submitButton)(), 1500);
}
};
const renableSubmitButton = ($submitButton) => () => {
$submitButton.data('clicked', '');
$submitButton.prop('disabled', false);
$submitButton.attr('aria-busy', 'false');
$submitButton.find('.loading-spinner').remove(); // clean up spinner
const renableSubmitButton = (submitButton) => () => {
submitButton.dataset.clicked = '';
submitButton.disabled = false;
submitButton.setAttribute('aria-busy', 'false');
const spinner = submitButton.querySelector('.loading-spinner');
if (spinner) spinner.remove(); // clean up spinner
};
$('form').on('submit', disableSubmitButtons);
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', disableSubmitButtons);
});
})();

View File

@@ -4,6 +4,16 @@
var Modules = window.NotifyModules;
const escapeHtml = (unsafe) => {
if (!unsafe) return '';
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
// Template functions for rendering component states
let renderStates = {
'initial': function(data) {
@@ -11,14 +21,14 @@
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
<input class="usa-radio__input" checked="checked" id="${escapeHtml(data.name)}-0" name="${escapeHtml(data.name)}" type="radio" value="">
<label class="usa-radio__label" for="${escapeHtml(data.name)}-0">Now</label>
</div>
</div>
` : ''}
<div class="radio-select__column margin-y-2">
${data.categories.map(category =>
`<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='${category}' />`
`<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='${escapeHtml(category)}' />`
).join('')}
</div>
`;
@@ -28,16 +38,16 @@
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
<input class="usa-radio__input" checked="checked" id="${escapeHtml(data.name)}-0" name="${escapeHtml(data.name)}" type="radio" value="">
<label class="usa-radio__label" for="${escapeHtml(data.name)}-0">Now</label>
</div>
</div>
` : ''}
<div class="radio-select__column margin-y-2">
${data.choices.map(choice => `
<div class="usa-radio js-option">
<input class="usa-radio__input" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
<input class="usa-radio__input" type="radio" value="${escapeHtml(choice.value)}" id="${escapeHtml(choice.id)}" name="${escapeHtml(data.name)}" />
<label class="usa-radio__label" for="${escapeHtml(choice.id)}">${escapeHtml(choice.label)}</label>
</div>
`).join('')}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
@@ -49,16 +59,16 @@
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
<input class="usa-radio__input" id="${escapeHtml(data.name)}-0" name="${escapeHtml(data.name)}" type="radio" value="">
<label class="usa-radio__label" for="${escapeHtml(data.name)}-0">Now</label>
</div>
</div>
` : ''}
<div class="radio-select__column margin-y-2">
${data.choices.map(choice => `
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
<input class="usa-radio__input" checked="checked" type="radio" value="${escapeHtml(choice.value)}" id="${escapeHtml(choice.id)}" name="${escapeHtml(data.name)}" />
<label class="usa-radio__label" for="${escapeHtml(choice.id)}">${escapeHtml(choice.label)}</label>
</div>
`).join('')}
</div>
@@ -70,12 +80,13 @@
};
let shiftFocus = function(elementToFocus, component) {
const radios = component.querySelectorAll('[type=radio]');
// The first option is always the default
if (elementToFocus === 'default') {
$('[type=radio]', component).eq(0).focus();
if (elementToFocus === 'default' && radios[0]) {
radios[0].focus();
}
if (elementToFocus === 'option') {
$('[type=radio]', component).eq(1).focus();
if (elementToFocus === 'option' && radios[1]) {
radios[1].focus();
}
};
@@ -83,24 +94,22 @@
this.start = function(component) {
let $component = $(component);
let render = (state, data) => {
$component.html(renderStates[state](data));
component.innerHTML = renderStates[state](data);
};
// store array of all options in component
let choices = $('label', $component).toArray().map(function(element) {
let $element = $(element);
let choices = Array.from(component.querySelectorAll('label')).map(function(element) {
return {
'id': $element.attr('for'),
'label': $.trim($element.text()),
'value': $element.prev('input').attr('value')
'id': element.htmlFor,
'label': element.textContent.trim(),
'value': element.previousElementSibling.value
};
});
let categories = $component.data('categories').split(',');
let name = $component.find('input').eq(0).attr('name');
let categories = component.dataset.categories.split(',');
let name = component.querySelector('input').name;
let mousedownOption = null;
let showNowAsDefault = (
$component.data('show-now-as-default').toString() === 'true' ?
component.dataset.showNowAsDefault === 'true' ?
{'name': name} : false
);
@@ -129,22 +138,23 @@
const parentNode = event.target.parentNode;
if (parentNode === mousedownOption) {
const value = $('input', parentNode).attr('value');
const input = parentNode.querySelector('input');
const value = input ? input.value : '';
selectOption(value);
// clear tracking
mousedownOption = null;
$(document).off('mouseup', trackMouseup);
document.removeEventListener('mouseup', trackMouseup);
}
};
// set events
$component
.on('click', '.radio-select__button--category', function(event) {
// set events using event delegation
component.addEventListener('click', function(event) {
// Handle category button clicks
if (event.target.classList.contains('radio-select__button--category')) {
event.preventDefault();
let wordsInDay = $(this).attr('value').split(' ');
let wordsInDay = event.target.value.split(' ');
let day = wordsInDay[wordsInDay.length - 1].toLowerCase();
render('choose', {
'choices': choices.filter(
@@ -154,57 +164,58 @@
'showNowAsDefault': showNowAsDefault
});
shiftFocus('option', component);
}
})
.on('mousedown', '.js-option', function(event) {
mousedownOption = this;
// mouseup on the same option completes the click action
$(document).on('mouseup', trackMouseup);
})
// space and enter, clicked on a radio confirm that option was selected
.on('keydown', 'input[type=radio]', function(event) {
// allow keypresses which arent enter or space through
if (event.which !== 13 && event.which !== 32) {
return true;
}
// Handle done button clicks
if (event.target.classList.contains('radio-select__button--done')) {
event.preventDefault();
let value = $(this).attr('value');
selectOption(value);
})
.on('click', '.radio-select__button--done', function(event) {
event.preventDefault();
let $selection = $('input[type=radio]:checked', this.parentNode);
if ($selection.length) {
let selection = event.target.parentNode.querySelector('input[type=radio]:checked');
if (selection) {
render('chosen', {
'choices': choices.filter(
element => element.value == $selection.eq(0).attr('value')
element => element.value == selection.value
),
'name': name,
'showNowAsDefault': showNowAsDefault
});
shiftFocus('option', component);
} else {
reset();
shiftFocus('default', component);
}
}
})
.on('click', '.radio-select__button--reset', function(event) {
// Handle reset button clicks
if (event.target.classList.contains('radio-select__button--reset')) {
event.preventDefault();
reset();
shiftFocus('default', component);
}
});
});
component.addEventListener('mousedown', function(event) {
// Handle option mousedown
const option = event.target.closest('.js-option');
if (option) {
mousedownOption = option;
// mouseup on the same option completes the click action
document.addEventListener('mouseup', trackMouseup);
}
});
component.addEventListener('keydown', function(event) {
// Handle radio keydown (space and enter)
if (event.target.type === 'radio') {
// allow keypresses which aren't enter or space through
if (event.which !== 13 && event.which !== 32) {
return true;
}
event.preventDefault();
let value = event.target.value;
selectOption(value);
}
});
// set HTML to initial state
render('initial', {
@@ -213,7 +224,7 @@
'showNowAsDefault': showNowAsDefault
});
$component.css({'height': 'auto'});
component.style.height = 'auto';
};

View File

@@ -1,28 +0,0 @@
(function(window) {
"use strict";
window.NotifyModules['radio-slider'] = function() {
this.start = function(component) {
$(component)
.on('click', function() {
valuesInLabel = $(this).find(':checked').next('label').text().split('/');
if (valuesInLabel.length === 2) {
leftValue = valuesInLabel[0];
rightValue = valuesInLabel[1];
$(this).find('.radio-slider-left-value').text(leftValue);
$(this).find('.radio-slider-right-value').text(rightValue);
}
})
.trigger('click');
};
};
})(window);

View File

@@ -1,995 +0,0 @@
;(function (window) {
'use strict';
var $ = window.jQuery;
var NotifyModules = window.NotifyModules || {};
var _mode = 'default';
// Constructor to make objects representing the area sticky elements can scroll in
var ScrollArea = function (el, edge, selector) {
var $el = el.$fixedEl;
var $scrollArea = $el.closest('.sticky-scroll-area');
if($scrollArea.length === 0) {
$scrollArea = $el.parent();
$scrollArea.addClass('sticky-scroll-area');
}
this._els = [el];
this.edge = edge;
this.selector = selector;
this.node = $scrollArea.get(0);
this.setEvents();
};
ScrollArea.prototype.addEl = function (el) {
this._els.push(el);
};
ScrollArea.prototype.hasEl = function (el) {
return $.inArray(el, this._els) !== -1;
};
ScrollArea.prototype.updateEls = function (usedEls) {
this._els = usedEls;
};
ScrollArea.prototype.setEvents = function () {
this.node.addEventListener('focus', this.focusHandler.bind(this), true);
$(this.node).on('keyup', 'textarea', this.focusHandler.bind(this));
};
ScrollArea.prototype.removeEvents = function () {
this.node.removeEventListener('focus', this.focusHandler.bind(this));
$(this.node).find('textarea').off('keyup', 'textarea', this.focusHandler.bind(this));
};
ScrollArea.prototype.getFocusedDetails = {
forElement: function ($focusedElement) {
var focused = {
'top': $focusedElement.offset().top,
'height': $focusedElement.outerHeight(),
'type': 'element'
};
focused.bottom = focused.top + focused.height;
return focused;
},
forCaret: function ($textarea) {
var textarea = $textarea.get(0);
var caretCoordinates = window.getCaretCoordinates(textarea, textarea.selectionEnd);
var focused = {
'top': $textarea.offset().top + caretCoordinates.top,
'height': caretCoordinates.height,
'type': 'caret'
};
focused.bottom = focused.top + focused.height;
return focused;
}
};
ScrollArea.prototype.focusHandler = function (e) {
this.scrollToRevealElement($(document.activeElement));
};
ScrollArea.prototype.scrollToRevealElement = function ($el) {
var nodeName = $el.get(0).nodeName.toLowerCase();
var endOfFurthestEl = focusOverlap.endOfFurthestEl(this._els, this.edge);
var isInSticky = function () {
return $el.closest(this.selector).length > 0;
}.bind(this);
var focused;
var overlap;
// if textarea is focused, we care about checking the caret, not the whole element
if (nodeName === 'textarea') {
focused = this.getFocusedDetails.forCaret($el);
} else {
if (isInSticky()) { return; }
focused = this.getFocusedDetails.forElement($el);
}
overlap = focusOverlap.getOverlap(focused, this.edge, endOfFurthestEl);
if (overlap > 0) {
focusOverlap.adjustForOverlap(focused, this.edge, overlap);
}
};
ScrollArea.prototype.destroy = function () {
this.removeEvents();
};
// Object collecting together methods for interacting with scrollareas
var scrollAreas = {
_scrollAreas: [],
getAreaForEl: function (el) {
var loopIdx = this._scrollAreas.length;
while(loopIdx--) {
if (this._scrollAreas[loopIdx].hasEl(el)) {
return this._scrollAreas[loopIdx];
}
}
return false;
},
getAreaByEl: function (el) {
var matches = $.grep(this._scrollAreas, function (area) {
return $.inArray(el, area.els) !== -1;
});
return matches[0] || false;
},
addEl: function (el, edge, selector) {
var scrollArea = this.getAreaForEl(el);
if (!scrollArea) {
this._scrollAreas.push(new ScrollArea(el, edge, selector));
} else {
scrollArea.addEl(el);
}
},
syncEls: function (elsInDOM) {
var self = this;
var unusedAreas = [];
var getUsed = function (area) {
var used = [];
$.each(elsInDOM, function (elIdx, el) {
if (area.hasEl(el)) {
used.push(el);
}
});
return used;
};
var deleteUnused = function (idx, areaIdx) {
// remove any events for overlap checking bound to the scrollArea
self._scrollAreas[areaIdx].destroy();
self._scrollAreas.splice(areaIdx, 1);
};
// update any scroll areas with els still in the DOM and track any with none
$.each(this._scrollAreas, function (areaIdx, area) {
var used = getUsed(area);
if (!used.length) {
unusedAreas.push(areaIdx);
}
area.updateEls(used);
});
// delete any scroll areas with no els still in DOM
$.each(unusedAreas, deleteUnused);
}
};
// Object collecting together methods for stopping sticky overlapping focused elements
var focusOverlap = {
getOverlap: function (focused, edge, endOfFurthestEl) {
if (!endOfFurthestEl) { return 0; }
if (edge === 'top') {
return endOfFurthestEl - focused.top;
} else {
return focused.bottom - endOfFurthestEl;
}
},
endOfFurthestEl: function (els, edge) {
var stuckEls = $.grep(els, function (el) { return el.isStuck(); });
var edgeOfEl;
var offsets;
if (edge === 'bottom') {
edgeOfEl = function (el) {
return el.$fixedEl.offset().top;
};
} else {
edgeOfEl = function (el) {
return el.$fixedEl.offset().top + el.height;
};
}
if (!stuckEls.length) { return false; }
offsets = $.map(stuckEls, function (el) { return edgeOfEl(el); });
return offsets.reduce(function (accumulator, offset) {
return (accumulator < offset) ? offset: accumulator;
});
},
adjustForOverlap: function (focused, edge, overlap) {
var scrollTop = $(window).scrollTop();
// scroll so element becomes visible
if (edge === 'top') {
$(window).scrollTop(scrollTop - overlap);
} else {
$(window).scrollTop(scrollTop + overlap);
}
}
};
// Object collecting together methods for dealing with marking the edge of a sticky, or group of
// sticky elements (as seen in dialog mode)
var oppositeEdge = {
_classes: {
'top': 'content-fixed__top',
'bottom': 'content-fixed__bottom'
},
_getClassForEdge: function (edge) {
return this._classes[edge];
},
mark: function (sticky) {
var edgeClass = this._getClassForEdge(sticky.edge);
var els;
if (_mode === 'dialog') {
els = [dialog.getElementAtOppositeEnd(sticky)];
} else {
els = sticky._els;
}
els = $.grep(els, function (el) { return el.isStuck(); });
$.each(els, function (i, el) {
el.$fixedEl.addClass(edgeClass);
});
},
unmark: function (sticky) {
var edgeClass = this._getClassForEdge(sticky.edge);
$.each(sticky._els, function (i, el) {
el.$fixedEl.removeClass(edgeClass);
});
}
};
// Constructor for objects holding data for each element to have sticky behaviour
var StickyElement = function ($el, sticky) {
this._sticky = sticky;
this.$fixedEl = $el;
this._initialFixedClass = 'content-fixed-onload';
this._fixedClass = 'content-fixed';
this._appliedClass = null;
this._$shim = null;
this._stopped = false;
this._hasLoaded = false;
this._canBeStuck = true;
this.verticalMargins = {
'top': parseInt(this.$fixedEl.css('margin-top'), 10),
'bottom': parseInt(this.$fixedEl.css('margin-bottom'), 10),
};
};
StickyElement.prototype._getShimCSS = function () {
return {
'width': this.horizontalSpace + 'px',
'height': this.height + 'px',
'margin-top': this.verticalMargins.top + 'px',
'margin-bottom': this.verticalMargins.bottom + 'px'
};
};
StickyElement.prototype.stickyClass = function () {
return (this._sticky._initialPositionsSet) ? this._fixedClass : this._initialFixedClass;
};
StickyElement.prototype.appliedClass = function () {
return this._appliedClass;
};
StickyElement.prototype.removeStickyClasses = function (sticky) {
this.$fixedEl.removeClass([
this._initialFixedClass,
this._fixedClass
].join(' '));
};
StickyElement.prototype.isStuck = function () {
return this._appliedClass !== null;
};
StickyElement.prototype.stick = function (sticky) {
this._appliedClass = this.stickyClass();
this.$fixedEl.addClass(this._appliedClass);
this._hasBeenCalled = true;
};
StickyElement.prototype.release = function (sticky) {
this._appliedClass = null;
this.removeStickyClasses(sticky);
this._hasBeenCalled = true;
};
// When a sticky element is moved into the 'stuck' state, a shim is inserted into the
// page to preserve the space the element occupies in the flow.
StickyElement.prototype.addShim = function (position) {
this._$shim = $('<div class="shim">&nbsp</div>');
this._$shim.css(this._getShimCSS());
this.$fixedEl[position](this._$shim);
};
StickyElement.prototype.removeShim = function () {
if (this._$shim !== null) {
this._$shim.remove();
this._$shim = null;
}
};
// Changes to the dimensions of a sticky element with a shim need to be passed on to the shim
StickyElement.prototype.updateShim = function () {
if (this._$shim) {
this._$shim.css(this._getShimCSS());
}
};
StickyElement.prototype.stop = function () {
this._stopped = true;
};
StickyElement.prototype.unstop = function () {
this._stopped = false;
};
StickyElement.prototype.isStopped = function () {
return this._stopped;
};
StickyElement.prototype.isInPage = function () {
var node = this.$fixedEl.get(0);
return (node === document.body) ? false : document.body.contains(node);
};
StickyElement.prototype.canBeStuck = function (val) {
if (val !== undefined) {
this._canBeStuck = val;
} else {
return this._canBeStuck;
}
};
StickyElement.prototype.hasLoaded = function (val) {
if (val !== undefined) {
this._hasLoaded = val;
} else {
return this._hasLoaded;
}
};
// Object collecting together methods for treating sticky elements as if they
// were wrapped by a dialog component
var dialog = {
hasResized: false,
spaceBetweenStickys: 40,
// we add padding of 20px around each sticky to give some space between it and the rest of the page
// this shouldn't apply between stickys in a stack
// (the in-page CSS handles this by each subsequent sticky in a sequence having margin: -40px)
_getPaddingBetweenEls: function (els) {
if (els.length <= 1) { return 0; }
return (els.length - 1) * this.spaceBetweenStickys;
},
_getTotalHeight: function (els) {
var reducer = function (accumulator, currentValue) {
return accumulator + currentValue;
};
var combinedHeight = $.map(els, function (el) { return el.height; }).reduce(reducer);
return combinedHeight - this._getPaddingBetweenEls(els);
},
_elsThatCanBeStuck: function (els) {
return $.grep(els, function (el) { return el.canBeStuck(); });
},
getOffsetFromEdge: function (el, sticky) {
var els = this._elsThatCanBeStuck(sticky._els).slice();
var elIdx;
// els must be arranged furtherest from window edge is stuck to first
// default direction is order in document
if (sticky.edge === 'top') {
els.reverse();
}
elIdx = els.indexOf(el);
// if next to window edge the dialog is stuck to, no offset
if (elIdx === (els.length - 1)) { return 0; }
// make els all those from this one to the window edge
els = els.slice(elIdx + 1);
// remove the space between those els and the one on the edge
return this._getTotalHeight(els) - this.spaceBetweenStickys;
},
getOffsetFromEnd: function (el, sticky) {
var els = this._elsThatCanBeStuck(sticky._els).slice();
var elIdx;
// els must be arranged furtherest from window edge is stuck to first
// default direction is order in document
if (sticky.edge === 'bottom') {
els.reverse();
}
elIdx = els.indexOf(el);
// if next to opposite edge to the one the dialog is stuck to, no offset
if (elIdx === (els.length - 1)) { return 0; }
// make els all those from this one to the window edge
els = els.slice(elIdx + 1);
return this._getTotalHeight(els) - this.spaceBetweenStickys;
},
// checks total height of all this._sticky elements against a height
// unsticks each that won't fit and marks them as unstickable
fitToHeight: function (sticky) {
var self = this;
var els = sticky._els.slice();
var height = sticky.getWindowDimensions().height;
var totalStickyHeight = function () {
return self._getTotalHeight(self._elsThatCanBeStuck(els));
};
var dialogFitsHeight = function () {
return totalStickyHeight() <= height;
};
// els must be arranged furtherest from window edge is stuck to first
// default direction is order in document
if (sticky.edge === 'top') {
els.reverse();
}
// reset elements
$.each(els, function (i, el) { el.canBeStuck(true); });
while (self._elsThatCanBeStuck(els).length && !dialogFitsHeight()) {
var currentEl = self._elsThatCanBeStuck(els)[0];
sticky.reset(currentEl);
currentEl.canBeStuck(false);
if (!self.hasResized) { self.hasResized = true; }
}
},
getElementAtStickyEdge: function (sticky) {
var els = this._elsThatCanBeStuck(sticky._els);
var idx = (sticky.edge === 'top') ? 0 : els.length - 1;
return els[idx];
},
// get element at the end opposite the sticky edge
getElementAtOppositeEnd: function (sticky) {
var els = this._elsThatCanBeStuck(sticky._els);
var idx = (sticky.edge === 'top') ? els.length - 1 : 0;
return els[idx];
},
getInPageEdgePosition: function (sticky) {
return this.getElementAtStickyEdge(sticky).inPageEdgePosition;
},
getHeight: function (els) {
return this._getTotalHeight(this._elsThatCanBeStuck(els));
},
adjustForResize: function (sticky) {
var windowHeight = sticky.getWindowDimensions().height;
if (sticky.edge === 'top') {
$(window).scrollTop(this.getInPageEdgePosition(sticky));
} else {
$(window).scrollTop(this.getInPageEdgePosition(sticky) - windowHeight);
}
this.hasResized = false;
},
releaseEl: function (el, sticky) {
el.$fixedEl.css(sticky.edge, '');
}
};
// Constructor for objects collecting together all generic behaviour for controlling the state of
// sticky elements
var Sticky = function (selector) {
this._hasScrolled = false;
this._scrollTimeout = false;
this._windowHasResized = false;
this._resizeTimeout = false;
this._elsLoaded = false;
this._initialPositionsSet = false;
this._els = [];
this.CSS_SELECTOR = selector;
this.STOP_PADDING = 10;
};
Sticky.prototype.setMode = function (mode) {
_mode = mode;
};
Sticky.prototype.getWindowDimensions = function () {
return {
height: $(global).height(),
width: $(global).width()
};
};
Sticky.prototype.getWindowPositions = function () {
return {
scrollTop: $(global).scrollTop()
};
};
// Change state of sticky elements based on their position relative to the window
Sticky.prototype.setElementPositions = function () {
var self = this,
windowDimensions = self.getWindowDimensions(),
windowTop = self.getWindowPositions().scrollTop,
windowPositions = {
'top': windowTop,
'bottom': windowTop + windowDimensions.height
};
var _setElementPosition = function (el) {
if (self.viewportIsWideEnough(windowDimensions.width)) {
if (self.windowNotPastScrolledFrom(windowPositions, self.getScrolledFrom(el))) {
self.reset(el);
} else { // past the point it sits in the document
if (self.windowNotPastScrollingTo(windowPositions, self.getScrollingTo(el))) {
self.stick(el);
if (el.isStopped()) {
self.unstop(el);
}
} else { // window past scrollingTo position
if (!el.isStuck()) {
self.stick(el);
}
self.stop(el);
}
}
} else {
self.reset(el);
}
};
// clean up any existing styles marking the edges of sticky elements
oppositeEdge.unmark(self);
$.each(self._els, function (i, el) {
if (el.canBeStuck()) {
_setElementPosition(el);
}
});
// add styles to mark the edge of sticky elements opposite to that stuck to the window
oppositeEdge.mark(self);
if (self._initialPositionsSet === false) { self._initialPositionsSet = true; }
};
// Store all the dimensions for a sticky element to limit DOM queries
Sticky.prototype.setElementDimensions = function (el, callback) {
var self = this;
var $el = el.$fixedEl;
var onHeightSet = function () {
// if element is shim'ed, pass changes in dimension on to the shim
if (el._$shim) {
el.updateShim();
}
if (callback !== undefined) {
callback();
}
};
this.setElWidth(el);
this.setElHeight(el, onHeightSet);
};
// Reset element to original state in the page
Sticky.prototype.reset = function (el) {
if (el.isStopped()) {
this.unstop(el);
}
if (el.isStuck()) {
this.release(el);
}
};
// Recalculate stored dimensions for all sticky elements
Sticky.prototype.recalculate = function () {
var self = this;
var onSyncComplete = function () {
scrollAreas.syncEls(self._els);
self.setEvents();
if (_mode === 'dialog') {
dialog.fitToHeight(self);
if (dialog.hasResized) {
dialog.adjustForResize(self);
}
}
self.setElementPositions();
};
this.syncWithDOM(onSyncComplete);
};
// Public method to scroll so an element isn't covered by the sticky nav
Sticky.prototype.scrollToRevealElement = function (el) {
var $el = $(el);
var scrollAreaNode = $el.closest('.sticky-scroll-area').get(0);
var matches = $.grep(scrollAreas._scrollAreas, function (scrollArea) {
return scrollArea.node === scrollAreaNode;
});
if (matches.length) {
matches[0].scrollToRevealElement($el);
}
};
Sticky.prototype.setElWidth = function (el) {
var $el = el.$fixedEl;
var scrollArea = scrollAreas.getAreaByEl(el);
var width = $(scrollArea.node).width();
el.horizontalSpace = width;
// if stuck, element won't inherit width from parent so set explicitly
if (el._$shim) {
$el.width(width);
}
};
Sticky.prototype.setElHeight = function (el, callback) {
var self = this;
var $el = el.$fixedEl;
var $img = $el.find('img');
var onload = function () {
el.height = $el.outerHeight();
// if element has a shim, the shim's offset represents the element's in-page position
if (el._$shim) {
el.inPageEdgePosition = self.getInPageEdgePosition(el._$shim);
} else {
el.inPageEdgePosition = self.getInPageEdgePosition($el);
}
callback();
};
if ((!el.hasLoaded()) && ($img.length > 0)) {
var image = new window.Image();
image.onload = function () {
onload();
};
image.src = $img.attr('src');
} else {
onload();
}
};
Sticky.prototype.allElementsLoaded = function (totalEls) {
return this._els.length === totalEls;
};
Sticky.prototype.getElForNode = function (node) {
var matches = $.grep(this._els, function (el) { return el.$fixedEl.is(node); });
return !!matches.length ? matches[0] : false;
};
Sticky.prototype.add = function (el, setPositions, cb) {
var self = this;
var $el = $(el);
var onDimensionsSet;
var elObj = this.getElForNode(el);
var exists = !!elObj;
onDimensionsSet = function () {
elObj.hasLoaded(true);
// guard against adding elements already stored
if (!exists) {
self._els.push(elObj);
}
if (setPositions) {
self.setElementPositions();
}
if (cb !== undefined) {
cb();
}
};
if (!exists) {
elObj = new StickyElement($el, self);
scrollAreas.addEl(elObj, self.edge, self.CSS_SELECTOR);
}
self.setElementDimensions(elObj, onDimensionsSet);
};
Sticky.prototype.remove = function (el) {
if ($.inArray(el, this._els) !== -1) {
// reset DOM node to original state
this.reset(el);
// remove sticky element object
this._els = $.grep(this._els, function (_el) { return _el !== el; });
}
};
// gets all sticky elements in the DOM and removes any in this._els no longer in attached to it
Sticky.prototype.syncWithDOM = function (callback) {
var self = this;
var $els = $(self.CSS_SELECTOR);
var numOfEls = $els.length;
var onLoaded;
onLoaded = function () {
if (self._els.length === numOfEls) {
self.endOfScrollArea = self.getEndOfScrollArea();
if (callback !== undefined) {
callback();
}
}
};
// remove any els no longer in the DOM
if (this._els.length) {
$.each(this._els, function (i, el) {
if (!el.isInPage()) {
self.remove(el);
}
});
}
if (numOfEls) {
// reset flag marking page load
this._initialPositionsSet = false;
$els.each(function (i, el) {
// delay setting position until all stickys are loaded
self.add(el, false, onLoaded);
});
}
};
Sticky.prototype.init = function () {
this.recalculate();
};
Sticky.prototype.setEvents = function () {
this._scrollEvent = this.onScroll.bind(this);
this._resizeEvent = this.onResize.bind(this);
// flag when scrolling takes place and check (and re-position) sticky elements relative to
// window position
if (this._scrollTimeout === false) {
$(global).scroll(this._scrollEvent);
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 = window.setInterval(this.checkResize.bind(this), 50);
}
};
Sticky.prototype.clearEvents = function () {
if (this._scrollTimeout !== false) {
$(global).off('scroll', this._scrollEvent);
window.clearInterval(this._scrollTimeout);
this._scrollTimeout = false;
}
if (this._resizeTimeout !== false) {
$(global).off('resize', this._resizeEvent);
window.clearInterval(this._resizeTimeout);
this._resizeTimeout = false;
}
};
Sticky.prototype.viewportIsWideEnough = function (windowWidth) {
return windowWidth > 768;
};
Sticky.prototype.onScroll = function () {
this._hasScrolled = true;
};
Sticky.prototype.onResize = function () {
this._windowHasResized = true;
};
Sticky.prototype.checkScroll = function () {
var self = this;
if (self._hasScrolled === true) {
self._hasScrolled = false;
self.setElementPositions();
}
};
Sticky.prototype.checkResize = function () {
var self = this,
windowWidth = self.getWindowDimensions().width;
if (self._windowHasResized === true) {
self._windowHasResized = false;
$.each(self._els, function (i, el) {
if (!self.viewportIsWideEnough(windowWidth)) {
self.reset(el);
} else {
self.setElementDimensions(el);
}
});
if (self.viewportIsWideEnough(windowWidth)) {
if (_mode === 'dialog') {
dialog.fitToHeight(self);
if (dialog.hasResized) {
dialog.adjustForResize(self);
}
}
self.setElementPositions();
}
}
};
Sticky.prototype.release = function (el) {
if (el.isStuck()) {
var $el = el.$fixedEl;
el.removeStickyClasses(this);
$el.css('width', '');
// clear styles from any elements stuck while in a dialog mode
dialog.releaseEl(el, this);
el.removeShim();
el.release(this);
}
};
// Extension of sticky object to add behaviours specific to sticking to top of window
var stickAtTop = new Sticky('.js-stick-at-top-when-scrolling');
stickAtTop.edge = 'top';
// Store furthest point sticky elements are allowed
stickAtTop.getEndOfScrollArea = function () {
var footer = $('.js-footer:eq(0)');
if (footer.length === 0) {
return 0;
}
return footer.offset().top - this.STOP_PADDING;
};
// position of the bottom edge when in the page flow
stickAtTop.getInPageEdgePosition = function ($el) {
return $el.offset().top;
};
stickAtTop.getScrolledFrom = function (el) {
if (_mode === 'dialog') {
return dialog.getInPageEdgePosition(this);
} else {
return el.inPageEdgePosition;
}
};
stickAtTop.getScrollingTo = function (el) {
var height = el.height;
if (_mode === 'dialog') {
height = dialog.getHeight(this._els);
}
return this.endOfScrollArea - height;
};
stickAtTop.getStoppingPosition = function (el) {
var offset = 0;
if (_mode === 'dialog') {
offset = dialog.getOffsetFromEnd(el, this);
}
return (this.endOfScrollArea - offset) - el.height;
};
stickAtTop.windowNotPastScrolledFrom = function (windowPositions, scrolledFrom) {
return scrolledFrom > windowPositions.top;
};
stickAtTop.windowNotPastScrollingTo = function (windowPositions, scrollingTo) {
return windowPositions.top < scrollingTo;
};
stickAtTop.stick = function (el) {
if (!el.isStuck()) {
var $el = el.$fixedEl;
var offset = 0;
if (_mode === 'dialog') {
offset = dialog.getOffsetFromEdge(el, this);
}
el.addShim('before');
$el.css({
// element will be absolutely positioned so cannot rely on parent element for width
'width': $el.width() + 'px',
'top': offset + 'px'
});
el.stick(this);
}
};
stickAtTop.stop = function (el) {
if (!el.isStopped()) {
el.$fixedEl.css({
'position': 'absolute',
'top': this.getStoppingPosition(el)
});
el.stop();
}
};
stickAtTop.unstop = function (el) {
var offset = 0;
if (_mode === 'dialog') {
offset = dialog.getOffsetFromEdge(el, this);
}
el.$fixedEl.css({
'position': '',
'top': offset + 'px'
});
el.unstop();
};
// Extension of sticky object to add behaviours specific to sticking to bottom of window
var stickAtBottom = new Sticky('.js-stick-at-bottom-when-scrolling');
stickAtBottom.edge = 'bottom';
// Store furthest point sticky elements are allowed
stickAtBottom.getEndOfScrollArea = function () {
var header = $('.js-header:eq(0)');
if (header.length === 0) {
return 0;
}
return (header.offset().top + header.outerHeight()) + this.STOP_PADDING;
};
// position of the bottom edge when in the page flow
stickAtBottom.getInPageEdgePosition = function ($el) {
return $el.offset().top + $el.outerHeight();
};
stickAtBottom.getScrolledFrom = function (el) {
if (_mode === 'dialog') {
return dialog.getInPageEdgePosition(this);
} else {
return el.inPageEdgePosition;
}
};
stickAtBottom.getScrollingTo = function (el) {
var height = el.height;
if (_mode === 'dialog') {
height = dialog.getHeight(this._els);
}
return this.endOfScrollArea + height;
};
stickAtBottom.getStoppingPosition = function (el) {
var offset = 0;
if (_mode === 'dialog') {
offset = dialog.getOffsetFromEnd(el, this);
}
return this.endOfScrollArea + offset;
};
stickAtBottom.windowNotPastScrolledFrom = function (windowPositions, scrolledFrom) {
return scrolledFrom < windowPositions.bottom;
};
stickAtBottom.windowNotPastScrollingTo = function (windowPositions, scrollingTo) {
return windowPositions.bottom > scrollingTo;
};
stickAtBottom.stick = function (el) {
if (!el.isStuck()) {
var $el = el.$fixedEl;
var offset = 0;
if (_mode === 'dialog') {
offset = dialog.getOffsetFromEdge(el, this);
}
el.addShim('after');
$el.css({
// element will be absolutely positioned so cannot rely on parent element for width
'width': $el.width() + 'px',
'bottom': offset + 'px'
});
el.stick(this);
}
};
stickAtBottom.stop = function (el) {
if (!el.isStopped()) {
el.$fixedEl.css({
'position': 'absolute',
'top': this.getStoppingPosition(el),
'bottom': 'auto'
});
el.stop();
}
};
stickAtBottom.unstop = function (el) {
var offset = 0;
if (_mode === 'dialog') {
offset = dialog.getOffsetFromEdge(el, this);
}
el.$fixedEl.css({
'position': '',
'top': '',
'bottom': offset + 'px'
});
el.unstop();
};
NotifyModules.stickAtTopWhenScrolling = stickAtTop;
NotifyModules.stickAtBottomWhenScrolling = stickAtBottom;
window.NotifyModules = NotifyModules;
})(window);

View File

@@ -1,66 +1,98 @@
(function(window) {
"use strict";
// HTML escaping utility to prevent XSS
const escapeHtml = (unsafe) => {
if (!unsafe) return '';
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
window.NotifyModules['template-folder-form'] = function() {
this.start = function(templateFolderForm) {
this.$form = $(templateFolderForm);
this.form = templateFolderForm;
// remove the hidden unknown button - if you've got JS enabled then the action you want to do is implied by
// which field is visible.
this.$form.find('button[value=unknown]').remove();
const unknownButton = this.form.querySelector('button[value=unknown]');
if (unknownButton) {
unknownButton.remove();
}
this.$liveRegionCounter = this.$form.find('.selection-counter');
this.liveRegionCounter = this.form.querySelector('.selection-counter');
// Critical: Verify live region counter exists before proceeding
if (!this.liveRegionCounter) {
console.error('templateFolderForm: .selection-counter element not found');
return;
}
// Get single channel data from DOM (must happen after DOM is ready)
const addNewTemplateForm = document.querySelector('div[id=add_new_template_form]');
this.$singleNotificationChannel = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-channel") : null;
this.$singleChannelService = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-service") : null;
this.singleNotificationChannel = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-channel") : null;
this.singleChannelService = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-service") : null;
this.$liveRegionCounter.before(this.nothingSelectedButtons);
this.$liveRegionCounter.before(this.itemsSelectedButtons);
this.liveRegionCounter.insertAdjacentElement('beforebegin', this.nothingSelectedButtons);
this.liveRegionCounter.insertAdjacentElement('beforebegin', this.itemsSelectedButtons);
// all the diff states that we want to show or hide
// all the diff states that we want to show or hide - using Map for better performance
this.states = [
{
key: 'nothing-selected-buttons',
$el: this.$form.find('#nothing_selected'),
el: this.form.querySelector('#nothing_selected'),
cancellable: false
},
{
key: 'items-selected-buttons',
$el: this.$form.find('#items_selected'),
el: this.form.querySelector('#items_selected'),
cancellable: false
},
{
key: 'move-to-existing-folder',
$el: this.$form.find('#move_to_folder_radios'),
el: this.form.querySelector('#move_to_folder_radios'),
cancellable: true,
setFocus: () => $('#move_to_folder_radios').focus(),
setFocus: () => {
const el = document.getElementById('move_to_folder_radios');
if (el) el.focus();
},
action: 'move to folder',
description: 'Press move to confirm or cancel to close'
},
{
key: 'move-to-new-folder',
$el: this.$form.find('#move_to_new_folder_form'),
el: this.form.querySelector('#move_to_new_folder_form'),
cancellable: true,
setFocus: () => $('#move_to_new_folder_form').focus(),
setFocus: () => {
const el = document.getElementById('move_to_new_folder_form');
if (el) el.focus();
},
action: 'move to new folder',
description: 'Press add to new folder to confirm name or cancel to close'
},
{
key: 'add-new-folder',
$el: this.$form.find('#add_new_folder_form'),
el: this.form.querySelector('#add_new_folder_form'),
cancellable: true,
setFocus: () => $('#add_new_folder_form').focus(),
setFocus: () => {
const el = document.getElementById('add_new_folder_form');
if (el) el.focus();
},
action: 'new folder',
description: 'Press add new folder to confirm name or cancel to close'
},
{
key: 'add-new-template',
$el: this.$form.find('#add_new_template_form'),
el: this.form.querySelector('#add_new_template_form'),
cancellable: true,
setFocus: () => $('#add_new_template_form').focus(),
setFocus: () => {
const el = document.getElementById('add_new_template_form');
if (el) el.focus();
},
action: 'new template',
description: 'Press continue to confirm selection or cancel to close'
}
@@ -70,8 +102,12 @@
this.states.filter(state => state.cancellable).forEach((x) => this.addCancelButton(x));
this.states.filter(state => state.key === 'items-selected-buttons').forEach(x => this.addClearButton(x));
// make elements focusabled
this.states.filter(state => state.setFocus).forEach(x => x.$el.attr('tabindex', '0'));
// make elements focusable
this.states.filter(state => state.setFocus).forEach(x => {
if (x.el) {
x.el.setAttribute('tabindex', '0');
}
});
this.addDescriptionsToStates();
@@ -79,7 +115,7 @@
this.activateStickyElements();
// first off show the new template / new folder buttons
this._lastState = this.$form.data('prev-state');
this._lastState = this.form.dataset.prevState;
if (this._lastState === undefined) {
this.selectActionButtons();
} else {
@@ -87,43 +123,55 @@
this.render();
}
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.form.addEventListener('click', (event) => {
const button = event.target.closest('button.usa-button');
if (button) {
this.actionButtonClicked(event);
}
});
this.form.addEventListener('change', (event) => {
if (event.target.matches('input[type=checkbox]')) {
this.templateFolderCheckboxChanged();
} else if (event.target.matches('input[name="add_template_by_template_type"]')) {
this.templateTypeChanged();
}
});
};
this.addDescriptionsToStates = function () {
let id, description;
$.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
.prepend(description)
.attr('aria-describedby', id);
this.states.filter(state => 'description' in state).forEach(state => {
const id = `${escapeHtml(state.key)}__description`;
const description = `<p class="usa-sr-only" id="${id}">${escapeHtml(state.description)}</p>`;
if (state.el) {
state.el.insertAdjacentHTML('afterbegin', description);
state.el.setAttribute('aria-describedby', id);
}
});
};
this.activateStickyElements = function() {
var oldClass = 'js-will-stick-at-bottom-when-scrolling';
var newClass = 'js-stick-at-bottom-when-scrolling';
const oldClass = 'js-will-stick-at-bottom-when-scrolling';
const newClass = 'js-stick-at-bottom-when-scrolling';
this.states.forEach(state => {
state.$el
.find('.' + oldClass)
.removeClass(oldClass)
.addClass(newClass);
if (state.el) {
state.el.querySelectorAll('.' + oldClass).forEach(el => {
el.classList.remove(oldClass);
el.classList.add(newClass);
});
}
});
};
this.addCancelButton = function(state) {
let selector = `[value=${state.key}]`;
let $cancel = this.makeButton('Cancel', {
const selector = `[value=${state.key}]`;
const cancel = this.makeButton('Cancel', {
'onclick': () => {
// clear existing data
state.$el.find('input:radio').prop('checked', false);
state.$el.find('input:text').val('');
if (state.el) {
state.el.querySelectorAll('input[type="radio"]').forEach(input => input.checked = false);
state.el.querySelectorAll('input[type="text"]').forEach(input => input.value = '');
}
// go back to action buttons
this.selectActionButtons(selector);
@@ -132,16 +180,20 @@
'nonvisualText': state.action
});
state.$el.find('[type=submit]').after($cancel);
if (state.el) {
const submitButton = state.el.querySelector('[type=submit]');
if (submitButton) {
submitButton.insertAdjacentElement('afterend', cancel);
}
}
};
this.addClearButton = function(state) {
let selector = 'button[value=add-new-template]';
let $clear = this.makeButton('Clear', {
const selector = 'button[value=add-new-template]';
const clear = this.makeButton('Clear', {
'onclick': () => {
// uncheck all templates and folders
this.$form.find('input:checkbox').prop('checked', false);
this.form.querySelectorAll('input[type="checkbox"]').forEach(input => input.checked = false);
// go back to action buttons
this.selectActionButtons(selector);
@@ -149,29 +201,47 @@
'nonvisualText': "selection"
});
state.$el.find('.template-list-selected-counter').append($clear);
if (state.el) {
const counter = state.el.querySelector('.template-list-selected-counter');
if (counter) {
counter.appendChild(clear);
}
}
};
this.makeButton = (text, opts) => {
let $btn = $('<a href=""></a>')
.html(text)
.addClass('usa-link js-cancel')
// isn't set if cancelSelector is undefined
.data('target', opts.cancelSelector || undefined)
.attr('tabindex', '0')
.on('click keydown', event => {
// space, enter or no keyCode (must be mouse input)
if ([13, 32, undefined].indexOf(event.keyCode) > -1) {
event.preventDefault();
if (opts.hasOwnProperty('onclick')) { opts.onclick(); }
}
});
const btn = document.createElement('a');
btn.href = '';
btn.textContent = text;
btn.classList.add('usa-link', 'js-cancel');
if (opts.hasOwnProperty('nonvisualText')) {
$btn.append(`<span class="usa-sr-only"> ${opts.nonvisualText}</span>`);
// isn't set if cancelSelector is undefined
if (opts.cancelSelector) {
btn.dataset.target = opts.cancelSelector;
}
btn.setAttribute('tabindex', '0');
const handler = event => {
// space, enter or no keyCode (must be mouse input)
if ([13, 32, undefined].indexOf(event.keyCode) > -1) {
event.preventDefault();
if (opts.hasOwnProperty('onclick')) {
opts.onclick();
}
}
};
return $btn;
btn.addEventListener('click', handler);
btn.addEventListener('keydown', handler);
if (opts.hasOwnProperty('nonvisualText')) {
const span = document.createElement('span');
span.className = 'usa-sr-only';
span.textContent = ' ' + opts.nonvisualText;
btn.appendChild(span);
}
return btn;
};
this.selectActionButtons = function (targetSelector) {
@@ -181,7 +251,10 @@
this.currentState = 'nothing-selected-buttons';
this.templateFolderCheckboxChanged();
if (targetSelector) {
$(targetSelector).focus();
const target = document.querySelector(targetSelector);
if (target) {
target.focus();
}
}
};
@@ -194,14 +267,16 @@
};
this.actionButtonClicked = function(event) {
this.currentState = $(event.currentTarget).val();
const button = event.target.closest('button.usa-button') || event.target;
this.currentState = button.value;
if (event.currentTarget.value === 'add-new-template' && this.$singleNotificationChannel) {
if (this.currentState === 'add-new-template' && this.singleNotificationChannel) {
event.preventDefault();
window.location = "/services/" + this.$singleChannelService + "/templates/add-" + this.$singleNotificationChannel;
window.location = "/services/" + encodeURIComponent(this.singleChannelService) + "/templates/add-" + encodeURIComponent(this.singleNotificationChannel);
} 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();
const selectedInput = this.form.querySelector('input[name="add_template_by_template_type"]:checked');
const selectedTemplateType = selectedInput ? selectedInput.value : null;
if (selectedTemplateType) {
// Template type is selected, let the form submit normally
@@ -209,7 +284,7 @@
} else {
// No template type selected, show the selection UI
event.preventDefault();
this.$form.find('input[type=checkbox]').prop('checked', false);
this.form.querySelectorAll('input[type=checkbox]').forEach(input => input.checked = false);
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
if (this.stateChanged()) {
@@ -254,15 +329,19 @@
return results.join(', ') + ' selected';
},
'update': numSelected => {
let message = (numSelected.total > 0) ? this.selectionStatus.selected(numSelected) : this.selectionStatus.default;
const message = (numSelected.total > 0) ? this.selectionStatus.selected(numSelected) : this.selectionStatus.default;
$('.template-list-selected-counter__count').html(message);
this.$liveRegionCounter.html(message);
const counters = document.querySelectorAll('.template-list-selected-counter__count');
counters.forEach(counter => counter.textContent = message);
if (this.liveRegionCounter) {
this.liveRegionCounter.textContent = message;
}
}
};
this.templateFolderCheckboxChanged = function() {
let numSelected = this.countSelectedCheckboxes();
const numSelected = this.countSelectedCheckboxes();
if (this.currentState === 'nothing-selected-buttons' && numSelected.total !== 0) {
// user has just selected first item
@@ -278,8 +357,11 @@
this.selectionStatus.update(numSelected);
$('.template-list-selected-counter').toggle(this.hasCheckboxes());
const counters = document.querySelectorAll('.template-list-selected-counter');
const shouldShow = this.hasCheckboxes();
counters.forEach(counter => {
counter.style.display = shouldShow ? '' : 'none';
});
};
this.templateTypeChanged = function() {
@@ -287,24 +369,31 @@
};
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"]');
const selectedInput = this.form.querySelector('input[name="add_template_by_template_type"]:checked');
const selectedTemplateType = selectedInput ? selectedInput.value : null;
const continueButton = this.form.querySelector('#add_new_template_form button[value="add-new-template"]');
if (selectedTemplateType) {
continueButton.prop('disabled', false);
} else {
continueButton.prop('disabled', true);
if (continueButton) {
continueButton.disabled = !selectedTemplateType;
}
};
this.hasCheckboxes = function() {
return !!this.$form.find('input:checkbox').length;
return this.form.querySelectorAll('input[type="checkbox"]').length > 0;
};
this.countSelectedCheckboxes = function() {
const allSelected = this.$form.find('input:checkbox:checked');
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 allSelected = Array.from(this.form.querySelectorAll('input[type="checkbox"]:checked'));
// Check for sibling elements to determine if checkbox is for template or folder
// This matches the original jQuery logic: $(el).siblings('.template-list-template')
const templates = allSelected.filter(el => {
if (!el.parentElement) return false;
return el.parentElement.querySelector('.template-list-template') !== null;
}).length;
const folders = allSelected.filter(el => {
if (!el.parentElement) return false;
return el.parentElement.querySelector('.template-list-folder') !== null;
}).length;
const results = {
'templates': templates,
'folders': folders,
@@ -314,48 +403,79 @@
};
this.render = function() {
let currentStateObj = this.states.filter(state => { return (state.key === this.currentState); })[0];
const currentStateObj = this.states.find(state => state.key === this.currentState);
let scrollTop;
// detach everything, unless they are the currentState
this.states.forEach(
state => (state.key === this.currentState ? this.$liveRegionCounter.before(state.$el) : state.$el.detach())
);
this.states.forEach(state => {
if (state.key === this.currentState) {
if (state.el) {
this.liveRegionCounter.insertAdjacentElement('beforebegin', state.el);
}
} else {
if (state.el && state.el.parentElement) {
state.el.remove();
}
}
});
if (this.currentState === 'add-new-template') {
this.$form.find('.template-list-item').addClass('js-hidden');
$('.live-search').addClass('js-hidden');
$('#breadcrumb-template-folders').addClass('js-hidden');
$('#template-list').addClass('js-hidden');
this.$form.find('input[type=checkbox]').prop('checked', false);
this.form.querySelectorAll('.template-list-item').forEach(el => el.classList.add('js-hidden'));
const liveSearch = document.querySelector('.live-search');
if (liveSearch) liveSearch.classList.add('js-hidden');
const breadcrumb = document.getElementById('breadcrumb-template-folders');
if (breadcrumb) breadcrumb.classList.add('js-hidden');
const templateList = document.getElementById('template-list');
if (templateList) templateList.classList.add('js-hidden');
this.form.querySelectorAll('input[type=checkbox]').forEach(input => input.checked = false);
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
$('#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.');
const pageTitle = document.getElementById('page-title');
if (pageTitle) pageTitle.textContent = 'New Template';
const pageDescription = document.getElementById('page-description');
if (pageDescription) pageDescription.textContent = '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');
$('#breadcrumb-template-folders').removeClass('js-hidden');
$('#template-list').removeClass('js-hidden');
this.form.querySelectorAll('.template-list-item').forEach(el => el.classList.remove('js-hidden'));
const liveSearch = document.querySelector('.live-search');
if (liveSearch) liveSearch.classList.remove('js-hidden');
const breadcrumb = document.getElementById('breadcrumb-template-folders');
if (breadcrumb) breadcrumb.classList.remove('js-hidden');
const templateList = document.getElementById('template-list');
if (templateList) templateList.classList.remove('js-hidden');
const pageTitle = document.getElementById('page-title');
if (pageTitle) pageTitle.textContent = 'Select or create a template';
const pageDescription = document.getElementById('page-description');
if (pageDescription) pageDescription.textContent = 'Every message starts with a template. To send, choose or create a template.';
$('#page-title').text('Select or create a template');
$('#page-description').text('Every message starts with a template. To send, choose or create a template.');
document.title = 'Select or create a template';
}
if (currentStateObj && 'setFocus' in currentStateObj) {
scrollTop = $(window).scrollTop();
scrollTop = window.scrollY;
currentStateObj.setFocus();
$(window).scrollTop(scrollTop);
window.scrollTo(window.scrollX, scrollTop);
}
};
this.nothingSelectedButtons = $(`
<div id="nothing_selected">
const createNothingSelectedButtons = () => {
const div = document.createElement('div');
div.id = 'nothing_selected';
div.innerHTML = `
<div class="js-stick-at-bottom-when-scrolling">
<div class="usa-button-group">
<button class="usa-button" value="add-new-template" aria-expanded="false" role="button">
@@ -371,11 +491,14 @@
</span>
</div>
</div>
</div>
`).get(0);
`;
return div;
};
this.itemsSelectedButtons = $(`
<div id="items_selected">
const createItemsSelectedButtons = () => {
const div = document.createElement('div');
div.id = 'items_selected';
div.innerHTML = `
<div class="js-stick-at-bottom-when-scrolling">
<div class="usa-button-group">
<button class="usa-button" value="move-to-existing-folder" aria-expanded="false" role="button">
@@ -387,12 +510,16 @@
</div>
<div class="template-list-selected-counter" aria-hidden="true">
<span class="template-list-selected-counter__count text-base" aria-hidden="true">
${this.selectionStatus.selected(1)}
${this.selectionStatus.selected({ templates: 1, folders: 0, total: 1 })}
</span>
</div>
</div>
</div>
`).get(0);
`;
return div;
};
this.nothingSelectedButtons = createNothingSelectedButtons();
this.itemsSelectedButtons = createItemsSelectedButtons();
};
})(window);

View File

@@ -3,9 +3,9 @@
window.NotifyModules['update-status'] = function() {
const getRenderer = $component => response => $component.html(
response.html
);
const getRenderer = component => response => {
component.innerHTML = response.html;
};
const throttle = (func, limit) => {
@@ -41,39 +41,38 @@
let id = 'update-status';
this.$component = $(component);
this.$textbox = $('#' + this.$component.data('target'));
this.component = component;
this.textbox = document.getElementById(this.component.dataset.target);
this.$component
.attr('id', id);
this.component.setAttribute('id', id);
this.$textbox
.attr(
'aria-describedby',
(
this.$textbox.attr('aria-describedby') || ''
) + (
this.$textbox.attr('aria-describedby') ? ' ' : ''
) + id
)
.on('input', throttle(this.update, 150))
.trigger('input');
const currentAriaDescribedBy = this.textbox.getAttribute('aria-describedby') || '';
const newAriaDescribedBy = currentAriaDescribedBy + (currentAriaDescribedBy ? ' ' : '') + id;
this.textbox.setAttribute('aria-describedby', newAriaDescribedBy);
this.textbox.addEventListener('input', throttle(this.update, 150));
this.textbox.dispatchEvent(new Event('input'));
};
this.update = () => {
$.ajax(
this.$component.data('updates-url'),
{
'method': 'post',
'data': this.$textbox.parents('form').serialize()
}
).done(
getRenderer(this.$component)
).fail(
() => {}
);
const form = this.textbox.closest('form');
const formData = new FormData(form);
fetch(this.component.dataset.updatesUrl, {
method: 'POST',
body: formData,
credentials: 'same-origin'
})
.then(response => {
if (!response.ok) {
throw new Error('HTTP error');
}
return response.json();
})
.then(getRenderer(this.component))
.catch(() => {});
};

View File

@@ -1,8 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
if (window.uswds && typeof window.uswds.init === 'function') {
console.log("Calling USWDS init");
window.uswds.init();
} else {
console.error("USWDS not found or init is not a function");
}
});

View File

@@ -1,24 +0,0 @@
/* eslint-disable no-var */
(function uswdsInit() {
"use strict";
var loadingClass = "usa-js-loading";
var fallback;
document.documentElement.classList.add(loadingClass);
function revertClass() {
document.documentElement.classList.remove(loadingClass);
}
fallback = setTimeout(revertClass, 8000);
function verifyLoaded() {
if (window.uswdsPresent) {
clearTimeout(fallback);
revertClass();
window.removeEventListener("load", verifyLoaded, true);
}
}
window.addEventListener("load", verifyLoaded, true);
})();

View File

@@ -1,2 +0,0 @@
!function(){"use strict";var n,e="usa-js-loading";function t(){document.documentElement.classList.remove(e)}document.documentElement.classList.add(e),n=setTimeout(t,8e3),window.addEventListener("load",function e(){window.uswdsPresent&&(clearTimeout(n),t(),window.removeEventListener("load",e,!0))},!0)}();
//# sourceMappingURL=uswds-init.min.js.map

View File

@@ -14,8 +14,8 @@ Learn how to [personalize messages](/using-notify/how-to) to increase response.
Learn about message _parts_ and [how limits are calculated](/using-notify/pricing).
5. ## Start sending messages
To remove the restrictions of Trial Mode and begin sending messages to clients complete the <a class="usa-link usa-link--external" href="https://docs.google.com/forms/d/1fnaBtxuGf3q-OdGVyt2LqBKvp9_P21kmKJa0yIK8rWM/edit">Go-Live Form</a>.
Well respond within one business day.
To remove the restrictions of Trial Mode and begin sending messages to people complete the <a class="usa-link usa-link--external" href="https://docs.google.com/forms/d/1fnaBtxuGf3q-OdGVyt2LqBKvp9_P21kmKJa0yIK8rWM/edit">Go-Live Form</a>.
We'll respond within one business day.
### Questions?
[Contact the Notify team](/support)

View File

@@ -80,14 +80,14 @@
document submission, and maintenance reminders.
</p>
<h3>Appointment reminders</h3>
<p class="text-light">Benchmark: Clients were <span class="text-bold">79%</span> more
<p class="text-light">Benchmark: People were <span class="text-bold">79%</span> more
likely to keep their appointment after receiving a text reminder.</p>
<ul>
<li>
<p>You will likely see more completed appointments.</p>
</li>
</ul>
<p class="text-light">Benchmark: Clients were <span class="text-bold">55%</span> more
<p class="text-light">Benchmark: People were <span class="text-bold">55%</span> more
likely to complete an interview after receiving an interview reminder</p>
<ul>
<li>
@@ -97,7 +97,7 @@
<h3>Document submission</h3>
<p class="text-light">
Benchmark: Clients were <span class="text-bold">6%</span> more likely to complete document submission after
Benchmark: People were <span class="text-bold">6%</span> more likely to complete document submission after
receiving a customized list of required documents via text
</p>
<ul>

View File

@@ -189,7 +189,7 @@
"image_src": asset_url('images/project-management.svg'),
"card_heading": "Case management systems",
"alt_text": "Graphic representing project management",
"p_text": "When it makes sense, include information about texts being sent to specific clients on individual splash
"p_text": "When it makes sense, include information about texts being sent to specific people on individual splash
pages or within case management notes.",
},
] %}