1 <?xml version="1.0" encoding="UTF-8"?>
2 <!--
3 - ***** BEGIN LICENSE BLOCK *****
4 - Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 -
6 - The contents of this file are subject to the Mozilla Public License Version
7 - 1.1 (the "License"); you may not use this file except in compliance with
8 - the License. You may obtain a copy of the License at
9 - http://www.mozilla.org/MPL/
10 -
11 - Software distributed under the License is distributed on an "AS IS" basis,
12 - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 - for the specific language governing rights and limitations under the
14 - License.
15 -
16 - The Original Code is calendar views.
17 -
18 - The Initial Developer of the Original Code is
19 - Oracle Corporation
20 - Portions created by the Initial Developer are Copyright (C) 2005
21 - the Initial Developer. All Rights Reserved.
22 -
23 - Contributor(s):
24 - Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
25 - Thomas Benisch <thomas.benisch@sun.com>
26 - Dan Mosedale <dan.mosedale@oracle.com>
27 - Michael Buettner <michael.buettner@sun.com>
28 - Philipp Kewisch <mozilla@kewis.ch>
29 - Markus Adrario <MarkusAdrario@web.de>
30 -
31 - Alternatively, the contents of this file may be used under the terms of
32 - either the GNU General Public License Version 2 or later (the "GPL"), or
33 - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
34 - in which case the provisions of the GPL or the LGPL are applicable instead
35 - of those above. If you wish to allow use of your version of this file only
36 - under the terms of either the GPL or the LGPL, and not to allow others to
37 - use your version of this file under the terms of the MPL, indicate your
38 - decision by deleting the provisions above and replace them with the notice
39 - and other provisions required by the GPL or the LGPL. If you do not delete
40 - the provisions above, a recipient may use your version of this file under
41 - the terms of any one of the MPL, the GPL or the LGPL.
42 -
43 - ***** END LICENSE BLOCK *****
44 -->
45
46 <!-- Note that this file depends on helper functions in calUtils.js-->
47
48 <bindings id="calendar-multiday-view-bindings"
49 xmlns="http://www.mozilla.org/xbl"
50 xmlns:html="http://www.w3.org/1999/xhtml"
51 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
52 xmlns:xbl="http://www.mozilla.org/xbl">
53
54 <!--
55 - This is the time bar that displays time divisions to the side
56 - or top of a multiday view.
57 -->
58 <binding id="calendar-time-bar">
59 <content>
60 <xul:box xbl:inherits="orient,width,height" flex="1" anonid="topbox">
61 </xul:box>
62 </content>
63
64 <implementation>
65 <field name="mPixPerMin">0.6</field>
66 <field name="mStartMin">0*60</field>
67 <field name="mEndMin">24*60</field>
68 <field name="mDayStartHour">0</field>
69 <field name="mDayEndHour">24</field>
70
71 <constructor>
72 this.relayout();
73 </constructor>
74
75 <method name="setDayStartEndHours">
76 <parameter name="aDayStartHour"/>
77 <parameter name="aDayEndHour"/>
78 <body><![CDATA[
79 if (aDayStartHour * 60 < this.mStartMin ||
80 aDayStartHour > aDayEndHour ||
81 aDayEndHour * 60 > this.mEndMin) {
82 throw Components.results.NS_ERROR_INVALID_ARG;
83 }
84 if (this.mDayStartHour != aDayStartHour ||
85 this.mDayEndHour != aDayEndHour) {
86 this.mDayEndHour = aDayEndHour;
87 this.mDayStartHour = aDayStartHour;
88
89 var topbox = document.getAnonymousElementByAttribute(this, "anonid", "topbox");
90 if (topbox.childNodes.length) {
91 // This only needs to be done if the initial relayout has
92 // already happened, otherwise it will be done then.
93 for (var hour = this.mStartMin / 60; hour < this.mEndMin / 60; hour++) {
94 if (hour < this.mDayStartHour || hour > this.mDayEndHour) {
95 topbox.childNodes[hour].setAttribute("off-time", "true");
96 } else {
97 topbox.childNodes[hour].removeAttribute("off-time");
98 }
99 }
100 }
101 }
102 ]]></body>
103 </method>
104
105 <method name="setAttribute">
106 <parameter name="aAttr"/>
107 <parameter name="aVal"/>
108 <body><![CDATA[
109 var needsrelayout = false;
110 if (aAttr == "orient") {
111 if (this.getAttribute("orient") != aVal)
112 needsrelayout = true;
113 }
114
115 // this should be done using lookupMethod(), see bug 286629
116 var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
117
118 if (needsrelayout) {
119 this.relayout();
120 }
121
122 return ret;
123 ]]></body>
124 </method>
125
126 <property name="pixelsPerMinute"
127 onget="return this.mPixPerMin"
128 onset="if (this.mPixPerMin != val) { this.mPixPerMin = val; this.relayout(); } return val;"/>
129
130 <method name="relayout">
131 <body><![CDATA[
132 //dump ("calendar-time-bar: relayout\n");
133 function createXULElement(el) {
134 return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
135 }
136
137 var topbox = document.getAnonymousElementByAttribute(this, "anonid", "topbox");
138 var orient = topbox.getAttribute("orient");
139 var otherorient = "vertical";
140 if (!orient) orient = "horizontal";
141 if (orient == "vertical") otherorient = "horizontal";
142
143 //dump ("calendar-time-bar: orient: " + orient + " other: " + otherorient + "\n");
144
145 function makeTimeBox(timestr, size) {
146 var box = createXULElement("box");
147 box.setAttribute("orient", orient);
148
149 if (orient == "horizontal") {
150 box.setAttribute("width", size);
151 } else {
152 box.setAttribute("height", size);
153 }
154
155 var label = createXULElement("label");
156 label.setAttribute("class", "calendar-time-bar-label");
157 label.setAttribute("value", timestr);
158 label.setAttribute("align", "center");
159
160 box.appendChild(label);
161
162 return box;
163 }
164
165 while (topbox.lastChild)
166 topbox.removeChild(topbox.lastChild);
167
168 var formatter = Components.classes["@mozilla.org/intl/scriptabledateformat;1"].
169 getService(Components.interfaces.nsIScriptableDateFormat);
170 var timeString;
171 var theMin = this.mStartMin;
172 var theHour = Math.floor(theMin / 60);
173 var durLeft = this.mEndMin - this.mStartMin;
174
175 while (durLeft > 0) {
176 var dur;
177 if (this.mEndMin - theMin < 60) {
178 dur = this.mEndMin - theMin;
179 } else {
180 dur = theMin % 60;
181 }
182 theMin += dur;
183 if (dur == 0) dur = 60;
184
185 // calculate duration pixel as the difference between
186 // start pixel and end pixel to avoid rounding errors.
187 var startPix = Math.round(theMin * this.mPixPerMin);
188 var endPix = Math.round((theMin + dur) * this.mPixPerMin);
189 var durPix = endPix - startPix;
190 var box;
191 if (dur != 60) {
192 box = makeTimeBox("", durPix);
193 } else {
194 timeString = formatter.FormatTime("",
195 Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
196 theHour, 0, 0);
197 box = makeTimeBox(timeString, durPix);
198 }
199
200 // Set up workweek hours
201 if (theHour < this.mDayStartHour || theHour >= this.mDayEndHour) {
202 box.setAttribute("off-time", "true");
203 }
204
205 box.setAttribute("class", "calendar-time-bar-box-" + (theHour % 2 == 0 ? "even" : "odd"));
206 topbox.appendChild(box);
207
208 durLeft -= dur;
209 theMin += dur;
210 theHour++;
211 }
212 ]]></body>
213 </method>
214 </implementation>
215 </binding>
216
217 <!--
218 - A simple gripbar that is displayed at the start and end of an
219 - event box. Needs to handle being dragged and resizing the
220 - event, thus changing its start/end time.
221 -->
222 <binding id="calendar-event-gripbar">
223 <content>
224 <xul:box anonid="thebox" flex="1">
225 <xul:spacer flex="1"/>
226 <xul:image xbl:inherits="class"/>
227 <xul:spacer flex="1"/>
228 </xul:box>
229 </content>
230
231 <implementation>
232 <!-- public -->
233 <field name="eventElement">null</field>
234
235 <property name="parentorient">
236 <getter><![CDATA[
237 return this.getAttribute("parentorient");
238 ]]></getter>
239 <setter><![CDATA[
240 this.setAttribute("parentorient", val);
241 var thebox = document.getAnonymousElementByAttribute(this, "anonid", "thebox");
242 if (val == "vertical")
243 thebox.setAttribute("orient", "horizontal");
244 else
245 thebox.setAttribute("orient", "vertical");
246 return val;
247 ]]></setter>
248 </property>
249
250 <!-- private -->
251 <field name="mSide">top</field>
252
253 <field name="mResizing">false</field>
254 <field name="mSizeStartX">0</field>
255 <field name="mSizeStartY">0</field>
256
257 <constructor><![CDATA[
258 if (this.getAttribute("side") == "top")
259 this.mSide = "top";
260 else if (this.getAttribute("side") == "bottom")
261 this.mSide = "bottom";
262 this.parentorient = this.getAttribute("parentorient");
263 ]]></constructor>
264
265 </implementation>
266
267 <handlers>
268 <handler event="mousedown" button="0"><![CDATA[
269 // store the attribute 'whichside' in the event object
270 // but *don't* call stopPropagation(). as soon as the
271 // enclosing event box will receive the event it will
272 // make use of this information in order to invoke the
273 // appropriate action.
274 event.whichside = this.getAttribute("whichside");
275 ]]></handler>
276 <handler event="click" button="0"><![CDATA[
277 event.stopPropagation();
278 ]]></handler>
279 </handlers>
280 </binding>
281
282 <!--
283 - A column for displaying event boxes in. One column per
284 - day; it manages the layout of the events given via add/deleteEvent.
285 -->
286 <binding id="calendar-event-column">
287 <content>
288 <xul:stack anonid="boxstack" flex="1" style="min-width: 1px; min-height: 1px">
289 <xul:box anonid="bgbox" flex="1" style="min-width: 1px; min-height: 1px"/>
290 <xul:box xbl:inherits="context" anonid="topbox" flex="1" equalsize="always" style="min-width: 1px; min-height: 1px"/>
291 <xul:box anonid="fgbox" flex="1" class="fgdragcontainer" style="min-width: 1px; min-height: 1px; overflow:hidden;">
292 <xul:box anonid="fgdragspacer" style="display: inherit; overflow: hidden;">
293 <xul:spacer flex="1"/>
294 <xul:label anonid="fgdragbox-startlabel" class="fgdragbox-label"/>
295 </xul:box>
296 <xul:box anonid="fgdragbox" class="fgdragbox" />
297 <xul:label anonid="fgdragbox-endlabel" class="fgdragbox-label"/>
298 </xul:box>
299 </xul:stack>
300 </content>
301
302 <implementation>
303 <constructor><![CDATA[
304 this.mEvents = Array();
305 this.mTimezone = UTC();
306 ]]></constructor>
307
308 <!-- fields -->
309 <field name="mPixPerMin">0.6</field>
310 <field name="mStartMin">0*60</field>
311 <field name="mEndMin">24*60</field>
312 <field name="mDayStartMin">8*60</field>
313 <field name="mDayEndMin">17*60</field>
314 <field name="mEvents">new Array()</field>
315 <field name="mEventMap">null</field>
316 <field name="mCalendarView">null</field>
317 <field name="mDate">null</field>
318 <field name="mTimezone">null</field>
319 <field name="mDragState">null</field>
320 <field name="mLayoutBatchCount">0</field>
321 <!-- Since we'll often be getting many events in rapid succession, this
322 timer helps ensure that we don't re-compute the event map too many
323 times in a short interval, and therefore improves performance.-->
324 <field name="mEventMapTimeout">null</field>
325 <!-- Sometimes we need to add resize handlers for columns with special
326 widths. When we relayout, we need to cancel those handlers -->
327 <field name="mHandlersToRemove">new Array()</field>
328
329 <!-- Set this true so that we know in our onAddItem listener to start
330 - modifying an event when it comes back to us as created
331 -->
332 <field name="mCreatedNewEvent">false</field>
333 <field name="mEventToEdit">null</field>
334
335 <!-- properties -->
336 <property name="pixelsPerMinute">
337 <getter><![CDATA[
338 return this.mPixPerMin;
339 ]]></getter>
340 <setter><![CDATA[
341 if (val <= 0.0)
342 val = 0.01;
343 if (val != this.mPixPerMin) {
344 this.mPixPerMin = val;
345 this.relayout();
346 }
347 return val;
348 ]]></setter>
349 </property>
350
351 <field name="mSelected">false</field>
352 <property name="selected">
353 <getter><![CDATA[
354 return this.mSelected;
355 ]]></getter>
356 <setter><![CDATA[
357 this.mSelected = val;
358 if (this.bgbox && this.bgbox.hasChildNodes()) {
359 var child = this.bgbox.firstChild;
360 while (child) {
361 if (val) {
362 child.setAttribute("selected", "true");
363 } else {
364 child.removeAttribute("selected");
365 }
366 child = child.nextSibling;
367 }
368 }
369 return val;
370 ]]></setter>
371 </property>
372
373 <property name="date">
374 <getter><![CDATA[
375 return this.mDate;
376 ]]></getter>
377 <setter><![CDATA[
378 this.mDate = val;
379
380 if (!compareObjects(val.timezone, this.mTimezone)) {
381 //dump ("++ column tz: " + val.timezone + "\n");
382 this.mTimezone = val.timezone;
383 if (!this.mLayoutBatchCount) {
384 this.recalculateStartEndMinutes();
385 }
386 }
387
388 return val;
389 ]]></setter>
390 </property>
391
392 <property name="calendarView"
393 onget="return this.mCalendarView;"
394 onset="return (this.mCalendarView = val);" />
395
396 <property
397 name="topbox"
398 readonly="true">
399 <getter><![CDATA[
400 return document.getAnonymousElementByAttribute(this, "anonid", "topbox");
401 ]]></getter>
402 </property>
403
404 <property
405 name="bgbox"
406 readonly="true">
407 <getter><![CDATA[
408 return document.getAnonymousElementByAttribute(this, "anonid", "bgbox");
409 ]]></getter>
410 </property>
411
412 <field name="mFgboxes">null</field>
413 <property
414 name="fgboxes"
415 readonly="true">
416 <getter><![CDATA[
417 if (this.mFgboxes == null) {
418 this.mFgboxes = {
419 box: document.getAnonymousElementByAttribute(this, "anonid", "fgbox"),
420 dragbox: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox"),
421 dragspacer: document.getAnonymousElementByAttribute(this, "anonid", "fgdragspacer"),
422 startlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-startlabel"),
423 endlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-endlabel")
424 };
425 }
426 return this.mFgboxes;
427 ]]></getter>
428 </property>
429
430 <property
431 name="events"
432 readonly="true"
433 onget="return this.methods"/>
434
435 <field name="mDayOff">false</field>
436 <property name="dayOff">
437 <getter><![CDATA[
438 return this.mDayOff;
439 ]]></getter>
440 <setter><![CDATA[
441 this.mDayOff = val;
442 return val;
443 ]]></setter>
444 </property>
445
446 <!-- mEvents -->
447 <field name="mSelectedChunks">[]</field>
448
449 <method name="selectOccurrence">
450 <parameter name="aOccurrence"/>
451 <body><![CDATA[
452 if (aOccurrence) {
453 var chunk = this.findChunkForOccurrence(aOccurrence);
454 if (!chunk) {
455 dump("++ Couldn't find chunk to select!!!\n");
456 return;
457 }
458 chunk.selected = true;
459 this.mSelectedChunks.push(chunk);
460 }
461 ]]></body>
462 </method>
463
464 <method name="unselectOccurrence">
465 <parameter name="aOccurrence"/>
466 <body><![CDATA[
467 if (aOccurrence) {
468 var chunk = this.findChunkForOccurrence(aOccurrence);
469 if (!chunk) {
470 dump ("++ Couldn't find chunk to unselect!!!\n");
471 return;
472 }
473 chunk.selected = false;
474 var index = this.mSelectedChunks.indexOf(chunk);
475 this.mSelectedChunks.splice(index, 1);
476 }
477 ]]></body>
478 </method>
479
480 <method name="findChunkForOccurrence">
481 <parameter name="aOccurrence"/>
482 <body><![CDATA[
483 for each (var chunk in this.mEventBoxes) {
484 if (chunk.occurrence.hashId == aOccurrence.hashId) {
485 return chunk;
486 }
487 }
488
489 return null;
490 ]]></body>
491 </method>
492
493 <method name="startLayoutBatchChange">
494 <body><![CDATA[
495 this.mLayoutBatchCount++;
496 ]]></body>
497 </method>
498 <method name="endLayoutBatchChange">
499 <body><![CDATA[
500 this.mLayoutBatchCount--;
501 if (this.mLayoutBatchCount == 0)
502 this.relayout();
503 ]]></body>
504 </method>
505
506 <method name="setAttribute">
507 <parameter name="aAttr"/>
508 <parameter name="aVal"/>
509 <body><![CDATA[
510 // this should be done using lookupMethod(), see bug 286629
511 var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
512
513 if (aAttr == "orient" && this.getAttribute("orient") != aVal) {
514 this.relayout();
515 }
516
517 return ret;
518 ]]></body>
519 </method>
520
521 <method name="internalDeleteEvent">
522 <parameter name="aOccurrence"/>
523 <body><![CDATA[
524 var itemIndex = -1;
525 var i;
526 for (var i in this.mEvents) {
527 occ = this.mEvents[i].event;
528 if (occ.hashId == aOccurrence.hashId)
529 {
530 itemIndex = i;
531 break;
532 }
533 }
534
535 if (itemIndex != -1) {
536 function isNotItem(a) {
537 return a.occurrence.hashId != aOccurrence.hashId;
538 }
539 this.mSelectedChunks = this.mSelectedChunks.filter(isNotItem);
540
541 this.mEvents.splice(itemIndex, 1);
542 return true;
543 } else {
544 return false;
545 }
546 ]]></body>
547 </method>
548
549 <method name="recalculateStartEndMinutes">
550 <body><![CDATA[
551 for each (var chunk in this.mEvents) {
552 var mins = this.getStartEndMinutesForOccurrence(chunk.event);
553 chunk.startMinute = mins.start;
554 chunk.endMinute = mins.end;
555 }
556
557 this.relayout();
558 ]]></body>
559 </method>
560
561 <!-- NOTE: This function may not return the true start and end time
562 of an occurrence if that occurrence starts or ends on a day
563 different than the day of this column -->
564 <method name="getStartEndMinutesForOccurrence">
565 <parameter name="aOccurrence"/>
566 <body><![CDATA[
567 var stdate = aOccurrence.startDate || aOccurrence.entryDate;
568 var enddate = aOccurrence.endDate || aOccurrence.dueDate;
569
570 if (!compareObjects(stdate.timezone, this.mTimezone)) {
571 stdate = stdate.getInTimezone (this.mTimezone);
572 }
573
574 if (!compareObjects(enddate.timezone, this.mTimezone)) {
575 enddate = enddate.getInTimezone (this.mTimezone);
576 }
577
578 var startHour = stdate.hour;
579 var startMinute = stdate.minute;
580 var endHour = enddate.hour;
581 var endMinute = enddate.minute;
582
583 // Handle cases where an event begins or ends on a day other than this
584 if (stdate.compare(this.mDate) == -1) {
585 startHour = 0;
586 startMinute = 0;
587 }
588 if (enddate.compare(this.mDate) == 1) {
589 endHour = 24;
590 endMinute = 0;
591 }
592
593 return { start: startHour * 60 + startMinute,
594 end: endHour * 60 + endMinute };
595 ]]></body>
596 </method>
597
598 <method name="createChunk">
599 <parameter name="aOccurrence"/>
600 <body><![CDATA[
601 var mins = this.getStartEndMinutesForOccurrence(aOccurrence);
602
603 var chunk = {
604 startMinute: mins.start,
605 endMinute: mins.end,
606 event: aOccurrence
607 };
608 return chunk;
609 ]]></body>
610 </method>
611
612 <method name="addEvent">
613 <parameter name="aOccurrence"/>
614 <body><![CDATA[
615 this.internalDeleteEvent(aOccurrence);
616
617 var chunk = this.createChunk(aOccurrence);
618 this.mEvents.push(chunk);
619 if (this.mEventMapTimeout) {
620 clearTimeout(this.mEventMapTimeout);
621 }
622 var column = this;
623
624 if (this.mCreatedNewEvent) {
625 this.mEventToEdit = aOccurrence;
626 }
627 // Fun with scoping...
628 this.mEventMapTimeout = setTimeout(function() { column.relayout.call(column) }, 5);
629 ]]></body>
630 </method>
631
632 <method name="deleteEvent">
633 <parameter name="aOccurrence"/>
634 <body><![CDATA[
635 if (this.internalDeleteEvent(aOccurrence))
636 this.relayout();
637 ]]></body>
638 </method>
639
640 <method name="clear">
641 <body><![CDATA[
642 while (this.bgbox && this.bgbox.hasChildNodes())
643 this.bgbox.removeChild(this.bgbox.lastChild);
644 while (this.topbox && this.topbox.hasChildNodes())
645 this.topbox.removeChild(this.topbox.lastChild);
646 for each (handler in this.mHandlersToRemove)
647 window.removeEventListener("resize", handler, true);
648 this.mHandlersToRemove = [];
649 ]]></body>
650 </method>
651
652 <method name="relayout">
653 <body><![CDATA[
654
655 if (this.mLayoutBatchCount > 0)
656 return;
657
658 function createXULElement(el) {
659 return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
660 }
661
662 this.clear();
663
664 var orient = this.getAttribute("orient");
665 var otherorient = "vertical";
666 if (!orient) orient = "horizontal";
667 if (orient == "vertical") otherorient = "horizontal";
668
669 // bgbox is used mainly for drawing the grid. at some point it may
670 // also be used for all-day events.
671 this.bgbox.setAttribute("orient", orient);
672
673 var theMin = this.mStartMin;
674 while (theMin < this.mEndMin) {
675 var dur = theMin % 60;
676 theMin += dur;
677 if (dur == 0) dur = 60;
678
679 var box = createXULElement("spacer");
680 // we key off this in a CSS selector
681 box.setAttribute("orient", orient);
682 box.setAttribute("class", "calendar-event-column-linebox");
683
684 if (this.mSelected) {
685 box.setAttribute("selected", "true");
686 }
687 if (this.mDayOff) {
688 box.setAttribute("weekend", "true");
689 }
690 if (theMin < this.mDayStartMin || theMin >= this.mDayEndMin) {
691 box.setAttribute("off-time", "true");
692 }
693
694 // Carry forth the day relation
695 box.setAttribute("relation", this.getAttribute("relation"));
696
697 // calculate duration pixel as the difference between
698 // start pixel and end pixel to avoid rounding errors.
699 var startPix = Math.round(theMin * this.mPixPerMin);
700 var endPix = Math.round((theMin + dur) * this.mPixPerMin);
701 var durPix = endPix - startPix;
702 if (orient == "vertical")
703 box.setAttribute("height", durPix);
704 else
705 box.setAttribute("width", durPix);
706
707 box.setAttribute("style", "min-width: 1px; min-height: 1px;");
708
709 this.bgbox.appendChild(box);
710 theMin += 60;
711 }
712
713 // fgbox is used for dragging events
714 this.fgboxes.box.setAttribute("orient", orient);
715 document.getAnonymousElementByAttribute(this, "anonid", "fgdragspacer").setAttribute("orient", orient);
716
717 // this one is set to otherorient, since it will contain
718 // child boxes set to "orient" (one for each set of
719 // overlapping event areas)
720 this.topbox.setAttribute("orient", otherorient);
721
722 this.mEventMap = this.computeEventMap();
723 this.mEventBoxes = new Array();
724
725 if (!this.mEventMap.length) {
726 return;
727 }
728
729 // First of all we create a xul:stack which
730 // will hold all events for this event column.
731 // The stack will be grouped below .../calendar-event-column/stack/topbox.
732 var stack = createXULElement("stack");
733 stack.setAttribute("flex", "1");
734 this.topbox.appendChild(stack);
735
736 var boxToEdit;
737
738 for each (var layer in this.mEventMap) {
739
740 // The event-map (this.mEventMap) contains an array of layers.
741 // For each layer we create a box below the stack just created above.
742 // So each different layer lives in a box that's contained in the stack.
743 var xulColumn = createXULElement("box");
744 xulColumn.setAttribute("orient", otherorient);
745 xulColumn.setAttribute("flex", "1");
746 xulColumn.setAttribute("style", "min-width: 1px; min-height: 1px;");
747 stack.appendChild(xulColumn);
748
749 var numBlocksInserted = 0;
750
751 // Each layer contains a list of the columns that
752 // need to be created for a span.
753 for each (var column in layer) {
754
755 var innerColumn = createXULElement("box");
756 innerColumn.setAttribute("orient", orient);
757 innerColumn.setAttribute("flex", 1);
758 var style = "min-width: 1px; min-height: 1px;";
759
760 if (column.specialSpan) {
761 // Special case when we can't simply rely on flex. Only
762 // happens when we have columns in the layer that need to
763 // be different sizes. That is, when we have a colSpan
764 // of 2, a total of 5 columns, and a startCol of 1.
765 // Then, our columns need to be laid out as 1/5, 2/5, 2/5.
766 if (orient == "vertical") {
767 style += "max-width: " +
768 column.specialSpan * this.topbox.boxObject.width +
769 "px;";
770 } else {
771 style += "max-height: " +
772 column.specialSpan * this.topbox.boxObject.height +
773 "px;";
774 }
775
776 // Now we need to set up a resize listener, since without
777 // it our box will look funny if the window resizes. This
778 // requires us to be *very* careful about closures, because
779 // we don't want things like column.specialSpan to change
780 function colResizeHandler(aInnerCol, aCalCol, aSpan) {
781 this.handleEvent = function(aEvent) {
782 var resizeStyle = "min-width: 1px; min-height: 1px;";
783 if (orient == "vertical") {
784 resizeStyle += "max-width: " +
785 aSpan * aCalCol.topbox.boxObject.width +
786 "px;";
787 } else {
788 resizeStyle += "max-height: " +
789 aSpan * aCalCol.topbox.boxObject.height +
790 "px;";
791 }
792 aInnerCol.setAttribute("style", resizeStyle);
793 };
794 }
795 var myResizeHandler = new colResizeHandler(innerColumn, this, column.specialSpan);
796 this.mHandlersToRemove.push(myResizeHandler);
797 window.addEventListener("resize", myResizeHandler, true);
798 }
799 innerColumn.setAttribute("style", style);
800
801 xulColumn.appendChild(innerColumn);
802
803 var curTime = 0;
804 for each (var chunk in column) {
805 var duration = chunk.duration;
806 if (!duration) {
807 continue;
808 }
809
810 if (chunk.event) {
811 var chunkBox = createXULElement("calendar-event-box");
812 chunkBox.setAttribute("context", this.getAttribute("item-context") || this.getAttribute("context"));
813 chunkBox.setAttribute("style", "min-width: 1px; min-height: 1px;");
814 chunkBox.setAttribute("orient", orient);
815 var durMinutes = duration.inSeconds / 60;
816 if (orient == "vertical") {
817 chunkBox.setAttribute("height", durMinutes * this.mPixPerMin);
818 } else {
819 chunkBox.setAttribute("width", durMinutes * this.mPixPerMin);
820 }
821
822 if (!isCalendarWritable(chunk.event.calendar)) {
823 chunkBox.setAttribute("readOnly", "true");
824 }
825
826 if (chunk.event.hashId in this.calendarView.mFlashingEvents) {
827 chunkBox.setAttribute("flashing", "true");
828 }
829
830 innerColumn.appendChild(chunkBox);
831
832 chunkBox.calendarView = this.calendarView;
833 chunkBox.occurrence = chunk.event;
834 chunkBox.parentColumn = this;
835
836 this.mEventBoxes.push(chunkBox);
837
838 if (this.mEventToEdit &&
839 chunkBox.occurrence.hashId == this.mEventToEdit.hashId) {
840 boxToEdit = chunkBox;
841 }
842 } else {
843 var chunkBox = createXULElement("spacer");
844 chunkBox.setAttribute("context", this.getAttribute("context"));
845 chunkBox.setAttribute("style", "min-width: 1px; min-height: 1px;");
846 chunkBox.setAttribute("orient", orient);
847 chunkBox.setAttribute("class", "calendar-empty-space-box");
848 innerColumn.appendChild(chunkBox);
849
850 var durMinutes = duration.inSeconds / 60;
851 if (orient == "vertical") {
852 chunkBox.setAttribute("height", durMinutes * this.mPixPerMin);
853 } else {
854 chunkBox.setAttribute("width", durMinutes * this.mPixPerMin);
855 }
856 }
857 }
858
859 numBlocksInserted++;
860 curTime += duration;
861 }
862
863 if (boxToEdit) {
864 this.mCreatedNewEvent = false;
865 this.mEventToEdit = null;
866 boxToEdit.startEditing();
867 }
868
869 if (numBlocksInserted == 0) {
870 // if we didn't insert any blocks, then
871 // forget about this column
872 stack.removeChild(xulColumn);
873 }
874 }
875 ]]></body>
876 </method>
877
878 <method name="computeEventMap">
879 <body><![CDATA[
880 /* We're going to create a series of 'blobs'. A blob is a series of
881 * events that create a continuous block of busy time. In other
882 * words, a blob ends when there is some time such that no events
883 * occupy that time.
884 *
885 * Each blob will be an array of objects with the following properties:
886 * item: the event/task
887 * startCol: the starting column to display the event in (0-indexed)
888 * colSpan: the number of columns the item spans
889 *
890 * An item with no conflicts will have startCol: 0 and colSpan: 1.
891 */
892 var blobs = new Array();
893 var currentBlob = new Array();
894
895 function sortByStart(a, b) {
896 // If you pass in tasks without both entry and due dates, I will
897 // kill you
898 var aStart = a.event.startDate || a.event.entryDate;
899 var bStart = b.event.startDate || b.event.entryDate;
900 var startComparison = aStart.compare(bStart);
901 if (startComparison != 0) {
902 return startComparison;
903 } else {
904 var aEnd = a.event.endDate || a.event.dueDate;
905 var bEnd = b.event.endDate || b.event.dueDate;
906 // If the items start at the same time, return the longer one
907 // first
908 return bEnd.compare(aEnd);
909 }
910 }
911
912 this.mEvents.sort(sortByStart);
913
914 // The end time of the last ending event in the entire blob
915 var latestItemEnd;
916
917 // This array keeps track of the last (latest ending) item in each of
918 // the columns of the current blob. We could reconstruct this data at
919 // any time by looking at the items in the blob, but that would hurt
920 // perf.
921 var colEndArray = new Array();
922
923 /* Go through a 3 step process to try and place each item.
924 * Step 1: Look for an existing column with room for the item.
925 * Step 2: Look for a previously placed item that can be shrunk in
926 * width to make room for the item.
927 * Step 3: Give up and create a new column for the item.
928 *
929 * (The steps are explained in more detail as we come to them)
930 */
931 for (var i in this.mEvents) {
932 var item = this.mEvents[i].event;
933 var itemStart = item.startDate || item.entryDate;
934 var itemEnd = item.endDate || item.dueDate;
935 if (!latestItemEnd) {
936 latestItemEnd = itemEnd;
937 }
938 if (currentBlob.length && latestItemEnd &&
939 itemStart.compare(latestItemEnd) != -1) {
940 // We're done with this current blob because item starts
941 // after the last event in the current blob ended.
942 blobs.push({blob: currentBlob, totalCols: colEndArray.length});
943
944 // Reset our variables
945 currentBlob = new Array();
946 colEndArray = new Array();
947 }
948
949 // Place the item in its correct place in the blob
950 var placedItem = false;
951
952 // Step 1
953 // Look for a possible column in the blob that has been left open. This
954 // would happen if we already have multiple columns but some of
955 // the cols have events before latestItemEnd. For instance
956 // | | |
957 // |______| |
958 // |ev1 |______|
959 // | |ev2 |
960 // |______| |
961 // | | |
962 // |OPEN! | |<--Our item's start time might be here
963 // | |______|
964 // | | |
965 //
966 // Remember that any time we're starting a new blob, colEndArray
967 // will be empty, but that's ok.
968 for (var ii=0; ii<colEndArray.length; ++ii) {
969 var colEnd = colEndArray[ii].endDate || colEndArray[ii].dueDate;
970 if (colEnd.compare(itemStart) != 1) {
971 // Yay, we can jump into this column
972 colEndArray[ii] = item;
973
974 // Check and see if there are any adjacent columns we can
975 // jump into as well.
976 var lastCol = Number(ii) + 1;
977 while (lastCol < colEndArray.length) {
978 var nextColEnd = colEndArray[lastCol].endDate ||
979 colEndArray[lastCol].dueDate;
980 // If the next column's item ends after we start, we
981 // can't expand any further
982 if (nextColEnd.compare(itemStart) == 1) {
983 break;
984 }
985 colEndArray[lastCol] = item;
986 lastCol++;
987 }
988 // Now construct the info we need to push into the blob
989 currentBlob.push({item: item,
990 startCol: ii,
991 colSpan: lastCol - ii});
992
993 // Update latestItemEnd
994 if (latestItemEnd &&
995 itemEnd.compare(latestItemEnd) == 1) {
996 latestItemEnd = itemEnd;
997 }
998 placedItem = true;
999 break; // Stop iterating through colEndArray
1000 }
1001 }
1002
1003 if (placedItem) {
1004 // Go get the next item
1005 continue;
1006 }
1007
1008 // Step 2
1009 // OK, all columns (if there are any) overlap us. Look if the
1010 // last item in any of the last items in those columns is taking
1011 // up 2 or more cols. If so, shrink it and stick the item in the
1012 // created space. For instance
1013 // |______|______|______|
1014 // |ev1 |ev3 |ev4 |
1015 // | | | |
1016 // | |______| |
1017 // | | |______|
1018 // | |_____________|
1019 // | |ev2 |
1020 // |______| |<--If our item's start time is
1021 // | |_____________| here, we can shrink ev2 and jump
1022 // | | | | in column #3
1023 //
1024 for (var jj=1; jj<colEndArray.length; ++jj) {
1025 if (colEndArray[jj].hashId == colEndArray[jj-1].hashId) {
1026 // Good we found a item that spanned multiple columns.
1027 // Find it in the blob so we can modify its properties
1028 for (var kk in currentBlob) {
1029 if (currentBlob[kk].item.hashId == colEndArray[jj].hashId) {
1030 // Take all but the first spot that the item spanned
1031 var spanOfShrunkItem = currentBlob[kk].colSpan;
1032 currentBlob.push({item: item,
1033 startCol: Number(currentBlob[kk].startCol) + 1,
1034 colSpan: spanOfShrunkItem - 1});
1035
1036 // Update colEndArray
1037 for (var ll = jj; ll < jj + spanOfShrunkItem - 1; ll++) {
1038 colEndArray[ll] = item;
1039 }
1040
1041 // Modify the data on the old item
1042 currentBlob[kk] = {item: currentBlob[kk].item,
1043 startCol: currentBlob[kk].startCol,
1044 colSpan: 1};
1045 // Update latestItemEnd
1046 if (latestItemEnd &&
1047 itemEnd.compare(latestItemEnd) == 1) {
1048 latestItemEnd = itemEnd;
1049 }
1050 break; // Stop iterating through currentBlob
1051 }
1052 }
1053 placedItem = true;
1054 break; // Stop iterating through colEndArray
1055 }
1056 }
1057
1058 if (placedItem) {
1059 // Go get the next item
1060 continue;
1061 }
1062
1063 // Step 3
1064 // Guess what? We still haven't placed the item. We need to
1065 // create a new column for it.
1066
1067 // All the items in the last column, except for the one* that
1068 // conflicts with the item we're trying to place, need to have
1069 // their span extended by 1, since we're adding the new column
1070 //
1071 // * Note that there can only be one, because we sorted our
1072 // events by start time, so this event must start later than
1073 // the start of any possible conflicts.
1074 var lastColNum = colEndArray.length;
1075 for (var mm in currentBlob) {
1076 var mmEnd = currentBlob[mm].item.endDate || currentBlob[mm].item.dueDate
1077 if (currentBlob[mm].startCol + currentBlob[mm].colSpan == lastColNum &&
1078 mmEnd.compare(itemStart) != 1) {
1079 currentBlob[mm] = {item: currentBlob[mm].item,
1080 startCol: currentBlob[mm].startCol,
1081 colSpan: currentBlob[mm].colSpan + 1};
1082 }
1083 }
1084 currentBlob.push({item: item,
1085 startCol: colEndArray.length,
1086 colSpan: 1});
1087 colEndArray.push(item);
1088
1089 // Update latestItemEnd
1090 if (latestItemEnd && itemEnd.compare(latestItemEnd) == 1) {
1091 latestItemEnd = itemEnd;
1092 }
1093 // Go get the next item
1094 }
1095 // Add the last blob
1096 blobs.push({blob: currentBlob,
1097 totalCols: colEndArray.length});
1098
1099 return this.setupBoxStructure(blobs);
1100 ]]></body>
1101 </method>
1102
1103 <method name="setupBoxStructure">
1104 <parameter name="aBlobs"/>
1105 <body><![CDATA[
1106 // This is actually going to end up being a 3-d array
1107 // 1st dimension: "layers", sets of columns of events that all
1108 // should have equal width*
1109 // 2nd dimension: "columns", individual columns of non-conflicting
1110 // items
1111 // 3rd dimension: "chunks", individual items or placeholders for
1112 // the blank time in between them
1113 //
1114 // * Note that 'equal width' isn't strictly correct. If we're
1115 // oriented differently, it will be height (and we'll have rows
1116 // not columns). What's more, in the 'specialSpan' case, the
1117 // columns won't actually have the same size, but will only all
1118 // be multiples of a common size. See the note in the relayout
1119 // function for more info on this (fairly rare) case.
1120 var layers = [];
1121
1122 // When we start a new blob, move to a new set of layers
1123 var layerOffset = 0;
1124 for each (var glob in aBlobs) {
1125
1126 var layerArray = [];
1127 var layerCounter = 1;
1128
1129 for each (var data in glob.blob) {
1130
1131 // from the item at hand we need to figure out on which
1132 // layer and on which column it should go.
1133 var layerIndex;
1134 var specialSpan = null;
1135
1136 // each blob receives its own layer, that's the first part of the story. within
1137 // a given blob we need to distribute the items on different layers depending on
1138 // the number of columns each item spans. if each item just spans a single column
1139 // the blob will cover *one* layer. if the blob contains items that span more than
1140 // a single column, this blob will cover more than one layer. the algorithm places
1141 // the items on the first layer in the case an item covers a single column. new layers
1142 // are introduced based on the start column and number of spanning columns of an item.
1143 if (data.colSpan != 1) {
1144 var index = glob.totalCols * data.colSpan + data.startCol;
1145 layerIndex = layerArray[index];
1146 if (!layerIndex) {
1147 layerIndex = layerCounter++;
1148 layerArray[index] = layerIndex;
1149 }
1150 var offset = ((glob.totalCols - data.colSpan) % glob.totalCols)
1151 if (offset != 0) {
1152 specialSpan = data.colSpan / glob.totalCols;
1153 }
1154 } else {
1155 layerIndex = 0;
1156 }
1157 layerIndex += layerOffset;
1158
1159 // Make sure there's room to insert stuff
1160 while (layerIndex >= layers.length) {
1161 layers.push([]);
1162 }
1163
1164 while (data.startCol >= layers[layerIndex].length) {
1165 layers[layerIndex].push([]);
1166 if (specialSpan) {
1167 layers[layerIndex][layers[layerIndex].length - 1].specialSpan = 1 / glob.totalCols;
1168 }
1169 }
1170
1171 // we now retrieve the column from 'layerIndex' and 'startCol'.
1172 var col = layers[layerIndex][data.startCol];
1173 if (specialSpan) {
1174 col.specialSpan = specialSpan;
1175 }
1176
1177 // take into account that items can span several days.
1178 // that's why i'm clipping the start- and end-time to the
1179 // timespan of this column.
1180 var start = data.item.startDate || data.item.entryDate;
1181 if (!compareObjects(start.timezone, this.mTimezone)) {
1182 start = start.getInTimezone(this.mTimezone);
1183 }
1184 if (start.year != this.date.year ||
1185 start.month != this.date.month ||
1186 start.day != this.date.day) {
1187 start = start.clone();
1188 start.resetTo(this.date.year,
1189 this.date.month,
1190 this.date.day,
1191 0,this.mStartMin,0,
1192 start.timezone);
1193 }
1194 var end = data.item.endDate || data.item.dueDate;
1195 if (!compareObjects(end.timezone, this.mTimezone)) {
1196 end = end.getInTimezone(this.mTimezone);
1197 }
1198 if (end.year != this.date.year ||
1199 end.month != this.date.month ||
1200 end.day != this.date.day) {
1201 end = end.clone();
1202 end.resetTo(this.date.year,
1203 this.date.month,
1204 this.date.day,
1205 0,this.mEndMin,0,
1206 end.timezone);
1207 }
1208 var prevEnd;
1209 if (col.length > 0) {
1210 // Fill in time gaps with a placeholder
1211 prevEnd = col[col.length - 1].endDate.clone();
1212 } else {
1213 // First event in the column, add a placeholder for the
1214 // blank time from this.mStartMin to the event's start
1215 prevEnd = start.clone();
1216 prevEnd.hour = 0;
1217 prevEnd.minute = this.mStartMin;
1218 }
1219 prevEnd.timezone = floating();
1220 // the reason why we need to calculate time durations
1221 // based on floating timezones is that we need avoid
1222 // dst gaps in this case. converting the date/times to
1223 // floating conveys this idea in a natural way. note that
1224 // we explicitly don't use getInTimezone() as it would
1225 // be slightly more expensive in terms of performance.
1226 var floatstart = start.clone();
1227 floatstart.timezone = floating();
1228 var dur = floatstart.subtractDate(prevEnd);
1229 if (dur.inSeconds) {
1230 col.push({duration: dur});
1231 }
1232 var floatend = end.clone();
1233 floatend.timezone = floating();
1234 col.push({event: data.item,
1235 endDate: end,
1236 duration: floatend.subtractDate(floatstart)});
1237 }
1238 layerOffset = layers.length;
1239 }
1240 return layers;
1241 ]]></body>
1242 </method>
1243
1244 <!--
1245 - Event sweep handlers
1246 -->
1247 <method name="onEventSweepMouseMove">
1248 <parameter name="event"/>
1249 <body><![CDATA[
1250 var col = document.calendarEventColumnDragging;
1251 if (!col) return;
1252
1253 if (event.clientX < (event.target.boxObject.x) ||
1254 event.clientX > (event.target.boxObject.x + event.target.boxObject.width) ||
1255 event.clientY < (event.target.boxObject.y) ||
1256 event.clientY > (event.target.boxObject.y + event.target.boxObject.height)) {
1257
1258 var dragState = col.mDragState;
1259 col.fgboxes.dragbox.removeAttribute("dragging");
1260 col.fgboxes.box.removeAttribute("dragging");
1261 window.removeEventListener("mousemove", col.onEventSweepMouseMove, false);
1262 window.removeEventListener("mouseup", col.onEventSweepMouseUp, false);
1263 document.calendarEventColumnDragging = null;
1264 col.mDragState = null;
1265
1266 var dragService = Components.classes["@mozilla.org/widget/dragservice;1"].
1267 getService(Components.interfaces.nsIDragService);
1268 var transfer = Components.classes["@mozilla.org/widget/transferable;1"].
1269 createInstance(Components.interfaces.nsITransferable);
1270 transfer.addDataFlavor("text/calendar");
1271
1272 var item = dragState.dragOccurrence;
1273
1274 // the multiday view currently exhibits a less than optimal strategy
1275 // in terms of item selection. items don't get automatically selected
1276 // when clicked and dragged, as to differentiate inline editing from
1277 // the act of selecting an event. but the application internal drop
1278 // targets will ask for selected items in order to pull the data from
1279 // the packets. that's why we need to make sure at least the currently
1280 // dragged event is contained in the set of selected items.
1281 var selectedItems = currentView().getSelectedItems({});
1282 if (!selectedItems.some(
1283 function (aItem) {
1284 return (aItem.hashId == item.hashId);
1285 })) {
1286 col.calendarView.setSelectedItems(1,
1287 [event.ctrlKey ? item.parentItem : item]);
1288 }
1289
1290 var flavourProvider = {
1291 QueryInterface: function(aIID) {
1292 ensureIID(
1293 [ Components.interfaces.nsIFlavorDataProvider,
1294 Components.interfaces.nsISupports], aIID);
1295 return this;
1296 },
1297 item: item,
1298
1299 getFlavorData: function(aInTransferable, aInFlavor, aOutData, aOutDataLen) {
1300 if ((aInFlavor == "application/vnd.x-moz-cal-event") ||
1301 (aInFlavor == "application/vnd.x-moz-cal-task")) {
1302 aOutData.value = this.item;
1303 aOutDataLen.value = 1;
1304 } else {
1305 ASSERT(false, "error:"+aInFlavor);
1306 }
1307 }
1308 };
1309
1310 if (isEvent(item)) {
1311 transfer.addDataFlavor("application/vnd.x-moz-cal-event");
1312 transfer.setTransferData("application/vnd.x-moz-cal-event", flavourProvider, 0);
1313 } else if (isToDo(item)) {
1314 transfer.addDataFlavor("application/vnd.x-moz-cal-task");
1315 transfer.setTransferData("application/vnd.x-moz-cal-task", flavourProvider, 0);
1316 }
1317
1318 var serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"].
1319 createInstance(Components.interfaces.calIIcsSerializer);
1320 serializer.addItems([item], 1);
1321
1322 var supportsString = Components.classes["@mozilla.org/supports-string;1"].
1323 createInstance(Components.interfaces.nsISupportsString);
1324 supportsString.data = serializer.serializeToString();
1325 transfer.setTransferData("text/calendar", supportsString, supportsString.data.length*2);
1326 transfer.setTransferData("text/unicode", supportsString, supportsString.data.length*2);
1327
1328 var action = dragService.DRAGDROP_ACTION_MOVE;
1329 var supArray = Components.classes["@mozilla.org/supports-array;1"].
1330 createInstance(Components.interfaces.nsISupportsArray);
1331 supArray.AppendElement(transfer);
1332
1333 dragService.invokeDragSession(col, supArray, null, action);
1334
1335 return;
1336 }
1337
1338 var dragState = col.mDragState;
1339
1340 col.fgboxes.box.setAttribute("dragging", "true");
1341 col.fgboxes.dragbox.setAttribute("dragging", "true");
1342
1343 // check if we need to jump a column
1344 if (dragState.dragType == "move") {
1345 newcol = col.calendarView.findColumnForClientPoint(event.screenX, event.screenY);
1346 if (newcol && newcol != col) {
1347 // kill our drag state
1348 col.fgboxes.dragbox.removeAttribute("dragging");
1349 col.fgboxes.box.removeAttribute("dragging");
1350
1351 // jump ship
1352 newcol.acceptInProgressSweep(dragState);
1353
1354 // restart event handling
1355 col.onEventSweepMouseMove(event);
1356
1357 return;
1358 }
1359 }
1360
1361 var pos;
1362 var sizeattr;
1363 if (col.getAttribute("orient") == "vertical") {
1364 pos = event.screenY - col.parentNode.boxObject.screenY - dragState.mouseOffset;
1365 sizeattr = "height";
1366 } else {
1367 pos = event.screenX - col.parentNode.boxObject.screenX - dragState.mouseOffset;
1368 sizeattr = "width";
1369 }
1370 // don't let pos go outside the window edges
1371 if (pos < 0)
1372 pos = 0;
1373
1374 // snap to 15 minute intervals
1375 var interval = col.mPixPerMin * 15;
1376 var curmin = Math.floor(pos/interval) * 15;
1377 var deltamin = curmin - dragState.origMin;
1378
1379 if (dragState.dragType == "new") {
1380 if (deltamin < 0) {
1381 dragState.startMin = dragState.origMin + deltamin;
1382 dragState.endMin = dragState.origMin;
1383 } else {
1384 dragState.startMin = dragState.origMin;
1385 dragState.endMin = dragState.origMin + deltamin;
1386 }
1387 } else if (dragState.dragType == "move") {
1388 // if we're moving, we can only move the start, and the end has to be exactly start+duration
1389 dragState.startMin = dragState.origMin + deltamin;
1390 dragState.endMin = dragState.startMin + dragState.limitDurationMin;
1391 } else if (dragState.dragType == "modify-start") {
1392 // if we're modifying the start, the end time is fixed.
1393 dragState.startMin = dragState.origMin + deltamin;
1394 dragState.endMin = dragState.limitEndMin;
1395
1396 // but we need to not go past the end; if we hit
1397 // the end, then we'll clamp to the previous 15-min interval
1398 if (dragState.endMin <= dragState.startMin)
1399 dragState.startMin = Math.floor((dragState.endMin - 15) / 15) * 15;
1400 } else if (dragState.dragType == "modify-end") {
1401 // if we're modifying the end, the start time is fixed, and we'll always
1402 // set the spacer to a constant size.
1403 dragState.startMin = dragState.limitStartMin;
1404 dragState.endMin = dragState.origMin + deltamin;
1405
1406 // but we need to not go past the start; if we hit
1407 // the start, then we'll clamp to the next 15-min interval
1408 if (dragState.endMin <= dragState.startMin)
1409 dragState.endMin = Math.floor((dragState.startMin + 15) / 15) * 15;
1410 }
1411
1412 // update the box sizes
1413 col.fgboxes.dragspacer.setAttribute(sizeattr, dragState.startMin * col.mPixPerMin);
1414 col.fgboxes.dragbox.setAttribute(sizeattr, Math.abs((dragState.endMin - dragState.startMin) * col.mPixPerMin));
1415
1416 // update the label
1417 col.updateDragLabels();
1418 ]]></body>
1419 </method>
1420
1421 <method name="onEventSweepMouseUp">
1422 <parameter name="event"/>
1423 <body><![CDATA[
1424 var col = document.calendarEventColumnDragging;
1425 if (!col) return;
1426
1427 var dragState = col.mDragState;
1428
1429 col.fgboxes.dragbox.removeAttribute("dragging");
1430 col.fgboxes.box.removeAttribute("dragging");
1431
1432 window.removeEventListener("mousemove", col.onEventSweepMouseMove, false);
1433 window.removeEventListener("mouseup", col.onEventSweepMouseUp, false);
1434
1435 document.calendarEventColumnDragging = null;
1436
1437 // if the user didn't sweep out at least a few pixels, ignore
1438 // unless we're in a different column
1439 if (dragState.origColumn == col) {
1440 var ignore = false;
1441 if (col.getAttribute("orient") == "vertical") {
1442 if (Math.abs(event.screenY - dragState.origLoc) < 3)
1443 ignore = true;
1444 } else {
1445 if (Math.abs(event.screenX - dragState.origLoc) < 3)
1446 ignore = true;
1447 }
1448
1449 if (ignore) {
1450 document.calendarEventColumnDragging = null;
1451 col.mDragState = null;
1452 return;
1453 }
1454 }
1455
1456 var newStart;
1457 var newEnd;
1458 var startTZ;
1459 var endTZ;
1460
1461 if (dragState.dragType == "new") {
1462 newStart = col.mDate.clone();
1463 newStart.isDate = false;
1464 newEnd = col.mDate.clone();
1465 newEnd.isDate = false;
1466 } else {
1467 var oldStart = dragState.dragOccurrence.startDate || dragState.dragOccurrence.entryDate;
1468 var oldEnd = dragState.dragOccurrence.endDate || dragState.dragOccurrence.dueDate;
1469 newStart = oldStart.clone();
1470 newEnd = oldEnd.clone();
1471
1472 // Our views are pegged to the default timezone. If the event
1473 // isn't also in the timezone, we're going to need to do some
1474 // tweaking. We could just do this for every eventm but
1475 // getInTimezone is slow, so it's much better to only do this
1476 // when the timezones actually differ from the view's.
1477 if (this.mTimezone != newStart.timezone ||
1478 this.mTimezone != newEnd.timezone) {
1479 startTZ = newStart.timezone;
1480 endTZ = newEnd.timezone;
1481 newStart = newStart.getInTimezone(col.calendarView.mTimezone);
1482 newEnd = newEnd.getInTimezone(col.calendarView.mTimezone);
1483 }
1484 }
1485
1486 var dragDay = col.mDate;
1487
1488 if (dragState.dragType == "modify-start" ||
1489 dragState.dragType == "new") {
1490 newStart.resetTo(dragDay.year, dragDay.month, dragDay.day,
1491 0, dragState.startMin + col.mStartMin, 0,
1492 newStart.timezone);
1493 }
1494
1495 if (dragState.dragType == "modify-end" ||
1496 dragState.dragType == "new") {
1497 newEnd.resetTo(dragDay.year, dragDay.month, dragDay.day,
1498 0, dragState.endMin + col.mStartMin, 0,
1499 newEnd.timezone);
1500 }
1501
1502 if (dragState.dragType == "move") {
1503 // Figure out how much the event moved.
1504 var duration = col.mDate.subtractDate(dragState.origDate);
1505 var minutes = dragState.startMin - dragState.origMin;
1506
1507 // Since both boxDate and beginMove are dates (note datetimes),
1508 // subtractDate will only give us a non-zero number of hours on
1509 // DST changes. While strictly speaking, subtractDate's behavior
1510 // is correct, we need to move the event a discrete number of
1511 // days here. There is no need for normalization here, since
1512 // addDuration does the job for us. Also note, the duration used
1513 // here is only used to move over multiple days. Moving on the
1514 // same day uses the minutes from the dragState.
1515 if (duration.hours == 23) {
1516 // entering DST
1517 duration.hours++;
1518 } else if (duration.hours == 1) {
1519 // leaving DST
1520 duration.hours--;
1521 }
1522
1523 if (duration.isNegative) {
1524 // Adding negative minutes to a negative duration makes the
1525 // duration more positive, but we want more negative, and
1526 // vice versa.
1527 minutes *= -1;
1528 }
1529 duration.minutes = minutes;
1530 duration.normalize();
1531
1532 newStart.addDuration(duration);
1533 newEnd.addDuration(duration);
1534 }
1535
1536 // If we tweaked tzs, put times back in their original ones
1537 if (startTZ) {
1538 newStart = newStart.getInTimezone(startTZ);
1539 }
1540 if (endTZ) {
1541 newEnd = newEnd.getInTimezone(endTZ);
1542 }
1543
1544 if (dragState.dragType == "new") {
1545 col.mCreatedNewEvent = true;
1546 col.calendarView.controller.createNewEvent(col.calendarView.displayCalendar,
1547 newStart,
1548 newEnd);
1549 } else if (dragState.dragType == "move" ||
1550 dragState.dragType == "modify-start" ||
1551 dragState.dragType == "modify-end")
1552 {
1553 col.calendarView.controller.modifyOccurrence(dragState.dragOccurrence,
1554 newStart, newEnd);
1555 }
1556 document.calendarEventColumnDragging = null;
1557 col.mDragState = null;
1558 ]]></body>
1559 </method>
1560
1561 <!-- This is called by an event box when a grippy on either side is dragged,
1562 - or when the middle is pressed to drag the event to move it. We create
1563 - the same type of view that we use to sweep out a new event, but we
1564 - initialize it based on the event's values and what type of dragging
1565 - we're doing. In addition, we constrain things like not being able to
1566 - drag the end before the start and vice versa.
1567 -->
1568 <method name="startSweepingToModifyEvent">
1569 <parameter name="aEventBox"/>
1570 <parameter name="aOccurrence"/>
1571 <!-- "start", "end", "middle" -->
1572 <parameter name="aGrabbedElement"/>
1573 <!-- mouse screenX/screenY from the event -->
1574 <parameter name="aMouseX"/>
1575 <parameter name="aMouseY"/>
1576 <body><![CDATA[
1577 if (!isCalendarWritable(aOccurrence.calendar) ||
1578 aOccurrence.calendar.getProperty("capabilities.events.supported") === false) {
1579 return;
1580 }
1581
1582 //dump ("startSweepingToModify\n");
1583 this.mDragState = {
1584 origColumn: this,
1585 dragOccurrence: aOccurrence,
1586 mouseOffset: 0
1587 };
1588
1589 var interval = this.mPixPerMin * 15;
1590 var sizeattr;
1591
1592 //dump ("AMY: " + aMouseY + " boY: " + this.parentNode.boxObject.screenY + "\n");
1593 var frameloc;
1594 if (this.getAttribute("orient") == "vertical") {
1595 this.mDragState.origLoc = aMouseY;
1596 frameloc = aMouseY - this.parentNode.boxObject.screenY;
1597 sizeattr = "height";
1598 } else {
1599 this.mDragState.origLoc = aMouseX;
1600 frameloc = aMouseX - this.parentNode.boxObject.screenX;
1601 sizeattr = "width";
1602 }
1603
1604 var mins = this.getStartEndMinutesForOccurrence(aOccurrence);
1605
1606 // these are only used to compute durations or to compute UI
1607 // sizes, so offset by this.mStartMin for sanity here (at the
1608 // expense of possible insanity later)
1609 mins.start -= this.mStartMin;
1610 mins.end -= this.mStartMin;
1611
1612 if (aGrabbedElement == "start") {
1613 this.mDragState.dragType = "modify-start";
1614 this.mDragState.limitEndMin = mins.end;
1615
1616 // snap start
1617 this.mDragState.origMin = Math.floor(mins.start/15) * 15;
1618 this.fgboxes.dragspacer.setAttribute(sizeattr, this.mDragState.origMin * this.mPixPerMin);
1619 this.fgboxes.dragbox.setAttribute(sizeattr, (mins.end - this.mDragState.origMin) * this.mPixPerMin);
1620 } else if (aGrabbedElement == "end") {
1621 this.mDragState.dragType = "modify-end";
1622 this.mDragState.limitStartMin = mins.start;
1623
1624 // snap end
1625 this.mDragState.origMin = Math.floor(mins.end/15) * 15;
1626 this.fgboxes.dragspacer.setAttribute(sizeattr, mins.start * this.mPixPerMin);
1627 this.fgboxes.dragbox.setAttribute(sizeattr, (this.mDragState.origMin - mins.start) * this.mPixPerMin);
1628 } else if (aGrabbedElement == "middle") {
1629 this.mDragState.dragType = "move";
1630 this.mDragState.limitDurationMin = mins.end - mins.start;
1631
1632 // in a move, origMin will be the min of the start element;
1633 // so we snap start again, but we keep the duration the same
1634 // (we move the end based on the duration of the event,
1635 // not including our snap)
1636 this.mDragState.origMin = Math.floor(mins.start/15) * 15;
1637
1638 // Because we can pass this event to other columns, we also need
1639 // to track the original column's date too, to get the correct offset
1640 this.mDragState.origDate = this.mDate;
1641 this.fgboxes.dragspacer.setAttribute(sizeattr, this.mDragState.origMin * this.mPixPerMin);
1642 this.fgboxes.dragbox.setAttribute(sizeattr, (mins.end - mins.start) * this.mPixPerMin);
1643
1644 // we need to set a mouse offset, since we're not dragging from
1645 // one end of the element
1646 if (aEventBox) {
1647 if (this.getAttribute("orient") == "vertical")
1648 this.mDragState.mouseOffset = aMouseY - aEventBox.boxObject.screenY;
1649 else
1650 this.mDragState.mouseOffset = aMouseX - aEventBox.boxObject.screenX;
1651 }
1652 } else {
1653 dump ("+++ Invalid grabbed element: '" + aGrabbedElement + "'\n");
1654 }
1655
1656 this.fgboxes.box.setAttribute("dragging", "true");
1657 this.fgboxes.dragbox.setAttribute("dragging", "true");
1658
1659 document.calendarEventColumnDragging = this;
1660
1661 //dump (">>> drag is: " + this.mDragState.dragType + "\n");
1662
1663 window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
1664 window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
1665 ]]></body>
1666 </method>
1667
1668 <!-- called by sibling columns to tell us to take over the sweeping
1669 - of an event. Used by "move".
1670 -->
1671 <method name="acceptInProgressSweep">
1672 <parameter name="aDragState"/>
1673 <body><![CDATA[
1674 this.mDragState = aDragState;
1675 document.calendarEventColumnDragging = this;
1676
1677 this.fgboxes.box.setAttribute("dragging", "true");
1678 this.fgboxes.dragbox.setAttribute("dragging", "true");
1679
1680 // the same event handlers are still valid,
1681 // because they use document.calendarEventColumnDragging.
1682 // So we really don't have anything to do here.
1683 ]]></body>
1684 </method>
1685
1686 <method name="updateDragLabels">
1687 <body><![CDATA[
1688 if (!this.mDragState) return;
1689
1690 var realstartmin = this.mDragState.startMin + this.mStartMin;
1691 var realendmin = this.mDragState.endMin + this.mStartMin;
1692
1693
1694 if (this.mDragState.dragType == "move") {
1695 realendmin = realstartmin + this.mDragState.limitDurationMin;
1696 } else if (this.mDragState.dragType == "start") {
1697 realendmin = this.mDragState.limitEndMin;
1698 } else if (this.mDragState.dragType == "end") {
1699 realstartmin = this.mDragSTate.limitStartMin;
1700 }
1701
1702 var starthr = Math.floor(realstartmin / 60);
1703 var startmin = realstartmin % 60;
1704
1705 var endhr = Math.floor(realendmin / 60);
1706 var endmin = realendmin % 60;
1707
1708 var formatter = Components.classes["@mozilla.org/intl/scriptabledateformat;1"].
1709 getService(Components.interfaces.nsIScriptableDateFormat);
1710 var startstr = formatter.FormatTime("",
1711 Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
1712 starthr, startmin, 0);
1713 var endstr = formatter.FormatTime("",
1714 Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
1715 endhr, endmin, 0);
1716
1717 this.fgboxes.startlabel.setAttribute("value", startstr);
1718 this.fgboxes.endlabel.setAttribute("value", endstr);
1719
1720 ]]></body>
1721 </method>
1722 <method name="setDayStartEndMinutes">
1723 <parameter name="aDayStartMin"/>
1724 <parameter name="aDayEndMin"/>
1725 <body><![CDATA[
1726 if (aDayStartMin < this.mStartMin || aDayStartMin > aDayEndMin ||
1727 aDayEndMin > this.mEndMin) {
1728 throw Components.results.NS_ERROR_INVALID_ARG;
1729 }
1730 if (this.mDayStartMin != aDayStartMin ||
1731 this.mDayEndMin != aDayEndMin) {
1732 this.mDayStartMin = aDayStartMin;
1733 this.mDayEndMin = aDayEndMin;
1734 }
1735 ]]></body>
1736 </method>
1737 </implementation>
1738
1739 <handlers>
1740 <handler event="dblclick" button="0"><![CDATA[
1741 if (this.calendarView.controller) {
1742 var newStart = this.date.clone();
1743 newStart.isDate = false;
1744 newStart.hour = 0;
1745
1746 const ROUND_INTERVAL = 15;
1747
1748 var interval = this.mPixPerMin * ROUND_INTERVAL;
1749 var pos;
1750 if (this.getAttribute("orient") == "vertical") {
1751 pos = event.screenY - this.parentNode.boxObject.screenY;
1752 } else {
1753 pos = event.screenX - this.parentNode.boxObject.screenX;
1754 }
1755 newStart.minute = (Math.round(pos/interval) * ROUND_INTERVAL) + this.mStartMin;
1756 this.calendarView.controller.createNewEvent(null, newStart, null);
1757 }
1758 ]]></handler>
1759
1760 <handler event="click" button="0"><![CDATA[
1761 this.calendarView.setSelectedItems(0, []);
1762 ]]></handler>
1763
1764 <!-- mouse down handler, in empty event column regions. Starts sweeping out a new
1765 - event.
1766 -->
1767 <handler event="mousedown"><![CDATA[
1768 // select this column
1769 this.calendarView.selectedDay = this.mDate;
1770
1771 // If the selected calendar is readOnly, we don't want any sweeping.
1772 var cal = getSelectedCalendar();
1773 if (!isCalendarWritable(cal) ||
1774 cal.getProperty("capabilities.events.supported") === false) {
1775 return;
1776 }
1777
1778 // Only start sweeping out an event if the left button was clicked
1779 if (event.button != 0) {
1780 return;
1781 }
1782
1783 // snap to 15 minute intervals
1784 var interval = this.mPixPerMin * 15;
1785
1786 this.mDragState = {
1787 origColumn: this,
1788 dragType: "new",
1789 mouseOffset: 0
1790 };
1791
1792 if (this.getAttribute("orient") == "vertical") {
1793 this.mDragState.origLoc = event.screenY;
1794 this.mDragState.origMin = Math.floor((event.screenY - this.parentNode.boxObject.screenY)/interval) * 15;
1795 this.fgboxes.dragspacer.setAttribute("height", this.mDragState.origMin * this.mPixPerMin);
1796 } else {
1797 this.mDragState.origLoc = event.screenX;
1798 this.mDragState.origMin = Math.floor((event.screenX - this.parentNode.boxObject.screenX)/interval) * 15;
1799 this.fgboxes.dragspacer.setAttribute("width", this.mDragState.origMin * this.mPixPerMin);
1800 }
1801
1802 document.calendarEventColumnDragging = this;
1803
1804 window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
1805 window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
1806 ]]></handler>
1807
1808
1809 </handlers>
1810 </binding>
1811
1812 <binding id="calendar-header-container">
1813 <content>
1814 <xul:vbox xbl:inherits="selected" anonid="thebox" flex="1" class="calendar-event-column-header"/>
1815 </content>
1816
1817 <implementation>
1818 <field name="mItemBoxes">null</field>
1819 <field name="mDate">null</field>
1820 <field name="mCalendarView">null</field>
1821
1822 <constructor><![CDATA[
1823 this.mItemBoxes = new Array();
1824 ]]></constructor>
1825
1826 <property name="mainbox">
1827 <getter><![CDATA[
1828 return document.getAnonymousElementByAttribute(this, 'anonid', 'thebox');
1829 ]]></getter>
1830 </property>
1831
1832 <property name="date">
1833 <getter><![CDATA[
1834 return this.mDate;
1835 ]]></getter>
1836 <setter><![CDATA[
1837 this.mDate = val;
1838 return val;
1839 ]]></setter>
1840 </property>
1841
1842 <property name="calendarView">
1843 <getter><![CDATA[
1844 return this.mCalendarView;
1845 ]]></getter>
1846 <setter><![CDATA[
1847 this.mCalendarView = val;
1848 return val;
1849 ]]></setter>
1850 </property>
1851
1852 <method name="findBoxForItem">
1853 <parameter name="aItem"/>
1854 <body><![CDATA[
1855 for each (var item in this.mItemBoxes) {
1856 if (aItem && item.occurrence.hasSameIds(aItem)) {
1857 // We can return directly, since there will only be one box per
1858 // item in the header.
1859 return item;
1860 }
1861 }
1862 return null;
1863 ]]></body>
1864 </method>
1865
1866 <method name="addEvent">
1867 <parameter name="aItem"/>
1868 <body><![CDATA[
1869 // prevent same items being added
1870 if (this.mItemBoxes.some(function (itemBox) {
1871 return itemBox.occurrence.hashId == aItem.hashId;
1872 })) {
1873 return;
1874 }
1875
1876 function createXULElement(el) {
1877 return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
1878 }
1879 var itemBox = createXULElement("calendar-editable-item");
1880
1881 this.mainbox.appendChild(itemBox);
1882 itemBox.calendarView = this.calendarView;
1883 itemBox.occurrence = aItem;
1884 var ctxt = this.calendarView.getAttribute("item-context") ||
1885 this.calendarView.getAttribute("context");
1886 itemBox.setAttribute("context", ctxt);
1887
1888 if (aItem.hashId in this.calendarView.mFlashingEvents) {
1889 itemBox.setAttribute("flashing", "true");
1890 }
1891
1892 this.mItemBoxes.push(itemBox);
1893 ]]></body>
1894 </method>
1895
1896 <method name="deleteEvent">
1897 <parameter name="aItem"/>
1898 <body><![CDATA[
1899 for (var i in this.mItemBoxes) {
1900 if (this.mItemBoxes[i].occurrence.hashId == aItem.hashId) {
1901 this.mainbox.removeChild(this.mItemBoxes[i]);
1902 this.mItemBoxes.splice(i, 1);
1903 break;
1904 }
1905 }
1906 ]]></body>
1907 </method>
1908
1909 <method name="selectOccurrence">
1910 <parameter name="aItem"/>
1911 <body><![CDATA[
1912 for each (itemBox in this.mItemBoxes) {
1913 if (aItem && (itemBox.occurrence.hashId == aItem.hashId)) {
1914 itemBox.selected = true;
1915 }
1916
1917 }
1918 ]]></body>
1919 </method>
1920 <method name="unselectOccurrence">
1921 <parameter name="aItem"/>
1922 <body><![CDATA[
1923 for each (itemBox in this.mItemBoxes) {
1924 if (aItem && (itemBox.occurrence.hashId == aItem.hashId)) {
1925 itemBox.selected = false;
1926 }
1927 }
1928 ]]></body>
1929 </method>
1930
1931 </implementation>
1932
1933 <handlers>
1934 <handler event="dblclick" button="0"><![CDATA[
1935 this.calendarView.controller.createNewEvent();
1936 ]]></handler>
1937 <handler event="mousedown"><![CDATA[
1938 this.calendarView.selectedDay = this.mDate;
1939 ]]></handler>
1940 <handler event="click" button="0"><![CDATA[
1941 this.calendarView.setSelectedItems(0, []);
1942 ]]></handler>
1943 </handlers>
1944 </binding>
1945
1946 <!--
1947 - An individual event box, to be inserted into a column.
1948 -->
1949 <binding id="calendar-event-box" extends="chrome://calendar/content/calendar-view-core.xml#calendar-editable-item">
1950
1951 <content>
1952 <xul:box anonid="shadowbox"
1953 xbl:inherits="width,height">
1954 <xul:box anonid="shadow-left-box"
1955 xbl:inherits="orient" >
1956 <xul:image anonid="shadow-left"
1957 class="calendar-event-box-shadow-left"
1958 flex="1"/>
1959 <xul:image anonid="shadow-edge-left"
1960 class="calendar-event-box-shadow-edge-left"/>
1961 </xul:box>
1962 <xul:box xbl:inherits="orient,width,height" flex="1">
1963 <xul:image anonid="shadow-left-if-vertical"
1964 class="calendar-event-box-shadow-left"
1965 hidden="true"/>
1966 <xul:box anonid="event-container" xbl:inherits="orient" flex="1">
1967 <xul:box class="calendar-event-selection" orient="horizontal" flex="1">
1968 <xul:stack anonid="eventbox"
1969 align="stretch"
1970 class="calendar-event-box-container"
1971 flex="1"
1972 xbl:inherits="context,parentorient=orient">
1973 <xul:vbox class="calendar-event-details">
1974 <xul:description anonid="event-name" class="calendar-event-details-core" flex="1"/>
1975 <xul:textbox anonid="event-name-textbox"
1976 class="plain calendar-event-details-core"
1977 flex="1"
1978 hidden="true"
1979 style="background: transparent !important;"
1980 wrap="true"/>
1981 </xul:vbox>
1982 <xul:image width="1px" class="calendar-event-box-gradient"/>
1983 <xul:hbox pack="end">
1984 <xul:vbox pack="start">
1985 <xul:image anonid="alarm-image"
1986 class="alarm-image"
1987 xbl:inherits="flashing"
1988 hidden="true"/>
1989 </xul:vbox>
1990 </xul:hbox>
1991 <xul:box xbl:inherits="orient" flex="1">
1992 <xul:calendar-event-gripbar anonid="gripbar1"
1993 class="calendar-event-box-grippy-top"
1994 whichside="start"
1995 xbl:inherits="parentorient=orient"/>
1996 <xul:spacer flex="1"/>
1997 <xul:calendar-event-gripbar anonid="gripbar2"
1998 class="calendar-event-box-grippy-bottom"
1999 whichside="end"
2000 xbl:inherits="parentorient=orient"/>
2001 </xul:box>
2002 <!-- Do not insert anything here, otherwise the event boxes will
2003 not be resizable using the gripbars. If you want to insert
2004 additional elements, do so above the box with the gripbars. -->
2005 </xul:stack>
2006 <xul:calendar-category-box anonid="category-box"/>
2007 </xul:box>
2008 </xul:box>
2009 <xul:image anonid="shadow-bottom"
2010 class="calendar-event-box-shadow-bottom"/>
2011 </xul:box>
2012 <xul:box xbl:inherits="orient">
2013 <xul:image anonid="shadow-edge-left-if-vertical"
2014 class="calendar-event-box-shadow-edge-left"
2015 hidden="true"/>
2016 <xul:image anonid="shadow-right"
2017 class="calendar-event-box-shadow-right"
2018 flex="1"/>
2019 <xul:image anonid="shadow-edge-right"
2020 class="calendar-event-box-shadow-edge-right"/>
2021 </xul:box>
2022 </xul:box>
2023 </content>
2024
2025 <implementation>
2026 <constructor><![CDATA[
2027 this.orient = this.getAttribute("orient");
2028 var otherorient = "vertical";
2029 if (!orient) orient = "horizontal";
2030 if (orient == "vertical") otherorient = "horizontal";
2031
2032 var shadowbox = document.getAnonymousElementByAttribute(this, "anonid", "shadowbox");
2033 shadowbox.setAttribute("orient",otherorient);
2034
2035 if (orient != "vertical") {
2036 // Switch bottom/right shadows
2037 var shadowBottom = document.getAnonymousElementByAttribute(this, "anonid", "shadow-bottom");
2038 var shadowRight = document.getAnonymousElementByAttribute(this, "anonid", "shadow-right");
2039 shadowBottom.setAttribute("class", "calendar-event-box-shadow-right");
2040 shadowRight.setAttribute("class", "calendar-event-box-shadow-bottom");
2041
2042 // Move left shadow in layout
2043 document.getAnonymousElementByAttribute(this, "anonid", "shadow-edge-left-if-vertical").removeAttribute("hidden");
2044 document.getAnonymousElementByAttribute(this, "anonid", "shadow-left-if-vertical").removeAttribute("hidden");
2045 document.getAnonymousElementByAttribute(this, "anonid", "shadow-left-box").setAttribute("hidden","true");
2046 }
2047 ]]></constructor>
2048
2049 <!-- fields -->
2050 <field name="mParentColumn">null</field>
2051
2052 <!-- methods/properties -->
2053 <method name="setAttribute">
2054 <parameter name="aAttr"/>
2055 <parameter name="aVal"/>
2056 <body><![CDATA[
2057 var needsrelayout = false;
2058 if (aAttr == "orient") {
2059 if (this.getAttribute("orient") != aVal)
2060 needsrelayout = true;
2061 }
2062
2063 // this should be done using lookupMethod(), see bug 286629
2064 var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
2065
2066 if (needsrelayout) {
2067 var otherorient = "vertical";
2068 if (val != "horizontal") otherorient = "horizontal";
2069
2070 var eventbox = document.getAnonymousElementByAttribute(this, "anonid", "eventbox");
2071 eventbox.setAttribute("orient", val);
2072 eventbox.setAttribute("class", "calendar-item calendar-event-box-" + val);
2073 var gb1 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar1");
2074 gb1.parentorient = val;
2075 var gb2 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar2");
2076 gb2.parentorient = val;
2077 }
2078
2079 return ret;
2080 ]]></body>
2081 </method>
2082
2083 <property name="parentColumn"
2084 onget="return this.mParentColumn;"
2085 onset="return (this.mParentColumn = val);"/>
2086
2087 <property name="startMinute">
2088 <getter><![CDATA[
2089 if (!this.mOccurrence)
2090 return 0;
2091 var startDate = this.mOccurrence.startDate || this.mOccurrence.entryDate;
2092 return startDate.hour * 60 + startDate.minute;
2093 ]]></getter>
2094 </property>
2095
2096 <property name="endMinute">
2097 <getter><![CDATA[
2098 if (!this.mOccurrence)
2099 return 0;
2100 var endDate = this.mOccurrence.endDate || this.mOccurrence.dueDate;
2101 return endDate.hour * 60 + endDate.minute;
2102 ]]></getter>
2103 </property>
2104
2105 <method name="setEditableLabel">
2106 <body><![CDATA[
2107 var evl = this.eventNameLabel;
2108 var item = this.mOccurrence;
2109
2110 if (item.title && item.title != "") {
2111 // Use <description> textContent so it can wrap.
2112 evl.textContent = item.title;
2113 } else {
2114 evl.textContent = calGetString("calendar", "eventUntitled");
2115 }
2116
2117 var gripbar = document.getAnonymousElementByAttribute(this, "anonid", "gripbar1").boxObject.height;
2118 var height = document.getAnonymousElementByAttribute(this, "anonid", "eventbox").boxObject.height;
2119 evl.setAttribute("style", "max-height: " + (height-gripbar*2) + "px");
2120 ]]></body>
2121 </method>
2122 </implementation>
2123
2124 <handlers>
2125 <handler event="mousedown" button="0"><![CDATA[
2126 event.stopPropagation();
2127
2128 if (this.mEditing)
2129 return;
2130
2131 this.parentColumn.calendarView.selectedDay = this.parentColumn.mDate;
2132 this.mInMouseDown = true;
2133 this.mMouseX = event.screenX;
2134 this.mMouseY = event.screenY;
2135
2136 var whichside = event.whichside;
2137 if (!whichside)
2138 return;
2139
2140 this.calendarView.setSelectedItems(1,
2141 [event.ctrlKey ? this.mOccurrence.parentItem : this.mOccurrence]);
2142
2143 // start dragging it
2144 this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, whichside, event.screenX, event.screenY);
2145 ]]></handler>
2146
2147 <handler event="mousemove"><![CDATA[
2148 if (!this.mInMouseDown)
2149 return;
2150 var dx = Math.abs(event.screenX - this.mMouseX);
2151 var dy = Math.abs(event.screenY - this.mMouseY);
2152 // more than a 3 pixel move?
2153 if ((dx*dx + dy*dy) > 9) {
2154 if (this.parentColumn) {
2155 if (this.editingTimer) {
2156 clearTimeout(this.editingTimer);
2157 this.editingTimer = null;
2158 }
2159
2160 this.calendarView.selectedItem = this.mOccurrence;
2161
2162 this.mEditing = false;
2163 if (this.calendarView)
2164 this.calendarView.activeInPlaceEdit = false;
2165
2166 this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, "middle", this.mMouseX, this.mMouseY);
2167 this.mInMouseDown = false;
2168 }
2169 }
2170 ]]></handler>
2171
2172 <handler event="mouseup"><![CDATA[
2173 if (this.mEditing)
2174 return;
2175
2176 this.mInMouseDown = false;
2177 ]]></handler>
2178
2179 <handler event="mouseover"><![CDATA[
2180 if (this.calendarView && this.calendarView.controller) {
2181 event.stopPropagation();
2182 onMouseOverItem(event);
2183 }
2184 ]]></handler>
2185 </handlers>
2186 </binding>
2187
2188 <binding id="calendar-day-label">
2189 <content>
2190 <xul:hbox anonid="mainbox" flex="1">
2191 <xul:spacer flex="1"/>
2192 <xul:vbox>
2193 <xul:label anonid="dateName" class="calendar-day-label-date"/>
2194 <xul:label anonid="longWeekdayName" class="calendar-day-label-name"/>
2195 <xul:label anonid="shortWeekdayName" class="calendar-day-label-name" hidden="true"/>
2196 </xul:vbox>
2197 <xul:spacer flex="1"/>
2198 </xul:hbox>
2199 </content>
2200 <implementation>
2201 <field name="longWeekdayPixels">0</field>
2202 <field name="mDate">null</field>
2203
2204 <property name="date">
2205 <getter><![CDATA[
2206 return this.mDate;
2207 ]]></getter>
2208 <setter><![CDATA[
2209 this.mDate = val;
2210 var df = Components.classes["@mozilla.org/calendar/datetime-formatter;1"].
2211 getService(Components.interfaces.calIDateTimeFormatter);
2212
2213 var shortName = document.getAnonymousElementByAttribute(this, "anonid", "shortWeekdayName");
2214 var longName = document.getAnonymousElementByAttribute(this, "anonid", "longWeekdayName");
2215 var day = document.getAnonymousElementByAttribute(this, "anonid", "dateName");
2216
2217 longName.setAttribute("value", calGetString("dateFormat","day." + (val.weekday + 1) + ".name"));
2218 shortName.setAttribute("value", calGetString("dateFormat","day." + (val.weekday + 1) + ".Mmm"));
2219 day.setAttribute("value", df.formatDateWithoutYear(val));
2220
2221 return val;
2222 ]]></setter>
2223 </property>
2224
2225 <property name="shortWeekNames">
2226 <getter><![CDATA[
2227 return document.getAnonymousElementByAttribute(this, "anonid", "shortWeekdayName").hasAttribute("hidden");
2228 ]]></getter>
2229 <setter><![CDATA[
2230 var shortName = document.getAnonymousElementByAttribute(this, "anonid", "shortWeekdayName");
2231 var longName = document.getAnonymousElementByAttribute(this, "anonid", "longWeekdayName");
2232
2233 // cache before change, in case we are switching to short
2234 this.cacheLongWeekdayPixels(longName);
2235
2236 if (val) {
2237 longName.setAttribute("hidden","true");
2238 shortName.removeAttribute("hidden");
2239 } else {
2240 longName.removeAttribute("hidden");
2241 shortName.setAttribute("hidden","true");
2242 }
2243
2244 // cache after change, in case we are switching from short
2245 this.cacheLongWeekdayPixels(longName);
2246
2247 return val;
2248 ]]></setter>
2249 </property>
2250
2251 <method name="cacheLongWeekdayPixels">
2252 <parameter name="longName"/>
2253 <body><![CDATA[
2254 // Only do this if the long weekdays are visible and we haven't already cached.
2255 if (!this.longWeekdayPixels && !longName.hasAttribute("hidden")) {
2256 this.longWeekdayPixels = longName.boxObject.width +
2257 parseInt(document.defaultView.getComputedStyle(longName, null).getPropertyValue("margin-left")) +
2258 parseInt(document.defaultView.getComputedStyle(longName, null).getPropertyValue("margin-right"));
2259 }
2260 ]]></body>
2261 </method>
2262 </implementation>
2263 </binding>
2264
2265 <binding id="calendar-multiday-view">
2266 <content>
2267 <xul:box anonid="mainbox" flex="1" onoverflow="adjustWeekdayLength();">
2268 <!-- these boxes are tricky: width or height in CSS depend on orient -->
2269 <xul:box anonid="labelbox">
2270 <xul:box anonid="labeltimespacer"/>
2271 <xul:box anonid="labeldaybox" class="calendar-label-day-box" flex="1"
2272 equalsize="always" />
2273 <xul:box anonid="labelscrollbarspacer"/>
2274 </xul:box>
2275 <xul:box anonid="headerbox">
2276 <xul:box anonid="headertimespacer"
2277 class="calendar-header-time-spacer"/>
2278 <xul:box anonid="headerdaybox" class="calendar-header-day-box"
2279 flex="1" equalsize="always" />
2280 <xul:box anonid="headerscrollbarspacer"/>
2281 </xul:box>
2282 <xul:scrollbox anonid="childbox" flex="1">
2283 <!-- the orient of the calendar-time-bar needs to be the opposite of the parent -->
2284 <xul:calendar-time-bar xbl:inherits="orient" anonid="timebar"/>
2285 <xul:box anonid="daybox" class="calendar-day-box" flex="1"
2286 equalsize="always"/>
2287 </xul:scrollbox>
2288 </xul:box>
2289 </content>
2290
2291 <implementation implements="calICalendarView">
2292
2293 <field name="mResizeHandler">null</field>
2294 <constructor><![CDATA[
2295 var self = this;
2296 this.mTimezone = UTC();
2297 this.mResizeHandler = function resizeHandler() { self.onResize(); };
2298 window.addEventListener("resize", this.mResizeHandler, true);
2299 this.mScrollHandler = function scrollHandler() { self.onScroll(); };
2300 window.addEventListener("scroll", this.mScrollHandler, true);
2301
2302 // set the flex attribute at the scrollbox-innerbox
2303 // (this can be removed, after Bug 343555 is fixed)
2304 var childbox = document.getAnonymousElementByAttribute(
2305 this, "anonid", "childbox");
2306 document.getAnonymousElementByAttribute(
2307 childbox, "class", "box-inherit scrollbox-innerbox").flex = "1";
2308
2309 this.reorient();
2310
2311 var alarmService = Components.classes['@mozilla.org/calendar/alarm-service;1']
2312 .getService(Components.interfaces.calIAlarmService);
2313 alarmService.addObserver(this.mObserver);
2314 ]]></constructor>
2315
2316 <destructor><![CDATA[
2317 if (this.mCalendar) {
2318 this.mCalendar.removeObserver(this.mObserver);
2319 }
2320 window.removeEventListener("scroll", this.mScrollHandler, true);
2321 window.removeEventListener("resize", this.mResizeHandler, true);
2322
2323 var alarmService = Components.classes['@mozilla.org/calendar/alarm-service;1']
2324 .getService(Components.interfaces.calIAlarmService);
2325 alarmService.removeObserver(this.mObserver);
2326 ]]></destructor>
2327
2328 <property name="daysInView" readonly="true">
2329 <getter><![CDATA[
2330 var labeldaybox = document.getAnonymousElementByAttribute(this, "anonid", "labeldaybox");
2331 return labeldaybox.childNodes && labeldaybox.childNodes.length;
2332 ]]></getter>
2333 </property>
2334
2335 <method name="onResize">
2336 <parameter name="aRealSelf"/>
2337 <body><![CDATA[
2338 var self = this;
2339 if (aRealSelf) {
2340 self = aRealSelf;
2341 }
2342 var childbox = document.getAnonymousElementByAttribute(self, "anonid", "childbox");
2343 var size;
2344 if (self.orient == "horizontal") {
2345 size = childbox.boxObject.width;
2346 } else {
2347 size = childbox.boxObject.height;
2348 }
2349 var ppm = size / self.mVisibleMinutes;
2350 ppm = Math.floor(ppm * 1000) / 1000;
2351 if (ppm < self.mMinPixelsPerMinute) {
2352 ppm = self.mMinPixelsPerMinute;
2353 }
2354 self.pixelsPerMinute = ppm;
2355 setTimeout(function(){self.scrollToMinute(self.mFirstVisibleMinute)}, 1);
2356
2357 // Fit the weekday labels while scrolling.
2358 self.adjustWeekdayLength();
2359 ]]></body>
2360 </method>
2361
2362 <field name="mScrollHandler">null</field>
2363 <method name="onScroll">
2364 <body><![CDATA[
2365 // workaround: deck changes shouldn't provoke onScroll events...
2366 var displayDeck = document.getElementById("displayDeck");
2367 if (displayDeck && displayDeck.selectedPanel.id != "calendar-view-box") {
2368 return;
2369 }
2370 var panel = this.parentNode.parentNode.parentNode;
2371 var deck = panel.parentNode;
2372 if (panel.id != deck.selectedPanel.id) {
2373 return;
2374 }
2375 if (deck.style.visibility == "collapse") {
2376 return;
2377 }
2378 this.mFirstVisibleMinute = this.getFirstVisibleMinute();
2379 ]]></body>
2380 </method>
2381
2382 <field name="mController">null</field>
2383 <field name="mCalendar">null</field>
2384 <field name="mStartDate">null</field>
2385 <field name="mEndDate">null</field>
2386 <!-- mDateList will always be sorted before being set -->
2387 <field name="mDateList">null</field>
2388 <!-- array of { date: calIDatetime, column: colbox, header: hdrbox } -->
2389 <field name="mDateColumns">null</field>
2390 <field name="mBatchCount">0</field>
2391 <field name="mPixPerMin">0.6</field>
2392 <field name="mMinPixelsPerMinute">0.1</field>
2393 <field name="mLongWeekdayTotalPixels">0</field>
2394 <field name="mSelectedItems">[]</field>
2395 <field name="mSelectedDayCol">null</field>
2396 <field name="mSelectedDay">null</field>
2397
2398 <field name="mStartMin">0*60</field>
2399 <field name="mEndMin">24*60</field>
2400 <field name="mDayStartMin">0</field>
2401 <field name="mDayEndMin">0</field>
2402 <field name="mVisibleMinutes">9*60</field>
2403 <field name="mTasksInView">false</field>
2404 <field name="mShowCompleted">false</field>
2405 <field name="mDisplayDaysOff">true</field>
2406 <field name="mDaysOffArray">[0,6]</field>
2407 <field name="mTimezone">null</field>
2408 <field name="mFlashingEvents">new Object()</field>
2409
2410 <field name="mObserver"><![CDATA[
2411 // the calIObserver, calICompositeObserver, and calIAlarmServiceObserver
2412 ({
2413 QueryInterface: function QueryInterface(aIID) {
2414 if (!aIID.equals(Components.interfaces.calIObserver) &&
2415 !aIID.equals(Components.interfaces.calIAlarmServiceObserver) &&
2416 !aIID.equals(Components.interfaces.calICompositeObserver) &&
2417 !aIID.equals(Components.interfaces.nsISupports)) {
2418 throw Components.results.NS_ERROR_NO_INTERFACE;
2419 }
2420
2421 return this;
2422 },
2423
2424 calView: this,
2425
2426 onStartBatch: function onStartBatch() {
2427 this.calView.mBatchCount++;
2428 },
2429 onEndBatch: function onEndBatch() {
2430 this.mBatchCount--;
2431 if (this.mBatchCount == 0) {
2432 this.calView.refresh();
2433 }
2434 },
2435 onLoad: function onLoad() {
2436 this.calView.refresh();
2437 },
2438 onAddItem: function onAddItem(aItem) {
2439 if (this.mBatchCount) {
2440 return;
2441 }
2442
2443 if (isToDo(aItem)) {
2444 if (!aItem.entryDate || !aItem.dueDate) {
2445 return;
2446 }
2447 if(!this.calView.mTasksInView){
2448 return;
2449 }
2450 if (aItem.isCompleted && !this.calView.mShowCompleted) {
2451 return;
2452 }
2453 }
2454
2455 var occs = aItem.getOccurrencesBetween(this.calView.startDate,
2456 this.calView.queryEndDate,
2457 {});
2458 //dump ("occs: " + occs.length + "\n");
2459 for each (var occ in occs) {
2460 this.calView.doAddEvent(occ);
2461 }
2462
2463 return;
2464 },
2465 onModifyItem: function onModifyItem(aNewItem, aOldItem) {
2466 if (this.mBatchCount) {
2467 return;
2468 }
2469
2470 if (isToDo(aNewItem) && isToDo(aOldItem) &&
2471 !this.calView.mTasksInView)
2472 return;
2473
2474 var occs;
2475
2476 if (!isToDo(aOldItem) ||
2477 (aOldItem.entryDate && aOldItem.dueDate)) {
2478 occs = aOldItem.getOccurrencesBetween(this.calView.startDate,
2479 this.calView.queryEndDate,
2480 {});
2481 for each (var occ in occs) {
2482 this.calView.doDeleteEvent(occ);
2483 }
2484 }
2485 if (isToDo(aNewItem)) {
2486 if (!aNewItem.entryDate || !aNewItem.dueDate || !this.calView.mTasksInView) {
2487 return;
2488 }
2489 if (aNewItem.isCompleted && !this.calView.mShowCompleted) {
2490 return;
2491 }
2492 }
2493
2494 occs = aNewItem.getOccurrencesBetween(this.calView.startDate,
2495 this.calView.queryEndDate,
2496 {});
2497 for each (var occ in occs) {
2498 this.calView.doAddEvent(occ);
2499 }
2500 },
2501 onDeleteItem: function onDeleteItem(aItem) {
2502 if (this.mBatchCount) {
2503 return;
2504 }
2505
2506 if (isToDo(aItem)) {
2507 if (!this.calView.mTasksInView) {
2508 return;
2509 }
2510 if (!aItem.entryDate || !aItem.dueDate) {
2511 return;
2512 }
2513 if (aItem.isCompleted && !this.calView.mShowCompleted) {
2514 return;
2515 }
2516 }
2517
2518 var occs = aItem.getOccurrencesBetween(this.calView.startDate,
2519 this.calView.queryEndDate,
2520 {});
2521 for each (var occ in occs) {
2522 this.calView.doDeleteEvent(occ);
2523 }
2524 },
2525 onError: function onError(aErrNo, aMessage) { },
2526
2527 onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
2528 switch (aName) {
2529 case "suppressAlarms":
2530 if (!getPrefSafe("calendar.alarms.indicator.show", true) ||
2531 aCalendar.getProperty("capabilities.alarms.popup.supported") === false ) {
2532 break;
2533 }
2534 // else fall through
2535 case "readOnly":
2536 // XXXvv we can be smarter about how we handle this stuff
2537 this.calView.refresh();
2538 break;
2539 }
2540 },
2541
2542 onPropertyDeleting: function(aCalendar, aName) {
2543 // Values are not important here yet.
2544 this.onPropertyChanged(aCalendar, aName, null, null);
2545 },
2546
2547 //
2548 // calIAlarmServiceObserver stuff
2549 //
2550 onAlarm: function onAlarm(aAlarmItem) {
2551 this.calView.flashAlarm(aAlarmItem, false);
2552 },
2553
2554 onRemoveAlarmsByItem: function onRemoveAlarmsByItem(aItem) {
2555 // Stop the flashing for the item.
2556 this.calView.flashAlarm(aItem, true);
2557 },
2558
2559 onRemoveAlarmsByCalendar: function onRemoveAlarmsByCalendar(aCalendar) {
2560 // Stop the flashing for all items of this calendar
2561 for each (var item in this.calView.mFlashingEvents) {
2562 if (item.calendar.id == aCalendar.id) {
2563 this.calView.flashAlarm(item, true);
2564 }
2565 }
2566 },
2567
2568 //
2569 // calICompositeObserver stuff
2570 // XXXvv we can be smarter about how we handle this stuff
2571 //
2572 onCalendarAdded: function onCalendarAdded(aCalendar) {
2573 //dump ("view onCalendarAdded\n");
2574 this.calView.refresh();
2575 },
2576
2577 onCalendarRemoved: function onCalendarRemoved(aCalendar) {
2578 //dump ("view onCalendarRemoved\n");
2579 this.calView.refresh();
2580 },
2581
2582 onDefaultCalendarChanged:
2583 function onDefaultCalendarChanged(aNewDefaultCalendar) {
2584 // don't care, for now
2585 }
2586 })
2587 ]]></field>
2588
2589 <field name="mOperationListener"><![CDATA[
2590 ({
2591 calView: this,
2592
2593 onOperationComplete:
2594 function onOperationComplete(aCalendar, aStatus, aOperationType,
2595 aId, aDetail) {
2596 // Fire event
2597 this.calView.fireEvent('viewloaded', aOperationType);
2598
2599 // signal that the current operation finished.
2600 this.calView.mRefreshPending = null;
2601
2602 // immediately start the next job on the queue.
2603 this.calView.popRefreshQueue();
2604 },
2605 onGetResult:
2606 function onGetResult(aCalendar, aStatus, aItemType, aDetail,
2607 aCount, aItems) {
2608 if (!Components.isSuccessCode(aStatus))
2609 return;
2610
2611 function hasGoodDates(item) {
2612 if (isToDo(item) && (!item.entryDate || !item.dueDate)) {
2613 return false;
2614 }
2615 return true;
2616 }
2617 aItems = aItems.filter(hasGoodDates);
2618
2619 for each (var item in aItems) {
2620 this.calView.doAddEvent(item);
2621 }
2622 }
2623 })
2624 ]]></field>
2625
2626 <method name="flashAlarm">
2627 <parameter name="aAlarmItem"/>
2628 <parameter name="aStop"/>
2629 <body><![CDATA[
2630 var showIndicator = getPrefSafe("calendar.alarms.indicator.show", true);
2631 var totaltime = getPrefSafe("calendar.alarms.indicator.totaltime", 3600);
2632
2633 if (!aStop && (!showIndicator || totaltime < 1)) {
2634 // No need to animate if the indicator should not be shown.
2635 return;
2636 }
2637
2638 // Helper function to save some duplicate code
2639 function setFlashingAttribute(aBox) {
2640 if (aStop) {
2641 aBox.removeAttribute("flashing");
2642 } else {
2643 aBox.setAttribute("flashing", "true");
2644 }
2645 }
2646
2647 // Make sure the flashing attribute is set or reset on all visible
2648 // boxes.
2649 var columns = this.findColumnsForItem(aAlarmItem);
2650 for each (var col in columns) {
2651 var box = col.column.findChunkForOccurrence(aAlarmItem);
2652 if (box && box.eventbox) {
2653 setFlashingAttribute(box.eventbox);
2654 }
2655 box = col.header.findBoxForItem(aAlarmItem);
2656 if (box) {
2657 setFlashingAttribute(box);
2658 }
2659 }
2660
2661 if (!aStop) {
2662 // Set up a timer to stop the flashing after the total time.
2663 var this_ = this;
2664 this.mFlashingEvents[aAlarmItem.hashId] = aAlarmItem;
2665 setTimeout(function() { this_.flashAlarm(aAlarmItem, true) }, totaltime);
2666 } else {
2667 // We are done flashing, prevent newly created event boxes from flashing.
2668 delete this.mFlashingEvents[aAlarmItem.hashId];
2669 }
2670 ]]></body>
2671 </method>
2672
2673 <!-- calICalendarView -->
2674 <property name="supportsDisjointDates"
2675 onget="return true"/>
2676 <property name="hasDisjointDates"
2677 onget="return (this.mDateList != null);"/>
2678
2679 <property name="controller"
2680 onget="return this.mController;"
2681 onset="return (this.mController = val);" />
2682
2683 <property name="displayCalendar">
2684 <getter><![CDATA[
2685 return this.mCalendar;
2686 ]]></getter>
2687 <setter><![CDATA[
2688 if (this.mCalendar)
2689 this.mCalendar.removeObserver (this.mObserver);
2690 this.mCalendar = val;
2691 this.mCalendar.addObserver (this.mObserver);
2692
2693 this.refresh();
2694 return val;
2695 ]]></setter>
2696 </property>
2697
2698 <property name="startDate">
2699 <getter><![CDATA[
2700 if (this.mStartDate) return this.mStartDate;
2701 else if (this.mDateList && this.mDateList.length > 0) return this.mDateList[0];
2702 else return null;
2703 ]]></getter>
2704 </property>
2705
2706 <property name="endDate">
2707 <getter><![CDATA[
2708 if (this.mEndDate) return this.mEndDate;
2709 else if (this.mDateList && this.mDateList.length > 0) return this.mDateList[this.mDateList.length-1];
2710 else return null;
2711 ]]></getter>
2712 </property>
2713
2714 <!-- the end date that should be used for getItems and similar queries -->
2715 <property name="queryEndDate" readonly="true">
2716 <getter><![CDATA[
2717 var end = this.endDate;
2718 if (!end)
2719 return null;
2720
2721 end = end.clone();
2722 end.day += 1;
2723 end.isDate = true;
2724
2725 return end;
2726 ]]></getter>
2727 </property>
2728
2729 <property name="tasksInView">
2730 <getter><![CDATA[
2731 return this.mTasksInView;
2732 ]]></getter>
2733 <setter><![CDATA[
2734 this.mTasksInView = val;
2735 return val;
2736 ]]></setter>
2737 </property>
2738
2739 <property name="showCompleted">
2740 <getter><![CDATA[
2741 return this.mShowCompleted;
2742 ]]></getter>
2743 <setter><![CDATA[
2744 this.mShowCompleted = val;
2745 return val;
2746 ]]></setter>
2747 </property>
2748
2749 <property name="timezone">
2750 <getter><![CDATA[
2751 return this.mTimezone;
2752 ]]></getter>
2753 <setter><![CDATA[
2754 this.mTimezone = val;
2755 return val;
2756 ]]></setter>
2757 </property>
2758
2759 <property name="displayDaysOff"
2760 onget="return this.mDisplayDaysOff;"
2761 onset="return (this.mDisplayDaysOff = val);"/>
2762
2763 <property name="daysOffArray">
2764 <getter><![CDATA[
2765 return this.mDaysOffArray;
2766 ]]></getter>
2767 <setter><![CDATA[
2768 this.mDaysOffArray = val;
2769 return val;
2770 ]]></setter>
2771 </property>
2772
2773 <method name="fireEvent">
2774 <parameter name="aEventName"/>
2775 <parameter name="aEventDetail"/>
2776 <body><![CDATA[
2777 var event = document.createEvent('Events');
2778 event.initEvent(aEventName, true, false);
2779 event.detail = aEventDetail;
2780 this.dispatchEvent(event);
2781 ]]></body>
2782 </method>
2783
2784 <method name="showDate">
2785 <parameter name="aDate"/>
2786 <body><![CDATA[
2787 var targetDate = aDate.getInTimezone(this.mTimezone);
2788 targetDate.isDate = true;
2789
2790 if (this.mStartDate && this.mEndDate) {
2791 if (this.mStartDate.compare(targetDate) <= 0 &&
2792 this.mEndDate.compare(targetDate) >= 0)
2793 return;
2794 } else if (this.mDateList) {
2795 for each (var d in this.mDateList) {
2796 // if date is already visible, nothing to do
2797 if (d.compare(targetDate) == 0)
2798 return;
2799 }
2800 }
2801
2802 // if we're only showing one date, then continue
2803 // to only show one date; otherwise, show the week.
2804 if (this.numVisibleDates == 1) {
2805 this.setDateRange(aDate, aDate);
2806 } else {
2807 this.setDateRange(aDate.startOfWeek, aDate.endOfWeek);
2808 }
2809
2810 this.selectedDay = targetDate;
2811 ]]></body>
2812 </method>
2813
2814 <method name="setDateRange">
2815 <parameter name="aStartDate"/>
2816 <parameter name="aEndDate"/>
2817 <body><![CDATA[
2818 // normalize dates to display timezone
2819 var startDate = aStartDate.getInTimezone(this.mTimezone);
2820 startDate.isDate = true;
2821 var endDate = aEndDate.getInTimezone(this.mTimezone);
2822 endDate.isDate = true;
2823 // make sure unnormalized version not used below
2824 aStartDate = aEndDate = null;
2825
2826 if (this.mStartDate && this.mEndDate &&
2827 this.mStartDate.compare(startDate) == 0 &&
2828 this.mEndDate.compare(endDate) == 0) {
2829 // Do not change anything if the date range already matches.
2830 // XXX In general it should be possible to return here, but
2831 // lightning doesn't like it when first initializing the view.
2832 // return;
2833 }
2834
2835 if (this.mDisplayDaysOff) {
2836 startDate.makeImmutable();
2837 endDate.makeImmutable();
2838 this.mDateList = null;
2839 this.mStartDate = startDate;
2840 this.mEndDate = endDate;
2841 //
2842 // For a true multiday view (e.g, 3 days advanced by one day
2843 // at a time), a smarter refresh could reuse boxes, comparing
2844 // the current date range and add/remove, instead of just
2845 // replacing.
2846 //
2847 this.refresh();
2848 } else { // workdays only
2849 var dateList = new Array();
2850 for (var d = startDate.clone(); d.compare(endDate) <= 0;) {
2851 if (this.mDaysOffArray.indexOf(d.weekday) == -1) {
2852 var workday = d.clone();
2853 workday.makeImmutable();
2854 dateList.push(workday);
2855 }
2856 d.day += 1;
2857 }
2858 this.setDateList(dateList.length, dateList);
2859 }
2860 ]]></body>
2861 </method>
2862
2863 <method name="setDateList">
2864 <parameter name="aCount"/>
2865 <parameter name="aDates"/>
2866 <body><![CDATA[
2867 this.mStartDate = null;
2868 this.mEndDate = null;
2869
2870 if (aCount == 0) {
2871 this.mDateList = null;
2872 } else {
2873 aDates.sort (function(a, b) { return a.compare(b); });
2874 this.mDateList = aDates.map(
2875 function dateMapper(d) {
2876 if (d.isDate && !d.isMutable)
2877 return d;
2878
2879 var newDate = d.clone();
2880 newDate.isDate = true;
2881 newDate.makeImmutable();
2882 return newDate;
2883 }
2884 );
2885 }
2886
2887 this.refresh();
2888 ]]></body>
2889 </method>
2890
2891 <method name="getDateList">
2892 <parameter name="aCount"/>
2893 <body><![CDATA[
2894 var dates = new Array();
2895 if (this.mStartDate && this.mEndDate) {
2896 var d = this.mStartDate.clone();
2897 while (d.compare(this.mEndDate) <= 0) {
2898 dates.push(d.clone());
2899 d.day += 1;
2900 }
2901 } else if (this.mDateList) {
2902 for each (var d in this.mDateList)
2903 dates.push(d.clone());
2904 }
2905
2906 aCount.value = dates.length;
2907 return dates;
2908 ]]></body>
2909 </method>
2910
2911 <property name="selectedDay">
2912 <getter><![CDATA[
2913 if (this.numVisibleDates == 1)
2914 return this.mDateColumns[0].date;
2915
2916 if (this.mSelectedDay)
2917 return this.mSelectedDay;
2918
2919 if (this.mSelectedDayCol)
2920 return this.mSelectedDayCol.date;
2921
2922 return null;
2923 ]]></getter>
2924 <setter><![CDATA[
2925 // ignore if just 1 visible, it's always selected,
2926 // but we don't indicate it
2927 if (this.numVisibleDates == 1) {
2928 this.fireEvent("dayselect", val);
2929 return val;
2930 }
2931
2932 if (this.mSelectedDayCol) {
2933 this.mSelectedDayCol.column.selected = false;
2934 this.mSelectedDayCol.header.removeAttribute("selected");
2935 }
2936
2937 if (val) {
2938 this.mSelectedDayCol = this.findColumnForDate(val);
2939 if (this.mSelectedDayCol) {
2940 this.mSelectedDay = this.mSelectedDayCol.date;
2941 this.mSelectedDayCol.column.selected = true;
2942 this.mSelectedDayCol.header.setAttribute("selected", "true");
2943 } else {
2944 this.mSelectedDay = val;
2945 }
2946 }
2947 this.fireEvent("dayselect", val);
2948 return val;
2949 ]]></setter>
2950 </property>
2951
2952 <method name="getSelectedItems">
2953 <parameter name="aCount"/>
2954 <body><![CDATA[
2955 aCount.value = this.mSelectedItems.length;
2956 return this.mSelectedItems;
2957 ]]></body>
2958 </method>
2959 <method name="setSelectedItems">
2960 <parameter name="aCount"/>
2961 <parameter name="aItems"/>
2962 <parameter name="aSuppressEvent"/>
2963 <body><![CDATA[
2964 if (this.mSelectedItems.length) {
2965 for each (var item in this.mSelectedItems) {
2966 var cols = this.findColumnsForItem(item);
2967 for each (col in cols) {
2968 col.header.unselectOccurrence(item);
2969 col.column.unselectOccurrence(item);
2970 }
2971 }
2972 }
2973 this.mSelectedItems = aItems || [];
2974
2975 for each (var item in this.mSelectedItems) {
2976 var cols = this.findColumnsForItem(item);
2977 if (cols.length > 0) {
2978 var start = item.startDate || item.entryDate;
2979 for each (col in cols) {
2980 if (start.isDate) {
2981 col.header.selectOccurrence(item);
2982 } else {
2983 col.column.selectOccurrence(item);
2984 }
2985 }
2986 }
2987 }
2988
2989 if (!aSuppressEvent) {
2990 this.fireEvent("itemselect", this.mSelectedItems);
2991 }
2992 ]]></body>
2993 </method>
2994
2995 <property name="pixelsPerMinute">
2996 <getter>return this.mPixPerMin</getter>
2997 <setter>this.setPixelsPerMin(val); return val;</setter>
2998 </property>
2999
3000 <property name="activeInPlaceEdit">
3001 <getter><![CDATA[
3002 return this.mInPlaceEditActive;
3003 ]]></getter>
3004 <setter><![CDATA[
3005 this.mInPlaceEditActive = val;
3006 return val;
3007 ]]></setter>
3008 </property>
3009
3010 <!-- private -->
3011
3012 <property name="numVisibleDates" readonly="true">
3013 <getter><![CDATA[
3014 if (this.mDateList)
3015 return this.mDateList.length;
3016
3017 var count = 0;
3018
3019 if (!this.mStartDate || !this.mEndDate) {
3020 // The view has not been initialized, so there are 0 visible dates.
3021 return count;
3022 }
3023
3024 var d = this.mStartDate.clone();
3025 while (d.compare(this.mEndDate) <= 0) {
3026 count++;
3027 d.day += 1;
3028 }
3029
3030 return count;
3031 ]]></getter>
3032 </property>
3033
3034 <property name="orient">
3035 <getter><![CDATA[return (this.getAttribute("orient") || "vertical");]]></getter>
3036 <setter><![CDATA[this.setAttribute("orient", val); return val;]]></setter>
3037 </property>
3038
3039 <method name="setAttribute">
3040 <parameter name="aAttr"/>
3041 <parameter name="aVal"/>
3042 <body><![CDATA[
3043 var needsreorient = false;
3044 var needsrelayout = false;
3045 if (aAttr == "orient") {
3046 if (this.getAttribute("orient") != aVal)
3047 needsreorient = true;
3048 }
3049
3050 if (aAttr == "context" || aAttr == "item-context")
3051 needsrelayout = true;
3052
3053 // this should be done using lookupMethod(), see bug 286629
3054 var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
3055
3056 if (needsrelayout && !needsreorient)
3057 this.relayout();
3058
3059 if (needsreorient)
3060 this.reorient();
3061
3062 return ret;
3063 ]]></body>
3064 </method>
3065
3066 <method name="reorient">
3067 <body><![CDATA[
3068 var orient = this.getAttribute("orient");
3069 var otherorient = "vertical";
3070 if (!orient) orient = "horizontal";
3071 if (orient == "vertical") otherorient = "horizontal";
3072
3073 if (orient == "horizontal")
3074 this.setPixelsPerMin(1.5);
3075 else
3076 this.setPixelsPerMin(0.6);
3077
3078 var normalelems = ['mainbox', 'timebar'];
3079 var otherelems = ['labelbox', 'labeldaybox', 'headertimespacer',
3080 'headerbox', 'headerdaybox', 'childbox', 'daybox'];
3081
3082 for each (var id in normalelems) {
3083 document.getAnonymousElementByAttribute(this, "anonid", id).setAttribute("orient", orient);
3084 }
3085
3086 for each (var id in otherelems) {
3087 document.getAnonymousElementByAttribute(this, "anonid", id).setAttribute("orient", otherorient);
3088 }
3089
3090 var labelbox = document.getAnonymousElementByAttribute(this, "anonid", "labelbox");
3091 var labeltimespacer = document.getAnonymousElementByAttribute(this, "anonid", "labeltimespacer");
3092 var headerbox = document.getAnonymousElementByAttribute(this, "anonid", "headerbox");
3093 var headertimespacer = document.getAnonymousElementByAttribute(this, "anonid", "headertimespacer");
3094 var timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
3095 var childbox = document.getAnonymousElementByAttribute(
3096 this, "anonid", "childbox");
3097 var mainbox = document.getAnonymousElementByAttribute(
3098 this, "anonid", "mainbox");
3099
3100 if (orient == "vertical") {
3101 childbox.setAttribute(
3102 "style", "overflow-x: hidden; overflow-y: auto;");
3103 mainbox.setAttribute(
3104 "style", "overflow-x: auto; overflow-y: hidden;");
3105 } else {
3106 childbox.setAttribute(
3107 "style", "overflow-x: auto; overflow-y: hidden;");
3108 mainbox.setAttribute(
3109 "style", "overflow-x: hidden; overflow-y: auto;");
3110 }
3111
3112 var boxes = ["daybox", "headerdaybox"];
3113 for each (var boxname in boxes) {
3114 var box = document.getAnonymousElementByAttribute(this, "anonid", boxname);
3115 var child = box.firstChild;
3116 while (child) {
3117 child.setAttribute("orient", orient);
3118 child = child.nextSibling;
3119 }
3120 }
3121 this.refresh();
3122 ]]></body>
3123 </method>
3124
3125 <field name="mRefreshQueue">[]</field>
3126 <field name="mRefreshPending">null</field>
3127
3128 <method name="popRefreshQueue">
3129 <body><![CDATA[
3130 var pendingRefresh = this.mRefreshPending;
3131 if (pendingRefresh) {
3132 if (pendingRefresh instanceof Components.interfaces.calIOperation) {
3133 this.mRefreshPending = null;
3134 pendingRefresh.cancel(null);
3135 } else {
3136 if(this.mRefreshQueue.length > 0) {
3137 this.relayout();
3138 }
3139 return;
3140 }
3141 }
3142
3143 var refreshJob = this.mRefreshQueue.pop();
3144 if (!refreshJob) {
3145 return;
3146 }
3147
3148 if (!this.startDate || !this.endDate)
3149 return;
3150
3151 // recreate our columns
3152 this.relayout();
3153
3154 if (!this.mCalendar)
3155 return;
3156
3157 // start our items query; for a disjoint date range
3158 // we get all the items, and just filter out the ones we don't
3159 // care about in addItem
3160
3161 var filter = this.mCalendar.ITEM_FILTER_CLASS_OCCURRENCES;
3162 if (this.mShowCompleted) {
3163 filter |= this.mCalendar.ITEM_FILTER_COMPLETED_ALL;
3164 } else {
3165 filter |= this.mCalendar.ITEM_FILTER_COMPLETED_NO;
3166 }
3167
3168 if(this.mTasksInView)
3169 filter |= this.mCalendar.ITEM_FILTER_TYPE_ALL;
3170 else
3171 filter |= this.mCalendar.ITEM_FILTER_TYPE_EVENT;
3172
3173 this.mRefreshPending = true;
3174 pendingRefresh = this.mCalendar.getItems(filter,
3175 0,
3176 this.startDate,
3177 this.queryEndDate,
3178 this.mOperationListener);
3179 if (pendingRefresh && pendingRefresh.isPending) { // support for calIOperation
3180 this.mRefreshPending = pendingRefresh;
3181 }
3182 ]]></body>
3183 </method>
3184
3185 <method name="refresh">
3186 <body><![CDATA[
3187 var refreshJob = {};
3188 this.mRefreshQueue.push(refreshJob);
3189 this.popRefreshQueue();
3190 ]]></body>
3191 </method>
3192
3193 <method name="relayout">
3194 <body><![CDATA[
3195 function createXULElement(el) {
3196 return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
3197 }
3198
3199 var orient = this.getAttribute("orient");
3200 var otherorient = "vertical";
3201 if (!orient) orient = "horizontal";
3202 if (orient == "vertical") otherorient = "horizontal";
3203
3204 var computedDateList;
3205 if (this.mDateList) {
3206 computedDateList = this.mDateList;
3207 } else if (this.mStartDate && this.mEndDate) {
3208 computedDateList = new Array();
3209
3210 var theDate = this.mStartDate.clone();
3211 while (theDate.compare(this.mEndDate) <= 0) {
3212 computedDateList.push(theDate.clone());
3213 theDate.day += 1;
3214 }
3215 }
3216
3217 var daybox = document.getAnonymousElementByAttribute(this, "anonid", "daybox");
3218 var headerdaybox = document.getAnonymousElementByAttribute(this, "anonid", "headerdaybox");
3219 var labeldaybox = document.getAnonymousElementByAttribute(this, "anonid", "labeldaybox");
3220
3221 if (!computedDateList || computedDateList.length == 0)
3222 return;
3223
3224 var calView = this;
3225 var dayStartMin = this.mDayStartMin;
3226 var dayEndMin = this.mDayEndMin;
3227 function setUpDayEventsBox(aDayBox) {
3228 aDayBox.setAttribute("class", "calendar-event-column-" + (counter % 2 == 0 ? "even" : "odd"));
3229 aDayBox.setAttribute("context", calView.getAttribute("context"));
3230 aDayBox.setAttribute("item-context", calView.getAttribute("item-context") || calView.getAttribute("context"));
3231 aDayBox.startLayoutBatchChange();
3232 aDayBox.date = d;
3233 aDayBox.setAttribute("orient", orient);
3234 aDayBox.calendarView = calView;
3235 aDayBox.setDayStartEndMinutes(dayStartMin, dayEndMin);
3236 }
3237
3238 function setUpDayHeaderBox(aDayBox) {
3239 aDayBox.date = d;
3240 aDayBox.calendarView = calView;
3241 aDayBox.setAttribute("orient", orient);
3242 }
3243
3244 this.mDateColumns = new Array();
3245
3246
3247 // get today's date
3248 var today = this.today();
3249 var counter = 0;
3250 var maxDayWidth = 0;
3251 var dayboxkids = daybox.childNodes;
3252 var headerboxkids = headerdaybox.childNodes;
3253 var labelboxkids = labeldaybox.childNodes;
3254
3255 for each (var d in computedDateList) {
3256 var dayEventsBox;
3257 if (counter < dayboxkids.length) {
3258 dayEventsBox = dayboxkids[counter];
3259 dayEventsBox.removeAttribute("relation");
3260 dayEventsBox.mEvents = new Array();
3261 } else {
3262 dayEventsBox = createXULElement("calendar-event-column");
3263 dayEventsBox.setAttribute("flex", "1");
3264 daybox.appendChild(dayEventsBox);
3265 }
3266 setUpDayEventsBox(dayEventsBox);
3267
3268 var dayHeaderBox;
3269 if (counter < headerboxkids.length) {
3270 dayHeaderBox = headerboxkids[counter];
3271 dayHeaderBox.removeAttribute("today");
3272 // Delete backwards to make sure we get them all
3273 // and delete until no more elements are left.
3274 while(dayHeaderBox.mItemBoxes.length != 0) {
3275 var num = dayHeaderBox.mItemBoxes.length;
3276 dayHeaderBox.deleteEvent(dayHeaderBox.mItemBoxes[num-1].occurrence);
3277 }
3278 } else {
3279 dayHeaderBox = createXULElement("calendar-header-container");
3280 dayHeaderBox.setAttribute("flex", "1");
3281 headerdaybox.appendChild(dayHeaderBox);
3282 }
3283 setUpDayHeaderBox(dayHeaderBox);
3284
3285 if (0 <= this.mDaysOffArray.indexOf(d.weekday)) {
3286 dayEventsBox.dayOff = true;
3287 dayHeaderBox.setAttribute("weekend", "true");
3288 } else {
3289 dayEventsBox.dayOff = false;
3290 dayHeaderBox.removeAttribute("weekend");
3291 }
3292
3293 // Set attributes for date relations.
3294 if (this.numVisibleDates > 1) {
3295 switch (d.compare(today)) {
3296 case -1:
3297 dayHeaderBox.setAttribute("relation", "past");
3298 dayEventsBox.setAttribute("relation", "past");
3299 break;
3300 case 0:
3301 dayHeaderBox.setAttribute("relation", "today");
3302 dayEventsBox.setAttribute("relation", "today");
3303 break;
3304 case 1:
3305 dayHeaderBox.setAttribute("relation", "future");
3306 dayEventsBox.setAttribute("relation", "future");
3307 break;
3308 }
3309 }
3310
3311 var labelbox;
3312
3313 var df = Components.classes["@mozilla.org/calendar/datetime-formatter;1"].
3314 getService(Components.interfaces.calIDateTimeFormatter);
3315 if (counter < labelboxkids.length) {
3316 labelbox = labelboxkids[counter];
3317 labelbox.date = d;
3318 labelbox.shortWeekNames = false;
3319 } else {
3320 labelbox = createXULElement("calendar-day-label");
3321 labelbox.setAttribute("flex", "1");
3322 labeldaybox.appendChild(labelbox);
3323
3324 labelbox.date = d;
3325 labelbox.shortWeekNames = false;
3326 }
3327 maxDayWidth = Math.max(maxDayWidth, labelbox.longWeekdayPixels);
3328
3329 // We don't want to actually mess with our original dates, plus
3330 // they're likely to be immutable.
3331 var d2 = d.clone();
3332 d2.isDate = true;
3333 d2.makeImmutable();
3334 this.mDateColumns.push ( { date: d2, column: dayEventsBox, header: dayHeaderBox } );
3335 counter++;
3336 }
3337
3338 // Remove any extra columns that may have been hanging around
3339 function removeExtraKids(elem) {
3340 while (counter < elem.childNodes.length) {
3341 elem.removeChild(elem.childNodes[counter]);
3342 }
3343 }
3344 removeExtraKids(daybox);
3345 removeExtraKids(headerdaybox);
3346 removeExtraKids(labeldaybox);
3347
3348 // Cache weekday length, give a 10 px grace period.
3349 this.mLongWeekdayTotalPixels = maxDayWidth * labeldaybox.childNodes.length + 10;
3350
3351 // fix pixels-per-minute
3352 this.onResize();
3353 for each (col in this.mDateColumns) {
3354 col.column.endLayoutBatchChange();
3355 }
3356
3357 // adjust scrollbar spacers
3358 // XXX For performance reasons this method call can be moved to the
3359 // XXX constructor and the reorient method as soon as the views
3360 // XXX are constructed statically (24 hrs).
3361 this.adjustScrollBarSpacers();
3362 ]]></body>
3363 </method>
3364
3365 <method name="findColumnForDate">
3366 <parameter name="aDate"/>
3367 <body><![CDATA[
3368 for each (var col in this.mDateColumns) {
3369 if (col.date.compare(aDate) == 0)
3370 return col;
3371 }
3372 return null;
3373 ]]></body>
3374 </method>
3375
3376 <method name="findColumnsForItem">
3377 <parameter name="aItem"/>
3378 <body><![CDATA[
3379 var columns = new Array();
3380
3381 if (!this.mDateColumns) {
3382 return columns;
3383 }
3384
3385 var tz = this.mDateColumns[0].date.timezone;
3386
3387 // Note that these may be dates or datetimes
3388 var startDate = aItem.startDate || aItem.entryDate;
3389 if (!startDate) {
3390 return columns;
3391 }
3392 var targetDate = startDate.getInTimezone(tz);
3393 var endDate = aItem.endDate || aItem.dueDate || startDate;
3394 var finishDate = endDate.getInTimezone(tz);
3395
3396 if (!targetDate.isDate) {
3397 // Set the time to 00:00 so that we get all the boxes
3398 targetDate.hour = 0;
3399 targetDate.minute = 0;
3400 targetDate.second = 0;
3401 }
3402
3403 if (targetDate.compare(finishDate) == 0) {
3404 // Zero length events are silly, but we have to handle them
3405 var col = this.findColumnForDate(targetDate);
3406 if (col) {
3407 columns.push(col);
3408 }
3409 }
3410
3411 while (targetDate.compare(finishDate) == -1) {
3412 var col = this.findColumnForDate(targetDate);
3413
3414 // This might not exist if the event spans the view start or end
3415 if (col) {
3416 columns.push(col);
3417 }
3418 targetDate.day += 1;
3419 }
3420
3421 return columns;
3422 ]]></body>
3423 </method>
3424
3425 <!-- for the given client-coord-system point, return
3426 - the calendar-event-column that contains it. If
3427 - no column contains it, return null.
3428 -->
3429 <method name="findColumnForClientPoint">
3430 <parameter name="aClientX"/>
3431 <parameter name="aClientY"/>
3432 <body><![CDATA[
3433 for each (var col in this.mDateColumns) {
3434 var bo = col.column.topbox.boxObject;
3435 if ((aClientX >= bo.screenX) && (aClientX < (bo.screenX + bo.width)) &&
3436 (aClientY >= bo.screenY) && (aClientY < (bo.screenY + bo.height)))
3437 {
3438 return col.column;
3439 }
3440 }
3441 return null;
3442 ]]></body>
3443 </method>
3444
3445 <method name="doAddEvent">
3446 <parameter name="aEvent"/>
3447 <body><![CDATA[
3448 //dump ("++ doAddevent\n");
3449 var cols = this.findColumnsForItem(aEvent);
3450 if (!cols.length)
3451 return;
3452
3453 for each (col in cols) {
3454 var column = col.column;
3455 var header = col.header;
3456
3457 var estart = aEvent.startDate || aEvent.entryDate;
3458 if (estart.isDate) {
3459 header.addEvent(aEvent);
3460 } else {
3461 column.addEvent(aEvent);
3462 }
3463 }
3464 ]]></body>
3465 </method>
3466
3467 <method name="doDeleteEvent">
3468 <parameter name="aEvent"/>
3469 <body><![CDATA[
3470 var cols = this.findColumnsForItem(aEvent);
3471 if (!cols.length)
3472 return;
3473
3474 for each (col in cols) {
3475 var column = col.column;
3476 var header = col.header;
3477
3478 var estart = aEvent.startDate || aEvent.entryDate;
3479 if (estart.isDate) {
3480 header.deleteEvent(aEvent);
3481 } else {
3482 column.deleteEvent(aEvent);
3483 }
3484 }
3485
3486 // See whether the item we are deleting was selected. If it was, then
3487 // fire the appropriate event so our watchers can update
3488 var found = false;
3489 for (var i = 0; i < this.mSelectedItems.length; i++) {
3490 if (this.mSelectedItems[i].hashId == aEvent.hashId) {
3491 this.mSelectedItems.splice(i, 1);
3492 found = true;
3493 }
3494 }
3495 if (found) {
3496 this.fireEvent("itemselect", this.mSelectedItems);
3497 }
3498 ]]></body>
3499 </method>
3500
3501 <method name="setPixelsPerMin">
3502 <parameter name="pixPerMin"/>
3503 <body><![CDATA[
3504 this.mPixPerMin = pixPerMin;
3505
3506 var timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
3507 timebar.pixelsPerMinute = pixPerMin;
3508
3509 for each (var col in this.mDateColumns) {
3510 col.column.pixelsPerMinute = pixPerMin;
3511 }
3512 ]]></body>
3513 </method>
3514
3515 <method name="today">
3516 <body><![CDATA[
3517 var date = createDateTime();
3518 date.jsDate = new Date();
3519 date = date.getInTimezone(this.mTimezone);
3520 date.isDate = true;
3521 return date;
3522 ]]></body>
3523 </method>
3524
3525 <method name="adjustWeekdayLength">
3526 <body><![CDATA[
3527 // This function is called from the resize handler and also from the
3528 // overflow event of the mainbox. The latter guarantees that the
3529 // labels are clipped in the instance that the overflow occurrs,
3530 // avoiding horizontal scrollbars from showing up for a short period.
3531
3532 var labeldaybox = document.getAnonymousElementByAttribute(this, "anonid", "labeldaybox");
3533 var labeldayboxkids = labeldaybox.childNodes;
3534 if (!labeldayboxkids || !labeldayboxkids[0]) {
3535 // This happens in rotated view. Don't do anything there.
3536 return;
3537 }
3538
3539 if (labeldayboxkids[0].boxObject.width * this.daysInView > this.mLongWeekdayTotalPixels) {
3540 // We have an underflow. Since the boxes all have equal width, we
3541 // can use the first boxes' width and multiply bu the number of
3542 // days in the view.
3543 for (var i = 0; i < labeldayboxkids.length; i++) {
3544 labeldayboxkids[i].shortWeekNames = false;
3545 }
3546 } else {
3547 // We have an overflow
3548 for (var i = 0; i < labeldayboxkids.length; i++) {
3549 labeldayboxkids[i].shortWeekNames = true;
3550 }
3551 }
3552 ]]></body>
3553 </method>
3554
3555 <method name="adjustScrollBarSpacers">
3556 <body><![CDATA[
3557 // get the view's orientation
3558 var propertyName;
3559 if (this.getAttribute("orient") == "vertical") {
3560 propertyName = "width";
3561 } else {
3562 propertyName = "height";
3563 }
3564
3565 // get the width/height of the childbox scrollbar
3566 var childbox = document.getAnonymousElementByAttribute(
3567 this, "anonid", "childbox");
3568 var propertyValue = childbox.boxObject.firstChild
3569 .boxObject[propertyName];
3570
3571 // set the same width/height for the label and header box spacers
3572 var headerScrollBarSpacer = document.getAnonymousElementByAttribute(
3573 this, "anonid", "headerscrollbarspacer");
3574 headerScrollBarSpacer.setAttribute(propertyName, propertyValue);
3575 var labelScrollBarSpacer = document.getAnonymousElementByAttribute(
3576 this, "anonid", "labelscrollbarspacer");
3577 labelScrollBarSpacer.setAttribute(propertyName, propertyValue);
3578 ]]></body>
3579 </method>
3580
3581 <field name="mFirstVisibleMinute">0</field>
3582 <method name="getFirstVisibleMinute">
3583 <body><![CDATA[
3584 var minute = 0;
3585 var childbox = document.getAnonymousElementByAttribute(this, "anonid", "childbox");
3586 var scrollBoxObject = childbox.boxObject.QueryInterface(Components.interfaces.nsIScrollBoxObject);
3587 if (scrollBoxObject != null) {
3588 var x = {};
3589 var y = {};
3590 scrollBoxObject.getPosition(x, y);
3591 if (childbox.getAttribute("orient") == "horizontal") {
3592 minute = Math.round(y.value/this.mPixPerMin);
3593 } else {
3594 minute = Math.round(x.value/this.mPixPerMin);
3595 }
3596 }
3597 return minute;
3598 ]]></body>
3599 </method>
3600
3601 <method name="setFirstVisibleMinute">
3602 <parameter name="aMinute"/>
3603 <body><![CDATA[
3604 this.mFirstVisibleMinute = aMinute;
3605 return this.mFirstVisibleMinute;
3606 ]]></body>
3607 </method>
3608
3609 <method name="scrollToMinute">
3610 <parameter name="aMinute"/>
3611 <body><![CDATA[
3612 var childbox = document.getAnonymousElementByAttribute(this, "anonid", "childbox");
3613 var scrollBoxObject = childbox.boxObject.QueryInterface(Components.interfaces.nsIScrollBoxObject);
3614 if (scrollBoxObject != null) {
3615 var x = {};
3616 var y = {};
3617 scrollBoxObject.getPosition(x, y);
3618 var pos = Math.round(aMinute * this.mPixPerMin);
3619 if (childbox.getAttribute("orient") == "horizontal") {
3620 scrollBoxObject.scrollTo(x.value, pos);
3621 } else {
3622 scrollBoxObject.scrollTo(pos, y.value);
3623 }
3624 }
3625 ]]></body>
3626 </method>
3627
3628 <method name="setDayStartEndMinutes">
3629 <parameter name="aDayStartMin"/>
3630 <parameter name="aDayEndMin"/>
3631 <body><![CDATA[
3632 if (aDayStartMin < this.mStartMin || aDayStartMin > aDayEndMin ||
3633 aDayEndMin > this.mEndMin) {
3634 throw Components.results.NS_ERROR_INVALID_ARG;
3635 }
3636 if (this.mDayStartMin != aDayStartMin ||
3637 this.mDayEndMin != aDayEndMin) {
3638
3639 this.mDayStartMin = aDayStartMin;
3640 this.mDayEndMin = aDayEndMin;
3641
3642 // Also update on the time-bar
3643 document.getAnonymousElementByAttribute(this, "anonid", "timebar")
3644 .setDayStartEndHours(this.mDayStartMin / 60,
3645 this.mDayEndMin / 60);
3646 }
3647
3648 ]]></body>
3649 </method>
3650
3651 <method name="setVisibleMinutes">
3652 <parameter name="aVisibleMinutes"/>
3653 <body><![CDATA[
3654 if (aVisibleMinutes <= 0 ||
3655 aVisibleMinutes > (this.mEndMin - this.mStartMin)) {
3656 throw Components.results.NS_ERROR_INVALID_ARG;
3657 }
3658 if (this.mVisibleMinutes != aVisibleMinutes) {
3659 this.mVisibleMinutes = aVisibleMinutes;
3660 }
3661 return this.mVisibleMinutes;
3662 ]]></body>
3663 </method>
3664 </implementation>
3665
3666 <handlers>
3667 <handler event="DOMMouseScroll"><![CDATA[
3668 if (!event.ctrlKey &&
3669 !event.shiftKey &&
3670 !event.altKey &&
3671 !event.metaKey) {
3672 // Only shift hours if no modifier is pressed.
3673 this.scrollToMinute(this.mFirstVisibleMinute +
3674 (event.detail < 0 ? -60 : 60));
3675 }
3676
3677 // We are taking care of scrolling, so prevent the default
3678 // action in any case.
3679 event.preventDefault();
3680 ]]></handler>
3681 <handler event="keypress"><![CDATA[
3682 const kKE = Components.interfaces.nsIDOMKeyEvent;
3683 if (event.keyCode == kKE.DOM_VK_BACK_SPACE ||
3684 event.keyCode == kKE.DOM_VK_DELETE)
3685 {
3686 if (!this.activeInPlaceEdit && this.mSelectedItems.length && this.controller) {
3687 this.controller.deleteOccurrences(this.mSelectedItems.length,
3688 this.mSelectedItems,
3689 event.ctrlKey,
3690 false);
3691 }
3692 }
3693 ]]></handler>
3694 </handlers>
3695 </binding>
3696 </bindings>
3697
3698 <!-- -*- Mode: xml; indent-tabs-mode: nil; -*- -->