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