clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

176
web/themes/gin/js/accent.js Normal file
View File

@@ -0,0 +1,176 @@
/* eslint-disable no-bitwise, no-nested-ternary, no-mutable-exports, comma-dangle, strict */
((Drupal, drupalSettings, once) => {
Drupal.behaviors.ginAccent = {
attach: function attach(context) {
once('ginAccent', 'body', context).forEach(() => {
// Check Darkmode.
Drupal.ginAccent.checkDarkmode();
// Set accent color.
Drupal.ginAccent.setAccentColor();
// Set focus color.
Drupal.ginAccent.setFocusColor();
});
},
};
Drupal.ginAccent = {
setAccentColor: function setAccentColor(preset = null, color = null) {
const accentColorPreset = preset != null ? preset : drupalSettings.gin.preset_accent_color;
document.body.setAttribute('data-gin-accent', accentColorPreset);
if (accentColorPreset === 'custom') {
this.setCustomAccentColor(color);
}
},
setCustomAccentColor: function setCustomAccentColor(color = null, element = document.body) {
// If custom color is set, generate colors through JS.
const accentColor = color != null ? color : drupalSettings.gin.accent_color;
if (accentColor) {
this.clearAccentColor(element);
const strippedAccentColor = accentColor.replace('#', '');
const darkAccentColor = this.mixColor('ffffff', strippedAccentColor, 65).replace('#', '');
const style = document.createElement('style');
style.className = 'gin-custom-colors';
style.innerHTML = `
[data-gin-accent="custom"] {\n\
--gin-color-primary-rgb: ${this.hexToRgb(accentColor)};\n\
--gin-color-primary-hover: ${this.shadeColor(accentColor, -10)};\n\
--gin-color-primary-active: ${this.shadeColor(accentColor, -15)};\n\
--gin-bg-app-rgb: ${this.hexToRgb(this.mixColor('ffffff', strippedAccentColor, 97))};\n\
--gin-bg-header: ${this.mixColor('ffffff', strippedAccentColor, 85)};\n\
--gin-color-sticky-rgb: ${this.hexToRgb(this.mixColor('ffffff', strippedAccentColor, 92))};\n\
}\n\
.gin--dark-mode[data-gin-accent="custom"],\n\
.gin--dark-mode [data-gin-accent="custom"] {\n\
--gin-color-primary-rgb: ${this.hexToRgb(darkAccentColor)};\n\
--gin-color-primary-hover: ${this.mixColor('ffffff', strippedAccentColor, 55)};\n\
--gin-color-primary-active: ${this.mixColor('ffffff', strippedAccentColor, 50)};\n\
--gin-bg-header: ${this.mixColor('2A2A2D', darkAccentColor, 88)};\n\
}\n\
`;
element.append(style);
}
},
clearAccentColor: (element = document.body) => {
if (element.querySelectorAll('.gin-custom-colors').length > 0) {
const removeElement = element.querySelector('.gin-custom-colors');
removeElement.parentNode.removeChild(removeElement);
}
},
setFocusColor: function setFocusColor(preset = null, color = null) {
const focusColorPreset = preset != null ? preset : drupalSettings.gin.preset_focus_color;
document.body.setAttribute('data-gin-focus', focusColorPreset);
if (focusColorPreset === 'custom') {
this.setCustomFocusColor(color);
}
},
setCustomFocusColor: function setCustomFocusColor(color = null, element = document.body) {
const accentColor = color != null ? color : drupalSettings.gin.focus_color;
// Set preset color.
if (accentColor) {
this.clearFocusColor(element);
const strippedAccentColor = accentColor.replace('#', '');
const darkAccentColor = this.mixColor('ffffff', strippedAccentColor, 65);
const style = document.createElement('style');
style.className = 'gin-custom-focus';
style.innerHTML = `
[data-gin-focus="custom"] {\n\
--gin-color-focus: ${accentColor};\n\
}\n\
.gin--dark-mode[data-gin-focus="custom"],\n\
.gin--dark-mode [data-gin-focus="custom"] {\n\
--gin-color-focus: ${darkAccentColor};\n\
}`;
element.append(style);
}
},
clearFocusColor: (element = document.body) => {
if (element.querySelectorAll('.gin-custom-focus').length > 0) {
const removeElement = element.querySelector('.gin-custom-focus');
removeElement.parentNode.removeChild(removeElement);
}
},
checkDarkmode: () => {
const darkmodeClass = drupalSettings.gin.darkmode_class;
// Change to Darkmode.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (e.matches && localStorage.getItem('Drupal.gin.darkmode') === 'auto') {
document.querySelector('html').classList.add(darkmodeClass);
}
});
// Change to Lightmode.
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
if (e.matches && localStorage.getItem('Drupal.gin.darkmode') === 'auto') {
document.querySelector('html').classList.remove(darkmodeClass);
}
});
},
// https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
hexToRgb: (hex) => {
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : null;
},
// https://gist.github.com/jedfoster/7939513
mixColor: (color_1, color_2, weight) => {
function d2h(d) { return d.toString(16); }
function h2d(h) { return parseInt(h, 16); }
weight = (typeof(weight) !== 'undefined') ? weight : 50;
var color = "#";
for (var i = 0; i <= 5; i += 2) {
var v1 = h2d(color_1.substr(i, 2)),
v2 = h2d(color_2.substr(i, 2)),
val = d2h(Math.floor(v2 + (v1 - v2) * (weight / 100.0)));
while(val.length < 2) { val = '0' + val; }
color += val;
}
return color;
},
shadeColor: (color, percent) => {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const B = ((num >> 8) & 0x00ff) + amt;
const G = (num & 0x0000ff) + amt;
return `#${(
0x1000000
+ (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000
+ (B < 255 ? (B < 1 ? 0 : B) : 255) * 0x100
+ (G < 255 ? (G < 1 ? 0 : G) : 255)
)
.toString(16)
.slice(1)}`;
},
};
})(Drupal, drupalSettings, once);

View File

@@ -0,0 +1,53 @@
((Drupal) => {
Drupal.behaviors.formDescriptionToggle = {
attach: (context) => {
context
.querySelectorAll('.help-icon__description-toggle')
.forEach((elem, index) => {
if (elem.dataset.formDescriptionToggleAttached) {
return;
}
elem.dataset.formDescriptionToggleAttached = true;
const a11yLabel = 'help-icon-label--' + Math.floor(Math.random() * 10000);
elem.setAttribute('id', a11yLabel);
elem.setAttribute('aria-expanded', 'false');
elem.setAttribute('aria-controls', 'target');
elem
.closest('.help-icon__description-container')
.querySelectorAll(
'.claro-details__description, .fieldset__description, .form-item__description',
)
.forEach((description) => {
description.setAttribute('aria-labelledby', a11yLabel);
});
elem.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
// Open details element on toggle.
if (event.currentTarget.parentElement.tagName === 'SUMMARY'
&& event.currentTarget.parentElement.parentElement.open === false) {
event.currentTarget.parentElement.parentElement.open = true;
}
event.currentTarget.focus(); // firefox button focus issue
event.currentTarget
.closest('.help-icon__description-container')
.querySelectorAll(
'.claro-details__description, .fieldset__description, .form-item__description',
)
.forEach((description, index) => {
if (index > 1) {
return;
}
const setStatus = description.classList.contains('visually-hidden');
event.currentTarget.setAttribute('aria-expanded', setStatus);
description.classList.toggle('visually-hidden');
description.setAttribute('aria-hidden', !setStatus);
});
});
});
}
};
})(Drupal);

View File

@@ -0,0 +1,33 @@
/* eslint-disable func-names, no-mutable-exports, comma-dangle, strict */
'use strict';
((Drupal) => {
Drupal.behaviors.ginEditForm = {
attach: (context) => {
once('ginEditForm', '.region-content form.gin-node-edit-form', context).forEach(form => {
const sticky = context.querySelector('.gin-sticky');
const newParent = context.querySelector('.region-sticky__items__inner');
if (newParent && newParent.querySelectorAll('.gin-sticky').length === 0) {
newParent.appendChild(sticky);
// Attach form elements to main form
const actionButtons = newParent.querySelectorAll('button, input, select, textarea');
const formLabels = newParent.querySelectorAll('label');
if (actionButtons.length > 0) {
actionButtons.forEach((el) => {
el.setAttribute('form', form.getAttribute('id'));
el.setAttribute('id', el.getAttribute('id') + '--gin-edit-form');
});
formLabels.forEach((el => {
el.setAttribute('for', el.getAttribute('for') + '--gin-edit-form');
}));
}
}
});
}
};
})(Drupal);

View File

@@ -0,0 +1,137 @@
/* eslint-disable func-names, no-mutable-exports, comma-dangle, strict */
((Drupal, drupalSettings, once) => {
Drupal.behaviors.ginCKEditor = {
attach: (context) => {
Drupal.ginCKEditor.init(context);
}
};
Drupal.ginCKEditor = {
init: (context) => {
once('ginCKEditors', 'body', context).forEach(() => {
if (window.CKEDITOR && CKEDITOR !== undefined) {
// If on CKEditor config, do nothing.
if (drupalSettings.path.currentPath.indexOf('admin/config/content/formats/manage') > -1) {
return;
}
// Get configs.
const variablesCss = drupalSettings.gin.variables_css_path;
const accentCss = drupalSettings.gin.accent_css_path;
const contentsCss = drupalSettings.gin.ckeditor_css_path;
const accentColorPreset = drupalSettings.gin.preset_accent_color;
const accentColor = drupalSettings.gin.accent_color;
const darkmodeClass = drupalSettings.gin.darkmode_class;
// Class for Darkmode.
if (
localStorage.getItem('Drupal.gin.darkmode') == 1 ||
localStorage.getItem('Drupal.gin.darkmode') === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches
) {
CKEDITOR.config.bodyClass = darkmodeClass;
}
// Content stylesheets.
if (CKEDITOR.config.contentsCss === undefined) {
CKEDITOR.config.contentsCss.push(
variablesCss,
accentCss,
contentsCss
);
}
// Contextmenu stylesheets.
if (CKEDITOR.config.contextmenu_contentsCss === undefined) {
CKEDITOR.config.contextmenu_contentsCss = new Array();
// Check if skinName is set.
if (typeof CKEDITOR.skinName === 'undefined') {
CKEDITOR.skinName = CKEDITOR.skin.name;
}
CKEDITOR.config.contextmenu_contentsCss.push(
CKEDITOR.skin.getPath('editor'),
variablesCss,
accentCss,
contentsCss
);
}
CKEDITOR.on('instanceReady', (element) => {
const editor = element.editor;
// Initial accent color.
editor.document.$.body.setAttribute('data-gin-accent', accentColorPreset);
if (accentColorPreset === 'custom' && accentColor) {
Drupal.ginAccent.setCustomAccentColor(accentColor, editor.document.$.head);
}
// Change from Code to Editor.
editor.on('mode', function() {
if (this.mode == 'wysiwyg') {
editor.document.$.body.setAttribute('data-gin-accent', accentColorPreset);
if (accentColorPreset === 'custom' && accentColor) {
Drupal.ginAccent.setCustomAccentColor(accentColor, editor.document.$.head);
}
if (localStorage.getItem('Drupal.gin.darkmode') === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
editor.document.$.body.classList.add(darkmodeClass);
} else {
editor.document.$.body.classList.remove(darkmodeClass);
}
}
}
});
// Contextual menu.
editor.on('menuShow', function(element) {
const darkModeClass = localStorage.getItem('Drupal.gin.darkmode') == 1 || localStorage.getItem('Drupal.gin.darkmode') === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches
? darkmodeClass
: '';
const iframeElement = element.data[0].element.$.childNodes[0].contentWindow.document;
if (darkModeClass) {
iframeElement.body.classList.add(darkModeClass);
}
iframeElement.body.setAttribute('data-gin-accent', accentColorPreset);
if (accentColorPreset === 'custom' && accentColor) {
Drupal.ginAccent.setCustomAccentColor(accentColor, iframeElement.head);
}
});
// Toggle Darkmode.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (e.matches && localStorage.getItem('Drupal.gin.darkmode') === 'auto') {
editor.document.$.body.classList.add(darkmodeClass);
if (document.querySelectorAll(`.${editor.id}.cke_panel`).length > 0) {
const iframeElement = document.querySelector(`.${editor.id}.cke_panel`).childNodes[0].contentWindow.document;
iframeElement.body.classList.add(darkmodeClass);
}
}
});
// Change to Lightmode.
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
if (e.matches && localStorage.getItem('Drupal.gin.darkmode') === 'auto') {
editor.document.$.body.classList.remove(darkmodeClass);
if (document.querySelectorAll(`.${editor.id}.cke_panel`).length > 0) {
const iframeElement = document.querySelector(`.${editor.id}.cke_panel`).childNodes[0].contentWindow.document;
iframeElement.body.classList.remove(darkmodeClass);
}
}
});
});
}
});
},
};
})(Drupal, drupalSettings, once);

106
web/themes/gin/js/init.js Normal file
View File

@@ -0,0 +1,106 @@
/* To inject this as early as possible
* we use native JS instead of Drupal's behaviors.
*/
// Legacy Check: Transform old localStorage items to newer ones.
function checkLegacy() {
if (localStorage.getItem('GinDarkMode')) {
localStorage.setItem('Drupal.gin.darkmode', localStorage.getItem('GinDarkMode'));
localStorage.removeItem('GinDarkMode');
}
if (localStorage.getItem('GinSidebarOpen')) {
localStorage.setItem('Drupal.gin.toolbarExpanded', localStorage.getItem('GinSidebarOpen'));
localStorage.removeItem('GinSidebarOpen');
}
}
checkLegacy();
// Darkmode Check.
function ginInitDarkmode() {
const darkModeClass = 'gin--dark-mode';
if (
localStorage.getItem('Drupal.gin.darkmode') == 1 ||
(localStorage.getItem('Drupal.gin.darkmode') === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add(darkModeClass);
} else {
document.documentElement.classList.contains(darkModeClass) === true && document.documentElement.classList.remove(darkModeClass);
}
}
ginInitDarkmode();
// GinDarkMode is not set yet.
window.addEventListener('DOMContentLoaded', () => {
if (!localStorage.getItem('Drupal.gin.darkmode')) {
localStorage.setItem('Drupal.gin.darkmode', drupalSettings.gin.darkmode);
ginInitDarkmode();
}
});
// Toolbar Check.
if (localStorage.getItem('Drupal.gin.toolbarExpanded')) {
const style = document.createElement('style');
const className = 'gin-toolbar-inline-styles';
style.className = className;
if (localStorage.getItem('Drupal.gin.toolbarExpanded') === 'true') {
style.innerHTML = `
@media (min-width: 976px) {
/* Small CSS hack to make sure this has the highest priority */
body.gin--vertical-toolbar.gin--vertical-toolbar.gin--vertical-toolbar {
padding-inline-start: 256px !important;
transition: none !important;
}
.gin--vertical-toolbar .toolbar-menu-administration {
min-width: var(--gin-toolbar-width, 256px);
transition: none;
}
.gin--vertical-toolbar .toolbar-menu-administration > .toolbar-menu > .menu-item > .toolbar-icon,
.gin--vertical-toolbar .toolbar-menu-administration > .toolbar-menu > .menu-item > .toolbar-box > .toolbar-icon {
min-width: calc(var(--gin-toolbar-width, 256px) - 16px);
}
}
`;
const scriptTag = document.querySelector('script');
scriptTag.parentNode.insertBefore(style, scriptTag);
} else if (document.getElementsByClassName(className).length > 0) {
document.getElementsByClassName(className)[0].remove();
}
}
// Sidebar check.
if (localStorage.getItem('Drupal.gin.sidebarExpanded.desktop')) {
const style = document.createElement('style');
const className = 'gin-sidebar-inline-styles';
style.className = className;
if (window.innerWidth < 1024 || localStorage.getItem('Drupal.gin.sidebarExpanded.desktop') === 'false') {
style.innerHTML = `
body {
--gin-sidebar-offset: 0px;
padding-inline-end: 0;
transition: none;
}
.layout-region-node-secondary {
transform: translateX(var(--gin-sidebar-width, 360px));
transition: none;
}
.meta-sidebar__overlay {
display: none;
}
`;
const scriptTag = document.querySelector('script');
scriptTag.parentNode.insertBefore(style, scriptTag);
} else if (document.getElementsByClassName(className).length > 0) {
document.getElementsByClassName(className)[0].remove();
}
}

View File

@@ -0,0 +1,2 @@
/*! @drupal/once - v1.0.1 - 2021-06-12 */
var once=function(){"use strict";var n=/[\11\12\14\15\40]+/,e="data-once",t=document;function r(n,t,r){return n[t+"Attribute"](e,r)}function o(e){if("string"!=typeof e)throw new TypeError("once ID must be a string");if(""===e||n.test(e))throw new RangeError("once ID must not be empty or contain spaces");return'[data-once~="'+e+'"]'}function u(n){if(!(n instanceof Element))throw new TypeError("The element must be an instance of Element");return!0}function i(n,e){void 0===e&&(e=t);var r=n;if(null===n)r=[];else{if(!n)throw new TypeError("Selector must not be empty");"string"!=typeof n||e!==t&&!u(e)?n instanceof Element&&(r=[n]):r=e.querySelectorAll(n)}return Array.prototype.slice.call(r)}function c(n,e,t){return e.filter((function(e){var r=u(e)&&e.matches(n);return r&&t&&t(e),r}))}function f(e,t){var o=t.add,u=t.remove,i=[];r(e,"has")&&r(e,"get").trim().split(n).forEach((function(n){i.indexOf(n)<0&&n!==u&&i.push(n)})),o&&i.push(o);var c=i.join(" ");r(e,""===c?"remove":"set",c)}function a(n,e,t){return c(":not("+o(n)+")",i(e,t),(function(e){return f(e,{add:n})}))}return a.remove=function(n,e,t){return c(o(n),i(e,t),(function(e){return f(e,{remove:n})}))},a.filter=function(n,e,t){return c(o(n),i(e,t))},a.find=function(n,e){return i(n?o(n):"[data-once]",e)},a}();

View File

@@ -0,0 +1,31 @@
/**
* @file
* Main JavaScript file for Dismiss module
*/
/* eslint-disable func-names, no-mutable-exports, comma-dangle, strict */
((Drupal, once) => {
Drupal.behaviors.ginMessages = {
attach: (context) => {
Drupal.ginMessages.dismissMessages(context);
}
};
Drupal.ginMessages = {
dismissMessages: (context = document) => {
once('gin-messages-dismiss', '.messages .button--dismiss', context).forEach(dismissButton => {
dismissButton.addEventListener('click', e => {
e.preventDefault();
const message = e.currentTarget.closest('.messages-list__item');
Drupal.ginMessages.hideMessage(message);
});
});
},
hideMessage: (message) => {
message.style.opacity = 0;
message.classList.add('visually-hidden');
},
};
})(Drupal, once);

View File

@@ -0,0 +1,666 @@
((Drupal, once, { computePosition, offset, arrow, shift, flip }) => {
/**
* The wrapping element for the navigation sidebar.
*/
let sidebar;
// Gin Custom start ---------------------
const breakpointLarge = 1280;
// Gin Custom end ------------------------
/**
* Collapsed toolbar keydown handling vars.
*/
let firstFocusableEl;
const firstLevelToolbarItems = Array.from(document.querySelectorAll('.navigation__logo, .toolbar-menu > .toolbar-menu__item--level-1 > .toolbar-link')); // 1st level menu items.
const keys = {
tab: 9,
esc: 27,
space: 32,
};
let currentIndex, subIndex;
/**
* Informs in the navigation is expanded.
*
* @returns {boolean} - If the navigation is expanded.
*/
function isNavExpanded() {
return document.documentElement.classList.contains('admin-toolbar-expanded');
}
/**
* If active item is in the menu trail, then expand the navigation so it is
* open, and then scroll to it. This happens on page load and when
* transitioning between expanded and collapsed states.
*/
function autoExpandToActiveMenuItem() {
const activeItems = sidebar.querySelectorAll('.is-active');
closeAllSubmenus();
activeItems.forEach(activeItem => {
activeItem?.closest('.toolbar-menu__item.toolbar-menu__item--level-2')?.classList.add('toolbar-menu__item--expanded');
activeItem?.closest('.toolbar-menu__item.toolbar-menu__item--level-2')?.classList.add('active-path');
// Only expand level one if sidebar is in expanded state.
// Gin Custom start ---------------------
activeItem?.closest('.toolbar-menu__item.toolbar-menu__item--level-1')?.classList.add('active-path');
// Gin Custom end ------------------------
});
// Scroll to the open trays so they're in view.
const expandedTray = sidebar.querySelector('.toolbar-menu__item.toolbar-menu__item--expanded');
expandedTray?.scrollIntoView({ behavior: 'smooth' });
// Gin Custom start ---------------------
// checkOverflow();
// Gin Custom end -----------------------
}
/**
* Searches the sidebar for all menu items, and when it finds the menu item
* for the link that it's currently on, it will add relevant CSS classes to it
* so it can be styled and opened to.
*
* @todo once we move into the Drupal core menu system, we should be able to
* remove this.
*/
function markCurrentPageInMenu() {
// Check all links on the sidebar (that are not in the shortcutsNav
// <div>) to see if they are the current page. If so, set a `current`
// and `is-active` CSS class on the parent <li>.
const sidebarLinks = sidebar.querySelectorAll('a.toolbar-link:not(.menu--shortcuts *)');
sidebarLinks.forEach(link => {
if (link.href === document.URL) {
link.parentElement.classList.add('current', 'is-active');
}
});
// Gin Custom start ---------------------
// Mark overview pages as active.
const sidebarTitles = sidebar.querySelectorAll('.toolbar-menu__item--level-1[data-url]');
sidebarTitles.forEach(title => {
if (title.getAttribute('data-url') === window.location.pathname) {
title.querySelector('button.toolbar-link')?.classList.add('current', 'is-active');
}
});
// Gin Custom end ------------------------
}
/**
* Expand / collapse sidebar
*
* @param {boolean} toState - the state which the sidebar will be
* transitioning to (true if expanded, false if collapsed).
*/
function expandCollapseSidebar(toState) {
const expandCollapseButton = sidebar.querySelector('[aria-controls="admin-toolbar"]');
if (toState) closeTooltip();
document.documentElement.classList.toggle('admin-toolbar-expanded', toState);
Drupal.displace(true);
sidebar.querySelector('#sidebar-state').textContent = toState ? Drupal.t('Collapse sidebar') : Drupal.t('Expand sidebar');
expandCollapseButton.setAttribute('aria-expanded', toState);
localStorage.setItem('Drupal.navigation.sidebarExpanded', toState);
autoExpandToActiveMenuItem();
if (toState) {
flyoutTooltipDetach();
}
else {
flyoutTooltipInit();
}
// Gin Custom start ---------------------
if (toState === true && window.innerWidth < breakpointLarge) {
Drupal.ginSidebar.collapseSidebar();
}
// Gin Custom end ------------------------
}
/**
* Show shadow on sticky section only when expanded and content is
* overflowing.
*
* @todo this should be using either CSS only or Intersection Observer instead
* of getBoundingClientRect().
*/
// Gin Custom start ---------------------
// function checkOverflow() {
// if (isNavExpanded()) {
// const stickyMenu = sidebar.querySelector('.admin-toolbar__sticky-section');
// const mainMenu = sidebar.querySelector('#menu-builder'); // @todo why are we using ID here?
// const stickyMenuTopPos = stickyMenu?.getBoundingClientRect().top;
// const mainMenuBottomPos = mainMenu?.getBoundingClientRect().bottom;
// stickyMenu?.classList.toggle('shadow', mainMenuBottomPos > stickyMenuTopPos);
// }
// }
// Gin Custom end ------------------------
/**
* Calculate and place flyouts relative to their parent links using the
* floating UI library.
*
* @param {Element} hoveredEl - <li> element that was hovered over.
*/
function positionFlyout(hoveredEl) {
const anchorEl = hoveredEl.querySelector('.toolbar-link'); // This is the <button> element within the <li>.
const flyoutEl = document.getElementById(anchorEl.getAttribute('aria-controls')); // Top level <ul> that contains menu items to be shown.
const arrowEl = flyoutEl?.querySelector('.toolbar-menu__arrow-ref'); // Empty <div> that is styled as an arrow.
computePosition(anchorEl, flyoutEl, {
placement: 'right',
middleware: [
offset(6),
flip({ padding: 16 }),
shift({ padding: 16 }),
arrow({ element: arrowEl }),
],
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(flyoutEl.style, {
left: `${x}px`,
top: `${y}px`,
});
// Accessing the data
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]];
Object.assign(arrowEl.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '-4px',
});
});
}
/**
* Calculate and place tooltips relative to their parent links using the
* floating UI library.
*
* @param {Element} anchorEl - <a> element within the navigation link
* that was hovered over.
* @param {Element} tooltipEl - Tooltip span
* shown.
*/
function positionTooltip(hoveredEl) {
const anchorEl = hoveredEl.querySelector('.toolbar-link'); // This is the <a> element within the navigation link.
const tooltipEl = document.querySelector('.tooltip'); // This is the tooltip span.
computePosition(anchorEl, tooltipEl, {
placement: 'right',
middleware: [
offset(6),
flip({ padding: 16 }),
shift({ padding: 16 }),
],
}).then(({ x, y }) => {
Object.assign(tooltipEl.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
/**
* When flyouts are active, any click outside of the flyout should close the
* flyout.
*
* @param {Event} e - The click event.
*
* @todo can we refactor this to something like blur or focusout? It's only
* called from one place.
*/
function closeFlyoutOnClickOutside(e) {
// This can trigger when expand/collapse button is clicked. We need to
// ensure this only runs when navigation is collapsed.
if (isNavExpanded()) return;
if (!e.target.closest('.cloned-flyout')) {
closeFlyout();
}
}
/**
* Open the flyout when in collapsed mode.
*
* @param {Event || Element} e - Either the mouseenter event from the parentListItem or an element.
*/
function openFlyout(e) {
// Only one flyout can be open at once, so close currently
// open flyouts.
const hoveredEl = e.target ? e.target : e.parentElement; // This is the <li> that was hovered over. Check if it's an event object or not.
const buttonEl = hoveredEl.querySelector('.toolbar-link'); // The level-1 list item <button>.
const clonedFlyout = hoveredEl.querySelector('.toolbar-menu__submenu').cloneNode(true); // Flyout clone.
const clonedFlyoutId = `${hoveredEl.id}--flyout-clone`; // ID for flyout aria-controls.
// if (hoveredEl.classList.contains('toolbar-menu__item--expanded')) return;
closeFlyout();
closeTooltip();
// Add aria attributes to the flyout and <button>.
// Add a class to easily remove flyout in closeFlyout().
// Append the cloned flyout to the body to fix overflow issues with
// vertical scrolling on the collapsed sidebar.
buttonEl.setAttribute('aria-controls', clonedFlyoutId);
buttonEl.setAttribute('aria-expanded', true);
clonedFlyout.setAttribute('id', clonedFlyoutId);
clonedFlyout.classList.add('cloned-flyout');
document.querySelector('body').append(clonedFlyout);
// Add click event listeners to all buttons and then contains the callback
// to expand / collapse the button's menus.
clonedFlyout.querySelectorAll('.toolbar-menu__item--has-dropdown > button').forEach(el => el.addEventListener('click', (e) => {
openCloseSubmenu(e.currentTarget.parentElement);
}));
// Add click event listeners to title buttons when navigation is collapsed.
clonedFlyout.querySelectorAll('.toolbar-menu__item--to-title > button').forEach(el => el.addEventListener('click', (e) => {
const dataUrl = el.getAttribute('data-url');
if (!isNavExpanded() && dataUrl) {
window.location.assign(dataUrl);
}
}));
// Gin Custom start ---------------------
// Add the event listeners for focus handling on flyout clone.
const flyoutEls = document.querySelectorAll('.cloned-flyout .toolbar-menu__item--level-2 > .toolbar-link, .cloned-flyout .toolbar-menu__item--level-3 > .toolbar-link');
flyoutEls?.forEach(el => {
el.addEventListener('keydown', handleKeydownFlyout, false);
});
// Gin Custom end ------------------------
// If the active submenu is not yet open (it might be open if it has
// focus-within and the user did a mouseleave and mouseenter).
// if (!hoveredEl.classList.contains('toolbar-menu__item--expanded')) {
// Only position if the submenu is not already open. This prevents the
// flyout from unexpectedly shifting.
positionFlyout(hoveredEl);
// }
hoveredEl.classList.add('toolbar-menu__item--expanded');
// When a level-1 item hover ends, check if the flyout has focus and if
// not, close it.
clonedFlyout.addEventListener('mouseleave', delayedFlyoutClose, false);
// When a flyout is open, listen for clicks outside the flyout.
document.addEventListener('click', closeFlyoutOnClickOutside, false);
// Auto expand to the active menu item on the cloned flyout.
autoExpandToActiveMenuItem();
}
/**
* Open the tooltip when in collapsed mode.
* Note: JS solution needed due to requirement for vertical scrolling.
* CSS solution of overflow-y:scroll and overflow-x:visible on sidebar is not possible.
*
* @param {Event} e - The mouseenter event from the parentListItem.
*/
function openTooltip(e) {
closeFlyout();
closeTooltip();
const hoveredEl = e.target; // This is the <li> that was hovered over.
const clonedTooltip = hoveredEl.querySelector('.toolbar-link > span').cloneNode(true); // Tooltip clone.
// Add a class to easily remove flyout in closeTooltip().
// Append the cloned tooltip to the body to fix overflow issues with
// vertical scrolling on the collapsed sidebar.
clonedTooltip.classList.add('tooltip');
document.querySelector('body').append(clonedTooltip);
if (!hoveredEl.classList.contains('toolbar-menu__item--expanded')) {
positionTooltip(hoveredEl);
}
}
/**
* Close the flyout.
*/
function closeFlyout() {
// Remove expanded class if sidebar is collapsed.
// Remove cloned flyout element.
if (!isNavExpanded()) {
// Remove the event listeners for focus handling on flyout clone.
const flyoutEls = document.querySelectorAll('.cloned-flyout .toolbar-menu__item--level-2, .cloned-flyout .toolbar-menu__item--level-3');
flyoutEls?.forEach(el => {
el.removeEventListener('keydown', handleKeydownFlyout);
});
const clonedFlyout = document.querySelector('.cloned-flyout');
const clonedFlyoutControl = document.querySelector(`[aria-controls=${clonedFlyout?.id}]`);
clonedFlyoutControl?.removeAttribute('aria-controls');
clonedFlyoutControl?.setAttribute('aria-expanded', false);
closeAllSubmenus();
clonedFlyout?.removeEventListener('mouseleave', delayedFlyoutClose);
clonedFlyout?.remove();
document.removeEventListener('click', closeFlyoutOnClickOutside);
}
}
/**
* Close the tooltip.
*/
function closeTooltip() {
if (!isNavExpanded()) {
const clonedTooltip = document.querySelector('.tooltip');
clonedTooltip?.remove();
}
}
/**
* Close the flyout after timer (if not hovered over again).
*
* @param {e} - mouseleave event.
*/
function delayedFlyoutClose(e) {
const parentListItem = e.currentTarget;
const currentFlyout = document.querySelector('.cloned-flyout');
// Do not close flyout if it contains focus.
if (currentFlyout.contains(document.activeElement)) return;
timer = setTimeout(() => {
closeFlyout();
parentListItem.removeEventListener('mouseover', () => clearTimeout(timer), { once: true });
}, 400);
parentListItem.addEventListener('mouseover', () => clearTimeout(timer), { once: true });
}
/**
* Keyboard navigation for the collapsed toolbar top level items.
* This is particularly complex because of the flyout positioning outside of
* the sidebar markup.
*
* @param {Event} event - The keydown event.
*
*/
function handleKeydownTopLevel(event) {
// Reset the currentIndex so it's always accurate.
currentIndex = firstLevelToolbarItems.indexOf(event.target);
switch (event.keyCode) {
case keys.tab:
if (event.shiftKey) {
// Focus the previous menu item.
currentIndex--;
firstLevelToolbarItems[currentIndex]?.focus();
} else {
// Focus the next menu item.
currentIndex++;
if (firstLevelToolbarItems[currentIndex]) {
firstLevelToolbarItems[currentIndex].focus();
} else {
firstFocusableEl.focus();
}
}
event.preventDefault();
break;
case keys.space:
// Open the flyout and focus the first item in the flyout.
if (this.parentElement.classList.contains('toolbar-menu__item--has-dropdown')) {
openFlyout(this);
window.setTimeout(() => document.querySelector('.cloned-flyout .toolbar-menu__item--level-2 .toolbar-link').focus(), 0);
}
event.preventDefault();
break;
case keys.esc:
// Leave the menu, focus goes to first focusable element in the page content.
firstFocusableEl.focus();
event.preventDefault();
break;
}
}
/**
* Keyboard navigation for the collapsed toolbar flyouts.
*
* @param {Event} event - The keydown event.
*
*/
function handleKeydownFlyout(event) {
let flyoutEls = Array.from(document.querySelectorAll('.cloned-flyout .toolbar-menu__item--level-2 > .toolbar-link, .cloned-flyout .toolbar-menu__item--expanded .toolbar-menu__item--level-3 .toolbar-link'));;
subIndex = flyoutEls.indexOf(event.target);
switch (event.keyCode) {
case keys.tab:
if (event.shiftKey) {
// Tab & shift.
if (document.activeElement == event.target.parentElement.querySelector('li:nth-child(2) .toolbar-link') &&
document.activeElement.parentElement.classList.contains('toolbar-menu__item--level-2')) {
// Focus previous element but this was the first element in the flyout.
// Close the flyout and focus the parent element.
currentIndex--;
window.setTimeout(() => firstLevelToolbarItems[currentIndex].focus(), 0);
closeFlyout();
subIndex = 1;
} else {
subIndex--;
window.setTimeout(() => flyoutEls[subIndex].focus(), 0);
}
} else {
// Tab.
if (document.activeElement == event.target.parentElement.querySelector('li:last-of-type .toolbar-link') &&
document.activeElement.parentElement.classList.contains('toolbar-menu__item--level-2')) {
// Focus next element but there are no more elements in the flyout.
// Close the flyout and focus the parent element.
// Timeout needed for Firefox.
currentIndex++;
window.setTimeout(() => firstLevelToolbarItems[currentIndex].focus(), 0);
closeFlyout();
subIndex = 1;
} else {
subIndex++;
window.setTimeout(() => flyoutEls[subIndex].focus(), 0);
}
}
event.preventDefault();
break;
case keys.space:
const thirdLevel = event.target.parentElement.querySelectorAll('.toolbar-menu__item--level-3 .toolbar-link');
let indexToAdd = flyoutEls.indexOf(event.target) + 1;
thirdLevel.forEach(item => {
flyoutEls.splice(indexToAdd, 0, item);
indexToAdd++;
});
subIndex++;
window.setTimeout(() => flyoutEls[subIndex].focus(), 0);
break;
case keys.esc:
if (document.querySelector('.cloned-flyout')) {
currentIndex++;
firstLevelToolbarItems[currentIndex].focus();
closeFlyout();
subIndex = 1;
}
event.preventDefault();
break;
}
}
/**
* Flyout and tooltip setup in the collapsed toolbar state. This gets called when toolbar
* is put into a collapsed state.
*/
function flyoutTooltipInit() {
// Flyouts.
sidebar.querySelectorAll('.toolbar-menu__item--level-1 > .toolbar-menu__submenu')?.forEach(flyoutEl => {
const parentListItem = flyoutEl.parentElement;
// when a level-1 list item with children is hovered, open the flyout
parentListItem.addEventListener('mouseenter', openFlyout, false);
});
// Tooltips.
sidebar.querySelectorAll('.toolbar-menu__item--level-1:not(.toolbar-menu__item--has-dropdown) > .toolbar-link')?.forEach(tooltipEl => {
const parentListItem = tooltipEl.parentElement;
// when a childless level-1 list item is hovered, open the tooltip
parentListItem.addEventListener('mouseenter', openTooltip, false);
parentListItem.addEventListener('mouseleave', closeTooltip, false);
});
// Handle focus and keyboard nav in the collapse toolbar.
// Needed due to flyout markup repositioning in the DOM.
currentIndex = 0;
subIndex = 1;
firstFocusableEl = getFirstFocusableEl();
firstLevelToolbarItems?.forEach(firstLevelEl => {
firstLevelEl.addEventListener('keydown', handleKeydownTopLevel, false);
});
}
/**
* Remove all flyout and tooltip related event listeners. This gets called when toolbar is
* put into an expanded state.
*/
function flyoutTooltipDetach() {
// Flyouts.
sidebar.querySelectorAll('.toolbar-menu__item--level-1 > .toolbar-menu__submenu')?.forEach(flyoutEl => {
const parentListItem = flyoutEl.parentElement;
parentListItem.removeEventListener('mouseenter', openFlyout);
});
// Tooltips.
sidebar.querySelectorAll('.toolbar-menu__item--level-1:not(.toolbar-menu__item--has-dropdown) > .toolbar-link')?.forEach(tooltipEl => {
const parentListItem = tooltipEl.parentElement;
parentListItem.removeEventListener('mouseenter', openTooltip);
parentListItem.removeEventListener('mouseleave', closeTooltip);
});
// Keyboard navigation for collapsed toolbar and flyouts.
firstLevelToolbarItems?.forEach(firstLevelEl => {
firstLevelEl.removeEventListener('keydown', handleKeydownTopLevel);
});
}
/**
* Close all submenus that are underneath the optional element parameter.
*
* @param {Element} [Element] - Optional element under which to close all
* submenus.
*/
function closeAllSubmenus(Element) {
const submenuParentElement = Element ?? sidebar;
const selectorsToIgnore = '.sidebar-toggle';
let itemsToClose = submenuParentElement.querySelectorAll('.toolbar-menu__item--expanded');
// Don't remove expanded class from active trail when toolbar is collapsed.
if (!isNavExpanded()) {
itemsToClose = submenuParentElement.querySelectorAll('.toolbar-menu__item--expanded:not(.active-path)');
}
itemsToClose.forEach(el => el.classList.remove('toolbar-menu__item--expanded'));
submenuParentElement.querySelectorAll(`.toolbar-link[aria-expanded="true"]:not(:is(${selectorsToIgnore}))`).forEach(el => {
el.setAttribute('aria-expanded', false);
el.querySelector('.toolbar-link__action').textContent = Drupal.t('Extend');
});
}
/**
* Open or close the submenu. This can happen in both the open and closed
* state.
*
* @param {Element} parentListItem - the parent <li> that needs to be opened
* or closed
* @param {boolean} [state] - optional state where it will end up (true if
* opened, or false if closed). If omitted, state will be toggled.
*/
function openCloseSubmenu(parentListItem, state) {
toState = state ?? parentListItem.classList.contains('toolbar-menu__item--expanded');
const buttonEl = parentListItem.querySelector('button.toolbar-link');
// If we're clicking on a top level menu item, ensure that all other menu
// items close. Otherwise just close any other sibling menu items.
if (buttonEl.matches('.toolbar-menu__item.toolbar-menu__item--level-1 > *')) {
closeAllSubmenus()
}
else {
closeAllSubmenus(parentListItem.parentElement);
}
parentListItem.classList.toggle('toolbar-menu__item--expanded', !toState);
buttonEl.setAttribute('aria-expanded', toState);
buttonEl.querySelector('.toolbar-link__action').textContent = toState ? Drupal.t('Extend') : Drupal.t('Collapse');
// Gin Custom start ---------------------
// checkOverflow();
// Gin Custom end ------------------------
}
/**
* Initialize Drupal.displace()
*
* We add the displace attribute to a separate full width element because we
* don't want this element to have transitions. Note that this element and the
* navbar share the same exact width.
*/
function initDisplace() {
const displaceElement = sidebar.querySelector('.admin-toolbar__displace-placeholder');
const edge = document.documentElement.dir === 'rtl' ? 'right' : 'left';
displaceElement.setAttribute(`data-offset-${edge}`, '');
Drupal.displace(true);
}
/**
* Get the first focusable element in the page content (not drupal toolbar).
* This is used for the focus handling in the toolbar.
*/
function getFirstFocusableEl() {
const nextEl = sidebar.nextElementSibling.tagName == 'SCRIPT' ? sidebar.nextElementSibling.nextElementSibling : sidebar.nextElementSibling;
const focusableEls = nextEl.querySelectorAll('input:not([disabled]), select:not([disabled]), textarea:not([disabled]), iframe, [href], button, [tabindex="-1"]');
return focusableEls[0];
}
/**
* Initialize everything.
*/
function init(el) {
sidebar = el;
firstFocusableEl = getFirstFocusableEl();
const expandCollapseButton = sidebar.querySelector('[aria-controls="admin-toolbar"]');
markCurrentPageInMenu();
expandCollapseSidebar(localStorage.getItem('Drupal.navigation.sidebarExpanded') !== 'false');
initDisplace();
// Event listener to expand/collapse the sidebar.
expandCollapseButton.addEventListener('click', () => expandCollapseSidebar(!isNavExpanded()));
// Safari does not give focus to <button> elements on click (other browsers
// do). This event listener normalizes the behavior across browsers.
sidebar.addEventListener('click', e => {
if (e.target.matches('button, button *')) {
e.target.closest('button').focus();
}
});
// Add click event listeners to all buttons and then contains the callback
// to expand / collapse the button's menus.
sidebar.querySelectorAll('.toolbar-menu__item--has-dropdown > button').forEach(el => el.addEventListener('click', (e) => {
openCloseSubmenu(e.currentTarget.parentElement);
}));
// Gin Custom start ---------------------
// Make overview buttons clickable when collapsed
sidebar.querySelectorAll('.toolbar-menu__item--level-1 > button.toolbar-link').forEach(el => el.addEventListener('click', () => {
const dataUrl = el.parentElement.getAttribute('data-url');
if (!isNavExpanded() && dataUrl) {
window.location.assign(dataUrl);
}
}));
// Gin Custom end ------------------------
// Gin Custom start ---------------------
// Show toolbar navigation with shortcut:
// OPTION + T (Mac) / ALT + T (Windows)
document.addEventListener('keydown', e => {
if (e.altKey === true && e.code === 'KeyT') {
expandCollapseSidebar(!isNavExpanded());
}
});
// Gin Custom end ------------------------
}
Drupal.behaviors.navigation = {
attach(context) {
once('navigation', '.admin-toolbar', context).forEach(init);
},
// Gin Custom start ---------------------
collapseSidebar() {
expandCollapseSidebar(false);
},
// Gin Custom end ------------------------
};
})(Drupal, once, FloatingUIDOM);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,749 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@floating-ui/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@floating-ui/core'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FloatingUIDOM = {}, global.FloatingUICore));
})(this, (function (exports, core) { 'use strict';
function getWindow(node) {
var _node$ownerDocument;
return ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window;
}
function getComputedStyle$1(element) {
return getWindow(element).getComputedStyle(element);
}
function isNode(value) {
return value instanceof getWindow(value).Node;
}
function getNodeName(node) {
if (isNode(node)) {
return (node.nodeName || '').toLowerCase();
}
// Mocked nodes in testing environments may not be instances of Node. By
// returning `#document` an infinite loop won't occur.
// https://github.com/floating-ui/floating-ui/issues/2317
return '#document';
}
function isHTMLElement(value) {
return value instanceof getWindow(value).HTMLElement;
}
function isElement(value) {
return value instanceof getWindow(value).Element;
}
function isShadowRoot(node) {
// Browsers without `ShadowRoot` support.
if (typeof ShadowRoot === 'undefined') {
return false;
}
return node instanceof getWindow(node).ShadowRoot || node instanceof ShadowRoot;
}
function isOverflowElement(element) {
const {
overflow,
overflowX,
overflowY,
display
} = getComputedStyle$1(element);
return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !['inline', 'contents'].includes(display);
}
function isTableElement(element) {
return ['table', 'td', 'th'].includes(getNodeName(element));
}
function isContainingBlock(element) {
const safari = isSafari();
const css = getComputedStyle$1(element);
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return css.transform !== 'none' || css.perspective !== 'none' || !safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false) || !safari && (css.filter ? css.filter !== 'none' : false) || ['transform', 'perspective', 'filter'].some(value => (css.willChange || '').includes(value)) || ['paint', 'layout', 'strict', 'content'].some(value => (css.contain || '').includes(value));
}
function isSafari() {
if (typeof CSS === 'undefined' || !CSS.supports) return false;
return CSS.supports('-webkit-backdrop-filter', 'none');
}
function isLastTraversableNode(node) {
return ['html', 'body', '#document'].includes(getNodeName(node));
}
const min = Math.min;
const max = Math.max;
const round = Math.round;
const floor = Math.floor;
const createEmptyCoords = v => ({
x: v,
y: v
});
function getCssDimensions(element) {
const css = getComputedStyle$1(element);
// In testing environments, the `width` and `height` properties are empty
// strings for SVG elements, returning NaN. Fallback to `0` in this case.
let width = parseFloat(css.width) || 0;
let height = parseFloat(css.height) || 0;
const hasOffset = isHTMLElement(element);
const offsetWidth = hasOffset ? element.offsetWidth : width;
const offsetHeight = hasOffset ? element.offsetHeight : height;
const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight;
if (shouldFallback) {
width = offsetWidth;
height = offsetHeight;
}
return {
width,
height,
$: shouldFallback
};
}
function unwrapElement(element) {
return !isElement(element) ? element.contextElement : element;
}
function getScale(element) {
const domElement = unwrapElement(element);
if (!isHTMLElement(domElement)) {
return createEmptyCoords(1);
}
const rect = domElement.getBoundingClientRect();
const {
width,
height,
$
} = getCssDimensions(domElement);
let x = ($ ? round(rect.width) : rect.width) / width;
let y = ($ ? round(rect.height) : rect.height) / height;
// 0, NaN, or Infinity should always fallback to 1.
if (!x || !Number.isFinite(x)) {
x = 1;
}
if (!y || !Number.isFinite(y)) {
y = 1;
}
return {
x,
y
};
}
const noOffsets = /*#__PURE__*/createEmptyCoords(0);
function getVisualOffsets(element, isFixed, floatingOffsetParent) {
var _win$visualViewport, _win$visualViewport2;
if (isFixed === void 0) {
isFixed = true;
}
if (!isSafari()) {
return noOffsets;
}
const win = element ? getWindow(element) : window;
if (!floatingOffsetParent || isFixed && floatingOffsetParent !== win) {
return noOffsets;
}
return {
x: ((_win$visualViewport = win.visualViewport) == null ? void 0 : _win$visualViewport.offsetLeft) || 0,
y: ((_win$visualViewport2 = win.visualViewport) == null ? void 0 : _win$visualViewport2.offsetTop) || 0
};
}
function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) {
if (includeScale === void 0) {
includeScale = false;
}
if (isFixedStrategy === void 0) {
isFixedStrategy = false;
}
const clientRect = element.getBoundingClientRect();
const domElement = unwrapElement(element);
let scale = createEmptyCoords(1);
if (includeScale) {
if (offsetParent) {
if (isElement(offsetParent)) {
scale = getScale(offsetParent);
}
} else {
scale = getScale(element);
}
}
const visualOffsets = getVisualOffsets(domElement, isFixedStrategy, offsetParent);
let x = (clientRect.left + visualOffsets.x) / scale.x;
let y = (clientRect.top + visualOffsets.y) / scale.y;
let width = clientRect.width / scale.x;
let height = clientRect.height / scale.y;
if (domElement) {
const win = getWindow(domElement);
const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent;
let currentIFrame = win.frameElement;
while (currentIFrame && offsetParent && offsetWin !== win) {
const iframeScale = getScale(currentIFrame);
const iframeRect = currentIFrame.getBoundingClientRect();
const css = getComputedStyle(currentIFrame);
const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x;
const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y;
x *= iframeScale.x;
y *= iframeScale.y;
width *= iframeScale.x;
height *= iframeScale.y;
x += left;
y += top;
currentIFrame = getWindow(currentIFrame).frameElement;
}
}
return core.rectToClientRect({
width,
height,
x,
y
});
}
function getDocumentElement(node) {
return ((isNode(node) ? node.ownerDocument : node.document) || window.document).documentElement;
}
function getNodeScroll(element) {
if (isElement(element)) {
return {
scrollLeft: element.scrollLeft,
scrollTop: element.scrollTop
};
}
return {
scrollLeft: element.pageXOffset,
scrollTop: element.pageYOffset
};
}
function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) {
let {
rect,
offsetParent,
strategy
} = _ref;
const isOffsetParentAnElement = isHTMLElement(offsetParent);
const documentElement = getDocumentElement(offsetParent);
if (offsetParent === documentElement) {
return rect;
}
let scroll = {
scrollLeft: 0,
scrollTop: 0
};
let scale = createEmptyCoords(1);
const offsets = createEmptyCoords(0);
if (isOffsetParentAnElement || !isOffsetParentAnElement && strategy !== 'fixed') {
if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
scroll = getNodeScroll(offsetParent);
}
if (isHTMLElement(offsetParent)) {
const offsetRect = getBoundingClientRect(offsetParent);
scale = getScale(offsetParent);
offsets.x = offsetRect.x + offsetParent.clientLeft;
offsets.y = offsetRect.y + offsetParent.clientTop;
}
}
return {
width: rect.width * scale.x,
height: rect.height * scale.y,
x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x,
y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y
};
}
function getWindowScrollBarX(element) {
// If <html> has a CSS width greater than the viewport, then this will be
// incorrect for RTL.
return getBoundingClientRect(getDocumentElement(element)).left + getNodeScroll(element).scrollLeft;
}
// Gets the entire size of the scrollable document area, even extending outside
// of the `<html>` and `<body>` rect bounds if horizontally scrollable.
function getDocumentRect(element) {
const html = getDocumentElement(element);
const scroll = getNodeScroll(element);
const body = element.ownerDocument.body;
const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth);
const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight);
let x = -scroll.scrollLeft + getWindowScrollBarX(element);
const y = -scroll.scrollTop;
if (getComputedStyle$1(body).direction === 'rtl') {
x += max(html.clientWidth, body.clientWidth) - width;
}
return {
width,
height,
x,
y
};
}
function getParentNode(node) {
if (getNodeName(node) === 'html') {
return node;
}
const result =
// Step into the shadow DOM of the parent of a slotted node.
node.assignedSlot ||
// DOM Element detected.
node.parentNode ||
// ShadowRoot detected.
isShadowRoot(node) && node.host ||
// Fallback.
getDocumentElement(node);
return isShadowRoot(result) ? result.host : result;
}
function getNearestOverflowAncestor(node) {
const parentNode = getParentNode(node);
if (isLastTraversableNode(parentNode)) {
return node.ownerDocument ? node.ownerDocument.body : node.body;
}
if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) {
return parentNode;
}
return getNearestOverflowAncestor(parentNode);
}
function getOverflowAncestors(node, list) {
var _node$ownerDocument;
if (list === void 0) {
list = [];
}
const scrollableAncestor = getNearestOverflowAncestor(node);
const isBody = scrollableAncestor === ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.body);
const win = getWindow(scrollableAncestor);
if (isBody) {
return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : []);
}
return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor));
}
function getViewportRect(element, strategy) {
const win = getWindow(element);
const html = getDocumentElement(element);
const visualViewport = win.visualViewport;
let width = html.clientWidth;
let height = html.clientHeight;
let x = 0;
let y = 0;
if (visualViewport) {
width = visualViewport.width;
height = visualViewport.height;
const visualViewportBased = isSafari();
if (!visualViewportBased || visualViewportBased && strategy === 'fixed') {
x = visualViewport.offsetLeft;
y = visualViewport.offsetTop;
}
}
return {
width,
height,
x,
y
};
}
// Returns the inner client rect, subtracting scrollbars if present.
function getInnerBoundingClientRect(element, strategy) {
const clientRect = getBoundingClientRect(element, true, strategy === 'fixed');
const top = clientRect.top + element.clientTop;
const left = clientRect.left + element.clientLeft;
const scale = isHTMLElement(element) ? getScale(element) : createEmptyCoords(1);
const width = element.clientWidth * scale.x;
const height = element.clientHeight * scale.y;
const x = left * scale.x;
const y = top * scale.y;
return {
width,
height,
x,
y
};
}
function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) {
let rect;
if (clippingAncestor === 'viewport') {
rect = getViewportRect(element, strategy);
} else if (clippingAncestor === 'document') {
rect = getDocumentRect(getDocumentElement(element));
} else if (isElement(clippingAncestor)) {
rect = getInnerBoundingClientRect(clippingAncestor, strategy);
} else {
const visualOffsets = getVisualOffsets(element);
rect = {
...clippingAncestor,
x: clippingAncestor.x - visualOffsets.x,
y: clippingAncestor.y - visualOffsets.y
};
}
return core.rectToClientRect(rect);
}
function hasFixedPositionAncestor(element, stopNode) {
const parentNode = getParentNode(element);
if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) {
return false;
}
return getComputedStyle$1(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode);
}
// A "clipping ancestor" is an `overflow` element with the characteristic of
// clipping (or hiding) child elements. This returns all clipping ancestors
// of the given element up the tree.
function getClippingElementAncestors(element, cache) {
const cachedResult = cache.get(element);
if (cachedResult) {
return cachedResult;
}
let result = getOverflowAncestors(element).filter(el => isElement(el) && getNodeName(el) !== 'body');
let currentContainingBlockComputedStyle = null;
const elementIsFixed = getComputedStyle$1(element).position === 'fixed';
let currentNode = elementIsFixed ? getParentNode(element) : element;
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
while (isElement(currentNode) && !isLastTraversableNode(currentNode)) {
const computedStyle = getComputedStyle$1(currentNode);
const currentNodeIsContaining = isContainingBlock(currentNode);
if (!currentNodeIsContaining && computedStyle.position === 'fixed') {
currentContainingBlockComputedStyle = null;
}
const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && ['absolute', 'fixed'].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode);
if (shouldDropCurrentNode) {
// Drop non-containing blocks.
result = result.filter(ancestor => ancestor !== currentNode);
} else {
// Record last containing block for next iteration.
currentContainingBlockComputedStyle = computedStyle;
}
currentNode = getParentNode(currentNode);
}
cache.set(element, result);
return result;
}
// Gets the maximum area that the element is visible in due to any number of
// clipping ancestors.
function getClippingRect(_ref) {
let {
element,
boundary,
rootBoundary,
strategy
} = _ref;
const elementClippingAncestors = boundary === 'clippingAncestors' ? getClippingElementAncestors(element, this._c) : [].concat(boundary);
const clippingAncestors = [...elementClippingAncestors, rootBoundary];
const firstClippingAncestor = clippingAncestors[0];
const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => {
const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy);
accRect.top = max(rect.top, accRect.top);
accRect.right = min(rect.right, accRect.right);
accRect.bottom = min(rect.bottom, accRect.bottom);
accRect.left = max(rect.left, accRect.left);
return accRect;
}, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy));
return {
width: clippingRect.right - clippingRect.left,
height: clippingRect.bottom - clippingRect.top,
x: clippingRect.left,
y: clippingRect.top
};
}
function getDimensions(element) {
return getCssDimensions(element);
}
function getTrueOffsetParent(element, polyfill) {
if (!isHTMLElement(element) || getComputedStyle$1(element).position === 'fixed') {
return null;
}
if (polyfill) {
return polyfill(element);
}
return element.offsetParent;
}
function getContainingBlock(element) {
let currentNode = getParentNode(element);
while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) {
if (isContainingBlock(currentNode)) {
return currentNode;
} else {
currentNode = getParentNode(currentNode);
}
}
return null;
}
// Gets the closest ancestor positioned element. Handles some edge cases,
// such as table ancestors and cross browser bugs.
function getOffsetParent(element, polyfill) {
const window = getWindow(element);
if (!isHTMLElement(element)) {
return window;
}
let offsetParent = getTrueOffsetParent(element, polyfill);
while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === 'static') {
offsetParent = getTrueOffsetParent(offsetParent, polyfill);
}
if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle$1(offsetParent).position === 'static' && !isContainingBlock(offsetParent))) {
return window;
}
return offsetParent || getContainingBlock(element) || window;
}
function getRectRelativeToOffsetParent(element, offsetParent, strategy) {
const isOffsetParentAnElement = isHTMLElement(offsetParent);
const documentElement = getDocumentElement(offsetParent);
const isFixed = strategy === 'fixed';
const rect = getBoundingClientRect(element, true, isFixed, offsetParent);
let scroll = {
scrollLeft: 0,
scrollTop: 0
};
const offsets = createEmptyCoords(0);
if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {
scroll = getNodeScroll(offsetParent);
}
if (isHTMLElement(offsetParent)) {
const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent);
offsets.x = offsetRect.x + offsetParent.clientLeft;
offsets.y = offsetRect.y + offsetParent.clientTop;
} else if (documentElement) {
offsets.x = getWindowScrollBarX(documentElement);
}
}
return {
x: rect.left + scroll.scrollLeft - offsets.x,
y: rect.top + scroll.scrollTop - offsets.y,
width: rect.width,
height: rect.height
};
}
const platform = {
getClippingRect,
convertOffsetParentRelativeRectToViewportRelativeRect,
isElement,
getDimensions,
getOffsetParent,
getDocumentElement,
getScale,
async getElementRects(_ref) {
let {
reference,
floating,
strategy
} = _ref;
const getOffsetParentFn = this.getOffsetParent || getOffsetParent;
const getDimensionsFn = this.getDimensions;
return {
reference: getRectRelativeToOffsetParent(reference, await getOffsetParentFn(floating), strategy),
floating: {
x: 0,
y: 0,
...(await getDimensionsFn(floating))
}
};
},
getClientRects: element => Array.from(element.getClientRects()),
isRTL: element => getComputedStyle$1(element).direction === 'rtl'
};
// https://samthor.au/2021/observing-dom/
function observeMove(element, onMove) {
let io = null;
let timeoutId;
const root = getDocumentElement(element);
function cleanup() {
clearTimeout(timeoutId);
io && io.disconnect();
io = null;
}
function refresh(skip, threshold) {
if (skip === void 0) {
skip = false;
}
if (threshold === void 0) {
threshold = 1;
}
cleanup();
const {
left,
top,
width,
height
} = element.getBoundingClientRect();
if (!skip) {
onMove();
}
if (!width || !height) {
return;
}
const insetTop = floor(top);
const insetRight = floor(root.clientWidth - (left + width));
const insetBottom = floor(root.clientHeight - (top + height));
const insetLeft = floor(left);
const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px";
let isFirstUpdate = true;
io = new IntersectionObserver(entries => {
const ratio = entries[0].intersectionRatio;
if (ratio !== threshold) {
if (!isFirstUpdate) {
return refresh();
}
if (ratio === 0) {
timeoutId = setTimeout(() => {
refresh(false, 1e-7);
}, 100);
} else {
refresh(false, ratio);
}
}
isFirstUpdate = false;
}, {
rootMargin,
threshold
});
io.observe(element);
}
refresh(true);
return cleanup;
}
/**
* Automatically updates the position of the floating element when necessary.
* Should only be called when the floating element is mounted on the DOM or
* visible on the screen.
* @returns cleanup function that should be invoked when the floating element is
* removed from the DOM or hidden from the screen.
* @see https://floating-ui.com/docs/autoUpdate
*/
function autoUpdate(reference, floating, update, options) {
if (options === void 0) {
options = {};
}
const {
ancestorScroll = true,
ancestorResize = true,
elementResize = true,
layoutShift = typeof IntersectionObserver === 'function',
animationFrame = false
} = options;
const referenceEl = unwrapElement(reference);
const ancestors = ancestorScroll || ancestorResize ? [...(referenceEl ? getOverflowAncestors(referenceEl) : []), ...getOverflowAncestors(floating)] : [];
ancestors.forEach(ancestor => {
ancestorScroll && ancestor.addEventListener('scroll', update, {
passive: true
});
ancestorResize && ancestor.addEventListener('resize', update);
});
const cleanupIo = referenceEl && layoutShift ? observeMove(referenceEl, update) : null;
let resizeObserver = null;
if (elementResize) {
resizeObserver = new ResizeObserver(update);
if (referenceEl && !animationFrame) {
resizeObserver.observe(referenceEl);
}
resizeObserver.observe(floating);
}
let frameId;
let prevRefRect = animationFrame ? getBoundingClientRect(reference) : null;
if (animationFrame) {
frameLoop();
}
function frameLoop() {
const nextRefRect = getBoundingClientRect(reference);
if (prevRefRect && (nextRefRect.x !== prevRefRect.x || nextRefRect.y !== prevRefRect.y || nextRefRect.width !== prevRefRect.width || nextRefRect.height !== prevRefRect.height)) {
update();
}
prevRefRect = nextRefRect;
frameId = requestAnimationFrame(frameLoop);
}
update();
return () => {
ancestors.forEach(ancestor => {
ancestorScroll && ancestor.removeEventListener('scroll', update);
ancestorResize && ancestor.removeEventListener('resize', update);
});
cleanupIo && cleanupIo();
resizeObserver && resizeObserver.disconnect();
resizeObserver = null;
if (animationFrame) {
cancelAnimationFrame(frameId);
}
};
}
/**
* Computes the `x` and `y` coordinates that will place the floating element
* next to a reference element when it is given a certain CSS positioning
* strategy.
*/
const computePosition = (reference, floating, options) => {
// This caches the expensive `getClippingElementAncestors` function so that
// multiple lifecycle resets re-use the same result. It only lives for a
// single call. If other functions become expensive, we can add them as well.
const cache = new Map();
const mergedOptions = {
platform,
...options
};
const platformWithCache = {
...mergedOptions.platform,
_c: cache
};
return core.computePosition(reference, floating, {
...mergedOptions,
platform: platformWithCache
});
};
Object.defineProperty(exports, 'arrow', {
enumerable: true,
get: function () { return core.arrow; }
});
Object.defineProperty(exports, 'autoPlacement', {
enumerable: true,
get: function () { return core.autoPlacement; }
});
Object.defineProperty(exports, 'detectOverflow', {
enumerable: true,
get: function () { return core.detectOverflow; }
});
Object.defineProperty(exports, 'flip', {
enumerable: true,
get: function () { return core.flip; }
});
Object.defineProperty(exports, 'hide', {
enumerable: true,
get: function () { return core.hide; }
});
Object.defineProperty(exports, 'inline', {
enumerable: true,
get: function () { return core.inline; }
});
Object.defineProperty(exports, 'limitShift', {
enumerable: true,
get: function () { return core.limitShift; }
});
Object.defineProperty(exports, 'offset', {
enumerable: true,
get: function () { return core.offset; }
});
Object.defineProperty(exports, 'shift', {
enumerable: true,
get: function () { return core.shift; }
});
Object.defineProperty(exports, 'size', {
enumerable: true,
get: function () { return core.size; }
});
exports.autoUpdate = autoUpdate;
exports.computePosition = computePosition;
exports.getOverflowAncestors = getOverflowAncestors;
exports.platform = platform;
Object.defineProperty(exports, '__esModule', { value: true });
}));

View File

@@ -0,0 +1,5 @@
((Drupal) => {
Drupal.theme.mediaEmbedPreviewError = () => {
return '<div class="media-embed-error media-embed-error--preview-error">' + Drupal.t('An error occurred while trying to preview the media. Please save your work and reload this page.') + '</div>';
};
})(Drupal);

View File

@@ -0,0 +1,81 @@
((Drupal, once) => {
Drupal.behaviors.ginMediaLibrary = {
attach: function attach() {
Drupal.ginMediaLibrary.init();
},
};
Drupal.ginMediaLibrary = {
init: function () {
once('media-library-select-all', '.js-media-library-view[data-view-display-id="page"]').forEach(el => {
if (el.querySelectorAll('.js-media-library-item').length) {
const header = document.querySelector('.media-library-views-form');
const selectAll = document.createElement('label');
selectAll.className = 'media-library-select-all';
selectAll.innerHTML = Drupal.theme('checkbox') + Drupal.t('Select all media');
selectAll.children[0].addEventListener('click', e => {
const currentTarget = e.currentTarget;
const checkboxes = currentTarget
.closest('.js-media-library-view')
.querySelectorAll('.js-media-library-item .form-boolean');
checkboxes.forEach(checkbox => {
const stateChanged = checkbox.checked !== currentTarget.checked;
if (stateChanged) {
checkbox.checked = currentTarget.checked;
checkbox.dispatchEvent(new Event('change'));
}
});
const announcement = currentTarget.checked ? Drupal.t('All @count items selected', {
'@count': checkboxes.length
}) : Drupal.t('Zero items selected');
Drupal.announce(announcement);
this.bulkOperations();
});
header.prepend(selectAll);
}
this.itemSelect();
});
},
itemSelect: () => {
document.querySelectorAll('.media-library-view .js-click-to-select-trigger, .media-library-view .media-library-item .form-checkbox')
.forEach(trigger => {
trigger.addEventListener('click', () => {
const selectAll = document.querySelector('.media-library-select-all .form-boolean');
const checkboxes = document.querySelectorAll('.media-library-view .media-library-item .form-boolean');
const checkboxesChecked = document.querySelectorAll('.media-library-view .media-library-item .form-boolean:checked');
if (selectAll && selectAll.checked === true && checkboxes.length !== checkboxesChecked.length) {
selectAll.checked = false;
selectAll.dispatchEvent(new Event('change'));
} else if (checkboxes.length === Array.from(checkboxes).filter(el => el.checked === true).length) {
selectAll.checked = true;
selectAll.dispatchEvent(new Event('change'));
}
Drupal.ginMediaLibrary.bulkOperations();
});
});
},
bulkOperations: () => {
const bulkOperations = document.querySelector('.media-library-view [data-drupal-selector*="edit-header"]');
const bulkOperationsStickyBar = document.querySelector('.media-library-views-form__bulk_form');
if (bulkOperations && document.querySelectorAll('.media-library-view .form-checkbox:checked').length > 0) {
bulkOperations.classList.add('is-sticky');
bulkOperationsStickyBar?.setAttribute('data-drupal-sticky-vbo', true);
} else {
bulkOperations.classList.remove('is-sticky');
bulkOperationsStickyBar?.setAttribute('data-drupal-sticky-vbo', false);
}
},
};
})(Drupal, once);

View File

@@ -0,0 +1,181 @@
/* eslint-disable func-names, no-mutable-exports, comma-dangle, strict */
((Drupal, drupalSettings) => {
Drupal.behaviors.ginSettings = {
attach: function attach(context) {
Drupal.ginSettings.init(context);
},
};
Drupal.ginSettings = {
init: function (context) {
// Watch Darkmode setting has changed.
context.querySelectorAll('input[name="enable_darkmode"]')
.forEach(el => el.addEventListener('change', e => {
const darkmode = e.currentTarget.value;
const accentColorPreset = document.querySelector('[data-drupal-selector="edit-preset-accent-color"] input:checked').value;
const focusColorPreset = document.querySelector('select[name="preset_focus_color"]').value;
// Toggle Darkmode.
this.darkmode(darkmode);
// Set custom color if 'custom' is set.
if (accentColorPreset === 'custom') {
const accentColorSetting = document.querySelector('input[name="accent_color"]').value;
Drupal.ginAccent.setCustomAccentColor(accentColorSetting);
} else {
Drupal.ginAccent.setAccentColor(accentColorPreset);
}
// Toggle Focus color.
if (focusColorPreset === 'custom') {
const focusColorSetting = document.querySelector('input[name="focus_color"]').value;
Drupal.ginAccent.setCustomFocusColor(focusColorSetting);
} else {
Drupal.ginAccent.setFocusColor(focusColorPreset);
}
}));
// Watch Accent color setting has changed.
context.querySelectorAll('[data-drupal-selector="edit-preset-accent-color"] input')
.forEach(el => el.addEventListener('change', e => {
const accentColorPreset = e.currentTarget.value;
// Update.
Drupal.ginAccent.clearAccentColor();
Drupal.ginAccent.setAccentColor(accentColorPreset);
// Set custom color if 'custom' is set.
if (accentColorPreset === 'custom') {
const accentColorSetting = document.querySelector('input[name="accent_color"]').value;
Drupal.ginAccent.setCustomAccentColor(accentColorSetting);
}
}));
// Watch Accent color picker has changed.
context.querySelectorAll('input[name="accent_picker"]')
.forEach(el => el.addEventListener('change', e => {
const accentColorSetting = e.currentTarget.value;
// Sync fields.
document.querySelector('input[name="accent_color"]').value = accentColorSetting;
// Update.
Drupal.ginAccent.setCustomAccentColor(accentColorSetting);
}));
// Watch Accent color setting has changed.
context.querySelectorAll('input[name="accent_color"]')
.forEach(el => el.addEventListener('change', e => {
const accentColorSetting = e.currentTarget.value;
// Sync fields.
document.querySelector('input[name="accent_picker"]').value = accentColorSetting;
// Update.
Drupal.ginAccent.setCustomAccentColor(accentColorSetting);
}));
// Watch Focus color setting has changed.
document.querySelector('select[name="preset_focus_color"]').addEventListener('change', e => {
const focusColorPreset = e.currentTarget.value;
// Update.
Drupal.ginAccent.clearFocusColor();
Drupal.ginAccent.setFocusColor(focusColorPreset);
// Set custom color if 'custom' is set.
if (focusColorPreset === 'custom') {
const focusColorSetting = document.querySelector('input[name="focus_color"]').value;
Drupal.ginAccent.setCustomFocusColor(focusColorSetting);
}
});
// Watch Focus color picker has changed.
document.querySelector('input[name="focus_picker"]').addEventListener('change', e => {
const focusColorSetting = e.currentTarget.value;
// Sync fields.
document.querySelector('input[name="focus_color"]').value = focusColorSetting;
// Update.
Drupal.ginAccent.setCustomFocusColor(focusColorSetting);
});
// Watch Accent color setting has changed.
document.querySelector('input[name="focus_color"]').addEventListener('change', e => {
const focusColorSetting = e.currentTarget.value;
// Sync fields.
document.querySelector('input[name="focus_picker"]').value = focusColorSetting;
// Update.
Drupal.ginAccent.setCustomFocusColor(focusColorSetting);
});
// Watch Hight contrast mode setting has changed.
document.querySelector('input[name="high_contrast_mode"]').addEventListener('change', e => {
const highContrastMode = e.currentTarget.matches(':checked');
// Update.
this.setHighContrastMode(highContrastMode);
});
// Watch save
document.querySelector('[data-drupal-selector="edit-submit"]').addEventListener('click', () => {
// Reset darkmode localStorage.
localStorage.setItem('Drupal.gin.darkmode', '');
});
},
darkmode: function (darkmodeParam = null) {
const darkmodeEnabled = darkmodeParam != null ? darkmodeParam : drupalSettings.gin.darkmode;
const darkmodeClass = drupalSettings.gin.darkmode_class;
if (
darkmodeEnabled == 1 ||
(darkmodeEnabled === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.querySelector('html').classList.add(darkmodeClass);
}
else {
document.querySelector('html').classList.remove(darkmodeClass);
}
// Reset localStorage.
localStorage.setItem('Drupal.gin.darkmode', '');
// Change to Darkmode.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (e.matches && document.querySelector('input[name="enable_darkmode"]:checked').value === 'auto') {
document.querySelector('html').classList.add(darkmodeClass);
}
});
// Change to Lightmode.
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
if (e.matches && document.querySelector('input[name="enable_darkmode"]:checked').value === 'auto') {
document.querySelector('html').classList.remove(darkmodeClass);
}
});
},
setHighContrastMode: function (param = null) {
const enabled = param != null ? param : drupalSettings.gin.highcontrastmode;
const className = drupalSettings.gin.highcontrastmode_class;
// Needs to check for both: backwards compatibility.
if (enabled === true || enabled === 1) {
document.body.classList.add(className);
}
else {
document.body.classList.remove(className);
}
},
};
})(Drupal, drupalSettings);

View File

@@ -0,0 +1,136 @@
/* eslint-disable func-names, no-mutable-exports, comma-dangle, strict */
((Drupal, drupalSettings, once) => {
const breakpoint = 1024;
const breakpointLarge = 1280;
const toolbarVariant = drupalSettings.gin.toolbar_variant;
const storageMobile = 'Drupal.gin.sidebarExpanded.mobile';
const storageDesktop = 'Drupal.gin.sidebarExpanded.desktop';
Drupal.behaviors.ginSidebar = {
attach: function attach(context) {
Drupal.ginSidebar.init(context);
},
};
Drupal.ginSidebar = {
init: function (context) {
once('ginSidebarInit', '#gin_sidebar', context).forEach(() => {
// If variable does not exist, create it, default being to show sidebar.
if (!localStorage.getItem(storageDesktop)) {
localStorage.setItem(storageDesktop, 'true');
}
// Set mobile initial to false.
if (window.innerWidth >= breakpoint) {
if (localStorage.getItem(storageDesktop) === 'true') {
this.showSidebar();
}
else {
this.collapseSidebar();
}
}
// Show navigation with shortcut:
// OPTION + S (Mac) / ALT + S (Windows)
document.addEventListener('keydown', e => {
if (e.altKey === true && e.code === 'KeyS') {
this.toggleSidebar();
}
});
window.onresize = Drupal.debounce(this.handleResize, 150);
});
// Toolbar toggle
once('ginSidebarToggle', '.meta-sidebar__trigger', context).forEach(el => el.addEventListener('click', e => {
e.preventDefault();
this.removeInlineStyles();
this.toggleSidebar();
}));
// Toolbar close
once('ginSidebarClose', '.meta-sidebar__close, .meta-sidebar__overlay', context).forEach(el => el.addEventListener('click', e => {
e.preventDefault();
this.removeInlineStyles();
this.collapseSidebar();
}));
},
toggleSidebar: () => {
// Set active state.
if (document.querySelector('.meta-sidebar__trigger').classList.contains('is-active')) {
Drupal.ginSidebar.collapseSidebar();
}
else {
Drupal.ginSidebar.showSidebar();
}
},
showSidebar: () => {
const chooseStorage = window.innerWidth < breakpoint ? storageMobile : storageDesktop;
const showLabel = Drupal.t('Hide sidebar panel');
const sidebarTrigger = document.querySelector('.meta-sidebar__trigger');
sidebarTrigger.setAttribute('title', showLabel);
sidebarTrigger.querySelector('span').innerHTML = showLabel;
sidebarTrigger.setAttribute('aria-expanded', 'true');
sidebarTrigger.classList.add('is-active');
document.body.setAttribute('data-meta-sidebar', 'open');
// Expose to localStorage.
localStorage.setItem(chooseStorage, 'true');
// Check which toolbar is active.
if (window.innerWidth < breakpointLarge) {
if (toolbarVariant === 'vertical') {
Drupal.ginToolbar.collapseToolbar();
} else if (toolbarVariant === 'new') {
Drupal.behaviors.navigation.collapseSidebar();
}
}
},
collapseSidebar: () => {
const chooseStorage = window.innerWidth < breakpoint ? storageMobile : storageDesktop;
const hideLabel = Drupal.t('Show sidebar panel');
const sidebarTrigger = document.querySelector('.meta-sidebar__trigger');
sidebarTrigger.setAttribute('title', hideLabel);
sidebarTrigger.querySelector('span').innerHTML = hideLabel;
sidebarTrigger.setAttribute('aria-expanded', 'false');
sidebarTrigger.classList.remove('is-active');
document.body.setAttribute('data-meta-sidebar', 'closed');
// Expose to localStorage.
localStorage.setItem(chooseStorage, 'false');
},
handleResize: () => {
Drupal.ginSidebar.removeInlineStyles();
// If small viewport, always collapse sidebar.
if (window.innerWidth < breakpoint) {
Drupal.ginSidebar.collapseSidebar();
} else {
// If large viewport, show sidebar if it was open before.
if (localStorage.getItem(storageDesktop) === 'true') {
Drupal.ginSidebar.showSidebar();
} else {
Drupal.ginSidebar.collapseSidebar();
}
}
},
removeInlineStyles: () => {
// Remove init styles.
const elementToRemove = document.querySelector('.gin-sidebar-inline-styles');
if (elementToRemove) {
elementToRemove.parentNode.removeChild(elementToRemove);
}
},
};
})(Drupal, drupalSettings, once);

View File

@@ -0,0 +1,26 @@
/* eslint-disable no-bitwise, no-nested-ternary, no-mutable-exports, comma-dangle, strict */
'use strict';
((Drupal) => {
Drupal.behaviors.ginSticky = {
attach: (context) => {
once('ginSticky', '.region-sticky-watcher').forEach(() => {
// Watch sticky header
const observer = new IntersectionObserver(
([e]) => {
const regionSticky = context.querySelector('.region-sticky');
regionSticky.classList.toggle('region-sticky--is-sticky', e.intersectionRatio < 1);
regionSticky.toggleAttribute('data-offset-top', e.intersectionRatio < 1);
Drupal.displace(true);
},
{ threshold: [1] }
);
const element = context.querySelector('.region-sticky-watcher');
if (element) {
observer.observe(element);
}
});
}
};
})(Drupal);

View File

@@ -0,0 +1,32 @@
((Drupal, once) => {
Drupal.behaviors.ginTableHeader = {
attach: (context) => {
Drupal.ginTableHeader.init(context);
},
};
Drupal.ginTableHeader = {
init: function (context) {
once('ginTableHeader', '.sticky-enabled', context).forEach(el => {
// Watch sticky table header.
const observer = new IntersectionObserver(
([e]) => {
if (context.querySelector('.gin-table-scroll-wrapper')) {
if (!e.isIntersecting && e.intersectionRect.top === Drupal.displace.offsets.top) {
context.querySelector('.gin-table-scroll-wrapper').classList.add('--is-sticky');
} else {
context.querySelector('.gin-table-scroll-wrapper').classList.remove('--is-sticky');
}
Drupal.displace(true);
}
},
{ threshold: 1.0, rootMargin: `-${Drupal.displace.offsets.top}px 0px 0px 0px` }
);
observer.observe(el.querySelector('thead'));
});
},
};
})(Drupal, once);

View File

@@ -0,0 +1,145 @@
/* eslint-disable func-names, no-mutable-exports, comma-dangle, strict */
((Drupal, drupalSettings, once) => {
const breakpointLarge = 1280;
const toolbarVariant = drupalSettings.gin.toolbar_variant;
Drupal.behaviors.ginToolbar = {
attach: (context) => {
Drupal.ginToolbar.init(context);
},
};
/**
* Replaces the "Home" link with "Back to site" link.
*
* Back to site link points to the last non-administrative page the user
* visited within the same browser tab.
*/
Drupal.behaviors.ginEscapeAdmin = {
attach: (context) => {
once('ginEscapeAdmin', '[data-gin-toolbar-escape-admin]', context).forEach(el => {
const escapeAdminPath = sessionStorage.getItem('escapeAdminPath');
if (drupalSettings.path.currentPathIsAdmin && escapeAdminPath !== null) {
el.setAttribute('href', escapeAdminPath);
}
});
},
};
Drupal.ginToolbar = {
init: function (context) {
once('ginToolbarInit', '#gin-toolbar-bar', context).forEach(() => {
const toolbarTrigger = document.querySelector('.toolbar-menu__trigger');
// Check for Drupal trayVerticalLocked and remove it.
if (toolbarVariant != 'classic' && localStorage.getItem('Drupal.toolbar.trayVerticalLocked')) {
localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
}
// Set sidebarState.
if (localStorage.getItem('Drupal.gin.toolbarExpanded') === 'true') {
document.body.setAttribute('data-toolbar-menu', 'open');
toolbarTrigger.classList.add('is-active');
}
else {
document.body.setAttribute('data-toolbar-menu', '');
toolbarTrigger.classList.remove('is-active');
}
// Show toolbar navigation with shortcut:
// OPTION + T (Mac) / ALT + T (Windows)
document.addEventListener('keydown', e => {
if (e.altKey === true && e.code === 'KeyT') {
this.toggleToolbar();
}
});
this.initDisplace();
});
// Toolbar toggle
once('ginToolbarToggle', '.toolbar-menu__trigger', context).forEach(el => el.addEventListener('click', e => {
e.preventDefault();
this.toggleToolbar();
}));
},
initDisplace: () => {
const toolbar = document.querySelector('#gin-toolbar-bar .toolbar-menu-administration');
if (toolbar) {
if (toolbarVariant === 'vertical') {
toolbar.setAttribute('data-offset-left', '');
} else {
toolbar.setAttribute('data-offset-top', '');
}
}
},
toggleToolbar: function () {
const toolbarTrigger = document.querySelector('.toolbar-menu__trigger');
// Toggle active class.
toolbarTrigger.classList.toggle('is-active');
if (toolbarTrigger.classList.contains('is-active')) {
this.showToolbar();
}
else {
this.collapseToolbar();
}
},
showToolbar: function () {
const active = 'true';
document.body.setAttribute('data-toolbar-menu', 'open');
// Write state to localStorage.
localStorage.setItem('Drupal.gin.toolbarExpanded', active);
this.dispatchToolbarEvent(active);
this.displaceToolbar();
// Check which toolbar is active.
if (window.innerWidth < breakpointLarge && toolbarVariant === 'vertical') {
Drupal.ginSidebar.collapseSidebar();
}
},
collapseToolbar: function () {
const toolbarTrigger = document.querySelector('.toolbar-menu__trigger');
const elementToRemove = document.querySelector('.gin-toolbar-inline-styles');
const active = 'false';
toolbarTrigger.classList.remove('is-active');
document.body.setAttribute('data-toolbar-menu', '');
if (elementToRemove) {
elementToRemove.parentNode.removeChild(elementToRemove);
}
// Write state to localStorage.
localStorage.setItem('Drupal.gin.toolbarExpanded', 'false');
this.dispatchToolbarEvent(active);
this.displaceToolbar();
},
dispatchToolbarEvent: (active) => {
// Dispatch event.
const event = new CustomEvent('toolbar-toggle', { detail: active === 'true'})
document.dispatchEvent(event);
},
displaceToolbar: () => {
ontransitionend = () => {
Drupal.displace(true);
};
},
};
})(Drupal, drupalSettings, once);