UNPKG

@plone/volto

Version:
188 lines (165 loc) 6.31 kB
import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import map from 'lodash/map'; import { Menu, Dropdown } from 'semantic-ui-react'; import AnchorLink from 'react-anchor-link-smooth-scroll'; import Slugger from 'github-slugger'; import { normalizeString } from '@plone/volto/helpers/Utils/Utils'; const RenderMenuItems = ({ items }) => { return map(items, (item) => { const { id, level, title, override_toc, plaintext } = item; const slug = override_toc ? Slugger.slug(normalizeString(plaintext)) : Slugger.slug(normalizeString(title)) || id; return ( item && ( <React.Fragment key={id}> <Menu.Item className={`headline-${level}`}> <AnchorLink href={`#${slug}`}>{title}</AnchorLink> </Menu.Item> {item.items?.length > 0 && <RenderMenuItems items={item.items} />} </React.Fragment> ) ); }); }; const View = ({ data, tocEntries }) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); // When the page is resized to prevent items from the TOC from going out of the viewport, // a dropdown menu is added containing all the items that don't fit. const handleResize = () => { const menuElement = document.querySelector('.responsive-menu'); const containerWidth = menuElement.offsetWidth; // Get all divs that contain the items from the TOC, except the dropdown button const nested = document.querySelectorAll( '.responsive-menu .item:not(.dropdown)', ); const nestedArray = Object.values(nested); const middle = Math.ceil(nestedArray.length / 2); const firstHalfNested = nestedArray.slice(0, middle); const secondHalfNested = nestedArray.slice(middle); const dropdown = document.querySelector('.dropdown'); const dropdownWidth = dropdown.offsetWidth; const firstHalfNestedHiddenItems = []; // Add a 'hidden' class for the items that should be in the dropdown firstHalfNested.forEach((item) => { const itemOffsetLeft = item.offsetLeft; const itemOffsetWidth = item.offsetWidth; if (itemOffsetLeft + itemOffsetWidth > containerWidth - dropdownWidth) { item.classList.add('hidden'); firstHalfNestedHiddenItems.push(item); } else { item.classList.remove('hidden'); } }); secondHalfNested.forEach((item) => item.classList.add('hidden-dropdown')); const diff = firstHalfNested.length - firstHalfNestedHiddenItems.length; const secondHalfNestedShownItems = secondHalfNested.slice(diff); secondHalfNestedShownItems.forEach((item) => item.classList.remove('hidden-dropdown'), ); // If there are elements that should be displayed in the dropdown, show the dropdown button if (secondHalfNestedShownItems.length > 0) dropdown.classList.remove('hidden-dropdown'); else { dropdown.classList.add('hidden-dropdown'); } }; const handleDropdownKeyDown = (event) => { const dropdownMenu = document.querySelector('.menu.transition'); if (event.key === 'ArrowDown' && isDropdownOpen) { event.preventDefault(); const menuItems = dropdownMenu.querySelectorAll( '.item:not(.hidden-dropdown)', ); const focusedItem = dropdownMenu.querySelector('.item.focused'); const focusedIndex = Array.from(menuItems).indexOf(focusedItem); if (focusedIndex === -1) { // No item is currently focused, so focus the first item menuItems[0].classList.add('focused'); } else if (focusedIndex === menuItems.length - 1) { // Remove focus from the currently focused item and close the dropdown focusedItem.classList.remove('focused'); setIsDropdownOpen(false); // Focus the next element on the page const nextElement = dropdownMenu.nextElementSibling; if (nextElement) { nextElement.focus(); } } else { // Remove focus from the currently focused item focusedItem.classList.remove('focused'); // Focus the next item or wrap around to the first item const nextIndex = (focusedIndex + 1) % menuItems.length; menuItems[nextIndex].classList.add('focused'); } } else if (event.key === 'Enter' && isDropdownOpen) { const focusedItem = dropdownMenu.querySelector('.item.focused'); if (focusedItem) { focusedItem.querySelector('a').click(); focusedItem.classList.remove('focused'); } } else if (event.key === 'Tab') { const focusedItem = dropdownMenu.querySelector('.item.focused'); if (focusedItem) { focusedItem.classList.remove('focused'); } } }; useEffect(() => { if (data.sticky) { const toc = document.querySelector('.horizontalMenu'); const tocPos = toc ? toc.offsetTop : 0; const handleScroll = () => { let scrollPos = window.scrollY; if (scrollPos > tocPos && toc) { toc.classList.add('sticky-toc'); } else if (scrollPos <= tocPos && toc) { toc.classList.remove('sticky-toc'); } }; window.addEventListener('scroll', handleScroll); return () => { window.removeEventListener('scroll', handleScroll); }; } }, [data.sticky]); useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }); return ( <> {data.title && !data.hide_title ? <h2>{data.title}</h2> : ''} <Menu className="responsive-menu"> <RenderMenuItems items={tocEntries} /> <Dropdown item text="More" className="hidden-dropdown" open={isDropdownOpen} onOpen={() => setIsDropdownOpen(true)} onClose={() => setIsDropdownOpen(false)} tabIndex={0} onKeyDown={handleDropdownKeyDown} > <Dropdown.Menu> <RenderMenuItems items={tocEntries} /> </Dropdown.Menu> </Dropdown> </Menu> </> ); }; /** * Property types. * @property {Object} propTypes Property types. * @static */ View.propTypes = { properties: PropTypes.objectOf(PropTypes.any).isRequired, }; export default View;