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