1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file defines a toolbar. 27 * 28 */ 29 30 /** 31 * Creates a toolbar. 32 * @constructor 33 * @class 34 * Creates a toolbar. Components must be added via the <code>add*()</code> functions. 35 * A toolbar is a horizontal or vertical strip of widgets (usually buttons). 36 * 37 * @author Ross Dargahi 38 * 39 * @param {hash} params a hash of parameters 40 * @param {DwtComposite} params.parent the parent widget 41 * @param {string} params.className the CSS class 42 * @param {DwtToolBar.HORIZ_STYLE|DwtToolBar.VERT_STYLE} params.posStyle the positioning style 43 * @param {constant} params.style the menu style 44 * @param {number} params.index the index at which to add this control among parent's children 45 * 46 * @extends DwtComposite 47 */ 48 DwtToolBar = function(params) { 49 if (arguments.length == 0) { return; } 50 params = Dwt.getParams(arguments, DwtToolBar.PARAMS); 51 52 params.className = params.className || "ZToolbar"; 53 DwtComposite.call(this, params); 54 55 // since we attach event handlers at the toolbar level, make sure we don't double up on 56 // handlers when we have a toolbar within a toolbar 57 if (params.parent instanceof DwtToolBar) { 58 this._hasSetMouseEvents = params.parent._hasSetMouseEvents; 59 } 60 if (params.handleMouse !== false && !this._hasSetMouseEvents) { 61 var events = [DwtEvent.ONMOUSEDOWN, DwtEvent.ONMOUSEUP, DwtEvent.ONCLICK]; 62 if (!AjxEnv.isIE) { 63 events.push(DwtEvent.ONMOUSEOVER, DwtEvent.ONMOUSEOUT); 64 } 65 this._setEventHdlrs(events); 66 this._hasSetMouseEvents = true; 67 } 68 69 this._style = params.style || DwtToolBar.HORIZ_STYLE; 70 this._createHtml(); 71 72 this._numFillers = 0; 73 this._curFocusIndex = 0; 74 75 // Let toolbar be a single tab stop, then manage focus among items using arrow keys 76 this.tabGroupMember = this; 77 78 this._keyMapName = (this._style == DwtToolBar.HORIZ_STYLE) ? DwtKeyMap.MAP_TOOLBAR_HORIZ : DwtKeyMap.MAP_TOOLBAR_VERT; 79 }; 80 81 DwtToolBar.PARAMS = ["parent", "className", "posStyle", "style", "index", "id"]; 82 83 DwtToolBar.prototype = new DwtComposite; 84 DwtToolBar.prototype.constructor = DwtToolBar; 85 DwtToolBar.prototype.role = 'toolbar'; 86 87 DwtToolBar.prototype.isDwtToolBar = true; 88 DwtToolBar.prototype.toString = function() { return "DwtToolBar"; }; 89 90 // 91 // Constants 92 // 93 94 /** 95 * Defines the "horizontal" style. 96 */ 97 DwtToolBar.HORIZ_STYLE = 1; 98 /** 99 * Defines the "vertical" style. 100 */ 101 DwtToolBar.VERT_STYLE = 2; 102 103 DwtToolBar.FIRST_ITEM = "ZFirstItem"; 104 DwtToolBar.LAST_ITEM = "ZLastItem"; 105 DwtToolBar.SELECTED_NEXT = DwtControl.SELECTED + "Next"; 106 DwtToolBar.SELECTED_PREV = DwtControl.SELECTED + "Prev"; 107 DwtToolBar._NEXT_PREV_RE = new RegExp( 108 "\\b" + 109 [ DwtToolBar.SELECTED_NEXT, DwtToolBar.SELECTED_PREV ].join("|") + 110 "\\b", "g" 111 ); 112 113 // 114 // Data 115 // 116 117 // main template 118 119 DwtToolBar.prototype.TEMPLATE = "dwt.Widgets#ZToolbar"; 120 121 // item templates 122 123 DwtToolBar.prototype.ITEM_TEMPLATE = "dwt.Widgets#ZToolbarItem"; 124 DwtToolBar.prototype.SEPARATOR_TEMPLATE = "dwt.Widgets#ZToolbarSeparator"; 125 DwtToolBar.prototype.SPACER_TEMPLATE = "dwt.Widgets#ZToolbarSpacer"; 126 DwtToolBar.prototype.FILLER_TEMPLATE = "dwt.Widgets#ZToolbarFiller"; 127 128 // static data 129 130 DwtToolBar.__itemCount = 0; 131 132 // 133 // Public methods 134 // 135 136 DwtToolBar.prototype.dispose = 137 function() { 138 DwtComposite.prototype.dispose.call(this); 139 this._itemsEl = null; 140 this._prefixEl = null; 141 this._suffixEl = null; 142 }; 143 144 /** 145 * Gets the item. 146 * 147 * @param {int} index the index 148 * @return {Object} the item 149 */ 150 DwtToolBar.prototype.getItem = 151 function(index) { 152 return this._children.get(index); 153 }; 154 155 /** 156 * Gets the item count. 157 * 158 * @return {number} the size of the children items 159 */ 160 DwtToolBar.prototype.getItemCount = 161 function() { 162 return this._children.size(); 163 }; 164 165 /** 166 * Gets the items. 167 * 168 * @return {array} an array of children items 169 */ 170 DwtToolBar.prototype.getItems = 171 function() { 172 return this._children.getArray(); 173 }; 174 175 // item creation 176 /** 177 * Adds a spacer. 178 * 179 * @param {string} className the spacer CSS class name 180 * @param {number} index the index for the spacer 181 * @return {Object} the newly added element 182 */ 183 DwtToolBar.prototype.addSpacer = 184 function(className, index) { 185 var spacer = new DwtToolBarSpacer({ 186 parent: this, 187 index: index, 188 className: className, 189 toolbarItemTemplate: this.SPACER_TEMPLATE, 190 id: this._htmlElId + '_spacer' + DwtToolBar.__itemCount 191 }); 192 193 return spacer; 194 }; 195 196 /** 197 * Adds a separator. 198 * 199 * @param {string} className the separator CSS class name 200 * @param {number} index the index for the separator 201 * @return {Object} the newly added element 202 */ 203 DwtToolBar.prototype.addSeparator = 204 function(className, index) { 205 var sep = new DwtToolBarSpacer({ 206 parent: this, 207 index: index, 208 className: className, 209 toolbarItemTemplate: this.SEPARATOR_TEMPLATE, 210 id: this._htmlElId + '_separator' + DwtToolBar.__itemCount 211 }); 212 213 return sep; 214 }; 215 216 /** 217 * Adds a filler. 218 * 219 * @param {string} className the CSS class name 220 * @param {number} index the index for the filler 221 * @return {Object} the newly added element 222 */ 223 DwtToolBar.prototype.addFiller = 224 function(className, index) { 225 var filler = new DwtToolBarSpacer({ 226 parent: this, 227 index: index, 228 className: className, 229 toolbarItemTemplate: this.FILLER_TEMPLATE, 230 id: this._htmlElId + '_filler' + DwtToolBar.__itemCount 231 }); 232 233 return filler; 234 }; 235 236 // DwtComposite methods 237 238 /** 239 * Adds a child item. 240 * 241 * @param {Object} child the child item 242 * @param {number} index the index for the child 243 */ 244 DwtToolBar.prototype.addChild = function(child, index) { 245 246 // get the reference element for insertion 247 var placeControl = this.getChild(index); 248 var placeEl = placeControl ? 249 placeControl.getHtmlElement().parentNode : this._suffixEl; 250 251 // actually add the child 252 DwtComposite.prototype.addChild.apply(this, arguments); 253 254 // create and insert the item element 255 if (this._itemsEl) { 256 var itemEl = this._createItemElement(child.toolbarItemTemplate); 257 this._itemsEl.insertBefore(itemEl, placeEl); 258 } 259 260 // finally, move the child to the item 261 child.reparentHtmlElement(itemEl); 262 }; 263 264 DwtToolBar.prototype.removeChild = function(child) { 265 266 var item = child.getHtmlElement().parentNode; 267 268 DwtComposite.prototype.removeChild.apply(this, arguments); 269 270 if (item && item.parentNode) { 271 item.parentNode.removeChild(item); 272 } 273 }; 274 275 // keyboard nav 276 277 /** 278 * Gets the key map name. 279 * 280 * @return {string} the key map name 281 */ 282 DwtToolBar.prototype.getKeyMapName = 283 function() { 284 return this._keyMapName; 285 }; 286 287 DwtToolBar.prototype.handleKeyAction = function(actionCode, ev) { 288 289 // if the user typed a left or right arrow in an INPUT, only go to the previous/next item if the cursor is at the 290 // beginning or end of the text in the INPUT 291 var curFocusIndex = this._curFocusIndex, 292 numItems = this.getItemCount(), 293 input = ev && ev.target && ev.target.nodeName.toLowerCase() === 'input' ? ev.target : null, 294 cursorPos = input && input.selectionStart, 295 valueLen = input && input.value && input.value.length; 296 297 if (numItems < 2) { 298 return false; 299 } 300 301 DBG.println(AjxDebug.FOCUS, 'DwtToolBar HANDLEKEYACTION: cur focus index = ' + curFocusIndex); 302 303 switch (actionCode) { 304 305 case DwtKeyMap.PREV: 306 if (input && cursorPos > 0) { 307 ev.forcePropagate = true; // don't let subsequent handlers block propagation 308 return false; 309 } 310 else if (curFocusIndex > 0) { 311 this._moveFocus(true, ev); 312 return true; 313 } 314 break; 315 316 case DwtKeyMap.NEXT: 317 if (input && cursorPos < valueLen) { 318 ev.forcePropagate = true; // don't let subsequent handlers block propagation 319 return false; 320 } 321 else if (curFocusIndex < numItems - 1) { 322 this._moveFocus(false); 323 return true; 324 } 325 break; 326 327 default: 328 // pass everything else to currently focused item 329 var item = this._getCurrentFocusItem(); 330 if (item) { 331 return item.handleKeyAction(actionCode, ev); 332 } 333 } 334 335 return true; 336 }; 337 338 // 339 // Protected methods 340 // 341 342 // utility 343 344 /** 345 * @private 346 */ 347 DwtToolBar.prototype._createItemId = 348 function(id) { 349 id = id || this._htmlElId; 350 var itemId = [id, "item", ++DwtToolBar.__itemCount].join("_"); 351 return itemId; 352 }; 353 354 // html creation 355 356 /** 357 * @private 358 */ 359 DwtToolBar.prototype._createHtml = function() { 360 361 var data = { id: this._htmlElId }; 362 this._createHtmlFromTemplate(this.TEMPLATE, data); 363 this._itemsEl = document.getElementById(data.id + "_items"); 364 this._prefixEl = document.getElementById(data.id + "_prefix"); 365 this._suffixEl = document.getElementById(data.id + "_suffix"); 366 }; 367 368 /** 369 * @private 370 */ 371 DwtToolBar.prototype._createItemElement = 372 function(templateId) { 373 templateId = templateId || this.ITEM_TEMPLATE; 374 var data = { id: this._htmlElId, itemId: this._createItemId() }; 375 var html = AjxTemplate.expand(templateId, data); 376 377 // the following is like scratching your back with your heel: 378 // var fragment = Dwt.toDocumentFragment(html, data.itemId); 379 // return (AjxUtil.getFirstElement(fragment)); 380 381 var cont = AjxStringUtil.calcDIV(); 382 cont.innerHTML = html; 383 return cont.firstChild.rows[0].cells[0]; // DIV->TABLE->TR->TD 384 }; 385 386 /** 387 * Focuses the current item. 388 * 389 * @param {DwtControl} item (optional) specific toolbar item to focus 390 */ 391 DwtToolBar.prototype.focus = function(item) { 392 393 DBG.println(AjxDebug.FOCUS, "DwtToolBar: FOCUS " + [this, this._htmlElId].join(' / ')); 394 395 this._setMenuKey(); 396 397 item = item || this._getCurrentFocusItem(); 398 if (item && this._canFocusItem(item)) { 399 this._curFocusIndex = this.__getButtonIndex(item); 400 return item.focus(); 401 } 402 else { 403 // if current item isn't focusable, find first one that is 404 return this._moveFocus(false); 405 } 406 }; 407 408 /** 409 * Blurs the current item. 410 * 411 * @param {DwtControl} item (optional) specific toolbar item to blur 412 * 413 * @private 414 */ 415 DwtToolBar.prototype.blur = function(item) { 416 417 DBG.println(AjxDebug.FOCUS, "DwtToolBar: BLUR"); 418 item = item || this._getCurrentFocusItem(); 419 if (item && item.blur) { 420 item.blur(); 421 } 422 }; 423 424 /** 425 * Returns the item at the given index, as long as it can accept focus. 426 * For now, we only move focus to simple components like buttons. Also, 427 * the item must be enabled and visible. 428 * 429 * @param {DwtControl} item an item within toolbar 430 * @return {boolean} true if the item can be focused 431 * 432 * @private 433 */ 434 DwtToolBar.prototype._canFocusItem = function(item) { 435 436 if (!item) { return false; } 437 if (!item.focus) { return false; } 438 if (item.isDwtToolBarSpacer) { return false; } 439 if (item.getEnabled && !item.getEnabled()) { return false; } 440 if (item.getVisible && !item.getVisible()) { return false; } 441 if (item.isDwtText && !item.getText()) { return false; } 442 443 return true; 444 }; 445 446 DwtToolBar.prototype._getCurrentFocusItem = function() { 447 448 return this.getItem(this._curFocusIndex); 449 }; 450 451 DwtToolBar.prototype.getEnabled = function() { 452 // toolbars delegate focus to their children, and so are only 'enabled' -- 453 // i.e. focusable -- when at least one child is 454 return this._children.some(function(child) { 455 return this._canFocusItem(child); 456 }, this); 457 }; 458 459 /** 460 * Moves focus to next or previous item that can take focus. 461 * 462 * @param {boolean} back if <code>true</code>, move focus to previous item 463 * 464 * @private 465 */ 466 DwtToolBar.prototype._moveFocus = function(back) { 467 468 var index = this._curFocusIndex, 469 maxIndex = this.getItemCount() - 1, 470 item = null, 471 found = false; 472 473 index = back ? index - 1 : index + 1; 474 while (!found && index >= 0 && index <= maxIndex) { 475 item = this.getItem(index); 476 if (this._canFocusItem(item)) { 477 found = true; 478 } 479 index = back ? index - 1 : index + 1; 480 } 481 482 if (item && found) { 483 this.blur(); 484 this.focus(item); 485 } 486 487 return item; 488 }; 489 490 // make sure the key for expanding a button submenu matches our style 491 DwtToolBar.prototype._setMenuKey = function() { 492 493 if (!this._submenuKeySet) { 494 var kbm = this.shell.getKeyboardMgr(); 495 if (kbm.isEnabled()) { 496 var kmm = kbm.__keyMapMgr; 497 if (kmm) { 498 if (this._style == DwtToolBar.HORIZ_STYLE) { 499 kmm.removeMapping(DwtKeyMap.MAP_BUTTON, "ArrowRight"); 500 kmm.setMapping(DwtKeyMap.MAP_BUTTON, "ArrowDown", DwtKeyMap.SUBMENU); 501 } else { 502 kmm.removeMapping(DwtKeyMap.MAP_BUTTON, "ArrowDown"); 503 kmm.setMapping(DwtKeyMap.MAP_BUTTON, "ArrowRight", DwtKeyMap.SUBMENU); 504 } 505 kmm.reloadMap(DwtKeyMap.MAP_BUTTON); 506 } 507 } 508 this._submenuKeySet = true; 509 } 510 }; 511 512 // Updates internal index when a child gets focus 513 DwtToolBar.prototype._childFocusListener = function(ev) { 514 515 DBG.println(AjxDebug.FOCUS, "DwtToolBar CHILDFOCUSLISTENER: " + [ ev.dwtObj, ev.dwtObj._htmlElId ].join(' / ')); 516 this._curFocusIndex = this.__getButtonIndex(ev.dwtObj); 517 }; 518 519 /** 520 * @private 521 */ 522 DwtToolBar.prototype.__markPrevNext = function(id, opened) { 523 524 var index = this.__getButtonIndex(id); 525 var prev = this.getChild(index - 1); 526 var next = this.getChild(index + 1); 527 528 if (opened) { 529 if (prev) { 530 Dwt.delClass(prev.getHtmlElement(), DwtToolBar._NEXT_PREV_RE, DwtToolBar.SELECTED_PREV); 531 } 532 if (next) { 533 Dwt.delClass(next.getHtmlElement(), DwtToolBar._NEXT_PREV_RE, DwtToolBar.SELECTED_NEXT); 534 } 535 } 536 else { 537 if (prev) { 538 Dwt.delClass(prev.getHtmlElement(), DwtToolBar._NEXT_PREV_RE); 539 } 540 if (next) { 541 Dwt.delClass(next.getHtmlElement(), DwtToolBar._NEXT_PREV_RE); 542 } 543 } 544 545 // hack: mark the first and last items so we can style them specially 546 // MOW note: this should really not be here, as it only needs to be done once, 547 // but I'm not sure where to put it otherwise 548 var first = this.getChild(0); 549 if (first) { 550 Dwt.addClass(first.getHtmlElement(), DwtToolBar.FIRST_ITEM); 551 } 552 553 var last = this.getChild(this.getItemCount()-1); 554 if (last) { 555 Dwt.addClass(last.getHtmlElement(), DwtToolBar.LAST_ITEM); 556 } 557 }; 558 559 /** 560 * Find the array index of a toolbar button. 561 * 562 * @param id {String|DwtControl} item ID, or item 563 * 564 * @return {number} Index of the id in the array, or -1 if the id does not exist. 565 * @private 566 */ 567 DwtToolBar.prototype.__getButtonIndex = function(id) { 568 569 var item = AjxUtil.isString(id) ? DwtControl.fromElementId(id) : id; 570 571 for (var i = 0; i <= this.getItemCount() - 1; i++) { 572 if (item === this.getItem(i)) { 573 return i; 574 } 575 } 576 577 return -1; 578 }; 579 580 // 581 // Classes 582 // 583 584 /** 585 * Creates a tool bar button. 586 * @constructor 587 * @class 588 * This class represents a toolbar button. 589 * 590 * @param {hash} params a hash of parameters 591 * @param {DwtComposite} parent the parent widget 592 * @param {constant} style the menu style 593 * @param {string} className the CSS class 594 * @param {DwtToolBar.HORIZ_STYLE|DwtToolBar.VERT_STYLE} posStyle the positioning style 595 * @param {Object} actionTiming the action timing 596 * @param {string} id the id 597 * @param {number} index the index at which to add this control among parent's children 598 * 599 * @extends DwtButton 600 */ 601 DwtToolBarButton = function(params) { 602 if (arguments.length == 0) { return; } 603 var params = Dwt.getParams(arguments, DwtToolBarButton.PARAMS); 604 params.className = params.className || "ZToolbarButton"; 605 DwtButton.call(this, params); 606 }; 607 608 DwtToolBarButton.PARAMS = ["parent", "style", "className", "posStyle", "actionTiming", "id", "index"]; 609 610 DwtToolBarButton.prototype = new DwtButton; 611 DwtToolBarButton.prototype.constructor = DwtToolBarButton; 612 613 DwtToolBarButton.prototype.isDwtToolBarButton = true; 614 DwtToolBarButton.prototype.toString = function() { return "DwtToolBarButton"; }; 615 616 // Data 617 DwtToolBarButton.prototype.TEMPLATE = "dwt.Widgets#ZToolbarButton"; 618 619 // Spacing controls (spacer, separator, filler) 620 DwtToolBarSpacer = function(params) { 621 if (arguments.length == 0) { return; } 622 this._noFocus = this.noTab = true; 623 this.toolbarItemTemplate = params.toolbarItemTemplate; 624 DwtControl.call(this, params); 625 }; 626 627 DwtToolBarSpacer.prototype = new DwtControl; 628 629 DwtToolBarSpacer.prototype.constructor = DwtToolBarSpacer; 630 631 DwtToolBarSpacer.prototype.isDwtToolBarSpacer = true; 632 DwtToolBarSpacer.prototype.toString = function() { return 'DwtToolBarSpacer'; }; 633 634 DwtToolBarSpacer.prototype.role = 'separator'; 635