You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

285 lines
10 KiB

8 months ago
  1. /*!
  2. * Bootstrap tab.js v5.3.3 (https://getbootstrap.com/)
  3. * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
  4. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  5. */
  6. (function (global, factory) {
  7. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :
  8. typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :
  9. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tab = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));
  10. })(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';
  11. /**
  12. * --------------------------------------------------------------------------
  13. * Bootstrap tab.js
  14. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  15. * --------------------------------------------------------------------------
  16. */
  17. /**
  18. * Constants
  19. */
  20. const NAME = 'tab';
  21. const DATA_KEY = 'bs.tab';
  22. const EVENT_KEY = `.${DATA_KEY}`;
  23. const EVENT_HIDE = `hide${EVENT_KEY}`;
  24. const EVENT_HIDDEN = `hidden${EVENT_KEY}`;
  25. const EVENT_SHOW = `show${EVENT_KEY}`;
  26. const EVENT_SHOWN = `shown${EVENT_KEY}`;
  27. const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`;
  28. const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;
  29. const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`;
  30. const ARROW_LEFT_KEY = 'ArrowLeft';
  31. const ARROW_RIGHT_KEY = 'ArrowRight';
  32. const ARROW_UP_KEY = 'ArrowUp';
  33. const ARROW_DOWN_KEY = 'ArrowDown';
  34. const HOME_KEY = 'Home';
  35. const END_KEY = 'End';
  36. const CLASS_NAME_ACTIVE = 'active';
  37. const CLASS_NAME_FADE = 'fade';
  38. const CLASS_NAME_SHOW = 'show';
  39. const CLASS_DROPDOWN = 'dropdown';
  40. const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';
  41. const SELECTOR_DROPDOWN_MENU = '.dropdown-menu';
  42. const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`;
  43. const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]';
  44. const SELECTOR_OUTER = '.nav-item, .list-group-item';
  45. const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`;
  46. const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]'; // TODO: could only be `tab` in v6
  47. const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`;
  48. const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`;
  49. /**
  50. * Class definition
  51. */
  52. class Tab extends BaseComponent {
  53. constructor(element) {
  54. super(element);
  55. this._parent = this._element.closest(SELECTOR_TAB_PANEL);
  56. if (!this._parent) {
  57. return;
  58. // TODO: should throw exception in v6
  59. // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`)
  60. }
  61. // Set up initial aria attributes
  62. this._setInitialAttributes(this._parent, this._getChildren());
  63. EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event));
  64. }
  65. // Getters
  66. static get NAME() {
  67. return NAME;
  68. }
  69. // Public
  70. show() {
  71. // Shows this elem and deactivate the active sibling if exists
  72. const innerElem = this._element;
  73. if (this._elemIsActive(innerElem)) {
  74. return;
  75. }
  76. // Search for active tab on same parent to deactivate it
  77. const active = this._getActiveElem();
  78. const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, {
  79. relatedTarget: innerElem
  80. }) : null;
  81. const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {
  82. relatedTarget: active
  83. });
  84. if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) {
  85. return;
  86. }
  87. this._deactivate(active, innerElem);
  88. this._activate(innerElem, active);
  89. }
  90. // Private
  91. _activate(element, relatedElem) {
  92. if (!element) {
  93. return;
  94. }
  95. element.classList.add(CLASS_NAME_ACTIVE);
  96. this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section
  97. const complete = () => {
  98. if (element.getAttribute('role') !== 'tab') {
  99. element.classList.add(CLASS_NAME_SHOW);
  100. return;
  101. }
  102. element.removeAttribute('tabindex');
  103. element.setAttribute('aria-selected', true);
  104. this._toggleDropDown(element, true);
  105. EventHandler.trigger(element, EVENT_SHOWN, {
  106. relatedTarget: relatedElem
  107. });
  108. };
  109. this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE));
  110. }
  111. _deactivate(element, relatedElem) {
  112. if (!element) {
  113. return;
  114. }
  115. element.classList.remove(CLASS_NAME_ACTIVE);
  116. element.blur();
  117. this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too
  118. const complete = () => {
  119. if (element.getAttribute('role') !== 'tab') {
  120. element.classList.remove(CLASS_NAME_SHOW);
  121. return;
  122. }
  123. element.setAttribute('aria-selected', false);
  124. element.setAttribute('tabindex', '-1');
  125. this._toggleDropDown(element, false);
  126. EventHandler.trigger(element, EVENT_HIDDEN, {
  127. relatedTarget: relatedElem
  128. });
  129. };
  130. this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE));
  131. }
  132. _keydown(event) {
  133. if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) {
  134. return;
  135. }
  136. event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page
  137. event.preventDefault();
  138. const children = this._getChildren().filter(element => !index_js.isDisabled(element));
  139. let nextActiveElement;
  140. if ([HOME_KEY, END_KEY].includes(event.key)) {
  141. nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1];
  142. } else {
  143. const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key);
  144. nextActiveElement = index_js.getNextActiveElement(children, event.target, isNext, true);
  145. }
  146. if (nextActiveElement) {
  147. nextActiveElement.focus({
  148. preventScroll: true
  149. });
  150. Tab.getOrCreateInstance(nextActiveElement).show();
  151. }
  152. }
  153. _getChildren() {
  154. // collection of inner elements
  155. return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent);
  156. }
  157. _getActiveElem() {
  158. return this._getChildren().find(child => this._elemIsActive(child)) || null;
  159. }
  160. _setInitialAttributes(parent, children) {
  161. this._setAttributeIfNotExists(parent, 'role', 'tablist');
  162. for (const child of children) {
  163. this._setInitialAttributesOnChild(child);
  164. }
  165. }
  166. _setInitialAttributesOnChild(child) {
  167. child = this._getInnerElement(child);
  168. const isActive = this._elemIsActive(child);
  169. const outerElem = this._getOuterElement(child);
  170. child.setAttribute('aria-selected', isActive);
  171. if (outerElem !== child) {
  172. this._setAttributeIfNotExists(outerElem, 'role', 'presentation');
  173. }
  174. if (!isActive) {
  175. child.setAttribute('tabindex', '-1');
  176. }
  177. this._setAttributeIfNotExists(child, 'role', 'tab');
  178. // set attributes to the related panel too
  179. this._setInitialAttributesOnTargetPanel(child);
  180. }
  181. _setInitialAttributesOnTargetPanel(child) {
  182. const target = SelectorEngine.getElementFromSelector(child);
  183. if (!target) {
  184. return;
  185. }
  186. this._setAttributeIfNotExists(target, 'role', 'tabpanel');
  187. if (child.id) {
  188. this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`);
  189. }
  190. }
  191. _toggleDropDown(element, open) {
  192. const outerElem = this._getOuterElement(element);
  193. if (!outerElem.classList.contains(CLASS_DROPDOWN)) {
  194. return;
  195. }
  196. const toggle = (selector, className) => {
  197. const element = SelectorEngine.findOne(selector, outerElem);
  198. if (element) {
  199. element.classList.toggle(className, open);
  200. }
  201. };
  202. toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE);
  203. toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW);
  204. outerElem.setAttribute('aria-expanded', open);
  205. }
  206. _setAttributeIfNotExists(element, attribute, value) {
  207. if (!element.hasAttribute(attribute)) {
  208. element.setAttribute(attribute, value);
  209. }
  210. }
  211. _elemIsActive(elem) {
  212. return elem.classList.contains(CLASS_NAME_ACTIVE);
  213. }
  214. // Try to get the inner element (usually the .nav-link)
  215. _getInnerElement(elem) {
  216. return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem);
  217. }
  218. // Try to get the outer element (usually the .nav-item)
  219. _getOuterElement(elem) {
  220. return elem.closest(SELECTOR_OUTER) || elem;
  221. }
  222. // Static
  223. static jQueryInterface(config) {
  224. return this.each(function () {
  225. const data = Tab.getOrCreateInstance(this);
  226. if (typeof config !== 'string') {
  227. return;
  228. }
  229. if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
  230. throw new TypeError(`No method named "${config}"`);
  231. }
  232. data[config]();
  233. });
  234. }
  235. }
  236. /**
  237. * Data API implementation
  238. */
  239. EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
  240. if (['A', 'AREA'].includes(this.tagName)) {
  241. event.preventDefault();
  242. }
  243. if (index_js.isDisabled(this)) {
  244. return;
  245. }
  246. Tab.getOrCreateInstance(this).show();
  247. });
  248. /**
  249. * Initialize on focus
  250. */
  251. EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
  252. for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) {
  253. Tab.getOrCreateInstance(element);
  254. }
  255. });
  256. /**
  257. * jQuery
  258. */
  259. index_js.defineJQueryPlugin(Tab);
  260. return Tab;
  261. }));
  262. //# sourceMappingURL=tab.js.map