gecko-dev/calendar/base/content/calendar-multiday-view.xml

2904 lines
105 KiB
XML

<?xml version="1.0"?>
<!--
- ***** BEGIN LICENSE BLOCK *****
- Version: MPL 1.1/GPL 2.0/LGPL 2.1
-
- The contents of this file are subject to the Mozilla Public License Version
- 1.1 (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
- http://www.mozilla.org/MPL/
-
- Software distributed under the License is distributed on an "AS IS" basis,
- WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- for the specific language governing rights and limitations under the
- License.
-
- The Original Code is calendar views.
-
- The Initial Developer of the Original Code is
- Oracle Corporation
- Portions created by the Initial Developer are Copyright (C) 2005
- the Initial Developer. All Rights Reserved.
-
- Contributor(s):
- Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
- Thomas Benisch <thomas.benisch@sun.com>
- Dan Mosedale <dan.mosedale@oracle.com>
-
- Alternatively, the contents of this file may be used under the terms of
- either the GNU General Public License Version 2 or later (the "GPL"), or
- the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- in which case the provisions of the GPL or the LGPL are applicable instead
- of those above. If you wish to allow use of your version of this file only
- under the terms of either the GPL or the LGPL, and not to allow others to
- use your version of this file under the terms of the MPL, indicate your
- decision by deleting the provisions above and replace them with the notice
- and other provisions required by the GPL or the LGPL. If you do not delete
- the provisions above, a recipient may use your version of this file under
- the terms of any one of the MPL, the GPL or the LGPL.
-
- ***** END LICENSE BLOCK *****
-->
<!-- Note that this file depends on helper functions in calendarUtils.js-->
<bindings id="calendar-multiday-view-bindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<!--
- This is the time bar that displays time divisions to the side
- or top of a multiday view.
-->
<binding id="calendar-time-bar">
<content>
<xul:box xbl:inherits="orient,width,height" flex="1" anonid="topbox">
</xul:box>
</content>
<implementation>
<field name="mPixPerMin">0.6</field>
<field name="mStartMin">8*60</field>
<field name="mEndMin">20*60</field>
<constructor>
this.relayout();
</constructor>
<method name="setAttribute">
<parameter name="aAttr"/>
<parameter name="aVal"/>
<body><![CDATA[
var needsrelayout = false;
if (aAttr == "orient") {
if (this.getAttribute("orient") != aVal)
needsrelayout = true;
}
// this should be done using lookupMethod(), see bug 286629
var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
if (needsrelayout) {
this.relayout();
}
return ret;
]]></body>
</method>
<property name="pixelsPerMinute"
onget="return this.mPixPerMin"
onset="if (this.mPixPerMin != val) { this.mPixPerMin = val; this.relayout(); } return val;"/>
<method name="setStartEndMinutes">
<parameter name="aStartMin"/>
<parameter name="aEndMin"/>
<body><![CDATA[
if (aStartMin < 0 || aStartMin > aEndMin)
throw Components.results.NS_ERROR_INVALID_ARG;
if (aEndMin < 0 || aEndMin > 24*60)
throw Components.results.NS_ERROR_INVALID_ARG;
if (this.mStartMin != aStartMin ||
this.mEndMin != aEndMin)
{
this.mStartMin = aStartMin;
this.mEndMin = aEndMin;
this.relayout();
}
]]></body>
</method>
<method name="relayout">
<body><![CDATA[
//dump ("calendar-time-bar: relayout\n");
function createXULElement(el) {
return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
}
var topbox = document.getAnonymousElementByAttribute(this, "anonid", "topbox");
var orient = topbox.getAttribute("orient");
var otherorient = "vertical";
if (!orient) orient = "horizontal";
if (orient == "vertical") otherorient = "horizontal";
//dump ("calendar-time-bar: orient: " + orient + " other: " + otherorient + "\n");
function makeTimeBox(timestr, size) {
var box = createXULElement("box");
box.setAttribute("orient", orient);
if (orient == "horizontal") {
box.setAttribute("width", size);
} else {
box.setAttribute("height", size);
}
var label = createXULElement("label");
label.setAttribute("class", "calendar-time-bar-label");
label.setAttribute("value", timestr);
label.setAttribute("align", "center");
box.appendChild(label);
return box;
}
while (topbox.lastChild)
topbox.removeChild(topbox.lastChild);
var formatter =
Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
.getService(Components.interfaces.nsIScriptableDateFormat);
var timeString;
var theMin = this.mStartMin;
var theHour = Math.floor(theMin / 60);
var durLeft = this.mEndMin - this.mStartMin;
while (durLeft > 0) {
var dur;
if (this.mEndMin - theMin < 60) {
dur = this.mEndMin - theMin;
} else {
dur = theMin % 60;
}
theMin += dur;
if (dur == 0) dur = 60;
var box;
if (dur != 60) {
box = makeTimeBox("", dur * this.mPixPerMin);
} else {
timeString = formatter.FormatTime("",
Components.interfaces.nsIScriptableDateFormat
.timeFormatNoSeconds, theHour, 0, 0);
box = makeTimeBox(timeString, dur * this.mPixPerMin);
}
box.setAttribute("class", "calendar-time-bar-box-" + (theHour % 2 == 0 ? "even" : "odd"));
topbox.appendChild(box);
durLeft -= dur;
theMin += dur;
theHour++;
}
]]></body>
</method>
</implementation>
</binding>
<!--
- A simple gripbar that is displayed at the start and end of an
- event box. Needs to handle being dragged and resizing the
- event, thus changing its start/end time.
-->
<binding id="calendar-event-gripbar">
<content>
<xul:box anonid="thebox" flex="1"/>
</content>
<implementation>
<!-- public -->
<field name="eventElement">null</field>
<property name="parentorient">
<getter><![CDATA[
return this.getAttribute("parentorient");
]]></getter>
<setter><![CDATA[
this.setAttribute("parentorient", val);
var thebox = document.getAnonymousElementByAttribute(this, "anonid", "thebox");
if (val == "vertical")
thebox.setAttribute("orient", "horizontal");
else
thebox.setAttribute("orient", "vertical");
return val;
]]></setter>
</property>
<!-- private -->
<field name="mSide">top</field>
<field name="mResizing">false</field>
<field name="mSizeStartX">0</field>
<field name="mSizeStartY">0</field>
<constructor><![CDATA[
if (this.getAttribute("side") == "top")
this.mSide = "top";
else if (this.getAttribute("side") == "bottom")
this.mSide = "bottom";
this.parentorient = this.getAttribute("parentorient");
]]></constructor>
</implementation>
<handlers>
<handler event="mousedown" button="0"><![CDATA[
event.stopPropagation();
var whichside = this.getAttribute("whichside");
if (!whichside)
return;
// we make assumptions about our position in the tree here;
// specifically, this should get us to a <calendar-event-box>
var evbox = this.parentNode.parentNode;
// still select it (since we'll stopPropagation())
evbox.calendarView.selectedItem = evbox.mOccurrence;
// then start dragging it
evbox.parentColumn.startSweepingToModifyEvent(evbox, evbox.mOccurrence, whichside, event.screenX, event.screenY);
]]></handler>
</handlers>
</binding>
<!--
- A column for displaying event boxes in. One column per
- day; it manages the layout of the events given via add/deleteEvent.
-->
<binding id="calendar-event-column">
<content>
<xul:stack anonid="boxstack" flex="1" style="min-width: 1px; min-height: 1px">
<xul:box anonid="bgbox" flex="1" style="min-width: 1px; min-height: 1px"/>
<xul:box xbl:inherits="context" anonid="topbox" flex="1" equalsize="always" style="min-width: 1px; min-height: 1px"/>
<xul:box anonid="fgbox" flex="1" class="fgdragcontainer" style="min-width: 1px; min-height: 1px">
<xul:box anonid="fgdragspacer" style="display: inherit; overflow: hidden;">
<xul:spacer flex="1"/>
<xul:label anonid="fgdragbox-startlabel" class="fgdragbox-label"/>
</xul:box>
<xul:box anonid="fgdragbox" class="fgdragbox" />
<xul:label anonid="fgdragbox-endlabel" class="fgdragbox-label"/>
</xul:box>
</xul:stack>
</content>
<implementation>
<constructor><![CDATA[
this.mEvents = Array();
]]></constructor>
<!-- fields -->
<field name="mPixPerMin">0.6</field>
<field name="mStartMin">8*60</field>
<field name="mEndMin">20*60</field>
<field name="mEvents">new Array()</field>
<field name="mEventMap">null</field>
<field name="mCalendarView">null</field>
<field name="mDate">null</field>
<field name="mTimezone">"UTC"</field>
<field name="mDragState">null</field>
<field name="mLayoutBatchCount">0</field>
<!-- Since we'll often be getting many events in rapid succession, this
timer helps ensure that we don't re-compute the event map too many
times in a short interval, and therefore improves performance.-->
<field name="mEventMapTimeout">null</field>
<!-- properties -->
<property name="pixelsPerMinute">
<getter><![CDATA[
return this.mPixPerMin;
]]></getter>
<setter><![CDATA[
if (val <= 0.0)
val = 0.01;
if (val != this.mPixPerMin) {
this.mPixPerMin = val;
this.relayout();
}
]]></setter>
</property>
<property name="selected">
<getter><![CDATA[
if (this.getAttribute("selected") == "true")
return true;
return false;
]]></getter>
<setter><![CDATA[
if (val)
this.setAttribute("selected", "true");
else
this.removeAttribute("selected");
return val;
]]></setter>
</property>
<property name="date">
<getter><![CDATA[
return this.mDate;
]]></getter>
<setter><![CDATA[
this.mDate = val;
if (val.timezone != this.mTimezone) {
//dump ("++ column tz: " + val.timezone + "\n");
this.mTimezone = val.timezone;
if (!this.mLayoutBatchCount) {
this.recalculateStartEndMinutes();
}
}
return val;
]]></setter>
</property>
<property name="calendarView"
onget="return this.mCalendarView;"
onset="return (this.mCalendarView = val);" />
<property
name="topbox"
readonly="true">
<getter><![CDATA[
return document.getAnonymousElementByAttribute(this, "anonid", "topbox");
]]></getter>
</property>
<property
name="bgbox"
readonly="true">
<getter><![CDATA[
return document.getAnonymousElementByAttribute(this, "anonid", "bgbox");
]]></getter>
</property>
<field name="mFgboxes">null</field>
<property
name="fgboxes"
readonly="true">
<getter><![CDATA[
if (this.mFgboxes == null) {
this.mFgboxes = {
box: document.getAnonymousElementByAttribute(this, "anonid", "fgbox"),
dragbox: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox"),
dragspacer: document.getAnonymousElementByAttribute(this, "anonid", "fgdragspacer"),
startlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-startlabel"),
endlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-endlabel")
};
}
return this.mFgboxes;
]]></getter>
</property>
<property
name="events"
readonly="true"
onget="return this.mEvents"/>
<!-- methods -->
<method name="setStartEndMinutes">
<parameter name="aStartMin"/>
<parameter name="aEndMin"/>
<body><![CDATA[
if (aStartMin < 0 || aStartMin > aEndMin)
throw Components.results.NS_ERROR_INVALID_ARG;
if (aEndMin < 0 || aEndMin > 24*60)
throw Components.results.NS_ERROR_INVALID_ARG;
if (this.mStartMin != aStartMin ||
this.mEndMin != aEndMin)
{
this.mStartMin = aStartMin;
this.mEndMin = aEndMin;
this.relayout();
}
]]></body>
</method>
<method name="selectOccurrence">
<parameter name="aOccurrence"/>
<body><![CDATA[
// if we're being asked to unselect, and we
// don't have a selection, return.
if (aOccurrence == null && !this.mCurrentSelection)
return;
if (this.mCurrentSelection) {
this.mCurrentSelection.eventbox.selected = false;
this.mCurrentSelection = null;
}
if (aOccurrence) {
var chunk = this.findChunkForOccurrence(aOccurrence);
if (!chunk || !chunk.eventbox) {
dump ("++ Couldn't find chunk to select!!!\n");
return;
}
chunk.eventbox.selected = true;
this.mCurrentSelection = chunk;
}
]]></body>
</method>
<method name="findChunkForOccurrence">
<parameter name="aOccurrence"/>
<body><![CDATA[
for each (var chunk in this.mEvents) {
if (chunk.event.hasSameIds(aOccurrence)) {
return chunk;
}
}
return null;
]]></body>
</method>
<method name="startLayoutBatchChange">
<body><![CDATA[
this.mLayoutBatchCount++;
]]></body>
</method>
<method name="endLayoutBatchChange">
<body><![CDATA[
this.mLayoutBatchCount--;
if (this.mLayoutBatchCount == 0)
this.relayout();
]]></body>
</method>
<method name="setAttribute">
<parameter name="aAttr"/>
<parameter name="aVal"/>
<body><![CDATA[
// this should be done using lookupMethod(), see bug 286629
var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
if (aAttr == "orient" && this.getAttribute("orient") != aVal) {
this.relayout();
}
return ret;
]]></body>
</method>
<method name="internalDeleteEvent">
<parameter name="aOccurrence"/>
<body><![CDATA[
var itemIndex = -1;
var i;
for (var i in this.mEvents) {
occ = this.mEvents[i].event;
if (occ.hasSameIds(aOccurrence))
{
itemIndex = i;
break;
}
}
if (itemIndex != -1) {
if (this.mSelectedItem == this.mEvents[itemIndex])
this.mSelectedItem = null;
this.mEvents.splice(itemIndex, 1);
return true;
} else {
return false;
}
]]></body>
</method>
<method name="recalculateStartEndMinutes">
<body><![CDATA[
for each (var chunk in this.mEvents) {
var mins = this.getStartEndMinutesForOccurrence(chunk.event);
chunk.startMinute = mins.start;
chunk.endMinute = mins.end;
}
this.relayout();
]]></body>
</method>
<!-- NOTE: This function may not return the true start and end time
of an occurrence if that occurrence starts or ends on a day
different than the day of this column -->
<method name="getStartEndMinutesForOccurrence">
<parameter name="aOccurrence"/>
<body><![CDATA[
var stdate = aOccurrence.startDate || aOccurrence.entryDate;
var enddate = aOccurrence.endDate || aOccurrence.dueDate;
if (stdate.timezone != this.mTimezone) {
stdate = stdate.getInTimezone (this.mTimezone);
}
if (enddate.timezone != this.mTimezone) {
enddate = enddate.getInTimezone (this.mTimezone);
}
var startHour = stdate.hour;
var startMinute = stdate.minute;
var endHour = enddate.hour;
var endMinute = enddate.minute;
// Handle cases where an event begins or ends on a day other than this
if (stdate.compare(this.mDate) == -1) {
startHour = 0;
startMinute = 0;
}
if (enddate.compare(this.mDate) == 1) {
endHour = 24;
endMinute = 0;
}
return { start: startHour * 60 + startMinute,
end: endHour * 60 + endMinute };
]]></body>
</method>
<method name="createChunk">
<parameter name="aOccurrence"/>
<body><![CDATA[
var mins = this.getStartEndMinutesForOccurrence(aOccurrence);
var chunk = {
startMinute: mins.start,
endMinute: mins.end,
event: aOccurrence
};
return chunk;
]]></body>
</method>
<method name="addEvent">
<parameter name="aOccurrence"/>
<body><![CDATA[
this.internalDeleteEvent(aOccurrence);
var chunk = this.createChunk(aOccurrence);
this.mEvents.push(chunk);
if (this.mEventMapTimeout) {
clearTimeout(this.mEventMapTimeout);
}
var column = this;
// Fun with scoping...
this.mEventMapTimeout = setTimeout(function() { column.relayout.call(column) }, 5);
]]></body>
</method>
<method name="deleteEvent">
<parameter name="aOccurrence"/>
<body><![CDATA[
if (this.internalDeleteEvent(aOccurrence))
this.relayout();
]]></body>
</method>
<method name="clear">
<body><![CDATA[
while (this.bgbox && this.bgbox.hasChildNodes())
this.bgbox.removeChild(this.bgbox.lastChild);
while (this.topbox && this.topbox.hasChildNodes())
this.topbox.removeChild(this.topbox.lastChild);
]]></body>
</method>
<method name="relayout">
<body><![CDATA[
if (this.mLayoutBatchCount > 0)
return;
function createXULElement(el) {
return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
}
this.clear();
var orient = this.getAttribute("orient");
var otherorient = "vertical";
if (!orient) orient = "horizontal";
if (orient == "vertical") otherorient = "horizontal";
// bgbox is used mainly for drawing the grid. at some point it may
// also be used for all-day events.
this.bgbox.setAttribute("orient", orient);
var theMin = this.mStartMin;
while (theMin < this.mEndMin) {
var dur = theMin % 60;
theMin += dur;
if (dur == 0) dur = 60;
var box = createXULElement("spacer");
// we key off this in a CSS selector
box.setAttribute("orient", orient);
box.setAttribute("class", "calendar-event-column-linebox");
if (orient == "vertical")
box.setAttribute("height", dur * this.mPixPerMin);
else
box.setAttribute("width", dur * this.mPixPerMin);
box.setAttribute("style", "min-width: 1px; min-height: 1px;");
this.bgbox.appendChild(box);
theMin += 60;
}
// fgbox is used for dragging events
this.fgboxes.box.setAttribute("orient", orient);
document.getAnonymousElementByAttribute(this, "anonid", "fgdragspacer").setAttribute("orient", orient);
// this one is set to otherorient, since it will contain
// child boxes set to "orient" (one for each set of
// overlapping event areas)
this.topbox.setAttribute("orient", otherorient);
this.mEventMap = this.computeEventMap();
for each (var column in this.mEventMap) {
var xulColumn = createXULElement("box");
xulColumn.setAttribute("orient", orient);
xulColumn.setAttribute("flex", "1");
xulColumn.setAttribute("style", "min-width: 1px; min-height: 1px;");
this.topbox.appendChild(xulColumn);
var numBlocksInserted = 0
var curTime = 0;
for each (var chunk in column) {
var duration = chunk.duration;
//dump ("curTime: " + curTime + " duration: " + duration + " ev: " + chunk.event + "\n");
// if this chunk isn't entirely visible, we skip it
if (curTime < this.mStartMin) {
if (curTime + duration <= this.mStartMin) {
curTime += duration;
continue;
}
// offset the duration so that stuff starts at
// whatever our start time is set to, if this item
// starts before our start time
var delta = this.mStartMin - curTime;
if (delta > 0) {
duration -= delta;
curTime += delta;
}
}
if (chunk.event) {
var chunkBox = createXULElement("calendar-event-box");
chunkBox.setAttribute("context", this.getAttribute("item-context") || this.getAttribute("context"));
chunkBox.setAttribute("style", "min-width: 1px; min-height: 1px;");
chunkBox.setAttribute("orient", orient);
if (orient == "vertical")
chunkBox.setAttribute("height", duration * this.mPixPerMin);
else
chunkBox.setAttribute("width", duration * this.mPixPerMin);
xulColumn.appendChild(chunkBox);
chunkBox.calendarView = this.calendarView;
chunkBox.occurrence = chunk.event.event;
chunkBox.parentColumn = this;
chunk.event.eventbox = chunkBox;
} else {
var chunkBox = createXULElement("spacer");
chunkBox.setAttribute("context", this.getAttribute("context"));
chunkBox.setAttribute("style", "min-width: 1px; min-height: 1px;");
chunkBox.setAttribute("orient", orient);
chunkBox.setAttribute("class", "calendar-empty-space-box");
xulColumn.appendChild(chunkBox);
if (orient == "vertical")
chunkBox.setAttribute("height", duration * this.mPixPerMin);
else
chunkBox.setAttribute("width", duration * this.mPixPerMin);
}
numBlocksInserted++;
curTime += duration;
}
if (numBlocksInserted == 0) {
// if we didn't insert any blocks, then
// forget about this column
this.topbox.removeChild(xulColumn);
}
}
]]></body>
</method>
<method name="computeEventMap">
<body><![CDATA[
//dump ("computeEventMap\n");
// we need to build a layout data structure
// that looks like this:
// [
// [
// { duration: 120 /* min */ },
// { duration: 180, event: ev },
// { duration: 240 }
// ],
// [
// { duration: 180 },
// { duration: 120, event: ev2 },
// { duration: 240 }
// ]
// ]
// Indicating two events that overlap, with each index in the main
// array indicating one vertical line of events. If an event can't be placed
// in the first line, it should be placed in the next, and so on.
this.mEvents.sort(function eventComparator(a,b) {
var comp = a.startMinute - b.startMinute;
if (comp != 0) return comp;
return b.endMinute - a.endMinute;
});
var eventMap = []
eventMap.push(new Array());
for each (var event in this.mEvents) {
if (event.startMinute == null || event.endMinute == null || event.event == null)
continue;
//dump ("=== event: " + event + " " + event.startMinute + "-" + event.endMinute + "\n");
var startAt = event.startMinute;
var endAt = event.endMinute;
var curCol = 0;
while (curCol < eventMap.length) {
//dump ("+ curCol: " + curCol + "\n");
var blockIndex = 0;
var curOffset = 0;
var finished = false;
var prevblock = null;
while (blockIndex < eventMap[curCol].length) {
var block = eventMap[curCol][blockIndex];
//dump (" blockIndex: " + blockIndex + " curOffset: " + curOffset + " block.duration: " + block.duration + " (startAt: " + startAt + ")\n");
if (curOffset <= startAt && curOffset + block.duration > startAt) {
// We want to insert the event here. prevblock contains the
// preceeding block, if any. block contains the block that already
// exists at this location, e.g.:
// .....v- startAt
// ~~----------+----------~~
// prevblock | block
// ~~----------+----------~~
// ^- curOffset
//
// The event we're trying to insert starts at startAt,
// which can be anywhere from curOffset - prevblock.duration to
// curOffset + block.duration.
//
// We need to look at block; if it's an event, then we evict ourselves
// to the next eventMap index. We also do this if it's free space
// and we can't fit ourselves here.
// If the previous block is an event, and we
// need to start in the middle of it, we push
// to the next column.
if (prevblock && prevblock.event && startAt < curOffset) {
//dump ("** break 1\n");
break;
}
// If the next block is an event, we push to the
// next column, since we can't break it.
if (block.event) {
//dump ("** break 2\n");
break;
}
// If the next block is free space, but it isn't
// large enough to hold our event, we push to the
// next column.
if (curOffset + block.duration < endAt) {
//dump ("** break 3\n");
break;
}
// Otherwise, we are ready to insert the event.
// Figure out how much to shrink the previous/following
// blocks.
var startDelta = startAt - curOffset;
if (startDelta < 0 || (prevblock && !prevblock.event)) {
// we need to shrink or expand the previous free space
eventMap[curCol][blockIndex-1].duration += startDelta;
curOffset += startDelta;
} else if (startDelta > 0) {
eventMap[curCol].splice(blockIndex, 0, { duration: startDelta });
curOffset += startDelta;
blockIndex++;
}
var endDelta = endAt - curOffset;
if (endDelta > 0) {
eventMap[curCol][blockIndex].duration -= startDelta;
}
// insert our event block
eventMap[curCol].splice(blockIndex, 0, { duration: endAt - startAt, event: event });
finished = true;
break;
}
prevblock = block;
curOffset += block.duration;
blockIndex++;
}
// we got to the end of the list, so just add to the end
if (blockIndex == eventMap[curCol].length && curOffset <= startAt) {
var delta = startAt - curOffset;
if (delta)
eventMap[curCol].push({ duration: delta });
eventMap[curCol].push({ duration: endAt - startAt, event: event });
finished = true;
}
if (finished) {
//dump (eventMap.toSource() + "\n");
break;
}
if (curCol+1 == eventMap.length) {
eventMap.push(new Array());
}
curCol++;
}
}
return eventMap;
]]></body>
</method>
<!--
- Event sweep handlers
-->
<method name="onEventSweepMouseMove">
<parameter name="event"/>
<body><![CDATA[
var col = document.calendarEventColumnDragging;
if (!col) return;
var dragState = col.mDragState;
col.fgboxes.box.setAttribute("dragging", "true");
col.fgboxes.dragbox.setAttribute("dragging", "true");
// check if we need to jump a column
if (dragState.dragType == "move") {
newcol = col.calendarView.findColumnForClientPoint(event.screenX, event.screenY);
if (newcol && newcol != col) {
// kill our drag state
col.fgboxes.dragbox.removeAttribute("dragging");
col.fgboxes.box.removeAttribute("dragging");
// jump ship
newcol.acceptInProgressSweep(dragState);
// restart event handling
col.onEventSweepMouseMove(event);
return;
}
}
var pos;
var sizeattr;
if (col.getAttribute("orient") == "vertical") {
pos = event.screenY - col.parentNode.boxObject.screenY - dragState.mouseOffset;
sizeattr = "height";
} else {
pos = event.screenX - col.parentNode.boxObject.screenX - dragState.mouseOffset;
sizeattr = "width";
}
// don't let pos go outside the window edges
if (pos < 0)
pos = 0;
// snap to 15 minute intervals
var interval = col.mPixPerMin * 15;
var curmin = Math.floor(pos/interval) * 15;
var deltamin = curmin - dragState.origMin;
if (dragState.dragType == "new") {
if (deltamin < 0) {
dragState.startMin = dragState.origMin + deltamin;
dragState.endMin = dragState.origMin;
} else {
dragState.startMin = dragState.origMin;
dragState.endMin = dragState.origMin + deltamin;
}
} else if (dragState.dragType == "move") {
// if we're moving, we can only move the start, and the end has to be exactly start+duration
dragState.startMin = dragState.origMin + deltamin;
dragState.endMin = dragState.startMin + dragState.limitDurationMin;
} else if (dragState.dragType == "modify-start") {
// if we're modifying the start, the end time is fixed.
dragState.startMin = dragState.origMin + deltamin;
dragState.endMin = dragState.limitEndMin;
// but we need to not go past the end; if we hit
// the end, then we'll clamp to the previous 15-min interval
if (dragState.endMin <= dragState.startMin)
dragState.startMin = Math.floor((dragState.endMin - 15) / 15) * 15;
} else if (dragState.dragType == "modify-end") {
// if we're modifying the end, the start time is fixed, and we'll always
// set the spacer to a constant size.
dragState.startMin = dragState.limitStartMin;
dragState.endMin = dragState.origMin + deltamin;
// but we need to not go past the start; if we hit
// the start, then we'll clamp to the next 15-min interval
if (dragState.endMin <= dragState.startMin)
dragState.endMin = Math.floor((dragState.startMin + 15) / 15) * 15;
}
// update the box sizes
col.fgboxes.dragspacer.setAttribute(sizeattr, dragState.startMin * col.mPixPerMin);
col.fgboxes.dragbox.setAttribute(sizeattr, Math.abs((dragState.endMin - dragState.startMin) * col.mPixPerMin));
// update the label
col.updateDragLabels();
]]></body>
</method>
<method name="onEventSweepMouseUp">
<parameter name="event"/>
<body><![CDATA[
var col = document.calendarEventColumnDragging;
if (!col) return;
var dragState = col.mDragState;
col.fgboxes.dragbox.removeAttribute("dragging");
col.fgboxes.box.removeAttribute("dragging");
window.removeEventListener("mousemove", col.onEventSweepMouseMove, true);
window.removeEventListener("mouseup", col.onEventSweepMouseUp, true);
document.calendarEventColumnDragging = null;
// if the user didn't sweep out at least a few pixels, ignore
// unless we're in a different column
if (dragState.origColumn == col) {
var ignore = false;
if (col.getAttribute("orient") == "vertical") {
if (Math.abs(event.screenY - dragState.origLoc) < 3)
ignore = true;
} else {
if (Math.abs(event.screenX - dragState.origLoc) < 3)
ignore = true;
}
if (ignore) {
document.calendarEventColumnDragging = null;
col.mDragState = null;
return;
}
}
var newStart;
var newEnd;
var startTZ;
var endTZ;
if (dragState.dragType == "new") {
newStart = col.mDate.clone();
newStart.isDate = false;
newEnd = col.mDate.clone();
newEnd.isDate = false;
} else {
var oldStart = dragState.dragOccurrence.startDate || dragState.dragOccurrence.entryDate;
var oldEnd = dragState.dragOccurrence.endDate || dragState.dragOccurrence.dueDate;
newStart = oldStart.clone();
newEnd = oldEnd.clone();
// Our views are pegged to the default timezone. If the event
// isn't also in the timezone, we're going to need to do some
// tweaking. We could just do this for every eventm but
// getInTimezone is slow, so it's much better to only do this
// when the timezones actually differ from the view's.
if (this.mTimezone != newStart.timezone ||
this.mTimezone != newEnd.timezone) {
startTZ = newStart.timezone;
endTZ = newEnd.timezone;
newStart = newStart.getInTimezone(col.calendarView.mTimezone);
newEnd = newEnd.getInTimezone(col.calendarView.mTimezone);
}
}
if (dragState.dragType == "modify-start" ||
dragState.dragType == "new") {
newStart.hour = 0;
newStart.minute = dragState.startMin + col.mStartMin;
newStart.normalize();
}
if (dragState.dragType == "modify-end" ||
dragState.dragType == "new") {
newEnd.hour = 0;
newEnd.minute = dragState.endMin + col.mStartMin;
newEnd.normalize();
}
if (dragState.dragType == "move") {
// Figure out how much the event moved.
var duration = col.mDate.subtractDate(dragState.origDate);
var minutes = dragState.startMin - dragState.origMin;
if (duration.isNegative) {
// Adding negative minutes to a negative duration makes the
// duration more positive, but we want more negative, and
// vice versa.
minutes *= -1;
}
duration.minutes = minutes;
duration.normalize();
newStart.addDuration(duration);
newStart.normalize();
newEnd.addDuration(duration);
newEnd.normalize();
}
// If we tweaked tzs, put times back in their original ones
if (startTZ) {
newStart = newStart.getInTimezone(startTZ);
}
if (endTZ) {
newEnd = newEnd.getInTimezone(endTZ);
}
if (dragState.dragType == "new") {
col.calendarView.controller.createNewEvent(col.calendarView.displayCalendar,
newStart,
newEnd);
} else if (dragState.dragType == "move" ||
dragState.dragType == "modify-start" ||
dragState.dragType == "modify-end")
{
col.calendarView.controller.modifyOccurrence(dragState.dragOccurrence,
newStart, newEnd);
}
document.calendarEventColumnDragging = null;
col.mDragState = null;
]]></body>
</method>
<!-- This is called by an event box when a grippy on either side is dragged,
- or when the middle is pressed to drag the event to move it. We create
- the same type of view that we use to sweep out a new event, but we
- initialize it based on the event's values and what type of dragging
- we're doing. In addition, we constrain things like not being able to
- drag the end before the start and vice versa.
-->
<method name="startSweepingToModifyEvent">
<parameter name="aEventBox"/>
<parameter name="aOccurrence"/>
<!-- "start", "end", "middle" -->
<parameter name="aGrabbedElement"/>
<!-- mouse screenX/screenY from the event -->
<parameter name="aMouseX"/>
<parameter name="aMouseY"/>
<body><![CDATA[
//dump ("startSweepingToModify\n");
this.mDragState = {
origColumn: this,
dragOccurrence: aOccurrence,
mouseOffset: 0
};
var interval = this.mPixPerMin * 15;
var sizeattr;
//dump ("AMY: " + aMouseY + " boY: " + this.parentNode.boxObject.screenY + "\n");
var frameloc;
if (this.getAttribute("orient") == "vertical") {
this.mDragState.origLoc = aMouseY;
frameloc = aMouseY - this.parentNode.boxObject.screenY;
sizeattr = "height";
} else {
this.mDragState.origLoc = aMouseX;
frameloc = aMouseX - this.parentNode.boxObject.screenX;
sizeattr = "width";
}
var mins = this.getStartEndMinutesForOccurrence(aOccurrence);
// these are only used to compute durations or to compute UI
// sizes, so offset by this.mStartMin for sanity here (at the
// expense of possible insanity later)
mins.start -= this.mStartMin;
mins.end -= this.mStartMin;
if (aGrabbedElement == "start") {
this.mDragState.dragType = "modify-start";
this.mDragState.limitEndMin = mins.end;
// snap start
this.mDragState.origMin = Math.floor(mins.start/15) * 15;
this.fgboxes.dragspacer.setAttribute(sizeattr, this.mDragState.origMin * this.mPixPerMin);
this.fgboxes.dragbox.setAttribute(sizeattr, (mins.end - this.mDragState.origMin) * this.mPixPerMin);
} else if (aGrabbedElement == "end") {
this.mDragState.dragType = "modify-end";
this.mDragState.limitStartMin = mins.start;
// snap end
this.mDragState.origMin = Math.floor(mins.end/15) * 15;
this.fgboxes.dragspacer.setAttribute(sizeattr, mins.start * this.mPixPerMin);
this.fgboxes.dragbox.setAttribute(sizeattr, (this.mDragState.origMin - mins.start) * this.mPixPerMin);
} else if (aGrabbedElement == "middle") {
this.mDragState.dragType = "move";
this.mDragState.limitDurationMin = mins.end - mins.start;
// in a move, origMin will be the min of the start element;
// so we snap start again, but we keep the duration the same
// (we move the end based on the duration of the event,
// not including our snap)
this.mDragState.origMin = Math.floor(mins.start/15) * 15;
// Because we can pass this event to other columns, we also need
// to track the original column's date too, to get the correct offset
this.mDragState.origDate = this.mDate;
this.fgboxes.dragspacer.setAttribute(sizeattr, this.mDragState.origMin * this.mPixPerMin);
this.fgboxes.dragbox.setAttribute(sizeattr, (mins.end - mins.start) * this.mPixPerMin);
// we need to set a mouse offset, since we're not dragging from
// one end of the element
if (aEventBox) {
if (this.getAttribute("orient") == "vertical")
this.mDragState.mouseOffset = aMouseY - aEventBox.boxObject.screenY;
else
this.mDragState.mouseOffset = aMouseX - aEventBox.boxObject.screenX;
}
} else {
dump ("+++ Invalid grabbed element: '" + aGrabbedElement + "'\n");
}
this.fgboxes.box.setAttribute("dragging", "true");
this.fgboxes.dragbox.setAttribute("dragging", "true");
document.calendarEventColumnDragging = this;
//dump (">>> drag is: " + this.mDragState.dragType + "\n");
window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
]]></body>
</method>
<!-- called by sibling columns to tell us to take over the sweeping
- of an event. Used by "move".
-->
<method name="acceptInProgressSweep">
<parameter name="aDragState"/>
<body><![CDATA[
this.mDragState = aDragState;
document.calendarEventColumnDragging = this;
this.fgboxes.box.setAttribute("dragging", "true");
this.fgboxes.dragbox.setAttribute("dragging", "true");
// the same event handlers are still valid,
// because they use document.calendarEventColumnDragging.
// So we really don't have anything to do here.
]]></body>
</method>
<method name="updateDragLabels">
<body><![CDATA[
if (!this.mDragState) return;
var realstartmin = this.mDragState.startMin + this.mStartMin;
var realendmin = this.mDragState.endMin + this.mStartMin;
if (this.mDragState.dragType == "move") {
realendmin = realstartmin + this.mDragState.limitDurationMin;
} else if (this.mDragState.dragType == "start") {
realendmin = this.mDragState.limitEndMin;
} else if (this.mDragState.dragType == "end") {
realstartmin = this.mDragSTate.limitStartMin;
}
var starthr = Math.floor(realstartmin / 60);
var startmin = realstartmin % 60;
var endhr = Math.floor(realendmin / 60);
var endmin = realendmin % 60;
var formatter =
Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
.getService(Components.interfaces.nsIScriptableDateFormat);
var startstr = formatter.FormatTime("",
Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
starthr, startmin, 0);
var endstr = formatter.FormatTime("",
Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
endhr, endmin, 0);
this.fgboxes.startlabel.setAttribute("value", startstr);
this.fgboxes.endlabel.setAttribute("value", endstr);
]]></body>
</method>
</implementation>
<handlers>
<handler event="dblclick" button="0"><![CDATA[
if (this.calendarView.controller) {
var newStart = this.date.clone();
newStart.isDate = false;
newStart.hour = 0;
const ROUND_INTERVAL = 15;
var interval = this.mPixPerMin * ROUND_INTERVAL;
var pos;
if (this.getAttribute("orient") == "vertical") {
pos = event.screenY - this.parentNode.boxObject.screenY;
} else {
pos = event.screenX - this.parentNode.boxObject.screenX;
}
newStart.minute = (Math.round(pos/interval) * ROUND_INTERVAL) + this.mStartMin;
newStart.normalize();
this.calendarView.controller.createNewEvent(null, newStart, null);
}
]]></handler>
<!-- mouse down handler, in empty event column regions. Starts sweeping out a new
- event.
-->
<handler event="mousedown"><![CDATA[
// select this column
this.calendarView.selectedDay = this.mDate;
// Only start sweeping out an event if the left button was clicked
if (event.button != 0) {
return;
}
// snap to 15 minute intervals
var interval = this.mPixPerMin * 15;
this.mDragState = {
origColumn: this,
dragType: "new",
mouseOffset: 0
};
if (this.getAttribute("orient") == "vertical") {
this.mDragState.origLoc = event.screenY;
this.mDragState.origMin = Math.floor((event.screenY - this.parentNode.boxObject.screenY)/interval) * 15;
this.fgboxes.dragspacer.setAttribute("height", this.mDragState.origMin * this.mPixPerMin);
} else {
this.mDragState.origLoc = event.screenX;
this.mDragState.origMin = Math.floor((event.screenX - this.parentNode.boxObject.screenX)/interval) * 15;
this.fgboxes.dragspacer.setAttribute("width", this.mDragState.origMin * this.mPixPerMin);
}
document.calendarEventColumnDragging = this;
window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
]]></handler>
</handlers>
</binding>
<binding id="calendar-header-container">
<content>
<xul:vbox xbl:inherits="selected" anonid="thebox" flex="1" class="calendar-event-column-header"/>
</content>
<implementation>
<field name="mItemBoxes">null</field>
<field name="mDate">null</field>
<field name="mCalendarView">null</field>
<constructor><![CDATA[
this.mItemBoxes = new Array();
]]></constructor>
<property name="mainbox">
<getter><![CDATA[
return document.getAnonymousElementByAttribute(this, 'anonid', 'thebox');
]]></getter>
</property>
<property name="date">
<getter><![CDATA[
return this.mDate;
]]></getter>
<setter><![CDATA[
this.mDate = val;
]]></setter>
</property>
<property name="calendarView">
<getter><![CDATA[
return this.mCalendarView;
]]></getter>
<setter><![CDATA[
this.mCalendarView = val;
]]></setter>
</property>
<method name="addEvent">
<parameter name="aItem"/>
<body><![CDATA[
function createXULElement(el) {
return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
}
var itemBox = createXULElement("calendar-editable-item");
this.mainbox.appendChild(itemBox);
itemBox.calendarView = this.calendarView;
itemBox.occurrence = aItem;
var ctxt = this.calendarView.getAttribute("item-context") ||
this.calendarView.getAttribute("context");
itemBox.setAttribute("context", ctxt);
this.mItemBoxes.push(itemBox);
]]></body>
</method>
<method name="deleteEvent">
<parameter name="aItem"/>
<body><![CDATA[
for (var i in this.mItemBoxes) {
if (this.mItemBoxes[i].occurrence.id == aItem.id) {
this.mainbox.removeChild(this.mItemBoxes[i]);
this.mItemBoxes.splice(i, 1);
}
}
]]></body>
</method>
<method name="selectOccurrence">
<parameter name="aItem"/>
<body><![CDATA[
for each (itemBox in this.mItemBoxes) {
if (aItem && itemBox.occurrence.id == aItem.id) {
itemBox.selected = true;
} else {
itemBox.selected = false;
}
}
]]></body>
</method>
</implementation>
<handlers>
<handler event="dblclick" button="0"><![CDATA[
this.calendarView.controller.createNewEvent();
]]></handler>
<handler event="mousedown"><![CDATA[
this.calendarView.selectedDay = this.mDate;
]]></handler>
</handlers>
</binding>
<!--
- An individual event box, to be inserted into a column.
-->
<binding id="calendar-event-box" extends="chrome://calendar/content/calendar-view-core.xml#calendar-editable-item">
<content>
<xul:box anonid="eventbox" xbl:inherits="orient,width,height" flex="1">
<xul:calendar-event-gripbar anonid="gripbar1" whichside="start" xbl:inherits="parentorient=orient"/>
<xul:vbox class="calendar-event-box-container" xbl:inherits="context,parentorient=orient" flex="1" align="left">
<xul:label anonid="event-name" flex="1" crop="right"/>
<xul:textbox class="plain" style="background: transparent !important"
anonid="event-name-textbox" crop="right" hidden="true" wrap="true"/>
<xul:spacer flex="1"/>
</xul:vbox>
<xul:calendar-event-gripbar anonid="gripbar2" whichside="end" xbl:inherits="parentorient=orient"/>
</xul:box>
</content>
<implementation>
<constructor><![CDATA[
this.orient = this.getAttribute("orient");
var otherorient = "vertical";
if (!orient) orient = "horizontal";
if (orient == "vertical") otherorient = "horizontal";
var self = this;
this.eventNameInput.onblur =
function onBlur() { self.stopEditing(true); };
this.eventNameInput.onkeypress = function onKeyPress(event) {
// save on enter
if (event.keyCode == 13)
self.stopEditing(true);
// abort on escape
else if (event.keyCode == 27)
self.stopEditing(false);
};
]]></constructor>
<!-- fields -->
<field name="mParentColumn">null</field>
<!-- methods/properties -->
<method name="setAttribute">
<parameter name="aAttr"/>
<parameter name="aVal"/>
<body><![CDATA[
var needsrelayout = false;
if (aAttr == "orient") {
if (this.getAttribute("orient") != aVal)
needsrelayout = true;
}
// this should be done using lookupMethod(), see bug 286629
var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
if (needsrelayout) {
var otherorient = "vertical";
if (val != "horizontal") otherorient = "horizontal";
var eventbox = document.getAnonymousElementByAttribute(this, "anonid", "eventbox");
eventbox.setAttribute("orient", val);
eventbox.setAttribute("class", "calendar-item calendar-event-box-" + val);
var gb1 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar1");
gb1.parentorient = val;
var gb2 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar2");
gb2.parentorient = val;
}
return ret;
]]></body>
</method>
<property name="parentColumn"
onget="return this.mParentColumn;"
onset="return (this.mParentColumn = val);"/>
<property name="startMinute">
<getter><![CDATA[
if (!this.mOccurrence)
return 0;
var startDate = this.mOccurrence.startDate || this.mOccurrence.entryDate;
return startDate.hour * 60 + startDate.minute;
]]></getter>
</property>
<property name="endMinute">
<getter><![CDATA[
if (!this.mOccurrence)
return 0;
var endDate = this.mOccurrence.endDate || this.mOccurrence.dueDate;
return endDate.hour * 60 + endDate.minute;
]]></getter>
</property>
</implementation>
<handlers>
<handler event="mousedown" button="0"><![CDATA[
event.stopPropagation();
if (this.mEditing)
return;
this.parentColumn.calendarView.selectedDay = this.parentColumn.mDate;
this.mInMouseDown = true;
this.mMouseX = event.screenX;
this.mMouseY = event.screenY;
]]></handler>
<handler event="mousemove"><![CDATA[
if (!this.mInMouseDown)
return;
var dx = Math.abs(event.screenX - this.mMouseX);
var dy = Math.abs(event.screenY - this.mMouseY);
// more than a 3 pixel move?
if ((dx*dx + dy*dy) > 9) {
if (this.parentColumn) {
if (this.editingTimer) {
clearTimeout(this.editingTimer);
this.editingTimer = null;
}
this.calendarView.selectedItem = this.mOccurrence;
this.mEditing = false;
if (this.calendarView)
this.calendarView.activeInPlaceEdit = false;
this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, "middle", this.mMouseX, this.mMouseY);
this.mInMouseDown = false;
}
}
]]></handler>
<handler event="mouseup"><![CDATA[
if (this.mEditing)
return;
this.mInMouseDown = false;
]]></handler>
<handler event="mouseover"><![CDATA[
if (this.calendarView && this.calendarView.controller) {
event.stopPropagation();
onMouseOverItem(event);
}
]]></handler>
</handlers>
</binding>
<binding id="calendar-multiday-view">
<content>
<xul:box anonid="mainbox" flex="1">
<!-- this thing is tricky; its width/height need to be programatically set based on the orientation -->
<xul:box anonid="labelbox">
<xul:box anonid="labeltimespacer">
<xul:button style="min-width: 10px; padding: 1px; background: red;"
anonid="rotate-button"
oncommand="var e=this.parentNode.parentNode.parentNode.parentNode; if (e.orient == 'horizontal') e.orient = 'vertical'; else e.orient = 'horizontal';" align="center"/>
</xul:box>
<xul:box anonid="labeldaybox" class="calendar-label-day-box" flex="1"
equalsize="always" />
<xul:box anonid="labelscrollbarspacer"/>
</xul:box>
<xul:box anonid="headerbox">
<xul:box anonid="headertimespacer"
class="calendar-header-time-spacer"/>
<xul:box anonid="headerdaybox" class="calendar-header-day-box"
flex="1" equalsize="always" />
<xul:box anonid="headerscrollbarspacer"/>
</xul:box>
<xul:scrollbox anonid="childbox" flex="1">
<!-- the orient of the calendar-time-bar needs to be the opposite of the parent -->
<xul:calendar-time-bar xbl:inherits="orient" anonid="timebar"/>
<xul:box anonid="daybox" class="calendar-day-box" flex="1"
equalsize="always"/>
</xul:scrollbox>
</xul:box>
</content>
<implementation implements="calICalendarView">
<field name="mResizeHandler">null</field>
<constructor><![CDATA[
var self = this;
this.mResizeHandler = function resizeHandler() { self.onResize(); };
window.addEventListener("resize", this.mResizeHandler, true);
var rButton = document.getAnonymousElementByAttribute(this, "anonid", "rotate-button");
rButton.label = calGetString("calendar", "rotate");
// set the flex attribute at the scrollbox-innerbox
// (this can be removed, after Bug 343555 is fixed)
var childbox = document.getAnonymousElementByAttribute(
this, "anonid", "childbox");
document.getAnonymousElementByAttribute(
childbox, "class", "box-inherit scrollbox-innerbox").flex = "1";
this.reorient();
]]></constructor>
<field name="mLastSize">0</field>
<method name="onResize">
<parameter name="aRealSelf"/>
<body><![CDATA[
var self = this;
if (aRealSelf) {
self = aRealSelf;
}
var timebar = document.getAnonymousElementByAttribute(self, "anonid", "timebar");
var daybox = document.getAnonymousElementByAttribute(self, "anonid", "daybox");
var orient = self.orient;
var size;
if (self.orient == "horizontal")
size = daybox.boxObject.width;
else
size = daybox.boxObject.height;
if (self.mLastSize > size) {
self.pixelsPerMinute = 0.01;
self.mLastSize = size;
if (self.orient == "horizontal")
size = daybox.boxObject.width;
else
size = daybox.boxObject.height;
}
self.mLastSize = size;
//self.removeAttribute("hidden");
var minutes = self.mEndMin - self.mStartMin;
var ppm = size / minutes;
ppm = Math.round(ppm * 10) / 10;
if (ppm < self.mMinPPM) {
ppm = self.mMinPPM;
}
self.pixelsPerMinute = ppm;
]]></body>
</method>
<field name="mController">null</field>
<field name="mCalendar">null</field>
<field name="mStartDate">null</field>
<field name="mEndDate">null</field>
<!-- mDateList will always be sorted before being set -->
<field name="mDateList">null</field>
<!-- array of { date: calIDatetime, column: colbox, header: hdrbox } -->
<field name="mDateColumns">null</field>
<field name="mBatchCount">0</field>
<field name="mPixPerMin">0.6</field>
<field name="mMinPPM">0.4</field>
<field name="mSelectedItem">null</field>
<field name="mSelectedDayCol">null</field>
<field name="mDefaultStartMin">8*60</field>
<field name="mDefaultEndMin">20*60</field>
<field name="mStartMin">8*60</field>
<field name="mEndMin">20*60</field>
<field name="mTasksInView">true</field>
<field name="mDaysOffArray">[0,6]</field>
<field name="mTimezone">"UTC"</field>
<field name="mSelectionObserver"><![CDATA[
({ calView: this,
onSelectionChanged: function selectionObserverCallback(itemSelectionArray) {
// The selection manager should be smart enough to only call this
// when the selection really did change, so just do it.
if (this.calView.mSelectedItem) {
var cols = this.calView.findColumnsForItem(this.calView.mSelectedItem);
var selectedStart = this.calView.mSelectedItem.startDate || this.calView.mSelectedItem.entryDate
for each (col in cols) {
if (!selectedStart.isDate) {
col.column.selectOccurrence(null);
} else {
col.header.selectOccurrence(null);
}
}
}
this.calView.mSelectedItem = itemSelectionArray[0];
if (!this.calView.mSelectedItem)
return;
var newSelectedStart = this.calView.mSelectedItem.startDate || this.calView.mSelectedItem.entryDate;
this.calView.showDate(newSelectedStart);
var cols = this.calView.findColumnsForItem(this.calView.mSelectedItem);
for each (col in cols) {
if (!newSelectedStart.isDate) {
col.column.selectOccurrence(this.calView.mSelectedItem);
} else {
col.header.selectOccurrence(this.calView.mSelectedItem);
}
}
}
})
]]></field>
<field name="mObserver"><![CDATA[
// the calIObserver, and calICompositeObserver
({
QueryInterface: function QueryInterface(aIID) {
if (!aIID.equals(Components.interfaces.calIObserver) &&
!aIID.equals(Components.interfaces.calICompositeObserver) &&
!aIID.equals(Components.interfaces.nsISupports)) {
throw Components.results.NS_ERROR_NO_INTERFACE;
}
return this;
},
calView: this,
onStartBatch: function onStartBatch() {
this.calView.mBatchCount++;
},
onEndBatch: function onEndBatch() {
this.mBatchCount--;
if (this.mBatchCount == 0) {
this.calView.refresh();
}
},
onLoad: function onLoad() {
this.calView.refresh();
},
onAddItem: function onAddItem(aItem) {
if (this.mBatchCount) {
return;
}
if (aItem instanceof Components.interfaces.calITodo &&
(!aItem.entryDate || !aItem.dueDate))
return;
if (aItem instanceof Components.interfaces.calITodo &&
!this.calView.mTasksInView)
return;
var occs = aItem.getOccurrencesBetween(this.calView.startDate,
this.calView.queryEndDate,
{});
//dump ("occs: " + occs.length + "\n");
for each (var occ in occs) {
this.calView.doAddEvent(occ);
}
var chunks = this.calView.createChunksForNonDateItems(occs);
if (this.calView.readjustStartEndMinutes(chunks, false)) {
this.calView.propagateStartEndMinutes();
}
return;
},
onModifyItem: function onModifyItem(aNewItem, aOldItem) {
if (this.mBatchCount) {
return;
}
if (aNewItem instanceof Components.interfaces.calITodo &&
!this.calView.mTasksInView)
return;
var occs;
if (!aOldItem instanceof Components.interfaces.calITodo ||
(aOldItem.entryDate && aOldItem.dueDate)) {
occs = aOldItem.getOccurrencesBetween(this.calView.startDate,
this.calView.queryEndDate,
{});
for each (var occ in occs) {
this.calView.doDeleteEvent(occ);
}
}
if (aNewItem instanceof Components.interfaces.calITodo &&
(!aNewItem.entryDate || !aNewItem.dueDate))
return;
occs = aOldItem.getOccurrencesBetween(this.calView.startDate,
this.calView.queryEndDate,
{});
for each (var occ in occs) {
this.calView.doDeleteEvent(occ);
}
occs = aNewItem.getOccurrencesBetween(this.calView.startDate,
this.calView.queryEndDate,
{});
for each (var occ in occs) {
this.calView.doAddEvent(occ);
}
this.calView.readjust();
},
onDeleteItem: function onDeleteItem(aItem) {
if (this.mBatchCount) {
return;
}
if (aItem instanceof Components.interfaces.calITodo &&
!this.calView.mTasksInView)
return;
if (aItem instanceof Components.interfaces.calITodo &&
(!aItem.entryDate || !aItem.dueDate))
return;
var occs = aItem.getOccurrencesBetween(this.calView.startDate,
this.calView.queryEndDate,
{});
for each (var occ in occs) {
this.calView.doDeleteEvent(occ);
}
this.calView.readjust();
},
onError: function onError(aErrNo, aMessage) { },
//
// calICompositeObserver stuff
// XXXvv we can be smarter about how we handle this stuff
//
onCalendarAdded: function onCalendarAdded(aCalendar) {
//dump ("view onCalendarAdded\n");
this.calView.refresh();
},
onCalendarRemoved: function onCalendarRemoved(aCalendar) {
//dump ("view onCalendarRemoved\n");
this.calView.refresh();
},
onDefaultCalendarChanged:
function onDefaultCalendarChanged(aNewDefaultCalendar) {
// don't care, for now
}
})
]]></field>
<field name="mOperationListener"><![CDATA[
({
calView: this,
onOperationComplete:
function onOperationComplete(aCalendar, aStatus, aOperationType,
aId, aDetail) {
if (this.calView.controller.selectionManager &&
this.calView.controller.selectionManager.selectedEvents[0])
this.calView.selectedItem = (this.calView.controller
.selectionManager.selectedEvents[0]);
},
onGetResult:
function onGetResult(aCalendar, aStatus, aItemType, aDetail,
aCount, aItems) {
if (!Components.isSuccessCode(aStatus))
return;
function hasGoodDates(item) {
if (item instanceof Components.interfaces.calITodo &&
(!item.entryDate || !item.dueDate)) {
return false;
}
return true;
}
aItems = aItems.filter(hasGoodDates);
var chunks = this.calView.createChunksForNonDateItems(aItems);
if (this.calView.readjustStartEndMinutes(chunks, false)) {
this.calView.propagateStartEndMinutes();
}
for each (var item in aItems) {
this.calView.doAddEvent(item);
}
}
})
]]></field>
<!-- calICalendarView -->
<property name="supportsDisjointDates"
onget="return true"/>
<property name="hasDisjointDates"
onget="return (this.mDateList != null);"/>
<property name="controller"
onget="return this.mController;"
onset="return (this.mController = val);" />
<property name="displayCalendar">
<getter><![CDATA[
return this.mCalendar;
]]></getter>
<setter><![CDATA[
if (this.mCalendar)
this.mCalendar.removeObserver (this.mObserver);
this.mCalendar = val;
this.mCalendar.addObserver (this.mObserver);
this.refresh();
]]></setter>
</property>
<property name="startDate">
<getter><![CDATA[
if (this.mStartDate) return this.mStartDate;
else if (this.mDateList && this.mDateList.length > 0) return this.mDateList[0];
else return null;
]]></getter>
</property>
<property name="endDate">
<getter><![CDATA[
if (this.mEndDate) return this.mEndDate;
else if (this.mDateList && this.mDateList.length > 0) return this.mDateList[this.mDateList.length-1];
else return null;
]]></getter>
</property>
<!-- the end date that should be used for getItems and similar queries -->
<property name="queryEndDate" readonly="true">
<getter><![CDATA[
var end = this.endDate;
if (!end)
return null;
end = end.clone();
end.day += 1;
end.isDate = true;
end.normalize();
return end;
]]></getter>
</property>
<property name="tasksInView">
<getter><![CDATA[
return this.mTasksInView;
]]></getter>
<setter><![CDATA[
this.mTasksInView = val;
]]></setter>
</property>
<property name="timezone">
<getter><![CDATA[
return this.mTimezone;
]]></getter>
<setter><![CDATA[
this.mTimezone = val;
return val;
]]></setter>
</property>
<property name="daysOffArray">
<getter><![CDATA[
return this.mDaysOffArray;
]]></getter>
<setter><![CDATA[
this.mDaysOffArray = val;
return val;
]]></setter>
</property>
<method name="fireEvent">
<parameter name="aEventName"/>
<parameter name="aEventDetail"/>
<body><![CDATA[
var event = document.createEvent('Events');
event.initEvent(aEventName, true, false);
event.detail = aEventDetail;
this.dispatchEvent(event);
]]></body>
</method>
<method name="showDate">
<parameter name="aDate"/>
<body><![CDATA[
var targetDate = aDate.getInTimezone(this.mTimezone);
targetDate.isDate = true;
if (this.mStartDate && this.mEndDate) {
if (this.mStartDate.compare(targetDate) <= 0 &&
this.mEndDate.compare(targetDate) >= 0)
return;
} else if (this.mDateList) {
for each (var d in this.mDateList) {
// if date is already visible, nothing to do
if (d.compare(targetDate) == 0)
return;
}
}
// if we're only showing one date, then continue
// to only show one date; otherwise, show the week.
if (this.numVisibleDates == 1) {
this.setDateRange(aDate, aDate);
} else {
this.setDateRange(aDate.startOfWeek, aDate.endOfWeek);
}
this.selectedDay = targetDate;
]]></body>
</method>
<method name="setDateRange">
<parameter name="aStartDate"/>
<parameter name="aEndDate"/>
<body><![CDATA[
//dump ("setDateRange\n");
this.mDateList = null;
this.mStartDate = aStartDate.getInTimezone(this.mTimezone);
this.mStartDate.isDate = true;
this.mStartDate.makeImmutable();
this.mEndDate = aEndDate.getInTimezone(this.mTimezone);
this.mEndDate.isDate = true;
this.mEndDate.makeImmutable();
// this function needs to be smarter, and needs to compare
// the current date range and add/remove, instead of just
// replacing.
this.refresh();
]]></body>
</method>
<method name="setDateList">
<parameter name="aCount"/>
<parameter name="aDates"/>
<body><![CDATA[
this.mStartDate = null;
this.mEndDate = null;
if (aCount == 0) {
this.mDateList = null;
} else {
aDates.sort (function(a, b) { return a.compare(b); });
this.mDateList = aDates.map(
function dateMapper(d) {
if (d.isDate && !d.isMutable)
return d;
var newDate = d.clone();
newDate.isDate = true;
newDate.makeImmutable();
return newDate;
}
);
}
this.refresh();
]]></body>
</method>
<method name="getDateList">
<parameter name="aCount"/>
<body><![CDATA[
var dates = new Array();
if (this.mStartDate && this.mEndDate) {
var d = this.mStartDate.clone();
while (d.compare(this.mEndDate) <= 0) {
dates.push(d.clone());
d.day += 1;
d.normalize();
}
} else if (this.mDateList) {
for each (var d in this.mDateList)
dates.push(d.clone());
}
aCount.value = dates.length;
return dates;
]]></body>
</method>
<property name="selectedDay">
<getter><![CDATA[
if (this.numVisibleDates == 1)
return this.mDateColumns[0].date;
if (this.mSelectedDayCol)
return this.mSelectedDayCol.date;
return null;
]]></getter>
<setter><![CDATA[
// ignore if just 1 visible, it's always selected,
// but we don't indicate it
if (this.numVisibleDates == 1) {
this.fireEvent("dayselect", val);
return val;
}
if (this.mSelectedDayCol) {
this.mSelectedDayCol.column.selected = false;
this.mSelectedDayCol.header.removeAttribute("selected");
}
if (val) {
this.mSelectedDayCol = this.findColumnForDate(val);
if (this.mSelectedDayCol) {
this.mSelectedDayCol.column.selected = true;
this.mSelectedDayCol.header.setAttribute("selected", "true");
} else {
dump ("XX couldn't find column to select for day: " + val + "\n");
return null;
}
}
this.fireEvent("dayselect", val);
return val;
]]></setter>
</property>
<property name="selectedItem">
<getter><![CDATA[
return this.mSelectedItem;
]]></getter>
<setter><![CDATA[
if (this.mSelectedItem != val) {
if (this.mSelectedItem) {
var cols = this.findColumnsForItem(this.mSelectedItem);
for each (col in cols) {
col.header.selectOccurrence(null);
col.column.selectOccurrence(null);
}
}
if (val) {
var cols = this.findColumnsForItem(val);
if (cols.length > 0) {
var start = val.startDate || val.entryDate;
for each (col in cols) {
if(start.isDate) {
col.header.selectOccurrence(val);
}
else {
col.column.selectOccurrence(val);
}
}
if(this.mController.selectionManager)
this.mController.selectionManager.replaceSelection(val);
} else {
val = null;
}
}
this.mSelectedItem = val;
}
return val;
]]></setter>
</property>
<property name="pixelsPerMinute">
<getter>return this.mPixPerMin</getter>
<setter>this.setPixelsPerMin(val); return val;</setter>
</property>
<property name="activeInPlaceEdit">
<getter><![CDATA[
return this.mInPlaceEditActive;
]]></getter>
<setter><![CDATA[
this.mInPlaceEditActive = val;
]]></setter>
</property>
<!-- private -->
<property name="numVisibleDates" readonly="true">
<getter><![CDATA[
if (this.mDateList)
return this.mDateList.length;
var count = 0;
var d = this.mStartDate.clone();
while (d.compare(this.mEndDate) <= 0) {
count++;
d.day += 1;
d.normalize();
}
return count;
]]></getter>
</property>
<property name="orient">
<getter>return (this.getAttribute("orient") || "vertical");</getter>
<setter>this.setAttribute("orient", val); return val;</setter>
</property>
<method name="setStartEndMinutes">
<parameter name="aStartMin"/>
<parameter name="aEndMin"/>
<body><![CDATA[
if (aStartMin < 0 || aStartMin > aEndMin)
throw Components.results.NS_ERROR_INVALID_ARG;
if (aEndMin < 0 || aEndMin > 24*60)
throw Components.results.NS_ERROR_INVALID_ARG;
if (this.mDefaultStartMin != aStartMin ||
this.mDefaultEndMin != aEndMin ||
this.mStartMin != aStartMin || this.mEndMin != aEndMin)
{
this.mDefaultStartMin = aStartMin;
this.mDefaultEndMin = aEndMin;
this.mStartMin = aStartMin;
this.mEndMin = aEndMin;
this.refresh();
}
]]></body>
</method>
<method name="setAttribute">
<parameter name="aAttr"/>
<parameter name="aVal"/>
<body><![CDATA[
var needsreorient = false;
var needsrelayout = false;
if (aAttr == "orient") {
if (this.getAttribute("orient") != aVal)
needsreorient = true;
}
if (aAttr == "context" || aAttr == "item-context")
needsrelayout = true;
// this should be done using lookupMethod(), see bug 286629
var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
if (needsrelayout && !needsreorient)
this.relayout();
if (needsreorient)
this.reorient();
return ret;
]]></body>
</method>
<method name="reorient">
<body><![CDATA[
var orient = this.getAttribute("orient");
var otherorient = "vertical";
if (!orient) orient = "horizontal";
if (orient == "vertical") otherorient = "horizontal";
if (orient == "horizontal")
this.setPixelsPerMin(1.5);
else
this.setPixelsPerMin(0.6);
var normalelems = ['mainbox', 'timebar'];
var otherelems = ['labelbox', 'labeldaybox', 'headertimespacer',
'headerbox', 'headerdaybox', 'childbox', 'daybox'];
for each (var id in normalelems) {
document.getAnonymousElementByAttribute(this, "anonid", id).setAttribute("orient", orient);
}
for each (var id in otherelems) {
document.getAnonymousElementByAttribute(this, "anonid", id).setAttribute("orient", otherorient);
}
var labelbox = document.getAnonymousElementByAttribute(this, "anonid", "labelbox");
var labeltimespacer = document.getAnonymousElementByAttribute(this, "anonid", "labeltimespacer");
var headerbox = document.getAnonymousElementByAttribute(this, "anonid", "headerbox");
var headertimespacer = document.getAnonymousElementByAttribute(this, "anonid", "headertimespacer");
var timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
var childbox = document.getAnonymousElementByAttribute(
this, "anonid", "childbox");
var mainbox = document.getAnonymousElementByAttribute(
this, "anonid", "mainbox");
if (orient == "vertical") {
headerbox.setAttribute("height", 50);
headerbox.removeAttribute("width");
labelbox.setAttribute("height", 30);
labelbox.removeAttribute("width");
var timebarWidth = 100;
timebar.setAttribute("width", timebarWidth);
timebar.removeAttribute("height");
headertimespacer.setAttribute("width", timebarWidth);
headertimespacer.removeAttribute("height");
labeltimespacer.setAttribute("width", timebarWidth);
labeltimespacer.removeAttribute("height");
childbox.setAttribute(
"style", "overflow-x: hidden; overflow-y: auto;");
mainbox.setAttribute(
"style", "overflow-x: auto; overflow-y: hidden;");
} else {
headerbox.setAttribute("width", 30);
headerbox.removeAttribute("height");
labelbox.setAttribute("width", 30);
labelbox.removeAttribute("height");
var timebarHeight = 40;
timebar.setAttribute("height", timebarHeight);
timebar.removeAttribute("width");
headertimespacer.setAttribute("height", timebarHeight);
headertimespacer.removeAttribute("width");
labeltimespacer.setAttribute("height", timebarHeight);
labeltimespacer.removeAttribute("width");
childbox.setAttribute(
"style", "overflow-x: auto; overflow-y: hidden;");
mainbox.setAttribute(
"style", "overflow-x: hidden; overflow-y: auto;");
}
var boxes = ["daybox", "headerdaybox", "labeldaybox"];
for each (var boxname in boxes) {
var box = document.getAnonymousElementByAttribute(this, "anonid", boxname);
var child = box.firstChild;
while (child) {
child.setAttribute("orient", orient);
child = child.nextSibling;
}
}
this.refresh();
]]></body>
</method>
<method name="refresh">
<body><![CDATA[
if (!this.startDate || !this.endDate)
return;
// we're throwing out all the events and starting over, we
// should reset to the default size
this.mStartMin = this.mDefaultStartMin;
this.mEndMin = this.mDefaultEndMin;
// recreate our columns
this.relayout();
if (!this.mCalendar)
return;
// start our items query; for a disjoint date range
// we get all the items, and just filter out the ones we don't
// care about in addItem
var filter = this.mCalendar.ITEM_FILTER_COMPLETED_ALL |
this.mCalendar.ITEM_FILTER_CLASS_OCCURRENCES;
if(this.mTasksInView)
filter |= this.mCalendar.ITEM_FILTER_TYPE_ALL;
else
filter |= this.mCalendar.ITEM_FILTER_TYPE_EVENT;
this.mCalendar.getItems(filter,
0,
this.startDate,
this.queryEndDate,
this.mOperationListener);
]]></body>
</method>
<method name="relayout">
<body><![CDATA[
function createXULElement(el) {
return document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", el);
}
var orient = this.getAttribute("orient");
var otherorient = "vertical";
if (!orient) orient = "horizontal";
if (orient == "vertical") otherorient = "horizontal";
var computedDateList;
if (this.mDateList) {
computedDateList = this.mDateList;
} else if (this.mStartDate && this.mEndDate) {
computedDateList = new Array();
var theDate = this.mStartDate.clone();
while (theDate.compare(this.mEndDate) <= 0) {
computedDateList.push(theDate.clone());
theDate.day += 1;
theDate.normalize();
}
}
var daybox = document.getAnonymousElementByAttribute(this, "anonid", "daybox");
var headerdaybox = document.getAnonymousElementByAttribute(this, "anonid", "headerdaybox");
var labeldaybox = document.getAnonymousElementByAttribute(this, "anonid", "labeldaybox");
if (!computedDateList || computedDateList.length == 0)
return;
// update timebar
var timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
timebar.setStartEndMinutes(this.mStartMin, this.mEndMin);
var calView = this;
function setUpDayEventsBox(aDayBox) {
aDayBox.setAttribute("class", "calendar-event-column-" + (counter % 2 == 0 ? "even" : "odd"));
aDayBox.setAttribute("context", calView.getAttribute("context"));
aDayBox.setAttribute("item-context", calView.getAttribute("item-context") || calView.getAttribute("context"));
aDayBox.startLayoutBatchChange();
aDayBox.date = d;
aDayBox.setStartEndMinutes(calView.mStartMin, calView.mEndMin);
aDayBox.setAttribute("orient", orient);
aDayBox.calendarView = calView;
}
function setUpDayHeaderBox(aDayBox) {
aDayBox.date = d;
aDayBox.calendarView = calView;
aDayBox.setAttribute("orient", orient);
}
this.mDateColumns = new Array();
// get today's date
var today = this.today();
var counter = 0;
var dayboxkids = daybox.childNodes;
var headerboxkids = headerdaybox.childNodes;
var labelboxkids = labeldaybox.childNodes;
for each (var d in computedDateList) {
var dayEventsBox;
if (counter < dayboxkids.length) {
dayEventsBox = dayboxkids[counter];
dayEventsBox.removeAttribute("today");
dayEventsBox.mEvents = new Array();
} else {
dayEventsBox = createXULElement("calendar-event-column");
dayEventsBox.setAttribute("flex", "1");
daybox.appendChild(dayEventsBox);
}
setUpDayEventsBox(dayEventsBox);
var dayHeaderBox;
if (counter < headerboxkids.length) {
dayHeaderBox = headerboxkids[counter];
dayHeaderBox.removeAttribute("today");
// Delete backwards to make sure we get them all
for (var i = dayHeaderBox.mItemBoxes.length-1; i >= 0; i--) {
dayHeaderBox.deleteEvent(dayHeaderBox.mItemBoxes[i].occurrence);
}
} else {
dayHeaderBox = createXULElement("calendar-header-container");
dayHeaderBox.setAttribute("flex", "1");
headerdaybox.appendChild(dayHeaderBox);
}
setUpDayHeaderBox(dayHeaderBox);
function matchesDayOff(dayOffNum) { return dayOffNum == d.weekday; }
if (this.mDaysOffArray.some(matchesDayOff)) {
dayEventsBox.setAttribute("weekend", "true");
dayHeaderBox.setAttribute("weekend", "true");
} else {
dayEventsBox.removeAttribute("weekend");
dayHeaderBox.removeAttribute("weekend");
}
// highlight today
if (this.numVisibleDates > 1 && d.compare(today) == 0) {
dayEventsBox.setAttribute("today", "true");
dayHeaderBox.setAttribute("today", "true");
}
var labelbox;
if (counter < labelboxkids.length) {
labelbox = labelboxkids[counter];
labelbox.firstChild.setAttribute("value", (d.month + 1) + "/" + d.day);
labelbox.childNodes[1].setAttribute("value", calGetString("dateFormat", "day."+ (d.weekday+1)+ ".Mmm"));
} else {
labelbox = createXULElement("box");
labelbox.setAttribute("flex", "1");
labelbox.setAttribute("class", "calendar-day-label-box");
var label = createXULElement("label");
label.setAttribute("value", (d.month + 1) + "/" + d.day);
label.setAttribute("class", "calendar-day-label-date");
labelbox.appendChild(label);
labeldaybox.appendChild(labelbox);
label = createXULElement("label");
label.setAttribute("value", calGetString("dateFormat", "day."+ (d.weekday+1)+ ".Mmm"));
label.setAttribute("class", "calendar-day-label-name");
labelbox.appendChild(label);
labeldaybox.appendChild(labelbox);
}
labelbox.setAttribute("orient", orient);
// We don't want to actually mess with our original dates, plus
// they're likely to be immutable.
var d2 = d.clone();
d2.isDate = true;
d2.makeImmutable();
this.mDateColumns.push ( { date: d2, column: dayEventsBox, header: dayHeaderBox } );
counter++;
}
// Remove any extra columns that may have been hanging around
function removeExtraKids(elem) {
while (counter < elem.childNodes.length) {
elem.removeChild(elem.childNodes[counter]);
}
}
removeExtraKids(daybox);
removeExtraKids(headerdaybox);
removeExtraKids(labeldaybox);
// fix pixels-per-minute
this.onResize();
for each (col in this.mDateColumns) {
col.column.endLayoutBatchChange();
}
// adjust scrollbar spacers
// XXX For performance reasons this method call can be moved to the
// XXX constructor and the reorient method as soon as the views
// XXX are constructed statically (24 hrs).
this.adjustScrollBarSpacers();
]]></body>
</method>
<method name="propagateStartEndMinutes">
<body><![CDATA[
// update timebar
var timebar = document.getAnonymousElementByAttribute(this,
"anonid",
"timebar");
timebar.setStartEndMinutes(this.mStartMin, this.mEndMin);
// fix pixels-per-minute
this.onResize();
for each (var d in this.mDateColumns) {
// each of these calls will cause the column in question
// to call relayout on itself
d.column.setStartEndMinutes(this.mStartMin, this.mEndMin);
}
return;
]]></body>
</method>
<method name="findColumnForDate">
<parameter name="aDate"/>
<body><![CDATA[
for each (var col in this.mDateColumns) {
if (col.date.compare(aDate) == 0)
return col;
}
return null;
]]></body>
</method>
<method name="findColumnsForItem">
<parameter name="aItem"/>
<body><![CDATA[
var columns = new Array();
var tz = this.mDateColumns[0].date.timezone;
// Note that these may be dates or datetimes
var startDate = aItem.startDate || aItem.entryDate;
var targetDate = startDate.getInTimezone(tz);
var endDate = aItem.endDate || aItem.dueDate;
var finishDate = endDate.getInTimezone(tz);
if (!targetDate.isDate) {
// Set the time to 00:00 so that we get all the boxes
targetDate.hour = 0;
targetDate.minute = 0;
targetDate.second = 0;
}
if (targetDate.compare(finishDate) == 0) {
// Zero length events are silly, but we have to handle them
var col = this.findColumnForDate(targetDate);
if (col) {
columns.push(col);
}
}
while (targetDate.compare(finishDate) == -1) {
var col = this.findColumnForDate(targetDate);
// This might not exist if the event spans the view start or end
if (col) {
columns.push(col);
}
targetDate.day += 1;
targetDate.normalize();
}
return columns;
]]></body>
</method>
<!-- for the given client-coord-system point, return
- the calendar-event-column that contains it. If
- no column contains it, return null.
-->
<method name="findColumnForClientPoint">
<parameter name="aClientX"/>
<parameter name="aClientY"/>
<body><![CDATA[
for each (var col in this.mDateColumns) {
var bo = col.column.topbox.boxObject;
if ((aClientX >= bo.screenX) && (aClientX < (bo.screenX + bo.width)) &&
(aClientY >= bo.screenY) && (aClientY < (bo.screenY + bo.height)))
{
return col.column;
}
}
return null;
]]></body>
</method>
<method name="doAddEvent">
<parameter name="aEvent"/>
<body><![CDATA[
//dump ("++ doAddevent\n");
var cols = this.findColumnsForItem(aEvent);
if (!cols.length)
return;
for each (col in cols) {
var column = col.column;
var header = col.header;
var estart = aEvent.startDate || aEvent.entryDate;
if (estart.isDate) {
header.addEvent(aEvent);
} else {
column.addEvent(aEvent);
}
}
]]></body>
</method>
<method name="doDeleteEvent">
<parameter name="aEvent"/>
<body><![CDATA[
var cols = this.findColumnsForItem(aEvent);
if (!cols.length)
return;
for each (col in cols) {
var column = col.column;
var header = col.header;
var estart = aEvent.startDate || aEvent.entryDate;
if (estart.isDate) {
header.deleteEvent(aEvent);
} else {
column.deleteEvent(aEvent);
}
}
]]></body>
</method>
<method name="setPixelsPerMin">
<parameter name="pixPerMin"/>
<body><![CDATA[
this.mPixPerMin = pixPerMin;
var timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
timebar.pixelsPerMinute = pixPerMin;
for each (var col in this.mDateColumns) {
col.column.pixelsPerMinute = pixPerMin;
}
]]></body>
</method>
<method name="readjustStartEndMinutes">
<parameter name="aChunks"/>
<parameter name="aIsAllChunks"/>
<body><![CDATA[
var hasChanged = false;
const MINS_PER_HOUR = 60;
const DAY_START_MIN = 0 * MINS_PER_HOUR;
const DAY_END_MIN = 24 * MINS_PER_HOUR;
// Start at the existing boundaries if aIsAllChunks is set,
// since in that case, we know that the boundaries will only
// be expanding. Otherwise, start at the defaults.
var earliestChunkStartMin =
(aIsAllChunks ? this.mDefaultStartMin : this.mStartMin);
var latestChunkEndMin =
(aIsAllChunks ? this.mDefaultEndMin : this.mEndMin);
for each (var chunk in aChunks) {
if (chunk.startMinute < earliestChunkStartMin) {
earliestChunkStartMin = chunk.startMinute;
}
if (chunk.endMinute > latestChunkEndMin) {
latestChunkEndMin = chunk.endMinute;
}
// break, if the start and end time match the day range
if (earliestChunkStartMin <= DAY_START_MIN
&& latestChunkEndMin >= DAY_END_MIN) {
break;
}
}
// round down
if (earliestChunkStartMin % MINS_PER_HOUR) {
earliestChunkStartMin -= (earliestChunkStartMin % MINS_PER_HOUR);
}
// round up
if (latestChunkEndMin % MINS_PER_HOUR) {
latestChunkEndMin += MINS_PER_HOUR -
(latestChunkEndMin % MINS_PER_HOUR);
}
// The only reason try/catch is necessary here is to work
// around XBL lossage. Without it, control never makes it
// through the try block, even though no exception is ever thrown!
try {
if (aIsAllChunks) {
// Since we've looked at all the in-view chunks,
// it's safe to either shrink or expand the time shown.
// Shrinking might be necessary when switching
// weeks, or if an event has been deleted, moved or
// shrunk out of the "non-default" area.
if (this.mStartMin != earliestChunkStartMin) {
this.mStartMin = earliestChunkStartMin;
hasChanged = true;
}
if (this.mEndMin != latestChunkEndMin) {
this.mEndMin = latestChunkEndMin;
hasChanged = true;
}
} else {
// here we are doing incremental adjustment, so we
// can only expand the time shown, never shrink it
if (this.mStartMin > earliestChunkStartMin) {
this.mStartMin = earliestChunkStartMin;
hasChanged = true;
}
if (this.mEndMin < latestChunkEndMin) {
this.mEndMin = latestChunkEndMin;
hasChanged = true;
}
}
} catch (e) {
Components.utils.reportError("Unexpected exception in "
+ "readjustStartEndMinutes: " + e);
throw e;
}
return hasChanged;
]]></body>
</method>
<method name="createChunksForNonDateItems">
<parameter name="aItems"/>
<body><![CDATA[
var chunks = new Array();
for each (var item in aItems) {
var startDate = item.startDate || item.entryDate;
if (startDate.isDate) {
continue;
}
var cols = this.findColumnsForItem(item);
if (!cols.length) {
continue;
}
// add a chunk for each date that has one
for each (var col in cols) {
chunks.push(col.column.createChunk(item));
}
}
return chunks;
]]></body>
</method>
<method name="getAllChunks">
<body><![CDATA[
var chunks = new Array();
for each (var col in this.mDateColumns) {
for each (var chunk in col.column.mEvents) {
chunks.push(chunk);
}
}
return chunks;
]]></body>
</method>
<method name="readjust">
<body><![CDATA[
if (this.readjustStartEndMinutes(this.getAllChunks(), true)) {
var selectedDay = this.selectedDay;
this.propagateStartEndMinutes();
if (selectedDay) {
this.selectedDay = selectedDay;
}
}
]]></body>
</method>
<method name="today">
<body><![CDATA[
var date = Components.classes["@mozilla.org/calendar/datetime;1"]
.createInstance(Components.interfaces.calIDateTime);
date.jsDate = new Date();
date = date.getInTimezone(this.mTimezone);
date.isDate = true;
return date;
]]></body>
</method>
<method name="adjustScrollBarSpacers">
<body><![CDATA[
// get the view's orientation
var propertyName;
if (this.getAttribute("orient") == "vertical") {
propertyName = "width";
} else {
propertyName = "height";
}
// get the width/height of the childbox scrollbar
var childbox = document.getAnonymousElementByAttribute(
this, "anonid", "childbox");
var propertyValue = childbox.boxObject.firstChild
.boxObject[propertyName];
// set the same width/height for the label and header box spacers
var headerScrollBarSpacer = document.getAnonymousElementByAttribute(
this, "anonid", "headerscrollbarspacer");
headerScrollBarSpacer.setAttribute(propertyName, propertyValue);
var labelScrollBarSpacer = document.getAnonymousElementByAttribute(
this, "anonid", "labelscrollbarspacer");
labelScrollBarSpacer.setAttribute(propertyName, propertyValue);
]]></body>
</method>
</implementation>
<handlers>
<handler event="keypress"><![CDATA[
const kKE = Components.interfaces.nsIDOMKeyEvent;
if (event.keyCode == kKE.DOM_VK_BACK_SPACE ||
event.keyCode == kKE.DOM_VK_DELETE)
{
if (!this.activeInPlaceEdit && this.selectedItem && this.controller) {
var occurrence = (event.ctrlKey) ? this.selectedItem.parentItem : this.selectedItem;
this.controller.deleteOccurrence(occurrence);
}
}
]]></handler>
<handler event="DOMAttrModified"><![CDATA[
// when a scrollbar appears/disappears in the view,
// it's disabled attribute is modified
if (event.attrName != "disabled" ||
event.originalTarget.localName != "scrollbar" ||
event.originalTarget.parentNode.getAttribute("anonid") !=
"childbox") {
return;
}
// adjust scrollbar spacers
this.adjustScrollBarSpacers();
]]></handler>
</handlers>
</binding>
</bindings>
<!-- -*- Mode: xml; indent-tabs-mode: nil; -*- -->