1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 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) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates an empty shortcuts page. 26 * @constructor 27 * @class 28 * This class represents a page that allows the user to specify custom 29 * keyboard shortcuts. Currently, we limit custom shortcuts to actions 30 * that involve a folder, tag, or saved search. The user specifies a 31 * number to refer to a particular organizer, which binds the shortcut 32 * to that organizer. For example, the user might assign the folder 33 * "test" the number 3, and then the shortcut "M,3" would move mail to 34 * that folder. 35 * <p> 36 * Only a single pref (the user's shortcuts gathered together in a string) 37 * is represented.</p> 38 * 39 * @author Conrad Damon 40 * 41 * @param {DwtControl} parent the containing widget 42 * @param {object} section the page 43 * @param {ZmPrefController} controller the prefs controller 44 * 45 * @extends ZmPreferencesPage 46 * 47 * @private 48 */ 49 ZmShortcutsPage = function(parent, section, controller) { 50 ZmPreferencesPage.apply(this, arguments); 51 }; 52 53 ZmShortcutsPage.prototype = new ZmPreferencesPage; 54 ZmShortcutsPage.prototype.constructor = ZmShortcutsPage; 55 56 ZmShortcutsPage.prototype.toString = 57 function () { 58 return "ZmShortcutsPage"; 59 }; 60 61 ZmShortcutsPage.prototype.hasResetButton = 62 function() { 63 return false; 64 }; 65 66 ZmShortcutsPage.prototype._createControls = 67 function(deferred) { 68 69 if (!appCtxt.getKeyboardMgr().__keyMapMgr) { 70 if (!deferred) { 71 appCtxt.getAppController().addListener(ZmAppEvent.POST_STARTUP, new AjxListener(this, this._createControls, [true])); 72 } 73 return; 74 } 75 76 var button = new DwtButton({parent:this}); 77 button.setText(ZmMsg.print); 78 var printButtonId = this._htmlElId + "_SHORTCUT_PRINT"; 79 var buttonDiv = document.getElementById(printButtonId); 80 buttonDiv.appendChild(button.getHtmlElement()); 81 button.addSelectionListener(new AjxListener(this, this._printListener)); 82 83 var col1 = {}; 84 col1.title = ZmMsg.shortcutsApp; 85 col1.type = ZmShortcutList.TYPE_APP; 86 col1.sort = true; 87 var list = new ZmShortcutList({style:ZmShortcutList.PREFS_STYLE, cols:[col1, ZmShortcutList.COL_SYS]}); 88 var listId = this._htmlElId + "_SHORTCUT_LIST"; 89 var listDiv = document.getElementById(listId); 90 listDiv.innerHTML = list.getContent(); 91 92 ZmPreferencesPage.prototype._createControls.call(this); 93 }; 94 95 ZmShortcutsPage.prototype._printListener = 96 function() { 97 var args = "height=650,width=900,location=no,menubar=yes,resizable=yes,scrollbars=yes,toolbar=no"; 98 var newWin = window.open("", "_blank", args); 99 100 var col1 = {}, col2 = {}; 101 col1.type = col2.type = ZmShortcutList.TYPE_APP; 102 col1.maps = ["global", "mail"]; 103 col2.omit = ["global", "mail"]; 104 col2.sort = true; 105 106 var list = new ZmShortcutList({style:ZmShortcutList.PRINT_STYLE, cols:[col1, col2, ZmShortcutList.COL_SYS]}); 107 108 var html = [], i = 0; 109 html[i++] = "<html><head>"; 110 html[i++] = "<link href='" + appContextPath + "/css/zm.css' rel='stylesheet' type='text/css' />"; 111 html[i++] = "</head><body>"; 112 html[i++] = "<div class='ShortcutsPrintHeader'>" + ZmMsg.keyboardShortcuts + "</div>"; 113 114 var doc = newWin.document; 115 doc.write(html.join("")); 116 117 var content = list.getContent(); 118 doc.write(content); 119 doc.write("</body></html>"); 120 121 doc.close(); 122 }; 123 124 125 /** 126 * Displays shortcuts in some sort of list. 127 * 128 * @param params 129 * @private 130 */ 131 ZmShortcutList = function(params) { 132 133 this._style = params.style; 134 if (!ZmShortcutList.modifierKeys) { 135 ZmShortcutList.modifierKeys = this._getModifierKeys(); 136 } 137 this._content = this._renderShortcuts(params.cols); 138 }; 139 140 ZmShortcutList.prototype = new DwtControl; 141 ZmShortcutList.prototype.constructor = ZmShortcutList; 142 143 ZmShortcutList.PREFS_STYLE = "prefs"; 144 ZmShortcutList.PRINT_STYLE = "print"; 145 ZmShortcutList.PANEL_STYLE = "panel"; 146 147 ZmShortcutList.TYPE_APP = "APP"; 148 ZmShortcutList.TYPE_SYS = "SYS"; 149 150 ZmShortcutList.COL_SYS = {}; 151 ZmShortcutList.COL_SYS.title = ZmMsg.shortcutsSys; 152 ZmShortcutList.COL_SYS.type = ZmShortcutList.TYPE_SYS; 153 ZmShortcutList.COL_SYS.sort = true; 154 ZmShortcutList.COL_SYS.maps = ["button", "menu", "list", "tree", "dialog", "toolbarHorizontal", 155 "editor", "tabView"]; 156 157 ZmShortcutList.prototype.getContent = 158 function() { 159 return this._content; 160 }; 161 162 // Set up map for interpolating modifier keys 163 ZmShortcutList.prototype._getModifierKeys = function() { 164 165 var modifierKeys = {}, 166 regex = /^keys\.\w+\.display$/, 167 keys = AjxUtil.filter(AjxUtil.keys(AjxKeys), function(key) { 168 return regex.test(key); 169 }); 170 171 for (var i = 0; i < keys.length; i++) { 172 var key = keys[i], 173 parts = key.split('.'); 174 175 modifierKeys[parts[1]] = AjxKeys[key]; 176 } 177 178 return modifierKeys; 179 }; 180 181 /** 182 * Displays shortcut documentation as a set of columns. 183 * 184 * @param cols [array] list of columns; each column may have: 185 * maps [array]* list of maps to show in this column; if absent, show all maps 186 * omit [array]* list of maps not to show; all others are shown 187 * title [string]* text for column header 188 * type [constant] app or sys 189 * sort [boolean]* if true, sort list of maps based on .sort values in props file 190 * 191 * @private 192 */ 193 ZmShortcutList.prototype._renderShortcuts = function(cols) { 194 195 var html = []; 196 var i = 0; 197 html[i++] = "<div class='ZmShortcutList'>"; 198 for (j = 0; j < cols.length; j++) { 199 i = this._getKeysHtml(cols[j], html, i); 200 } 201 html[i++] = "</div>"; 202 203 return html.join(""); 204 }; 205 206 ZmShortcutList.prototype._getKeysHtml = function(params, html, i) { 207 208 var keys = (params.type == ZmShortcutList.TYPE_APP) ? ZmKeys : AjxKeys; 209 var kmm = appCtxt.getKeyboardMgr().__keyMapMgr; 210 var mapDesc = {}, mapsFound = [], mapsHash = {}, keySequences = {}, mapsToShow = {}, mapsToOmit = {}; 211 if (params.maps) { 212 for (var k = 0; k < params.maps.length; k++) { 213 mapsToShow[params.maps[k]] = true; 214 } 215 } 216 if (params.omit) { 217 for (var k = 0; k < params.omit.length; k++) { 218 mapsToOmit[params.omit[k]] = true; 219 } 220 } 221 for (var propName in keys) { 222 var propValue = keys[propName]; 223 if (!propValue || (typeof propValue != "string")) { continue; } 224 var parts = propName.split("."); 225 var map = parts[0]; 226 if ((params.maps && !mapsToShow[map]) || (params.omit && mapsToOmit[map])) { continue; } 227 var isMap = (parts.length == 2); 228 var action = isMap ? null : parts[1]; 229 var field = parts[parts.length - 1]; 230 231 if (action && (map != ZmKeyMap.MAP_CUSTOM)) { 232 // make sure shortcut is defined && available 233 var ks = kmm.getKeySequences(map, action); 234 if (!(ks && ks.length)) { continue; } 235 } 236 if (field == "description") { 237 if (isMap) { 238 mapsFound.push(map); 239 mapsHash[map] = true; 240 mapDesc[map] = propValue; 241 } else { 242 keySequences[map] = keySequences[map] || []; 243 keySequences[map].push([map, action].join(".")); 244 } 245 } 246 } 247 248 var sortFunc = function(keyA, keyB) { 249 var sortPropNameA = [keyA, "sort"].join("."); 250 var sortPropNameB = [keyB, "sort"].join("."); 251 var sortA = keys[sortPropNameA] ? Number(keys[sortPropNameA]) : 0; 252 var sortB = keys[sortPropNameB] ? Number(keys[sortPropNameB]) : 0; 253 return (sortA > sortB) ? 1 : (sortA < sortB) ? -1 : 0; 254 } 255 var maps = []; 256 if (params.sort || !params.maps) { 257 mapsFound.sort(sortFunc); 258 maps = mapsFound; 259 } else { 260 for (var j = 0; j < params.maps.length; j++) { 261 var map = params.maps[j]; 262 if (mapsHash[map]) { 263 maps.push(map); 264 } 265 } 266 } 267 268 for (var j = 0; j < maps.length; j++) { 269 var map = maps[j]; 270 if (!keySequences[map]) { continue; } 271 var mapDesc = keys[[map, "description"].join(".")]; 272 html[i++] = "<dl class='" + ZmShortcutList._getClass("shortcutListMap", this._style) + "'>"; 273 html[i++] = "<lh class='title' role='header' aria-level='3'>" + mapDesc + "</lh>"; 274 275 var actions = keySequences[map]; 276 if (actions && actions.length) { 277 actions.sort(sortFunc); 278 for (var k = 0; k < actions.length; k++) { 279 var action = actions[k]; 280 var ks = ZmShortcutList._formatDisplay(keys[[action, "display"].join(".")]); 281 var desc = keys[[action, "description"].join(".")]; 282 var keySeq = ks.split(/\s*;\s*/); 283 var keySeq1 = []; 284 for (var m = 0; m < keySeq.length; m++) { 285 html[i++] = "<dt>" + ZmShortcutList._formatKeySequence(keySeq[m], this._style) + "</dt>"; 286 html[i++] = "<dd>" + desc + "</dd>"; 287 } 288 } 289 } 290 html[i++] = "</dl>"; 291 } 292 293 return i; 294 }; 295 296 // Replace {mod} with the proper localized and/or platform-specific version, eg 297 // replace {meta} with Cmd, or, in German, {ctrl} with Strg. 298 ZmShortcutList._formatDisplay = function(keySeq) { 299 return keySeq.replace(/\{(\w+)\}/g, function(match, p1) { 300 return ZmShortcutList.modifierKeys[p1]; 301 }); 302 }; 303 304 305 // Translates a key sequence into a friendlier, more readable version 306 ZmShortcutList._formatKeySequence = 307 function(ks, style) { 308 309 var html = []; 310 var i = 0; 311 html[i++] = "<span class='" + ZmShortcutList._getClass("shortcutKeyCombo", style) + "'>"; 312 313 var keys = ((ks[ks.length - 1] != DwtKeyMap.SEP) && (ks != DwtKeyMap.SEP)) ? ks.split(DwtKeyMap.SEP) : [ks]; 314 for (var j = 0; j < keys.length; j++) { 315 var key = keys[j]; 316 var parts = key.split(DwtKeyMap.JOIN); 317 var baseIdx = parts.length - 1; 318 // base can be: printable char or escaped char name (eg "Comma") 319 var base = parts[baseIdx]; 320 if (ZmKeyMap.ENTITY[base]) { 321 base = ZmKeyMap.ENTITY[base]; 322 } 323 parts[baseIdx] = base; 324 var newParts = []; 325 for (var k = 0; k < parts.length; k++) { 326 newParts.push(ZmShortcutList._formatKey(parts[k], style)); 327 } 328 html[i++] = newParts.join("+"); 329 } 330 html[i++] = "</span>"; 331 332 return html.join(""); 333 }; 334 335 ZmShortcutList._formatKey = 336 function(key, style) { 337 return ["<span class='", ZmShortcutList._getClass("shortcutKey", style), "'>", key, "</span>"].join(""); 338 }; 339 340 /** 341 * Returns a string with two styles in it, a base style and a modifier, eg "shortcutListMap prefs". 342 * 343 * @param base [string] base style 344 * @param style [string] style modifier 345 * 346 * @private 347 */ 348 ZmShortcutList._getClass = 349 function(base, style) { 350 return [base, style].join(" "); 351 }; 352 353 354 ZmShortcutsPanel = function() { 355 356 ZmShortcutsPanel.INSTANCE = this; 357 var className = appCtxt.isChildWindow ? "ZmShortcutsWindow" : "ZmShortcutsPanel"; 358 DwtControl.call(this, {parent:appCtxt.getShell(), className:className, posStyle:Dwt.ABSOLUTE_STYLE}); 359 360 this._createHtml(); 361 362 this._tabGroup = new DwtTabGroup(this.toString()); 363 this._tabGroup.addMember(this); 364 }; 365 366 ZmShortcutsPanel.prototype = new DwtControl; 367 ZmShortcutsPanel.prototype.constructor = ZmShortcutsPanel; 368 369 ZmShortcutsPanel.prototype.toString = 370 function() { 371 return "ZmShortcutsPanel"; 372 } 373 374 ZmShortcutsPanel.prototype.popup = 375 function(cols) { 376 var kbMgr = appCtxt.getKeyboardMgr(); 377 kbMgr.pushDefaultHandler(this); 378 this._cols = cols; 379 Dwt.setZIndex(appCtxt.getShell()._veilOverlay, Dwt.Z_VEIL); 380 var list = new ZmShortcutList({style:ZmShortcutList.PANEL_STYLE, cols:cols}); 381 this._contentDiv.innerHTML = list.getContent(); 382 if (!appCtxt.isChildWindow) { 383 this._position(); 384 } 385 this._contentDiv.scrollTop = 0; 386 kbMgr.pushTabGroup(this._tabGroup); 387 }; 388 389 ZmShortcutsPanel.prototype.popdown = 390 function(maps) { 391 var kbMgr = appCtxt.getKeyboardMgr(); 392 kbMgr.popTabGroup(this._tabGroup); 393 this.setLocation(Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE); 394 Dwt.setZIndex(appCtxt.getShell()._veilOverlay, Dwt.Z_HIDDEN); 395 kbMgr.popDefaultHandler(); 396 }; 397 398 ZmShortcutsPanel.prototype.handleKeyEvent = 399 function(ev) { 400 if (ev && ev.charCode === 27) { 401 ZmShortcutsPanel.closeCallback(); 402 return true; 403 } 404 }; 405 406 ZmShortcutsPanel.prototype._createHtml = function() { 407 408 var headerId = [this._htmlElId, "header"].join("_"); 409 var containerId = [this._htmlElId, "container"].join("_"); 410 var contentId = [this._htmlElId, "content"].join("_"); 411 var html = []; 412 var i = 0; 413 html[i++] = "<div class='ShortcutsPanelHeader' id='" + headerId + "'>"; 414 html[i++] = "<div class='title' role='header' aria-level='2'>" + ZmMsg.keyboardShortcuts + "</div>"; 415 // set up HTML to create two columns using floats 416 html[i++] = "<div class='container' id='" + containerId + "'>"; 417 html[i++] = "<div class='description'>" + ZmMsg.shortcutsCurrent + "</div>"; 418 html[i++] = "<div class='actions'>"; 419 html[i++] = "<span class='link' onclick='ZmShortcutsPanel.closeCallback();'>" + ZmMsg.close + "</span>"; 420 if (!appCtxt.isChildWindow) { 421 html[i++] = "<br /><span class='link' onclick='ZmShortcutsPanel.newWindowCallback();'>" + ZmMsg.newWindow + "</span>"; 422 } 423 html[i++] = "</div></div></div>"; 424 html[i++] = "<hr />"; 425 html[i++] = "<div id='" + contentId + "' style='overflow:auto;width: 100%;'></div>"; 426 427 this.getHtmlElement().innerHTML = html.join(""); 428 this._headerDiv = document.getElementById(headerId); 429 this._contentDiv = document.getElementById(contentId); 430 var headerHeight = Dwt.getSize(this._headerDiv).y; 431 var containerHeight = Dwt.getSize(containerId).y; 432 var h = this.getSize().y - headerHeight - containerHeight; 433 Dwt.setSize(this._contentDiv, Dwt.DEFAULT, h - 20); 434 this.setZIndex(Dwt.Z_DIALOG); 435 }; 436 437 ZmShortcutsPanel.closeCallback = 438 function() { 439 if (appCtxt.isChildWindow) { 440 window.close(); 441 } else { 442 ZmShortcutsPanel.INSTANCE.popdown(); 443 } 444 }; 445 446 ZmShortcutsPanel.newWindowCallback = 447 function() { 448 var newWinObj = appCtxt.getNewWindow(false, 820, 650); 449 if (newWinObj) { 450 newWinObj.command = "shortcuts"; 451 newWinObj.params = {cols:ZmShortcutsPanel.INSTANCE._cols}; 452 } 453 ZmShortcutsPanel.closeCallback(); 454 }; 455