UNPKG

bootstrap-italia

Version:

Bootstrap Italia è un tema Bootstrap 5 per la creazione di applicazioni web nel pieno rispetto delle linee guida di design per i siti internet e i servizi digitali della PA

617 lines (498 loc) 16.4 kB
import * as Popper from '@popperjs/core'; import { isRTL, findShadowRoot, noop, getUID, getElement } from './util/index.js'; import { DefaultAllowlist } from './util/sanitizer.js'; import EventHandler from './dom/event-handler.js'; import Manipulator from './dom/manipulator.js'; import BaseComponent from './base-component.js'; import TemplateFactory from './util/template-factory.js'; /** * -------------------------------------------------------------------------- * Bootstrap Italia (https://italia.github.io/bootstrap-italia/) * Authors: https://github.com/italia/bootstrap-italia/blob/main/AUTHORS * Licensed under BSD-3-Clause license (https://github.com/italia/bootstrap-italia/blob/main/LICENSE) * This a fork of Bootstrap: Initial license and original file name below * Bootstrap (v5.2.3): tooltip.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ /** * Constants */ const NAME = 'tooltip'; const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); const CLASS_NAME_FADE = 'fade'; const CLASS_NAME_MODAL = 'modal'; const CLASS_NAME_SHOW = 'show'; const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; const EVENT_MODAL_HIDE = 'hide.bs.modal'; const TRIGGER_HOVER = 'hover'; const TRIGGER_FOCUS = 'focus'; const TRIGGER_CLICK = 'click'; const TRIGGER_MANUAL = 'manual'; const EVENT_HIDE = 'hide'; const EVENT_HIDDEN = 'hidden'; const EVENT_SHOW = 'show'; const EVENT_SHOWN = 'shown'; const EVENT_INSERTED = 'inserted'; const EVENT_CLICK = 'click'; const EVENT_FOCUSIN = 'focusin'; const EVENT_FOCUSOUT = 'focusout'; const EVENT_MOUSEENTER = 'mouseenter'; const EVENT_MOUSELEAVE = 'mouseleave'; const AttachmentMap = { AUTO: 'auto', TOP: 'top', RIGHT: isRTL() ? 'left' : 'right', BOTTOM: 'bottom', LEFT: isRTL() ? 'right' : 'left', }; const Default = { allowList: DefaultAllowlist, animation: true, boundary: 'clippingParents', container: false, customClass: '', delay: 0, fallbackPlacements: ['top', 'right', 'bottom', 'left'], html: false, offset: [0, 0], placement: 'top', popperConfig: null, sanitize: true, sanitizeFn: null, selector: false, template: '<div class="tooltip" role="tooltip">' + '<div class="tooltip-arrow"></div>' + '<div class="tooltip-inner"></div>' + '</div>', title: '', trigger: 'hover focus', }; const DefaultType = { allowList: 'object', animation: 'boolean', boundary: '(string|element)', container: '(string|element|boolean)', customClass: '(string|function)', delay: '(number|object)', fallbackPlacements: 'array', html: 'boolean', offset: '(array|string|function)', placement: '(string|function)', popperConfig: '(null|object|function)', sanitize: 'boolean', sanitizeFn: '(null|function)', selector: '(string|boolean)', template: 'string', title: '(string|element|function)', trigger: 'string', }; /** * Class definition */ class Tooltip extends BaseComponent { constructor(element, config) { if (typeof Popper === 'undefined') { throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)") } super(element, config); // Private this._isEnabled = true; this._timeout = 0; this._isHovered = null; this._activeTrigger = {}; this._popper = null; this._templateFactory = null; this._newContent = null; // Protected this.tip = null; this._setListeners(); if (!this._config.selector) { this._fixTitle(); } } // Getters static get Default() { return Default } static get DefaultType() { return DefaultType } static get NAME() { return NAME } // Public enable() { this._isEnabled = true; } disable() { this._isEnabled = false; } toggleEnabled() { this._isEnabled = !this._isEnabled; } toggle() { if (!this._isEnabled) { return } this._activeTrigger.click = !this._activeTrigger.click; if (this._isShown()) { this._leave(); return } this._enter(); } dispose() { clearTimeout(this._timeout); EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); if (this._element.getAttribute('data-bs-original-title')) { this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); } this._disposePopper(); super.dispose(); } show() { if (this._element.style.display === 'none') { throw new Error('Please use show on visible elements') } if (!(this._isWithContent() && this._isEnabled)) { return } const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW)); const shadowRoot = findShadowRoot(this._element); const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); if (showEvent.defaultPrevented || !isInTheDom) { return } // todo v6 remove this OR make it optional this._disposePopper(); const tip = this._getTipElement(); this._element.setAttribute('aria-describedby', tip.getAttribute('id')); const { container } = this._config; if (!this._element.ownerDocument.documentElement.contains(this.tip)) { container.append(tip); EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); } this._popper = this._createPopper(tip); tip.classList.add(CLASS_NAME_SHOW); // If this is a touch-enabled device we add extra // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html if ('ontouchstart' in document.documentElement) { for (const element of [].concat(...document.body.children)) { EventHandler.on(element, 'mouseover', noop); } } document.addEventListener( 'keyup', (event) => { if (event.key === 'Escape') { this.hide(); } }, { once: true } ); const complete = () => { EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN)); if (this._isHovered === false) { this._leave(); } this._isHovered = false; }; this._queueCallback(complete, this.tip, this._isAnimated()); } hide() { if (!this._isShown()) { return } const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE)); if (hideEvent.defaultPrevented) { return } const tip = this._getTipElement(); tip.classList.remove(CLASS_NAME_SHOW); // If this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support if ('ontouchstart' in document.documentElement) { for (const element of [].concat(...document.body.children)) { EventHandler.off(element, 'mouseover', noop); } } this._activeTrigger[TRIGGER_CLICK] = false; this._activeTrigger[TRIGGER_FOCUS] = false; this._activeTrigger[TRIGGER_HOVER] = false; this._isHovered = null; // it is a trick to support manual triggering const complete = () => { if (this._isWithActiveTrigger()) { return } if (!this._isHovered) { this._disposePopper(); } this._element.removeAttribute('aria-describedby'); EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN)); }; this._queueCallback(complete, this.tip, this._isAnimated()); } update() { if (this._popper) { this._popper.update(); } } // Protected _isWithContent() { return Boolean(this._getTitle()) } _getTipElement() { if (!this.tip) { this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); } return this.tip } _createTipElement(content) { const tip = this._getTemplateFactory(content).toHtml(); // todo: remove this check on v6 if (!tip) { return null } tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW); // todo: on v6 the following can be achieved with CSS only tip.classList.add(`bs-${this.constructor.NAME}-auto`); const tipId = getUID(this.constructor.NAME).toString(); tip.setAttribute('id', tipId); if (this._isAnimated()) { tip.classList.add(CLASS_NAME_FADE); } return tip } setContent(content) { this._newContent = content; if (this._isShown()) { this._disposePopper(); this.show(); } } _getTemplateFactory(content) { if (this._templateFactory) { this._templateFactory.changeContent(content); } else { this._templateFactory = new TemplateFactory({ ...this._config, // the `content` var has to be after `this._config` // to override config.content in case of popover content, extraClass: this._resolvePossibleFunction(this._config.customClass), }); } return this._templateFactory } _getContentForTemplate() { return { [SELECTOR_TOOLTIP_INNER]: this._getTitle(), } } _getTitle() { return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title') } // Private _initializeOnDelegatedTarget(event) { return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()) } _isAnimated() { return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE)) } _isShown() { return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW) } _createPopper(tip) { const placement = typeof this._config.placement === 'function' ? this._config.placement.call(this, tip, this._element) : this._config.placement; const attachment = AttachmentMap[placement.toUpperCase()]; return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) } _getOffset() { const { offset } = this._config; if (typeof offset === 'string') { return offset.split(',').map((value) => Number.parseInt(value, 10)) } if (typeof offset === 'function') { return (popperData) => offset(popperData, this._element) } return offset } _resolvePossibleFunction(arg) { return typeof arg === 'function' ? arg.call(this._element) : arg } _getPopperConfig(attachment) { const defaultBsPopperConfig = { placement: attachment, modifiers: [ { name: 'flip', options: { fallbackPlacements: this._config.fallbackPlacements, }, }, { name: 'offset', options: { offset: this._getOffset(), }, }, { name: 'preventOverflow', options: { boundary: this._config.boundary, }, }, { name: 'arrow', options: { element: `.${this.constructor.NAME}-arrow`, }, }, { name: 'preSetPlacement', enabled: true, phase: 'beforeMain', fn: (data) => { // Pre-set Popper's placement attribute in order to read the arrow sizes properly. // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement this._getTipElement().setAttribute('data-popper-placement', data.state.placement); }, }, ], }; return { ...defaultBsPopperConfig, ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig), } } _setListeners() { const triggers = this._config.trigger.split(' '); for (const trigger of triggers) { if (trigger === 'click') { EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, (event) => { const context = this._initializeOnDelegatedTarget(event); context.toggle(); }); } else if (trigger !== TRIGGER_MANUAL) { const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN); const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT); EventHandler.on(this._element, eventIn, this._config.selector, (event) => { const context = this._initializeOnDelegatedTarget(event); context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; context._enter(); }); EventHandler.on(this._element, eventOut, this._config.selector, (event) => { const context = this._initializeOnDelegatedTarget(event); context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); context._leave(); }); } } this._hideModalHandler = () => { if (this._element) { this.hide(); } }; EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); } _fixTitle() { const title = this._element.getAttribute('title'); if (!title) { return } if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { this._element.setAttribute('aria-label', title); } this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility this._element.removeAttribute('title'); } _enter() { if (this._isShown() || this._isHovered) { this._isHovered = true; return } this._isHovered = true; this._setTimeout(() => { if (this._isHovered) { this.show(); } }, this._config.delay.show); } _leave() { if (this._isWithActiveTrigger()) { return } this._isHovered = false; this._setTimeout(() => { if (!this._isHovered) { this.hide(); } }, this._config.delay.hide); } _setTimeout(handler, timeout) { clearTimeout(this._timeout); this._timeout = setTimeout(handler, timeout); } _isWithActiveTrigger() { return Object.values(this._activeTrigger).includes(true) } _getConfig(config) { const dataAttributes = Manipulator.getDataAttributes(this._element); for (const dataAttribute of Object.keys(dataAttributes)) { if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { delete dataAttributes[dataAttribute]; } } config = { ...dataAttributes, ...(typeof config === 'object' && config ? config : {}), }; config = this._mergeConfigObj(config); config = this._configAfterMerge(config); this._typeCheckConfig(config); return config } _configAfterMerge(config) { config.container = config.container === false ? document.body : getElement(config.container); if (typeof config.delay === 'number') { config.delay = { show: config.delay, hide: config.delay, }; } if (typeof config.title === 'number') { config.title = config.title.toString(); } if (typeof config.content === 'number') { config.content = config.content.toString(); } return config } _getDelegateConfig() { const config = {}; for (const key in this._config) { if (this.constructor.Default[key] !== this._config[key]) { config[key] = this._config[key]; } } config.selector = false; config.trigger = 'manual'; // In the future can be replaced with: // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) // `Object.fromEntries(keysWithDifferentValues)` return config } _disposePopper() { if (this._popper) { this._popper.destroy(); this._popper = null; } if (this.tip) { this.tip.remove(); this.tip = null; } } } export { Tooltip as default }; //# sourceMappingURL=tooltip.js.map