Fossil SCM

fossil-scm / src / fossil.tabs.js
Blame History Raw 260 lines
1
"use strict";
2
(function(F/*fossil object*/){
3
const E = (s)=>document.querySelector(s),
4
EA = (s)=>document.querySelectorAll(s),
5
D = F.dom;
6
7
/**
8
Creates a TabManager. If passed a truthy first argument, it is
9
passed to init(). If passed a truthy second argument, it must be
10
an Object holding configuration options:
11
12
{
13
tabAccessKeys: boolean (=true)
14
If true, tab buttons are assigned "accesskey" values
15
equal to their 1-based tab number.
16
}
17
*/
18
const TabManager = function(domElem, options){
19
this.e = {};
20
this.options = F.mergeLastWins(TabManager.defaultOptions , options);
21
if(domElem) this.init(domElem);
22
};
23
24
/**
25
Default values for the options object passed to the TabManager
26
constructor. Changing these affects the defaults of all
27
TabManager instances instantiated after that point.
28
*/
29
TabManager.defaultOptions = {
30
tabAccessKeys: true
31
};
32
33
/**
34
Internal helper to normalize a method argument to a tab
35
element. arg may be a tab DOM element, a selector string, or an
36
index into tabMgr.e.tabs.childNodes. Returns the corresponding
37
tab element.
38
*/
39
const tabArg = function(arg,tabMgr){
40
if('string'===typeof arg) arg = E(arg);
41
else if(tabMgr && 'number'===typeof arg && arg>=0){
42
arg = tabMgr.e.tabs.childNodes[arg];
43
}
44
return arg;
45
};
46
47
/**
48
Sets sets the visibility of tab element e to on or off. e MUST be
49
a TabManager tab element.
50
*/
51
const setVisible = function(e,yes){
52
D[yes ? 'removeClass' : 'addClass'](e, 'hidden');
53
};
54
55
TabManager.prototype = {
56
/**
57
Initializes the tabs associated with the given tab container
58
(DOM element or selector for a single element). This must be
59
called once before using any other member functions of a given
60
instance, noting that the constructor will call this if it is
61
passed an argument.
62
63
The tab container must have an 'id' attribute. This function
64
looks through the DOM for all elements which have
65
data-tab-parent=thatId. For each one it creates a button to
66
switch to that tab and moves the element into this.e.tabs,
67
*possibly* injecting an intermediary element between
68
this.e.tabs and the element.
69
70
The label for each tab is set by the data-tab-label attribute
71
of each element, defaulting to something not terribly useful.
72
73
When it's done, it auto-selects the first tab unless a tab has
74
a truthy numeric value in its data-tab-select attribute, in
75
which case the last tab to have such a property is selected.
76
77
This method must only be called once per instance. TabManagers
78
may be nested but must not share any tabs instances.
79
80
Returns this object.
81
82
DOM elements of potential interest to users:
83
84
this.e.container = the outermost container element.
85
86
this.e.tabBar = the button bar. Each "button" (whether it's a
87
buttor not is unspecified) has a class of .tab-button.
88
89
this.e.tabs = the parent for all of the tab elements.
90
91
It is legal, within reason, to manipulate these a bit, in
92
particular this.e.container, e.g. by adding more children to
93
it. Do not remove elements from the tabs or tabBar, however, or
94
the tab state may get sorely out of sync.
95
96
CSS classes: the container element has whatever class(es) the
97
client sets on. this.e.tabBar gets the 'tab-bar' class and
98
this.e.tabs gets the 'tabs' class. It's hypothetically possible
99
to move the tabs to either side or the bottom using only CSS,
100
but it's never been tested.
101
*/
102
init: function(container){
103
container = tabArg(container);
104
const cID = container.getAttribute('id');
105
if(!cID){
106
throw new Error("Tab container element is missing 'id' attribute.");
107
}
108
const c = this.e.container = container;
109
this.e.tabBar = D.addClass(D.div(),'tab-bar');
110
this.e.tabs = D.addClass(D.div(),'tabs');
111
D.append(c, this.e.tabBar, this.e.tabs);
112
let selectIndex = 0;
113
EA('[data-tab-parent='+cID+']').forEach((c,n)=>{
114
if(+c.dataset.tabSelect) selectIndex=n;
115
this.addTab(c);
116
});
117
return this.switchToTab(selectIndex);
118
},
119
120
/**
121
For the given tab element, unique selector string, or integer
122
(0-based tab number), returns the button associated with that
123
tab, or undefined if the argument does not match any current
124
tab.
125
*/
126
getButtonForTab: function(tab){
127
tab = tabArg(tab,this);
128
var i = -1;
129
this.e.tabs.childNodes.forEach(function(e,n){
130
if(e===tab) i = n;
131
});
132
return i>=0 ? this.e.tabBar.childNodes[i] : undefined;
133
},
134
/**
135
Adds the given DOM element or unique selector as the next
136
tab in the tab container, adding a button to switch to
137
the tab. Returns this object.
138
139
If this object's options include a truthy tabAccessKeys then
140
each tab button gets assigned an accesskey attribute equal to
141
its 1-based index in the tab list. e.g. key 1 is the first tab
142
and key 5 is the 5th. Whether/how that accesskey is accessed is
143
dependent on the browser and its OS:
144
145
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey
146
*/
147
addTab: function f(tab){
148
if(!f.click){
149
f.click = function(e){
150
e.target.$manager.switchToTab(e.target.$tab);
151
};
152
}
153
tab = tabArg(tab);
154
tab.remove();
155
D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
156
const tabCount = this.e.tabBar.childNodes.length+1;
157
const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount;
158
const btn = D.addClass(D.append(D.span(), lbl), 'tab-button');
159
D.append(this.e.tabBar,btn);
160
btn.$manager = this;
161
btn.$tab = tab;
162
if(this.options.tabAccessKeys){
163
D.attr(btn, 'accesskey', tabCount);
164
}
165
btn.addEventListener('click', f.click, false);
166
return this;
167
},
168
169
/**
170
Internal. Fires a new CustomEvent to all listeners which have
171
registered via this.addEventListener().
172
*/
173
_dispatchEvent: function(name, detail){
174
try{
175
this.e.container.dispatchEvent(
176
new CustomEvent(name, {detail: detail})
177
);
178
}catch(e){
179
/* ignore */
180
}
181
return this;
182
},
183
184
/**
185
Registers an event listener for this object's custom events.
186
The callback gets a CustomEvent object with a 'detail'
187
propertly holding any tab-related state for the event. The events
188
are:
189
190
- 'before-switch-from' is emitted immediately before a new tab
191
is switched away from. detail = the tab element being switched
192
away from.
193
194
- 'before-switch-to' is emitted immediately before a new tab is
195
switched to. detail = the tab element.
196
197
- 'after-switch-to' is emitted immediately after a new tab is
198
switched to. detail = the tab element.
199
200
Any exceptions thrown by listeners are caught and ignored, to
201
avoid that they knock the tab state out of sync.
202
203
Returns this object.
204
*/
205
addEventListener: function(eventName, callback){
206
this.e.container.addEventListener(eventName, callback, false);
207
return this;
208
},
209
210
/**
211
Inserts the given DOM element immediately after the tab bar.
212
Intended for a status bar or similar always-visible component.
213
Returns this object.
214
*/
215
addCustomWidget: function(e){
216
this.e.container.insertBefore(e, this.e.tabs);
217
return this;
218
},
219
220
/**
221
If the given DOM element, unique selector, or integer (0-based
222
tab number) is one of this object's tabs, the UI makes that tab
223
the currently-visible one, firing any relevant events. Returns
224
this object. If the argument is the current tab, this is a
225
no-op, and no events are fired.
226
*/
227
switchToTab: function(tab){
228
tab = tabArg(tab,this);
229
const self = this;
230
if(tab===this._currentTab) return this;
231
else if(this._currentTab){
232
this._dispatchEvent('before-switch-from', this._currentTab);
233
}
234
delete this._currentTab;
235
this.e.tabs.childNodes.forEach((e,ndx)=>{
236
const btn = this.e.tabBar.childNodes[ndx];
237
if(e===tab){
238
if(D.hasClass(e,'selected')){
239
return;
240
}
241
self._dispatchEvent('before-switch-to',tab);
242
setVisible(e, true);
243
this._currentTab = e;
244
D.addClass(btn,'selected');
245
self._dispatchEvent('after-switch-to',tab);
246
}else{
247
if(D.hasClass(e,'selected')){
248
return;
249
}
250
setVisible(e, false);
251
D.removeClass(btn,'selected');
252
}
253
});
254
return this;
255
}
256
};
257
258
F.TabManager = TabManager;
259
})(window.fossil);
260

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button