1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 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, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates the filter rules view.
 26  * @class
 27  * This class represents the filters tab view in preferences application.
 28  * 
 29  * @param	{DwtComposite}	parent		the parent widget
 30  * @param	{ZmController}	controller	the controller
 31  *
 32  * @extends		DwtTabViewPage
 33  * 
 34  * @see		ZmPreferencesApp
 35  */
 36 ZmFilterRulesView = function(parent, controller) {
 37 
 38 	DwtTabViewPage.call(this, parent, "ZmPreferencesPage ZmFilterRulesView", Dwt.STATIC_STYLE);
 39 
 40 	this._controller = controller;
 41 	this._prefsController = AjxDispatcher.run("GetPrefController");
 42 
 43 	this._rules = AjxDispatcher.run(controller.isOutgoing() ? "GetOutgoingFilterRules" : "GetFilterRules");
 44 
 45 	var section = ZmPref.getPrefSectionWithPref(ZmSetting.FILTERS);
 46 	this._title = [ZmMsg.zimbraTitle, controller.getApp().getDisplayName(), section && section.title].join(": ");
 47 
 48 	this._rendered = false;
 49 
 50 	this._tabGroup = new DwtTabGroup(this._htmlElId);
 51 };
 52 
 53 ZmFilterRulesView.prototype = new DwtTabViewPage;
 54 ZmFilterRulesView.prototype.constructor = ZmFilterRulesView;
 55 
 56 ZmFilterRulesView.prototype.toString =
 57 function() {
 58 	return "ZmFilterRulesView";
 59 };
 60 
 61 ZmFilterRulesView.prototype.showMe =
 62 function() {
 63 	Dwt.setTitle(this._title);
 64 	var section = ZmPref.getPrefSectionWithPref(ZmSetting.FILTERS);
 65 
 66 	this._prefsController._resetOperations(this._prefsController._toolbar, section && section.id);
 67     //TODO: We got to optimize / avoid force-rendering logic for multi-account setup
 68 	if (this.hasRendered && !appCtxt.isOffline) { return; }
 69 
 70 	// create the html
 71 	var data = {id:this._htmlElId};
 72 	this.getHtmlElement().innerHTML = AjxTemplate.expand("prefs.Pages#MailFilter", data);
 73 
 74 	// create toolbar
 75 	var toolbarEl = Dwt.byId(data.id + "_toolbar");
 76 	if (toolbarEl) {
 77 		var buttons = this._controller.getToolbarButtons();
 78 		this._toolbar = new ZmButtonToolBar({parent:this, buttons:buttons, posStyle:Dwt.STATIC_STYLE,
 79 											 context:ZmId.VIEW_FILTER_RULES});
 80 		this._toolbar.replaceElement(toolbarEl);
 81 		this._tabGroup.addMember(this._toolbar);
 82 	}
 83 
 84 	// create list view
 85 	var listViewEl = Dwt.byId(data.id + "_list");
 86 	// add chooser
 87 	this._chooser = new ZmFilterRulesChooser(this._controller, {parent:this});
 88 	this._chooser.reparentHtmlElement(listViewEl + "_chooser");
 89 	var width = this._chooser.getWidth(this.parent);
 90 	var height = this._chooser.getHeight(this.parent);
 91 	this._chooser.resize(width, height); //still call this so the height is set correctly, but let's set the width to non specific values, to work better on resize:
 92 	//remove the width explicit setting and keep the current height setting.
 93 	this._chooser.sourceListView.setSize(Dwt.CLEAR, Dwt.DEFAULT);
 94 	this._chooser.targetListView.setSize(Dwt.CLEAR, Dwt.DEFAULT);
 95 	this._controller.initialize(this._toolbar, this._chooser.activeListView, this._chooser.notActiveListView);
 96 	this.hasRendered = true;
 97 };
 98 
 99 /**
100  * Gets the title.
101  * 
102  * @return	{String}	the title
103  */
104 ZmFilterRulesView.prototype.getTitle =
105 function() {
106 	return this._title;
107 };
108 
109 /**
110  * Gets the toolbar.
111  * 
112  * @return	{ZmButtonToolBar}		the toolbar
113  */
114 ZmFilterRulesView.prototype.getToolbar =
115 function() {
116 	return this._toolbar;
117 };
118 
119 /**
120  * Gets the list view.
121  * 
122  * @return	{DwtListView}	the list view
123  */
124 ZmFilterRulesView.prototype.getListView =
125 function() {
126 	return this._listView;
127 };
128 
129 /**
130  * Gets the tab group.
131  * 
132  * @return	{DwtTabGroup}		the tab group
133  */
134 ZmFilterRulesView.prototype.getTabGroupMember =
135 function() {
136 	return this._tabGroup;
137 };
138 
139 // View is always in sync with rules
140 ZmFilterRulesView.prototype.reset = function() {};
141 
142 ZmFilterRulesView.prototype.resetOnAccountChange =
143 function() {
144 	this.hasRendered = false;
145 };
146 
147 /**
148  * Creates a filter rule chooser.
149  * @class
150  * This class creates a specialized chooser for the filter rule list view.
151  *
152  * @param {ZmController}	controller			the filter rule controller
153  * @param {hash}		params		chooser params
154  * 
155  * @extends		DwtChooser
156  * 
157  * @private
158  */
159 ZmFilterRulesChooser = function(controller, params) {
160 	this._controller = controller;
161 	DwtChooser.call(this, params);
162 	this._rules = AjxDispatcher.run(controller.isOutgoing() ? "GetOutgoingFilterRules" : "GetFilterRules");
163 	this._rules.addChangeListener(new AjxListener(this, this._changeListener));
164 };
165 
166 ZmFilterRulesChooser.prototype = new DwtChooser;
167 ZmFilterRulesChooser.prototype.constructor = ZmFilterRulesChooser;
168 ZmFilterRulesChooser.MOVE_UP_BTN_ID = "__moveUp__";
169 ZmFilterRulesChooser.MOVE_DOWN_BTN_ID = "__moveDown__";
170 ZmFilterRulesChooser.CHOOSER_HEIGHT = 300;
171 ZmFilterRulesChooser.CHOOSER_WIDTH = 300;
172 ZmFilterRulesChooser.WIDTH_FUDGE = 111; //if button size not this sets the correct width
173 ZmFilterRulesChooser.HEIGHT_FUDGE = 200;
174 /**
175  * @private
176  */
177 ZmFilterRulesChooser.prototype._createSourceListView =
178 function() {
179 	return new ZmFilterChooserNotActiveListView(this);
180 };
181 
182 /**
183  * @private
184  */
185 ZmFilterRulesChooser.prototype._createTargetListView =
186 function() {
187 	return new ZmFilterChooserActiveListView(this);
188 };
189 
190 ZmFilterRulesChooser.prototype._initialize = 
191 function() {
192 	DwtChooser.prototype._initialize.call(this);
193 	this._moveUpButtonId = Dwt.getNextId();
194 	this._moveUpButton = this._setupButton(ZmFilterRulesChooser.MOVE_UP_BTN_ID, this._moveUpButtonId, this._moveUpButtonDivId, ZmMsg.filterMoveUp);
195 	this._moveUpButton.addSelectionListener(new AjxListener(this._controller, this._controller.moveUpListener));
196 	this._moveUpButton.setImage("UpArrow");
197 	this._moveUpButton.setEnabled(false);
198 	this._moveDownButtonId = Dwt.getNextId();
199 	this._moveDownButton = this._setupButton(ZmFilterRulesChooser.MOVE_DOWN_BTN_ID, this._moveDownButtonId, this._moveDownButtonDivId, ZmMsg.filterMoveDown);
200 	this._moveDownButton.addSelectionListener(new AjxListener(this._controller, this._controller.moveDownListener));
201 	this._moveDownButton.setImage("DownArrow");
202 	this._moveDownButton.setEnabled(false);
203 	this._removeButton.setEnabled(false);
204 	this._removeButton.setAlign(DwtLabel.IMAGE_RIGHT);
205 	this._removeButton.setImage("RightDoubleArrow");
206 	this._removeButton.setEnabled(false);
207 	this._transferButton =  this._button[this._buttonInfo[0].id];
208 	this._transferButton.setImage("LeftDoubleArrow");
209 	this._transferButton.setEnabled(false);
210 	this.notActiveListView = this.sourceListView;
211 	this.activeListView = this.targetListView;
212     AjxUtil.foreach([this._moveUpButton, this._moveDownButton, this._removeButton, this._transferButton],
213     function(item){
214             var htmlElement = item.getHtmlElement();
215             if (htmlElement && htmlElement.firstChild) htmlElement.firstChild.style.width = "100%";
216     });
217 };
218 
219 ZmFilterRulesChooser.prototype._createHtml = 
220 function() {
221 
222 	this._sourceListViewDivId	= Dwt.getNextId();
223 	this._targetListViewDivId	= Dwt.getNextId();
224 	this._buttonsDivId			= Dwt.getNextId();
225 	this._removeButtonDivId		= Dwt.getNextId();
226 	this._moveUpButtonDivId		= Dwt.getNextId();
227 	this._moveDownButtonDivId	= Dwt.getNextId();
228 	var data = {
229 		        targetDivId: this._targetListViewDivId,
230 		        sourceDivId: this._sourceListViewDivId,
231 				buttonsDivId: this._buttonsDivId,
232 				transferButtonId: this._buttonDivId[this._buttonInfo[0].id],
233 				removeButtonId: this._removeButtonDivId,
234 				moveUpButtonId: this._moveUpButtonDivId,
235 				moveDownButtonId: this._moveDownButtonDivId
236 				};
237 	this.getHtmlElement().innerHTML = AjxTemplate.expand("prefs.Pages#MailFilterListView", data);
238 };
239 
240 /**
241  * In general, we just re-display all the rules when anything changes, rather
242  * than trying to update a particular row.
243  *
244  * @param {DwtEvent}	ev		the event
245  * 
246  * @private
247  */
248 ZmFilterRulesChooser.prototype._changeListener =
249 function(ev) {
250 	if (ev.type != ZmEvent.S_FILTER) {
251 		AjxDebug.println(AjxDebug.FILTER, "FILTER RULES: ev.type is not S_FILTER; ev.type == " + ev.type);
252 		return;
253 	}
254 	AjxDebug.println(AjxDebug.FILTER, "FILTER RULES: ev.type == " + ev.type);
255 	if (ev.event == ZmEvent.E_MODIFY) {
256 		this._controller.resetListView(ev.getDetail("index"));
257 		AjxDebug.println(AjxDebug.FILTER, "FILTER RULES: MODIFY event, called resetListview");
258 		if (ev.source && ev.source.getNumberOfRules() == 0) {
259 			this._enableButtons(); //disable transfer buttons
260 		}
261 	}
262 };
263 
264 /**
265  * Clicking a transfer button moves selected items to the target list.
266  *
267  * @param {DwtEvent}		ev		the click event
268  * 
269  * @private
270  */
271 ZmFilterRulesChooser.prototype._transferButtonListener =
272 function(ev) {
273 	var button = DwtControl.getTargetControl(ev);
274 	var sel = this.notActiveListView.getSelection();
275 	if (sel && sel.length) {
276 		this.transfer(sel);
277 		var list = this.notActiveListView.getList();
278 		if (list && list.size()) {
279 			this._selectFirst(DwtChooserListView.SOURCE);
280 		} else {
281 			this._enableButtons();
282 		}
283 	} 
284 };
285 
286 /**
287 * Moves or copies items from the source list to the target list, paying attention
288 * to current mode.
289 *
290 * @param {AjxVector|array|Object|hash} list a list of items or hash of lists
291 * @param {string} id the ID of the transfer button that was used
292 * @param {boolean} skipNotify if <code>true</code>, do not notify listeners
293 */
294 ZmFilterRulesChooser.prototype.transfer =
295 function(list, id, skipNotify) {
296 	DwtChooser.prototype.transfer.call(this, list, id, skipNotify);
297 	for (var i=0; i<list.length; i++) {
298 		var rule = this._rules.getRuleByName(list[i].name);
299 		if (rule) {
300 			rule.active = true;
301 			this._rules.moveToBottom(rule, true);
302 		}
303 	}
304 	this._rules.saveRules(0, true);
305 };
306 
307 /**
308  * Removes items from target list, paying attention to current mode. Also handles button state.
309  *
310  * @param {AjxVector|array|Object|hash}	list			a list of items or hash of lists
311  * @param {boolean}	skipNotify	if <code>true</code>, do not notify listeners
312  */
313 ZmFilterRulesChooser.prototype.remove =
314 function(list, skipNotify) {
315 	DwtChooser.prototype.remove.call(this, list, skipNotify);
316 	for (var i=0; i<list.length; i++) {
317 		var rule = this._rules.getRuleByName(list[i].name);
318 		if (rule) {
319 			rule.active = false;
320 			this._rules.moveToBottom(rule, true);
321 		}
322 	}
323 	this._rules.saveRules(0, true);
324 };
325 
326 /**
327  * Removes an item from the target list.
328  *
329  * @param {Object}	item		the item to remove
330  * @param {boolean}	skipNotify	if <code>true</code>, don't notify listeners
331  * 
332  * @private
333  */
334 ZmFilterRulesChooser.prototype._removeFromTarget =
335 function(item, skipNotify) {
336 	if (!item) return;
337 	var list = this.activeListView.getList();
338 	if (!list) return;
339 	if (!list.contains(item)) return;
340 
341 	this.activeListView.removeItem(item, skipNotify);
342 };
343 
344 /**
345  * Enable/disable buttons as appropriate.
346  *
347  * @private
348  */
349 ZmFilterRulesChooser.prototype._enableButtons =
350 function(sForce, tForce) {
351 	DwtChooser.prototype._enableButtons.call(this, sForce, tForce);
352 	
353 	var activeEnabled = (sForce || (this.activeListView.getSelectionCount() > 0));
354 	var availableEnabled = (tForce || (this.notActiveListView.getSelectionCount() > 0));
355 	
356 	var listView = activeEnabled ? this.activeListView : this.notActiveListView;
357 	if (listView.getSelectionCount() > 1 || listView._list.size() <= 1) {
358 		this._moveUpButton.setEnabled(false);
359 		this._moveDownButton.setEnabled(false);
360 	}
361 	else if (listView.getSelectionCount() == 1) {
362 		var sel = listView.getSelection();
363 		var firstItem = listView._list.get(0);
364 		var lastItem = listView._list.get(listView._list.size()-1);
365 		if (firstItem && firstItem.id == sel[0].id) {
366 			this._moveUpButton.setEnabled(false);
367 			this._moveDownButton.setEnabled(true);
368 		}
369 		else if (lastItem && lastItem.id == sel[0].id) {
370 			this._moveDownButton.setEnabled(false);
371 			this._moveUpButton.setEnabled(true);
372 		}
373 		else {
374 			this._moveDownButton.setEnabled(true);
375 			this._moveUpButton.setEnabled(true);
376 		}
377 	}
378 };
379 
380 
381 /**
382  * Single-click selects an item, double-click adds selected items to target list.
383  *
384  * @param {DwtEvent}	ev		the click event
385  * 
386  * @private
387  */
388 ZmFilterRulesChooser.prototype._sourceListener =
389 function(ev) {
390 	if (this._activeButtonId == DwtChooser.REMOVE_BTN_ID) {
391 		// single-click activates appropriate transfer button if needed
392 		var id = this._lastActiveTransferButtonId ? this._lastActiveTransferButtonId : this._buttonInfo[0].id;
393 		this._setActiveButton(id);
394 	}
395 	this.targetListView.deselectAll();
396 	this._enableButtons();
397 };
398 
399 /**
400  * Single-click selects an item, double-click removes it from the target list.
401  *
402  * @param {DwtEvent}		ev		the click event
403  * 
404  * @private
405  */
406 ZmFilterRulesChooser.prototype._targetListener =
407 function(ev) {
408 	this._setActiveButton(DwtChooser.REMOVE_BTN_ID);
409 	this.sourceListView.deselectAll();
410 	this._enableButtons();
411 
412 };
413 
414 /**
415  * Calculates the chooser height based on the parent element height
416  * @param parent    {DwtControl} parent
417  * @return {int} height
418  */
419 ZmFilterRulesChooser.prototype.getHeight = 
420 function(parent) {
421 	if (!parent) {
422 		return ZmFilterRulesChooser.CHOOSER_HEIGHT;
423 	}
424 	var height = parseInt(parent.getHtmlElement().style.height);
425 	return height - ZmFilterRulesChooser.HEIGHT_FUDGE;
426 };
427 
428 /**
429  * calculates chooser width based on parent element width.
430  * @param parent {DwtControl} parent
431  * @return {int} width
432  */
433 ZmFilterRulesChooser.prototype.getWidth = 
434 function(parent) {
435 	if (!parent) {
436 		return ZmFilterRulesChooser.CHOOSER_WIDTH;
437 	}
438 
439 	var widthFudge = ZmFilterRulesChooser.WIDTH_FUDGE;
440 	var width = parseInt(parent.getHtmlElement().style.width);
441 	var buttonsDiv = document.getElementById(this._buttonsDivId);
442 	if (buttonsDiv) {
443 		var btnSz = Dwt.getSize(buttonsDiv); 
444 		if (btnSz && btnSz.x > 0) {
445 			widthFudge = ZmFilterRulesChooser.WIDTH_FUDGE - btnSz.x;
446 		}
447 	}
448 	
449 	return width - widthFudge;	
450 };		
451 
452 /**
453  * Creates a source list view.
454  * @class
455  * This class creates a specialized source list view for the contact chooser.
456  * 
457  * @param {DwtComposite}	parent			the contact picker
458  * 
459  * @extends		DwtChooserListView
460  * 
461  * @private
462  */
463 ZmFilterChooserActiveListView = function(parent) {
464 	DwtChooserListView.call(this, {parent:parent, type:DwtChooserListView.SOURCE});
465 	this.setScrollStyle(Dwt.CLIP);
466 };
467 
468 ZmFilterChooserActiveListView.prototype = new DwtChooserListView;
469 ZmFilterChooserActiveListView.prototype.constructor = ZmFilterChooserActiveListView;
470 
471 /**
472  * Returns a string representation of the object.
473  * 
474  * @return		{String}		a string representation of the object
475  * @private
476  */
477 ZmFilterChooserActiveListView.prototype.toString =
478 function() {
479 	return "ZmFilterChooserActiveListView";
480 };
481 
482 ZmFilterChooserActiveListView.prototype._getCellContents = 
483 function(html, idx, item, field, colIdx, params) {
484 	if (AjxEnv.isIE) {
485 		var maxWidth = AjxStringUtil.getWidth(item);
486 		html[idx++] = "<div style='float; left; overflow: visible; width: " + maxWidth + ";'>";
487 		html[idx++] = AjxStringUtil.htmlEncode(item.name);
488 		html[idx++] = "</div>";		
489 	}
490 	else {
491 		html[idx++] = AjxStringUtil.htmlEncode(item.name);
492 	}
493 	return idx;
494 };
495 
496 /**
497  * Only show active rules that have at least one valid action (eg, if the only action
498  * is "tag" and tagging is disabled, don't show the rule).
499  *
500  * @param list
501  * 
502  * @private
503  */
504 ZmFilterChooserActiveListView.prototype.set =
505 function(list) {
506 	var list1 = new AjxVector();
507 	var len = list.size();
508 	for (var i = 0; i < len; i++) {
509 		var rule = list.get(i);
510 		if (rule.hasValidAction() && rule.active) {
511 			list1.add(rule);
512 		}
513 	}
514 	DwtListView.prototype.set.call(this, list1);
515 };
516 
517 ZmFilterChooserActiveListView.prototype._getHeaderList =
518 function() {
519 	return [(new DwtListHeaderItem({text: ZmMsg.activeFilters}))];
520 };
521 
522 
523 /**
524  * Returns a string of any extra attributes to be used for the TD.
525  *
526  * @param item		[object]	item to render
527  * @param field		[constant]	column identifier
528  * @param params	[hash]*		hash of optional params
529  * 
530  * @private
531  */
532 ZmFilterChooserActiveListView.prototype._getCellAttrText =
533 function(item, field, params) {
534 	return "style='position: relative; overflow: visible;'";
535 };
536 
537 /**
538  * Creates the target list view.
539  * @class
540  * This class creates a specialized target list view for the contact chooser.
541  * 
542  * @param {DwtComposite}	parent			the contact picker
543  * @extends		DwtChooserListView
544  * 
545  * @private
546  */
547 ZmFilterChooserNotActiveListView = function(parent) {
548 	DwtChooserListView.call(this, {parent:parent, type:DwtChooserListView.TARGET});
549 	this.setScrollStyle(Dwt.CLIP);
550 };
551 
552 ZmFilterChooserNotActiveListView.prototype = new DwtChooserListView;
553 ZmFilterChooserNotActiveListView.prototype.constructor = ZmFilterChooserNotActiveListView;
554 
555 /**
556  * Returns a string representation of the object.
557  * 
558  * @return		{String}		a string representation of the object
559  */
560 ZmFilterChooserNotActiveListView.prototype.toString =
561 function() {
562 	return "ZmFilterChooserNotActiveListView";
563 };
564 
565 ZmFilterChooserNotActiveListView.prototype._getCellContents = 
566 function(html, idx, item, field, colIdx, params) {
567 	html[idx++] = AjxStringUtil.htmlEncode(item.name);
568 	return idx;
569 };
570 
571 ZmFilterChooserNotActiveListView.prototype._getHeaderList =
572 function() {
573 	return [(new DwtListHeaderItem({text: ZmMsg.availableFilters}))];
574 };
575 
576 /**
577  * Only show non-active rules that have at least one valid action (eg, if the only action
578  * is "tag" and tagging is disabled, don't show the rule).
579  *
580  * @param list
581  * 
582  * @private
583  */
584 ZmFilterChooserNotActiveListView.prototype.set =
585 function(list) {
586 	var list1 = new AjxVector();
587 	var len = list.size();
588 	for (var i = 0; i < len; i++) {
589 		var rule = list.get(i);
590 		if (rule.hasValidAction() && !rule.active) {
591 			list1.add(rule);
592 		}
593 	}
594 	DwtListView.prototype.set.call(this, list1);
595 };
596 
597 ZmFilterChooserNotActiveListView.prototype._getCellContents = 
598 function(html, idx, item, field, colIdx, params) {
599 	if (AjxEnv.isIE) {
600 		var maxWidth = AjxStringUtil.getWidth(item);
601 		html[idx++] = "<div style='float; left; overflow: visible; width: " + maxWidth + ";'>";
602 		html[idx++] = AjxStringUtil.htmlEncode(item.name);
603 		html[idx++] = "</div>";		
604 	}
605 	else {
606 		html[idx++] = AjxStringUtil.htmlEncode(item.name);
607 	}
608 	return idx;
609 };
610 
611 /**
612  * Returns a string of any extra attributes to be used for the TD.
613  *
614  * @param item		[object]	item to render
615  * @param field		[constant]	column identifier
616  * @param params	[hash]*		hash of optional params
617  * 
618  * @private
619  */
620 ZmFilterChooserNotActiveListView.prototype._getCellAttrText =
621 function(item, field, params) {
622 	return "style='position: relative; overflow: visible;'";
623 };
624