Initial Drupal 11 with DDEV setup
This commit is contained in:
181
web/core/modules/navigation/js/admin-toolbar-wrapper.js
Normal file
181
web/core/modules/navigation/js/admin-toolbar-wrapper.js
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
*
|
||||
* Common JS managing behavior of admin-toolbar.
|
||||
*
|
||||
* Init Toolbar triggers.
|
||||
*
|
||||
* One trigger is button in Toolbar.
|
||||
* Another button in control panel on mobile.
|
||||
* Third is mobile shadow.
|
||||
* Fourth is close sidebar button on mobile.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
*/
|
||||
|
||||
(
|
||||
(Drupal, once) => {
|
||||
/**
|
||||
* Constant representing the event name for toggling the admin toolbar state.
|
||||
* @type {string}
|
||||
*/
|
||||
const HTML_TRIGGER_EVENT = 'toggle-admin-toolbar';
|
||||
|
||||
/**
|
||||
* Constant representing the event name for toggling the admin toolbar content.
|
||||
* @type {string}
|
||||
*/
|
||||
const SIDEBAR_CONTENT_EVENT = 'toggle-admin-toolbar-content';
|
||||
|
||||
if (
|
||||
once('admin-toolbar-document-triggers-listener', document.documentElement)
|
||||
.length
|
||||
) {
|
||||
const doc = document.documentElement;
|
||||
|
||||
// This is special attribute which added to apply css
|
||||
// with animations and avoid layout shift.
|
||||
setTimeout(() => {
|
||||
doc.setAttribute('data-admin-toolbar-transitions', true);
|
||||
}, 100);
|
||||
|
||||
doc.addEventListener(HTML_TRIGGER_EVENT, (e) => {
|
||||
// Prevents multiple triggering while transitioning.
|
||||
const newState = e.detail.state;
|
||||
const isUserInput = e.detail.manual;
|
||||
|
||||
document.documentElement.setAttribute(
|
||||
'data-admin-toolbar',
|
||||
newState ? 'expanded' : 'collapsed',
|
||||
);
|
||||
|
||||
// Set [data-admin-toolbar-body-scroll='locked']
|
||||
// See css/components/body-scroll-lock.pcss.css.
|
||||
|
||||
document.documentElement.setAttribute(
|
||||
'data-admin-toolbar-body-scroll',
|
||||
newState ? 'locked' : 'unlocked',
|
||||
);
|
||||
|
||||
doc.querySelector('.admin-toolbar')?.dispatchEvent(
|
||||
new CustomEvent(SIDEBAR_CONTENT_EVENT, {
|
||||
detail: {
|
||||
state: newState,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (isUserInput) {
|
||||
document.documentElement.setAttribute(
|
||||
'data-admin-toolbar-animating',
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
document.documentElement.removeAttribute(
|
||||
'data-admin-toolbar-animating',
|
||||
);
|
||||
}, 200);
|
||||
|
||||
Drupal.displace(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {HTMLElement} el - The admin toolbar wrapper.
|
||||
*/
|
||||
const initDisplace = (el) => {
|
||||
const displaceElement = el.querySelector(
|
||||
'.admin-toolbar__displace-placeholder',
|
||||
);
|
||||
const edge = document.documentElement.dir === 'rtl' ? 'right' : 'left';
|
||||
displaceElement?.setAttribute(`data-offset-${edge}`, '');
|
||||
Drupal.displace(true);
|
||||
};
|
||||
|
||||
// Any triggers on page. Inside or outside sidebar.
|
||||
// For now button in sidebar + mobile header and background.
|
||||
|
||||
Drupal.behaviors.navigationProcessToolbarTriggers = {
|
||||
attach: (context) => {
|
||||
once('navigation-displace', '.admin-toolbar', context).forEach(
|
||||
initDisplace,
|
||||
);
|
||||
|
||||
const triggers = once(
|
||||
'admin-toolbar-trigger',
|
||||
'[aria-controls="admin-toolbar"]',
|
||||
context,
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the state of all trigger elements based on the provided state.
|
||||
*
|
||||
* @param {boolean} toState The new state of the sidebar.
|
||||
*/
|
||||
const toggleTriggers = (toState) => {
|
||||
triggers.forEach((trigger) => {
|
||||
trigger.setAttribute('aria-expanded', toState);
|
||||
const text =
|
||||
trigger.querySelector('[data-toolbar-text]') ||
|
||||
trigger.querySelector('[data-toolbar-action]');
|
||||
if (text) {
|
||||
text.textContent = toState
|
||||
? Drupal.t('Collapse sidebar')
|
||||
: Drupal.t('Expand sidebar');
|
||||
}
|
||||
});
|
||||
localStorage.setItem('Drupal.navigation.sidebarExpanded', toState);
|
||||
};
|
||||
|
||||
if (triggers.length) {
|
||||
let firstState =
|
||||
localStorage.getItem('Drupal.navigation.sidebarExpanded') !==
|
||||
'false';
|
||||
|
||||
// We need to display closed sidebar on init on mobile.
|
||||
if (window.matchMedia('(max-width: 1023px)').matches) {
|
||||
firstState = false;
|
||||
}
|
||||
|
||||
// Set values on load.
|
||||
toggleTriggers(firstState);
|
||||
document.documentElement.dispatchEvent(
|
||||
new CustomEvent(HTML_TRIGGER_EVENT, {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
state: firstState,
|
||||
manual: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
triggers.forEach((trigger) => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
const state =
|
||||
e.currentTarget.getAttribute('aria-expanded') === 'false';
|
||||
trigger.dispatchEvent(
|
||||
new CustomEvent(HTML_TRIGGER_EVENT, {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
state,
|
||||
manual: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
toggleTriggers(state);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
)(Drupal, once);
|
||||
265
web/core/modules/navigation/js/arrow-navigation.js
Normal file
265
web/core/modules/navigation/js/arrow-navigation.js
Normal file
@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @file
|
||||
* Keyboard navigation component.
|
||||
*/
|
||||
|
||||
((Drupal, once, { focusable }) => {
|
||||
/**
|
||||
* Attaches the keyboard navigation functionality.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior to the `.admin-toolbar` element.
|
||||
*/
|
||||
Drupal.behaviors.keyboardNavigation = {
|
||||
attach: (context) => {
|
||||
once('keyboard-processed', '.admin-toolbar', context).forEach(
|
||||
(sidebar) => {
|
||||
const IS_RTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
const isInteractive = (element) =>
|
||||
element.getAttribute('aria-expanded');
|
||||
|
||||
const getFocusableGroup = (element) =>
|
||||
element.closest('[class*="toolbar-menu--level-"]') ||
|
||||
element.closest('[data-toolbar-popover-wrapper]') ||
|
||||
element.closest('.admin-toolbar');
|
||||
|
||||
const findFirstElementByChar = (focusableElements, targetChar) => {
|
||||
const elementWIthChar = Array.prototype.find.call(
|
||||
focusableElements,
|
||||
(element) => {
|
||||
const dataText = element.dataset.indexText;
|
||||
return dataText && dataText[0] === targetChar;
|
||||
},
|
||||
);
|
||||
|
||||
return elementWIthChar;
|
||||
};
|
||||
|
||||
const checkChar = ({ key, target }) => {
|
||||
const currentGroup = getFocusableGroup(target);
|
||||
const foundElementWithIndexChar = findFirstElementByChar(
|
||||
focusable(currentGroup),
|
||||
key,
|
||||
);
|
||||
if (foundElementWithIndexChar) {
|
||||
foundElementWithIndexChar.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const focusFirstInGroup = (focusableElements) => {
|
||||
focusableElements[0].focus();
|
||||
};
|
||||
|
||||
const focusLastInGroup = (focusableElements) => {
|
||||
focusableElements[focusableElements.length - 1].focus();
|
||||
};
|
||||
|
||||
const focusNextInGroup = (focusableElements, element) => {
|
||||
const currentIndex = Array.prototype.indexOf.call(
|
||||
focusableElements,
|
||||
element,
|
||||
);
|
||||
|
||||
if (currentIndex === focusableElements.length - 1) {
|
||||
focusableElements[0].focus();
|
||||
} else {
|
||||
focusableElements[currentIndex + 1].focus();
|
||||
}
|
||||
};
|
||||
|
||||
const focusPreviousInGroup = (focusableElements, element) => {
|
||||
const currentIndex = Array.prototype.indexOf.call(
|
||||
focusableElements,
|
||||
element,
|
||||
);
|
||||
|
||||
if (currentIndex === 0) {
|
||||
focusableElements[focusableElements.length - 1].focus();
|
||||
} else {
|
||||
focusableElements[currentIndex - 1].focus();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMenu = (element, state) =>
|
||||
element.dispatchEvent(
|
||||
new CustomEvent('toolbar-menu-set-toggle', {
|
||||
bubbles: false,
|
||||
detail: {
|
||||
state,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const closePopover = (element) =>
|
||||
element.dispatchEvent(
|
||||
new CustomEvent('toolbar-popover-close', { bubbles: true }),
|
||||
);
|
||||
|
||||
const openPopover = (element) =>
|
||||
element.dispatchEvent(
|
||||
new CustomEvent('toolbar-popover-open', { bubbles: true }),
|
||||
);
|
||||
|
||||
const focusClosestPopoverTrigger = (element) => {
|
||||
element
|
||||
.closest('[data-toolbar-popover]')
|
||||
?.querySelector('[data-toolbar-popover-control]')
|
||||
?.focus();
|
||||
};
|
||||
|
||||
const focusFirstMenuElement = (element) => {
|
||||
const elements = focusable(
|
||||
element
|
||||
.closest('.toolbar-menu__item')
|
||||
?.querySelector('.toolbar-menu'),
|
||||
);
|
||||
if (elements?.length) {
|
||||
elements[0].focus();
|
||||
}
|
||||
};
|
||||
|
||||
const focusFirstPopoverElement = (element) => {
|
||||
// Zero is always popover trigger.
|
||||
// And Popover header can be not interactive.
|
||||
const elements = focusable(
|
||||
element.closest('[data-toolbar-popover]'),
|
||||
);
|
||||
if (elements?.length >= 1) {
|
||||
elements[1].focus();
|
||||
}
|
||||
};
|
||||
|
||||
const focusLastPopoverElement = (element) => {
|
||||
const elements = focusable(
|
||||
element.closest('[data-toolbar-popover]'),
|
||||
);
|
||||
if (elements?.length > 0) {
|
||||
elements[elements.length - 1].focus();
|
||||
}
|
||||
};
|
||||
|
||||
const closeNonInteractiveElement = (element) => {
|
||||
// If we are inside submenus.
|
||||
if (element.closest('[class*="toolbar-menu--level-"]')) {
|
||||
const trigger =
|
||||
element.closest('.toolbar-menu')?.previousElementSibling;
|
||||
toggleMenu(trigger, false);
|
||||
trigger.focus();
|
||||
} else {
|
||||
closePopover(element);
|
||||
focusClosestPopoverTrigger(element);
|
||||
}
|
||||
};
|
||||
|
||||
const openInteractiveElement = (element) => {
|
||||
// If menu button.
|
||||
if (element.hasAttribute('data-toolbar-menu-trigger')) {
|
||||
toggleMenu(element, true);
|
||||
focusFirstMenuElement(element);
|
||||
}
|
||||
// If popover trigger.
|
||||
if (element.hasAttribute('data-toolbar-popover-control')) {
|
||||
openPopover(element);
|
||||
focusFirstPopoverElement(element);
|
||||
}
|
||||
};
|
||||
|
||||
const closeInteractiveElement = (element) => {
|
||||
// If menu button.
|
||||
if (element.hasAttribute('data-toolbar-menu-trigger')) {
|
||||
if (element.getAttribute('aria-expanded') === 'false') {
|
||||
closeNonInteractiveElement(element);
|
||||
} else {
|
||||
toggleMenu(element, false);
|
||||
focusFirstMenuElement(element);
|
||||
}
|
||||
}
|
||||
// If popover trigger.
|
||||
if (element.hasAttribute('data-toolbar-popover-control')) {
|
||||
openPopover(element);
|
||||
focusLastPopoverElement(element);
|
||||
}
|
||||
};
|
||||
|
||||
const arrowsSideControl = ({ key, target }) => {
|
||||
if (
|
||||
(key === 'ArrowRight' && !IS_RTL) ||
|
||||
(key === 'ArrowLeft' && IS_RTL)
|
||||
) {
|
||||
if (isInteractive(target)) {
|
||||
openInteractiveElement(target);
|
||||
// If also we want to care about expand button.
|
||||
if (
|
||||
target.getAttribute('aria-controls') === 'admin-toolbar' &&
|
||||
target.getAttribute('aria-expanded') === 'false'
|
||||
) {
|
||||
target.click();
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(key === 'ArrowRight' && IS_RTL) ||
|
||||
(key === 'ArrowLeft' && !IS_RTL)
|
||||
) {
|
||||
if (isInteractive(target)) {
|
||||
closeInteractiveElement(target);
|
||||
|
||||
// If also we want to care about expand button.
|
||||
if (
|
||||
target.getAttribute('aria-controls') === 'admin-toolbar' &&
|
||||
target.getAttribute('aria-expanded') !== 'false'
|
||||
) {
|
||||
target.click();
|
||||
}
|
||||
} else {
|
||||
closeNonInteractiveElement(target);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const arrowsDirectionControl = ({ key, target }) => {
|
||||
const focusableElements = focusable(getFocusableGroup(target));
|
||||
if (key === 'ArrowUp') {
|
||||
focusPreviousInGroup(focusableElements, target);
|
||||
} else if (key === 'ArrowDown') {
|
||||
focusNextInGroup(focusableElements, target);
|
||||
}
|
||||
};
|
||||
|
||||
sidebar.addEventListener('keydown', (e) => {
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
closePopover(e.target);
|
||||
focusClosestPopoverTrigger(e.target);
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
arrowsSideControl(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
arrowsDirectionControl(e);
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
focusFirstInGroup(getFocusableGroup(e.target));
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
focusLastInGroup(getFocusableGroup(e.target));
|
||||
break;
|
||||
default:
|
||||
checkChar(e);
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
})(Drupal, once, window.tabbable);
|
||||
57
web/core/modules/navigation/js/safe-triangle.js
Normal file
57
web/core/modules/navigation/js/safe-triangle.js
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* Element that improves sub-menu UX by implementing the Safe Triangle strategy.
|
||||
* @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles
|
||||
*/
|
||||
|
||||
((Drupal, once) => {
|
||||
/**
|
||||
* Update CSS variables values for positioning the safe triangle element.
|
||||
*
|
||||
* @param {CSSStyleDeclaration} style
|
||||
* Style property of the parent button.
|
||||
* @param {number} clientX
|
||||
* Horizontal position relative to the element.
|
||||
* @param {number} clientY
|
||||
* Vertical position relative to the element.
|
||||
*/
|
||||
function handleMouseMove({ currentTarget: { style }, clientX, clientY }) {
|
||||
style.setProperty('--safe-triangle-cursor-x', `${clientX}px`);
|
||||
style.setProperty('--safe-triangle-cursor-y', `${clientY}px`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the safe triangle behavior to all required triggers.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the safe triangle behavior.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Removes the safe triangle element.
|
||||
*/
|
||||
Drupal.behaviors.safeTriangleInit = {
|
||||
attach: (context) => {
|
||||
once('safe-triangle', '[data-has-safe-triangle]', context).forEach(
|
||||
(button) => {
|
||||
button.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
'<div data-safe-triangle></div>',
|
||||
);
|
||||
button.addEventListener('mousemove', handleMouseMove);
|
||||
},
|
||||
);
|
||||
},
|
||||
detach: (context, settings, trigger) => {
|
||||
if (trigger === 'unload') {
|
||||
once
|
||||
.remove('safe-triangle', '[data-has-safe-triangle]', context)
|
||||
.forEach((button) => {
|
||||
button.querySelector('[data-safe-triangle]')?.remove();
|
||||
button.removeEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
})(Drupal, once);
|
||||
155
web/core/modules/navigation/js/sidebar.js
Normal file
155
web/core/modules/navigation/js/sidebar.js
Normal file
@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* Sidebar component.
|
||||
*
|
||||
* Only few common things. Like close all popovers when one is opened.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior to the `.admin-toolbar` element.
|
||||
*/
|
||||
|
||||
(
|
||||
(Drupal, once) => {
|
||||
/**
|
||||
* Drupal behaviors object.
|
||||
*
|
||||
* @type {Drupal~behaviors}
|
||||
*/
|
||||
|
||||
Drupal.behaviors.navigation = {
|
||||
attach(context) {
|
||||
/**
|
||||
* Sidebar element with the `.admin-toolbar` class.
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
once('navigation', '.admin-toolbar', context).forEach((sidebar) => {
|
||||
const backButton = sidebar.querySelector(
|
||||
'[data-toolbar-back-control]',
|
||||
);
|
||||
if (!backButton) {
|
||||
// We're in layout editing mode and the .admin-toolbar we have in
|
||||
// scope here is the empty one that only exists to leave space for
|
||||
// the one added by layout builder. We need to use an empty
|
||||
// .admin-toolbar element because the css uses the adjacent
|
||||
// sibling selector.
|
||||
// @see \navigation_page_top();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* All menu triggers.
|
||||
*
|
||||
* @type {NodeList}
|
||||
*/
|
||||
const buttons = sidebar.querySelectorAll(
|
||||
'[data-toolbar-menu-trigger]',
|
||||
);
|
||||
|
||||
/**
|
||||
* All popovers and tooltip triggers.
|
||||
*
|
||||
* @type {NodeList}
|
||||
*/
|
||||
// const popovers = sidebar.querySelectorAll('[data-toolbar-popover]');
|
||||
|
||||
/**
|
||||
* NodeList of all tooltip elements.
|
||||
*
|
||||
* @type {NodeList}
|
||||
*/
|
||||
const tooltips = sidebar.querySelectorAll('[data-drupal-tooltip]');
|
||||
|
||||
const closeButtons = () => {
|
||||
buttons.forEach((button) => {
|
||||
button.dispatchEvent(
|
||||
new CustomEvent('toolbar-menu-set-toggle', {
|
||||
detail: {
|
||||
state: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const closePopovers = (current = false) => {
|
||||
// TODO: Find way to use popovers variable.
|
||||
// This change needed because BigPipe replaces user popover.
|
||||
sidebar
|
||||
.querySelectorAll('[data-toolbar-popover]')
|
||||
.forEach((popover) => {
|
||||
if (
|
||||
current &&
|
||||
current instanceof Element &&
|
||||
popover.isEqualNode(current)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
popover.dispatchEvent(
|
||||
new CustomEvent('toolbar-popover-close', {}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Add click event listeners to all buttons and then contains the callback
|
||||
// to expand / collapse the button's menus.
|
||||
sidebar.addEventListener('click', (e) => {
|
||||
if (e.target.matches('button, button *')) {
|
||||
e.target.closest('button').focus();
|
||||
}
|
||||
});
|
||||
|
||||
// We want to close all popovers when we close sidebar.
|
||||
sidebar.addEventListener('toggle-admin-toolbar-content', (e) => {
|
||||
if (!e.detail.state) {
|
||||
closePopovers();
|
||||
}
|
||||
});
|
||||
|
||||
// When any popover opened we close all others.
|
||||
sidebar.addEventListener('toolbar-popover-toggled', (e) => {
|
||||
if (e.detail.state) {
|
||||
closeButtons();
|
||||
closePopovers(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
// When any menu opened we close all others.
|
||||
sidebar.addEventListener('toolbar-menu-toggled', (e) => {
|
||||
if (e.detail.state) {
|
||||
// We want to close buttons on when new opened only if they are on same level.
|
||||
const targetLevel = e.detail.level;
|
||||
buttons.forEach((button) => {
|
||||
const buttonLevel = button.dataset.toolbarMenuTrigger;
|
||||
if (
|
||||
!button.isEqualNode(e.target) &&
|
||||
+buttonLevel === +targetLevel
|
||||
) {
|
||||
button.dispatchEvent(
|
||||
new CustomEvent('toolbar-menu-set-toggle', {
|
||||
detail: {
|
||||
state: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
backButton.addEventListener('click', closePopovers);
|
||||
|
||||
// Tooltips triggered on hover and focus so add an extra event listener
|
||||
// to close all popovers.
|
||||
tooltips.forEach((tooltip) => {
|
||||
['mouseover', 'focus'].forEach((e) => {
|
||||
tooltip.addEventListener(e, closePopovers);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
)(Drupal, once);
|
||||
53
web/core/modules/navigation/js/toolbar-dropdown.js
Normal file
53
web/core/modules/navigation/js/toolbar-dropdown.js
Normal file
@ -0,0 +1,53 @@
|
||||
/* cspell:ignore uidom */
|
||||
((Drupal, once, { computePosition, offset, shift, flip }) => {
|
||||
/**
|
||||
* Attaches the dropdown behavior to all required triggers.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the dropdown behavior.
|
||||
*/
|
||||
Drupal.behaviors.dropdownInit = {
|
||||
attach: (context) => {
|
||||
once('dropdown-trigger', '[data-drupal-dropdown]', context).forEach(
|
||||
(trigger) => {
|
||||
const dropdown = trigger.nextElementSibling;
|
||||
|
||||
const updatePosition = () => {
|
||||
computePosition(trigger, dropdown, {
|
||||
strategy: 'fixed',
|
||||
placement: trigger.dataset.drupalDropdownPosition || 'bottom',
|
||||
middleware: [
|
||||
flip({ padding: 16 }),
|
||||
offset(6),
|
||||
shift({ padding: 16 }),
|
||||
],
|
||||
}).then(({ x, y }) => {
|
||||
Object.assign(dropdown.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
trigger.addEventListener('click', (e) => {
|
||||
updatePosition();
|
||||
trigger.setAttribute(
|
||||
'aria-expanded',
|
||||
e.currentTarget.getAttribute('aria-expanded') === 'false',
|
||||
);
|
||||
});
|
||||
|
||||
// Event listener to close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const isButtonClicked = trigger.contains(e.target);
|
||||
if (!isButtonClicked) {
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
})(Drupal, once, FloatingUIDOM);
|
||||
135
web/core/modules/navigation/js/toolbar-menu.js
Normal file
135
web/core/modules/navigation/js/toolbar-menu.js
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
*
|
||||
* Toolbar menu code.
|
||||
*
|
||||
* Toggle own state
|
||||
* Listens toolbar-menu-set-toggle to change state.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
*/
|
||||
|
||||
(
|
||||
(Drupal, once) => {
|
||||
/**
|
||||
* Constant for the "toolbar-menu-set-toggle" event name.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const TOOLBAR_MENU_SET_TOGGLE = 'toolbar-menu-set-toggle';
|
||||
|
||||
/**
|
||||
* Initializes menu buttons.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Toggles aria-expanded attribute.
|
||||
* Changes buttons inner text.
|
||||
* Listens event when should be expanded.
|
||||
*/
|
||||
Drupal.behaviors.navigationProcessToolbarMenuTriggers = {
|
||||
attach: (context) => {
|
||||
once(
|
||||
'toolbar-menu-trigger',
|
||||
'[data-toolbar-menu-trigger]',
|
||||
context,
|
||||
).forEach((button) => {
|
||||
const menu = button.nextElementSibling;
|
||||
|
||||
/**
|
||||
* Element containing the button text.
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
const text = button.querySelector('[data-toolbar-action]');
|
||||
|
||||
/**
|
||||
* Toggles the button's aria-expanded attribute and updates its text.
|
||||
* This is only one function which change state of button.
|
||||
*
|
||||
* @param {boolean} state The button state it should be expanded or collapsed.
|
||||
*/
|
||||
const toggleButtonState = (state) => {
|
||||
button.setAttribute('aria-expanded', state);
|
||||
if (text) {
|
||||
text.textContent = state
|
||||
? Drupal.t('Collapse')
|
||||
: Drupal.t('Extend');
|
||||
}
|
||||
if (state) {
|
||||
menu.removeAttribute('inert');
|
||||
} else {
|
||||
menu.setAttribute('inert', true);
|
||||
}
|
||||
};
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
const level = e.currentTarget.dataset.toolbarMenuTrigger;
|
||||
const state =
|
||||
e.currentTarget.getAttribute('aria-expanded') === 'false';
|
||||
toggleButtonState(state);
|
||||
button.dispatchEvent(
|
||||
new CustomEvent('toolbar-menu-toggled', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
state,
|
||||
level,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// State of submenu button can be changed by CustomEvent.
|
||||
button.addEventListener(TOOLBAR_MENU_SET_TOGGLE, (e) => {
|
||||
const newState = e.detail.state;
|
||||
toggleButtonState(newState);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes menu links.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
*
|
||||
* When current url it adds classes and dispatch event to popover.
|
||||
*/
|
||||
Drupal.behaviors.navigationProcessToolbarMenuLinks = {
|
||||
attach: (context) => {
|
||||
once(
|
||||
'toolbar-menu-link',
|
||||
'a.toolbar-menu__link, a.toolbar-button',
|
||||
context,
|
||||
).forEach((link) => {
|
||||
// What we do if menu link is in current url.
|
||||
if (document.URL === link.href) {
|
||||
link.classList.add('current', 'is-active');
|
||||
|
||||
link.dispatchEvent(
|
||||
new CustomEvent('toolbar-active-url', {
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// We also want to open all parent menus.
|
||||
const menu = link.closest('.toolbar-menu');
|
||||
if (menu) {
|
||||
menu.previousElementSibling.dispatchEvent(
|
||||
new CustomEvent(TOOLBAR_MENU_SET_TOGGLE, {
|
||||
detail: {
|
||||
state: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
)(Drupal, once);
|
||||
153
web/core/modules/navigation/js/toolbar-popover.js
Normal file
153
web/core/modules/navigation/js/toolbar-popover.js
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
*
|
||||
* Toolbar popover.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
*/
|
||||
|
||||
const POPOVER_OPEN_DELAY = 150;
|
||||
const POPOVER_CLOSE_DELAY = 400;
|
||||
const POPOVER_NO_CLICK_DELAY = 500;
|
||||
|
||||
((Drupal, once) => {
|
||||
Drupal.behaviors.navigationProcessPopovers = {
|
||||
/**
|
||||
* Attaches the behavior to the context element.
|
||||
*
|
||||
* @param {HTMLElement} context The context element to attach the behavior to.
|
||||
*/
|
||||
attach: (context) => {
|
||||
once(
|
||||
'toolbar-popover',
|
||||
context.querySelectorAll('[data-toolbar-popover]'),
|
||||
).forEach((popover) => {
|
||||
// This is trigger of popover. Currently only first level button.
|
||||
const button = popover.querySelector('[data-toolbar-popover-control]');
|
||||
// This is tooltip content. Currently child menus only.
|
||||
const tooltip = popover.querySelector('[data-toolbar-popover-wrapper]');
|
||||
|
||||
if (!button || !tooltip) return;
|
||||
|
||||
const expandPopover = () => {
|
||||
popover.classList.add('toolbar-popover--expanded');
|
||||
button.dataset.drupalNoClick = 'true';
|
||||
tooltip.removeAttribute('inert');
|
||||
setTimeout(() => {
|
||||
delete button.dataset.drupalNoClick;
|
||||
}, POPOVER_NO_CLICK_DELAY);
|
||||
};
|
||||
|
||||
const collapsePopover = () => {
|
||||
popover.classList.remove('toolbar-popover--expanded');
|
||||
tooltip.setAttribute('inert', true);
|
||||
delete button.dataset.drupalNoClick;
|
||||
};
|
||||
|
||||
/**
|
||||
* We need to change state of trigger and popover.
|
||||
*
|
||||
* @param {boolean} state The popover state.
|
||||
*
|
||||
* @param {boolean} initialLoad Happens on page loads.
|
||||
*/
|
||||
const toggleState = (state, initialLoad = false) => {
|
||||
/* eslint-disable-next-line no-unused-expressions */
|
||||
state && !initialLoad ? expandPopover() : collapsePopover();
|
||||
button.setAttribute('aria-expanded', state && !initialLoad);
|
||||
|
||||
const text = button.querySelector('[data-toolbar-action]');
|
||||
if (text) {
|
||||
text.textContent = state
|
||||
? Drupal.t('Collapse')
|
||||
: Drupal.t('Extend');
|
||||
}
|
||||
|
||||
// Dispatch event to sidebar.js
|
||||
popover.dispatchEvent(
|
||||
new CustomEvent('toolbar-popover-toggled', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
state,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const isPopoverHoverOrFocus = () =>
|
||||
popover.contains(document.activeElement) || popover.matches(':hover');
|
||||
|
||||
const delayedClose = () => {
|
||||
setTimeout(() => {
|
||||
if (isPopoverHoverOrFocus()) return;
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
close();
|
||||
}, POPOVER_CLOSE_DELAY);
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
['mouseleave', 'focusout'].forEach((e) => {
|
||||
button.addEventListener(e, delayedClose, false);
|
||||
tooltip.addEventListener(e, delayedClose, false);
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
toggleState(false);
|
||||
['mouseleave', 'focusout'].forEach((e) => {
|
||||
button.removeEventListener(e, delayedClose);
|
||||
tooltip.removeEventListener(e, delayedClose);
|
||||
});
|
||||
};
|
||||
|
||||
button.addEventListener('mouseover', () => {
|
||||
// This is not needed because no hover on mobile.
|
||||
// @todo test is after.
|
||||
|
||||
if (window.matchMedia('(max-width: 1023px)').matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// If it is accident hover ignore it.
|
||||
// If in this timeout popover already opened by click.
|
||||
if (
|
||||
!button.matches(':hover') ||
|
||||
!button.getAttribute('aria-expanded') === 'false'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleState(true);
|
||||
open();
|
||||
}, POPOVER_OPEN_DELAY);
|
||||
});
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
const state =
|
||||
e.currentTarget.getAttribute('aria-expanded') === 'false';
|
||||
|
||||
if (!e.currentTarget.dataset.drupalNoClick) {
|
||||
toggleState(state);
|
||||
}
|
||||
});
|
||||
|
||||
// Listens events from sidebar.js.
|
||||
popover.addEventListener('toolbar-popover-close', () => {
|
||||
close();
|
||||
});
|
||||
|
||||
// TODO: Add toggle with state.
|
||||
popover.addEventListener('toolbar-popover-open', () => {
|
||||
toggleState(true);
|
||||
});
|
||||
|
||||
// Listens events from toolbar-menu.js
|
||||
popover.addEventListener('toolbar-active-url', () => {
|
||||
toggleState(true, true);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
})(Drupal, once);
|
||||
83
web/core/modules/navigation/js/tooltip.js
Normal file
83
web/core/modules/navigation/js/tooltip.js
Normal file
@ -0,0 +1,83 @@
|
||||
/* cspell:ignore uidom */
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* Simple tooltip component.
|
||||
*
|
||||
* To use it just add:
|
||||
*
|
||||
* data-drupal-tooltip="title" - Text displayed in tooltip.
|
||||
*
|
||||
* data-drupal-tooltip-class="extra-class" - Optional class for css.
|
||||
*
|
||||
* data-drupal-tooltip-position="top" - Tooltip position (default right).
|
||||
*
|
||||
* @see https://floating-ui.com/ for available placement options.
|
||||
*/
|
||||
|
||||
((Drupal, once, { computePosition, offset, shift, flip }) => {
|
||||
/**
|
||||
* Theme function for a tooltip.
|
||||
*
|
||||
* @param {object} dataset
|
||||
* The dataset object.
|
||||
* @param {string} dataset.drupalTooltipClass
|
||||
* Extra class for theming.
|
||||
* @param {string} dataset.drupalTooltip
|
||||
* The text for tooltip.
|
||||
*
|
||||
* @return {HTMLElement}
|
||||
* A DOM Node.
|
||||
*/
|
||||
Drupal.theme.tooltipWrapper = (dataset) =>
|
||||
`<div class="toolbar-tooltip ${dataset.drupalTooltipClass || ''}">
|
||||
${dataset.drupalTooltip}
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
* Attaches the tooltip behavior to all required triggers.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the tooltip behavior.
|
||||
*/
|
||||
Drupal.behaviors.tooltipInit = {
|
||||
attach: (context) => {
|
||||
once('tooltip-trigger', '[data-drupal-tooltip]', context).forEach(
|
||||
(trigger) => {
|
||||
trigger.insertAdjacentHTML(
|
||||
'afterend',
|
||||
Drupal.theme.tooltipWrapper(trigger.dataset),
|
||||
);
|
||||
const tooltip = trigger.nextElementSibling;
|
||||
|
||||
const updatePosition = () => {
|
||||
computePosition(trigger, tooltip, {
|
||||
strategy: 'fixed',
|
||||
placement: trigger.dataset.drupalTooltipPosition || 'right',
|
||||
middleware: [
|
||||
flip({ padding: 16 }),
|
||||
offset(6),
|
||||
shift({ padding: 16 }),
|
||||
],
|
||||
}).then(({ x, y }) => {
|
||||
Object.assign(tooltip.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Small trick to avoid tooltip stays on same place when button size changed.
|
||||
const ro = new ResizeObserver(updatePosition);
|
||||
|
||||
ro.observe(trigger);
|
||||
|
||||
trigger.addEventListener('mouseover', updatePosition);
|
||||
trigger.addEventListener('focus', updatePosition);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
})(Drupal, once, FloatingUIDOM);
|
||||
Reference in New Issue
Block a user