397 lines
10 KiB
JavaScript
397 lines
10 KiB
JavaScript
|
/*! DSFR v1.11.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.11.2'
|
||
|
};
|
||
|
|
||
|
const api = window[config.namespace];
|
||
|
|
||
|
/**
|
||
|
* 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 = 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 = this.node.scrollLeft;
|
||
|
const isMin = scrollLeft <= SCROLL_OFFSET;
|
||
|
const max = this.node.scrollWidth - this.node.clientWidth - SCROLL_OFFSET;
|
||
|
|
||
|
const isMax = Math.abs(scrollLeft) >= max;
|
||
|
const isRtl = document.documentElement.getAttribute('dir') === '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;
|
||
|
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);
|
||
|
//# sourceMappingURL=tab.module.js.map
|