Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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