Fossil SCM

fossil-scm / src / fossil.tabs.js
Source Blame History 259 lines
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);

Keyboard Shortcuts

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