UNPKG

@codegouvfr/react-dsfr

Version:

French State Design System React integration library

1,821 lines (1,492 loc) 78.2 kB
/*! DSFR v1.13.2 | SPDX-License-Identifier: MIT | License-Filename: LICENSE.md | restricted use (see terms and conditions) */ const config = { prefix: 'fr', namespace: 'dsfr', organisation: '@gouvfr', version: '1.13.2' }; const api = window[config.namespace]; const ACCORDION = api.internals.ns.selector('accordion'); const COLLAPSE$2 = api.internals.ns.selector('collapse'); const AccordionSelector = { GROUP: api.internals.ns.selector('accordions-group'), ACCORDION: ACCORDION, COLLAPSE: `${ACCORDION} > ${COLLAPSE$2}, ${ACCORDION} > *:not(${ACCORDION}):not(${COLLAPSE$2}) > ${COLLAPSE$2}, ${ACCORDION} > *:not(${ACCORDION}):not(${COLLAPSE$2}) > *:not(${ACCORDION}):not(${COLLAPSE$2}) > ${COLLAPSE$2}`, COLLAPSE_LEGACY: `${ACCORDION} ${COLLAPSE$2}`, BUTTON: `${ACCORDION}__btn` }; class Accordion extends api.core.Instance { static get instanceClassName () { return 'Accordion'; } get collapsePrimary () { const buttons = this.element.children.map(child => child.getInstance('CollapseButton')).filter(button => button !== null && button.hasClass(AccordionSelector.BUTTON)); return buttons[0]; } } class AccordionsGroup extends api.core.CollapsesGroup { static get instanceClassName () { return 'AccordionsGroup'; } validate (member) { const match = member.node.matches(api.internals.legacy.isLegacy ? AccordionSelector.COLLAPSE_LEGACY : AccordionSelector.COLLAPSE); return super.validate(member) && match; } } api.accordion = { Accordion: Accordion, AccordionSelector: AccordionSelector, AccordionsGroup: AccordionsGroup }; api.internals.register(api.accordion.AccordionSelector.GROUP, api.accordion.AccordionsGroup); api.internals.register(api.accordion.AccordionSelector.ACCORDION, api.accordion.Accordion); const ButtonSelector = { EQUISIZED_BUTTON: `${api.internals.ns.selector('btns-group--equisized')} ${api.internals.ns.selector('btn')}`, EQUISIZED_GROUP: api.internals.ns.selector('btns-group--equisized') }; api.button = { ButtonSelector: ButtonSelector }; api.internals.register(api.button.ButtonSelector.EQUISIZED_BUTTON, api.core.Equisized); api.internals.register(api.button.ButtonSelector.EQUISIZED_GROUP, api.core.EquisizedsGroup); class CardDownload extends api.core.Instance { static get instanceClassName () { return 'CardDownload'; } init () { this.addAscent(api.core.AssessEmission.UPDATE, details => { this.descend(api.core.AssessEmission.UPDATE, details); }); this.addAscent(api.core.AssessEmission.ADDED, () => { this.descend(api.core.AssessEmission.ADDED); }); } } const CardSelector = { DOWNLOAD: api.internals.ns.selector('card--download'), DOWNLOAD_DETAIL: `${api.internals.ns.selector('card--download')} ${api.internals.ns.selector('card__end')} ${api.internals.ns.selector('card__detail')}` }; api.card = { CardSelector: CardSelector, CardDownload: CardDownload }; api.internals.register(api.card.CardSelector.DOWNLOAD, api.card.CardDownload); api.internals.register(api.card.CardSelector.DOWNLOAD_DETAIL, api.core.AssessDetail); const CheckboxSelector = { INPUT: `${api.internals.ns.selector('checkbox-group')} input[type="checkbox"]` }; const CheckboxEmission = { CHANGE: api.internals.ns.emission('checkbox', 'change'), RETRIEVE: api.internals.ns.emission('checkbox', 'retrieve') }; class CheckboxInput extends api.core.Instance { static get instanceClassName () { return 'CheckboxInput'; } constructor () { super(); this._handlingChange = this.handleChange.bind(this); } init () { this.node.addEventListener('change', this._handlingChange); this.addDescent(CheckboxEmission.RETRIEVE, this._handlingChange); this.handleChange(); } get isChecked () { return this.node.checked; } handleChange () { this.ascend(CheckboxEmission.CHANGE, this.node); } } api.checkbox = { CheckboxSelector: CheckboxSelector, CheckboxEmission: CheckboxEmission, CheckboxInput: CheckboxInput }; api.internals.register(api.checkbox.CheckboxSelector.INPUT, api.checkbox.CheckboxInput); const SegmentedSelector = { SEGMENTED: api.internals.ns.selector('segmented'), SEGMENTED_ELEMENTS: api.internals.ns.selector('segmented__elements'), SEGMENTED_ELEMENT: api.internals.ns.selector('segmented__element input'), SEGMENTED_LEGEND: api.internals.ns.selector('segmented__legend') }; const SegmentedEmission = { ADDED: api.internals.ns.emission('segmented', 'added'), REMOVED: api.internals.ns.emission('segmented', 'removed') }; class Segmented extends api.core.Instance { static get instanceClassName () { return 'Segmented'; } init () { this.elements = this.node.querySelector(SegmentedSelector.SEGMENTED_ELEMENTS); this.legend = this.node.querySelector(SegmentedSelector.SEGMENTED_LEGEND); this.addAscent(SegmentedEmission.ADDED, this.resize.bind(this)); this.addAscent(SegmentedEmission.REMOVED, this.resize.bind(this)); this._isLegendInline = this.legend && this.legend.classList.contains(`${api.prefix}-segmented__legend--inline`); this.isResizing = true; } resize () { const SEGMENTED_VERTICAL = `${api.prefix}-segmented--vertical`; const LEGEND_INLINE = `${api.prefix}-segmented__legend--inline`; const gapOffset = 16; this.removeClass(SEGMENTED_VERTICAL); if (this._isLegendInline) { this.legend.classList.add(LEGEND_INLINE); if (this.node.offsetWidth > this.node.parentNode.offsetWidth || (this.elements.scrollWidth + this.legend.offsetWidth + gapOffset) > this.node.parentNode.offsetWidth) { this.legend.classList.remove(LEGEND_INLINE); } } if (this.elements.offsetWidth > this.node.parentNode.offsetWidth || this.elements.scrollWidth > this.node.parentNode.offsetWidth) { this.addClass(SEGMENTED_VERTICAL); } else { this.removeClass(SEGMENTED_VERTICAL); } } } class SegmentedElement extends api.core.Instance { static get instanceClassName () { return 'SegmentedElement'; } init () { this.ascend(SegmentedEmission.ADDED); } dispose () { this.ascend(SegmentedEmission.REMOVED); } } api.segmented = { SegmentedSelector: SegmentedSelector, SegmentedEmission: SegmentedEmission, SegmentedElement: SegmentedElement, Segmented: Segmented }; api.internals.register(api.segmented.SegmentedSelector.SEGMENTED, api.segmented.Segmented); api.internals.register(api.segmented.SegmentedSelector.SEGMENTED_ELEMENT, api.segmented.SegmentedElement); const BreadcrumbSelector = { BREADCRUMB: api.internals.ns.selector('breadcrumb'), BUTTON: api.internals.ns.selector('breadcrumb__button') }; class Breadcrumb extends api.core.Instance { constructor () { super(); this.count = 0; this.focusing = this.focus.bind(this); } static get instanceClassName () { return 'Breadcrumb'; } init () { this.getCollapse(); this.isResizing = true; } get proxy () { const scope = this; return Object.assign(super.proxy, { focus: scope.focus.bind(scope), disclose: scope.collapse.disclose.bind(scope.collapse) }); } getCollapse () { const collapse = this.collapse; if (collapse) { collapse.listen(api.core.DisclosureEvent.DISCLOSE, this.focusing); } else { this.addAscent(api.core.DisclosureEmission.ADDED, this.getCollapse.bind(this)); } } resize () { const collapse = this.collapse; const links = this.links; if (!collapse || !links.length) return; if (this.isBreakpoint(api.core.Breakpoints.MD)) { if (collapse.buttonHasFocus) links[0].focus(); } else { if (links.indexOf(document.activeElement) > -1) collapse.focus(); } } get links () { return [...this.querySelectorAll('a[href]')]; } get collapse () { return this.element.getDescendantInstances(api.core.Collapse.instanceClassName, null, true)[0]; } focus () { this.count = 0; this._focus(); } _focus () { const link = this.links[0]; if (!link) return; link.focus(); this.request(this.verify.bind(this)); } verify () { this.count++; if (this.count > 100) return; const link = this.links[0]; if (!link) return; if (document.activeElement !== link) this._focus(); } get collapsePrimary () { const buttons = this.element.children.map(child => child.getInstance('CollapseButton')).filter(button => button !== null && button.hasClass(BreadcrumbSelector.BUTTON)); return buttons[0]; } } api.breadcrumb = { BreadcrumbSelector: BreadcrumbSelector, Breadcrumb: Breadcrumb }; api.internals.register(api.breadcrumb.BreadcrumbSelector.BREADCRUMB, api.breadcrumb.Breadcrumb); const TooltipSelector = { TOOLTIP: api.internals.ns.selector('tooltip'), SHOWN: api.internals.ns.selector('tooltip--shown'), HIDDING: api.internals.ns.selector('tooltip--hidding'), BUTTON: api.internals.ns.selector('btn--tooltip') }; const TooltipReferentState = { FOCUS: 1 << 0, HOVER: 1 << 1 }; class TooltipReferent extends api.core.PlacementReferent { constructor () { super(); this._state = 0; } static get instanceClassName () { return 'TooltipReferent'; } init () { super.init(); this.listen('focusin', this.focusIn.bind(this)); this.listen('focusout', this.focusOut.bind(this)); if (!this.matches(TooltipSelector.BUTTON)) { const mouseover = this.mouseover.bind(this); this.listen('mouseover', mouseover); this.placement.listen('mouseover', mouseover); const mouseout = this.mouseout.bind(this); this.listen('mouseout', mouseout); this.placement.listen('mouseout', mouseout); } this.addEmission(api.core.RootEmission.KEYDOWN, this._keydown.bind(this)); this.listen('click', this._click.bind(this)); this.addEmission(api.core.RootEmission.CLICK, this._clickOut.bind(this)); } _click () { this.focus(); } _clickOut (target) { if (!this.node.contains(target)) this.blur(); } _keydown (keyCode) { switch (keyCode) { case api.core.KeyCodes.ESCAPE: this.blur(); this.close(); break; } } close () { this.state = 0; } get state () { return this._state; } set state (value) { if (this._state === value) return; this.isShown = value > 0; this._state = value; } focusIn () { this.state |= TooltipReferentState.FOCUS; } focusOut () { this.state &= ~TooltipReferentState.FOCUS; } mouseover () { this.state |= TooltipReferentState.HOVER; } mouseout () { this.state &= ~TooltipReferentState.HOVER; } } const ns = name => `${config.prefix}-${name}`; ns.selector = (name, notation) => { if (notation === undefined) notation = '.'; return `${notation}${ns(name)}`; }; ns.attr = (name) => `data-${ns(name)}`; ns.attr.selector = (name, value) => { let result = ns.attr(name); if (value !== undefined) result += `="${value}"`; return `[${result}]`; }; ns.event = (type) => `${config.namespace}.${type}`; ns.emission = (domain, type) => `emission:${domain}.${type}`; const TooltipEvent = { SHOW: ns.event('show'), HIDE: ns.event('hide') }; const TooltipState = { HIDDEN: 'hidden', SHOWN: 'shown', HIDING: 'hiding' }; class Tooltip extends api.core.Placement { constructor () { super(api.core.PlacementMode.AUTO, [api.core.PlacementPosition.TOP, api.core.PlacementPosition.BOTTOM], [api.core.PlacementAlign.CENTER, api.core.PlacementAlign.START, api.core.PlacementAlign.END]); this.modifier = ''; this._state = TooltipState.HIDDEN; } static get instanceClassName () { return 'Tooltip'; } init () { super.init(); this.register(`[aria-describedby="${this.id}"]`, TooltipReferent); this.listen('transitionend', this.transitionEnd.bind(this)); } transitionEnd () { if (this._state === TooltipState.HIDING) { this.removeClass(TooltipSelector.SHOWN); this.removeClass(TooltipSelector.HIDDING); this._state = TooltipState.HIDDEN; this.isShown = false; } } get isShown () { return super.isShown; } set isShown (value) { if (!this.isEnabled) return; switch (true) { case value: this._state = TooltipState.SHOWN; this.addClass(TooltipSelector.SHOWN); this.removeClass(TooltipSelector.HIDDING); this.dispatch(TooltipEvent.SHOW); super.isShown = true; break; case this.isShown && !value && this._state === TooltipState.SHOWN: this._state = TooltipState.HIDING; this.addClass(TooltipSelector.HIDDING); break; case this.isShown && !value && this._state === TooltipState.HIDDEN: this.dispatch(TooltipEvent.HIDE); this.removeClass(TooltipSelector.HIDDING); super.isShown = false; break; } } render () { super.render(); this.rect = this.getRect(); let x = this.referentRect.center - this.rect.center; const limit = this.rect.width * 0.5 - 8; if (x < -limit) x = -limit; if (x > limit) x = limit; this.setProperty('--arrow-x', `${x.toFixed(2)}px`); } } api.tooltip = { Tooltip: Tooltip, TooltipSelector: TooltipSelector, TooltipEvent: TooltipEvent }; api.internals.register(api.tooltip.TooltipSelector.TOOLTIP, api.tooltip.Tooltip); class ToggleInput extends api.core.Instance { static get instanceClassName () { return 'ToggleInput'; } get isChecked () { return this.node.checked; } } class ToggleStatusLabel extends api.core.Instance { static get instanceClassName () { return 'ToggleStatusLabel'; } init () { this.register(`input[id="${this.getAttribute('for')}"]`, ToggleInput); this.update(); this.isSwappingFont = true; } get proxy () { const scope = this; return Object.assign(super.proxy, { update: scope.update.bind(scope) }); } get input () { return this.getRegisteredInstances('ToggleInput')[0]; } update () { this.node.style.removeProperty('--toggle-status-width'); const checked = this.input.isChecked; const style = getComputedStyle(this.node, ':before'); let maxWidth = parseFloat(style.width); this.input.node.checked = !checked; const style2 = getComputedStyle(this.node, ':before'); const width = parseFloat(style2.width); if (width > maxWidth) maxWidth = width; this.input.node.checked = checked; this.node.style.setProperty('--toggle-status-width', (maxWidth / 16) + 'rem'); } swapFont (families) { this.update(); } } const ToggleSelector = { STATUS_LABEL: `${api.internals.ns.selector('toggle__label')}${api.internals.ns.attr.selector('checked-label')}${api.internals.ns.attr.selector('unchecked-label')}` }; // import { ToggleInput } from './script/toggle/toggle-input.js'; api.toggle = { ToggleStatusLabel: ToggleStatusLabel, ToggleSelector: ToggleSelector }; api.internals.register(api.toggle.ToggleSelector.STATUS_LABEL, api.toggle.ToggleStatusLabel); const ITEM$1 = api.internals.ns.selector('sidemenu__item'); const COLLAPSE$1 = api.internals.ns.selector('collapse'); const SidemenuSelector = { LIST: api.internals.ns.selector('sidemenu__list'), COLLAPSE: `${ITEM$1} > ${COLLAPSE$1}, ${ITEM$1} > *:not(${ITEM$1}):not(${COLLAPSE$1}) > ${COLLAPSE$1}, ${ITEM$1} > *:not(${ITEM$1}):not(${COLLAPSE$1}) > *:not(${ITEM$1}):not(${COLLAPSE$1}) > ${COLLAPSE$1}`, COLLAPSE_LEGACY: `${ITEM$1} ${COLLAPSE$1}`, ITEM: api.internals.ns.selector('sidemenu__item'), BUTTON: api.internals.ns.selector('sidemenu__btn') }; class SidemenuList extends api.core.CollapsesGroup { static get instanceClassName () { return 'SidemenuList'; } validate (member) { return super.validate(member) && member.node.matches(api.internals.legacy.isLegacy ? SidemenuSelector.COLLAPSE_LEGACY : SidemenuSelector.COLLAPSE); } } class SidemenuItem extends api.core.Instance { static get instanceClassName () { return 'SidemenuItem'; } get collapsePrimary () { const buttons = this.element.children.map(child => child.getInstance('CollapseButton')).filter(button => button !== null && button.hasClass(SidemenuSelector.BUTTON)); return buttons[0]; } } api.sidemenu = { SidemenuList: SidemenuList, SidemenuItem: SidemenuItem, SidemenuSelector: SidemenuSelector }; api.internals.register(api.sidemenu.SidemenuSelector.LIST, api.sidemenu.SidemenuList); api.internals.register(api.sidemenu.SidemenuSelector.ITEM, api.sidemenu.SidemenuItem); const ModalSelector = { MODAL: api.internals.ns.selector('modal'), SCROLL_DIVIDER: api.internals.ns.selector('scroll-divider'), BODY: api.internals.ns.selector('modal__body'), TITLE: api.internals.ns.selector('modal__title') }; class ModalButton extends api.core.DisclosureButton { constructor () { super(api.core.DisclosureType.OPENED); } static get instanceClassName () { return 'ModalButton'; } } const ModalAttribute = { CONCEALING_BACKDROP: api.internals.ns.attr('concealing-backdrop') }; class Modal extends api.core.Disclosure { constructor () { super(api.core.DisclosureType.OPENED, ModalSelector.MODAL, ModalButton, 'ModalsGroup'); this._isDecorated = false; this.scrolling = this.resize.bind(this, false); this.resizing = this.resize.bind(this, true); } static get instanceClassName () { return 'Modal'; } init () { super.init(); this._isDialog = this.node.tagName === 'DIALOG'; this.listenClick(); this.addEmission(api.core.RootEmission.KEYDOWN, this._keydown.bind(this)); } _keydown (keyCode) { switch (keyCode) { case api.core.KeyCodes.ESCAPE: this._escape(); break; } } // TODO v2 : passer les tagName d'action en constante _escape () { const tagName = document.activeElement ? document.activeElement.tagName : undefined; switch (tagName) { case 'INPUT': case 'LABEL': case 'TEXTAREA': case 'SELECT': case 'AUDIO': case 'VIDEO': break; default: if (this.isDisclosed) { this.conceal(); this.focus(); } } } retrieved () { this._ensureAccessibleName(); } get body () { return this.element.getDescendantInstances('ModalBody', 'Modal')[0]; } handleClick (e) { if (e.target === this.node && this.getAttribute(ModalAttribute.CONCEALING_BACKDROP) !== 'false') this.conceal(); } disclose (withhold) { if (!super.disclose(withhold)) return false; if (this.body) { this.body.isResizing = true; this.body.resize(); } this.isScrollLocked = true; this.setAttribute('aria-modal', 'true'); this.setAttribute('open', 'true'); if (!this._isDialog) { this.decorateDialog(); } return true; } conceal (withhold, preventFocus) { if (!super.conceal(withhold, preventFocus)) return false; this.isScrollLocked = false; this.removeAttribute('aria-modal'); this.removeAttribute('open'); if (this.body) this.body.isResizing = false; if (!this._isDialog) { this.stripDialog(); } return true; } get isDialog () { return this._isDialog; } set isDialog (value) { this._isDialog = value; } get isActive () { return super.isActive; } set isActive (value) { super.isActive = value; if (value) this._ensureAccessibleName(); } decorateDialog () { if (this._isDecorated) return; this._isDecorated = true; this._hasDialogRole = this.getAttribute('role') === 'dialog'; if (!this._hasDialogRole) this.setAttribute('role', 'dialog'); } stripDialog () { if (!this._isDecorated) return; this._isDecorated = false; if (!this._hasDialogRole) this.removeAttribute('role'); } _setAccessibleName (node, append) { const id = this.retrieveNodeId(node, append); this.warn(`add reference to ${append} for accessible name (aria-labelledby)`); this.setAttribute('aria-labelledby', id); } _ensureAccessibleName () { if (!this.isActive || !this.isEnabled || (this.isEnabled && (this.hasAttribute('aria-labelledby') || this.hasAttribute('aria-label')))) return; this.warn('missing accessible name'); const title = this.node.querySelector(ModalSelector.TITLE); const primary = this.primaryButtons[0]; switch (true) { case title !== null: this._setAccessibleName(title, 'title'); break; case primary !== undefined: this.warn('missing required title, fallback to primary button'); this._setAccessibleName(primary, 'primary'); break; } } } const unordereds = [ '[tabindex="0"]', 'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', 'details>summary:first-of-type', 'details', 'iframe' ]; const UNORDEREDS = unordereds.join(); const ordereds = [ '[tabindex]:not([tabindex="-1"]):not([tabindex="0"])' ]; const ORDEREDS = ordereds.join(); const isFocusable = (element, container) => { if (!(element instanceof Element)) return false; const style = window.getComputedStyle(element); if (!style) return false; if (style.visibility === 'hidden') return false; if (container === undefined) container = element; while (container.contains(element)) { if (style.display === 'none') return false; element = element.parentElement; } return true; }; class FocusTrap { constructor (onTrap, onUntrap) { this.element = null; this.activeElement = null; this.onTrap = onTrap; this.onUntrap = onUntrap; this.waiting = this.wait.bind(this); this.handling = this.handle.bind(this); this.focusing = this.maintainFocus.bind(this); this.current = null; } get trapped () { return this.element !== null; } trap (element) { if (this.trapped) this.untrap(); this.element = element; this.isTrapping = true; this.wait(); if (this.onTrap) this.onTrap(); } wait () { if (!isFocusable(this.element)) { window.requestAnimationFrame(this.waiting); return; } this.trapping(); } trapping () { if (!this.isTrapping) return; this.isTrapping = false; const focusables = this.focusables; if (focusables.length && focusables.indexOf(document.activeElement) === -1) focusables[0].focus(); this.element.setAttribute('aria-modal', true); window.addEventListener('keydown', this.handling); document.body.addEventListener('focus', this.focusing, true); } stun (node) { for (const child of node.children) { if (child === this.element) continue; if (child.contains(this.element)) { this.stun(child); continue; } this.stunneds.push(new Stunned(child)); } } maintainFocus (event) { if (!this.element.contains(event.target)) { const focusables = this.focusables; if (focusables.length === 0) return; const first = focusables[0]; event.preventDefault(); first.focus(); } } handle (e) { if (e.keyCode !== 9) return; const focusables = this.focusables; if (focusables.length === 0) return; const first = focusables[0]; const last = focusables[focusables.length - 1]; const index = focusables.indexOf(document.activeElement); if (e.shiftKey) { if (!this.element.contains(document.activeElement) || index < 1) { e.preventDefault(); last.focus(); } else if (document.activeElement.tabIndex > 0 || focusables[index - 1].tabIndex > 0) { e.preventDefault(); focusables[index - 1].focus(); } } else { if (!this.element.contains(document.activeElement) || index === focusables.length - 1 || index === -1) { e.preventDefault(); first.focus(); } else if (document.activeElement.tabIndex > 0) { e.preventDefault(); focusables[index + 1].focus(); } } } get focusables () { let unordereds = api.internals.dom.querySelectorAllArray(this.element, UNORDEREDS); /** * filtrage des radiobutttons de même name (la navigations d'un groupe de radio se fait à la flèche et non pas au tab **/ const radios = api.internals.dom.querySelectorAllArray(document.documentElement, 'input[type="radio"]'); if (radios.length) { const groups = {}; for (const radio of radios) { const name = radio.getAttribute('name'); if (groups[name] === undefined) groups[name] = new RadioButtonGroup(name); groups[name].push(radio); } unordereds = unordereds.filter((unordered) => { if (unordered.tagName.toLowerCase() !== 'input' || (unordered.getAttribute('type') && unordered.getAttribute('type').toLowerCase() !== 'radio')) return true; const name = unordered.getAttribute('name'); return groups[name].keep(unordered); }); } const ordereds = api.internals.dom.querySelectorAllArray(this.element, ORDEREDS); ordereds.sort((a, b) => a.tabIndex - b.tabIndex); const noDuplicates = unordereds.filter((element) => ordereds.indexOf(element) === -1); const concateneds = ordereds.concat(noDuplicates); return concateneds.filter((element) => element.tabIndex !== '-1' && isFocusable(element, this.element)); } untrap () { if (!this.trapped) return; this.isTrapping = false; this.element.removeAttribute('aria-modal'); window.removeEventListener('keydown', this.handling); document.body.removeEventListener('focus', this.focusing, true); this.element = null; if (this.onUntrap) this.onUntrap(); } dispose () { this.untrap(); } } class Stunned { constructor (element) { this.element = element; // this.hidden = element.getAttribute('aria-hidden'); this.inert = element.getAttribute('inert'); // this.element.setAttribute('aria-hidden', true); this.element.setAttribute('inert', ''); } unstun () { /* if (this.hidden === null) this.element.removeAttribute('aria-hidden'); else this.element.setAttribute('aria-hidden', this.hidden); */ if (this.inert === null) this.element.removeAttribute('inert'); else this.element.setAttribute('inert', this.inert); } } class RadioButtonGroup { constructor (name) { this.name = name; this.buttons = []; } push (button) { this.buttons.push(button); if (button === document.activeElement || button.checked || this.selected === undefined) this.selected = button; } keep (button) { return this.selected === button; } } class ModalsGroup extends api.core.DisclosuresGroup { constructor () { super('Modal', false); this.focusTrap = new FocusTrap(); } static get instanceClassName () { return 'ModalsGroup'; } apply (value, initial) { super.apply(value, initial); if (this.current === null) this.focusTrap.untrap(); else this.focusTrap.trap(this.current.node); } } const OFFSET = 32; // 32px => 8v => 2rem class ModalBody extends api.core.Instance { static get instanceClassName () { return 'ModalBody'; } init () { this.listen('scroll', this.divide.bind(this)); } divide () { if (this.node.scrollHeight > this.node.clientHeight) { if (this.node.offsetHeight + this.node.scrollTop >= this.node.scrollHeight) { this.removeClass(ModalSelector.SCROLL_DIVIDER); } else { this.addClass(ModalSelector.SCROLL_DIVIDER); } } else { this.removeClass(ModalSelector.SCROLL_DIVIDER); } } resize () { this.adjust(); this.request(this.adjust.bind(this)); } adjust () { const offset = OFFSET * (this.isBreakpoint(api.core.Breakpoints.MD) ? 2 : 1); if (this.isLegacy) this.style.maxHeight = `${window.innerHeight - offset}px`; else this.style.setProperty('--modal-max-height', `${window.innerHeight - offset}px`); this.divide(); } } api.modal = { Modal: Modal, ModalButton: ModalButton, ModalBody: ModalBody, ModalsGroup: ModalsGroup, ModalSelector: ModalSelector }; api.internals.register(api.modal.ModalSelector.MODAL, api.modal.Modal); api.internals.register(api.modal.ModalSelector.BODY, api.modal.ModalBody); api.internals.register(api.core.RootSelector.ROOT, api.modal.ModalsGroup); const PasswordEmission = { TOGGLE: api.internals.ns.emission('password', 'toggle'), ADJUST: api.internals.ns.emission('password', 'adjust') }; class PasswordToggle extends api.core.Instance { static get instanceClassName () { return 'PasswordToggle'; } init () { this.listenClick(); this.ascend(PasswordEmission.ADJUST, this.width); this.isSwappingFont = true; this._isChecked = this.isChecked; } get width () { const style = getComputedStyle(this.node.parentNode); return parseInt(style.width); } get isChecked () { return this.node.checked; } set isChecked (value) { this._isChecked = value; this.ascend(PasswordEmission.TOGGLE, value); } handleClick () { this.isChecked = !this._isChecked; } swapFont (families) { this.ascend(PasswordEmission.ADJUST, this.width); } } class Password extends api.core.Instance { static get instanceClassName () { return 'Password'; } init () { this.addAscent(PasswordEmission.TOGGLE, this.toggle.bind(this)); this.addAscent(PasswordEmission.ADJUST, this.adjust.bind(this)); } toggle (value) { this.descend(PasswordEmission.TOGGLE, value); } adjust (value) { this.descend(PasswordEmission.ADJUST, value); } } const PasswordSelector = { PASSWORD: api.internals.ns.selector('password'), INPUT: api.internals.ns.selector('password__input'), LABEL: api.internals.ns.selector('password__label'), TOOGLE: `${api.internals.ns.selector('password__checkbox')} input[type="checkbox"]` }; class PasswordInput extends api.core.Instance { static get instanceClassName () { return 'PasswordInput'; } init () { this.addDescent(PasswordEmission.TOGGLE, this.toggle.bind(this)); this._isRevealed = this.hasAttribute('type') === 'password'; this.listen('keydown', this.capslock.bind(this)); // for capslock enabled this.listen('keyup', this.capslock.bind(this)); // for capslock desabled } toggle (value) { this.isRevealed = value; this.setAttribute('type', value ? 'text' : 'password'); } get isRevealed () { return this._isRevealed; } capslock (event) { if (event && typeof event.getModifierState !== 'function') return; if (event.getModifierState('CapsLock')) { this.node.parentNode.setAttribute(api.internals.ns.attr('capslock'), ''); } else { this.node.parentNode.removeAttribute(api.internals.ns.attr('capslock')); } } set isRevealed (value) { this._isRevealed = value; this.setAttribute('type', value ? 'text' : 'password'); } } class PasswordLabel extends api.core.Instance { static get instanceClassName () { return 'PasswordLabel'; } init () { this.addDescent(PasswordEmission.ADJUST, this.adjust.bind(this)); } adjust (value) { const valueREM = Math.ceil(value / 16); this.node.style.paddingRight = valueREM + 'rem'; } } api.password = { Password: Password, PasswordToggle: PasswordToggle, PasswordSelector: PasswordSelector, PasswordInput: PasswordInput, PasswordLabel: PasswordLabel }; api.internals.register(api.password.PasswordSelector.INPUT, api.password.PasswordInput); api.internals.register(api.password.PasswordSelector.PASSWORD, api.password.Password); api.internals.register(api.password.PasswordSelector.TOOGLE, api.password.PasswordToggle); api.internals.register(api.password.PasswordSelector.LABEL, api.password.PasswordLabel); const ITEM = api.internals.ns.selector('nav__item'); const COLLAPSE = api.internals.ns.selector('collapse'); const NavigationSelector = { NAVIGATION: api.internals.ns.selector('nav'), COLLAPSE: `${ITEM} > ${COLLAPSE}, ${ITEM} > *:not(${ITEM}):not(${COLLAPSE}) > ${COLLAPSE}, ${ITEM} > *:not(${ITEM}):not(${COLLAPSE}) > *:not(${ITEM}):not(${COLLAPSE}) > ${COLLAPSE}`, COLLAPSE_LEGACY: `${ITEM} ${COLLAPSE}`, ITEM: ITEM, ITEM_RIGHT: `${ITEM}--align-right`, MENU: api.internals.ns.selector('menu'), BUTTON: api.internals.ns.selector('nav__btn'), TRANSLATE_BUTTON: api.internals.ns.selector('translate__btn') }; class NavigationItem extends api.core.Instance { constructor () { super(); this._isRightAligned = false; } static get instanceClassName () { return 'NavigationItem'; } init () { this.addAscent(api.core.DisclosureEmission.ADDED, this.calculate.bind(this)); this.addAscent(api.core.DisclosureEmission.REMOVED, this.calculate.bind(this)); this.isResizing = true; this.calculate(); } resize () { this.calculate(); } calculate () { const collapse = this.element.getDescendantInstances(api.core.Collapse.instanceClassName, null, true)[0]; if (collapse && this.isBreakpoint(api.core.Breakpoints.LG) && collapse.element.node.matches(NavigationSelector.MENU)) { const right = this.element.node.parentElement.getBoundingClientRect().right; // todo: ne fonctionne que si la nav fait 100% du container const width = collapse.element.node.getBoundingClientRect().width; const left = this.element.node.getBoundingClientRect().left; this.isRightAligned = left + width > right; } else this.isRightAligned = false; } get isRightAligned () { return this._isRightAligned; } set isRightAligned (value) { if (this._isRightAligned === value) return; this._isRightAligned = value; if (value) api.internals.dom.addClass(this.element.node, NavigationSelector.ITEM_RIGHT); else api.internals.dom.removeClass(this.element.node, NavigationSelector.ITEM_RIGHT); } get collapsePrimary () { const buttons = this.element.children.map(child => child.getInstance('CollapseButton')).filter(button => button !== null && (button.hasClass(NavigationSelector.BUTTON) || button.hasClass(NavigationSelector.TRANSLATE_BUTTON))); return buttons[0]; } } const NavigationMousePosition = { NONE: -1, INSIDE: 0, OUTSIDE: 1 }; class Navigation extends api.core.CollapsesGroup { static get instanceClassName () { return 'Navigation'; } init () { super.init(); this.clicked = false; this.out = false; this.addEmission(api.core.RootEmission.CLICK, this._handleRootClick.bind(this)); this.listen('mousedown', this.handleMouseDown.bind(this)); this.addEmission(api.core.RootEmission.KEYDOWN, this._keydown.bind(this)); this.listenClick({ capture: true }); this.isResizing = true; } validate (member) { return super.validate(member) && member.element.node.matches(api.internals.legacy.isLegacy ? NavigationSelector.COLLAPSE_LEGACY : NavigationSelector.COLLAPSE); } get hasOpenedMenu () { return this.isBreakpoint(api.core.Breakpoints.LG) && this.index > -1; } _keydown (keyCode) { switch (keyCode) { case api.core.KeyCodes.ESCAPE: if (!this.hasOpenedMenu) return; this.index = -1; break; case api.core.KeyCodes.TAB: if (!this.hasOpenedMenu) return; this.request(() => { if (this.current.node.contains(document.activeElement)) return; this.index = -1; }); break; } } handleMouseDown (e) { if (!this.hasOpenedMenu) return; this.position = this.current.node.contains(e.target) ? NavigationMousePosition.INSIDE : NavigationMousePosition.OUTSIDE; this.requestPosition(); } handleClick (e) { if (e.target.matches('a, button') && !e.target.matches('[aria-controls]') && !e.target.matches(api.core.DisclosureSelector.PREVENT_CONCEAL)) { this.index = -1; } } _handleRootClick (target) { if (!this.isBreakpoint(api.core.Breakpoints.LG)) return; if (!this.node.contains(target)) { this.out = true; this.requestPosition(); } } requestPosition () { if (this.isRequesting) return; this.isRequesting = true; this.request(this.getPosition.bind(this)); } getPosition () { if (this.out) { switch (this.position) { case NavigationMousePosition.OUTSIDE: this.index = -1; break; case NavigationMousePosition.INSIDE: if (this.current && !this.current.node.contains(document.activeElement)) this.current.focus(); break; default: if (this.index > -1 && !this.current.hasFocus) this.index = -1; } } this.request(this.requested.bind(this)); } requested () { this.position = NavigationMousePosition.NONE; this.out = false; this.isRequesting = false; } get index () { return super.index; } set index (value) { if (value === -1 && this.current && this.current.hasFocus) this.current.focus(); super.index = value; } get canUngroup () { return !this.isBreakpoint(api.core.Breakpoints.LG); } resize () { this.update(); } } api.navigation = { Navigation: Navigation, NavigationItem: NavigationItem, NavigationMousePosition: NavigationMousePosition, NavigationSelector: NavigationSelector }; api.internals.register(api.navigation.NavigationSelector.NAVIGATION, api.navigation.Navigation); api.internals.register(api.navigation.NavigationSelector.ITEM, api.navigation.NavigationItem); /** * TabButton correspond au bouton cliquable qui change le panel * TabButton étend de DisclosureButton qui ajoute/enelve l'attribut aria-selected, * Et change l'attributte tabindex a 0 si le boutton est actif (value=true), -1 s'il n'est pas actif (value=false) */ class TabButton extends api.core.DisclosureButton { constructor () { super(api.core.DisclosureType.SELECT); } static get instanceClassName () { return 'TabButton'; } handleClick (e) { super.handleClick(e); this.focus(); } apply (value) { super.apply(value); if (this.isPrimary) { this.setAttribute('tabindex', value ? '0' : '-1'); if (value) { if (this.list) this.list.focalize(this); } } } get list () { return this.element.getAscendantInstance('TabsList', 'TabsGroup'); } } const TabSelector = { TAB: api.internals.ns.selector('tabs__tab'), GROUP: api.internals.ns.selector('tabs'), PANEL: api.internals.ns.selector('tabs__panel'), LIST: api.internals.ns.selector('tabs__list'), SHADOW: api.internals.ns.selector('tabs__shadow'), SHADOW_LEFT: api.internals.ns.selector('tabs__shadow--left'), SHADOW_RIGHT: api.internals.ns.selector('tabs__shadow--right'), PANEL_START: api.internals.ns.selector('tabs__panel--direction-start'), PANEL_END: api.internals.ns.selector('tabs__panel--direction-end') }; const TabPanelDirection = { START: 'direction-start', END: 'direction-end', NONE: 'none' }; /** * Tab coorespond au panel d'un élement Tabs (tab panel) * Tab étend disclosure qui ajoute/enleve le modifier --selected, * et ajoute/eleve l'attribut hidden, sur le panel */ class TabPanel extends api.core.Disclosure { constructor () { super(api.core.DisclosureType.SELECT, TabSelector.PANEL, TabButton, 'TabsGroup'); this._direction = TabPanelDirection.NONE; this._isPreventingTransition = false; } static get instanceClassName () { return 'TabPanel'; } get direction () { return this._direction; } set direction (value) { if (value === this._direction) return; switch (this._direction) { case TabPanelDirection.START: this.removeClass(TabSelector.PANEL_START); break; case TabPanelDirection.END: this.removeClass(TabSelector.PANEL_END); break; case TabPanelDirection.NONE: break; default: return; } this._direction = value; switch (this._direction) { case TabPanelDirection.START: this.addClass(TabSelector.PANEL_START); break; case TabPanelDirection.END: this.addClass(TabSelector.PANEL_END); break; } } get isPreventingTransition () { return this._isPreventingTransition; } set isPreventingTransition (value) { if (this._isPreventingTransition === value) return; if (value) this.addClass(api.internals.motion.TransitionSelector.NONE); else this.removeClass(api.internals.motion.TransitionSelector.NONE); this._isPreventingTransition = value === true; } translate (direction, initial) { this.isPreventingTransition = initial; this.direction = direction; } reset () { if (this.group) this.group.retrieve(true); } _electPrimaries (candidates) { if (!this.group || !this.group.list) return []; return super._electPrimaries(candidates).filter(candidate => this.group.list.node.contains(candidate.node)); } } const TabKeys = { LEFT: 'tab_keys_left', RIGHT: 'tab_keys_right', HOME: 'tab_keys_home', END: 'tab_keys_end' }; const TabEmission = { PRESS_KEY: api.internals.ns.emission('tab', 'press_key'), LIST_HEIGHT: api.internals.ns.emission('tab', 'list_height') }; /** * TabGroup est la classe étendue de DiscosuresGroup * Correspond à un objet Tabs avec plusieurs tab-button & Tab (panel) */ class TabsGroup extends api.core.DisclosuresGroup { constructor () { super('TabPanel'); } static get instanceClassName () { return 'TabsGroup'; } init () { super.init(); this.listen('transitionend', this.transitionend.bind(this)); this.addAscent(TabEmission.PRESS_KEY, this.pressKey.bind(this)); this.addAscent(TabEmission.LIST_HEIGHT, this.setListHeight.bind(this)); this.isRendering = true; } getIndex (defaultIndex = 0) { super.getIndex(defaultIndex); } get list () { return this.element.getDescendantInstances('TabsList', 'TabsGroup', true)[0]; } setListHeight (value) { this.listHeight = value; } transitionend (e) { this.isPreventingTransition = true; } get buttonHasFocus () { return this.members.some(member => member.buttonHasFocus); } pressKey (key) { switch (key) { case TabKeys.LEFT: this.pressLeft(); break; case TabKeys.RIGHT: this.pressRight(); break; case TabKeys.HOME: this.pressHome(); break; case TabKeys.END: this.pressEnd(); break; } } /** * Selectionne l'element suivant de la liste si on est sur un bouton * Si on est à la fin on retourne au début */ pressRight () { if (this.buttonHasFocus) { if (this.index < this.length - 1) { this.index++; } else { this.index = 0; } this.focus(); } }; /** * Selectionne l'element précédent de la liste si on est sur un bouton * Si on est au debut retourne a la fin */ pressLeft () { if (this.buttonHasFocus) { if (this.index > 0) { this.index--; } else { this.index = this.length - 1; } this.focus(); } }; /** * Selectionne le permier element de la liste si on est sur un bouton */ pressHome () { if (this.buttonHasFocus) { this.index = 0; this.focus(); } }; /** * Selectionne le dernier element de la liste si on est sur un bouton */ pressEnd () { if (this.buttonHasFocus) { this.index = this.length - 1; this.focus(); } }; focus () { if (this.current) { this.current.focus(); } } apply () { for (let i = 0; i < this._index; i++) this.members[i].translate(TabPanelDirection.START); if (this.current) this.current.translate(TabPanelDirection.NONE); for (let i = this._index + 1; i < this.length; i++) this.members[i].translate(TabPanelDirection.END); this.isPreventingTransition = false; } get isPreventingTransition () { return this._isPreventingTransition; } set isPreventingTransition (value) { if (this._isPreventingTransition === value) return; if (value) this.addClass(api.internals.motion.TransitionSelector.NONE); else this.removeClass(api.internals.motion.TransitionSelector.NONE); this._isPreventingTransition = value === true; } render () { if (this.current === null) return; this.node.scrollTop = 0; this.node.scrollLeft = 0; const paneHeight = Math.round(this.current.node.offsetHeight); if (this.panelHeight === paneHeight) return; this.panelHeight = paneHeight; this.style.setProperty('--tabs-height', (this.panelHeight + this.listHeight) + 'px'); } } const FOCALIZE_OFFSET = 16; const SCROLL_OFFSET$1 = 16; // valeur en px du scroll avant laquelle le shadow s'active ou se desactive class TabsList extends api.core.Instance { static get instanceClassName () { return 'TabsList'; } init () { this.listen('scroll', this.scroll.bind(this)); this.listenKey(api.core.KeyCodes.RIGHT, this.ascend.bind(this, TabEmission.PRESS_KEY, TabKeys.RIGHT), true, true); this.listenKey(api.core.KeyCodes.LEFT, this.ascend.bind(this, TabEmission.PRESS_KEY, TabKeys.LEFT), true, true); this.listenKey(api.core.KeyCodes.HOME, this.ascend.bind(this, TabEmission.PRESS_KEY, TabKeys.HOME), true, true); this.listenKey(api.core.KeyCodes.END, this.ascend.bind(this, TabEmission.PRESS_KEY, TabKeys.END), true, true); this.isResizing = true; } focalize (btn) { const btnRect = btn.getRect(); const listRect = this.getRect(); const actualScroll = this.node.scrollLeft; if (btnRect.left < listRect.left) this.node.scrollTo(actualScroll - listRect.left + btnRect.left - FOCALIZE_OFFSET, 0); else if (btnRect.right > listRect.right) this.node.scrollTo(actualScroll - listRect.right + btnRect.right + FOCALIZE_OFFSET, 0); } get isScrolling () { return this._isScrolling; } set isScrolling (value) { if (this._isScrolling === value) return; this._isScrolling = value; this.apply(); } apply () { if (this._isScrolling) { this.addClass(TabSelector.SHADOW); this.scroll(); } else { this.removeClass(TabSelector.SHADOW_RIGHT); this.removeClass(TabSelector.SHADOW_LEFT); this.removeClass(TabSelector.SHADOW); } } /* ajoute la classe fr-table__shadow-left ou fr-table__shadow-right sur fr-table en fonction d'une valeur de scroll et du sens (right, left) */ scroll () { const scrollLeft = Math.abs(this.node.scrollLeft); const isMin = scrollLeft <= SCROLL_OFFSET$1; const max = this.node.scrollWidth - this.node.clientWidth - SCROLL_OFFSET$1; const isMax = Math.abs(scrollLeft) >= max; const isRtl = getComputedStyle(this.node).direction === 'rtl'; const minSelector = isRtl ? TabSelector.SHADOW_RIGHT : TabSelector.SHADOW_LEFT; const maxSelector = isRtl ? TabSelector.SHADOW_LEFT : TabSelector.SHADOW_RIGHT; if (isMin) { this.removeClass(minSelector); } else { this.addClass(minSelector); } if (isMax) { this.removeClass(maxSelector); } else { this.addClass(maxSelector); } } resize () { this.isScrolling = this.node.scrollWidth > this.node.clientWidth + SCROLL_OFFSET$1; const height = this.getRect().height; this.setProperty('--tabs-list-height', `${height}px`); this.ascend(TabEmission.LIST_HEIGHT, height); } dispose () { this.isScrolling = false; } } api.tab = { TabPanel: TabPanel, TabButton: TabButton, TabsGroup: TabsGroup, TabsList: TabsList, TabSelector: TabSelector, TabEmission: TabEmission }; api.internals.register(api.tab.TabSelector.PANEL, api.tab.TabPanel); api.internals.register(api.tab.TabSelector.GROUP, api.tab.TabsGroup); api.internals.register(api.tab.TabSelector.LIST, api.tab.TabsList); const TagEvent = { DISMISS: api.internals.ns.event('dismiss') }; class TagDismissible extends api.core.Instance { static get instanceClassName () { return 'TagDismissible'; } init () { this.listenClick(); } handleClick () { this.focusClosest(); switch (api.mode) { case api.Modes.ANGULAR: case api.Modes.REACT: case api.Modes.VUE: this.request(this.verify.bind(this)); break; default: this.remove(); } this.dispatch(TagEvent.DISMISS); } verify () { if (document.body.contains(this.node)) this.warn(`a TagDismissible has just been dismissed and should be removed from the dom. In ${api.mode} mode, the api doesn't handle dom modification. An event ${TagEvent.DISMISS} is dispatched by the element to trigger the removal`); } } const TagSelector = { PRESSABLE: `${api.internals.ns.selector('tag')}[aria-pressed]`, DISMISSIBLE: `${api.internals.ns.selector('tag--dismiss')}` }; api.tag = { TagDismissible: TagDismissible, TagSelector: TagSelector, TagEvent: TagEvent }; api.internals.register(api.tag.TagSelector.PRESSABLE, api.core.Toggle); api.internals.register(api.tag.TagSelector.DISMISSIBLE, api.tag.TagDismissible); const TRANSCRIPTION = api.internals.ns.selector('transcription'); const TranscriptionSelector = { TRANSCRIPTION: TRANSCRIPTION, BUTTON: `${TRANSCRIPTION}__btn` }; class Transcription extends api.core.Instance { static get instanceClassName () { return 'Transcription'; } get collapsePrimary () { const buttons = this.element.children.map(child => child.getInstance('CollapseButton')).filter(button => button !== null && button.hasClass(TranscriptionSelector.BUTTON)); return buttons[0]; } } api.transcription = { Transcription: Transcription, TranscriptionSelector: TranscriptionSelector }; api.internals.register(api.transcription.TranscriptionSelector.TRANSCRIPTION, api.transcription.Transcription); class TileDownload extends api.core.Instance { static get instanceClassName () { return 'Ti