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