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

2263 lines
81 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>
-
- 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 *****
-->
<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 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 {
box = makeTimeBox(String(theHour) + ":00", 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.preventBubble();
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 preventBubble())
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>
<!-- 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;
this.recalculateStartEndMinutes();
}
return val;
]]></setter>
</property>
onget="return this.mDate;"
onset="return (this.mDate = val);" />
<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) {
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.id == aOccurrence.id &&
chunk.event.startDate.compare(aOccurrence.startDate) == 0)
{
return chunk;
}
}
return null;
]]></body>
</method>
<method name="setAttribute">
<parameter name="aAttr"/>
<parameter name="aVal"/>
<body><![CDATA[
var needsrelayout = false;
if (aAttr == "orient") {
if (this.getAttribute("orient") != aVal)
needsrelayout = 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) {
this.relayout();
}
return ret;
]]></body>
</method>
<method name="internalDeleteEvent">
<parameter name="aOccurrence"/>
<body><![CDATA[
var itemIndex = -1;
var i;
for (i = 0; i < this.mEvents.length; i++) {
occ = this.mEvents[i].event;
if (occ.id == aOccurrence.id &&
occ.startDate.compare(aOccurrence.startDate) == 0)
{
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>
<method name="getStartEndMinutesForOccurrence">
<parameter name="aOccurrence"/>
<body><![CDATA[
var stdate = aOccurrence.startDate;
var enddate = aOccurrence.endDate;
if (stdate.timezone != this.mTimezone)
stdate = stdate.getInTimezone (this.mTimezone);
if (enddate.timezone != this.mTimezone)
enddate = enddate.getInTimezone (this.mTimezone);
return { start: stdate.hour * 60 + stdate.minute,
end: enddate.hour * 60 + enddate.minute };
]]></body>
</method>
<method name="addEvent">
<parameter name="aOccurrence"/>
<body><![CDATA[
this.internalDeleteEvent(aOccurrence);
var mins = this.getStartEndMinutesForOccurrence(aOccurrence);
var chunk = {
startMinute: mins.start,
endMinute: mins.end,
event: aOccurrence
};
this.mEvents.push(chunk);
this.relayout();
]]></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.lastChild)
this.bgbox.removeChild(this.bgbox.lastChild);
while (this.topbox.lastChild)
this.topbox.removeChild(this.topbox.lastChild);
]]></body>
</method>
<method name="relayout">
<body><![CDATA[
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);
// chunkBox.setAttribute("style", "background: #eeeeff; padding: 0px 1px 0px 1px");
// chunkBox.setAttribute("flex", "1");
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;
chunkBox.setAttribute("item-calendar", chunk.event.event.calendar.uri.spec);
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 (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.startDate.isDate)
// continue;
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;
// 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;
}
}
// XXX ok, yeah, this stuff needs some JS helper love
var estart = col.mDate.clone();
estart.isDate = false;
estart.hour = 0;
estart.minute = dragState.startMin + col.mStartMin;
estart.normalize();
var eend = col.mDate.clone();
eend.isDate = false;
eend.hour = 0;
eend.minute = dragState.endMin + col.mStartMin;
eend.normalize();
if (dragState.dragType == "new") {
col.calendarView.controller.createNewEvent(col.calendarView.displayCalendar,
estart,
eend);
} else if (dragState.dragType == "move" ||
dragState.dragType == "modify-start" ||
dragState.dragType == "modify-end")
{
col.calendarView.controller.modifyOccurrence(dragState.dragOccurrence, estart, eend);
}
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;
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");
// cheat and call the sweep move event handler;
// this will take care of setting up the sizes right for everything
document.calendarEventColumnDragging = this;
//this.onEventSweepMouseMove(event);
//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;
this.fgboxes.startlabel.setAttribute("value", starthr + ":" + (startmin < 10 ? "0" : "") + startmin);
this.fgboxes.endlabel.setAttribute("value", endhr + ":" + (endmin < 10 ? "0" : "") + endmin);
]]></body>
</method>
</implementation>
<handlers>
<handler event="dblclick"><![CDATA[
if (this.calendarView.controller) {
this.calendarView.controller.createNewEvent (this.mCalendarView.displayCalendar,
this.mDate);
}
]]></handler>
<!-- mouse down handler, in empty event column regions. Starts sweeping out a new
- event.
-->
<handler event="mousedown" button="0"><![CDATA[
// select this column
this.calendarView.selectedDay = this.mDate;
// 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);
}
this.fgboxes.box.setAttribute("dragging", "true");
this.fgboxes.dragbox.setAttribute("dragging", "true");
// cheat and call the sweep move event handler;
// this will take care of setting up the sizes right for everything
document.calendarEventColumnDragging = this;
this.onEventSweepMouseMove(event);
window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
]]></handler>
</handlers>
</binding>
<!--
- An individual event box, to be inserted into a column.
-->
<binding id="calendar-event-box">
<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() { self.stopEditing(true); };
this.eventNameInput.onkeypress = function(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="mOccurrence">null</field>
<field name="mParentColumn">null</field>
<field name="mCalendarView">null</field>
<field name="mSelected">null</field>
<property name="calendarView"
onget="return this.mCalendarView;"
onset="return (this.mCalendarView = val);" />
<!-- 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-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="eventNameLabel" readonly="true"
onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'event-name');"/>
<property name="eventNameTextbox" readonly="true"
onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'event-name-textbox');"/>
<property name="eventNameInput" readonly="true"
onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'event-name-textbox').inputField;"/>
<property name="occurrence">
<getter><![CDATA[
return this.mOccurrence;
]]></getter>
<setter><![CDATA[
this.mOccurrence = val;
var evl = this.eventNameLabel;
while (evl.firstChild)
evl.removeChild(evl.firstChild);
if (val && val != "")
evl.appendChild(document.createTextNode(val.title));
else
evl.appendChild(document.createTextNode("Untitled Event"));
return val;
]]></setter>
</property>
<property name="selected">
<getter><![CDATA[
return this.mSelected;
]]></getter>
<setter><![CDATA[
if (val && !this.mSelected) {
this.mSelected = true;
this.setAttribute("selected", "true");
} else if (!val && this.mSelected) {
this.mSelected = null;
this.removeAttribute("selected");
}
return val;
]]></setter>
</property>
<property name="parentColumn"
onget="return this.mParentColumn;"
onset="return (this.mParentColumn = val);"/>
<property name="startMinute" readonly="true"
onget="if (!this.mOccurrence) return 0; return this.mOccurrence.startDate.hour * 60 + this.mOccurrence.startDate.minute"/>
<property name="endMinute" readonly="true"
onget="if (!this.mOccurrence) return 0; return this.mOccurrence.endDate.hour * 60 + this.mOccurrence.endDate.minute"/>
<method name="startEditing">
<body><![CDATA[
this.editingTimer = null;
this.mOriginalTextLabel = this.mOccurrence.title;
this.eventNameLabel.setAttribute("hidden", "true");
this.mEditing = true;
this.eventNameTextbox.value = this.mOriginalTextLabel;
this.eventNameTextbox.removeAttribute("hidden");
if (this.calendarView)
this.calendarView.activeInPlaceEdit = true;
this.eventNameInput.focus();
this.eventNameInput.select();
]]></body>
</method>
<method name="stopEditing">
<parameter name="saveChanges"/>
<body><![CDATA[
this.mEditing = false;
if (this.calendarView)
this.calendarView.activeInPlaceEdit = false;
if (!saveChanges) {
this.eventNameTextbox.setAttribute("hidden", "true");
this.eventNameLabel.removeAttribute("hidden");
return;
}
if (this.eventNameTextbox.value != this.mOriginalTextLabel) {
var clone = this.mOccurrence.clone();
clone.title = this.eventNameTextbox.value;
clone.calendar.modifyItem(clone, this.mOccurrence, null);
// Note that as soon as we do the modifyItem, this element ceases to exist,
// so don't bother trying to modify anything further here! ('this' exists,
// because it's being kept alive, but our child content etc. is all gone)
}
]]></body>
</method>
</implementation>
<handlers>
<handler event="click"><![CDATA[
var evTime = Date.now();
if (this.mMouseDownTime && (evTime - this.mMouseDownTime > 1000)) {
// not even a click!
return;
}
if (this.mLastClickTime && (evTime - this.mLastClickTime < 350)) {
// double click
if (this.editingTimer) {
clearTimeout(this.editingTimer);
this.editingTimer = null;
}
if (this.calendarView.controller && this.mOccurrence) {
var occurrence = (event.ctrlKey) ? this.mOccurrence.parentItem : this.mOccurrence;
this.calendarView.controller.modifyOccurrence(occurrence);
}
} else {
// start single click timeout
if (this.selected) {
var self = this;
if (this.editingTimer) clearTimeout(this.editingTimer);
this.editingTimer = setTimeout(function () { self.startEditing(); }, 350);
} else {
this.calendarView.selectedItem = this.mOccurrence;
}
this.mLastClickTime = evTime;
}
]]></handler>
<handler event="mousedown"><![CDATA[
event.preventBubble();
if (this.mEditing)
return;
this.mInMouseDown = true;
this.mMouseX = event.screenX;
this.mMouseY = event.screenY;
this.mMouseDownTime = Date.now();
]]></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>
</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;" label="R"
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" flex="1" equalsize="always" />
</xul:box>
<xul:box anonid="headerbox">
<xul:box anonid="headertimespacer"/>
<xul:box anonid="headerdaybox" flex="1" equalsize="always" />
</xul:box>
<xul:box 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" flex="1" equalsize="always" />
</xul:box>
</xul:box>
</content>
<implementation implements="calICalendarView">
<field name="mResizeHandler">null</field>
<constructor><![CDATA[
var self = this;
this.mResizeHandler = function() { self.onResize(); };
window.addEventListener("resize", this.mResizeHandler, true);
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;
self.pixelsPerMinute = Math.round(ppm * 10) / 10;
]]></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="mSelectedItem">null</field>
<field name="mSelectedDayCol">null</field>
<field name="mStartMin">8*60</field>
<field name="mEndMin">20*60</field>
<field name="mObserver"><![CDATA[
// the calIObserver, and calICompositeObserver
({
QueryInterface: function (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() {
this.calView.mBatchCount++;
},
onEndBatch: function() {
this.mBatchCount--;
if (this.mBatchCount == 0) {
this.calView.refresh();
}
},
onLoad: function() {
this.calView.refresh();
},
onAddItem: function (aItem) {
//dump ("++ AddItem " + aItem + "\n");
if (!(aItem instanceof Components.interfaces.calIEvent))
return;
aItem = aItem.QueryInterface(Components.interfaces.calIEvent);
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);
}
},
onModifyItem: function (aNewItem, aOldItem) {
//dump ("++ ModifyItem\n");
if (!(aOldItem instanceof Components.interfaces.calIEvent))
return;
if (!(aNewItem instanceof Components.interfaces.calIEvent))
return;
var occs;
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);
}
},
onDeleteItem: function (aItem) {
//dump ("++ DeleteItem\n");
if (!(aItem instanceof Components.interfaces.calIEvent))
return;
var occs = aItem.getOccurrencesBetween(this.calView.startDate,
this.calView.queryEndDate,
{});
for each (var occ in occs) {
this.calView.doDeleteEvent(occ);
}
},
//XXXvv Alarm could, in theory, flash the event or something
onAlarm: function (aAlarmItem) { },
onError: function (aErrNo, aMessage) { },
//
// calICompositeObserver stuff
// XXXvv we can be smarter about how we handle this stuff
//
onCalendarAdded: function (aCalendar) {
//dump ("view onCalendarAdded\n");
this.calView.refresh();
},
onCalendarRemoved: function (aCalendar) {
//dump ("view onCalendarRemoved\n");
this.calView.refresh();
},
onDefaultCalendarChanged: function (aNewDefaultCalendar) {
// don't care, for now
}
})
]]></field>
<field name="mOperationListener"><![CDATA[
({
calView: this,
onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
// nothing to do
//dump ("+++ OnOperationComplete (detail: " + aDetail + ")\n");
},
onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
if (!Components.isSuccessCode(aStatus))
return;
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>
<method name="showDate">
<parameter name="aDate"/>
<body><![CDATA[
var targetDate = aDate.clone();
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 = aDate;
]]></body>
</method>
<method name="setDateRange">
<parameter name="aStartDate"/>
<parameter name="aEndDate"/>
<body><![CDATA[
//dump ("setDateRange\n");
this.mDateList = null;
this.mStartDate = aStartDate.clone();
this.mStartDate.makeImmutable();
this.mEndDate = aEndDate.clone();
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 (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)
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;
}
}
return val;
]]></setter>
</property>
<property name="selectedItem">
<getter><![CDATA[
return this.mSelectedItem;
]]></getter>
<setter><![CDATA[
if (this.mSelectedItem != val) {
if (this.mSelectedItem) {
var col = this.findColumnForEvent(this.mSelectedItem);
if (col) {
col.column.selectOccurrence(null);
} else {
dump ("Thought I had a selected occurrence (id: " + this.mSelectedItem.id + "), but couldn't find a column for it!\n");
}
}
if (val) {
var col = this.findColumnForEvent(val);
if (col)
col.column.selectOccurrence(val);
else
val = null;
}
return (this.mSelectedItem = 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.mStartMin != aStartMin ||
this.mEndMin != aEndMin)
{
this.mStartMin = aStartMin;
this.mEndMin = aEndMin;
this.relayout();
}
]]></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', '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");
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");
} 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");
}
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.relayout();
]]></body>
</method>
<method name="refresh">
<body><![CDATA[
if (!this.startDate || !this.endDate)
return;
// recreate our columns as necessary
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
this.mCalendar.getItems(this.mCalendar.ITEM_FILTER_COMPLETED_ALL |
this.mCalendar.ITEM_FILTER_TYPE_EVENT |
this.mCalendar.ITEM_FILTER_CLASS_OCCURRENCES,
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");
while (daybox.lastChild)
daybox.removeChild(daybox.lastChild);
while (headerdaybox.lastChild)
headerdaybox.removeChild(headerdaybox.lastChild);
while (labeldaybox.lastChild)
labeldaybox.removeChild(labeldaybox.lastChild);
this.mDateColumns = new Array();
if (!computedDateList || computedDateList.length == 0)
return;
// update timebar
var timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
timebar.setStartEndMinutes(this.mStartMin, this.mEndMin);
var counter = 0;
for each (var d in computedDateList) {
var dayEventsBox = createXULElement("calendar-event-column");
dayEventsBox.setAttribute("flex", "1");
dayEventsBox.setAttribute("class", "calendar-event-column-" + (counter % 2 == 0 ? "even" : "odd"));
dayEventsBox.setAttribute("context", this.getAttribute("context"));
dayEventsBox.setAttribute("item-context", this.getAttribute("item-context") || this.getAttribute("context"));
daybox.appendChild(dayEventsBox);
dayEventsBox.setStartEndMinutes(this.mStartMin, this.mEndMin);
dayEventsBox.setAttribute("orient", orient);
dayEventsBox.date = d;
dayEventsBox.calendarView = this;
var dayHeaderBox = createXULElement("box");
dayHeaderBox.setAttribute("class", "calendar-event-column-header");
dayHeaderBox.setAttribute("flex", "1");
headerdaybox.appendChild(dayHeaderBox);
dayHeaderBox.setAttribute("orient", orient);
var labelbox = createXULElement("box");
labelbox.setAttribute("flex", "1");
labelbox.setAttribute("orient", orient);
labelbox.setAttribute("class", "calendar-day-label-box");
var label;
label = createXULElement("label");
//label.setAttribute("flex", "1");
label.setAttribute("value", (d.month + 1) + "/" + d.day);
label.setAttribute("class", "calendar-day-label-date");
labelbox.appendChild(label);
// XXX Localize me, get me out of here
var dayNames = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ];
var dayNamesShort = [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ];
label = createXULElement("label");
//label.setAttribute("flex", "1");
label.setAttribute("value", dayNamesShort[d.weekday]);
label.setAttribute("class", "calendar-day-label-name");
labelbox.appendChild(label);
labeldaybox.appendChild(labelbox);
d.isDate = true;
this.mDateColumns.push ( { date: d, column: dayEventsBox, header: dayHeaderBox } );
}
// fix pixels-per-minute
this.onResize();
]]></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="findColumnForEvent">
<parameter name="aEvent"/>
<body><![CDATA[
return this.findColumnForDate(aEvent.startDate);
]]></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 col = this.findColumnForEvent(aEvent);
if (!col)
return;
var column = col.column;
var header = col.header;
var estart = aEvent.startDate;
if (estart.isDate) {
// add it to header
} else {
column.addEvent(aEvent);
}
]]></body>
</method>
<method name="doDeleteEvent">
<parameter name="aEvent"/>
<body><![CDATA[
var col = this.findColumnForEvent(aEvent);
if (!col)
return;
var column = col.column;
var header = col.header;
var estart = aEvent.startDate;
if (estart.isDate) {
// remove from header
} else {
column.deleteEvent(aEvent);
}
]]></body>
</method>
<method name="setPixelsPerMin">
<parameter name="pixPerMin"/>
<body><![CDATA[
var timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
timebar.pixelsPerMinute = pixPerMin;
for each (var col in this.mDateColumns) {
col.column.pixelsPerMinute = pixPerMin;
}
]]></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>
</handlers>
</binding>
</bindings>
<!-- -*- Mode: xml; indent-tabs-mode: nil; -*- -->