diff --git a/blocks/v2-breadcrumb/v2-breadcrumb.css b/blocks/v2-breadcrumb/v2-breadcrumb.css new file mode 100644 index 000000000..cc1dbd31c --- /dev/null +++ b/blocks/v2-breadcrumb/v2-breadcrumb.css @@ -0,0 +1,55 @@ +.v2-breadcrumb-wrapper{ + background-color: var(--c-secondary-graphite); +} + +.v2-breadcrumb { + padding: 18px 16px 14px; + color: var(--c-primary-white); + max-width: var(--wrapper-width); + margin: 0 auto; + display: flex; +} + +.v2-breadcrumb .v2-breadcrumb__crumb { + text-transform: capitalize; + color: inherit; + display: flex; +} + +/* .v2-breadcrumb__crumb:not(.v2-breadcrumb__crumb--home)::before { + content: '/'; + padding: 0 10px; +} + +.v2-breadcrumb__crumb:not(.v2-breadcrumb__crumb--home):hover::before { + display: inline-block; + text-decoration: none; +} + +.v2-breadcrumb__crumb:not(.v2-breadcrumb__crumb--home):empty { + width: 0; +} + +.v2-breadcrumb__crumb:not(.v2-breadcrumb__crumb--home):empty::before { + content: ''; +} */ + +.v2-breadcrumb__crumb-separator { + padding: 0 10px; + color: var(--c-primary-white); + font-family: var(--ff-body); +} + +.v2-breadcrumb__crumb--active { + color: var(--c-primary-white); + font-family: var(--ff-body-bold); + white-space: nowrap; +} + +.v2-breadcrumb__crumb--active::before { + font-family: var(--ff-body) +} + +.v2-breadcrumb__crumb--active:hover { + text-decoration: none; +} diff --git a/blocks/v2-breadcrumb/v2-breadcrumb.js b/blocks/v2-breadcrumb/v2-breadcrumb.js new file mode 100644 index 000000000..fa179e2cc --- /dev/null +++ b/blocks/v2-breadcrumb/v2-breadcrumb.js @@ -0,0 +1,121 @@ +import { readBlockConfig } from '../../scripts/lib-franklin.js'; +import { createElement } from '../../scripts/common.js'; + +const blockName = 'v2-breadcrumb'; +const sectionStatus = 'data-section-status'; +const homeText = { + home: 'home', + ellipsis: '...', +}; + +const removePathDash = (str) => str.replace(/-/g, ' ').toLowerCase(); + +const getPadding = (elCompCSS) => parseInt(elCompCSS.getPropertyValue('padding-left'), 10) + + parseInt(elCompCSS.getPropertyValue('padding-right'), 10); + +const getCrumbsWidth = (block) => { + const crumbs = block.querySelectorAll(`.${blockName}__crumb`); + return [...crumbs].reduce((acc, item) => { + const itemCompCSS = window.getComputedStyle(item); + return acc + parseInt(itemCompCSS.getPropertyValue('width'), 10); + }, 0); +}; + +const getBlockWidth = (block) => { + const computedCSS = window.getComputedStyle(block); + const blockWidth = parseInt(computedCSS.getPropertyValue('width'), 10); + const boxSizing = computedCSS.getPropertyValue('box-sizing'); + const padding = boxSizing === 'border-box' ? getPadding(computedCSS) : 0; + return blockWidth - padding; +}; + +const areCrumbsFit = (block) => getCrumbsWidth(block) < getBlockWidth(block); + +const newSeparator = () => { + const separator = createElement('span', { classes: [`${blockName}__crumb-separator`] }); + separator.textContent = '/'; + return separator; +}; + +export default function decorate(block) { + const cfg = readBlockConfig(block); + const hasPath = cfg && Object.hasOwn(cfg, 'path'); + const url = new URL(window.location.href); + + if (hasPath) url.pathname = cfg.path; + + const path = url.pathname.split('/').filter(Boolean); + const crumbs = path.map((_, i) => { + const crumbProps = { 'data-content': removePathDash(path[i]) }; + const crumbClasses = [`${blockName}__crumb`]; + if (i !== path.length - 1) { + crumbProps.href = `/${path.slice(0, i + 1).join('/')}/`; + } else { + crumbClasses.push(`${blockName}__crumb--active`); + crumbProps['aria-current'] = 'page'; + } + const crumb = createElement('a', { classes: crumbClasses, props: crumbProps }); + crumb.textContent = removePathDash(path[i]); + crumb.prepend(newSeparator()); + return crumb; + }); + const homeEl = createElement('a', { + classes: [`${blockName}__crumb`, `${blockName}__crumb--home`], + props: { href: '/' }, + }); + + homeEl.textContent = homeText.home; + crumbs.unshift(homeEl); + block.textContent = ''; + block.append(...crumbs); + block.parentElement.classList.add('full-width'); + block.setAttribute('aria-label', 'Breadcrumb'); + + const CheckCrumbsFits = () => { + // 1st check if home fits, if not it become an ellipsis + if (!areCrumbsFit(block)) homeEl.textContent = homeText.ellipsis; + // if still doesn't fit, remove active crumb + if (!areCrumbsFit(block)) { + crumbs.at(-1).textContent = ''; + } + // if it still doesn't fit again, remove the crumbs from the middle + if (!areCrumbsFit(block)) { + let i = 1; + while (i < crumbs.length - 1 && !areCrumbsFit(block)) { + crumbs[i].textContent = ''; + i += 1; + } + } + }; + + const rObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + if (entry.contentBoxSize) { + // add again the content from each item and check if it fits again or not + homeEl.textContent = homeText.home; + crumbs.forEach((crumb, i) => { + if (i === 0) return; + crumb.textContent = crumb.dataset.content; + crumb.prepend(newSeparator()); + }); + CheckCrumbsFits(); + } + }); + }); + + const mObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // check if the attribute data-section-status has the value 'loaded' + if (mutation.attributeName !== sectionStatus) return; + const section = mutation.target; + const status = section.getAttribute(sectionStatus); + if (status !== 'loaded') return; + CheckCrumbsFits(); + rObserver.observe(block); + mObserver.disconnect(); + }); + }); + mObserver.observe(block.closest('.section'), { + childList: true, attributeFilter: [sectionStatus], + }); +}