Unreviewed, revert r284181
[WebKit-https.git] / Websites / webkit.org / demos / calendar / Calendar.js
1 var monthStrings = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
2 // Date object that represents the currently displayed month
3 var monthOnDisplay;
4 // Table that maps a date (in milliseconds) to the corresponding Day object
5 var dateToDayObjectMap;
6 // Calendar type currently selected
7 var selectedCalendarType = "home";
8 // Calendar event currently selected
9 var selectedCalendarEvent = null;
10 // The highest ID number that we have assigned so far
11 var highestID = 0;
12 // Date (in milliseconds) of the first day displayed in the calendar
13 var calendarStartTime = 0;
14 // Date (in milliseconds) of the last day displayed in the calendar
15 var calendarEndTime = 0;
16 // Audio sound for network connection lost event
17 var networkDropSound = new Audio("Boom.aiff");
18
19 // Override day box height if the number of rows in calendar is not 5 
20 var insertedStyleRuleIndexForDayBox = -1;
21 function updateCalendarRowCount(count) 
22 {
23     var stylesheet = document.styleSheets[0];
24     if (count == 5) {   // The case our stylesheet already handles, and probably the most common case
25         if (insertedStyleRuleIndexForDayBox >= 0) {
26             // Remove the style rule we previously added
27             stylesheet.deleteRule(insertedStyleRuleIndexForDayBox);
28         }
29         insertedStyleRuleIndexForDayBox = -1;
30         return;
31     }
32     if (insertedStyleRuleIndexForDayBox < 0)
33         insertedStyleRuleIndexForDayBox = stylesheet.insertRule(".day.box { }", stylesheet.cssRules.length);
34     var styleRule = stylesheet.cssRules[insertedStyleRuleIndexForDayBox];
35     var h = 100 / count;
36     styleRule.style.height = h + "%";
37 }
38
39 function initMonthOnDisplay() 
40 {
41     monthOnDisplay = CalendarState.currentMonth();
42     monthOnDisplayUpdated();
43 }
44
45 function monthOnDisplayUpdated() 
46 {
47     var monthTitle = monthStrings[monthOnDisplay.getMonth()] + " " + monthOnDisplay.getFullYear();
48     document.getElementById("monthTitle").innerText = monthTitle;
49     CalendarState.setCurrentMonth(monthOnDisplay.toString());
50     initDaysGrid();
51     CalendarDatabase.openWebKitCalendarEvents(); 
52 }
53
54 function initDaysGrid() 
55 {
56     // Figure out the first day that should be displayed in this grid.
57     var displayedDate = new Date(monthOnDisplay);
58     displayedDate.setDate(displayedDate.getDate() - displayedDate.getDay());
59     
60     // Fill out the entire grid
61     var daysGrid = document.getElementById("daysGrid");
62     daysGrid.removeChildren();
63     dateToDayObjectMap = new Object();
64     
65     calendarStartTime = displayedDate.getTime();
66     var doneWithThisMonth = false;
67     var rows = 0;
68     while (!doneWithThisMonth) {
69         rows++;
70         for (var i = 0; i < 7; ++i) {
71             var dateTime = displayedDate.getTime();
72             var dayObj = new Day(displayedDate);
73             dateToDayObjectMap[dateTime] = dayObj;
74             dayObj.attach(daysGrid);
75             // Increment by a day
76             displayedDate.setDate(displayedDate.getDate() + 1);
77         }
78         doneWithThisMonth = (displayedDate.getMonth() != monthOnDisplay.getMonth());
79     }
80     updateCalendarRowCount(rows);
81     displayedDate.setDate(displayedDate.getDate() - 1);     // Roll back to the last displayed date
82     displayedDate.setHours(23, 59, 59, 999);
83     calendarEndTime = displayedDate.getTime();
84 }
85
86 function isCalendarTypeVisible(type) 
87 {
88     var input = document.getElementById(type);
89     if (!input || input.tagName != "INPUT")
90         return;
91     return input.checked;
92 }
93
94 function addEventsOfCalendarType(calendarType)
95 {
96     CalendarDatabase.loadEventsFromDBForCalendarType(calendarType);
97 }
98
99 function removeEventsOfCalendarType(calendarType)
100 {
101     var daysGridElement = document.getElementById("daysGrid");
102     var dayElements = daysGridElement.childNodes;
103     for (var i = 0; i < dayElements.length; i++) {
104         var dayObj = dayObjectFromElement(dayElements[i]);
105         if (!dayObj)
106             continue;
107         dayObj.hideEventsOfCalendarType(calendarType);
108     }
109 }
110
111 // Event handlers --------------------------------------------------
112
113 function pageLoaded() 
114 {
115     document.getElementById("gridView").addEventListener("selectstart", stopEvent, true);
116     document.getElementById("searchResults").addEventListener("selectstart", stopEvent, true);
117     document.body.addEventListener("keyup", keyUpHandler, false);
118     document.body.addEventListener("online", displayOnlineStatus, true);
119     document.body.addEventListener("offline", displayOnlineStatus, true);
120     displayOnlineStatus();
121
122     initSearchArea();
123     // Initialize the checked states of the calendars
124     var calendarCheckboxes = document.getElementById("calendarList").getElementsByTagName("INPUT");
125     for (var i = 0; i < calendarCheckboxes.length; i++)
126         calendarCheckboxes[i].checked = CalendarState.calendarChecked(calendarCheckboxes[i].id);
127     // Initialize the calendar grid.
128     initMonthOnDisplay();
129 }
130
131 function previousMonth() 
132 {
133     monthOnDisplay.setMonth(monthOnDisplay.getMonth() - 1);
134     monthOnDisplayUpdated();
135 }
136
137 function nextMonth() 
138 {
139     monthOnDisplay.setMonth(monthOnDisplay.getMonth() + 1);
140     monthOnDisplayUpdated();
141 }
142
143 function calendarSelected(event) 
144 {
145     if (event.target.tagName == "INPUT")
146         return;
147     var oldSelectedInput = document.getElementById(selectedCalendarType);
148     var oldListItemElement = oldSelectedInput.findParentOfTagName("LI");
149     oldListItemElement.removeStyleClass("selected");
150     event.target.addStyleClass("selected");
151     selectedCalendarType = event.target.getElementsByTagName("INPUT")[0].id;
152 }
153
154 function calendarClicked(event) 
155 {
156     if (event.target.tagName != "INPUT")
157         return;
158     var calendarType = event.target.id;
159     var checked = event.target.checked;
160     CalendarState.setCalendarChecked(calendarType, checked);
161     if (checked)
162         addEventsOfCalendarType(calendarType);
163     else
164         removeEventsOfCalendarType(calendarType);
165 }
166
167 function keyUpHandler(event) 
168 {
169     switch (event.keyIdentifier) {
170         case "U+007F":   // Delete key
171             if (selectedCalendarEvent && selectedCalendarEvent.day)
172                 selectedCalendarEvent.day.deleteEvent(selectedCalendarEvent);
173             break;
174         case "Enter":
175             if (selectedCalendarEvent && !document.getElementById("eventOverlay").hasStyleClass("show"))
176                 selectedCalendarEvent.selected();
177             break;
178         default:
179             break;
180     }
181 }
182
183 function eventDetailsDismissed(event) 
184 {
185     if (selectedCalendarEvent) {
186         selectedCalendarEvent.listItemNode.removeStyleClass("selected");
187         // Update selected event
188         selectedCalendarEvent.detailsUpdated();
189     }
190     
191     // Hide event details
192     var eventOverlayElement = document.getElementById("eventOverlay");
193     // FIXME: would have added transitionend listener here but not supported yet.  Use setTimeout for now.
194     // Set timer to hide map and disclosure arrow - this is delayed so things wouldn't change while event details is fading out.
195     setTimeout("hideMapDisclosureArrow()", 1000);
196     eventOverlayElement.removeStyleClass("show");
197
198     selectedCalendarEvent = null;
199
200     // Show the gridView
201     document.getElementById("gridView").removeStyleClass("inactive");
202 }
203
204 function searchForEvent(query) 
205 {
206     if (query.length == 0) {
207         var searchResultsList = document.getElementById("searchResults");
208         searchResultsList.removeChildren();
209         unhighlightAllEvents();
210         return;
211     }
212     query = "%" + query + "%";
213         
214     CalendarDatabase.queryEventsInDB(query);
215 }
216
217 // Online status ----------------------------------------------------
218
219 function isOnline()
220 {
221     return navigator.onLine;
222 }
223
224 function displayOnlineStatus()
225 {
226     var statusIcon = document.getElementById("onlineStatusIcon");
227     if (isOnline())
228         statusIcon.removeStyleClass("offline");
229     else {
230         statusIcon.addStyleClass("offline");
231         networkDropSound.play();
232     }
233 }
234
235 // Map revelation ---------------------------------------------------
236
237 function hideMap() 
238 {
239     document.getElementById("eventDisclosureArrow").removeStyleClass("expanded");
240     document.getElementById("map").removeStyleClass("show");
241     document.getElementById("details").removeStyleClass("showingMap");
242 }
243
244 function showMap() 
245 {
246     document.getElementById("eventDisclosureArrow").addStyleClass("expanded");
247     document.getElementById("map").addStyleClass("show");
248     document.getElementById("details").addStyleClass("showingMap");
249 }
250
251 function toggleArrow() 
252 {
253     var newMapShowState = !document.getElementById("map").hasStyleClass("show");
254     if (newMapShowState)
255         showMap();
256     else
257         hideMap();
258 }
259
260 function hideMapDisclosureArrow() 
261 {
262     document.getElementById("eventDisclosureArrow").removeStyleClass("show");
263     document.getElementById("map").removeChildren();
264     hideMap();
265 }
266
267 function showMapDisclosureArrow() 
268 {
269     document.getElementById("eventDisclosureArrow").addStyleClass("show");
270 }
271
272 var locationImage;
273
274 function mapImageReceived(image) 
275 {
276     if (!image)
277         return;
278     image.addStyleClass("mapImage");
279     document.getElementById("map").appendChild(image);
280     showMapDisclosureArrow();
281 }
282
283 function requestMapImage() 
284 {
285     if (!isOnline())
286         return;
287     if (!locationImage)
288         locationImage = new LocationImage();
289     var address = document.getElementById("eventLocation").innerText;
290     locationImage.requestLocationImage(address, mapImageReceived);
291 }
292
293 // Search area ------------------------------------------------------
294
295 var dividerBarDragOffset = 0;
296 var dividerBarHeight = 24;
297
298 function initSearchArea() 
299 {
300     document.getElementById("dividerBar").addEventListener("mousedown", startDividerBarDragging, true);
301 }
302
303 function startDividerBarDragging(event) 
304 {
305     var dividerBarElement = document.getElementById("dividerBar");
306     if (event.target !== dividerBarElement)
307         return;
308
309     document.addEventListener("mousemove", dividerBarDragging, true);
310     document.addEventListener("mouseup", endDividerBarDragging, true);
311     
312     document.body.style.cursor = "row-resize";
313     
314     dividerBarDragOffset = event.pageY - document.getElementById("searchArea").totalOffsetTop;
315     stopEvent(event);
316 }
317
318 function dividerBarDragging(event) 
319 {
320     var dividerBarElement = document.getElementById("dividerBar");
321     var searchAreaElement = document.getElementById("searchArea");
322     var sidePanelHeight = document.getElementById("sidePanel").offsetHeight;
323     var calendarListElement = document.getElementById("calendarList");
324     var calendarListBottom = calendarListElement.totalOffsetTop + calendarListElement.offsetHeight;
325     
326     var dividerTop = event.pageY + dividerBarDragOffset;
327     dividerTop = Number.constrain(dividerTop, calendarListBottom, sidePanelHeight - dividerBarHeight);
328     var searchAreaTop = dividerTop + dividerBarHeight;
329     
330     dividerBarElement.style.top = dividerTop + "px";
331     searchAreaElement.style.top = searchAreaTop + "px";
332
333     stopEvent(event);
334 }
335
336 function endDividerBarDragging(event) 
337 {
338     document.removeEventListener("mousemove", dividerBarDragging, true);
339     document.removeEventListener("mouseup", endDividerBarDragging, true);
340
341     document.body.style.removeProperty("cursor");
342     dividerBarDragOffset = 0;
343     stopEvent(event);
344 }
345
346 // LocalStorage access ---------------------------------------------
347
348 var CalendarState = {
349     currentMonth: function() 
350     {
351         var month = new Date();
352         // Retrieve the month on display saved from last time this app is launched.
353         if (localStorage.monthOnDisplay)
354             month.setTime(Date.parse(localStorage.monthOnDisplay));
355         // First time we load this page - just use the current month.
356         month.setDate(1);
357         month.setHours(0, 0, 0, 0);
358         return month;
359     },
360
361     setCurrentMonth: function(monthString) 
362     {
363         localStorage.monthOnDisplay = monthString;
364     },
365
366     toCalendarKey: function(calendarType) 
367     {
368         return calendarType + "CalendarChecked";
369     },
370
371     calendarChecked: function(calendarType) 
372     {
373         var value = localStorage.getItem(CalendarState.toCalendarKey(calendarType));
374         if (!value)
375             return true;
376         return (value == "yes") ? true : false;
377     },
378
379     setCalendarChecked: function(calendarType, checked) 
380     {
381         localStorage.setItem(CalendarState.toCalendarKey(calendarType), checked ? "yes" : "no");
382     },
383 }
384
385 // Database access -------------------------------------------------
386
387 var CalendarDatabase = {
388     // The Events database object
389     db: null,
390     
391     // Flag that tracks if we have opened the database for the very first time
392     dbOpened: false,
393
394     // REVIEW: can probably have a method that takes in a SQL statement, the arguments to the statement, optional callback and error
395     // callback methods and execute the transaction on the database, and then have other methods just call that one method.
396     // But we'll spell out the steps to make the database transaction in each method here for demonstration purposes.
397
398     openWebKitCalendarEvents: function() 
399     {
400         // We have already made sure the WebKitCalendarEvents table has been created. Just load events directly.
401         if (CalendarDatabase.dbOpened) {
402             CalendarDatabase.loadEventsFromDB();
403             return;
404         }
405         CalendarDatabase.dbOpened = true;
406         
407         // Query for opening WebKitCalendarEvents table
408         var openTableStatement = "CREATE TABLE IF NOT EXISTS WebKitCalendarEvents (id REAL UNIQUE, eventTitle TEXT, eventLocation TEXT, startTime REAL, endTime REAL, eventCalendar TEXT, eventDetails TEXT)";
409         
410         // SQLStatementCallback - gets called after the table is created
411         function sqlStatementCallback(result) { CalendarDatabase.loadEventsFromDB(); };
412
413         // SQLStatementErrorCallback - gets called if there's an error opening the table
414         function sqlStatementErrorCallback(tx, err) { alert("Error opening WebKitCalendarEvents: " + err.message); };
415
416         // SQLTransactionCallback
417         function sqlTransactionCallback(tx) { tx.executeSql(openTableStatement, [], sqlStatementCallback, sqlStatementErrorCallback); };
418
419         CalendarDatabase.db.transaction(sqlTransactionCallback);
420     },
421
422     open: function() 
423     {
424         try {
425             if (!window.openDatabase) {
426                 alert("Couldn't open the database.  Please try with a WebKit nightly with the database feature enabled.");
427                 return;
428             }
429             CalendarDatabase.db = openDatabase("Events", "1.0", "Events Database", 1000000);
430             if (!CalendarDatabase.db)
431                 alert("Failed to open the database on disk.");
432         } catch(err) { }
433     },
434
435     loadEventsFromDB: function() 
436     {
437         var self = this;
438
439         // SQL query to retrieve all the events for this month
440         var eventsQuery = "SELECT id, eventTitle, eventLocation, startTime, endTime, eventCalendar, eventDetails FROM WebKitCalendarEvents WHERE (startTime BETWEEN ? and ?)";
441         
442         // Arguments to the SQL query above
443         var sqlArguments = [calendarStartTime, calendarEndTime];
444         
445         // SQLStatementCallback to process the query result
446         function sqlStatementCallback(tx, result) { self.processLoadedEvents(result.rows); };
447         
448         // SQLStatementErrorCallback that handles any error from the query
449         function sqlStatementErrorCallback(tx, error) { alert("Failed to retrieve events from database - " + error.message); };
450         
451         // SQLTransactionCallback
452         function sqlTransactionCallback(tx) { tx.executeSql(eventsQuery, sqlArguments, sqlStatementCallback, sqlStatementErrorCallback); };
453         
454         CalendarDatabase.db.transaction(sqlTransactionCallback);
455     },
456
457     saveAsNewEventToDB: function(calendarEvent) 
458     {
459         // SQL statement to insert new event into the database table
460         var insertEventStatement = "INSERT INTO WebKitCalendarEvents(id, eventTitle, eventLocation, startTime, endTime, eventCalendar, eventDetails) VALUES (?, ?, ?, ?, ?, ?, ?)";
461         
462         // Arguments to the SQL statement above
463         var sqlArguments = [calendarEvent.id, calendarEvent.title, calendarEvent.location, calendarEvent.from.getTime(), calendarEvent.to.getTime(), calendarEvent.calendar, calendarEvent.details];
464         
465         // SQLTransactionCallback
466         function sqlTransactionCallback(tx) { tx.executeSql(insertEventStatement, sqlArguments); };
467
468         CalendarDatabase.db.transaction(sqlTransactionCallback);
469     },
470
471     saveEventToDB: function(calendarEvent) {
472         // SQL statement to update an event in the database table
473         var updateEventStatement = "UPDATE WebKitCalendarEvents SET eventTitle = ?, eventLocation = ?, startTime = ?, endTime = ?, eventCalendar = ?, eventDetails = ? WHERE id = ?";
474         
475         // Arguments to the SQL statement above
476         var sqlArguments = [calendarEvent.title, calendarEvent.location, calendarEvent.from.getTime(), calendarEvent.to.getTime(), calendarEvent.calendar, calendarEvent.details, calendarEvent.id];
477         
478         // SQLTransactionCallback
479         function sqlTransactionCallback(tx) { tx.executeSql(updateEventStatement, sqlArguments); };
480
481         CalendarDatabase.db.transaction(sqlTransactionCallback);
482     },
483
484     deleteEventFromDB: function(calendarEvent) 
485     {
486         // SQL statement to delete an event from the database table
487         var deleteEventStatement = "DELETE FROM WebKitCalendarEvents WHERE id = ?";
488         
489         // Arguments to the SQL statement above
490         var sqlArguments = [calendarEvent.id];
491
492         // SQLTransactionCallback
493         function sqlTransactionCallback(tx) { tx.executeSql(deleteEventStatement, sqlArguments); };
494         CalendarDatabase.db.transaction(sqlTransactionCallback);
495     },
496
497     queryEventsInDB: function(query) 
498     {
499         var self = this;
500
501         // SQL query to search for events with keyword
502         var searchEventQuery = "SELECT id, eventTitle, eventLocation, startTime, endTime, eventCalendar, eventDetails FROM WebKitCalendarEvents WHERE eventTitle LIKE ? OR eventDetails LIKE ? OR eventLocation LIKE ?";
503         
504         // Arguments to the SQL query
505         var sqlArguments = [query, query, query];
506         
507         // SQLStatementCallback that processes the query result
508         function sqlStatementCallback(tx, result) { self.processQueryResults(result.rows); };
509         
510         // SQLStatementErrorCallback that reports any error from the query
511         function sqlStatementErrorCallback(tx, error) { alert("Failed to retrieve events from database - " + error.message); };
512         
513         // SQLTransactionCallback
514         function sqlTransactionCallback(tx) { tx.executeSql(searchEventQuery, sqlArguments, sqlStatementCallback, sqlTransactionCallback); };
515         
516         CalendarDatabase.db.transaction(sqlTransactionCallback);    
517     },
518
519     loadEventsFromDBForCalendarType: function(calendarType) 
520     {
521         var self = this;
522
523         // SQL query to retrieve all the events for this month
524         var eventsQuery = "SELECT id, eventTitle, eventLocation, startTime, endTime, eventCalendar, eventDetails FROM WebKitCalendarEvents WHERE (startTime BETWEEN ? and ?) AND eventCalendar = ?";
525         
526         // Arguments to the SQL query above
527         var sqlArguments = [calendarStartTime, calendarEndTime, calendarType];
528         
529         // SQLStatementCallback to process the query result
530         function sqlStatementCallback(tx, result) { self.processLoadedEvents(result.rows); };
531         
532         // SQLStatementErrorCallback that handles any error from the query
533         function sqlStatementErrorCallback(tx, error) { alert("Failed to retrieve events from database - " + error.message); };
534         
535         // SQLTransactionCallback
536         function sqlTransactionCallback(tx) { tx.executeSql(eventsQuery, sqlArguments, sqlStatementCallback, sqlStatementErrorCallback); };
537         
538         CalendarDatabase.db.transaction(sqlTransactionCallback);
539     },
540
541     processLoadedEvents: function(rows)
542     {
543         for (var i = 0; i < rows.length; i++) {
544             var row = rows.item(i);
545             var dayDate = Date.dayDateFromTime(row["startTime"]);
546             var dayObj = dateToDayObjectMap[dayDate.getTime()];
547             if (!dayObj)
548                 continue;
549
550             dayObj.insertEvent(CalendarEvent.calendarEventFromResultRow(row, false));
551
552             // Keep track of the highest id seen.
553             if (row["id"] > highestID)
554                 highestID = row["id"];
555         }
556     },
557
558     processQueryResults: function(rows)
559     {
560         var searchResultsList = document.getElementById("searchResults");
561         searchResultsList.removeChildren();
562         unhighlightAllEvents();
563         for (var i = 0; i < rows.length; i++) {
564             var row = rows.item(i);
565             var calendarEvent = CalendarEvent.calendarEventFromResultRow(row, true);
566             highlightEventInCalendar(calendarEvent);
567             searchResultsList.appendChild(calendarEvent.searchResultAsListItem());
568         }
569     }
570 }
571
572 // Open Database!
573 CalendarDatabase.open();
574
575 function highlightEventInCalendar(calendarEvent)
576 {
577     var itemsToHighlight = document.querySelectorAll("ul.contents li." + calendarEvent.calendar);
578     for (var i = 0; i < itemsToHighlight.length; ++i) {
579         var listItem = itemsToHighlight[i];
580         if (listItem.innerText !== calendarEvent.title)
581             continue;
582         if (listItem.hasStyleClass("highlighted"))
583             continue;
584         listItem.addStyleClass("highlighted");
585         break;
586     }
587 }
588
589 function unhighlightAllEvents()
590 {
591     var highlightedItems = document.querySelectorAll("ul.contents li.highlighted");
592     for (var i = 0; i < highlightedItems.length; ++i)
593         highlightedItems[i].removeStyleClass("highlighted");
594 }
595
596 // Day object -------------------------------------------------------
597
598 function dayObjectFromElement(element) 
599 {
600     var parent = element;
601     while (parent && !parent.dayObject)
602         parent = parent.parentNode;
603     return parent ? parent.dayObject : null;
604 }
605
606 function Day(date) 
607 {
608     this.date = new Date(date);
609     this.divNode = null;
610     this.contentsListNode = null;
611     this.eventsArray = null;
612 }
613
614 Day.prototype.attach = function(parent) 
615 {
616     /* The HTML of each day looks like this:
617         <div class="day box">
618             <div class="date">1</div>
619             <ul class="contents">
620                 <li>Event 1</li>
621                 <li>Event 2</li>
622             </ul>
623         </div>
624     */
625
626     if (this.divNode)
627         throw("We have already created html elements for this day!");
628     this.divNode = document.createElement("div");
629     this.divNode.dayObject = this;
630     this.divNode.addStyleClass("day");
631     this.divNode.addStyleClass("box");
632     if (this.date.getMonth() != monthOnDisplay.getMonth())
633         this.divNode.addStyleClass("notThisMonth");
634     if (this.date.isToday())
635         this.divNode.addStyleClass("today");
636     
637     var dateDiv = document.createElement("div");
638     dateDiv.addStyleClass("date");
639     dateDiv.innerText = this.date.getDate();
640     this.divNode.appendChild(dateDiv);
641     
642     this.contentsListNode = document.createElement("ul");
643     this.contentsListNode.addStyleClass("contents");
644     this.contentsListNode.addEventListener("dblclick", Day.newEvent, false);
645     this.divNode.appendChild(this.contentsListNode);
646     
647     parent.appendChild(this.divNode);
648 }
649
650 Day.newEvent = function(event) 
651 {
652     var element = event.target;
653     var dayObj = dayObjectFromElement(element);
654     if (!dayObj || dayObj.contentsListNode != element)
655         return;
656         
657     var calendarEvent = new CalendarEvent(dayObj.date, dayObj, selectedCalendarType, false);
658     calendarEvent.title = "New Event";
659     calendarEvent.from = dayObj.defaultEventStartTime();
660     var endTime = new Date(calendarEvent.from);
661     endTime.setHours(endTime.getHours() + 1);
662     calendarEvent.to = endTime;
663     dayObj.insertEvent(calendarEvent);
664     CalendarDatabase.saveAsNewEventToDB(calendarEvent);
665     selectedCalendarEvent = calendarEvent;
666     calendarEvent.show();
667     
668     stopEvent(event);
669 }
670
671 Day.prototype.insertEvent = function(calendarEvent) 
672 {
673     if (!this.eventsArray)
674         this.eventsArray = new Array();
675     // Remove the event from array if it's already there.
676     var index = this.eventsArray.indexOf(calendarEvent);
677     if (index >= 0)
678         this.eventsArray.splice(index, 1);
679     calendarEvent.detach();
680     // Don't display this in the calendar if the calendar is unchecked
681     if (!isCalendarTypeVisible(calendarEvent.calendar))
682         return;
683     // We want to insert the calendarEvent in order of start time
684     for (index = 0; index < this.eventsArray.length; index++) {
685         if (this.eventsArray[index].from > calendarEvent.from)
686             break;
687     }
688     this.eventsArray.splice(index, 0, calendarEvent);
689     calendarEvent.attach();
690 }
691
692 Day.prototype.deleteEvent = function(calendarEvent) 
693 {
694     CalendarDatabase.deleteEventFromDB(calendarEvent);
695     this.hideEvent(calendarEvent);
696 }
697
698 Day.prototype.hideEvent = function(calendarEvent)
699 {
700     calendarEvent.detach();
701     selectedCalendarEvent = null;
702     if (!this.eventsArray)
703         return;
704     var index = this.eventsArray.indexOf(calendarEvent);
705     if (index >= 0)
706         this.eventsArray.splice(index, 1);
707 }
708
709 Day.prototype.hideEventsOfCalendarType = function(calendarType) 
710 {
711     if (!this.eventsArray)
712         return;
713     var i = 0;
714     while (i < this.eventsArray.length) {
715         if (this.eventsArray[i].calendar == calendarType)
716             this.hideEvent(this.eventsArray[i]);
717         else
718             i++;
719     }
720 }
721
722 Day.prototype.defaultEventStartTime = function() 
723 {
724     var startTime;
725     if (!this.eventsArray || !this.eventsArray.length) {
726         startTime = new Date(this.date);
727         startTime.setHours(9, 0, 0, 0);     // Default: events start at 9am!
728         return startTime;
729     }
730     var lastEvent = this.eventsArray[this.eventsArray.length-1];
731     startTime = new Date(lastEvent.to);
732     startTime.roundToHour();
733     return startTime;
734 }
735
736 // CalendarEvent object -------------------------------------------------------
737
738 function calendarEventFromElement(element) 
739 {
740     var parent = element;
741     while (parent) {
742         if (parent.calendarEvent)
743             return parent.calendarEvent;
744         parent = parent.parentNode;
745     }
746     return null;
747 }
748
749 function CalendarEvent(date, day, calendar, fromSearch) 
750 {
751     this.date = date;
752     this.day = day;
753     this.fromSearch = fromSearch;
754     this.id = ++highestID;
755     this.title = "";
756     this.location = "";
757     this.from = null;
758     this.to = null;
759     this.calendar = calendar;
760     this.details = "";
761     this.listItemNode = null;
762 }
763
764 CalendarEvent.prototype.attach = function() 
765 {
766     var parentNode = this.day.contentsListNode;
767     if (!parentNode)
768         throw("Must have created day box's html hierarchy before adding calendar events.");
769     if (this.listItemNode)
770         parentNode.removeChild(this.listItemNode);
771     this.listItemNode = document.createElement("li");
772     this.listItemNode.calendarEvent = this;
773     this.listItemNode.addStyleClass(this.calendar);
774     this.listItemNode.innerText = this.title;
775     this.listItemNode.addEventListener("click", CalendarEvent.eventSelected, false);
776     var index = this.day.eventsArray.indexOf(this);
777     if (index < 0)
778         throw("Cannot attach if CalendarEvent does not belong to a Day object.");
779     var adjacentNode = null;
780     if (index < this.day.contentsListNode.childNodes.length)
781         adjacentNode = this.day.contentsListNode.childNodes[index];
782     this.day.contentsListNode.insertBefore(this.listItemNode, adjacentNode);
783 }
784
785 CalendarEvent.prototype.detach = function() 
786 {
787     var parentNode = this.day.contentsListNode;
788     if (parentNode && this.listItemNode)
789         parentNode.removeChild(this.listItemNode);
790     this.listItemNode = null;
791 }
792
793 CalendarEvent.eventSelected = function(event) 
794 {
795     var calendarEvent = calendarEventFromElement(event.target);
796     if (!calendarEvent)
797         return;
798     calendarEvent.selected();
799     stopEvent(event);
800 }
801
802 CalendarEvent.prototype.selected = function() 
803 {
804     if (selectedCalendarEvent == this) {
805         selectedCalendarEvent.show();
806         return;
807     }
808     if (selectedCalendarEvent)
809         selectedCalendarEvent.listItemNode.removeStyleClass("selected");
810     selectedCalendarEvent = this;
811     if (selectedCalendarEvent)
812         selectedCalendarEvent.listItemNode.addStyleClass("selected");
813 }
814
815 function minutesString(minutes) 
816 {
817     if (minutes < 10)
818         return "0" + minutes;
819     return minutes.toString();
820 }
821
822 CalendarEvent.prototype.show = function() 
823 {
824     // Update the event details
825     document.getElementById("eventTitle").innerText = this.title;
826     document.getElementById("eventLocation").innerText = this.location;
827     document.getElementById("eventFromDate").innerText = this.date.toLocaleDateString();
828     document.getElementById("eventFromHours").innerText = this.from.getHours();
829     document.getElementById("eventFromMinutes").innerText = minutesString(this.from.getMinutes());
830     document.getElementById("eventToDate").innerText = this.date.toLocaleDateString();
831     document.getElementById("eventToHours").innerText = this.to.getHours();
832     document.getElementById("eventToMinutes").innerText = minutesString(this.to.getMinutes());
833     document.getElementById("eventCalendarType").value = this.calendar;
834     document.getElementById("eventDetails").innerText = this.details;
835     
836     // Reset the map
837     requestMapImage();
838
839     // Fade out the gridView
840     document.getElementById("gridView").addStyleClass("inactive");
841         
842     // Show event details
843     document.getElementById("eventOverlay").addStyleClass("show");
844 }
845
846 CalendarEvent.prototype.detailsUpdated = function() 
847 {
848     // FIXME: error checking!!
849     this.title = document.getElementById("eventTitle").innerText;
850     this.location = document.getElementById("eventLocation").innerText;
851     this.from.setHours(document.getElementById("eventFromHours").innerText);
852     this.from.setMinutes(document.getElementById("eventFromMinutes").innerText);
853     this.to.setHours(document.getElementById("eventToHours").innerText);
854     this.to.setMinutes(document.getElementById("eventToMinutes").innerText);
855     this.calendar = document.getElementById("eventCalendarType").value;
856     this.details = document.getElementById("eventDetails").innerText;
857
858     CalendarDatabase.saveEventToDB(this);
859     if (!this.fromSearch && this.day)
860         this.day.insertEvent(this);
861 }
862
863 CalendarEvent.prototype.toString = function() 
864 {
865     return "CalendarEvent: " + this.title + " starting on " + this.from.toString();
866 }
867
868 CalendarEvent.prototype.searchResultAsListItem = function() 
869 {
870     var listItem = document.createElement("li");
871     var titleText = document.createTextNode(this.title);
872     var locationText = this.location.length > 0 ? document.createTextNode(this.location) : null;
873     var startTimeText = document.createTextNode(this.from.toLocaleString());
874     listItem.appendChild(titleText);
875     listItem.appendChild(document.createElement("br"));
876     if (locationText) {
877         listItem.appendChild(locationText);
878         listItem.appendChild(document.createElement("br"));
879     }
880     listItem.appendChild(startTimeText);
881     listItem.calendarEvent = this;
882     listItem.addStyleClass(this.calendar);
883     listItem.addEventListener("click", CalendarEvent.eventSelected, false);
884     this.listItemNode = listItem;
885     return listItem;
886 }
887
888 CalendarEvent.calendarEventFromResultRow = function(row, fromSearch) 
889 {
890     var dayDate = Date.dayDateFromTime(row["startTime"]);
891     var dayObj = dateToDayObjectMap[dayDate.getTime()];
892     var calendarEvent = new CalendarEvent(dayDate, dayObj, row["eventCalendar"], fromSearch);
893     calendarEvent.id = row["id"];
894     calendarEvent.title = row["eventTitle"];
895     calendarEvent.location = row["eventLocation"];
896     calendarEvent.from = new Date();
897     calendarEvent.from.setTime(row["startTime"]);
898     calendarEvent.to = new Date();
899     calendarEvent.to.setTime(row["endTime"]);
900     calendarEvent.details = row["eventDetails"];
901     return calendarEvent;
902 }
903
904 // Miscellaneous methods ------------------------------------------------
905
906 function stopEvent(event) 
907 {
908     event.preventDefault();
909     event.stopPropagation();
910 }