3264 строки
129 KiB
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>
|