first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

45
core/themes/claro/js/ajax.js Executable file
View File

@@ -0,0 +1,45 @@
/**
* @file
* Ajax theme overrides for Claro.
*/
((Drupal) => {
/**
* Theme override of the ajax progress indicator for full screen.
*
* @return {string}
* The HTML markup for the throbber.
*/
Drupal.theme.ajaxProgressIndicatorFullscreen = () =>
'<div class="ajax-progress ajax-progress--fullscreen"><div class="ajax-progress__throbber ajax-progress__throbber--fullscreen">&nbsp;</div></div>';
/**
* Theme override of the ajax progress indicator.
*
* @param {string} message
* The message shown on the UI.
* @return {string}
* The HTML markup for the throbber.
*/
Drupal.theme.ajaxProgressThrobber = (message) => {
// Build markup without adding extra white space since it affects rendering.
const messageMarkup =
typeof message === 'string'
? Drupal.theme('ajaxProgressMessage', message)
: '';
const throbber = '<div class="ajax-progress__throbber">&nbsp;</div>';
return `<div class="ajax-progress ajax-progress--throbber">${throbber}${messageMarkup}</div>`;
};
/**
* Theme override of the ajax progress message.
*
* @param {string} message
* The message shown on the UI.
* @return {string}
* The HTML markup for the throbber.
*/
Drupal.theme.ajaxProgressMessage = (message) =>
`<div class="ajax-progress__message">${message}</div>`;
})(Drupal);

View File

@@ -0,0 +1,41 @@
/**
* @file
* Claro's enhancement for autocomplete form element.
*/
// cspell:ignore is-autocompleting
(($, Drupal, once) => {
Drupal.behaviors.claroAutoCompete = {
attach(context) {
once('claroAutoComplete', 'input.form-autocomplete', context).forEach(
(value) => {
const $input = $(value);
const classRemove = ($autoCompleteElem) => {
$autoCompleteElem.removeClass('is-autocompleting');
$autoCompleteElem
.siblings('[data-drupal-selector="autocomplete-message"]')
.addClass('hidden');
};
$input.autocomplete({
search(event) {
const result = Drupal.autocomplete.options.search(event);
if (result) {
$(event.target).addClass('is-autocompleting');
$(event.target)
.siblings('[data-drupal-selector="autocomplete-message"]')
.removeClass('hidden');
}
return result;
},
response(event) {
classRemove($(event.target));
},
});
},
);
},
};
})(jQuery, Drupal, once);

View File

@@ -0,0 +1,15 @@
/**
* @file
* Theme override for checkbox.
*/
((Drupal) => {
/**
* Constructs a checkbox input element.
*
* @return {string}
* A string representing a DOM fragment.
*/
Drupal.theme.checkbox = () =>
'<input type="checkbox" class="form-checkbox form-boolean form-boolean--type-checkbox"/>';
})(Drupal);

View File

@@ -0,0 +1,8 @@
WHAT IS THIS DIRECTORY FOR?
--------------------------------
This directory is for JS files previously inherited from the Classy theme.
WHY WERE CLASSY JS FILES COPIED HERE?
-------------------------------------------
Classy was removed in Drupal 10. To prepare for Classy's removal, JS files that
would otherwise be inherited from Classy were copied here.

View File

@@ -0,0 +1,23 @@
/**
* @file
* Theme overrides for the Media Embed CKEditor plugin previously provided by
* the now-removed Classy theme.
*/
((Drupal) => {
/**
* Themes the error displayed when the media embed preview fails.
*
* @param {string} error
* The error message to display
*
* @return {string}
* A string representing a DOM fragment.
*
* @see media-embed-error.html.twig
*/
Drupal.theme.mediaEmbedPreviewError = () =>
`<div class="media-embed-error media-embed-error--preview-error">${Drupal.t(
'An error occurred while trying to preview the media. Save your work and reload this page.',
)}</div>`;
})(Drupal);

49
core/themes/claro/js/details.js Executable file
View File

@@ -0,0 +1,49 @@
/**
* @file
* Claro's polyfill enhancements for HTML5 details.
*/
(($, Drupal) => {
/**
* Workaround for Firefox.
*
* Firefox applies the focus state only for keyboard navigation.
* We have to manually trigger focus to make the behavior consistent across
* browsers.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.claroDetails = {
attach(context) {
// The second argument of once() needs to be an instance of Element, but
// document is an instance of Document, replace it with the html Element.
$(once('claroDetails', context === document ? 'html' : context)).on(
'click',
(event) => {
if (event.target.nodeName === 'SUMMARY') {
$(event.target).trigger('focus');
}
},
);
},
};
/**
* Theme override providing a wrapper for summarized details content.
*
* @return {string}
* The markup for the element that will contain the summarized content.
*/
Drupal.theme.detailsSummarizedContentWrapper = () =>
`<span class="claro-details__summary-summary"></span>`;
/**
* Theme override of summarized details content text.
*
* @param {string|null} [text]
* (optional) The summarized content displayed in the summary.
* @return {string}
* The formatted summarized content text.
*/
Drupal.theme.detailsSummarizedContentText = (text) => text || '';
})(jQuery, Drupal);

View File

@@ -0,0 +1,23 @@
/**
* @file
* Theme overrides for Claro.
*/
((Drupal) => {
/**
* Overrides the dropbutton toggle markup.
*
* We have to keep the 'dropbutton-toggle' CSS class because the dropbutton JS
* operates with that one.
*
* @param {object} options
* Options object.
* @param {string} [options.title]
* The button text.
*
* @return {string}
* A string representing a DOM fragment.
*/
Drupal.theme.dropbuttonToggle = (options) =>
`<li class="dropbutton-toggle"><button type="button" class="dropbutton__toggle"><span class="visually-hidden">${options.title}</span></button></li>`;
})(Drupal);

View File

@@ -0,0 +1,70 @@
/**
* @file
* Media Library overrides for Claro
*/
(($, Drupal, window) => {
/**
* Update the media library selection when loaded or media items are selected.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to select media items.
*/
Drupal.behaviors.MediaLibraryItemSelectionClaro = {
attach() {
// Move the selection count to the beginning of the button pane after it
// has been added to the Media Library dialog.
// @todo replace with theme function override in
// https://drupal.org/node/3134526
if (!once('media-library-selection-info-claro-event', 'html').length) {
return;
}
window.addEventListener('dialog:aftercreate', (e) => {
const $element = $(e.target);
// Since the dialog HTML is not part of the context, we can't use
// context here.
const moveCounter = ($selectedCount, $buttonPane) => {
const $moveSelectedCount = $selectedCount.detach();
$buttonPane.prepend($moveSelectedCount);
};
const $buttonPane = $element
.closest('.media-library-widget-modal')
.find('.ui-dialog-buttonpane');
if (!$buttonPane.length) {
return;
}
const $selectedCount = $buttonPane.find(
'.js-media-library-selected-count',
);
// If the `selected` counter is already present, it can be moved from
// the end of the button pane to the beginning.
if ($selectedCount.length) {
moveCounter($selectedCount, $buttonPane);
} else {
// If the `selected` counter is not yet present, create a mutation
// observer that checks for items added to the button pane. As soon
// as the counter is added, move it from the end of the button pane
// to the beginning.
const selectedCountObserver = new MutationObserver(() => {
const $selectedCountFind = $buttonPane.find(
'.js-media-library-selected-count',
);
if ($selectedCountFind.length) {
moveCounter($selectedCountFind, $buttonPane);
selectedCountObserver.disconnect();
}
});
selectedCountObserver.observe($buttonPane[0], {
attributes: false,
childList: true,
characterData: false,
subtree: true,
});
}
});
},
};
})(jQuery, Drupal, window);

View File

@@ -0,0 +1,53 @@
/**
* @file
* Message template overrides.
*/
((Drupal) => {
/**
* Overrides message theme function.
*
* @param {object} message
* The message object.
* @param {string} message.text
* The message text.
* @param {object} options
* The message context.
* @param {string} options.type
* The message type.
* @param {string} options.id
* ID of the message, for reference.
*
* @return {HTMLElement}
* A DOM Node.
*/
Drupal.theme.message = ({ text }, { type, id }) => {
const messagesTypes = Drupal.Message.getMessageTypeLabels();
const messageWrapper = document.createElement('div');
messageWrapper.setAttribute(
'class',
`messages messages--${type} messages-list__item`,
);
messageWrapper.setAttribute(
'role',
type === 'error' || type === 'warning' ? 'alert' : 'status',
);
messageWrapper.setAttribute('aria-labelledby', `${id}-title`);
messageWrapper.setAttribute('data-drupal-message-id', id);
messageWrapper.setAttribute('data-drupal-message-type', type);
messageWrapper.innerHTML = `
<div class="messages__header">
<h2 id="${id}-title" class="messages__title">
${messagesTypes[type]}
</h2>
</div>
<div class="messages__content">
${text}
</div>
`;
return messageWrapper;
};
})(Drupal);

View File

@@ -0,0 +1,29 @@
(() => {
function findActiveStep(steps) {
for (let i = 0; i < steps.length; i++) {
if (steps[i].className === 'is-active') {
return i + 1;
}
}
// The final "Finished" step is never "active".
if (steps[steps.length - 1].className === 'done') {
return steps.length;
}
return 0;
}
function installStepsSetup() {
const steps = document.querySelectorAll('.task-list li');
if (steps.length) {
const header = document.querySelector('header[role="banner"]');
const stepIndicator = document.createElement('div');
stepIndicator.className = 'step-indicator';
stepIndicator.innerHTML = `${findActiveStep(steps)}/${steps.length}`;
header.appendChild(stepIndicator);
}
}
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', installStepsSetup);
}
})();

View File

@@ -0,0 +1,90 @@
/**
* @file
* Responsive navigation tabs.
*
* This also supports collapsible navigable is the 'is-collapsible' class is
* added to the main element, and a target element is included.
*/
(($, Drupal) => {
function init(tab) {
const $tab = $(tab);
const $target = $tab.find('[data-drupal-nav-tabs-target]');
const $active = $target.find('.js-active-tab');
const openMenu = () => {
$target.toggleClass('is-open');
$target.find('button').attr('aria-expanded', $target.hasClass('is-open'));
};
const toggleOrder = (reset) => {
const current = $active.index();
const original = $active.data('original-order');
// Do not change order if already first or if already reset.
if (original === 0 || reset === (current === original)) {
return;
}
const siblings = {
first: '[data-original-order="0"]',
previous: `[data-original-order="${original - 1}"]`,
};
const $first = $target.find(siblings.first);
const $previous = $target.find(siblings.previous);
if (reset && current !== original) {
$active.insertAfter($previous);
} else if (!reset && current === original) {
$active.insertBefore($first);
}
};
const toggleCollapsed = ({ matches }) => {
if (matches) {
if ($tab.hasClass('is-horizontal') && !$tab.attr('data-width')) {
let width = 0;
$target.find('.js-tabs-link').each((index, value) => {
width += $(value).outerWidth();
});
$tab.attr('data-width', width);
}
// Collapse the tabs if the combined width of the tabs is greater than
// the width of the parent container.
const isHorizontal = $tab.attr('data-width') <= $tab.outerWidth();
$tab.toggleClass('is-horizontal', isHorizontal);
$tab.find('button').attr('aria-expanded', null);
toggleOrder(isHorizontal);
} else {
toggleOrder(false);
$tab.find('button').attr('aria-expanded', 'false');
}
};
$tab.addClass('position-container is-horizontal-enabled');
$target.find('.js-tab').each((index, element) => {
const $item = $(element);
$item.attr('data-original-order', $item.index());
});
$tab.on('click.tabs', '[data-drupal-nav-tabs-trigger]', openMenu);
const mql = window.matchMedia('(min-width: 48em)');
mql.addEventListener('change', toggleCollapsed);
toggleCollapsed(mql);
}
/**
* Initialize the tabs JS.
*/
Drupal.behaviors.navTabs = {
attach(context) {
once(
'nav-tabs',
'[data-drupal-nav-tabs].is-collapsible',
context,
).forEach(init);
},
};
})(jQuery, Drupal);

251
core/themes/claro/js/tabledrag.js Executable file
View File

@@ -0,0 +1,251 @@
/**
* @file
* tabledrag.js overrides and functionality extensions.
*/
(($, Drupal) => {
/**
* Extends core's Tabledrag functionality.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.claroTableDrag = {
attach(context, settings) {
/**
* Refactors the table row markup to improve item label text wrapping.
*
* This addresses an issue specific to item labels that are long enough
* to be wrapped to a new line. Without this fix, a new line may start
* at the beginning of the table row, instead of the expected behavior of
* starting at the x axis of the first line.
*
* Addressing this issue requires changing the structure of a tabledrag
* cell's first row.
* @example
* <!-- Default tabledrag structure, which has the wrapping problem. -->
* <tr class="draggable">
* <td>
* <!--
* Indentations are next to each other because they are styled as
* `float: left;`
* -->
* <div class="indentation"></div>
* <div class="indentation"></div>
* <a class="tabledrag-handle"></a>
* <!-- If the text in this link wraps enough times that the element
* is taller than the floated elements preceding it, some lines
* will wrap to the beginning of the row instead of aligning with
* the beginning of the link text.
* -->
* <a class="menu-item__link">A longer label that may require wrapping</a>
* </td>
* <!-- etc. -->
* </tr>
* @example
* <!-- Claro tabledrag structure, this fixes the wrapping problem. -->
* <tr class="draggable">
* <td class="tabledrag-cell">
* <div class="tabledrag-cell-content">
* <!-- Indentations are next to each other because
* .table-drag-cell-content is styled as `display: table-row;`
* and .table-drag-cell-content > * is styled as
* `display: table-cell;`
* -->
* <div class="indentation"></div>
* <div class="indentation"></div>
* <a class="tabledrag-handle"></a>
* <div class="tabledrag-cell-content__item">
* <!-- Placing the link inside a div styled as
* `display: table-cell;` allows the text to wrap within
* the boundaries of the "cell".
* -->
* <a class="menu-item__link">A longer label that may require wrapping</a>
* </div>
* </div>
* </td>
* <!-- additional <td> -->
* </tr>
*
* @param {number} index
* The index in the loop, as provided by `jQuery.each`.
* @param {HTMLElement} row
* A draggable table row.
*
* @todo this may be removable as part of https://drupal.org/node/3083044
*/
const createItemWrapBoundaries = (row) => {
const $row = $(row);
const $firstCell = $row
.find('td:first-of-type')
.eq(0)
.wrapInner(Drupal.theme('tableDragCellContentWrapper'))
.wrapInner(
$(Drupal.theme('tableDragCellItemsWrapper')).addClass(
'js-tabledrag-cell-content',
),
);
const $targetElem = $firstCell.find('.js-tabledrag-cell-content');
// Move handle into the '.js-tabledrag-cell-content' target.
$targetElem
.eq(0)
.find(
'> .tabledrag-cell-content__item > .js-tabledrag-handle, > .tabledrag-cell-content__item > .js-indentation',
)
.prependTo($targetElem);
};
// Find each row in a draggable table and process it with
// createItemWrapBoundaries().
Object.keys(settings.tableDrag || {}).forEach((base) => {
once(
'claroTabledrag',
$(context)
.find(`#${base}`)
.find('> tr.draggable, > tbody > tr.draggable'),
).forEach(createItemWrapBoundaries);
});
},
};
$.extend(Drupal.tableDrag.prototype.row.prototype, {
/**
* Add an asterisk or other marker to the changed row.
*
* @todo this may be removable as part of https://drupal.org/node/3084910
*/
markChanged() {
const marker = $(Drupal.theme('tableDragChangedMarker')).addClass(
'js-tabledrag-changed-marker',
);
const cell = $(this.element).find('td:first-of-type');
if (cell.find('.js-tabledrag-changed-marker').length === 0) {
cell.find('.js-tabledrag-handle').after(marker);
}
Drupal.tableDrag[this.table.id].changedRowIds.add(this.element.id);
},
/**
* Moves added indents into Claro's wrapper element.
*
* For indents to work properly, they must be inside the wrapper
* created by createItemWrapBoundaries(). When an indent is added via
* dragging, core's tabledrag functionality does not add it inside the
* wrapper. This function fires immediately after an indent is added, which
* moves the indent into that wrapper.
*
* @see Drupal.tableDrag.prototype.row.prototype.indent
*
* @todo this may be removable as part of https://drupal.org/node/3083044
*/
onIndent() {
$(this.table)
.find('.tabledrag-cell > .js-indentation')
.each((index, indentToMove) => {
const $indentToMove = $(indentToMove);
const $cellContent = $indentToMove.siblings(
'.tabledrag-cell-content',
);
$indentToMove.prependTo($cellContent);
});
},
});
$.extend(
Drupal.theme,
/** @lends Drupal.theme */ {
/**
* Constructs the table drag changed marker.
*
* @return {string}
* Markup for the indentation.
*/
tableDragIndentation() {
return '<div class="js-indentation indentation"><svg xmlns="http://www.w3.org/2000/svg" class="tree" width="25" height="25" viewBox="0 0 25 25"><path class="tree__item tree__item-child-ltr tree__item-child-last-ltr tree__item-horizontal tree__item-horizontal-right" d="M12,12.5 H25" stroke="#888"/><path class="tree__item tree__item-child-rtl tree__item-child-last-rtl tree__item-horizontal tree__horizontal-left" d="M0,12.5 H13" stroke="#888"/><path class="tree__item tree__item-child-ltr tree__item-child-rtl tree__item-child-last-ltr tree__item-child-last-rtl tree__vertical tree__vertical-top" d="M12.5,12 v-99" stroke="#888"/><path class="tree__item tree__item-child-ltr tree__item-child-rtl tree__vertical tree__vertical-bottom" d="M12.5,12 v99" stroke="#888"/></svg></div>';
},
/**
* Constructs the table drag changed warning.
*
* @return {string}
* Markup for the warning.
*/
tableDragChangedWarning() {
return `<div class="tabledrag-changed-warning messages messages--warning" role="alert">${Drupal.theme(
'tableDragChangedMarker',
)} ${Drupal.t('You have unsaved changes.')}</div>`;
},
/**
* Constructs the table drag handle.
*
* @return {string}
* A string representing a DOM fragment.
*/
tableDragHandle: (dragOrientation = 'drag') => {
const title =
dragOrientation === 'drag-y'
? Drupal.t('Change order')
: Drupal.t('Move in any direction');
return `<a href="#" title="${title}" class="tabledrag-handle js-tabledrag-handle"></a>`;
},
/**
* The button for toggling table row weight visibility.
*
* @return {string}
* HTML markup for the weight toggle button and its container.
*/
tableDragToggle: () =>
`<div class="tabledrag-toggle-weight-wrapper" data-drupal-selector="tabledrag-toggle-weight-wrapper">
<button type="button" class="link action-link tabledrag-toggle-weight" data-drupal-selector="tabledrag-toggle-weight"></button>
</div>`,
/**
* Constructs contents of the toggle weight button.
*
* @param {boolean} show
* If the table weights are currently displayed.
*
* @return {string}
* HTML markup for the weight toggle button content.
*/
toggleButtonContent: (show) => {
const classes = [
'action-link',
'action-link--extrasmall',
'tabledrag-toggle-weight',
];
let text = '';
if (show) {
classes.push('action-link--icon-hide');
text = Drupal.t('Hide row weights');
} else {
classes.push('action-link--icon-show');
text = Drupal.t('Show row weights');
}
return `<span class="${classes.join(' ')}">${text}</a>`;
},
/**
* Constructs the wrapper for the initial content of the drag cell.
*
* @return {string}
* A string representing a DOM fragment.
*/
tableDragCellContentWrapper() {
return '<div class="tabledrag-cell-content__item"></div>';
},
/**
* Constructs the wrapper for the whole table drag cell.
*
* @return {string}
* A string representing a DOM fragment.
*/
tableDragCellItemsWrapper() {
return '<div class="tabledrag-cell-content"></div>';
},
},
);
})(jQuery, Drupal);

View File

@@ -0,0 +1,379 @@
/**
* @file
* Extends table select functionality for Claro.
*/
(($, Drupal, { tabbable }) => {
Drupal.ClaroBulkActions = class {
constructor(bulkActions) {
this.bulkActions = bulkActions;
this.form = this.bulkActions.closest('form');
this.form.querySelectorAll('tr').forEach((element) => {
element.classList.add('views-form__bulk-operations-row');
});
this.checkboxes = this.form.querySelectorAll(
'[class$="bulk-form"]:not(.select-all) input[type="checkbox"]',
);
this.selectAll = this.form.querySelectorAll(
'.select-all > [type="checkbox"]',
);
this.$tabbable = $(tabbable(this.form));
this.bulkActionsSticky = false;
this.scrollingTimeout = '';
this.ignoreScrollEvent = false;
$(this.checkboxes).on('change', (event) =>
this.rowCheckboxHandler(event),
);
$(this.selectAll).on('change', (event) => this.selectAllHandler(event));
this.$tabbable.on('focus', (event) => this.focusHandler(event));
this.$tabbable.on('blur', (event) => this.blurHandler(event));
// The will contain the CSS that hides the spacer during scroll
// and resize.
this.spacerCss = document.createElement('style');
document.body.appendChild(this.spacerCss);
const scrollResizeHandler = Drupal.debounce(() => {
this.scrollResizeHandler();
}, 10);
$(window).on('scroll', () => scrollResizeHandler());
$(window).on('resize', () => scrollResizeHandler());
// Execute checkbox handler after the load event. This ensures that the
// actions form is sticky if any checkboxes are already checked on page
// load. One of the situations where it is possible to have pre-checked
// checkboxes on load is when the page is requested via the back button.
// window.addEventListener('load', () => this.rowCheckboxHandler({}));
$(window).on('load', () => this.rowCheckboxHandler({}));
}
/**
* Ensures that focusable elements hidden under a sticky remain focusable.
*
* @param {Object} event
* A jQuery Event object.
*/
/* eslint-disable-next-line class-methods-use-this */
blurHandler(event) {
// This event handler should only proceed if the event came from direct
// interaction with the form element. If this fires on events triggered
// via JavaScript there may be undesirable side effects.
if (!event.hasOwnProperty('isTrigger')) {
const row = event.target.closest('tr');
const nextSibling = row ? row.nextElementSibling : null;
// Any row in this table potentially has a spacer div preceding it. The
// spacer is added to prevent focusable elements from appearing
// underneath the sticky Views Bulk Actions form. Any element underneath
// this spacer is beneath the viewport. If an element beneath
// the viewport receives focus and the previously focused element was
// above the spacer, some browsers have difficulty determining how much
// scrolling is necessary to bring the newly focused element into view.
// To prevent this potential miscalculation, the spacer is momentarily
// removed when blur occurs on rows preceding it. The spacer is
// reintroduced immediately after the next item receives focus.
if (
nextSibling &&
nextSibling.getAttribute('data-drupal-table-row-spacer')
) {
nextSibling.parentNode.removeChild(nextSibling);
}
}
}
/**
* If a partially covered element receives focus, scroll it into full view.
*
* @param {Object} event
* A jQuery Event object.
*/
focusHandler(event) {
// Do not scroll down when element inside bulk actions is focused.
if (event.currentTarget.closest('[data-drupal-views-bulk-actions]')) {
return;
}
const stickyRect = this.bulkActions.getBoundingClientRect();
const stickyStart = stickyRect.y;
const elementRect = event.target.getBoundingClientRect();
const elementStart = elementRect.y;
const elementEnd = elementStart + elementRect.height;
if (elementEnd > stickyStart) {
window.scrollBy(0, elementEnd - stickyStart);
}
this.underStickyHandler();
}
/**
* Temporarily hides the spacer before calling underStickyHandler().
*
* The spacer is added to prevent the "show numbers" functionality of speech
* navigation from labeling inputs under the stickied bulk actions form. It
* does this by pushing these elements further down the page so they are out
* of the viewport entirely. The presence of this spacer should be invisible
* to users. Because this invisibility is partially achieved via
* calculations based on scroll position and viewport size, the spacer is
* hidden during these events, and reintroduced 500 milliseconds after all
* scroll and resize events have completed.
*/
scrollResizeHandler() {
// Add CSS rule that hides the spacer. CSS is used instead of removing
// the spacer from the DOM as the change occurs faster.
this.spacerCss.innerHTML =
'[data-drupal-table-row-spacer] { display: none; }';
if (!this.ignoreScrollEvent) {
// Remove the timeout that un-hides the spacer. If this function is
// called, then scrolling is still happening and spacers should stay
// hidden.
clearTimeout(this.scrollingTimeout);
// Shortly after scrolling tops, the spacer is re-added.
this.scrollingTimeout = setTimeout(() => {
this.spacerCss.innerHTML = '';
this.underStickyHandler();
}, 500);
}
}
/**
* Moves tabbable elements that are underneath the bulk actions form.
*
* Focusable elements inside a table row should not be positioned underneath
* a sticky Views Bulk Action form. If this isn't prevented, it can be
* confusing for speech navigation users when the "show numbers" feature
* is enabled. Numbers will be provided for the elements within the Bulk
* Actions form and the table row elements directly underneath, and it can
* be difficult to discern which number corresponds to which element. To
* prevent this confusion, a spacer div is added before the table row, and
* this spacer pushes the row further down so the focusable elements are out
* of viewport.
*/
underStickyHandler() {
document
.querySelectorAll('[data-drupal-table-row-spacer]')
.forEach((element) => {
element.parentNode.removeChild(element);
});
if (this.bulkActionsSticky) {
// Will be set to true as soon as the forEach() hits a row that is
// completely under the sticky header, indicating that no further
// processing is needed. Using a For...Of loop to accomplish this
// is preferable, but not supported by IE11.
let pastStickyHeader = false;
const stickyRect = this.bulkActions.getBoundingClientRect();
const stickyStart = stickyRect.y;
const stickyEnd = stickyStart + stickyRect.height;
// Loop through each table row. If a row has focusable elements under
// the sticky Views Bulk Actions form, add a spacer that pushes the row
// down the page and outside of the viewport.
this.form.querySelectorAll('tbody tr').forEach((row) => {
if (!pastStickyHeader) {
const rowRect = row.getBoundingClientRect();
const rowStart = rowRect.y;
const rowEnd = rowStart + rowRect.height;
if (rowStart > stickyEnd) {
pastStickyHeader = true;
} else if (rowEnd > stickyStart) {
// Get padding amount for the row's cells, which are used to
// determine where a row can be pushed out of the viewport
// without any visible difference.
const cellTopPadding = Array.from(
row.querySelectorAll('td.views-field'),
).map((element) =>
document.defaultView
.getComputedStyle(element, '')
.getPropertyValue('padding-top')
.replace('px', ''),
);
const minimumTopPadding = Math.min.apply(null, cellTopPadding);
// If all parts of the table row that could be displaying content
// are under the sticky.
if (rowStart + minimumTopPadding >= stickyStart) {
// If the row scrolled underneath the sticky has the element
// with focus, the addition of a spacer can potentially create
// an additional scroll event that can lead to unwanted results.
// The variables below are used to identify this so a flag can
// be set to bypass scroll handler actions in just those
// instances.
const oldScrollTop =
window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft =
window.pageXOffset || document.documentElement.scrollLeft;
const rowContainsActiveElement = row.contains(
document.activeElement,
);
// If the row contains the active element, set the flag that
// bypasses the actions of scrollResizeHandler() as a call to
// window.scrollTo() may be needed.
if (rowContainsActiveElement) {
this.ignoreScrollEvent = true;
}
// a spacer to push it out of the viewport. Because the elements
// are fully underneath the sticky, the added spacer should not
// result in any visible difference.
const spacer = document.createElement('div');
spacer.style.height = `${stickyRect.height}px`;
spacer.setAttribute('data-drupal-table-row-spacer', true);
row.parentNode.insertBefore(spacer, row);
// Will be used to determine if a scroll position change
// occurred due to adding the spacer.
const newScrollTop =
window.pageYOffset || document.documentElement.scrollTop;
// If the browser pushed the row back into the viewport after
// the spacer was added, return the scroll position to the
// intended location.
const windowBottom =
window.innerHeight || document.documentElement.clientHeight;
if (
rowContainsActiveElement &&
oldScrollTop !== newScrollTop &&
rowStart < windowBottom
) {
window.scrollTo(scrollLeft, oldScrollTop);
}
// Set this flag back to its default value of false.
this.ignoreScrollEvent = false;
}
}
}
});
}
}
/**
* Triggered when the `select all` button is clicked.
*
* @param {Object} event
* A jQuery Event object.
*/
selectAllHandler(event) {
// This event handler should only proceed if the event came from direct
// interaction with the form element. If this fires on events triggered
// via JavaScript there may be undesirable side effects.
if (!event.hasOwnProperty('isTrigger')) {
const itemsCheckedCount = event.target.checked
? this.checkboxes.length
: 0;
this.updateStatus(itemsCheckedCount);
this.underStickyHandler();
}
}
/**
* Triggered when a row is checked or unchecked.
*
* @param {Object} event
* A jQuery Event object.
*/
rowCheckboxHandler(event) {
// This event handler should only proceed if the event came from direct
// interaction with the form element. If this fires on events triggered
// via JavaScript there may be undesirable side effects.
if (!event.hasOwnProperty('isTrigger')) {
this.updateStatus(
Array.prototype.slice
.call(this.checkboxes)
.filter((checkbox) => checkbox.checked).length,
);
}
}
/**
* Update the bulk actions label and announcements.
*
* @param {number} count
* The number of checkboxes checked.
*/
updateStatus(count) {
// A status message that will be displayed in the bulk actions form and
// announced by the screen reader.
let statusMessage = '';
// This will remain empty unless the actions form is made sticky and
// previously was not.
let operationsAvailableMessage = '';
if (count > 0) {
// Check if bulk operations has changed from not-sticky to sticky.
if (!this.bulkActionsSticky) {
operationsAvailableMessage = Drupal.t(
'Bulk actions are now available. These actions will be applied to all selected items. This can be accessed via the "Skip to bulk actions" link that appears after every enabled checkbox. ',
);
this.bulkActionsSticky = true;
// Run the underStickyHandler after the CSS animation completes.
// Near the end of this there is an additional call to
// underStickyHandler without a timeout. This covers users who have
// animations disabled, and resets all items to visible if the bulk
// actions form is no longer sticky.
setTimeout(() => this.underStickyHandler(), 350);
// When the actions form becomes sticky, it appears via an animation
// at the bottom of the viewport. If this form is already above the
// viewport, the animation would look odd. In these instances the
// animation is bypassed.
const stickyRect = this.bulkActions.getBoundingClientRect();
const bypassAnimation =
stickyRect.top + stickyRect.height <
window.scrollY + window.innerHeight;
// Determine add/remove with ternary since IE11 does not support the
// second argument for classList.toggle().
const classAction = bypassAnimation ? 'add' : 'remove';
this.bulkActions.classList[classAction](
'views-form__header--bypass-animation',
);
}
statusMessage = Drupal.formatPlural(
count,
'1 item selected',
'@count items selected',
);
} else {
this.bulkActionsSticky = false;
statusMessage = Drupal.t('No items selected');
setTimeout(() => this.underStickyHandler(), 350);
}
// Update the attribute that instructs the bulk actions form to be sticky.
this.bulkActions.setAttribute(
'data-drupal-sticky-vbo',
this.bulkActionsSticky,
);
// Update the bulk actions form label with the number of items checked.
this.bulkActions.querySelector(
'[data-drupal-views-bulk-actions-status]',
).textContent = statusMessage;
// Announce these changes to the screen reader.
Drupal.announce(operationsAvailableMessage + statusMessage);
this.underStickyHandler();
}
};
Drupal.behaviors.claroTableSelect = {
attach(context) {
const bulkActions = once(
'ClaroBulkActions',
'[data-drupal-views-bulk-actions]',
context,
);
bulkActions.map(
(bulkActionForm) =>
/* eslint-disable-next-line no-new */
new Drupal.ClaroBulkActions(bulkActionForm),
);
},
};
})(jQuery, Drupal, window.tabbable, once);

View File

@@ -0,0 +1,81 @@
/**
* @file
* Password confirm widget template overrides.
*/
((Drupal) => {
Object.assign(Drupal.user.password.css, {
passwordWeak: 'is-weak',
widgetInitial: 'is-initial',
passwordEmpty: 'is-password-empty',
passwordFilled: 'is-password-filled',
confirmEmpty: 'is-confirm-empty',
confirmFilled: 'is-confirm-filled',
});
/**
* Constructs a password confirm message element.
*
* @param {object} passwordSettings
* An object containing password related settings and translated text to
* display.
* @param {string} passwordSettings.confirmTitle
* The translated confirm description that labels the actual confirm text.
*
* @return {string}
* Markup for the password confirm message.
*/
Drupal.theme.passwordConfirmMessage = ({ confirmTitle }) => {
const confirmTextWrapper =
'<span class="password-match-message__text" data-drupal-selector="password-match-status-text"></span>';
return `<div aria-live="polite" aria-atomic="true" class="password-match-message" data-drupal-selector="password-confirm-message">${confirmTitle} ${confirmTextWrapper}</div>`;
};
/**
* Constructs a password strength message.
*
* @param {object} passwordSettings
* An object containing password related settings and translated text to
* display.
* @param {string} passwordSettings.strengthTitle
* The title that precedes the strength text.
*
* @return {string}
* Markup for the password strength indicator.
*/
Drupal.theme.passwordStrength = ({ strengthTitle }) => {
const strengthBar =
'<div class="password-strength__bar" data-drupal-selector="password-strength-indicator"></div>';
const strengthText =
'<span class="password-strength__text" data-drupal-selector="password-strength-text"></span>';
return `
<div class="password-strength">
<div class="password-strength__track" data-drupal-selector="password-strength-meter">${strengthBar}</div>
<div aria-live="polite" aria-atomic="true" class="password-strength__title">${strengthTitle} ${strengthText}</div>
</div>
`;
};
/**
* Constructs password suggestions tips.
*
* @param {object} passwordSettings
* An object containing password related settings and translated tex t to
* display.
* @param {string} passwordSettings.hasWeaknesses
* The title that precedes tips.
* @param {Array.<string>} tips
* Array containing the tips.
*
* @return {string}
* Markup for the password suggestions.
*/
Drupal.theme.passwordSuggestions = ({ hasWeaknesses }, tips) =>
`<div class="password-suggestions">${
tips.length
? `${hasWeaknesses}<ul class="password-suggestions__tips"><li class="password-suggestions__tip">${tips.join(
'</li><li class="password-suggestions__tip">',
)}</li></ul>`
: ''
}</div>`;
})(Drupal);

View File

@@ -0,0 +1,41 @@
/**
* @file
* Overrides vertical tabs theming to enable Claro designs.
*/
(($, Drupal) => {
/**
* Theme function for a vertical tab.
*
* @param {object} settings
* An object with the following keys:
* @param {string} settings.title
* The name of the tab.
*
* @return {object}
* This function has to return an object with at least these keys:
* - item: The root tab jQuery element
* - link: The anchor tag that acts as the clickable area of the tab
* (jQuery version)
* - summary: The jQuery element that contains the tab summary
*/
Drupal.theme.verticalTab = (settings) => {
const tab = {};
tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>');
tab.title[0].textContent = settings.title;
tab.item = $(
'<li class="vertical-tabs__menu-item" tabindex="-1"></li>',
).append(
(tab.link = $('<a href="#" class="vertical-tabs__menu-link"></a>').append(
$('<span class="vertical-tabs__menu-link-content"></span>')
.append(tab.title)
.append(
(tab.summary = $(
'<span class="vertical-tabs__menu-link-summary"></span>',
)),
),
)),
);
return tab;
};
})(jQuery, Drupal);