1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 ZmCalBaseView = function(parent, className, posStyle, controller, view, readonly) {
 25 	if (arguments.length == 0) { return; }
 26 
 27 	DwtComposite.call(this, {parent:parent, className:className, posStyle:posStyle, id:ZmId.getViewId(view)});
 28 
 29 	this._isReadOnly = readonly;
 30 
 31 	// BEGIN LIST-RELATED
 32 	this._setMouseEventHdlrs();
 33 	this.setCursor("default");
 34 
 35 	this.addListener(DwtEvent.ONMOUSEOUT, this._mouseOutListener.bind(this));
 36 	this.addListener(DwtEvent.ONDBLCLICK, this._doubleClickListener.bind(this));
 37 	this.addListener(DwtEvent.ONMOUSEDOWN, this._mouseDownListener.bind(this));
 38 	this.addListener(DwtEvent.ONMOUSEUP, this._mouseUpListener.bind(this));
 39 	this.addListener(DwtEvent.ONMOUSEMOVE, this._mouseMoveListener.bind(this));
 40 	this.addListener(DwtEvent.ONFOCUS, this._focusListener.bind(this));
 41 
 42 	this._controller = controller;
 43 	this.view = view;	
 44 	this._evtMgr = new AjxEventMgr();	 
 45 	this._selectedItems = new AjxVector();
 46 	this._selEv = new DwtSelectionEvent(true);
 47 	this._actionEv = new DwtListViewActionEvent(true);
 48 	this._focusFirstAppt = true;
 49 
 50 	this._normalClass = "appt";
 51 	this._selectedClass = [this._normalClass, DwtCssStyle.SELECTED].join('-');
 52 	this._disabledSelectedClass = [this._selectedClass, DwtCssStyle.DISABLED].join("-");
 53 
 54 	// the key is the HTML ID of the item's associated DIV; the value is an object
 55 	// with information about that row
 56 	this._data = {};
 57 
 58 	// END LIST-RELATED
 59 		
 60 	this._timeRangeStart = 0;
 61 	this._timeRangeEnd = 0;
 62 	this.addControlListener(this._controlListener.bind(this));
 63 	this._createHtml();
 64 	this._needsRefresh = true;
 65 };
 66 
 67 ZmCalBaseView.prototype = new DwtComposite;
 68 ZmCalBaseView.prototype.constructor = ZmCalBaseView;
 69 
 70 ZmCalBaseView.TIME_SELECTION = "ZmCalTimeSelection";
 71 ZmCalBaseView.VIEW_ACTION = "ZmCalViewAction";
 72 
 73 ZmCalBaseView.TYPE_APPTS_DAYGRID = 1; // grid holding days, for example
 74 ZmCalBaseView.TYPE_APPT = 2; // an appt
 75 ZmCalBaseView.TYPE_HOURS_COL = 3; // hours on lefthand side
 76 ZmCalBaseView.TYPE_APPT_BOTTOM_SASH = 4; // a sash for appt duration
 77 ZmCalBaseView.TYPE_APPT_TOP_SASH = 5; // a sash for appt duration
 78 ZmCalBaseView.TYPE_DAY_HEADER = 6; // over date header for a day
 79 ZmCalBaseView.TYPE_MONTH_DAY = 7; // over a day in month view
 80 ZmCalBaseView.TYPE_ALL_DAY = 8; // all day div area in day view
 81 ZmCalBaseView.TYPE_SCHED_FREEBUSY = 9; // free/busy union
 82 ZmCalBaseView.TYPE_DAY_SEP = 10;//allday separator
 83 
 84 ZmCalBaseView.headerColorDelta = 0;
 85 ZmCalBaseView.bodyColorDelta = .5;
 86 ZmCalBaseView.deepenColorAdjustment = .9;
 87 ZmCalBaseView.darkThreshold = (256 * 3) / 2;
 88 ZmCalBaseView.deepenThreshold = .3;
 89 ZmCalBaseView.WORK_HOURS_TIME_FORMAT = "HHmm";
 90 
 91 ZmCalBaseView._getColors = function(color) {
 92 	// generate header and body colors
 93     color = color || ZmOrganizer.COLOR_VALUES[ZmOrganizer.DEFAULT_COLOR[ZmOrganizer.CALENDAR]];
 94 	var hs = { bgcolor: AjxColor.darken(color, ZmCalBaseView.headerColorDelta) };
 95 	var hd = { bgcolor: AjxColor.deepen(hs.bgcolor, ZmCalBaseView.deepenColorAdjustment) };
 96 	var bs = { bgcolor: AjxColor.lighten(color, ZmCalBaseView.bodyColorDelta)  };
 97 	var bd = { bgcolor: AjxColor.deepen(bs.bgcolor, ZmCalBaseView.deepenColorAdjustment) };
 98 
 99 	// ensure enough difference between background and deeper colors
100 	var cs = AjxColor.components(hs.bgcolor);
101 	var cd = AjxColor.components(hd.bgcolor);
102 	var ss = cs[0]+cs[1]+cs[2];
103 	var sd = cd[0]+cd[1]+cd[2];
104 	if (ss/sd > 1 - ZmCalBaseView.deepenThreshold) {
105 		hs.bgcolor = AjxColor.lighten(hd.bgcolor, ZmCalBaseView.deepenThreshold);
106 		bs.bgcolor = AjxColor.lighten(bd.bgcolor, ZmCalBaseView.deepenThreshold);
107 	}
108 
109 	// use light text color for dark backgrounds
110 	hs.color = ZmCalBaseView._isDark(hs.bgcolor) && "#ffffff";
111 	hd.color = ZmCalBaseView._isDark(hd.bgcolor) && "#ffffff";
112 	bs.color = ZmCalBaseView._isDark(bs.bgcolor) && "#ffffff";
113 	bd.color = ZmCalBaseView._isDark(bd.bgcolor) && "#ffffff";
114 
115 	return { standard: { header: hs, body: bs }, deeper: { header: hd, body: bd } };
116 };
117 
118 /**
119  * Gets the key map name.
120  *
121  * @return	{String}	the key map name
122  */
123 ZmCalBaseView.prototype.getKeyMapName =
124     function() {
125         return ZmKeyMap.MAP_CALENDAR;
126     };
127 
128 /**
129  * Handles the key action.
130  *
131  * @param	{constant}		actionCode		the action code
132  * @param	{Object}	ev		the event
133  * @see		ZmApp.ACTION_CODES_R
134  * @see		ZmKeyMap
135  * @see     DwtControl
136  */
137 ZmCalBaseView.prototype.handleKeyAction = function(actionCode, ev) {
138     switch (actionCode) {
139     // the next/prev appointment just iterate over all appointments in
140     // view in a chronological order
141     case ZmKeyMap.NEXT_APPT:
142     case ZmKeyMap.PREV_APPT:
143         this._iterateSelection(actionCode === ZmKeyMap.NEXT_APPT);
144         return true;
145 
146     // the next/prev appointment whose start day differs from the
147     // current selection
148     case ZmKeyMap.NEXT_DAY:
149     case ZmKeyMap.PREV_DAY:
150         this._iterateSelection(actionCode === ZmKeyMap.NEXT_DAY, function(appt) {
151             return appt.startDate.toDateString();
152         });
153         return true;
154 
155     // pagination
156     case ZmKeyMap.PREV_PAGE:
157     case ZmKeyMap.NEXT_PAGE:
158         this._paginate(actionCode === ZmKeyMap.NEXT_PAGE);
159         return true;
160 
161     //Gets the Esc key handle
162     case DwtKeyMap.CANCEL:
163         this.deselectAll();
164         return true;
165 
166     case DwtKeyMap.SELECT:
167         return this._doubleClickListener(ev);
168 
169     case DwtKeyMap.SUBMENU:
170         // notify action listeners so we pop up a context menu --
171         // however, 'ev' is a keyboard event and has no position, so
172         // we create one for positioning the menu over the appointment
173         var rect = this.getTargetItemDiv(ev).getBoundingClientRect();
174         ev.item = this.getTargetItem(ev);
175         ev.docX = rect.left + rect.width / 2;
176         ev.docY = rect.top + rect.height / 2;
177 
178         this._evtMgr.notifyListeners(DwtEvent.ACTION, ev);
179 
180         return true;
181     }
182 
183     return DwtComposite.prototype.handleKeyAction.apply(this, arguments);
184 };
185 
186 ZmCalBaseView.prototype._paginate = function(forward) {
187     this._focusFirstAppt = forward;
188     this._controller._paginate(this.view, forward);
189 };
190 
191 /**
192  * Iterate the selection over appoinments; if no next/previous
193  * appointment found, switch pages.
194  *
195  * @param forward	[Boolean]*		whether to iterate forwards or backwards
196  *
197  * @param keyFunc	[Function]		function to map appointments to values;
198  *									select the first appointnment with a value
199  *									different from the current selection
200  *
201  * @private
202  */
203 ZmCalBaseView.prototype._iterateSelection = function(forward, keyFunc) {
204     var list = this.getList();
205 
206     if (!list.size()) {
207         this._paginate(forward);
208         return;
209     }
210 
211     if (this.getSelectionCount() == 0) {
212         this.setSelection(forward ? list.get(0) : list.getLast());
213     }
214 
215     if (!keyFunc) {
216         keyFunc = function(x) { return x; }
217     }
218 
219     var selappt = this.getSelection()[0]
220     var selIdx = list.indexOf(selappt);
221     var selKey = keyFunc(selappt);
222 
223     for (var i = selIdx; i < list.size() && i >= 0; i += (forward ? 1 : -1)) {
224         var appt = list.get(i);
225 
226         if (keyFunc(appt) != selKey) {
227             this.setSelection(appt);
228             return;
229         }
230     }
231 
232     this._paginate(forward);
233 };
234 
235 ZmCalBaseView._toColorsCss =
236 function(object) {
237 	var a = [ "background-color:",object.bgcolor,";" ];
238 	if (object.color) {
239 		a.push("color:",object.color,";");
240 	}
241 	return a.join("");
242 };
243 
244 ZmCalBaseView._isDark =
245 function(color) {
246 	var c = AjxColor.components(color);
247 	return c[0]+c[1]+c[2] < ZmCalBaseView.darkThreshold;
248 };
249 
250 ZmCalBaseView.prototype.getController =
251 function() {
252 	return this._controller;
253 };
254 
255 ZmCalBaseView.prototype.firstDayOfWeek =
256 function() {
257 	return appCtxt.get(ZmSetting.CAL_FIRST_DAY_OF_WEEK) || 0;
258 };
259 
260 
261 ZmCalBaseView.getWorkingHours =
262 function() {
263 	return appCtxt.get(ZmSetting.CAL_WORKING_HOURS) || 0;
264 };
265 
266 ZmCalBaseView.parseWorkingHours =
267 function(wHrsString) {
268     if(wHrsString === 0) {
269         return [];
270     }
271 	var userTimeZone = appCtxt.get(ZmSetting.DEFAULT_TIMEZONE),
272         currentTimeZone = AjxTimezone.getServerId(AjxTimezone.DEFAULT),
273         wHrsPerDay = wHrsString.split(','),
274         i,
275         wHrs = [],
276         wDay,
277         w,
278         offset1,
279         offset2,
280         hourMinOffset = 0,
281         idx,
282         startDate = new Date(),
283         endDate = new Date(),
284         hourMin,
285         startDayIdx,
286         endDayIdx,
287         curDayIdx = endDate.getDay(),
288         tf = new AjxDateFormat(ZmCalBaseView.WORK_HOURS_TIME_FORMAT);
289 
290     //Helper inner functions, these functions takes the advantage of the fact that wHrs is available in local scope
291     function isWorkingDay(idx) {
292         return wHrs[idx] && wHrs[idx].isWorkingDay;
293     }
294 
295     function setWorkingDay(idx, startTime, endTime) {
296         if(isWorkingDay(idx)) {
297             addWorkingTime(idx, startTime, endTime);
298         }
299         else {
300             addWorkingDay(idx, startTime, endTime);
301         }
302     }
303 
304     function setNonWorkingDay(idx) {
305         wHrs[idx] = {};
306         wHrs[idx].isWorkingDay = false;
307         wHrs[idx].startTime = ["0000"];
308         wHrs[idx].endTime = ["0000"];
309     }
310 
311     function addWorkingDay(idx, startTime, endTime) {
312         wHrs[idx] = {};
313         wHrs[idx].isWorkingDay = true;
314         wHrs[idx].startTime = [startTime];
315         wHrs[idx].endTime = [endTime];
316     }
317 
318     function addWorkingTime(idx, startTime, endTime) {
319         wHrs[idx].startTime.push(startTime);
320         wHrs[idx].endTime.push(endTime);
321     }
322     
323     if(userTimeZone != currentTimeZone) {
324         offset1 = AjxTimezone.getOffset(AjxTimezone.getClientId(currentTimeZone), startDate);
325         offset2 = AjxTimezone.getOffset(AjxTimezone.getClientId(userTimeZone), startDate);
326         hourMinOffset = offset2 - offset1;
327     }
328     for(i=0; i<wHrsPerDay.length; i++) {
329         wDay = wHrsPerDay[i].split(':');
330         w = {};
331         idx = wDay[0]-1;
332         if(wDay[1] === "N") {
333             if(!isWorkingDay(idx)) {
334                 setNonWorkingDay(idx);
335             }
336             continue;
337         }
338 
339         if(hourMinOffset) {
340             endDate = new Date();
341             startDate = new Date();
342             
343             endDate.setHours(wDay[3]/100, wDay[3]%100);
344             hourMin = endDate.getHours() * 60 + endDate.getMinutes() - hourMinOffset;
345             endDate.setHours(hourMin/60, hourMin%60);
346             endDayIdx = endDate.getDay();
347 
348             startDate.setHours(wDay[2]/100, wDay[2]%100);
349             hourMin = startDate.getHours() * 60 + startDate.getMinutes() - hourMinOffset;
350             startDate.setHours(hourMin/60, hourMin%60);
351             startDayIdx = startDate.getDay();
352 
353             if(startDayIdx == curDayIdx && endDayIdx == curDayIdx) {
354                 //Case 1 working time starts current day and ends on the current day -- IDEAL one :)
355                 setWorkingDay(idx, tf.format(startDate), tf.format(endDate));
356             }
357             else if((endDayIdx == 0 && startDayIdx == 6) ||
358                     (startDayIdx < curDayIdx  && endDayIdx == curDayIdx)) {
359                 //Case 2 working time starts prev day and ends on current day
360                 startDayIdx = idx-1;
361                 if(startDayIdx < 0) {
362                    startDayIdx = 6;
363                 }
364                 setWorkingDay(startDayIdx, tf.format(startDate), "2400");
365                 setWorkingDay(idx, "0000", tf.format(endDate));
366             }
367             else if((startDayIdx == 6 && endDayIdx == 0) || 
368                     (startDayIdx == curDayIdx  && endDayIdx > curDayIdx)) {
369                 //Case 3 working time starts current day and ends on next day
370                 endDayIdx = idx+1;
371                 if(endDayIdx > 6) {
372                    endDayIdx = 0; 
373                 }
374                 setWorkingDay(endDayIdx, "0000", tf.format(endDate));
375                 setWorkingDay(idx, tf.format(startDate), "2400");
376             }
377             else if(startDayIdx < curDayIdx &&
378                     endDayIdx < curDayIdx &&
379                     startDayIdx == endDayIdx) {
380                 //EDGE CASE 1: working time starts and ends on the prev day
381                 startDayIdx = idx-1;
382                 setWorkingDay(startDayIdx, tf.format(startDate), tf.format(endDate));
383                 if(!isWorkingDay(idx)) {
384                     setNonWorkingDay(idx);
385                 }
386             }
387 
388             else if(startDayIdx > curDayIdx &&
389                     endDayIdx > curDayIdx &&
390                     startDayIdx == endDayIdx) {
391                 //EDGE CASE 2: working time starts and ends on the next day
392                 endDayIdx = idx+1;
393                 setWorkingDay(endDayIdx, tf.format(startDate), tf.format(endDate));
394                 if(!isWorkingDay(idx)) {
395                     setNonWorkingDay(idx);
396                 }
397             }            
398         }
399         else {
400             //There is no timezone diff, client and server are in the same timezone
401             setWorkingDay(idx, wDay[2], wDay[3]);
402         }
403 
404     }
405     return wHrs;
406 };
407 
408 ZmCalBaseView.prototype.addViewActionListener =
409 function(listener) {
410 	this._evtMgr.addListener(ZmCalBaseView.VIEW_ACTION, listener);
411 };
412 
413 ZmCalBaseView.prototype.removeViewActionListener =
414 function(listener) {
415 	this._evtMgr.removeListener(ZmCalBaseView.VIEW_ACTION, listener);
416 };
417 
418 // BEGIN LIST-RELATED
419 
420 ZmCalBaseView.prototype.addSelectionListener = 
421 function(listener) {
422 	this._evtMgr.addListener(DwtEvent.SELECTION, listener);
423 };
424 
425 ZmCalBaseView.prototype.removeSelectionListener = 
426 function(listener) {
427 	this._evtMgr.removeListener(DwtEvent.SELECTION, listener);    	
428 };
429 
430 ZmCalBaseView.prototype.addActionListener = 
431 function(listener) {
432 	this._evtMgr.addListener(DwtEvent.ACTION, listener);
433 };
434 
435 ZmCalBaseView.prototype.removeActionListener = 
436 function(listener) {
437 	this._evtMgr.removeListener(DwtEvent.ACTION, listener);    	
438 };
439 
440 ZmCalBaseView.prototype.getList = 
441 function() {
442 	return this._list;
443 };
444 
445 ZmCalBaseView.prototype.associateItemWithElement =
446 function (item, element, type, optionalId) {
447 	DwtListView.prototype.associateItemWithElement.apply(this, arguments);
448 };
449 
450 ZmCalBaseView.prototype.getItemFromElement =
451 function(el) {
452 	return DwtListView.prototype.getItemFromElement.apply(this, arguments);
453 };
454 
455 ZmCalBaseView.prototype.getTargetItemDiv =
456 function(ev)  {
457 	return this.findItemDiv(DwtUiEvent.getTarget(ev));
458 };
459 
460 ZmCalBaseView.prototype.getTargetItem =
461 function(ev)  {
462 	return this.findItem(DwtUiEvent.getTarget(ev));
463 };
464 
465 ZmCalBaseView.prototype.findItem =
466 function(el) {
467 	return DwtListView.prototype.findItem.apply(this, arguments);
468 };
469 
470 ZmCalBaseView.prototype.findItemDiv =
471 function(el) {
472 	return DwtListView.prototype.findItemDiv.apply(this, arguments);
473 };
474 
475 ZmCalBaseView.prototype._getItemData =
476 function(el, field, id) {
477 	return DwtListView.prototype._getItemData.apply(this, arguments);
478 };
479 
480 ZmCalBaseView.prototype._setItemData =
481 function(id, field, value) {
482 	DwtListView.prototype._setItemData.apply(this, arguments);
483 };
484 
485 ZmCalBaseView.prototype.deselectAll =
486 function() {
487     this.deselectAppt(this._selectedItems);
488 };
489 
490 /**
491  * Returns a style appropriate to the given item type. Subclasses should override to return
492  * styles for different item types. This implementation does not consider the type.
493  * 
494  * @param type		[constant]*		a type constant
495  * @param selected	[boolean]*		if true, return a style for an item that has been selected
496  * @param disabled	[boolean]*		if true, return a style for an item that has been disabled
497  * @param item		[object]*		item behind the div
498  * 
499  * @private
500  */
501 ZmCalBaseView.prototype._getStyle =
502 function(type, selected, disabled, item) {
503 	return (!selected)
504 		? this._normalClass
505 		: (disabled ? this._disabledSelectedClass : this._selectedClass);
506 };
507 
508 ZmCalBaseView.prototype.getToolTipContent =
509 function(ev) {
510 	var div = this.getTargetItemDiv(ev);
511 	if (!div) { return null; }
512 	if (this._getItemData(div, "type") != ZmCalBaseView.TYPE_APPT) { return null; }
513 
514 	var item = this.getItemFromElement(div);
515 	return item.getToolTip(this._controller);
516 };
517 
518 // tooltip position will be based on cursor
519 ZmCalBaseView.prototype.getTooltipBase =
520 function(hoverEv) {
521 	return null;
522 };
523 
524 ZmCalBaseView.prototype.getApptDetails =
525 function(appt, callback, uid) {
526 	if (this._currentMouseOverApptId &&
527 		this._currentMouseOverApptId == uid)
528 	{
529 		this._currentMouseOverApptId = null;
530 		appt.getDetails(null, callback, null, null, true);
531 	}
532 };
533 
534 ZmCalBaseView.prototype._mouseOutListener = 
535 function(ev) {
536 	var div = this.getTargetItemDiv(ev);
537 	if (!div) { return; }
538 
539 	// NOTE: The DwtListView handles the mouse events on the list items
540 	//		 that have associated tooltip text. Therefore, we must
541 	//		 explicitly null out the tooltip content whenever we handle
542 	//		 a mouse out event. This will prevent the tooltip from
543 	//		 being displayed when we re-enter the listview even though
544 	//		 we're not over a list item.
545 	if (this._getItemData(div, "type") == ZmCalBaseView.TYPE_APPT) {
546 		this.setToolTipContent(null);
547 	}
548 	this._mouseOutAction(ev, div);
549 };
550 
551 ZmCalBaseView.prototype._mouseOutAction = 
552 function(ev, div) {
553 	return true;
554 };
555 
556 
557 ZmCalBaseView.prototype._mouseMoveListener = 
558 function(ev) {
559 	// do nothing
560 };
561 
562 ZmCalBaseView.prototype._focusListener =
563 function(ev) {
564     var item = this.getTargetItem(ev);
565 
566     if (item) {
567         this.setSelection(item);
568     }
569 };
570 
571 // XXX: why not use Dwt.findAncestor?
572 ZmCalBaseView.prototype._findAncestor =
573 function(elem, attr) {
574 	while (elem && (elem[attr] == null)) {
575 		elem = elem.parentNode;
576 	}
577 	return elem;
578 };
579 
580 ZmCalBaseView.prototype._mouseDownListener = 
581 function(ev) {
582 	if (this._isReadOnly) { return; }
583 
584 	var div = this.getTargetItemDiv(ev);
585 	if (!div) {
586 		return this._mouseDownAction(ev, div);
587 	}
588 
589 	this._clickDiv = div;
590 	if (this._getItemData(div, "type") == ZmCalBaseView.TYPE_APPT) {
591 		if (ev.button == DwtMouseEvent.LEFT || ev.button == DwtMouseEvent.RIGHT) {
592 			this._itemClicked(div, ev);
593 		}
594 	}
595 	return this._mouseDownAction(ev, div);
596 };
597 
598 ZmCalBaseView.prototype._mouseDownAction = 
599 function(ev, div) {
600 	return !Dwt.ffScrollbarCheck(ev);
601 };
602 
603 ZmCalBaseView.prototype._mouseUpListener = 
604 function(ev) {
605 	delete this._clickDiv;
606 	return this._mouseUpAction(ev, this.getTargetItemDiv(ev));
607 };
608 
609 ZmCalBaseView.prototype._mouseUpAction = 
610 function(ev, div) {
611 	return !Dwt.ffScrollbarCheck(ev);
612 };
613 
614 ZmCalBaseView.prototype._doubleClickAction = 
615 function(ev, div) { return true; };
616 
617 ZmCalBaseView.prototype._doubleClickListener =
618 function(ev) {
619 	var div = this.getTargetItemDiv(ev);
620 	if (!div) { return;	}
621 
622 	var handled = false;
623 
624 	if (this._getItemData(div, "type") == ZmCalBaseView.TYPE_APPT) {
625 		if (this._evtMgr.isListenerRegistered(DwtEvent.SELECTION)) {
626 			DwtUiEvent.copy(this._selEv, ev);
627 			var item = this.getItemFromElement(div);
628             var orig = item.getOrig();
629             item = orig && orig.isMultiDay() ? orig : item;
630 			this._selEv.item = item;
631 			this._selEv.detail = DwtListView.ITEM_DBL_CLICKED;
632 			this._evtMgr.notifyListeners(DwtEvent.SELECTION, this._selEv);
633 
634 			handled = true;
635 		}
636 	}
637 	return this._doubleClickAction(ev, div) || handled;
638 };
639 
640 ZmCalBaseView.prototype._itemClicked =
641 function(clickedEl, ev) {
642 	var i;
643 	var selected = this._selectedItems.contains(clickedEl);
644 	var item = this.getItemFromElement(clickedEl);
645 	var type = this._getItemData(clickedEl, "type");
646 
647 	if (ev.shiftKey && selected) {
648         //Deselect the current selected appointment
649         this.deselectAppt([clickedEl]);
650 	} else if (!selected) {
651 		this.setSelection(item);
652 	}
653 
654 	if (ev.button == DwtMouseEvent.RIGHT) {
655 		DwtUiEvent.copy(this._actionEv, ev);
656 		this._actionEv.item = item;
657 		this._evtMgr.notifyListeners(DwtEvent.ACTION, this._actionEv);
658 	}
659 };
660 
661 // YUCK: ZmListView overloads b/c ZmListController thinks its always dealing w/ ZmListView's
662 ZmCalBaseView.prototype.setSelectionCbox = function(obj, bContained) {};
663 ZmCalBaseView.prototype.setSelectionHdrCbox = function(check) {};
664 
665 ZmCalBaseView.prototype.setSelection =
666 function(item, skipNotify) {
667 	var el = this._getElFromItem(item);
668 
669 	if (el) {
670 		var i;
671 		var a = this._selectedItems.getArray();
672 		var sz = this._selectedItems.size();
673 		for (i = 0; i < sz; i++) {
674 			a[i].className = this._getStyle(this._getItemData(a[i], "type"));
675 		}
676 		this._selectedItems.removeAll();
677 		this._selectedItems.add(el);
678 
679 		el.className = this._getStyle(this._getItemData(el, "type"), true, !this.getEnabled(), item);
680 
681 		this.setFocusElement(el);
682 		Dwt.clearHandler(el, DwtEvent.ONCLICK);
683 
684 		if (!skipNotify && this._evtMgr.isListenerRegistered(DwtEvent.SELECTION)) {
685 			var selEv = new DwtSelectionEvent(true);
686 			selEv.button = DwtMouseEvent.LEFT;
687 			selEv.target = el;
688 			selEv.item = item;
689 			selEv.detail = DwtListView.ITEM_SELECTED;
690 			this._evtMgr.notifyListeners(DwtEvent.SELECTION, selEv);
691 		}	
692 	}
693 };
694 
695 ZmCalBaseView.prototype._getItemCountType = function() {
696 	return ZmId.ITEM_APPOINTMENT;
697 };
698 
699 ZmCalBaseView.prototype.getSelectionCount =
700 function() {
701 	return this._selectedItems.size();
702 };
703 
704 ZmCalBaseView.prototype.getSelection =
705 function() {
706 	var a = new Array();
707 	var sa = this._selectedItems.getArray();
708 	var saLen = this._selectedItems.size();
709 	for (var i = 0; i < saLen; i++) {
710 		a[i] = this.getItemFromElement(sa[i]);
711 	}
712 	return a;
713 };
714 
715 ZmCalBaseView.prototype.getSelectedItems =
716 function() {
717 	return this._selectedItems;
718 };
719 
720 ZmCalBaseView.prototype.handleActionPopdown = 
721 function(ev) {
722 	// clear out old right click selection
723     ZmCalViewController._contextMenuOpened = false;
724 
725     if(ev && ev._ev && ev._ev.type === "mousedown"){//Only check for mouse events
726         var htmlEl = DwtUiEvent.getTarget(ev._ev),
727             element = document.getElementById(this._bodyDivId) || document.getElementById(this._daysId);
728 
729         if(element){
730             while (htmlEl !== null) {
731                 if(htmlEl === element){
732                     ZmCalViewController._contextMenuOpened = true;
733                     break;
734                 }
735                 htmlEl = htmlEl.parentNode;
736             }
737         }
738     }
739 };
740 
741 // END LIST-RELATED
742 
743 ZmCalBaseView.prototype.getTitle =
744 function() {
745 	return [ZmMsg.zimbraTitle, this.getCalTitle()].join(": ");
746 };
747 
748 ZmCalBaseView.prototype.needsRefresh = 
749 function() {
750 	return this._needsRefresh;
751 };
752 
753 ZmCalBaseView.prototype.setNeedsRefresh = 
754 function(refresh) {
755 	 this._needsRefresh = refresh;
756 };
757 
758 ZmCalBaseView.prototype._getItemId =
759 function(item) {
760 	return item ? (DwtId.getListViewItemId(DwtId.WIDGET_ITEM, this.view, item.getUniqueId())) : null;
761 };
762 
763 ZmCalBaseView.prototype.addTimeSelectionListener = 
764 function(listener) {
765 	this.addListener(ZmCalBaseView.TIME_SELECTION, listener);
766 };
767 
768 ZmCalBaseView.prototype.removeTimeSelectionListener = 
769 function(listener) { 
770 	this.removeListener(ZmCalBaseView.TIME_SELECTION, listener);
771 };
772 
773 ZmCalBaseView.prototype.addDateRangeListener = 
774 function(listener) {
775 	this.addListener(DwtEvent.DATE_RANGE, listener);
776 };
777 
778 ZmCalBaseView.prototype.removeDateRangeListener = 
779 function(listener) { 
780 	this.removeListener(DwtEvent.DATE_RANGE, listener);
781 };
782 
783 ZmCalBaseView.prototype.getRollField =
784 function() {
785 	// override.
786 	return 0;
787 };
788 
789 ZmCalBaseView.prototype.getDate =
790 function() {
791 	return this._date;
792 };
793 //to override
794 ZmCalBaseView.prototype.getAtttendees =
795 function() {
796     return null;
797 };
798 
799 ZmCalBaseView.prototype.getTimeRange =
800 function() {
801 	return { start: this._timeRangeStart, end: this._timeRangeEnd };
802 };
803 
804 ZmCalBaseView.prototype.isInView =
805 function(appt) {
806 	return appt.isInRange(this._timeRangeStart, this._timeRangeEnd);
807 };
808 
809 ZmCalBaseView.prototype.isStartInView =
810 function(appt) {
811 	return appt.isStartInRange(this._timeRangeStart, this._timeRangeEnd);
812 };
813 
814 ZmCalBaseView.prototype.isEndInView =
815 function(appt) {
816 	return appt.isEndInRange(this._timeRangeStart, this._timeRangeEnd);
817 };
818 
819 ZmCalBaseView.prototype._dayKey =
820 function(date) {
821 	return (date.getFullYear()+"/"+date.getMonth()+"/"+date.getDate());
822 };
823 
824 ZmCalBaseView.prototype.setDate =
825 function(date, duration, roll) {
826 	this._duration = duration;
827 	this._date = new Date(date.getTime());
828 	var d = new Date(date.getTime());
829 	d.setHours(0, 0, 0, 0);
830 	var t = d.getTime();
831 	if (roll || t < this._timeRangeStart || t >= this._timeRangeEnd) {
832 		this._resetList();
833 		this._updateRange();		
834 		this._dateUpdate(true);
835 		this._updateTitle();
836 		
837 		// Notify any listeners
838 		if (this.isListenerRegistered(DwtEvent.DATE_RANGE)) {
839 			if (!this._dateRangeEvent)
840 				this._dateRangeEvent = new DwtDateRangeEvent(true);
841 			this._dateRangeEvent.item = this;
842 			this._dateRangeEvent.start = new Date(this._timeRangeStart);
843 			this._dateRangeEvent.end = new Date(this._timeRangeEnd);
844 			this.notifyListeners(DwtEvent.DATE_RANGE, this._dateRangeEvent);
845 		}
846 	} else {
847 		this._dateUpdate(false);
848 	}
849 };
850 
851 ZmCalBaseView.prototype._dateUpdate =
852 function(rangeChanged) {
853 	// override: responsible for updating any view-specific data when the date
854 	// changes during a setDate call.
855 };
856 
857 ZmCalBaseView.prototype._apptSelected =
858 function() {
859 	// override: called when an appointment is clicked to see if the view should
860 	// de-select a selected time range. For example, in day view if you have
861 	// selected the 8:00 AM row and then click on an appt, the 8:00 AM row
862 	// should be de-selected. If you are in month view though and have a day
863 	// selected, thne day should still be selected if the appt you clicked on is
864 	// in the same day.
865 };
866 
867 // override
868 ZmCalBaseView.prototype._updateRange =
869 function() { 
870 	this._updateDays();
871 	this._timeRangeStart = this._days[0].date.getTime();
872 	//this._timeRangeEnd = this._days[this.numDays-1].date.getTime() + AjxDateUtil.MSEC_PER_DAY;
873     var endDate = this._days[this.numDays-1].date;
874     endDate.setHours(23, 59, 59, 999);
875 	this._timeRangeEnd = endDate.getTime();
876 };
877 
878 // override 
879 ZmCalBaseView.prototype._updateTitle =
880 function() { };
881 
882 ZmCalBaseView.prototype.addAppt = 
883 function(ao) {
884 	var item = this._createItemHtml(ao);
885     if (!item) {
886         return;
887     }
888 	var div = this._getDivForAppt(ao);
889 	if (div) div.appendChild(item);
890 
891 	this._postApptCreate(ao,div);	
892 };
893 
894 // override
895 ZmCalBaseView.prototype._postApptCreate =
896 function(appt,div) {
897 };
898 
899 ZmCalBaseView.prototype.set = 
900 function(list) {
901 	this._preSet();
902 	this.deselectAll();
903 	list = list.filter(this.isInView.bind(this));
904 	this._resetList();
905 	this._list = list;
906     var showDeclined = appCtxt.get(ZmSetting.CAL_SHOW_DECLINED_MEETINGS);
907     if (list) {
908 		var size = list.size();
909 		if (size != 0) {
910 			for (var i=0; i < size; i++) {
911 				var ao = list.get(i);
912                 if (showDeclined || (ao.ptst != ZmCalBaseItem.PSTATUS_DECLINED)) {
913 				    this.addAppt(ao);
914                 }
915 			}
916 		}
917 	}
918 
919 	this._postSet(list);
920 
921 	// the calendar itself may have focus; this re-focuses any items
922 	// within it
923 	var selappt = this._focusFirstAppt ? this._list.get(0) : this._list.getLast();
924 	this._focusFirstAppt = true;
925 
926 	if (selappt) {
927 		this.setSelection(selappt);
928 	}
929 };
930 
931 // override
932 ZmCalBaseView.prototype._fanoutAllDay =
933 function(appt) {
934 	return true;
935 };
936 
937 // override
938 ZmCalBaseView.prototype._postSet =
939 function(appt) {};
940 
941 // override
942 ZmCalBaseView.prototype._preSet =
943 function(appt) {};
944 
945 // override
946 ZmCalBaseView.prototype._getDivForAppt =
947 function(appt) {};
948 
949 ZmCalBaseView.prototype._addApptIcons =
950 function(appt, html, idx) {
951 	html[idx++] = "<table border=0 cellpadding=0 cellspacing=0 style='display:inline'><tr>";
952 
953 	if (appt.otherAttendees) {
954 		html[idx++] = "<td>";
955 		html[idx++] = AjxImg.getImageHtml("ApptMeeting");
956 		html[idx++] = "</td>";
957 	}
958 
959 	if (appt.isException) {
960 		html[idx++] = "<td>";
961 		html[idx++] = AjxImg.getImageHtml("ApptException");
962 		html[idx++] = "</td>";
963 	} else if (appt.isRecurring()) {
964 		html[idx++] = "<td>";
965 		html[idx++] = AjxImg.getImageHtml("ApptRecur");
966 		html[idx++] = "</td>";
967 	}
968 
969 	if (appt.alarm) {
970 		html[idx++] = "<td>";
971 		html[idx++] = AjxImg.getImageHtml("ApptReminder");
972 		html[idx++] = "</td>";
973 	}
974 	html[idx++] = "</tr></table>";
975 
976 	return idx;
977 };
978 
979 ZmCalBaseView.prototype._getElFromItem = 
980 function(item) {
981 	return document.getElementById(this._getItemId(item));
982 };
983 
984 ZmCalBaseView.prototype._resetList =
985 function() {
986 	var list = this.getList();
987 	var size = list ? list.size() : 0;
988 	if (size == 0) return;
989 
990 	this.setFocusElement(this.getHtmlElement());
991 
992 	for (var i=0; i < size; i++) {
993 		var ao = list.get(i);
994 		var id = this._getItemId(ao);
995 		var appt = document.getElementById(id);
996 		if (appt) {
997 			appt.parentNode.removeChild(appt);
998 			this._data[id] = null;
999 		}
1000 	}
1001 	list.removeAll();
1002 	this.removeAll();
1003 };
1004 
1005 ZmCalBaseView.prototype.removeAll =
1006 function() {
1007 	this._selectedItems.removeAll();
1008 };
1009 
1010 ZmCalBaseView.prototype.layoutView =
1011 function() {
1012     this._layout();
1013 };
1014 
1015 ZmCalBaseView.prototype.getCalTitle = 
1016 function() {
1017 	return this._title;
1018 };
1019 
1020 ZmCalBaseView.prototype._getStartDate =
1021 function() {
1022 	var timeRange = this.getTimeRange();
1023 	return new Date(timeRange.start);
1024 };
1025 
1026 // override
1027 ZmCalBaseView.prototype._createItemHtml =
1028 function(appt) {};
1029 
1030 // override
1031 ZmCalBaseView.prototype._createHtml =
1032 function() {};
1033 
1034 // override
1035 ZmCalBaseView.prototype.checkIndicatorNeed =
1036 function(viewId, startDate) {};
1037 
1038 ZmCalBaseView.prototype._controlListener =
1039 function(ev) {
1040 	if ((ev.oldWidth != ev.newWidth) ||
1041 		(ev.oldHeight != ev.newHeight))
1042 	{
1043 		this._layout();
1044 	}
1045 };
1046 
1047 // override
1048 ZmCalBaseView.prototype._layout =
1049 function() {};
1050 
1051 ZmCalBaseView.prototype._timeSelectionEvent =
1052 function(date, duration, isDblClick, allDay, folderId, shiftKey) {
1053 	if (!this._selectionEvent) this._selectionEvent = new DwtSelectionEvent(true);
1054 	var sev = this._selectionEvent;
1055 	sev._isDblClick = isDblClick;
1056 	sev.item = this;
1057 	sev.detail = date;
1058 	sev.duration = duration;
1059 	sev.isAllDay = allDay;
1060 	sev.folderId = folderId;
1061 	sev.force = false;
1062 	sev.shiftKey = shiftKey;
1063 	this.notifyListeners(ZmCalBaseView.TIME_SELECTION, this._selectionEvent);
1064 	sev._isDblClick = false;
1065 };
1066 
1067 
1068 ZmCalBaseView._setApptOpacity =
1069 function(appt, div) {
1070     var opacity = this.getApptOpacity(appt);
1071 	Dwt.setOpacity(div, opacity);
1072 };
1073 
1074 ZmCalBaseView.getApptOpacity =
1075 function(appt) {
1076     var opacity = 100;
1077 
1078     switch (appt.ptst) {
1079 		case ZmCalBaseItem.PSTATUS_DECLINED:	opacity = ZmCalColView._OPACITY_APPT_DECLINED;  break;
1080 		case ZmCalBaseItem.PSTATUS_TENTATIVE:	opacity = ZmCalColView._OPACITY_APPT_TENTATIVE; break;
1081 		default:								opacity = ZmCalColView._OPACITY_APPT_NORMAL;    break;
1082 	}
1083 
1084 	// obey free busy status for organizer's appts
1085 	if (appt.fba && appt.isOrganizer()) {
1086 		 switch (appt.fba) {
1087 			case "F":	opacity = ZmCalColView._OPACITY_APPT_FREE; break;
1088 			case "B":	opacity = ZmCalColView._OPACITY_APPT_BUSY; break;
1089 			case "T":	opacity = ZmCalColView._OPACITY_APPT_TENTATIVE; break;
1090 		 }
1091 	}
1092     return opacity;
1093 };
1094 
1095 ZmCalBaseView._emptyHdlr =
1096 function(ev) {
1097 	var mouseEv = DwtShell.mouseEvent;
1098 	mouseEv.setFromDhtmlEvent(ev);
1099 	mouseEv._stopPropagation = true;
1100 	mouseEv._returnValue = false;
1101 	mouseEv.setToDhtmlEvent(ev);
1102 	return false;
1103 };
1104 
1105 
1106 ZmCalBaseView.prototype._apptMouseDownAction =
1107 function(ev, apptEl, appt) {
1108 	if (ev.button != DwtMouseEvent.LEFT) { return false; }
1109 
1110     if (!appt) {
1111         appt = this.getItemFromElement(apptEl);
1112     }
1113 	var calendar = appCtxt.getById(appt.folderId);
1114 	var isRemote = Boolean(calendar.url);
1115     if (appt.isReadOnly() || isRemote || appCtxt.isWebClientOffline()) return false;
1116 
1117 	var apptOffset = Dwt.toWindow(ev.target, ev.elementX, ev.elementY, apptEl, false);
1118 
1119 	var data = {
1120 		dndStarted: false,
1121 		appt: appt,
1122 		view: this,
1123 		apptEl: apptEl,
1124 		apptOffset: apptOffset,
1125 		docX: ev.docX,
1126 		docY: ev.docY
1127 	};
1128 
1129 	var capture = new DwtMouseEventCapture({
1130 		targetObj:data,
1131 		mouseOverHdlr:ZmCalBaseView._emptyHdlr,
1132 		mouseDownHdlr:ZmCalBaseView._emptyHdlr, // mouse down (already handled by action)
1133 		mouseMoveHdlr:ZmCalBaseView._apptMouseMoveHdlr,
1134 		mouseUpHdlr:  ZmCalBaseView._apptMouseUpHdlr,
1135 		mouseOutHdlr: ZmCalBaseView._emptyHdlr
1136 	});
1137     DBG.println(AjxDebug.DBG3,"data.docX,Y: " + data.docX + "," + data.docY);
1138 
1139     this._createContainerRect(data);
1140     // Problem with Month View ??
1141     this._controller.setCurrentListView(this);
1142 
1143 	capture.capture();
1144 	return false;
1145 };
1146 
1147 
1148 
1149 ZmCalBaseView.prototype._getApptDragProxy =
1150 function(data) {
1151 	// set icon
1152 	var icon;
1153 	if (this._apptDragProxyDivId == null) {
1154 		icon = document.createElement("div");
1155 		icon.id = this._apptDragProxyDivId = Dwt.getNextId();
1156 		Dwt.setPosition(icon, Dwt.ABSOLUTE_STYLE);
1157 		this.shell.getHtmlElement().appendChild(icon);
1158 		Dwt.setZIndex(icon, Dwt.Z_DND);
1159 	} else {
1160 		icon = document.getElementById(this._apptDragProxyDivId);
1161 	}
1162 	icon.className = DwtCssStyle.NOT_DROPPABLE;
1163 
1164 	var appt = data.appt;
1165 	var formatter = AjxDateFormat.getDateInstance(AjxDateFormat.SHORT);
1166 	var color = ZmCalendarApp.COLORS[this._controller.getCalendarColor(appt.folderId)];
1167 	if (appt.ptst != ZmCalBaseItem.PSTATUS_NEEDS_ACTION) {
1168 		color += "Bg";
1169 	}
1170 
1171 	var proxyData = {
1172 		shortDate: formatter.format(appt.startDate),
1173 		dur: appt.getShortStartHour(),
1174 		color: color,
1175 		apptName: AjxStringUtil.htmlEncode(appt.getName())
1176 	};
1177 
1178 	icon.innerHTML = AjxTemplate.expand("calendar.Calendar#ApptDragProxy", proxyData);
1179 
1180 	var imgHtml = AjxImg.getImageHtml("RoundPlus", "position:absolute; top:30; left:-11; visibility:hidden");
1181 	icon.appendChild(Dwt.parseHtmlFragment(imgHtml));
1182 
1183 	return icon;
1184 };
1185 
1186 
1187 
1188 ZmCalBaseView._apptMouseMoveHdlr =
1189 function(ev) {
1190     var data = DwtMouseEventCapture.getTargetObj();
1191     if (!data) return false;
1192 
1193 	var mouseEv = DwtShell.mouseEvent;
1194 	mouseEv.setFromDhtmlEvent(ev, true);
1195     var view = data.view;
1196 
1197 	var deltaX = mouseEv.docX - data.docX;
1198 	var deltaY = mouseEv.docY - data.docY;
1199     DBG.println(AjxDebug.DBG3,"_apptMouseMoveHdlr mouseEv.docY: " + mouseEv.docY + ",   data.docY: " + data.docY);
1200 
1201 	if (!data.dndStarted) {
1202 		var withinThreshold = (Math.abs(deltaX) < ZmCalColView.DRAG_THRESHOLD && Math.abs(deltaY) < ZmCalColView.DRAG_THRESHOLD);
1203 		if (withinThreshold || !view._apptDndBegin(data)) {
1204 			mouseEv._stopPropagation = true;
1205 			mouseEv._returnValue = false;
1206 			mouseEv.setToDhtmlEvent(ev);
1207 			return false;
1208 		}
1209 	}
1210 
1211 	if (view._apptDraggedOut(mouseEv.docX, mouseEv.docY)) {
1212 		// simulate DND
1213         DBG.println(AjxDebug.DBG3,"MouseMove DragOut");
1214         view._dragOut(mouseEv, data);
1215 	}
1216 	else
1217 	{
1218 		if (data._lastDraggedOut) {
1219 			data._lastDraggedOut = false;
1220 			if (data.icon) {
1221 				Dwt.setVisible(data.icon, false);
1222 			}
1223             view._restoreHighlight(data);
1224 		}
1225         var obj = data.dndObj;
1226 		obj._lastDestDwtObj = null;
1227         if (!data.disableScroll) {
1228             var scrollOffset = view._handleApptScrollRegion(mouseEv.docX, mouseEv.docY, ZmCalColView._HOUR_HEIGHT, data);
1229             if (scrollOffset != 0) {
1230                 deltaY += scrollOffset;
1231             }
1232         }
1233 
1234 		// snap new location to grid
1235         view._doApptMove(data, deltaX, deltaY);
1236 	}
1237 	mouseEv._stopPropagation = true;
1238 	mouseEv._returnValue = false;
1239 	mouseEv.setToDhtmlEvent(ev);
1240 	return false;
1241 };
1242 
1243 
1244 
1245 ZmCalBaseView.prototype._dragOut =
1246 function(mouseEv, data) {
1247     // simulate DND
1248     var obj = data.dndObj;
1249     if (!data._lastDraggedOut) {
1250         data._lastDraggedOut = true;
1251         this._clearSnap(data.snap);
1252         data.startDate = new Date(data.appt.getStartTime());
1253         this._restoreApptLoc(data);
1254         if (!data.icon) {
1255             data.icon = this._getApptDragProxy(data);
1256         }
1257         Dwt.setVisible(data.icon, true);
1258     }
1259     Dwt.setLocation(data.icon, mouseEv.docX+5, mouseEv.docY+5);
1260     var destDwtObj = mouseEv.dwtObj;
1261     var obj = data.dndObj;
1262 
1263     if (destDwtObj && destDwtObj._dropTarget)
1264     {
1265         if (destDwtObj != obj._lastDestDwtObj ||
1266             destDwtObj._dropTarget.hasMultipleTargets())
1267         {
1268             //DBG.println("dwtObj = "+destDwtObj._dropTarget);
1269             if (destDwtObj._dropTarget._dragEnter(Dwt.DND_DROP_MOVE, destDwtObj, {data: data.appt}, mouseEv, data.icon)) {
1270                 //obj._setDragProxyState(true);
1271                 data.icon.className = DwtCssStyle.DROPPABLE;
1272                 obj._dropAllowed = true;
1273                 destDwtObj._dragEnter(mouseEv);
1274             } else {
1275                 //obj._setDragProxyState(false);
1276                 data.icon.className = DwtCssStyle.NOT_DROPPABLE;
1277                 obj._dropAllowed = false;
1278             }
1279         } else if (obj._dropAllowed) {
1280             destDwtObj._dragOver(mouseEv);
1281         }
1282     } else {
1283         data.icon.className = DwtCssStyle.NOT_DROPPABLE;
1284         //obj._setDragProxyState(false);
1285     }
1286 
1287     if (obj._lastDestDwtObj &&
1288         obj._lastDestDwtObj != destDwtObj &&
1289         obj._lastDestDwtObj._dropTarget &&
1290         obj._lastDestDwtObj != obj)
1291     {
1292         obj._lastDestDwtObj._dragLeave(mouseEv);
1293         obj._lastDestDwtObj._dropTarget._dragLeave();
1294     }
1295     obj._lastDestDwtObj = destDwtObj;
1296 
1297 }
1298 
1299 ZmCalBaseView.prototype._apptDraggedOut =
1300 function(docX, docY) {
1301     var draggedOut = this._containerRect ? true : false;
1302     return draggedOut &&
1303            ((docY < this._containerRect.y) ||
1304             (docY > (this._containerRect.y + this._containerRect.height)) ||
1305             (docX < this._containerRect.x) ||
1306             (docX > (this._containerRect.x + this._containerRect.width)));
1307 };
1308 
1309 ZmCalBaseView._apptMouseUpHdlr =
1310 function(ev) {
1311 	//DBG.println("ZmCalBaseView._apptMouseUpHdlr: "+ev.shiftKey);
1312 	var data = DwtMouseEventCapture.getTargetObj();
1313 
1314 
1315 	var mouseEv = DwtShell.mouseEvent;
1316     if (ev && mouseEv) {
1317 	    mouseEv.setFromDhtmlEvent(ev, true);
1318     }
1319 	DwtMouseEventCapture.getCaptureObj().release();
1320 
1321 	var draggedOut = data.view._apptDraggedOut(mouseEv.docX, mouseEv.docY);
1322 
1323 	if (data.dndStarted && data.appt) {
1324         data.view._deselectDnDHighlight(data);
1325 		//notify Zimlet when an appt is dragged.
1326  		appCtxt.notifyZimlets("onApptDrag", [data]);
1327 		if (data.startDate.getTime() != data.appt._orig.getStartTime() && !draggedOut) {
1328 			if (data.icon) Dwt.setVisible(data.icon, false);
1329 			// save before we muck with start/end dates
1330 			var origDuration = data.appt._orig.getDuration();
1331 			data.view._autoScrollDisabled = true;
1332 			var cc = appCtxt.getCurrentController();
1333 			var endDate = new Date(data.startDate.getTime() + origDuration);
1334 			var errorCallback = new AjxCallback(null, ZmCalColView._handleDnDError, data);
1335 			var sdOffset = data.startDate ? (data.startDate.getTime() - data.appt._orig.getStartTime()) : null;
1336 			var edOffset = endDate ? (endDate.getTime() - data.appt._orig.getEndTime() ) : null;
1337 			cc.dndUpdateApptDate(data.appt._orig, sdOffset, edOffset, null, errorCallback, mouseEv);
1338 		} else {
1339             data.view._restoreAppt(data);
1340 		}
1341 
1342 		if (draggedOut) {
1343 			var obj = data.dndObj;
1344 			obj._lastDestDwtObj = null;
1345 			var destDwtObj = mouseEv.dwtObj;
1346 			if (destDwtObj != null &&
1347 				destDwtObj._dropTarget != null &&
1348 				obj._dropAllowed &&
1349 				destDwtObj != obj)
1350 			{
1351 				destDwtObj._drop(mouseEv);
1352 				var srcData = {
1353 					data: data.appt,
1354 					controller: data.view._controller
1355 				};
1356 				destDwtObj._dropTarget._drop(srcData, mouseEv);
1357 				obj._dragging = DwtControl._NO_DRAG;
1358 				if (data.icon) Dwt.setVisible(data.icon, false);
1359 			}
1360 			else {
1361 				// The following code sets up the drop effect for when an
1362 				// item is dropped onto an invalid target. Basically the
1363 				// drag icon will spring back to its starting location.
1364 				var bd = data.view._badDrop = { dragEndX: mouseEv.docX, dragEndY: mouseEv.docY, dragStartX: data.docX, dragStartY: data.docY };
1365 				bd.icon = data.icon;
1366 				if (data.view._badDropAction == null) {
1367 					data.view._badDropAction = new AjxTimedAction(data.view, data.view._apptBadDropEffect);
1368 				}
1369 
1370 				// Line equation is y = mx + c. Solve for c, and set up d (direction)
1371 				var m = (bd.dragEndY - bd.dragStartY) / (bd.dragEndX - bd.dragStartX);
1372 				data.view._badDropAction.args = [m, bd.dragStartY - (m * bd.dragStartX), (bd.dragStartX - bd.dragEndX < 0) ? -1 : 1];
1373 				AjxTimedAction.scheduleAction(data.view._badDropAction, 0);
1374 			}
1375 		}
1376 	}
1377 
1378     if (mouseEv) {
1379         mouseEv._stopPropagation = true;
1380         mouseEv._returnValue = false;
1381         if (ev) {
1382             mouseEv.setToDhtmlEvent(ev);
1383         }
1384     }
1385 	return false;
1386 };
1387 
1388 ZmCalBaseView.prototype._deselectDnDHighlight =
1389 function(data) {
1390 }
1391 ZmCalBaseView.prototype._restoreAppt =
1392 function(data) {
1393 }
1394 
1395 
1396 ZmCalBaseView.prototype._apptBadDropEffect =
1397 function(m, c, d) {
1398 	var usingX = (Math.abs(m) <= 1);
1399 	// Use the bigger delta to control the snap effect
1400 	var bd = this._badDrop;
1401 	var delta = usingX ? bd.dragStartX - bd.dragEndX : bd.dragStartY - bd.dragEndY;
1402 	if (delta * d > 0) {
1403 		if (usingX) {
1404 			bd.dragEndX += (30 * d);
1405 			bd.icon.style.top = m * bd.dragEndX + c;
1406 			bd.icon.style.left = bd.dragEndX;
1407 		} else {
1408 			bd.dragEndY += (30 * d);
1409 			bd.icon.style.top = bd.dragEndY;
1410 			bd.icon.style.left = (bd.dragEndY - c) / m;
1411 		}
1412 		AjxTimedAction.scheduleAction(this._badDropAction, 0);
1413 	} else {
1414 		Dwt.setVisible(bd.icon, false);
1415 		bd.icon = null;
1416 	}
1417 };
1418 
1419 // --- Functions to be overridden for DnD
1420 ZmCalBaseView.prototype._createContainerRect =
1421 function(data) {
1422     this._containerRect = new DwtRectangle(0,0,0,0);
1423 }
1424 
1425 ZmCalBaseView.prototype._clearSnap =
1426 function(snap) { }
1427 
1428 ZmCalBaseView.prototype._apptDndBegin =
1429 function(data) {
1430     return  false;
1431 }
1432 
1433 ZmCalBaseView.prototype._restoreHighlight =
1434 function(data) { }
1435 
1436 ZmCalBaseView.prototype._doApptMove =
1437 function(data, deltaX, deltaY) { }
1438 
1439 ZmCalBaseView.prototype._restoreApptLoc =
1440 function(data) { }
1441 
1442 ZmCalBaseView.prototype._cancelNewApptDrag =
1443 function(data) {
1444     if (data && data.newApptDivEl) {
1445         // ESC key is pressed while dragging the mouse
1446         // Undo the drag event and hide the new appt div
1447         data.gridEl.style.cursor = 'auto';
1448         var col = data.view._getColFromX(data.gridX);
1449 	    data.folderId = col ? (col.cal ? col.cal.id : null) : null;
1450 		Dwt.setVisible(data.newApptDivEl, false);
1451     }
1452 };
1453 
1454 ZmCalBaseView.prototype._handleApptScrollRegion =
1455 function(docX, docY, incr, data) {  }
1456 
1457 ZmCalBaseView.prototype.startIndicatorTimer=function() { };
1458 
1459 ZmCalBaseView.prototype.setTimer=function(min){
1460     var period = min*60*1000;
1461     return AjxTimedAction.scheduleAction(new AjxTimedAction(this, this.updateTimeIndicator), period);
1462 };
1463 
1464 ZmCalBaseView.prototype.updateTimeIndicator=function() { };
1465 
1466 /**
1467  * De-selects a selected appointment
1468  *
1469  * @param   {array}  appts an array of appointments
1470  */
1471 ZmCalBaseView.prototype.deselectAppt =
1472 function (appts) {
1473     appts = AjxUtil.toArray(appts);
1474 
1475     var type = this._getItemData(appts, "type");
1476 
1477     for(var i = 0; i < appts.length; i++) {
1478         var selIdx = this._selectedItems.indexOf(appts[i]);
1479 
1480         if (selIdx < 0) {
1481             continue;
1482         }
1483 
1484         // despite their code and general architecture, calendar
1485         // views never have more than one selected item, so just
1486         // switch to focus to the view itself
1487         this.setFocusElement(this.getHtmlElement());
1488 
1489         appts[i].className = this._getStyle(type);
1490         this._selectedItems.remove(appts[i]);
1491         this._selEv.detail = DwtListView.ITEM_DESELECTED;
1492         this._selEv.item = appts[i];
1493         this._evtMgr.notifyListeners(DwtEvent.SELECTION, this._selEv);
1494     }
1495 };
1496