releases-comm-central/calendar/base/content/calendar-multiday-view.xml

3264 строки
129 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<!--
- ***** 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>
- Michael Buettner <michael.buettner@sun.com>
- Philipp Kewisch <mozilla@kewis.ch>
- Markus Adrario <MarkusAdrario@web.de>
- Berend Cornelius <berend.cornelius@sun.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 calUtils.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">0*60</field>
<field name="mEndMin">24*60</field>
<field name="mDayStartHour">0</field>
<field name="mDayEndHour">24</field>
<constructor>
this.relayout();
</constructor>
<method name="setDayStartEndHours">
<parameter name="aDayStartHour"/>
<parameter name="aDayEndHour"/>
<body><![CDATA[
if (aDayStartHour * 60 < this.mStartMin ||
aDayStartHour > aDayEndHour ||
aDayEndHour * 60 > this.mEndMin) {
throw Components.results.NS_ERROR_INVALID_ARG;
}
if (this.mDayStartHour != aDayStartHour ||
this.mDayEndHour != aDayEndHour) {
this.mDayEndHour = aDayEndHour;
this.mDayStartHour = aDayStartHour;
var topbox = document.getAnonymousElementByAttribute(this, "anonid", "topbox");
if (topbox.childNodes.length) {
// This only needs to be done if the initial relayout has
// already happened, otherwise it will be done then.
for (var hour = this.mStartMin / 60; hour < this.mEndMin / 60; hour++) {
if (hour < this.mDayStartHour || hour > this.mDayEndHour) {
topbox.childNodes[hour].setAttribute("off-time", "true");
} else {
topbox.childNodes[hour].removeAttribute("off-time");
}
}
}
}
]]></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;
}
// 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="relayout">
<body><![CDATA[
//dump ("calendar-time-bar: relayout\n");
var topbox = document.getAnonymousElementByAttribute(this, "anonid", "topbox");
var orient = topbox.getAttribute("orient");
var otherorient = getOtherOrientation(orient);
//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;
// calculate duration pixel as the difference between
// start pixel and end pixel to avoid rounding errors.
var startPix = Math.round(theMin * this.mPixPerMin);
var endPix = Math.round((theMin + dur) * this.mPixPerMin);
var durPix = endPix - startPix;
var box;
if (dur != 60) {
box = makeTimeBox("", durPix);
} else {
timeString = formatter.FormatTime("",
Components.interfaces.nsIScriptableDateFormat.timeFormatNoSeconds,
theHour, 0, 0);
box = makeTimeBox(timeString, durPix);
}
// Set up workweek hours
if (theHour < this.mDayStartHour || theHour >= this.mDayEndHour) {
box.setAttribute("off-time", "true");
}
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"
pack="center"
xbl:inherits="align=whichside">
<xul:image xbl:inherits="class"/>
</xul:box>
</content>
<implementation>
<property name="parentorient">
<getter><![CDATA[
return this.getAttribute("parentorient");
]]></getter>
<setter><![CDATA[
var thebox = document.getAnonymousElementByAttribute(this, "anonid", "thebox");
this.setAttribute("parentorient", val);
thebox.setAttribute("orient", getOtherOrientation(val));
return val;
]]></setter>
</property>
<!-- private -->
<constructor><![CDATA[
this.parentorient = this.getAttribute("parentorient");
]]></constructor>
</implementation>
<handlers>
<handler event="mousedown" button="0"><![CDATA[
// store the attribute 'whichside' in the event object
// but *don't* call stopPropagation(). as soon as the
// enclosing event box will receive the event it will
// make use of this information in order to invoke the
// appropriate action.
event.whichside = this.getAttribute("whichside");
]]></handler>
<handler event="click" button="0"><![CDATA[
event.stopPropagation();
]]></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" mousethrough="always"/>
<xul:box anonid="fgbox" flex="1" class="fgdragcontainer" style="min-width: 1px; min-height: 1px; overflow:hidden;">
<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>
<xul:calendar-event-box anonid="config-box" hidden="true" xbl:inherits="orient"/>
</content>
<implementation>
<constructor><![CDATA[
this.mEventInfos = Array();
this.mTimezone = UTC();
]]></constructor>
<!-- fields -->
<field name="mPixPerMin">0.6</field>
<field name="mStartMin">0*60</field>
<field name="mEndMin">24*60</field>
<field name="mDayStartMin">8*60</field>
<field name="mDayEndMin">17*60</field>
<!--an array of objects that contain information about the events that are to be
displayed. The contained fields are:
event: The event that is to be displayed in a 'calendar-event-box'
layoutStart: The 'start'-datetime object of the event in the timezone of the view
layoutEnd: The 'end'-datetime object of the event in the timezone of the view.
The 'layoutEnd' may be different from the real 'end' time of the
event because it considers a certain minimum duration of the event
that is basically dependent of the font-size of the event-box label -->
<field name="mEventInfos">new Array()</field>
<field name="mEventMap">null</field>
<field name="mCalendarView">null</field>
<field name="mDate">null</field>
<field name="mTimezone">null</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>
<!-- Sometimes we need to add resize handlers for columns with special
widths. When we relayout, we need to cancel those handlers -->
<field name="mHandlersToRemove">new Array()</field>
<!-- Set this true so that we know in our onAddItem listener to start
- modifying an event when it comes back to us as created
-->
<field name="mCreatedNewEvent">false</field>
<field name="mEventToEdit">null</field>
<field name="mSelectedItemIds">new Object()</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();
}
return val;
]]></setter>
</property>
<field name="mSelected">false</field>
<property name="selected">
<getter><![CDATA[
return this.mSelected;
]]></getter>
<setter><![CDATA[
this.mSelected = val;
if (this.bgbox && this.bgbox.hasChildNodes()) {
var child = this.bgbox.firstChild;
while (child) {
if (val) {
child.setAttribute("selected", "true");
} else {
child.removeAttribute("selected");
}
child = child.nextSibling;
}
}
return val;
]]></setter>
</property>
<property name="date">
<getter><![CDATA[
return this.mDate;
]]></getter>
<setter><![CDATA[
this.mDate = val;
if (!compareObjects(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>
<field name="mMinDuration">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.methods"/>
<field name="mDayOff">false</field>
<property name="dayOff">
<getter><![CDATA[
return this.mDayOff;
]]></getter>
<setter><![CDATA[
this.mDayOff = val;
return val;
]]></setter>
</property>
<!-- mEventInfos -->
<field name="mSelectedChunks">[]</field>
<method name="selectOccurrence">
<parameter name="aOccurrence"/>
<body><![CDATA[
if (aOccurrence) {
this.mSelectedItemIds[aOccurrence.hashId] = true;
var chunk = this.findChunkForOccurrence(aOccurrence);
if (!chunk) {
dump("++ Couldn't find chunk to select!!!\n");
return;
}
chunk.selected = true;
this.mSelectedChunks.push(chunk);
}
]]></body>
</method>
<method name="unselectOccurrence">
<parameter name="aOccurrence"/>
<body><![CDATA[
if (aOccurrence) {
delete this.mSelectedItemIds[aOccurrence.hashId];
var chunk = this.findChunkForOccurrence(aOccurrence);
if (!chunk) {
dump ("++ Couldn't find chunk to unselect!!!\n");
return;
}
chunk.selected = false;
var index = this.mSelectedChunks.indexOf(chunk);
this.mSelectedChunks.splice(index, 1);
}
]]></body>
</method>
<method name="findChunkForOccurrence">
<parameter name="aOccurrence"/>
<body><![CDATA[
for each (var chunk in this.mEventBoxes) {
if (chunk.occurrence.hashId == aOccurrence.hashId) {
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 occ;
for (var i in this.mEventInfos) {
occ = this.mEventInfos[i].event;
if (occ.hashId == aOccurrence.hashId)
{
itemIndex = i;
break;
}
}
if (itemIndex != -1) {
delete this.mSelectedItemIds[occ.hashId];
function isNotItem(a) {
return a.occurrence.hashId != aOccurrence.hashId;
}
this.mSelectedChunks = this.mSelectedChunks.filter(isNotItem);
this.mEventInfos.splice(itemIndex, 1);
return true;
} else {
return false;
}
]]></body>
</method>
<method name="recalculateStartEndMinutes">
<body><![CDATA[
for each (var chunk in this.mEventInfos) {
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 (!compareObjects(stdate.timezone, this.mTimezone)) {
stdate = stdate.getInTimezone (this.mTimezone);
}
if (!compareObjects(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.mEventInfos.push(chunk);
if (this.mEventMapTimeout) {
clearTimeout(this.mEventMapTimeout);
}
var column = this;
if (this.mCreatedNewEvent) {
this.mEventToEdit = aOccurrence;
}
// 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);
for each (handler in this.mHandlersToRemove){
this.calendarView.viewBroadcaster.removeEventListener(this.calendarView.getAttribute("type") + "viewresized", handler, true);
}
this.mHandlersToRemove = [];
]]></body>
</method>
<method name="relayout">
<body><![CDATA[
if (this.mLayoutBatchCount > 0)
return;
this.clear();
var orient = this.getAttribute("orient");
this.bgbox.setAttribute("orient", orient);
// bgbox is used mainly for drawing the grid. at some point it may
// also be used for all-day events.
var otherorient = getOtherOrientation(orient);
var configBox = document.getAnonymousElementByAttribute(this, "anonid", "config-box");
configBox.removeAttribute("hidden");
var minSize = configBox.getOptimalMinSize();
configBox.setAttribute("hidden", "true");
this.mMinDuration = Components.classes["@mozilla.org/calendar/duration;1"]
.createInstance(Components.interfaces.calIDuration);
this.mMinDuration.minutes = parseInt(minSize/this.mPixPerMin);
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 (this.mSelected) {
box.setAttribute("selected", "true");
}
if (this.mDayOff) {
box.setAttribute("weekend", "true");
}
if (theMin < this.mDayStartMin || theMin >= this.mDayEndMin) {
box.setAttribute("off-time", "true");
}
// Carry forth the day relation
box.setAttribute("relation", this.getAttribute("relation"));
// calculate duration pixel as the difference between
// start pixel and end pixel to avoid rounding errors.
var startPix = Math.round(theMin * this.mPixPerMin);
var endPix = Math.round((theMin + dur) * this.mPixPerMin);
var durPix = endPix - startPix;
if (orient == "vertical")
box.setAttribute("height", durPix);
else
box.setAttribute("width", durPix);
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();
this.mEventBoxes = new Array();
if (!this.mEventMap.length) {
return;
}
// First of all we create a xul:stack which
// will hold all events for this event column.
// The stack will be grouped below .../calendar-event-column/stack/topbox.
var stack = createXULElement("stack");
stack.setAttribute("flex", "1");
this.topbox.appendChild(stack);
var boxToEdit;
for each (var layer in this.mEventMap) {
// The event-map (this.mEventMap) contains an array of layers.
// For each layer we create a box below the stack just created above.
// So each different layer lives in a box that's contained in the stack.
var xulColumn = createXULElement("box");
xulColumn.setAttribute("orient", otherorient);
xulColumn.setAttribute("flex", "1");
xulColumn.setAttribute("style", "min-width: 1px; min-height: 1px;");
stack.appendChild(xulColumn);
var numBlocksInserted = 0;
// Each layer contains a list of the columns that
// need to be created for a span.
for each (var column in layer) {
var innerColumn = createXULElement("box");
innerColumn.setAttribute("orient", orient);
innerColumn.setAttribute("flex", 1);
var style = "min-width: 1px; min-height: 1px;";
if (column.specialSpan) {
// Special case when we can't simply rely on flex. Only
// happens when we have columns in the layer that need to
// be different sizes. That is, when we have a colSpan
// of 2, a total of 5 columns, and a startCol of 1.
// Then, our columns need to be laid out as 1/5, 2/5, 2/5.
if (orient == "vertical") {
style += "max-width: " +
column.specialSpan * this.topbox.boxObject.width +
"px;";
} else {
style += "max-height: " +
column.specialSpan * this.topbox.boxObject.height +
"px;";
}
// Now we need to set up a resize listener, since without
// it our box will look funny if the window resizes. This
// requires us to be *very* careful about closures, because
// we don't want things like column.specialSpan to change
function colResizeHandler(aInnerCol, aCalCol, aSpan) {
this.handleEvent = function(aEvent) {
var resizeStyle = "min-width: 1px; min-height: 1px;";
if (orient == "vertical") {
resizeStyle += "max-width: " +
aSpan * aCalCol.topbox.boxObject.width +
"px;";
} else {
resizeStyle += "max-height: " +
aSpan * aCalCol.topbox.boxObject.height +
"px;";
}
aInnerCol.setAttribute("style", resizeStyle);
};
}
var myResizeHandler = new colResizeHandler(innerColumn, this, column.specialSpan);
this.mHandlersToRemove.push(myResizeHandler);
this.calendarView.viewBroadcaster.addEventListener(this.calendarView.getAttribute("type") + "viewresized", myResizeHandler, true);
}
innerColumn.setAttribute("style", style);
xulColumn.appendChild(innerColumn);
var curTime = 0;
for each (var chunk in column) {
var duration = chunk.duration;
if (!duration) {
continue;
}
if (chunk.event) {
var chunkBox = createXULElement("calendar-event-box");
var durMinutes = duration.inSeconds / 60;
var size = Math.max(durMinutes * this.mPixPerMin, minSize);
if (orient == "vertical") {
chunkBox.setAttribute("height", size);
} else {
chunkBox.setAttribute("width", size);
}
chunkBox.setAttribute("context",
this.getAttribute("item-context") ||
this.getAttribute("context"));
chunkBox.setAttribute("orient", orient);
innerColumn.appendChild(chunkBox);
chunkBox.calendarView = this.calendarView;
chunkBox.occurrence = chunk.event;
chunkBox.parentColumn = this;
if (chunk.event.hashId in this.mSelectedItemIds) {
chunkBox.selected = true;
}
this.mEventBoxes.push(chunkBox);
if (this.mEventToEdit &&
chunkBox.occurrence.hashId == this.mEventToEdit.hashId) {
boxToEdit = 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");
innerColumn.appendChild(chunkBox);
var durMinutes = duration.inSeconds / 60;
if (orient == "vertical") {
chunkBox.setAttribute("height", durMinutes * this.mPixPerMin);
} else {
chunkBox.setAttribute("width", durMinutes * this.mPixPerMin);
}
}
}
numBlocksInserted++;
curTime += duration;
}
if (boxToEdit) {
this.mCreatedNewEvent = false;
this.mEventToEdit = null;
boxToEdit.startEditing();
}
if (numBlocksInserted == 0) {
// if we didn't insert any blocks, then
// forget about this column
stack.removeChild(xulColumn);
}
}
]]></body>
</method>
<method name="computeEventMap">
<body><![CDATA[
/* We're going to create a series of 'blobs'. A blob is a series of
* events that create a continuous block of busy time. In other
* words, a blob ends when there is some time such that no events
* occupy that time.
*
* Each blob will be an array of objects with the following properties:
* item: the event/task
* startCol: the starting column to display the event in (0-indexed)
* colSpan: the number of columns the item spans
*
* An item with no conflicts will have startCol: 0 and colSpan: 1.
*/
var blobs = new Array();
var currentBlob = new Array();
function sortByStart(aEventInfo, bEventInfo) {
// If you pass in tasks without both entry and due dates, I will
// kill you
var startComparison = aEventInfo.layoutStart.compare(bEventInfo.layoutStart);
if (startComparison != 0) {
return startComparison;
} else {
// If the items start at the same time, return the longer one
// first
return bEventInfo.layoutEnd.compare(aEventInfo.layoutEnd);
}
}
var self = this;
this.mEventInfos.forEach(function(aEventInfo) {
let item = aEventInfo.event.clone();
let start = item.startDate || item.entryDate;
start = start.getInTimezone(self.mTimezone);
aEventInfo.layoutStart = start;
let end = item.endDate || item.dueDate
end = end.getInTimezone(self.mTimezone);
let secEnd = start.clone();
secEnd.addDuration(self.mMinDuration);
if (secEnd.nativeTime > end.nativeTime) {
aEventInfo.layoutEnd = secEnd;
} else {
aEventInfo.layoutEnd = end;
}
return aEventInfo;
});
this.mEventInfos.sort(sortByStart);
// The end time of the last ending event in the entire blob
var latestItemEnd;
// This array keeps track of the last (latest ending) item in each of
// the columns of the current blob. We could reconstruct this data at
// any time by looking at the items in the blob, but that would hurt
// perf.
var colEndArray = new Array();
/* Go through a 3 step process to try and place each item.
* Step 1: Look for an existing column with room for the item.
* Step 2: Look for a previously placed item that can be shrunk in
* width to make room for the item.
* Step 3: Give up and create a new column for the item.
*
* (The steps are explained in more detail as we come to them)
*/
for (var i in this.mEventInfos) {
var curItemInfo = {event: this.mEventInfos[i].event,
layoutStart: this.mEventInfos[i].layoutStart,
layoutEnd: this.mEventInfos[i].layoutEnd};
if (!latestItemEnd) {
latestItemEnd = curItemInfo.layoutEnd;
}
if (currentBlob.length && latestItemEnd &&
curItemInfo.layoutStart.compare(latestItemEnd) != -1) {
// We're done with this current blob because item starts
// after the last event in the current blob ended.
blobs.push({blob: currentBlob, totalCols: colEndArray.length});
// Reset our variables
currentBlob = new Array();
colEndArray = new Array();
}
// Place the item in its correct place in the blob
var placedItem = false;
// Step 1
// Look for a possible column in the blob that has been left open. This
// would happen if we already have multiple columns but some of
// the cols have events before latestItemEnd. For instance
// | | |
// |______| |
// |ev1 |______|
// | |ev2 |
// |______| |
// | | |
// |OPEN! | |<--Our item's start time might be here
// | |______|
// | | |
//
// Remember that any time we're starting a new blob, colEndArray
// will be empty, but that's ok.
for (var ii = 0; ii < colEndArray.length; ++ii) {
var colStart = colEndArray[ii].layoutStart;
var colEnd = colEndArray[ii].layoutEnd;
if (colEnd.compare(curItemInfo.layoutStart) != 1) {
// Yay, we can jump into this column
colEndArray[ii] = curItemInfo;
// Check and see if there are any adjacent columns we can
// jump into as well.
var lastCol = Number(ii) + 1;
while (lastCol < colEndArray.length) {
var nextColStart = colEndArray[lastCol].layoutStart;
var nextColEnd = colEndArray[lastCol].layoutEnd;
// If the next column's item ends after we start, we
// can't expand any further
if (nextColEnd.compare(curItemInfo.layoutStart) == 1) {
break;
}
colEndArray[lastCol] = curItemInfo;
lastCol++;
}
// Now construct the info we need to push into the blob
currentBlob.push({itemInfo: curItemInfo,
startCol: ii,
colSpan: lastCol - ii});
// Update latestItemEnd
if (latestItemEnd &&
curItemInfo.layoutEnd.compare(latestItemEnd) == 1) {
latestItemEnd = curItemInfo.layoutEnd;
}
placedItem = true;
break; // Stop iterating through colEndArray
}
}
if (placedItem) {
// Go get the next item
continue;
}
// Step 2
// OK, all columns (if there are any) overlap us. Look if the
// last item in any of the last items in those columns is taking
// up 2 or more cols. If so, shrink it and stick the item in the
// created space. For instance
// |______|______|______|
// |ev1 |ev3 |ev4 |
// | | | |
// | |______| |
// | | |______|
// | |_____________|
// | |ev2 |
// |______| |<--If our item's start time is
// | |_____________| here, we can shrink ev2 and jump
// | | | | in column #3
//
for (var jj=1; jj<colEndArray.length; ++jj) {
if (colEndArray[jj].event.hashId == colEndArray[jj-1].event.hashId) {
// Good we found a item that spanned multiple columns.
// Find it in the blob so we can modify its properties
for (var kk in currentBlob) {
if (currentBlob[kk].itemInfo.event.hashId == colEndArray[jj].event.hashId) {
// Take all but the first spot that the item spanned
var spanOfShrunkItem = currentBlob[kk].colSpan;
currentBlob.push({itemInfo: curItemInfo,
startCol: Number(currentBlob[kk].startCol) + 1,
colSpan: spanOfShrunkItem - 1});
// Update colEndArray
for (var ll = jj; ll < jj + spanOfShrunkItem - 1; ll++) {
colEndArray[ll] = curItemInfo;
}
// Modify the data on the old item
currentBlob[kk] = {itemInfo: currentBlob[kk].itemInfo,
startCol: currentBlob[kk].startCol,
colSpan: 1};
// Update latestItemEnd
if (latestItemEnd &&
curItemInfo.layoutEnd.compare(latestItemEnd) == 1) {
latestItemEnd = curItemInfo.layoutEnd;
}
break; // Stop iterating through currentBlob
}
}
placedItem = true;
break; // Stop iterating through colEndArray
}
}
if (placedItem) {
// Go get the next item
continue;
}
// Step 3
// Guess what? We still haven't placed the item. We need to
// create a new column for it.
// All the items in the last column, except for the one* that
// conflicts with the item we're trying to place, need to have
// their span extended by 1, since we're adding the new column
//
// * Note that there can only be one, because we sorted our
// events by start time, so this event must start later than
// the start of any possible conflicts.
var lastColNum = colEndArray.length;
for (var mm in currentBlob) {
var mmStart = currentBlob[mm].itemInfo.layoutStart;
var mmEnd = currentBlob[mm].itemInfo.layoutEnd;
if (currentBlob[mm].startCol + currentBlob[mm].colSpan == lastColNum &&
mmEnd.compare(curItemInfo.layoutStart) != 1) {
currentBlob[mm] = {itemInfo: currentBlob[mm].itemInfo,
startCol: currentBlob[mm].startCol,
colSpan: currentBlob[mm].colSpan + 1};
}
}
currentBlob.push({itemInfo: curItemInfo,
startCol: colEndArray.length,
colSpan: 1});
colEndArray.push(curItemInfo);
// Update latestItemEnd
if (latestItemEnd && curItemInfo.layoutEnd.compare(latestItemEnd) == 1) {
latestItemEnd = curItemInfo.layoutEnd;
}
// Go get the next item
}
// Add the last blob
blobs.push({blob: currentBlob,
totalCols: colEndArray.length});
return this.setupBoxStructure(blobs);
]]></body>
</method>
<method name="setupBoxStructure">
<parameter name="aBlobs"/>
<body><![CDATA[
// This is actually going to end up being a 3-d array
// 1st dimension: "layers", sets of columns of events that all
// should have equal width*
// 2nd dimension: "columns", individual columns of non-conflicting
// items
// 3rd dimension: "chunks", individual items or placeholders for
// the blank time in between them
//
// * Note that 'equal width' isn't strictly correct. If we're
// oriented differently, it will be height (and we'll have rows
// not columns). What's more, in the 'specialSpan' case, the
// columns won't actually have the same size, but will only all
// be multiples of a common size. See the note in the relayout
// function for more info on this (fairly rare) case.
var layers = [];
// When we start a new blob, move to a new set of layers
var layerOffset = 0;
for each (var glob in aBlobs) {
var layerArray = [];
var layerCounter = 1;
for each (var data in glob.blob) {
// from the item at hand we need to figure out on which
// layer and on which column it should go.
var layerIndex;
var specialSpan = null;
// each blob receives its own layer, that's the first part of the story. within
// a given blob we need to distribute the items on different layers depending on
// the number of columns each item spans. if each item just spans a single column
// the blob will cover *one* layer. if the blob contains items that span more than
// a single column, this blob will cover more than one layer. the algorithm places
// the items on the first layer in the case an item covers a single column. new layers
// are introduced based on the start column and number of spanning columns of an item.
if (data.colSpan != 1) {
var index = glob.totalCols * data.colSpan + data.startCol;
layerIndex = layerArray[index];
if (!layerIndex) {
layerIndex = layerCounter++;
layerArray[index] = layerIndex;
}
var offset = ((glob.totalCols - data.colSpan) % glob.totalCols)
if (offset != 0) {
specialSpan = data.colSpan / glob.totalCols;
}
} else {
layerIndex = 0;
}
layerIndex += layerOffset;
// Make sure there's room to insert stuff
while (layerIndex >= layers.length) {
layers.push([]);
}
while (data.startCol >= layers[layerIndex].length) {
layers[layerIndex].push([]);
if (specialSpan) {
layers[layerIndex][layers[layerIndex].length - 1].specialSpan = 1 / glob.totalCols;
}
}
// we now retrieve the column from 'layerIndex' and 'startCol'.
var col = layers[layerIndex][data.startCol];
if (specialSpan) {
col.specialSpan = specialSpan;
}
// take into account that items can span several days.
// that's why i'm clipping the start- and end-time to the
// timespan of this column.
var start = data.itemInfo.layoutStart;
var end = data.itemInfo.layoutEnd;
if (start.year != this.date.year ||
start.month != this.date.month ||
start.day != this.date.day) {
start = start.clone();
start.resetTo(this.date.year,
this.date.month,
this.date.day,
0,this.mStartMin,0,
start.timezone);
}
if (end.year != this.date.year ||
end.month != this.date.month ||
end.day != this.date.day) {
end = end.clone();
end.resetTo(this.date.year,
this.date.month,
this.date.day,
0,this.mEndMin,0,
end.timezone);
}
var prevEnd;
if (col.length > 0) {
// Fill in time gaps with a placeholder
prevEnd = col[col.length - 1].endDate.clone();
} else {
// First event in the column, add a placeholder for the
// blank time from this.mStartMin to the event's start
prevEnd = start.clone();
prevEnd.hour = 0;
prevEnd.minute = this.mStartMin;
}
prevEnd.timezone = floating();
// the reason why we need to calculate time durations
// based on floating timezones is that we need avoid
// dst gaps in this case. converting the date/times to
// floating conveys this idea in a natural way. note that
// we explicitly don't use getInTimezone() as it would
// be slightly more expensive in terms of performance.
var floatstart = start.clone();
floatstart.timezone = floating();
var dur = floatstart.subtractDate(prevEnd);
if (dur.inSeconds) {
col.push({duration: dur});
}
var floatend = end.clone();
floatend.timezone = floating();
col.push({event: data.itemInfo.event,
endDate: end,
duration: floatend.subtractDate(floatstart)});
}
layerOffset = layers.length;
}
return layers;
]]></body>
</method>
<!--
- Event sweep handlers
-->
<method name="onEventSweepMouseMove">
<parameter name="event"/>
<body><![CDATA[
var col = document.calendarEventColumnDragging;
if (!col) return;
if (event.clientX < (event.target.boxObject.x) ||
event.clientX > (event.target.boxObject.x + event.target.boxObject.width) ||
event.clientY < (event.target.boxObject.y) ||
event.clientY > (event.target.boxObject.y + event.target.boxObject.height)) {
var dragState = col.mDragState;
col.fgboxes.dragbox.removeAttribute("dragging");
col.fgboxes.box.removeAttribute("dragging");
window.removeEventListener("mousemove", col.onEventSweepMouseMove, false);
window.removeEventListener("mouseup", col.onEventSweepMouseUp, false);
document.calendarEventColumnDragging = null;
col.mDragState = null;
// the multiday view currently exhibits a less than optimal strategy
// in terms of item selection. items don't get automatically selected
// when clicked and dragged, as to differentiate inline editing from
// the act of selecting an event. but the application internal drop
// targets will ask for selected items in order to pull the data from
// the packets. that's why we need to make sure at least the currently
// dragged event is contained in the set of selected items.
let selectedItems = this.getSelectedItems({});
if (!selectedItems.some(
function (aItem) {
return (aItem.hashId == item.hashId);
})) {
col.calendarView.setSelectedItems(1,
[event.ctrlKey ? item.parentItem : item]);
}
invokeEventDragSession(dragState.dragOccurrence, 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, false);
window.removeEventListener("mouseup", col.onEventSweepMouseUp, false);
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);
}
}
var dragDay = col.mDate;
if (dragState.dragType == "modify-start" ||
dragState.dragType == "new") {
newStart.resetTo(dragDay.year, dragDay.month, dragDay.day,
0, dragState.startMin + col.mStartMin, 0,
newStart.timezone);
}
if (dragState.dragType == "modify-end" ||
dragState.dragType == "new") {
newEnd.resetTo(dragDay.year, dragDay.month, dragDay.day,
0, dragState.endMin + col.mStartMin, 0,
newEnd.timezone);
}
if (dragState.dragType == "move") {
// Figure out how much the event moved.
var duration = col.mDate.subtractDate(dragState.origDate);
var minutes = dragState.startMin - dragState.origMin;
// Since both boxDate and beginMove are dates (note datetimes),
// subtractDate will only give us a non-zero number of hours on
// DST changes. While strictly speaking, subtractDate's behavior
// is correct, we need to move the event a discrete number of
// days here. There is no need for normalization here, since
// addDuration does the job for us. Also note, the duration used
// here is only used to move over multiple days. Moving on the
// same day uses the minutes from the dragState.
if (duration.hours == 23) {
// entering DST
duration.hours++;
} else if (duration.hours == 1) {
// leaving DST
duration.hours--;
}
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);
newEnd.addDuration(duration);
}
// 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.mCreatedNewEvent = true;
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[
if (!isCalendarWritable(aOccurrence.calendar) ||
aOccurrence.calendar.getProperty("capabilities.events.supported") === false) {
return;
}
//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>
<method name="setDayStartEndMinutes">
<parameter name="aDayStartMin"/>
<parameter name="aDayEndMin"/>
<body><![CDATA[
if (aDayStartMin < this.mStartMin || aDayStartMin > aDayEndMin ||
aDayEndMin > this.mEndMin) {
throw Components.results.NS_ERROR_INVALID_ARG;
}
if (this.mDayStartMin != aDayStartMin ||
this.mDayEndMin != aDayEndMin) {
this.mDayStartMin = aDayStartMin;
this.mDayEndMin = aDayEndMin;
}
]]></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;
this.calendarView.controller.createNewEvent(null, newStart, null);
}
]]></handler>
<handler event="click" button="0"><![CDATA[
this.calendarView.setSelectedItems(0, []);
]]></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;
// If the selected calendar is readOnly, we don't want any sweeping.
var cal = getSelectedCalendar();
if (!isCalendarWritable(cal) ||
cal.getProperty("capabilities.events.supported") === false) {
return;
}
// 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" extends="chrome://calendar/content/widgets/calendar-widgets.xml#dragndropContainer">
<content xbl:inherits="selected" flex="1" class="calendar-event-column-header">
<children/>
</content>
<implementation>
<field name="mItemBoxes">null</field>
<constructor><![CDATA[
this.mItemBoxes = new Array();
]]></constructor>
<property name="date">
<getter><![CDATA[
return this.mDate;
]]></getter>
<setter><![CDATA[
this.mDate = val;
return val;
]]></setter>
</property>
<method name="findBoxForItem">
<parameter name="aItem"/>
<body><![CDATA[
for each (var item in this.mItemBoxes) {
if (aItem && item.occurrence.hasSameIds(aItem)) {
// We can return directly, since there will only be one box per
// item in the header.
return item;
}
}
return null;
]]></body>
</method>
<method name="addEvent">
<parameter name="aItem"/>
<body><![CDATA[
// prevent same items being added
if (this.mItemBoxes.some(function (itemBox) {
return itemBox.occurrence.hashId == aItem.hashId;
})) {
return;
}
var itemBox = createXULElement("calendar-editable-item");
this.appendChild(itemBox);
itemBox.calendarView = this.calendarView;
itemBox.occurrence = aItem;
var ctxt = this.calendarView.getAttribute("item-context") ||
this.calendarView.getAttribute("context");
itemBox.setAttribute("context", ctxt);
if (aItem.hashId in this.calendarView.mFlashingEvents) {
itemBox.setAttribute("flashing", "true");
}
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.hashId == aItem.hashId) {
this.removeChild(this.mItemBoxes[i]);
this.mItemBoxes.splice(i, 1);
break;
}
}
]]></body>
</method>
<method name="onDropItem">
<parameter name="aItem"/>
<body><![CDATA[
let newItem = cal.moveItem(aItem, this.mDate);
newItem = cal.setItemToAllDay(newItem, true);
return newItem;
]]></body>
</method>
<method name="selectOccurrence">
<parameter name="aItem"/>
<body><![CDATA[
for each (itemBox in this.mItemBoxes) {
if (aItem && (itemBox.occurrence.hashId == aItem.hashId)) {
itemBox.selected = true;
}
}
]]></body>
</method>
<method name="unselectOccurrence">
<parameter name="aItem"/>
<body><![CDATA[
for each (itemBox in this.mItemBoxes) {
if (aItem && (itemBox.occurrence.hashId == aItem.hashId)) {
itemBox.selected = false;
}
}
]]></body>
</method>
</implementation>
<handlers>
<handler event="dblclick" button="0"><![CDATA[
this.calendarView.controller.createNewEvent(null, this.mDate, null, true);
]]></handler>
<handler event="mousedown"><![CDATA[
this.calendarView.selectedDay = this.mDate;
]]></handler>
<handler event="click" button="0"><![CDATA[
this.calendarView.setSelectedItems(0, []);
]]></handler>
<handler event="DOMMouseScroll"><![CDATA[
if (this.getAttribute("orient") == "vertical") {
// In vertical view (normal), don't let the parent multiday view
// handle the scrolling in its bubbling phase. The default action
// will make the box scroll here.
// TODO We could scroll by the height of exactly one event box, but
// since a normal box's boxObject doesn't implement nsIScrollBoxObject,
// there is no way to scroll by pixels. Using a xul:scrollbox has
// problems since the equalsize attribute isn't inherited by the
// inner box, and even if that is worked around, something makes the
// rotated view look bad in that case.
event.stopPropagation();
}
]]></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 tooltip="itemTooltip" mousethrough="never">
<xul:box xbl:inherits="orient,width,height" flex="1">
<xul:box anonid="event-container"
class="calendar-color-box"
xbl:inherits="orient,readonly,flashing,alarm,allday,priority,progress,status,calendar,categories,calendar-uri,calendar-id"
flex="1">
<xul:box class="calendar-event-selection" orient="horizontal" flex="1">
<xul:stack anonid="eventbox"
align="stretch"
class="calendar-event-box-container"
flex="1"
xbl:inherits="context,parentorient=orient,readonly,flashing,alarm,allday,priority,progress,status,calendar,categories">
<xul:image flex="1" class="calendar-event-box-gradient"/>
<xul:vbox class="calendar-event-details" anonid="calendar-event-details">
<xul:description anonid="event-name" class="calendar-event-details-core" flex="1"/>
<xul:textbox anonid="event-name-textbox"
class="plain calendar-event-details-core calendar-event-name-textbox"
flex="1"
hidden="true"
wrap="true"/>
</xul:vbox>
<xul:hbox pack="end">
<xul:hbox anonid="alarm-icons-box"
class="alarm-icons-box"
pack="end"
align="top"
xbl:inherits="flashing"/>
<xul:calendar-category-box anonid="category-box" xbl:inherits="categories" pack="end"/>
</xul:hbox>
<xul:box xbl:inherits="orient">
<xul:calendar-event-gripbar anonid="gripbar1"
class="calendar-event-box-grippy-top"
whichside="start"
xbl:inherits="parentorient=orient"/>
<xul:spacer flex="1"/>
<xul:calendar-event-gripbar anonid="gripbar2"
class="calendar-event-box-grippy-bottom"
whichside="end"
xbl:inherits="parentorient=orient"/>
</xul:box>
<!-- Do not insert anything here, otherwise the event boxes will
not be resizable using the gripbars. If you want to insert
additional elements, do so above the box with the gripbars. -->
</xul:stack>
</xul:box>
</xul:box>
</xul:box>
</content>
<implementation>
<constructor><![CDATA[
this.orient = this.getAttribute("orient");
]]></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);
var gb1 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar1");
gb1.parentorient = val;
var gb2 = document.getAnonymousElementByAttribute(this, "anonid", "gripbar2");
gb2.parentorient = val;
}
return ret;
]]></body>
</method>
<method name="getOptimalMinSize">
<body><![CDATA[
if (this.getAttribute("orient") == "vertical") {
var minHeight = getOptimalMinimumHeight(this.eventNameLabel) +
getSummarizedStyleValues(document.getAnonymousElementByAttribute(this, "anonid", "eventbox"), ["margin-bottom", "margin-top"]) +
getSummarizedStyleValues(this, ["border-bottom-width", "border-top-width"]);
this.setAttribute("minheight", minHeight);
this.setAttribute("minwidth", "1");
return minHeight;
} else {
this.eventNameLabel.setAttribute("style", "min-width: 2em");
var minWidth = getOptimalMinimumWidth(this.eventNameLabel);
this.setAttribute("minwidth", minWidth);
this.setAttribute("minheight", "1");
return minWidth;
}
]]></body>
</method>
<property name="parentColumn"
onget="return this.mParentColumn;"
onset="return (this.mParentColumn = val);"/>
<property name="startMinute" readonly="true">
<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" readonly="true">
<getter><![CDATA[
if (!this.mOccurrence)
return 0;
var endDate = this.mOccurrence.endDate || this.mOccurrence.dueDate;
return endDate.hour * 60 + endDate.minute;
]]></getter>
</property>
<method name="setEditableLabel">
<body><![CDATA[
var evl = this.eventNameLabel;
var item = this.mOccurrence;
if (item.title && item.title != "") {
// Use <description> textContent so it can wrap.
evl.textContent = item.title;
} else {
evl.textContent = calGetString("calendar", "eventUntitled");
}
var gripbar = document.getAnonymousElementByAttribute(this, "anonid", "gripbar1").boxObject.height;
var height = document.getAnonymousElementByAttribute(this, "anonid", "eventbox").boxObject.height;
evl.setAttribute("style", "max-height: " + Math.max(0, height-gripbar*2) + "px");
]]></body>
</method>
</implementation>
<handlers>
<handler event="mousedown" button="0"><![CDATA[
event.stopPropagation();
if (this.mEditing)
return;
this.parentColumn.calendarView.selectedDay = this.parentColumn.mDate;
this.mMouseX = event.screenX;
this.mMouseY = event.screenY;
var whichside = event.whichside;
if (!whichside) {
// may be click or drag,
// so wait for mousemove (or mouseout if fast) to start item move drag
this.mInMouseDown = true;
return;
} else {
this.calendarView.setSelectedItems(1,
[event.ctrlKey ? this.mOccurrence.parentItem : this.mOccurrence]);
// start edge resize drag
this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, whichside, event.screenX, 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;
this.parentColumn.startSweepingToModifyEvent(this, this.mOccurrence, "middle", this.mMouseX, this.mMouseY);
this.mInMouseDown = false;
}
}
]]></handler>
<handler event="mouseout"><![CDATA[
if (!this.mEditing && this.mInMouseDown && this.parentColumn) {
if (this.editingTimer) {
clearTimeout(this.editingTimer);
this.editingTimer = null;
}
this.calendarView.selectedItem = this.mOccurrence;
this.mEditing = 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" extends="chrome://calendar/content/calendar-base-view.xml#calendar-base-view">
<content flex="1" orient="vertical" xbl:inherits="context,item-context">
<xul:box anonid="mainbox" flex="1">
<!-- these boxes are tricky: width or height in CSS depend on orient -->
<xul:box anonid="labelbox">
<xul:box anonid="labeltimespacer"/>
<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="scrollbox" 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">
<constructor><![CDATA[
// get day start/end hour from prefs and set on the view
this.setDayStartEndMinutes(getPrefSafe("calendar.view.daystarthour", 8) * 60,
getPrefSafe("calendar.view.dayendhour", 17) * 60);
// initially scroll to the day start hour in the view
this.scrollToMinute(this.mDayStartMin);
// get visible hours from prefs and set on the view
let visibleMinutes = getPrefSafe("calendar.view.visiblehours", 9) * 60;
this.setVisibleMinutes(visibleMinutes);
var self = this;
// set the flex attribute at the scrollbox-innerbox
// (this can be removed, after Bug 343555 is fixed)
let scrollbox = document.getAnonymousElementByAttribute(
this, "anonid", "scrollbox");
document.getAnonymousElementByAttribute(
scrollbox, "class", "box-inherit scrollbox-innerbox").flex = "1";
this.reorient();
]]></constructor>
<property name="daysInView" readonly="true">
<getter><![CDATA[
return this.labeldaybox.childNodes && this.labeldaybox.childNodes.length;
]]></getter>
</property>
<method name="handlePreference">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aPreference"/>
<body><![CDATA[
aSubject.QueryInterface(Components.interfaces.nsIPrefBranch2);
switch (aPreference) {
case "calendar.view.daystarthour":
this.setDayStartEndMinutes(aSubject.getIntPref(aPreference) * 60,
this.mDayEndMin);
this.refreshView();
break;
case "calendar.view.dayendhour":
this.setDayStartEndMinutes(this.mDayStartMin,
aSubject.getIntPref(aPreference) * 60);
this.refreshView();
break;
case "calendar.view.visiblehours":
this.setVisibleMinutes(aSubject.getIntPref(aPreference) * 60);
this.refreshView();
break;
default:
this.handleCommonPreference(aSubject, aTopic, aPreference);
break;
}
return;
]]></body>
</method>
<method name="onResize">
<parameter name="aRealSelf"/>
<body><![CDATA[
var self = this;
if (aRealSelf) {
self = aRealSelf;
}
let scrollbox = document.getAnonymousElementByAttribute(self, "anonid", "scrollbox");
var size;
if (self.orient == "horizontal") {
size = scrollbox.boxObject.width;
} else {
size = scrollbox.boxObject.height;
}
var ppm = size / self.mVisibleMinutes;
ppm = Math.floor(ppm * 1000) / 1000;
if (ppm < self.mMinPixelsPerMinute) {
ppm = self.mMinPixelsPerMinute;
}
self.pixelsPerMinute = ppm;
setTimeout(function(){self.scrollToMinute(self.mFirstVisibleMinute)}, 0);
// Fit the weekday labels while scrolling.
self.adjustWeekdayLength(self.getAttribute("orient") == "horizontal");
]]></body>
</method>
<!-- 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="mMinPixelsPerMinute">0.1</field>
<field name="mSelectedDayCol">null</field>
<field name="mSelectedDay">null</field>
<field name="mStartMin">0*60</field>
<field name="mEndMin">24*60</field>
<field name="mDayStartMin">0</field>
<field name="mDayEndMin">0</field>
<field name="mVisibleMinutes">9*60</field>
<method name="flashAlarm">
<parameter name="aAlarmItem"/>
<parameter name="aStop"/>
<body><![CDATA[
var showIndicator = getPrefSafe("calendar.alarms.indicator.show", true);
var totaltime = getPrefSafe("calendar.alarms.indicator.totaltime", 3600);
if (!aStop && (!showIndicator || totaltime < 1)) {
// No need to animate if the indicator should not be shown.
return;
}
// Helper function to save some duplicate code
function setFlashingAttribute(aBox) {
if (aStop) {
aBox.removeAttribute("flashing");
} else {
aBox.setAttribute("flashing", "true");
}
}
// Make sure the flashing attribute is set or reset on all visible
// boxes.
var columns = this.findColumnsForItem(aAlarmItem);
for each (var col in columns) {
var box = col.column.findChunkForOccurrence(aAlarmItem);
if (box && box.eventbox) {
setFlashingAttribute(box.eventbox);
}
box = col.header.findBoxForItem(aAlarmItem);
if (box) {
setFlashingAttribute(box);
}
}
if (!aStop) {
// Set up a timer to stop the flashing after the total time.
var this_ = this;
this.mFlashingEvents[aAlarmItem.hashId] = aAlarmItem;
setTimeout(function() { this_.flashAlarm(aAlarmItem, true) }, totaltime);
} else {
// We are done flashing, prevent newly created event boxes from flashing.
delete this.mFlashingEvents[aAlarmItem.hashId];
}
]]></body>
</method>
<!-- calICalendarView -->
<property name="supportsDisjointDates"
onget="return true"/>
<property name="hasDisjointDates"
onget="return (this.mDateList != null);"/>
<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>
<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[
this.rangeStartDate = aStartDate;
this.rangeEndDate = aEndDate;
// normalize dates to display timezone
var startDate = aStartDate.getInTimezone(this.mTimezone);
startDate.isDate = true;
var endDate = aEndDate.getInTimezone(this.mTimezone);
endDate.isDate = true;
// make sure unnormalized version not used below
aStartDate = aEndDate = null;
if (this.mStartDate && this.mEndDate &&
this.mStartDate.compare(startDate) == 0 &&
this.mEndDate.compare(endDate) == 0) {
// Do not change anything if the date range already matches.
// XXX In general it should be possible to return here, but
// lightning doesn't like it when first initializing the view.
// return;
}
if (this.mDisplayDaysOff) {
// tell old date columns to forget old selected occurrences.
var selectedItems = this.getSelectedItems({});
this.setSelectedItems(0, [], true); // suppress event, will restore
startDate.makeImmutable();
endDate.makeImmutable();
this.mDateList = null;
this.mStartDate = startDate;
this.mEndDate = endDate;
//
// For a true multiday view (e.g, 3 days advanced by one day
// at a time), a smarter refresh could reuse boxes, comparing
// the current date range and add/remove, instead of just
// replacing.
//
this.refresh();
// restore selected item occurrences in view with new date columns.
this.setSelectedItems(selectedItems.length, selectedItems, true);
} else { // workdays only
var dateList = new Array();
for (var d = startDate.clone(); d.compare(endDate) <= 0;) {
if (this.mDaysOffArray.indexOf(d.weekday) == -1) {
var workday = d.clone();
workday.makeImmutable();
dateList.push(workday);
}
d.day += 1;
}
this.setDateList(dateList.length, dateList);
}
]]></body>
</method>
<method name="setDateList">
<parameter name="aCount"/>
<parameter name="aDates"/>
<body><![CDATA[
// tell old date columns to forget old selected occurrences.
var selectedItems = this.getSelectedItems({});
this.setSelectedItems(0, [], true); // suppress event, will restore
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();
// restore selected item occurrences in view with new date columns.
this.setSelectedItems(selectedItems.length, selectedItems, true);
]]></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;
}
} 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[
var selected;
if (this.numVisibleDates == 1) {
selected = this.mDateColumns[0].date;
} else if (this.mSelectedDay) {
selected = this.mSelectedDay;
} else if (this.mSelectedDayCol) {
selected = this.mSelectedDayCol.date;
}
// TODO Make sure the selected day is valid
// TODO select now if it is in the range?
return selected;
]]></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.mSelectedDay = this.mSelectedDayCol.date;
this.mSelectedDayCol.column.selected = true;
this.mSelectedDayCol.header.setAttribute("selected", "true");
} else {
this.mSelectedDay = val;
}
}
var headerColLabel = this.selectColumnHeader(val);
this.fireEvent("dayselect", val);
return val;
]]></setter>
</property>
<method name="getSelectedItems">
<parameter name="aCount"/>
<body><![CDATA[
aCount.value = this.mSelectedItems.length;
return this.mSelectedItems;
]]></body>
</method>
<method name="setSelectedItems">
<parameter name="aCount"/>
<parameter name="aItems"/>
<parameter name="aSuppressEvent"/>
<body><![CDATA[
for each (var item in this.mSelectedItems) {
for each (var occ in this.getItemOccurrencesInView(item)) {
var cols = this.findColumnsForItem(occ);
for each (col in cols) {
col.header.unselectOccurrence(occ);
col.column.unselectOccurrence(occ);
}
}
}
this.mSelectedItems = aItems || [];
for each (var item in this.mSelectedItems) {
for each (var occ in this.getItemOccurrencesInView(item)) {
var cols = this.findColumnsForItem(occ);
if (cols.length > 0) {
var start = item.startDate || item.entryDate;
for each (col in cols) {
if (start.isDate) {
col.header.selectOccurrence(occ);
} else {
col.column.selectOccurrence(occ);
}
}
}
}
}
if (!aSuppressEvent) {
this.fireEvent("itemselect", this.mSelectedItems);
}
]]></body>
</method>
<method name="getItemOccurrencesInView">
<parameter name="aItem"/>
<body><![CDATA[
if (aItem.recurrenceInfo && aItem.recurrenceStartDate) {
// if selected a parent item, show occurrence(s) in view range
return aItem.getOccurrencesBetween(this.startDate, this.queryEndDate, {}, 0);
} else if (aItem.recurrenceStartDate) {
return [aItem];
} else { // undated todo
return [];
}
]]></body>
</method>
<method name="centerSelectedItems">
<body><![CDATA[
var displayTZ = calendarDefaultTimezone();
var lowMinute = 24 * 60;
var highMinute = 0;
for each (var item in this.mSelectedItems) {
var startDateProperty = calGetStartDateProp(item);
var endDateProperty = calGetEndDateProp(item);
var occs = [];
if (item.recurrenceInfo) {
// if selected a parent item, show occurrence(s) in view range
occs = item.getOccurrencesBetween(this.startDate, this.queryEndDate, {}, 0);
} else {
occs = [item];
}
for each (var occ in occs) {
var occStart = occ[startDateProperty];
var occEnd = occ[endDateProperty];
// must have at least one of start or end
if (!occStart && !occEnd) {
continue; // task with no dates
}
// if just has single datetime, treat as zero duration item
// (such as task with due datetime or start datetime only)
occStart = (occStart || occEnd);
occEnd = (occEnd || occStart);
// Now both occStart and occEnd are datetimes.
// skip occurrence if all-day: it won't show in time view.
if (occStart.isDate || occEnd.isDate) {
continue;
}
// Trim dates to view. (Not mutated so just reuse view dates)
if (this.startDate.compare(occStart) > 0) {
occStart = this.startDate;
}
if (this.queryEndDate.compare(occEnd) < 0) {
occEnd = this.queryEndDate;
}
// Convert to display timezone if different
if (occStart.timezone != displayTZ) {
occStart = occStart.getInTimezone(displayTZ);
}
if (occEnd.timezone != displayTZ) {
occEnd = occEnd.getInTimezone(displayTZ);
}
// If crosses midnite in current TZ, set end just
// before midnite after start so start/title usually visible.
if (!sameDay(occStart, occEnd)) {
occEnd = occStart.clone();
occEnd.day = occStart.day;
occEnd.hour = 23;
occEnd.minute = 59;
}
// Ensure range shows occ
lowMinute = Math.min(occStart.hour * 60 + occStart.minute,
lowMinute);
highMinute = Math.max(occEnd.hour * 60 + occEnd.minute,
highMinute);
}
}
var displayDuration = highMinute - lowMinute;
if (this.mSelectedItems.length &&
displayDuration >= 0) {
let minute;
if (displayDuration <= this.mVisibleMinutes) {
minute = lowMinute + (displayDuration - this.mVisibleMinutes) / 2
} else if (this.mSelectedItems.length == 1) {
// If the displayDuration doesn't fit into the visible
// minutes, but only one event is selected, then go ahead and
// center the event start.
minute = Math.max(0, lowMinute - (this.mVisibleMinutes / 2));
}
this.scrollToMinute(minute);
}
]]></body>
</method>
<property name="pixelsPerMinute">
<getter><![CDATA[
return this.mPixPerMin
]]></getter>
<setter><![CDATA[
this.mPixPerMin = val;
let timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
timebar.pixelsPerMinute = val;
for each (let col in this.mDateColumns) {
col.column.pixelsPerMinute = val;
}
return val;
]]></setter>
</property>
<!-- private -->
<property name="numVisibleDates" readonly="true">
<getter><![CDATA[
if (this.mDateList)
return this.mDateList.length;
var count = 0;
if (!this.mStartDate || !this.mEndDate) {
// The view has not been initialized, so there are 0 visible dates.
return count;
}
var d = this.mStartDate.clone();
while (d.compare(this.mEndDate) <= 0) {
count++;
d.day += 1;
}
return count;
]]></getter>
</property>
<property name="orient">
<getter><![CDATA[return (this.getAttribute("orient") || "vertical");]]></getter>
<setter><![CDATA[this.setAttribute("orient", val); return val;]]></setter>
</property>
<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.pixelsPerMinute = 1.5;
} else {
this.pixelsPerMinute = 0.6;
}
var normalelems = ['mainbox', 'timebar'];
var otherelems = ['labelbox', 'labeldaybox', 'headertimespacer',
'headerbox', 'headerdaybox', 'scrollbox', '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 scrollbox = document.getAnonymousElementByAttribute(
this, "anonid", "scrollbox");
var mainbox = document.getAnonymousElementByAttribute(
this, "anonid", "mainbox");
if (orient == "vertical") {
scrollbox.setAttribute(
"style", "overflow-x: hidden; overflow-y: auto;");
mainbox.setAttribute(
"style", "overflow-x: auto; overflow-y: hidden;");
} else {
scrollbox.setAttribute(
"style", "overflow-x: auto; overflow-y: hidden;");
mainbox.setAttribute(
"style", "overflow-x: hidden; overflow-y: auto;");
}
var boxes = ["daybox", "headerdaybox"];
for each (var boxname in boxes) {
var box = document.getAnonymousElementByAttribute(this, "anonid", boxname);
setAttributeToChildren(box, "orient", orient);
}
var box = document.getAnonymousElementByAttribute(this, "anonid", "labeldaybox");
setAttributeToChildren(this.labeldaybox, "orient", otherorient);
// Refresh
this.refresh();
]]></body>
</method>
<method name="relayout">
<body><![CDATA[
var orient = this.getAttribute("orient");
if (!orient) orient = "horizontal";
var otherorient = getOtherOrientation(orient);
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;
}
}
var daybox = document.getAnonymousElementByAttribute(this, "anonid", "daybox");
var headerdaybox = document.getAnonymousElementByAttribute(this, "anonid", "headerdaybox");
if (!computedDateList || computedDateList.length == 0)
return;
var calView = this;
var dayStartMin = this.mDayStartMin;
var dayEndMin = this.mDayEndMin;
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.setAttribute("orient", orient);
aDayBox.calendarView = calView;
aDayBox.setDayStartEndMinutes(dayStartMin, dayEndMin);
}
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;
let labelboxkids = this.labeldaybox.childNodes;
for each (var d in computedDateList) {
var dayEventsBox;
if (counter < dayboxkids.length) {
dayEventsBox = dayboxkids[counter];
dayEventsBox.removeAttribute("relation");
dayEventsBox.mEventInfos = 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
// and delete until no more elements are left.
while(dayHeaderBox.mItemBoxes.length != 0) {
var num = dayHeaderBox.mItemBoxes.length;
dayHeaderBox.deleteEvent(dayHeaderBox.mItemBoxes[num-1].occurrence);
}
} else {
dayHeaderBox = createXULElement("calendar-header-container");
dayHeaderBox.setAttribute("flex", "1");
headerdaybox.appendChild(dayHeaderBox);
}
setUpDayHeaderBox(dayHeaderBox);
if (0 <= this.mDaysOffArray.indexOf(d.weekday)) {
dayEventsBox.dayOff = true;
dayHeaderBox.setAttribute("weekend", "true");
} else {
dayEventsBox.dayOff = false;
dayHeaderBox.removeAttribute("weekend");
}
var labelbox;
if (counter < labelboxkids.length) {
labelbox = labelboxkids[counter];
labelbox.date = d;
} else {
labelbox = createXULElement("calendar-day-label");
labelbox.setAttribute("orient", otherorient);
this.labeldaybox.appendChild(labelbox);
labelbox.date = d;
}
// Set attributes for date relations.
if (this.numVisibleDates > 1) {
switch (d.compare(today)) {
case -1:
dayHeaderBox.setAttribute("relation", "past");
dayEventsBox.setAttribute("relation", "past");
labelbox.setAttribute("relation", "past");
break;
case 0:
dayHeaderBox.setAttribute("relation", "today");
dayEventsBox.setAttribute("relation", "today");
labelbox.setAttribute("relation", "today");
break;
case 1:
dayHeaderBox.setAttribute("relation", "future");
dayEventsBox.setAttribute("relation", "future");
labelbox.setAttribute("relation", "future");
break;
}
}
// 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(this.labeldaybox);
// fix pixels-per-minute
this.onResize();
for each (col in this.mDateColumns) {
col.column.endLayoutBatchChange();
}
// Adjust scrollbar spacers
this.adjustScrollBarSpacers();
]]></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="selectColumnHeader">
<parameter name="aDate"/>
<body><![CDATA[
let child = this.labeldaybox.firstChild;
while (child) {
if (child.date.compare(aDate) == 0) {
child.setAttribute("selected", "true");
} else {
child.removeAttribute("selected");
}
child = child.nextSibling;
}
]]></body>
</method>
<method name="findColumnsForOccurrences">
<parameter name="aOccurrences"/>
<body><![CDATA[
if (!this.mDateColumns || !this.mDateColumns.length) {
return [];
}
var occMap = {};
for each (var occ in aOccurrences) {
var startDate = occ[calGetStartDateProp(occ)]
.getInTimezone(this.mStartDate.timezone);
var endDate = occ[calGetEndDateProp(occ)]
.getInTimezone(this.mEndDate.timezone) || startDate;
if (startDate.compare(this.mStartDate) >= 0 &&
endDate.compare(this.mEndDate) <= 0) {
for (var i = startDate.day; i <= endDate.day; i++) {
occMap[i] = true;
}
}
}
return this.mDateColumns.filter(function(col) {
return (col.date.day in occMap);
});
]]></body>
</method>
<method name="findColumnsForItem">
<parameter name="aItem"/>
<body><![CDATA[
var columns = new Array();
if (!this.mDateColumns) {
return columns;
}
var tz = this.mDateColumns[0].date.timezone;
// Note that these may be dates or datetimes
var startDate = aItem.startDate || aItem.entryDate;
if (!startDate) {
return columns;
}
var targetDate = startDate.getInTimezone(tz);
var endDate = aItem.endDate || aItem.dueDate || startDate;
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;
}
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="adjustScrollbarSpacersForAlldayEvents">
<parameter name="aEvent"/>
<body><![CDATA[
var startDate = aEvent[calGetStartDateProp(aEvent)];
var endDate = aEvent[calGetEndDateProp(aEvent)];
if (startDate && startDate.isDate ||
endDate && endDate.isDate) {
// If this is an all day event, then the header with allday
// events could possibly get a scrollbar. Readjust them.
this.adjustScrollBarSpacers();
}
]]></body>
</method>
<method name="doAddItem">
<parameter name="aEvent"/>
<body><![CDATA[
//dump ("++ doAddItem\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);
}
}
this.adjustScrollbarSpacersForAlldayEvents(aEvent);
]]></body>
</method>
<method name="doDeleteItem">
<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);
}
}
// See whether the item we are deleting was selected. If it was, then
// fire the appropriate event so our watchers can update
var found = false;
for (var i = 0; i < this.mSelectedItems.length; i++) {
if (this.mSelectedItems[i].hashId == aEvent.hashId) {
this.mSelectedItems.splice(i, 1);
found = true;
}
}
if (found) {
this.fireEvent("itemselect", this.mSelectedItems);
}
this.adjustScrollbarSpacersForAlldayEvents(aEvent);
]]></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 scrollbox scrollbar
var scrollbox = document.getAnonymousElementByAttribute(
this, "anonid", "scrollbox");
var propertyValue = scrollbox.boxObject.firstChild
.boxObject[propertyName];
// Check if we need to show the headerScrollbarSpacer at all
var headerPropertyValue = propertyValue;
var headerDayBox = document.getAnonymousElementByAttribute(
this, "anonid", "headerdaybox");
if (headerDayBox) {
// Only do this when there are multiple days
var headerDayBoxMaxHeight = parseInt(document.defaultView.getComputedStyle(headerDayBox, null)
.getPropertyValue("max-height"), 10);
if (this.getAttribute("orient") == "vertical" &&
headerDayBox.boxObject.height >= headerDayBoxMaxHeight) {
// If the headerDayBox is just as high as the max-height, then
// there is already a scrollbar and we don't need to show the
// headerScrollbarSpacer. This is only valid for the non-rotated
// view.
headerPropertyValue = 0;
}
}
// set the same width/height for the label and header box spacers
var headerScrollBarSpacer = document.getAnonymousElementByAttribute(
this, "anonid", "headerscrollbarspacer");
headerScrollBarSpacer.setAttribute(propertyName, headerPropertyValue);
var labelScrollBarSpacer = document.getAnonymousElementByAttribute(
this, "anonid", "labelscrollbarspacer");
labelScrollBarSpacer.setAttribute(propertyName, propertyValue);
]]></body>
</method>
<field name="mFirstVisibleMinute">0</field>
<method name="scrollToMinute">
<parameter name="aMinute"/>
<body><![CDATA[
let scrollbox = document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
let scrollBoxObject = scrollbox.boxObject.QueryInterface(Components.interfaces.nsIScrollBoxObject);
if (scrollBoxObject && scrollbox.scrollHeight > 0) {
let x = {}, y = {};
scrollBoxObject.getPosition(x, y);
let pos = Math.round(aMinute * this.mPixPerMin);
if (scrollbox.getAttribute("orient") == "horizontal") {
scrollBoxObject.scrollTo(x.value, pos);
} else {
scrollBoxObject.scrollTo(pos, y.value);
}
}
// Set the first visible minute in any case, we want to move to the
// right minute as soon as possible if we couldn't do so above.
this.mFirstVisibleMinute = aMinute;
]]></body>
</method>
<method name="setDayStartEndMinutes">
<parameter name="aDayStartMin"/>
<parameter name="aDayEndMin"/>
<body><![CDATA[
if (aDayStartMin < this.mStartMin || aDayStartMin > aDayEndMin ||
aDayEndMin > this.mEndMin) {
throw Components.results.NS_ERROR_INVALID_ARG;
}
if (this.mDayStartMin != aDayStartMin ||
this.mDayEndMin != aDayEndMin) {
this.mDayStartMin = aDayStartMin;
this.mDayEndMin = aDayEndMin;
// Also update on the time-bar
document.getAnonymousElementByAttribute(this, "anonid", "timebar")
.setDayStartEndHours(this.mDayStartMin / 60,
this.mDayEndMin / 60);
}
]]></body>
</method>
<method name="setVisibleMinutes">
<parameter name="aVisibleMinutes"/>
<body><![CDATA[
if (aVisibleMinutes <= 0 ||
aVisibleMinutes > (this.mEndMin - this.mStartMin)) {
throw Components.results.NS_ERROR_INVALID_ARG;
}
if (this.mVisibleMinutes != aVisibleMinutes) {
this.mVisibleMinutes = aVisibleMinutes;
}
return this.mVisibleMinutes;
]]></body>
</method>
</implementation>
<handlers>
<handler event="DOMMouseScroll" phase="bubbling"><![CDATA[
if (!event.ctrlKey &&
!event.shiftKey &&
!event.altKey &&
!event.metaKey) {
// Only shift hours if no modifier is pressed.
this.scrollToMinute(this.mFirstVisibleMinute +
(event.detail < 0 ? -60 : 60));
}
// We are taking care of scrolling, so prevent the default
// action in any case.
event.preventDefault();
]]></handler>
<handler event="scroll" phase="bubbling"><![CDATA[
let scrollbox = document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
let scrollBoxObject = scrollbox.boxObject.QueryInterface(Components.interfaces.nsIScrollBoxObject);
if (scrollBoxObject && scrollbox.scrollHeight > 0) {
// We need to update the first visible minute, but only if the
// scrollbox has been sized.
let x = {}, y = {};
scrollBoxObject.getPosition(x, y);
if (scrollbox.getAttribute("orient") == "horizontal") {
this.mFirstVisibleMinute = Math.round(y.value/this.mPixPerMin);
} else {
this.mFirstVisibleMinute = Math.round(x.value/this.mPixPerMin);
}
}
]]></handler>
</handlers>
</binding>
</bindings>