-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtoggle.js
416 lines (337 loc) · 11.6 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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
'use strict';
/**
* The Simple Toggle class. This will toggle the class 'active' and 'hidden'
* on target elements, determined by a click event on a selected link or
* element. This will also toggle the aria-hidden attribute for targeted
* elements to support screen readers. Target settings and other functionality
* can be controlled through data attributes.
*
* This uses the .matches() method which will require a polyfill for IE
* https://polyfill.io/v2/docs/features/#Element_prototype_matches
*
* @class
*/
class Toggle {
/**
* @constructor
*
* @param {Object} s Settings for this Toggle instance
*
* @return {Object} The class
*/
constructor(s) {
// Create an object to store existing toggle listeners (if it doesn't exist)
if (!window.hasOwnProperty(Toggle.callback))
window[Toggle.callback] = [];
s = (!s) ? {} : s;
this.settings = {
selector: (s.selector) ? s.selector : Toggle.selector,
namespace: (s.namespace) ? s.namespace : Toggle.namespace,
inactiveClass: (s.inactiveClass) ? s.inactiveClass : Toggle.inactiveClass,
activeClass: (s.activeClass) ? s.activeClass : Toggle.activeClass,
before: (s.before) ? s.before : false,
after: (s.after) ? s.after : false,
valid: (s.valid) ? s.valid : false,
focusable: (s.hasOwnProperty('focusable')) ? s.focusable : true,
jump: (s.hasOwnProperty('jump')) ? s.jump : true
};
// Store the element for potential use in callbacks
this.element = (s.element) ? s.element : false;
if (this.element) {
this.element.addEventListener('click', (event) => {
this.toggle(event);
});
} else {
// If there isn't an existing instantiated toggle, add the event listener.
if (!window[Toggle.callback].hasOwnProperty(this.settings.selector)) {
let body = document.querySelector('body');
for (let i = 0; i < Toggle.events.length; i++) {
let tggleEvent = Toggle.events[i];
body.addEventListener(tggleEvent, event => {
if (!event.target.matches(this.settings.selector))
return;
this.event = event;
let type = event.type.toUpperCase();
if (
this[event.type] &&
Toggle.elements[type] &&
Toggle.elements[type].includes(event.target.tagName)
) this[event.type](event);
});
}
}
}
// Record that a toggle using this selector has been instantiated.
// This prevents double toggling.
window[Toggle.callback][this.settings.selector] = true;
return this;
}
/**
* Click event handler
*
* @param {Event} event The original click event
*/
click(event) {
this.toggle(event);
}
/**
* Input/select/textarea change event handler. Checks to see if the
* event.target is valid then toggles accordingly.
*
* @param {Event} event The original input change event
*/
change(event) {
let valid = event.target.checkValidity();
if (valid && !this.isActive(event.target)) {
this.toggle(event); // show
} else if (!valid && this.isActive(event.target)) {
this.toggle(event); // hide
}
}
/**
* Check to see if the toggle is active
*
* @param {Object} element The toggle element (trigger)
*/
isActive(element) {
let active = false;
if (this.settings.activeClass) {
active = element.classList.contains(this.settings.activeClass)
}
// if () {
// Toggle.elementAriaRoles
// TODO: Add catch to see if element aria roles are toggled
// }
// if () {
// Toggle.targetAriaRoles
// TODO: Add catch to see if target aria roles are toggled
// }
return active;
}
/**
* Get the target of the toggle element (trigger)
*
* @param {Object} el The toggle element (trigger)
*/
getTarget(element) {
let target = false;
/** Anchor Links */
target = (element.hasAttribute('href')) ?
document.querySelector(element.getAttribute('href')) : target;
/** Toggle Controls */
target = (element.hasAttribute('aria-controls')) ?
document.querySelector(`#${element.getAttribute('aria-controls')}`) : target;
return target;
}
/**
* The toggle event proxy for getting and setting the element/s and target
*
* @param {Object} event The main click event
*
* @return {Object} The Toggle instance
*/
toggle(event) {
let element = event.target;
let target = false;
let focusable = [];
event.preventDefault();
target = this.getTarget(element);
/** Focusable Children */
focusable = (target) ?
target.querySelectorAll(Toggle.elFocusable.join(', ')) : focusable;
/** Main Functionality */
if (!target) return this;
this.elementToggle(element, target, focusable);
/** Undo */
if (element.dataset[`${this.settings.namespace}Undo`]) {
const undo = document.querySelector(
element.dataset[`${this.settings.namespace}Undo`]
);
undo.addEventListener('click', (event) => {
event.preventDefault();
this.elementToggle(element, target);
undo.removeEventListener('click');
});
}
return this;
}
/**
* Get other toggles that might control the same element
*
* @param {Object} element The toggling element
*
* @return {NodeList} List of other toggling elements
* that control the target
*/
getOthers(element) {
let selector = false;
if (element.hasAttribute('href')) {
selector = `[href="${element.getAttribute('href')}"]`;
} else if (element.hasAttribute('aria-controls')) {
selector = `[aria-controls="${element.getAttribute('aria-controls')}"]`;
}
return (selector) ? document.querySelectorAll(selector) : [];
}
/**
* Hide the Toggle Target's focusable children from focus.
* If an element has the data-attribute `data-toggle-tabindex`
* it will use that as the default tab index of the element.
*
* @param {NodeList} elements List of focusable elements
*
* @return {Object} The Toggle Instance
*/
toggleFocusable(elements) {
elements.forEach(element => {
let tabindex = element.getAttribute('tabindex');
if (tabindex === '-1') {
let dataDefault = element
.getAttribute(`data-${Toggle.namespace}-tabindex`);
if (dataDefault) {
element.setAttribute('tabindex', dataDefault);
} else {
element.removeAttribute('tabindex');
}
} else {
element.setAttribute('tabindex', '-1');
}
});
return this;
}
/**
* Jumps to Element visibly and shifts focus
* to the element by setting the tabindex
*
* @param {Object} element The Toggling Element
* @param {Object} target The Target Element
*
* @return {Object} The Toggle instance
*/
jumpTo(element, target) {
// Reset the history state. This will clear out
// the hash when the target is toggled closed
history.pushState('', '',
window.location.pathname + window.location.search);
// Focus if active
if (target.classList.contains(this.settings.activeClass)) {
window.location.hash = element.getAttribute('href');
target.setAttribute('tabindex', '0');
target.focus({preventScroll: true});
} else {
target.removeAttribute('tabindex');
}
return this;
}
/**
* The main toggling method for attributes
*
* @param {Object} element The Toggle element
* @param {Object} target The Target element to toggle active/hidden
* @param {NodeList} focusable Any focusable children in the target
*
* @return {Object} The Toggle instance
*/
elementToggle(element, target, focusable = []) {
let i = 0;
let attr = '';
let value = '';
/**
* Store elements for potential use in callbacks
*/
this.element = element;
this.target = target;
this.others = this.getOthers(element);
this.focusable = focusable;
/**
* Validity method property that will cancel the toggle if it returns false
*/
if (this.settings.valid && !this.settings.valid(this))
return this;
/**
* Toggling before hook
*/
if (this.settings.before)
this.settings.before(this);
/**
* Toggle Element and Target classes
*/
if (this.settings.activeClass) {
this.element.classList.toggle(this.settings.activeClass);
this.target.classList.toggle(this.settings.activeClass);
// If there are other toggles that control the same element
this.others.forEach(other => {
if (other !== this.element)
other.classList.toggle(this.settings.activeClass);
});
}
if (this.settings.inactiveClass)
target.classList.toggle(this.settings.inactiveClass);
/**
* Target Element Aria Attributes
*/
for (i = 0; i < Toggle.targetAriaRoles.length; i++) {
attr = Toggle.targetAriaRoles[i];
value = this.target.getAttribute(attr);
if (value != '' && value)
this.target.setAttribute(attr, (value === 'true') ? 'false' : 'true');
}
/**
* Toggle the target's focusable children tabindex
*/
if (this.settings.focusable)
this.toggleFocusable(this.focusable);
/**
* Jump to Target Element if Toggle Element is an anchor link
*/
if (this.settings.jump && this.element.hasAttribute('href'))
this.jumpTo(this.element, this.target);
/**
* Toggle Element (including multi toggles) Aria Attributes
*/
for (i = 0; i < Toggle.elAriaRoles.length; i++) {
attr = Toggle.elAriaRoles[i];
value = this.element.getAttribute(attr);
if (value != '' && value)
this.element.setAttribute(attr, (value === 'true') ? 'false' : 'true');
// If there are other toggles that control the same element
this.others.forEach((other) => {
if (other !== this.element && other.getAttribute(attr))
other.setAttribute(attr, (value === 'true') ? 'false' : 'true');
});
}
/**
* Toggling complete hook
*/
if (this.settings.after)
this.settings.after(this);
return this;
}
}
/** @type {String} The main selector to add the toggling function to */
Toggle.selector = '[data-js*="toggle"]';
/** @type {String} The namespace for our data attribute settings */
Toggle.namespace = 'toggle';
/** @type {String} The hide class */
Toggle.inactiveClass = 'hidden';
/** @type {String} The active class */
Toggle.activeClass = 'active';
/** @type {Array} Aria roles to toggle true/false on the toggling element */
Toggle.elAriaRoles = ['aria-pressed', 'aria-expanded'];
/** @type {Array} Aria roles to toggle true/false on the target element */
Toggle.targetAriaRoles = ['aria-hidden'];
/** @type {Array} Focusable elements to hide within the hidden target element */
Toggle.elFocusable = [
'a', 'button', 'input', 'select', 'textarea', 'object', 'embed', 'form',
'fieldset', 'legend', 'label', 'area', 'audio', 'video', 'iframe', 'svg',
'details', 'table', '[tabindex]', '[contenteditable]', '[usemap]'
];
/** @type {Array} Key attribute for storing toggles in the window */
Toggle.callback = ['TogglesCallback'];
/** @type {Array} Default events to to watch for toggling. Each must have a handler in the class and elements to look for in Toggle.elements */
Toggle.events = ['click', 'change'];
/** @type {Array} Elements to delegate to each event handler */
Toggle.elements = {
CLICK: ['A', 'BUTTON'],
CHANGE: ['SELECT', 'INPUT', 'TEXTAREA']
};
export default Toggle;