Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/formats/html/tabby/js/tabby.js
12923 views
1
(function (root, factory) {
2
if (typeof define === "function" && define.amd) {
3
define([], function () {
4
return factory(root);
5
});
6
} else if (typeof exports === "object") {
7
module.exports = factory(root);
8
} else {
9
root.Tabby = factory(root);
10
}
11
})(
12
typeof global !== "undefined"
13
? global
14
: typeof window !== "undefined"
15
? window
16
: this,
17
function (window) {
18
"use strict";
19
20
//
21
// Variables
22
//
23
24
var defaults = {
25
idPrefix: "tabby-toggle_",
26
default: "[data-tabby-default]",
27
};
28
29
//
30
// Methods
31
//
32
33
/**
34
* Merge two or more objects together.
35
* @param {Object} objects The objects to merge together
36
* @returns {Object} Merged values of defaults and options
37
*/
38
var extend = function () {
39
var merged = {};
40
Array.prototype.forEach.call(arguments, function (obj) {
41
for (var key in obj) {
42
if (!obj.hasOwnProperty(key)) return;
43
merged[key] = obj[key];
44
}
45
});
46
return merged;
47
};
48
49
/**
50
* Emit a custom event
51
* @param {String} type The event type
52
* @param {Node} tab The tab to attach the event to
53
* @param {Node} details Details about the event
54
*/
55
var emitEvent = function (tab, details) {
56
// Create a new event
57
var event;
58
if (typeof window.CustomEvent === "function") {
59
event = new CustomEvent("tabby", {
60
bubbles: true,
61
cancelable: true,
62
detail: details,
63
});
64
} else {
65
event = document.createEvent("CustomEvent");
66
event.initCustomEvent("tabby", true, true, details);
67
}
68
69
// Dispatch the event
70
tab.dispatchEvent(event);
71
};
72
73
var focusHandler = function (event) {
74
toggle(event.target);
75
};
76
77
var getKeyboardFocusableElements = function (element) {
78
return [
79
...element.querySelectorAll(
80
'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])'
81
),
82
].filter(
83
(el) => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden")
84
);
85
};
86
87
/**
88
* Remove roles and attributes from a tab and its content
89
* @param {Node} tab The tab
90
* @param {Node} content The tab content
91
* @param {Object} settings User settings and options
92
*/
93
var destroyTab = function (tab, content, settings) {
94
// Remove the generated ID
95
if (tab.id.slice(0, settings.idPrefix.length) === settings.idPrefix) {
96
tab.id = "";
97
}
98
99
// remove event listener
100
tab.removeEventListener("focus", focusHandler, true);
101
102
// Remove roles
103
tab.removeAttribute("role");
104
tab.removeAttribute("aria-controls");
105
tab.removeAttribute("aria-selected");
106
tab.removeAttribute("tabindex");
107
tab.closest("li").removeAttribute("role");
108
content.removeAttribute("role");
109
content.removeAttribute("aria-labelledby");
110
content.removeAttribute("hidden");
111
};
112
113
/**
114
* Add the required roles and attributes to a tab and its content
115
* @param {Node} tab The tab
116
* @param {Node} content The tab content
117
* @param {Object} settings User settings and options
118
*/
119
var setupTab = function (tab, content, settings) {
120
// Give tab an ID if it doesn't already have one
121
if (!tab.id) {
122
tab.id = settings.idPrefix + content.id;
123
}
124
125
// Add roles
126
tab.setAttribute("role", "tab");
127
tab.setAttribute("aria-controls", content.id);
128
tab.closest("li").setAttribute("role", "presentation");
129
content.setAttribute("role", "tabpanel");
130
content.setAttribute("aria-labelledby", tab.id);
131
132
// Add selected state
133
if (tab.matches(settings.default)) {
134
tab.setAttribute("aria-selected", "true");
135
} else {
136
tab.setAttribute("aria-selected", "false");
137
content.setAttribute("hidden", "hidden");
138
}
139
140
// add focus event listender
141
tab.addEventListener("focus", focusHandler);
142
};
143
144
/**
145
* Hide a tab and its content
146
* @param {Node} newTab The new tab that's replacing it
147
*/
148
var hide = function (newTab) {
149
// Variables
150
var tabGroup = newTab.closest('[role="tablist"]');
151
if (!tabGroup) return {};
152
var tab = tabGroup.querySelector('[role="tab"][aria-selected="true"]');
153
if (!tab) return {};
154
var content = document.querySelector(tab.hash);
155
156
// Hide the tab
157
tab.setAttribute("aria-selected", "false");
158
159
// Hide the content
160
if (!content) return { previousTab: tab };
161
content.setAttribute("hidden", "hidden");
162
163
// Return the hidden tab and content
164
return {
165
previousTab: tab,
166
previousContent: content,
167
};
168
};
169
170
/**
171
* Show a tab and its content
172
* @param {Node} tab The tab
173
* @param {Node} content The tab content
174
*/
175
var show = function (tab, content) {
176
tab.setAttribute("aria-selected", "true");
177
content.removeAttribute("hidden");
178
tab.focus();
179
};
180
181
/**
182
* Toggle a new tab
183
* @param {Node} tab The tab to show
184
*/
185
var toggle = function (tab) {
186
// Make sure there's a tab to toggle and it's not already active
187
if (!tab || tab.getAttribute("aria-selected") == "true") return;
188
189
// Variables
190
var content = document.querySelector(tab.hash);
191
if (!content) return;
192
193
// Hide active tab and content
194
var details = hide(tab);
195
196
// Show new tab and content
197
show(tab, content);
198
199
// Add event details
200
details.tab = tab;
201
details.content = content;
202
203
// Emit a custom event
204
emitEvent(tab, details);
205
};
206
207
/**
208
* Get all of the tabs in a tablist
209
* @param {Node} tab A tab from the list
210
* @return {Object} The tabs and the index of the currently active one
211
*/
212
var getTabsMap = function (tab) {
213
var tabGroup = tab.closest('[role="tablist"]');
214
var tabs = tabGroup ? tabGroup.querySelectorAll('[role="tab"]') : null;
215
if (!tabs) return;
216
return {
217
tabs: tabs,
218
index: Array.prototype.indexOf.call(tabs, tab),
219
};
220
};
221
222
/**
223
* Switch the active tab based on keyboard activity
224
* @param {Node} tab The currently active tab
225
* @param {Key} key The key that was pressed
226
*/
227
var switchTabs = function (tab, key) {
228
// Get a map of tabs
229
var map = getTabsMap(tab);
230
if (!map) return;
231
var length = map.tabs.length - 1;
232
var index;
233
234
// Go to previous tab
235
if (["ArrowUp", "ArrowLeft", "Up", "Left"].indexOf(key) > -1) {
236
index = map.index < 1 ? length : map.index - 1;
237
}
238
239
// Go to next tab
240
else if (["ArrowDown", "ArrowRight", "Down", "Right"].indexOf(key) > -1) {
241
index = map.index === length ? 0 : map.index + 1;
242
}
243
244
// Go to home
245
else if (key === "Home") {
246
index = 0;
247
}
248
249
// Go to end
250
else if (key === "End") {
251
index = length;
252
}
253
254
// Toggle the tab
255
toggle(map.tabs[index]);
256
};
257
258
/**
259
* Create the Constructor object
260
*/
261
var Constructor = function (selector, options) {
262
//
263
// Variables
264
//
265
266
var publicAPIs = {};
267
var settings, tabWrapper;
268
269
//
270
// Methods
271
//
272
273
publicAPIs.destroy = function () {
274
// Get all tabs
275
var tabs = tabWrapper.querySelectorAll("a");
276
277
// Add roles to tabs
278
Array.prototype.forEach.call(tabs, function (tab) {
279
// Get the tab content
280
var content = document.querySelector(tab.hash);
281
if (!content) return;
282
283
// Setup the tab
284
destroyTab(tab, content, settings);
285
});
286
287
// Remove role from wrapper
288
tabWrapper.removeAttribute("role");
289
290
// Remove event listeners
291
document.documentElement.removeEventListener(
292
"click",
293
clickHandler,
294
true
295
);
296
tabWrapper.removeEventListener("keydown", keyHandler, true);
297
298
// Reset variables
299
settings = null;
300
tabWrapper = null;
301
};
302
303
/**
304
* Setup the DOM with the proper attributes
305
*/
306
publicAPIs.setup = function () {
307
// Variables
308
tabWrapper = document.querySelector(selector);
309
if (!tabWrapper) return;
310
var tabs = tabWrapper.querySelectorAll("a");
311
312
// Add role to wrapper
313
tabWrapper.setAttribute("role", "tablist");
314
315
// Add roles to tabs. provide dynanmic tab indexes if we are within reveal
316
var contentTabindexes =
317
window.document.body.classList.contains("reveal-viewport");
318
var nextTabindex = 1;
319
Array.prototype.forEach.call(tabs, function (tab) {
320
if (contentTabindexes) {
321
tab.setAttribute("tabindex", "" + nextTabindex++);
322
} else {
323
tab.setAttribute("tabindex", "0");
324
}
325
326
// Get the tab content
327
var content = document.querySelector(tab.hash);
328
if (!content) return;
329
330
// set tab indexes for content
331
if (contentTabindexes) {
332
getKeyboardFocusableElements(content).forEach(function (el) {
333
el.setAttribute("tabindex", "" + nextTabindex++);
334
});
335
}
336
337
// Setup the tab
338
setupTab(tab, content, settings);
339
});
340
};
341
342
/**
343
* Toggle a tab based on an ID
344
* @param {String|Node} id The tab to toggle
345
*/
346
publicAPIs.toggle = function (id) {
347
// Get the tab
348
var tab = id;
349
if (typeof id === "string") {
350
tab = document.querySelector(
351
selector + ' [role="tab"][href*="' + id + '"]'
352
);
353
}
354
355
// Toggle the tab
356
toggle(tab);
357
};
358
359
/**
360
* Handle click events
361
*/
362
var clickHandler = function (event) {
363
// Only run on toggles
364
var tab = event.target.closest(selector + ' [role="tab"]');
365
if (!tab) return;
366
367
// Prevent link behavior
368
event.preventDefault();
369
370
// Toggle the tab
371
toggle(tab);
372
};
373
374
/**
375
* Handle keydown events
376
*/
377
var keyHandler = function (event) {
378
// Only run if a tab is in focus
379
var tab = document.activeElement;
380
if (!tab.matches(selector + ' [role="tab"]')) return;
381
382
// Only run for specific keys
383
if (["Home", "End"].indexOf(event.key) < 0) return;
384
385
// Switch tabs
386
switchTabs(tab, event.key);
387
};
388
389
/**
390
* Initialize the instance
391
*/
392
var init = function () {
393
// Merge user options with defaults
394
settings = extend(defaults, options || {});
395
396
// Setup the DOM
397
publicAPIs.setup();
398
399
// Add event listeners
400
document.documentElement.addEventListener("click", clickHandler, true);
401
tabWrapper.addEventListener("keydown", keyHandler, true);
402
};
403
404
//
405
// Initialize and return the Public APIs
406
//
407
408
init();
409
return publicAPIs;
410
};
411
412
//
413
// Return the Constructor
414
//
415
416
return Constructor;
417
}
418
);
419
420