1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 ZmCalListView = function(parent, posStyle, controller, dropTgt) {
 25 	if (arguments.length == 0) { return; }
 26 
 27 	var params = {
 28 		parent: parent,
 29 		posStyle: posStyle,
 30 		controller: controller,
 31 		dropTgt: dropTgt,
 32 		view: ZmId.VIEW_CAL_LIST,
 33 		headerList: this._getHeaderList(parent),
 34 		pageless: true
 35 	};
 36 	ZmApptListView.call(this, params);
 37 
 38 	this._dateSearchBar = this._createSearchBar(parent);
 39 
 40 	this._needsRefresh = true;
 41 	this._timeRangeStart = 0;
 42 	this._timeRangeEnd = 0;
 43 	this._title = "";
 44 };
 45 
 46 ZmCalListView.prototype = new ZmApptListView;
 47 ZmCalListView.prototype.constructor = ZmCalListView;
 48 
 49 
 50 // Consts
 51 ZmCalListView.DEFAULT_CALENDAR_PERIOD	= AjxDateUtil.MSEC_PER_DAY * 14;			// 2 weeks
 52 ZmCalListView.DEFAULT_SEARCH_PERIOD		= AjxDateUtil.MSEC_PER_DAY * 400;			// 400 days (maximum supported by the server)
 53 
 54 
 55 // Public methods
 56 
 57 ZmCalListView.prototype.toString =
 58 function() {
 59 	return "ZmCalListView";
 60 };
 61 
 62 
 63 // ZmCalBaseView methods
 64 
 65 ZmCalListView.prototype.getTimeRange =
 66 function() {
 67 	return { start:this._timeRangeStart, end:this._timeRangeEnd };
 68 };
 69 
 70 ZmCalListView.prototype.getTitle =
 71 function() {
 72 	return [ZmMsg.zimbraTitle, this.getCalTitle()].join(": ");
 73 };
 74 
 75 ZmCalListView.prototype.getCalTitle =
 76 function() {
 77 	return this._title;
 78 };
 79 
 80 ZmCalListView.prototype.needsRefresh =
 81 function() {
 82 	return this._needsRefresh;
 83 };
 84 
 85 ZmCalListView.prototype.setNeedsRefresh =
 86 function(needsRefresh) {
 87 	this._needsRefresh = needsRefresh;
 88 };
 89 
 90 ZmCalListView.prototype.createHeaderHtml =
 91 function(defaultColumnSort) {
 92 	DwtListView.prototype.createHeaderHtml.call(this, defaultColumnSort, true);
 93 };
 94 
 95 ZmCalListView.prototype.getDate =
 96 function() {
 97 	return this._date;
 98 };
 99 
100 ZmCalListView.prototype.setDate =
101 function(date, duration, roll) {
102 	this._date = new Date(date.getTime());
103 
104 	var d = new Date(date.getTime());
105 	d.setHours(0, 0, 0, 0);
106 	this._timeRangeStart = d.getTime();
107 	this._timeRangeEnd = this._timeRangeStart + ZmCalListView.DEFAULT_CALENDAR_PERIOD;
108 
109 	this._updateTitle();
110 	this._segmentedDates = [];
111 
112 	// update widgets
113 	var startDate = new Date(this._timeRangeStart);
114 	var endDate = new Date(this._timeRangeEnd);
115 	this._startDateField.setValue(AjxDateUtil.simpleComputeDateStr(startDate));
116 	this._endDateField.setValue(AjxDateUtil.simpleComputeDateStr(endDate));
117 
118 	this._updateDateRange(startDate, endDate);
119 
120 	// Notify any listeners
121 	if (this.isListenerRegistered(DwtEvent.DATE_RANGE)) {
122 		if (!this._dateRangeEvent) {
123 			this._dateRangeEvent = new DwtDateRangeEvent(true);
124 		}
125 		this._dateRangeEvent.item = this;
126 		this._dateRangeEvent.start = new Date(this._timeRangeStart);
127 		this._dateRangeEvent.end = new Date(this._timeRangeEnd);
128 		this.notifyListeners(DwtEvent.DATE_RANGE, this._dateRangeEvent);
129 	}
130 };
131 
132 ZmCalListView.prototype.getRollField =
133 function() {
134 	return AjxDateUtil.TWO_WEEKS;
135 };
136 
137 ZmCalListView.prototype._fanoutAllDay =
138 function(appt) {
139 	return false;
140 };
141 
142 ZmCalListView.prototype._apptSelected =
143 function() {
144 	// do nothing
145 };
146 
147 ZmCalListView.prototype._updateTitle =
148 function() {
149 	var dayFormatter = DwtCalendar.getDayFormatter();
150 	var start = new Date(this._timeRangeStart);
151 	var end = new Date(this._timeRangeEnd);
152 
153 	this._title = [
154 		dayFormatter.format(start), " - ", dayFormatter.format(end)
155 	].join("");
156 };
157 
158 ZmCalListView.prototype._updateDateRange =
159 function(startDate, endDate) {
160 	var params = [
161 		AjxDateUtil._getMonthName(startDate, true),
162 		startDate.getDate(),
163 		AjxDateUtil._getMonthName(endDate, true),
164 		endDate.getDate()
165 	];
166 	this._dateRangeField.innerHTML = AjxMessageFormat.format(ZmMsg.viewCalListDateRange, params);
167 };
168 
169 ZmCalListView.prototype.addTimeSelectionListener =
170 function(listener) {
171 	// do nothing
172 };
173 
174 ZmCalListView.prototype.addDateRangeListener =
175 function(listener) {
176 	this.addListener(DwtEvent.DATE_RANGE, listener);
177 };
178 
179 ZmCalListView.prototype.addViewActionListener =
180 function(listener) {
181 	// do nothing
182 };
183 
184 
185 // DwtListView methods
186 
187 ZmCalListView.prototype.setBounds =
188 function(x, y, width, height) {
189 	// set height to 32px (plus 1px for bottom border) to adjust for the new date-range toolbar
190     if (this._dateSearchBar) {
191         this._dateSearchBar.setBounds(x, y, width, 33);
192         ZmListView.prototype.setBounds.call(this, x, y+33, width, height-33);
193     }
194     else {
195         ZmListView.prototype.setBounds.apply(this, arguments);
196     }
197 };
198 
199 ZmCalListView.prototype.setLocation = function(x, y) {
200     // HACK: setBounds calls setLocation so only relocate date search bar
201     // HACK: when the location is NOWHERE
202     if (this._dateSearchBar && x == Dwt.LOC_NOWHERE) {
203         this._dateSearchBar.setLocation(x, y);
204     }
205     ZmApptListView.prototype.setLocation.call(this, x, y);
206 };
207 
208 // NOTE: Currently setLocation is called with values of NOWHERE when they
209 // NOTE: want the control to disappear. But I'm adding an override for
210 // NOTE: setVisible as well to be defensive against future changes.
211 ZmCalListView.prototype.setVisible = function(visible) {
212     if (this._dateSearchBar) {
213         this._dateSearchBar.setVisible(visible);
214     }
215     ZmApptListView.prototype.setVisible.apply(this, arguments);
216 };
217 
218 ZmCalListView.prototype._mouseOverAction =
219 function(ev, div) {
220 	DwtListView.prototype._mouseOverAction.call(this, ev, div);
221 	var id = ev.target.id || div.id;
222 	if (!id) { return true; }
223 
224 	// check if we're hovering over a column header
225 	var data = this._data[div.id];
226 	var type = data.type;
227 	if (type && type == DwtListView.TYPE_HEADER_ITEM) {
228 		var itemIdx = data.index;
229 		var field = this._headerList[itemIdx]._field;
230         // Bug: 76489 - Added <span> as workaround to show tooltip as HTML
231         // The ideal fix should add a method in DwtControl to remove the tooltip
232 		this.setToolTipContent('<span>'+this._getHeaderToolTip(field, itemIdx)+'</span>');
233 	} else {
234 		var item = this.getItemFromElement(div);
235 		if (item) {
236 			var match = this._parseId(id);
237 			if (!match) { return; }
238 			this.setToolTipContent(this._getToolTip({field:match.field, item:item, ev:ev, div:div, match:match}));
239 			if (match.field != ZmItem.F_SELECTION && match.field != ZmItem.F_TAG && item.getToolTip) {
240 				// load attendee status if necessary
241 				if (item.otherAttendees && (item.ptstHashMap == null)) {
242 					var clone = ZmAppt.quickClone(item);
243 					var uid = this._currentMouseOverApptId = clone.getUniqueId();
244 					var callback = new AjxCallback(null, ZmApptViewHelper.refreshApptTooltip, [clone, this]);
245 					AjxTimedAction.scheduleAction(new AjxTimedAction(this, this.getApptDetails, [clone, callback, uid]), 2000);
246 				}
247 			}
248 		}
249 	}
250 	return true;
251 };
252 
253 ZmCalListView.prototype.getApptDetails =
254 function(appt, callback, uid) {
255 	if (this._currentMouseOverApptId &&
256 		this._currentMouseOverApptId == uid)
257 	{
258 		this._currentMouseOverApptId = null;
259 		appt.getDetails(null, callback, null, null, true);
260 	}
261 };
262 
263 ZmCalListView.prototype._createSearchBar = function(parent) {
264     var id = this._htmlElId;
265 
266     var searchBar = new DwtComposite({parent:parent, className:"ZmCalListViewSearchBar", posStyle:DwtControl.ABSOLUTE_STYLE});
267     searchBar.getHtmlElement().innerHTML = AjxTemplate.expand("calendar.Calendar#ListViewSearchBar",id);
268 
269     var controls = new DwtMessageComposite({
270         parent: searchBar,
271         parentElement: Dwt.byId(id+"_searchBarControls"),
272         format: ZmMsg.showApptsFromThrough,
273         controlCallback: this._createSearchBarComponent.bind(this),
274     });
275 
276     this._dateRangeField = document.getElementById(id+"_searchBarDate");
277     this._makeFocusable(this._dateRangeField);
278 
279     return searchBar;
280 };
281 
282 ZmCalListView.prototype._getSearchBarTabGroup = function() {
283 	if (!this._dateSearchBarTabGroup) {
284 		var tg = this._dateSearchBarTabGroup =
285 			new DwtTabGroup('ZmCalListView search');
286 
287 		tg.addMember([
288 			this._dateSearchBar.getChild(0).getTabGroupMember(),
289 			this._dateRangeField
290 		]);
291 	}
292 
293 	return this._dateSearchBarTabGroup;
294 }
295 
296 ZmCalListView.prototype._createSearchBarComponent = function(searchBar, segment, i) {
297     var isStart = segment.getIndex() == 0;
298     var id = this._htmlElId;
299     var prefix = isStart ? "_start" : "_end";
300 
301     var component = new DwtToolBar({parent:searchBar});
302 
303     var inputId = [id,prefix,"DateInput"].join("");
304     var input = new DwtInputField({id: inputId, parent: component});
305     Dwt.setHandler(input.getInputElement(), DwtEvent.ONCHANGE,
306                    this._onDatesChange.bind(this, isStart));
307 
308     var dateButtonListener = new AjxListener(this, this._dateButtonListener);
309     var dateCalSelectionListener = new AjxListener(this, this._dateCalSelectionListener);
310     var buttonId = [id,prefix,"MiniCal"].join("");
311     var button = ZmCalendarApp.createMiniCalButton(component, buttonId, dateButtonListener, dateCalSelectionListener, false);
312 
313     // this.getTabGroupMember().addMember([inputEl, button]);
314 
315     if (isStart) {
316         this._startDateField = input;
317         this._startDateField.setToolTipContent(ZmMsg.startDate);
318         this._startDateButton = button;
319     }
320     else {
321         this._endDateField = input;
322         this._endDateField.setToolTipContent(ZmMsg.endDate);
323         this._endDateButton = button;
324     }
325 
326     return component;
327 };
328 
329 /**
330  * Event listener triggered when user clicks on the down arrow button to bring
331  * up the date picker.
332  *
333  * @param ev		[Event]		Browser event
334  * @private
335  */
336 ZmCalListView.prototype._dateButtonListener =
337 function(ev) {
338 	var calDate = ev.item == this._startDateButton
339 		? AjxDateUtil.simpleParseDateStr(this._startDateField.getValue())
340 		: AjxDateUtil.simpleParseDateStr(this._endDateField.getValue());
341 
342 	// if date was input by user and its foobar, reset to today's date
343 	if (isNaN(calDate)) {
344 		calDate = new Date();
345 		var field = ev.item == this._startDateButton
346 			? this._startDateField : this._endDateField;
347 		field.setValue(AjxDateUtil.simpleComputeDateStr(calDate));
348 	}
349 
350 	// always reset the date to current field's date
351 	var menu = ev.item.getMenu();
352 	var cal = menu.getItem(0);
353 	cal.setDate(calDate, true);
354 	ev.item.popup();
355 
356     if(AjxEnv.isIE) {
357         //DwtMenu adds padding of 6px each side
358         //IE has to add 12px to width and height to adjust the calendar
359         var menuSize = menu.getSize();
360         menu.setSize(menuSize.x+12, menuSize.y+12);
361         menu.getHtmlElement().style.width = "180px";
362     }
363 };
364 
365 /**
366  * Event listener triggered when user selects date in the date-picker.
367  *
368  * @param ev		[Event]		Browser event
369  * @private
370  */
371 ZmCalListView.prototype._dateCalSelectionListener =
372 function(ev) {
373 	var parentButton = ev.item.parent.parent;
374 
375 	// update the appropriate field w/ the chosen date
376 	var field = (parentButton == this._startDateButton)
377 		? this._startDateField : this._endDateField;
378 	field.setValue(AjxDateUtil.simpleComputeDateStr(ev.detail));
379 
380 	// change the start/end date if they mismatch
381 	this._handleDateChange(parentButton == this._startDateButton);
382 };
383 
384 /**
385  * Called when user selects a new date from the date-picker. Normalizes the
386  * start/end dates if user chose start date to be after end date or vice versa.
387  * Also updates the UI with the new date ranges and initiates SearchRequest.
388  *
389  * @param isStartDate
390  * @private
391  */
392 ZmCalListView.prototype._handleDateChange =
393 function(isStartDate) {
394 	var start = AjxDateUtil.simpleParseDateStr(this._startDateField.getValue());
395 	var end = AjxDateUtil.simpleParseDateStr(this._endDateField.getValue());
396 
397 	var startTime = start.getTime();
398 	var endTime = end.getTime() + AjxDateUtil.MSEC_PER_DAY;
399 
400 	// normalize dates
401 	if (isStartDate && startTime >= endTime) {
402 		endTime = startTime + AjxDateUtil.MSEC_PER_DAY;
403 		end = new Date(endTime);
404 		this._endDateField.setValue(AjxDateUtil.simpleComputeDateStr(end));
405 	}
406 	else if (endTime <= startTime) {
407 		startTime = end.getTime() - AjxDateUtil.MSEC_PER_DAY;
408 		start = new Date(startTime);
409 		this._startDateField.setValue(AjxDateUtil.simpleComputeDateStr(start));
410 	}
411 
412 	this._timeRangeStart = startTime;
413 	this._timeRangeEnd = endTime;
414 
415 	this._updateDateRange(start, end);
416 	this._updateTitle();
417 
418 	this._segmentedDates = [];
419 
420 	this._segmentDates(startTime, endTime);
421 	this.set((new AjxVector()), null, true); // clear the current list
422 	this._search();
423 };
424 
425 /**
426  * Chunks the date range into intervals per the default search period. We do
427  * this to avoid taxing the server with a large date range.
428  *
429  * @param startTime		[String]	start time in ms
430  * @param endTime		[String]	end time in ms
431  * @private
432  */
433 ZmCalListView.prototype._segmentDates =
434 function(startTime, endTime) {
435 	var startPeriod = startTime;
436 	var endPeriod = startTime + ZmCalListView.DEFAULT_SEARCH_PERIOD;
437 
438 	// reset back to end time if we're search less than next block (e.g. two weeks)
439 	if (endPeriod > endTime) {
440 		endPeriod = endTime;
441 	}
442 
443 	do {
444 		this._segmentedDates.push({startTime: startPeriod, endTime: endPeriod});
445 
446 		startPeriod += ZmCalListView.DEFAULT_SEARCH_PERIOD;
447 
448 		var newEndPeriod = endPeriod + ZmCalListView.DEFAULT_SEARCH_PERIOD;
449 		endPeriod = (newEndPeriod > endTime) ? endTime : newEndPeriod;
450 	}
451 	while (startPeriod < endTime);
452 };
453 
454 /**
455  * Makes a SearchRequest for the first chunk of appointments
456  *
457  * @private
458  */
459 ZmCalListView.prototype._search =
460 function() {
461 	var dates = this._segmentedDates.shift();
462 
463 	var params = {
464 		start: dates.startTime,
465 		end: dates.endTime,
466 		folderIds: this._controller.getCheckedCalendarFolderIds(),
467 		callback: (new AjxCallback(this, this._handleSearchResponse)),
468 		noBusyOverlay: true,
469 		query: this._controller._userQuery
470 	};
471 
472 	this._controller.apptCache.getApptSummaries(params);
473 };
474 
475 /**
476  * Appends the SearchResponse results to the listview. Attempts to request the
477  * next chunk of appointments if the user's scrollbar isn't shown.
478  *
479  * @param list		[AjxVector]		list returned by ZmApptCache
480  * @private
481  */
482 ZmCalListView.prototype._handleSearchResponse =
483 function(list) {
484 
485 	this.addItems(list.getArray());
486     Dwt.setTitle(this.getTitle());
487 	// if we have more days to fetch, search again for the next set
488 	if (this._segmentedDates.length > 0 && this._getItemsNeeded(true) > 0) {
489 		this._search();
490 	}
491 };
492 
493 /**
494  * Method overridden to hnadle action popdown - left it blank coz DwtListView.prototype.handleActionPopdown is clearing
495  * the this._rightSelItem.
496  *
497  * @param	{array}		itemArray		an array of items
498  */
499 ZmCalListView.prototype.handleActionPopdown =
500 function(ev) {
501     //kept empty to avoid clearing of this._rightSelItem.
502 };
503 
504 /**
505  * Adds the items.
506  * The function is overridden to not to show the "No results found" if anything is present in the list.
507  *
508  * @param	{array}		itemArray		an array of items
509  */
510 ZmCalListView.prototype.addItems =
511 function(itemArray) {
512 	if (AjxUtil.isArray(itemArray)) {
513 		if (!this._list) {
514 			this._list = new AjxVector();
515 		}
516 
517 		// clear the "no results" message before adding!
518 		if (this._list.size() == 0) {
519 			this._resetList();
520 		}
521 
522 		// Prune the appts before passing to the underlying ListView
523 		var showDeclined = appCtxt.get(ZmSetting.CAL_SHOW_DECLINED_MEETINGS);
524 		var filterV = new AjxVector();
525 		for (var i = 0; i < itemArray.length; i++) {
526             var appt = itemArray[i];
527             if (showDeclined || (appt.ptst != ZmCalBaseItem.PSTATUS_DECLINED)) {
528                 filterV.add(appt);
529             }
530 		}
531 
532         //Bug fix# 80459. Since ZmCalListView inherits from ZmApptListView, make use of the sorting function and use the sorted list to render
533         //By default the list is sorted on date and thereafter we use the changed sort field if any
534         this._sortList(filterV, this._defaultSortField);
535 
536 		this._renderList(filterV, this._list.size() != 0, true);
537 		this._list.addList(filterV.getArray());
538         this._resetColWidth();
539         //Does not make sense but required to make the scrollbar appear
540         var size = this.getSize();
541         this._listDiv.style.height = (size.y - DwtListView.HEADERITEM_HEIGHT)+"px";
542 	}
543 };
544 
545 /**
546  * This method gets called when the user scrolls up/down. If there are more
547  * appointments to request, it does so.
548  *
549  * @private
550  */
551 ZmCalListView.prototype._checkItemCount =
552 function() {
553 	if (this._segmentedDates.length > 0) {
554 		this._search();
555 	}
556 };
557 
558 /**
559  * Called when the date input field loses focus.
560  *
561  * @param isStartDate		[Boolean]	If true, the start date field is what changed.
562  * @private
563  */
564 ZmCalListView.prototype._onDatesChange =
565 function(isStartDate) {
566 	if (ZmApptViewHelper.handleDateChange(this._startDateField, this._endDateField, isStartDate)) {
567 		this._handleDateChange(isStartDate);
568 	}
569 };
570