forked from bootstrap-vue/bootstrap-vue
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtoggle.js
145 lines (130 loc) · 4.97 KB
/
toggle.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import looseEqual from '../../utils/loose-equal'
import { addClass, hasAttr, removeAttr, removeClass, setAttr } from '../../utils/dom'
import { isBrowser } from '../../utils/env'
import { bindTargets, getTargets, unbindTargets } from '../../utils/target'
// Target listen types
const listenTypes = { click: true }
// Property key for handler storage
const BV_TOGGLE = '__BV_toggle__'
const BV_TOGGLE_STATE = '__BV_toggle_STATE__'
const BV_TOGGLE_CONTROLS = '__BV_toggle_CONTROLS__'
const BV_TOGGLE_TARGETS = '__BV_toggle_TARGETS__'
// Emitted control event for collapse (emitted to collapse)
const EVENT_TOGGLE = 'bv::toggle::collapse'
// Listen to event for toggle state update (emitted by collapse)
const EVENT_STATE = 'bv::collapse::state'
// Private event emitted on $root to ensure the toggle state is always synced.
// Gets emitted even if the state of b-collapse has not changed.
// This event is NOT to be documented as people should not be using it.
const EVENT_STATE_SYNC = 'bv::collapse::sync::state'
// Private event we send to collapse to request state update sync event
const EVENT_STATE_REQUEST = 'bv::request::collapse::state'
// Reset and remove a property from the provided element
const resetProp = (el, prop) => {
el[prop] = null
delete el[prop]
}
// Handle targets update
const handleTargets = ({ targets, vnode }) => {
targets.forEach(target => {
vnode.context.$root.$emit(EVENT_TOGGLE, target)
})
}
// Handle directive updates
/* istanbul ignore next: not easy to test */
const handleUpdate = (el, binding, vnode) => {
if (!isBrowser) {
return
}
if (!looseEqual(getTargets(binding), el[BV_TOGGLE_TARGETS])) {
// Targets have changed, so update accordingly
unbindTargets(vnode, binding, listenTypes)
const targets = bindTargets(vnode, binding, listenTypes, handleTargets)
// Update targets array to element
el[BV_TOGGLE_TARGETS] = targets
// Add aria attributes to element
el[BV_TOGGLE_CONTROLS] = targets.join(' ')
// ensure aria-controls is up to date
setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
// Request a state update from targets so that we can ensure
// expanded state is correct
targets.forEach(target => {
vnode.context.$root.$emit(EVENT_STATE_REQUEST, target)
})
}
// Ensure the collapse class and aria-* attributes persist
// after element is updated (either by parent re-rendering
// or changes to this element or it's contents
if (el[BV_TOGGLE_STATE] === true) {
addClass(el, 'collapsed')
setAttr(el, 'aria-expanded', 'true')
} else if (el[BV_TOGGLE_STATE] === false) {
removeClass(el, 'collapsed')
setAttr(el, 'aria-expanded', 'false')
}
setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
}
/*
* Export our directive
*/
export const VBToggle = {
bind(el, binding, vnode) {
const targets = bindTargets(vnode, binding, listenTypes, handleTargets)
if (isBrowser && vnode.context && targets.length > 0) {
// Add targets array to element
el[BV_TOGGLE_TARGETS] = targets
// Add aria attributes to element
el[BV_TOGGLE_CONTROLS] = targets.join(' ')
// State is initially collapsed until we receive a state event
el[BV_TOGGLE_STATE] = false
setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
setAttr(el, 'aria-expanded', 'false')
// If element is not a button, we add `role="button"` for accessibility
if (el.tagName !== 'BUTTON' && !hasAttr(el, 'role')) {
setAttr(el, 'role', 'button')
}
// Toggle state handler
const toggleDirectiveHandler = (id, state) => {
const targets = el[BV_TOGGLE_TARGETS] || []
if (targets.indexOf(id) !== -1) {
// Set aria-expanded state
setAttr(el, 'aria-expanded', state ? 'true' : 'false')
// Set/Clear 'collapsed' class state
el[BV_TOGGLE_STATE] = state
if (state) {
removeClass(el, 'collapsed')
} else {
addClass(el, 'collapsed')
}
}
}
// Store the toggle handler on the element
el[BV_TOGGLE] = toggleDirectiveHandler
// Listen for toggle state changes (public)
vnode.context.$root.$on(EVENT_STATE, el[BV_TOGGLE])
// Listen for toggle state sync (private)
vnode.context.$root.$on(EVENT_STATE_SYNC, el[BV_TOGGLE])
}
},
componentUpdated: handleUpdate,
updated: handleUpdate,
unbind(el, binding, vnode) /* istanbul ignore next */ {
unbindTargets(vnode, binding, listenTypes)
// Remove our $root listener
if (el[BV_TOGGLE]) {
vnode.context.$root.$off(EVENT_STATE, el[BV_TOGGLE])
vnode.context.$root.$off(EVENT_STATE_SYNC, el[BV_TOGGLE])
}
// Reset custom props
resetProp(el, BV_TOGGLE)
resetProp(el, BV_TOGGLE_STATE)
resetProp(el, BV_TOGGLE_CONTROLS)
resetProp(el, BV_TOGGLE_TARGETS)
// Reset classes/attrs
removeClass(el, 'collapsed')
removeAttr(el, 'aria-expanded')
removeAttr(el, 'aria-controls')
removeAttr(el, 'role')
}
}
export default VBToggle