<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE dialog [
<!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
<!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
<!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %dtd3;
]>
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="attendees-list">
<content>
<xul:listbox anonid="listbox"
seltype="multiple"
class="listbox-noborder"
rows="-1"
flex="1">
<xul:listcols>
<xul:listcol/>
<xul:listcol/>
<xul:listcol flex="1"/>
</xul:listcols>
<xul:listitem anonid="item" class="addressingWidgetItem" allowevents="true">
<xul:listcell class="addressingWidgetCell" align="center" pack="center">
<xul:image id="attendeeCol1#1" anonid="rolestatus-icon"/>
</xul:listcell>
<xul:listcell class="addressingWidgetCell">
<xul:image id="attendeeCol2#1" anonid="usertype-icon" class="usertype-icon" onclick="this.parentNode.select();"/>
</xul:listcell>
<xul:listcell class="addressingWidgetCell">
<xul:textbox id="attendeeCol3#1"
anonid="input"
class="plain textbox-addressingWidget uri-element"
type="autocomplete"
flex="1"
autocompletesearch="addrbook ldap"
timeout="300"
maxrows="4"
completedefaultindex="true"
forcecomplete="true"
minresultsforpopup="1"
onblur="if (this.localName == 'textbox') document.getBindingParent(this).onBlurInput(event);"
ignoreblurwhilesearching="true"
oninput="this.setAttribute('dirty', 'true');">
</xul:textbox>
</xul:listcell>
</xul:listitem>
</xul:listbox>
</content>
<implementation>
<field name="mMaxAttendees">0</field>
<field name="mContentHeight">0</field>
<field name="mRowHeight">0</field>
<field name="mNumColumns">0</field>
<field name="mIsOffline">0</field>
<field name="mIsReadOnly">false</field>
<field name="mIsInvitation">false</field>
<field name="mPopupOpen">false</field>
<constructor><![CDATA[
Components.utils.import("resource://calendar/modules/calUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
this.mMaxAttendees = 0;
var self = this;
var load = function loadHandler() {
self.onLoad();
};
window.addEventListener("load", load, true);
]]></constructor>
<method name="onLoad">
<body><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var template =
document.getAnonymousElementByAttribute(
this, "anonid", "item");
this.onInitialize();
// this trigger the continous update chain, which
// effectively calls this.onModify() on predefined
// time intervals [each second].
var self = this;
var callback = function() {
setTimeout(callback, 1000);
self.onModify();
}
callback();
]]></body>
</method>
<method name="onInitialize">
<body><![CDATA[
var args = window.arguments[0];
var organizer = args.organizer;
var attendees = args.attendees;
var calendar = args.calendar;
this.mIsReadOnly = calendar.readOnly;
// assume we're the organizer [in case that the calendar
// does not support the concept of identities].
var organizerID = ((organizer && organizer.id)
? organizer.id
: calendar.getProperty("organizerId"));
calendar = cal.wrapInstance(calendar, Components.interfaces.calISchedulingSupport);
this.mIsInvitation = (calendar && calendar.isInvitation(args.item));
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var template =
document.getAnonymousElementByAttribute(
this, "anonid", "item");
template.focus();
if (this.mIsReadOnly || this.mIsInvitation) {
listbox.setAttribute("disabled", "true");
}
// TODO: the organizer should show up in the attendee list, but this information
// should be based on the organizer contained in the appropriate field of calIItemBase.
// This is currently not supported, since we're still missing calendar identities.
if (organizerID && organizerID != "") {
if (!organizer) {
organizer = this.createAttendee();
organizer.id = organizerID;
organizer.role = "CHAIR";
organizer.participationStatus = "ACCEPTED";
} else {
if (!organizer.id) {
organizer.id = organizerID;
}
if (!organizer.role) {
organizer.role = "CHAIR";
}
if (!organizer.participationStatus) {
organizer.participationStatus = "ACCEPTED";
}
}
if (!organizer.commonName || !organizer.commonName.length) {
organizer.commonName = calendar.getProperty("organizerCN");
}
organizer.isOrganizer = true;
this.appendAttendee(organizer, listbox, template, true);
}
var numRowsAdded = 0;
if (attendees.length > 0) {
for each (var attendee in attendees) {
this.appendAttendee(attendee, listbox, template, false);
numRowsAdded++;
}
}
if (numRowsAdded == 0) {
this.appendAttendee(null, listbox, template, false);
}
// detach the template item from the listbox, but hold the reference.
// until this function returns we add at least a single copy of this template back again.
listbox.removeChild(template);
this.setFocus(this.mMaxAttendees);
]]></body>
</method>
<!-- appends a new row using an existing attendee structure -->
<method name="appendAttendee">
<parameter name="aAttendee"/>
<parameter name="aParentNode"/>
<parameter name="aTemplateNode"/>
<parameter name="aDisableIfOrganizer"/>
<body><![CDATA[
// create a new listbox item and append it to our parent control.
var newNode = aTemplateNode.cloneNode(true);
var input =
document.getAnonymousElementByAttribute(
newNode, "anonid", "input");
var roleStatusIcon =
document.getAnonymousElementByAttribute(
newNode, "anonid", "rolestatus-icon");
var userTypeIcon =
document.getAnonymousElementByAttribute(
newNode, "anonid", "usertype-icon");
// We always clone the first row. The problem is that the first row
// could be focused. When we clone that row, we end up with a cloned
// XUL textbox that has a focused attribute set. Therefore we think
// we're focused and don't properly refocus. The best solution to this
// would be to clone a template row that didn't really have any presentation,
// rather than using the real visible first row of the listbox.
// For now we'll just put in a hack that ensures the focused attribute
// is never copied when the node is cloned.
if (input.getAttribute('focused') != '') {
input.removeAttribute('focused');
}
aParentNode.appendChild(newNode);
// the template could have its fields disabled,
// that's why we need to reset their status.
input.removeAttribute("disabled");
userTypeIcon.removeAttribute("disabled");
roleStatusIcon.removeAttribute("disabled");
if (this.mIsReadOnly || this.mIsInvitation) {
input.setAttribute("disabled", "true");
userTypeIcon.setAttribute("disabled", "true");
roleStatusIcon.setAttribute("disabled", "true");
}
// disable the input-field [name <email>] if this attendee
// appears to be the organizer.
if (aDisableIfOrganizer && aAttendee && aAttendee.isOrganizer) {
input.setAttribute("disabled", "true");
}
this.mMaxAttendees++;
var rowNumber = this.mMaxAttendees;
if (rowNumber >= 0) {
roleStatusIcon.setAttribute("id", "attendeeCol1#" + rowNumber);
userTypeIcon.setAttribute("id", "attendeeCol2#" + rowNumber);
input.setAttribute("id", "attendeeCol3#" + rowNumber);
}
if (!aAttendee) {
aAttendee = this.createAttendee();
}
// construct the display string from common name and/or email address.
var inputValue = aAttendee.commonName;
var regexp = new RegExp("^mailto:(.*)", "i");
if (inputValue) {
// Make the commonName appear in quotes if it
// contains a comma or semicolon.
if (inputValue.search(/[,;]/) != -1) {
inputValue = '"' + inputValue + '"';
}
var email = aAttendee.id;
if (email && email.length) {
if (regexp.test(email)) {
inputValue += ' <' + RegExp.$1 + '>';
} else {
inputValue += ' <' + email + '>';
}
}
} else {
var email = aAttendee.id;
if (email && email.length) {
if (regexp.test(email)) {
inputValue = RegExp.$1;
} else {
inputValue = email;
}
}
}
// remove leading spaces
while (inputValue && inputValue[0] == " " ) {
inputValue = inputValue.substring(1, inputValue.length);
}
input.setAttribute("value", inputValue);
input.value = inputValue;
input.attendee = aAttendee;
input.setAttribute("dirty", "true");
if (aAttendee) {
// Set up userType
setElementValue(userTypeIcon, aAttendee.userType || false, "cutype");
this.updateTooltip(userTypeIcon);
// Set up role/status icon
if (aAttendee.isOrganizer) {
roleStatusIcon.setAttribute("class", "status-icon");
setElementValue(roleStatusIcon, aAttendee.participationStatus || false, "status");
} else {
roleStatusIcon.setAttribute("class", "role-icon");
setElementValue(roleStatusIcon, aAttendee.role || false, "role");
}
this.updateTooltip(roleStatusIcon);
}
return true;
]]></body>
</method>
<method name="appendNewRow">
<parameter name="aSetFocus"/>
<parameter name="aInsertAfter"/>
<body><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var listitem1 = this.getListItem(1);
var newNode = null;
if (listbox && listitem1) {
var newAttendee = this.createAttendee();
var nextDummy = this.getNextDummyRow();
newNode = listitem1.cloneNode(true);
if (aInsertAfter) {
listbox.insertBefore(newNode, aInsertAfter.nextSibling);
} else if (nextDummy) {
listbox.replaceChild(newNode, nextDummy);
} else {
listbox.appendChild(newNode);
}
var input =
document.getAnonymousElementByAttribute(
newNode, "anonid", "input");
var roleStatusIcon =
document.getAnonymousElementByAttribute(
newNode, "anonid", "rolestatus-icon");
var userTypeIcon =
document.getAnonymousElementByAttribute(
newNode, "anonid", "usertype-icon");
// the template could have its fields disabled,
// that's why we need to reset their status.
input.removeAttribute("disabled");
roleStatusIcon.removeAttribute("disabled");
userTypeIcon.removeAttribute("disabled");
if (this.mIsReadOnly || this.mIsInvitation) {
input.setAttribute("disabled", "true");
roleStatusIcon.setAttribute("disabled", "true");
userTypeIcon.setAttribute("disabled", "true");
}
this.mMaxAttendees++;
var rowNumber = this.mMaxAttendees;
if (rowNumber >= 0) {
roleStatusIcon.setAttribute("id", "attendeeCol1#" + rowNumber);
userTypeIcon.setAttribute("id", "attendeeCol2#" + rowNumber);
input.setAttribute("id", "attendeeCol3#" + rowNumber);
}
input.value = null;
input.removeAttribute("value");
input.attendee = newAttendee;
// set role and participation status
//status.setAttribute("status", newAttendee.participationStatus);
//role.setAttribute("role", newAttendee.role);
roleStatusIcon.setAttribute("class", "role-icon");
roleStatusIcon.setAttribute("role", "REQ-PARTICIPANT");
userTypeIcon.setAttribute("cutype", "INDIVIDUAL");
// Set tooltip for rolenames and usertype icon
this.updateTooltip(roleStatusIcon);
this.updateTooltip(userTypeIcon);
// We always clone the first row. The problem is that the first row
// could be focused. When we clone that row, we end up with a cloned
// XUL textbox that has a focused attribute set. Therefore we think
// we're focused and don't properly refocus. The best solution to this
// would be to clone a template row that didn't really have any presentation,
// rather than using the real visible first row of the listbox.
// For now we'll just put in a hack that ensures the focused attribute
// is never copied when the node is cloned.
if (input.getAttribute('focused') != '') {
input.removeAttribute('focused');
}
// focus on new input widget
if (aSetFocus) {
this.setFocus(newNode);
}
}
return newNode;
]]></body>
</method>
<property name="attendees">
<getter><![CDATA[
Components.utils.import("resource:///modules/mailServices.js");
let attendees = [];
let inputField;
for (let i = 1; inputField = this.getInputElement(i); i++) {
let fieldValue = inputField.value;
if (fieldValue != "") {
// the inputfield already has a reference to the attendee
// object, we just need to fill in the name.
let attendee = inputField.attendee.clone();
attendee.role = this.getRoleElement(i).getAttribute("role");
//attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
let userType = this.getUserTypeElement(i).getAttribute("cutype");
attendee.userType = (userType == "INDIVIDUAL" ? null : userType) // INDIVIDUAL is the default
// break the list of potentially many attendees back into individual names
let emailAddresses = {};
let names = {};
let fullNames = {};
MailServices.headerParser.parseHeadersWithArray(fieldValue,
emailAddresses,
names,
fullNames);
if (emailAddresses.value.length > 0) {
// If the new address has no 'mailto'-prefix but seems
// to look like an email-address, we prepend the prefix.
// This also allows for non-email-addresses.
let email = emailAddresses.value[0];
if (email.toLowerCase().indexOf("mailto:") != 0) {
if (email.indexOf("@") >= 0) {
email = "MAILTO:" + email;
}
}
attendee.id = email;
}
if (names.value.length > 0) {
attendee.commonName = names.value[0];
}
// append the attendee object to the list of attendees.
if (!attendee.isOrganizer || i > 1) {
attendees.push(attendee);
}
}
}
return attendees;
]]></getter>
</property>
<property name="organizer">
<getter><![CDATA[
Components.utils.import("resource:///modules/mailServices.js");
let inputField;
for (let i = 1; inputField = this.getInputElement(i); i++) {
let fieldValue = inputField.value;
if (fieldValue != "") {
// The inputfield already has a reference to the attendee
// object, we just need to fill in the name.
let attendee = inputField.attendee.clone();
//attendee.role = this.getRoleElement(i).getAttribute("role");
attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
// Organizers do not have a CUTYPE
attendee.userType = null;
// break the list of potentially many attendees back into individual names
let emailAddresses = {};
let names = {};
let fullNames = {};
MailServices.headerParser.parseHeadersWithArray(fieldValue,
emailAddresses,
names,
fullNames);
if (emailAddresses.value.length > 0) {
// if the new address has no 'mailto'-prefix but seems
// to look like an email-address, we prepend the prefix.
// this also allows for non-email-addresses.
let email = emailAddresses.value[0];
if (email.toLowerCase().indexOf("mailto:") != 0) {
if (email.indexOf("@") >= 0) {
email = "MAILTO:" + email;
}
}
attendee.id = email;
}
if (names.value.length > 0) {
attendee.commonName = names.value[0];
}
if (attendee.isOrganizer) {
return attendee;
}
}
}
return null;
]]></getter>
</property>
<method name="onBlurInput">
<parameter name="event"/>
<body><![CDATA[
Components.utils.import("resource:///modules/mailServices.js");
let emailAddresses = {};
let names = {};
let fullNames = {};
MailServices.headerParser.parseHeadersWithArray(event.target.value,
emailAddresses,
names,
fullNames);
if (emailAddresses.value.length > 1) {
let firstFullName = fullNames.value.shift();
event.target.value = firstFullName;
let insertAfterItem = this.getListItem(this.getRowByInputElement(event.target));
for each (let full in fullNames.value) {
insertAfterItem = this.appendNewRow(false, insertAfterItem);
let textinput = this.getInputFromListitem(insertAfterItem);
textinput.value = full;
}
}
]]></body>
</method>
<method name="_resolveListByName">
<parameter name="value"/>
<body><![CDATA[
Components.utils.import("resource:///modules/mailServices.js");
let emailAddresses = {};
let names = {};
let fullNames = {};
MailServices.headerParser.parseHeadersWithArray(value,
emailAddresses,
names,
fullNames);
let abDir = this._findListInAddrBooks(names.value[0]);
return abDir;
]]></body>
</method>
<method name="_findListInAddrBooks">
<parameter name="entryname"/>
<body><![CDATA[
Components.utils.import("resource:///modules/mailServices.js");
let allAddressBooks = MailServices.ab.directories;
while (allAddressBooks.hasMoreElements()) {
let abDir = null;
try {
abDir = allAddressBooks.getNext()
.QueryInterface(Components.interfaces.nsIAbDirectory);
} catch (ex) {
cal.WARN ("[eventDialog] Error Encountered" + ex);
}
if (abDir != null && abDir.supportsMailingLists) {
let childNodes = abDir.childNodes;
while (childNodes.hasMoreElements()) {
let dir = null;
try {
dir = childNodes.getNext().QueryInterface(Components.interfaces.nsIAbDirectory);
} catch (ex) {
cal.WARN ("[eventDialog] Error Encountered" + ex);
}
if (dir && dir.isMailList && (dir.dirName == entryname)) {
return dir;
}
}
}
}
return null;
]]></body>
</method>
<method name="_getListEntriesInt">
<parameter name="mailingList"/>
<parameter name="attendees"/>
<parameter name="allListsUri"/>
<body><![CDATA[
function in_list(aList, listid) {
for (var l=0;l<aList.length;l++){
if (aList[l]===listid) return true;
}
return false;
}
let addressLists = mailingList.addressLists;
for (var i=0;i<addressLists.length;i++) {
let abCard = addressLists.queryElementAt(i, Components.interfaces.nsIAbCard);
let thisId = abCard.primaryEmail;
if (abCard.displayName.length > 0) {
let rCn = abCard.displayName;
if (rCn.indexOf(",") >= 0) {
rCn = '"' + rCn + '"';
}
thisId = rCn + " <" + thisId + ">";
}
if (in_list(attendees, thisId)) continue;
if (abCard.displayName.length > 0) {
let ml = this._findListInAddrBooks(abCard.displayName);
if (null!=ml){
if (in_list(allListsUri, ml.URI)) continue;
allListsUri.push(ml.URI);
this._getListEntriesInt(ml, attendees, allListsUri);
continue;
}
}
attendees.push(thisId);
}
return attendees;
]]></body>
</method>
<method name="_getListEntries">
<parameter name="mailingList"/>
<body><![CDATA[
let attendees = [];
let allListsUri = [];
allListsUri.push(mailingList.URI);
this._getListEntriesInt(mailingList, attendees, allListsUri);
return attendees;
]]></body>
</method>
<method name="_fillListItemWithEntry">
<parameter name="listitem"/>
<parameter name="entry"/>
<parameter name="rowNumber"/>
<body><![CDATA[
let newAttendee = this.createAttendee(entry);
let input = document.getAnonymousElementByAttribute(listitem, "anonid", "input");
input.removeAttribute("disabled");
input.setAttribute("id", "attendeeCol3#" + rowNumber);
input.attendee = newAttendee;
input.value = entry;
input.setAttribute("value", entry);
input.setAttribute("dirty", "true");
if (input.getAttribute('focused') != '') {
input.removeAttribute('focused');
}
let roleStatusIcon = document.getAnonymousElementByAttribute(listitem, "anonid", "rolestatus-icon");
roleStatusIcon.removeAttribute("disabled");
roleStatusIcon.setAttribute("id", "attendeeCol1#" + rowNumber);
roleStatusIcon.setAttribute("class", "role-icon");
roleStatusIcon.setAttribute("role", newAttendee.role);
let userTypeIcon = document.getAnonymousElementByAttribute(listitem, "anonid", "usertype-icon");
userTypeIcon.removeAttribute("disabled");
userTypeIcon.setAttribute("id", "attendeeCol2#" + rowNumber);
userTypeIcon.setAttribute("cutype", newAttendee.userType);
]]></body>
</method>
<method name="resolvePotentialList">
<parameter name="input"/>
<body><![CDATA[
let fieldValue = input.value;
if (input.id.length > 0 && fieldValue.length > 0) {
let mailingList = this._resolveListByName(fieldValue);
if (mailingList) {
let entries = this._getListEntries(mailingList);
if (entries.length > 0) {
let currentIndex = parseInt(input.id.substr(13));
let template = document.getAnonymousElementByAttribute(this, "anonid", "item");
let currentNode = template.parentNode.childNodes[currentIndex];
this._fillListItemWithEntry(currentNode, entries[0], currentIndex);
entries.shift();
let nextNode = template.parentNode.childNodes[currentIndex+1];
currentIndex++;
for each (let entry in entries) {
currentNode = template.cloneNode(true);
template.parentNode.insertBefore(currentNode, nextNode);
this._fillListItemWithEntry(currentNode, entry, currentIndex);
currentIndex++;
}
this.mMaxAttendees += entries.length;
for (let i = currentIndex; i <= this.mMaxAttendees; i++) {
let row = template.parentNode.childNodes[i];
let roleStatusIcon = document.getAnonymousElementByAttribute(row, "anonid", "rolestatus-icon");
roleStatusIcon.setAttribute("id", "attendeeCol1#" + i);
let userTypeIcon = document.getAnonymousElementByAttribute(row, "anonid", "usertype-icon");
userTypeIcon.setAttribute("id", "attendeeCol2#" + i);
let input = document.getAnonymousElementByAttribute(row, "anonid", "input");
input.setAttribute("id", "attendeeCol3#" + i);
input.setAttribute("dirty", "true");
}
}
}
}
]]></body>
</method>
<method name="onModify">
<body><![CDATA[
Components.utils.import("resource:///modules/mailServices.js");
let list = [];
for (let i = 1; i <= this.mMaxAttendees; i++) {
// retrieve the string from the appropriate row
let input = this.getInputElement(i);
let fieldValue = input.value;
// parse the string to break this down to individual names and addresses
let email = "";
let emailAddresses = {};
let names = {};
let fullNames = {};
MailServices.headerParser.parseHeadersWithArray(fieldValue,
emailAddresses,
names,
fullNames);
if (emailAddresses.value.length > 0) {
// if the new address has no 'mailto'-prefix but seems
// to look like an email-address, we prepend the prefix.
// this also allows for non-email-addresses.
email = emailAddresses.value[0];
if (email.toLowerCase().indexOf("mailto:") != 0) {
if (email.indexOf("@") >= 0) {
email = "MAILTO:" + email;
}
}
}
let isdirty = false;
if (input.hasAttribute("dirty")) {
isdirty = input.getAttribute("dirty");
}
input.removeAttribute("dirty");
let entry = {
dirty: isdirty,
calid: email
};
list.push(entry);
}
let event = document.createEvent('Events');
event.initEvent('modify', true, false);
event.details = list;
this.dispatchEvent(event);
]]></body>
</method>
<method name="updateTooltip">
<parameter name="targetIcon"/>
<body><![CDATA[
// Function setting the tooltip of attendeeicons based on their role
if (targetIcon.className == "role-icon") {
let role = targetIcon.getAttribute("role");
// Set tooltip for rolenames
const roleMap = {
"REQ-PARTICIPANT": "required",
"OPT-PARTICIPANT": "optional",
"NON-PARTICIPANT": "nonparticipant",
"CHAIR": "chair"
}
let roleNameString = "event.attendee.role." + (role in roleMap ? roleMap[role] : "unknown");
let tooltip = cal.calGetString("calendar-event-dialog-attendees",
roleNameString,
(role in roleMap ? null : [role]));
targetIcon.setAttribute("tooltiptext", tooltip);
} else if (targetIcon.className == "usertype-icon") {
let cutype = targetIcon.getAttribute("cutype");
const cutypeMap = {
"INDIVIDUAL": "individual",
"GROUP": "group",
"RESOURCE": "resource",
"ROOM": "room",
// I've decided UNKNOWN will not be handled.
};
let cutypeString = "event.attendee.usertype." + (cutype in cutypeMap ? cutypeMap[cutype] : "unknown");
let tooltip = cal.calGetString("calendar-event-dialog-attendees",
cutypeString,
(cutypeString in cutypeMap ? null : [cutype]));
}
]]></body>
</method>
<property name="documentSize">
<getter><![CDATA[
return this.mRowHeight * this.mMaxAttendees;
]]></getter>
</property>
<method name="fitDummyRows">
<body><![CDATA[
var self = this;
var func = function attendees_list_fitDummyRows() {
self.calcContentHeight();
self.createOrRemoveDummyRows();
}
setTimeout(func, 0);
]]></body>
</method>
<method name="calcContentHeight">
<body><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var items = listbox.getElementsByTagNameNS('*', 'listitem');
this.mContentHeight = 0;
if (items.length > 0) {
var i = 0;
do {
this.mRowHeight = items[i].boxObject.height;
++i;
} while (i < items.length && !this.mRowHeight);
this.mContentHeight = this.mRowHeight * items.length;
}
]]></body>
</method>
<method name="createOrRemoveDummyRows">
<body><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var listboxHeight = listbox.boxObject.height;
// remove rows to remove scrollbar
var kids = listbox.childNodes;
for (var i = kids.length - 1; this.mContentHeight > listboxHeight && i >= 0; --i) {
if (kids[i].hasAttribute("_isDummyRow")) {
this.mContentHeight -= this.mRowHeight;
listbox.removeChild(kids[i]);
}
}
// add rows to fill space
if (this.mRowHeight) {
while (this.mContentHeight + this.mRowHeight < listboxHeight) {
this.createDummyItem(listbox);
this.mContentHeight += this.mRowHeight;
}
}
]]></body>
</method>
<method name="createDummyCell">
<parameter name="aParent"/>
<body><![CDATA[
var cell = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "listcell");
cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
if (aParent) {
aParent.appendChild(cell);
}
return cell;
]]></body>
</method>
<method name="createDummyItem">
<parameter name="aParent"/>
<body><![CDATA[
var titem = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "listitem");
titem.setAttribute("_isDummyRow", "true");
titem.setAttribute("class", "dummy-row");
for (var i = this.numColumns; i > 0; i--) {
this.createDummyCell(titem);
}
if (aParent) {
aParent.appendChild(titem);
}
return titem;
]]></body>
</method>
<!-- gets the next row from the top down -->
<method name="getNextDummyRow">
<body><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var kids = listbox.childNodes;
for (var i = 0; i < kids.length; ++i) {
if (kids[i].hasAttribute("_isDummyRow")) {
return kids[i];
}
}
return null;
]]></body>
</method>
<!-- This method returns the <xul:listitem> at row numer 'aRow' -->
<method name="getListItem">
<parameter name="aRow"/>
<body><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
if (listbox && aRow > 0) {
var listitems = listbox.getElementsByTagNameNS('*', 'listitem');
if (listitems && listitems.length >= aRow) {
return listitems[aRow - 1];
}
}
return 0;
]]></body>
</method>
<method name="getInputFromListitem">
<parameter name="aListItem"/>
<body><![CDATA[
return aListItem.getElementsByTagNameNS("*", "textbox")[0];
]]></body>
</method>
<method name="getRowByInputElement">
<parameter name="aElement"/>
<body><![CDATA[
var row = 0;
while (aElement && aElement.localName != "listitem") {
aElement = aElement.parentNode;
}
if (aElement) {
while (aElement) {
if (aElement.localName == "listitem") {
++row;
}
aElement = aElement.previousSibling;
}
}
return row;
]]></body>
</method>
<!-- This method returns the <xul:textbox> that contains
the name of the attendee at row number 'aRow' -->
<method name="getInputElement">
<parameter name="aRow"/>
<body><![CDATA[
return document.getAnonymousElementByAttribute(this, "id", "attendeeCol3#" + aRow);
]]></body>
</method>
<method name="getRoleElement">
<parameter name="aRow"/>
<body><![CDATA[
return document.getAnonymousElementByAttribute(this, "id", "attendeeCol1#" + aRow);
]]></body>
</method>
<method name="getStatusElement">
<parameter name="aRow"/>
<body><![CDATA[
return document.getAnonymousElementByAttribute(this, "id", "attendeeCol1#" + aRow);
]]></body>
</method>
<method name="getUserTypeElement">
<parameter name="aRow"/>
<body><![CDATA[
return document.getAnonymousElementByAttribute(this, "id", "attendeeCol2#" + aRow);
]]></body>
</method>
<method name="setFocus">
<parameter name="aRow"/>
<body><![CDATA[
var self = this;
var set_focus = function() {
var node;
if (typeof aRow == 'number') {
node = self.getListItem(aRow);
} else {
node = aRow;
}
// do we need to scroll in order to see the selected row?
var listbox =
document.getAnonymousElementByAttribute(
self, "anonid", "listbox");
var firstVisibleRow = listbox.getIndexOfFirstVisibleRow();
var numOfVisibleRows = listbox.getNumberOfVisibleRows();
if (aRow <= firstVisibleRow) {
listbox.scrollToIndex(aRow - 1);
} else {
if (aRow - 1 >= (firstVisibleRow + numOfVisibleRows)) {
listbox.scrollToIndex(aRow - numOfVisibleRows);
}
}
var input =
document.getAnonymousElementByAttribute(
node, "anonid", "input");
input.focus();
}
setTimeout(set_focus, 0);
]]></body>
</method>
<property name="firstVisibleRow">
<getter><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
return listbox.getIndexOfFirstVisibleRow();
]]></getter>
</property>
<method name="createAttendee">
<body><![CDATA[
var attendee = createAttendee();
attendee.id = "";
attendee.rsvp = "TRUE";
attendee.role = "REQ-PARTICIPANT";
attendee.participationStatus = "NEEDS-ACTION";
return attendee;
]]></body>
</method>
<property name="numColumns">
<getter><![CDATA[
if (!this.mNumColumns) {
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var listCols = listbox.getElementsByTagNameNS('*', 'listcol');
this.mNumColumns = listCols.length;
if (!this.mNumColumns) {
this.mNumColumns = 1;
}
}
return this.mNumColumns;
]]></getter>
</property>
<property name="ratio">
<setter><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var rowcount = listbox.getRowCount();
listbox.scrollToIndex(Math.floor(rowcount * val));
return val;
]]></setter>
</property>
<method name="arrowHit">
<parameter name="aElement"/>
<parameter name="aDirection"/>
<body><![CDATA[
var row = this.getRowByInputElement(aElement) + aDirection;
if (row) {
if (row > this.mMaxAttendees) {
this.appendNewRow(true);
} else {
var input = this.getInputElement(row);
if (input.hasAttribute("disabled")) {
return;
}
this.setFocus(row);
}
var event = document.createEvent('Events');
event.initEvent('rowchange', true, false);
event.details = row;
this.dispatchEvent(event);
}
]]></body>
</method>
<method name="deleteHit">
<parameter name="aElement"/>
<body><![CDATA[
// don't delete the row if it's the last one remaining
if (this.mMaxAttendees <= 1) {
return;
}
var row = this.getRowByInputElement(aElement);
this.deleteRow(row);
if (row > 1) {
row = row - 1;
}
this.setFocus(row);
this.onModify();
var event = document.createEvent('Events');
event.initEvent('rowchange', true, false);
event.details = row;
this.dispatchEvent(event);
]]></body>
</method>
<method name="deleteRow">
<parameter name="aRow"/>
<body><![CDATA[
// reset id's in order to not break the sequence
var maxAttendees = this.mMaxAttendees;
this.removeRow(aRow);
var numberOfCols = this.numColumns;
for (var row = aRow + 1; row <= maxAttendees; row++) {
for (var col = 1; col <= numberOfCols; col++) {
var colID = "attendeeCol" + col + "#" + row;
var elem = document.getAnonymousElementByAttribute(this, "id", colID);
if (elem) {
elem.setAttribute("id", "attendeeCol" + col + "#" + (row - 1));
}
}
}
]]></body>
</method>
<method name="removeRow">
<parameter name="aRow"/>
<body><![CDATA[
var listbox =
document.getAnonymousElementByAttribute(
this, "anonid", "listbox");
var nodeToRemove = this.getListItem(aRow);
nodeToRemove.parentNode.removeChild(nodeToRemove);
this.fitDummyRows();
this.mMaxAttendees--;
]]></body>
</method>
</implementation>
<handlers>
<handler event="click" button="0"><![CDATA[
function cycle(values, current) {
let nextIndex = (values.indexOf(current) + 1) % values.length;
return values[nextIndex];
}
let target = event.originalTarget;
if (target.className == "role-icon") {
if (target.getAttribute("disabled") != "true") {
const roleCycle = [ "REQ-PARTICIPANT", "OPT-PARTICIPANT",
"NON-PARTICIPANT", "CHAIR" ];
let nextValue = cycle(roleCycle, target.getAttribute("role"));
target.setAttribute("role", nextValue);
this.updateTooltip(target);
}
} else if (target.className == "status-icon") {
if (target.getAttribute("disabled") != "true") {
const statusCycle = [ "ACCEPTED", "DECLINED", "TENTATIVE" ];
let nextValue = cycle(statusCycle, target.getAttribute("status"));
target.setAttribute("status", nextValue);
this.updateTooltip(target);
}
} else if (target.className == "usertype-icon") {
let fieldNum = target.getAttribute("id").split("#")[1];
let inputField = this.getInputElement(fieldNum);
if (target.getAttribute("disabled") != "true" &&
!inputField.attendee.isOrganizer) {
const cutypeCycle = [ "INDIVIDUAL", "GROUP", "RESOURCE", "ROOM" ];
let nextValue = cycle(cutypeCycle, target.getAttribute("cutype"));
target.setAttribute("cutype", nextValue);
this.updateTooltip(target);
}
} else if (this.mIsReadOnly || this.mIsInvitation || target == null ||
(target.localName != "listboxbody" &&
target.localName != "listcell" &&
target.localName != "listitem")) {
// These are cases where we don't want to append a new row, keep
// them here so we can put the rest in the else case.
} else {
let lastInput = this.getInputElement(this.mMaxAttendees);
if (lastInput && lastInput.value) {
this.appendNewRow(true);
}
}
]]></handler>
<handler event="popupshown"><![CDATA[
this.mPopupOpen = true;
]]></handler>
<handler event="popuphidden"><![CDATA[
this.mPopupOpen = false;
]]></handler>
<handler event="keydown"><![CDATA[
if (this.mIsReadOnly || this.mIsInvitation) {
return;
}
if (event.originalTarget.localName == "input") {
switch (event.keyCode) {
case KeyEvent.DOM_VK_DELETE:
case KeyEvent.DOM_VK_BACK_SPACE:
if (!event.originalTarget.value) {
this.deleteHit(event.originalTarget);
}
event.stopPropagation();
break;
case KeyEvent.DOM_VK_RETURN:
this.arrowHit(event.originalTarget, 1);
event.stopPropagation();
event.preventDefault();
break;
}
}
]]></handler>
<handler event="keypress" phase="capturing"><![CDATA[
// In case we're currently showing the autocompletion popup
// don't care about keypress-events and let them go. Otherwise
// this event indicates the user wants to travel between
// the different attendees. In this case we set the focus
// appropriately and stop the event propagation.
if (this.mPopupOpen || this.mIsReadOnly || this.mIsInvitation) {
return;
}
if (event.originalTarget.localName == "input") {
switch (event.keyCode) {
case KeyEvent.DOM_VK_UP:
this.arrowHit(event.originalTarget, -1);
event.stopPropagation();
break;
case KeyEvent.DOM_VK_DOWN:
this.arrowHit(event.originalTarget, 1);
event.stopPropagation();
break;
case KeyEvent.DOM_VK_TAB:
this.arrowHit(event.originalTarget, event.shiftKey ? -1 : +1);
event.stopPropagation();
event.preventDefault();
break;
case KeyEvent.DOM_VK_RETURN:
event.stopPropagation();
event.preventDefault();
break;
}
}
]]></handler>
</handlers>
</binding>
<!-- the 'selection-bar' binding implements the vertical bar that provides
a visual indication for the time range the event is configured for. -->
<binding id="selection-bar">
<content>
<xul:scrollbox anonid="scrollbox" width="0" orient="horizontal" flex="1">
<xul:box class="selection-bar" anonid="selection-bar">
<xul:box class="selection-bar-left" anonid="leftbox"/>
<xul:spacer class="selection-bar-spacer" flex="1"/>
<xul:box class="selection-bar-right" anonid="rightbox"/>
</xul:box>
</xul:scrollbox>
</content>
<implementation>
<field name="mRange">0</field>
<field name="mStartHour">0</field>
<field name="mEndHour">24</field>
<field name="mContentWidth">0</field>
<field name="mHeaderHeight">0</field>
<field name="mRatio">0</field>
<field name="mBaseDate">null</field>
<field name="mStartDate">null</field>
<field name="mEndDate">null</field>
<field name="mMouseX">0</field>
<field name="mMouseY">0</field>
<field name="mDragState">0</field>
<field name="mMargin">0</field>
<field name="mWidth">0</field>
<field name="mForce24Hours">false</field>
<field name="mZoomFactor">100</field>
<!-- constant that defines at which ratio an event is clipped, when moved or resized -->
<field name="mfClipRatio">0.7</field>
<field name="mLeftBox"/>
<field name="mRightBox"/>
<field name="mSelectionbar"/>
<property name="zoomFactor">
<getter><![CDATA[
return this.mZoomFactor;
]]></getter>
<setter><![CDATA[
this.mZoomFactor = val;
return val;
]]></setter>
</property>
<property name="force24Hours">
<getter><![CDATA[
return this.mForce24Hours;
]]></getter>
<setter><![CDATA[
this.mForce24Hours = val;
this.initTimeRange();
this.update();
return val;
]]></setter>
</property>
<property name="ratio">
<setter><![CDATA[
this.mRatio = val;
this.update();
return val;
]]></setter>
</property>
<constructor><![CDATA[
this.initTimeRange();
// The basedate is the date/time from which the display
// of the timebar starts. The range is the number of days
// we should be able to show. the start- and enddate
// is the time the event is scheduled for.
this.mRange = Number(this.getAttribute("range"));
this.mSelectionbar =
document.getAnonymousElementByAttribute(
this, "anonid", "selection-bar");
]]></constructor>
<property name="baseDate">
<setter><![CDATA[
// we need to convert the date/time in question in
// order to calculate with hours that are aligned
// with our timebar display.
var kDefaultTimezone = calendarDefaultTimezone();
this.mBaseDate = val.getInTimezone(kDefaultTimezone);
this.mBaseDate.isDate = true;
this.mBaseDate.makeImmutable();
return val;
]]></setter>
</property>
<property name="startDate">
<setter><![CDATA[
// currently we *always* set the basedate to be
// equal to the startdate. we'll most probably
// want to change this later.
this.baseDate = val;
// we need to convert the date/time in question in
// order to calculate with hours that are aligned
// with our timebar display.
var kDefaultTimezone = calendarDefaultTimezone();
this.mStartDate = val.getInTimezone(kDefaultTimezone);
this.mStartDate.makeImmutable();
return val;
]]></setter>
<getter><![CDATA[
return this.mStartDate;
]]></getter>
</property>
<property name="endDate">
<setter><![CDATA[
// we need to convert the date/time in question in
// order to calculate with hours that are aligned
// with our timebar display.
var kDefaultTimezone = calendarDefaultTimezone();
this.mEndDate = val.getInTimezone(kDefaultTimezone);
if (this.mEndDate.isDate) {
this.mEndDate.day += 1;
}
this.mEndDate.makeImmutable();
return val;
]]></setter>
<getter><![CDATA[
return this.mEndDate;
]]></getter>
</property>
<property name="leftdragWidth">
<getter><![CDATA[
if (!this.mLeftBox) {
this.mLeftBox =
document.getAnonymousElementByAttribute(
this, "anonid", "leftbox");
}
return this.mLeftBox.boxObject.width;
]]></getter>
</property>
<property name="rightdragWidth">
<getter><![CDATA[
if (!this.mRightBox) {
this.mRightBox =
document.getAnonymousElementByAttribute(
this, "anonid", "rightbox");
}
return this.mRightBox.boxObject.width;
]]></getter>
</property>
<method name="init">
<parameter name="width"/>
<parameter name="height"/>
<body><![CDATA[
this.mContentWidth = width;
this.mHeaderHeight = height + 2;
this.mMargin = 0;
this.update();
]]></body>
</method>
<!-- given some specific date this method calculates the
corrposonding offset in fractional hours -->
<method name="date2offset">
<parameter name="date"/>
<body><![CDATA[
var num_hours = this.mEndHour - this.mStartHour;
var diff = date.subtractDate(this.mBaseDate);
var offset = diff.days * num_hours;
var hours = (diff.hours - this.mStartHour) + (diff.minutes / 60.0);
if (hours < 0) {
hours = 0;
}
if (hours > num_hours) {
hours = num_hours;
}
offset += hours;
return offset;
]]></body>
</method>
<method name="update">
<body><![CDATA[
if (!this.mStartDate || !this.mEndDate) {
return;
}
// Calculate the relation of startdate/basedate and enddate/startdate.
var offset = this.mStartDate.subtractDate(this.mBaseDate);
var duration = this.mEndDate.subtractDate(this.mStartDate);
// Calculate how much pixels a single hour and a single day take up.
var num_hours = this.mEndHour - this.mStartHour;
var hour_width = this.mContentWidth / num_hours;
// Calculate the offset in fractional hours that corrospond
// to our start- and end-time.
var start_offset_in_hours = this.date2offset(this.mStartDate);
var end_offset_in_hours = this.date2offset(this.mEndDate);
var duration_in_hours = end_offset_in_hours - start_offset_in_hours;
// Calculate width & margin for the selection bar based on the
// relation of startdate/basedate and enddate/startdate.
// This is a simple conversion from hours to pixels.
this.mWidth = duration_in_hours * hour_width;
var totaldragwidths = this.leftdragWidth + this.rightdragWidth;
if (this.mWidth < totaldragwidths) {
this.mWidth = totaldragwidths;
}
this.mMargin = start_offset_in_hours * hour_width;
// Calculate the difference between content and container in pixels.
// The container is the window showing this control, the content is the
// total number of pixels the selection bar can theoretically take up.
var total_width = this.mContentWidth * this.mRange - this.parentNode.boxObject.width;
// Calculate the current scroll offset.
var offset = Math.floor(total_width * this.mRatio);
// The final margin is the difference between the date-based margin
// and the scroll-based margin.
this.mMargin -= offset;
// Set the styles based on the calculations above for the 'selection-bar'.
var style = "width: " + this.mWidth +
"px; -moz-margin-start: " + this.mMargin +
"px; margin-top: " + this.mHeaderHeight + "px;";
this.mSelectionbar.setAttribute("style", style);
var event = document.createEvent('Events');
event.initEvent('timechange', true, false);
event.startDate = this.mStartDate;
event.endDate = this.mEndDate.clone();
if (event.endDate.isDate) {
event.endDate.day--;
}
event.endDate.makeImmutable();
this.dispatchEvent(event);
]]></body>
</method>
<method name="setWidth">
<parameter name="width"/>
<body><![CDATA[
var scrollbox =
document.getAnonymousElementByAttribute(
this, "anonid", "scrollbox");
scrollbox.setAttribute("width", width);
]]></body>
</method>
<method name="initTimeRange">
<body><![CDATA[
if (this.force24Hours) {
this.mStartHour = 0;
this.mEndHour = 24;
} else {
this.mStartHour = getPrefSafe("calendar.view.daystarthour", 8);
this.mEndHour = getPrefSafe("calendar.view.dayendhour", 19);
}
]]></body>
</method>
<method name="moveTime">
<parameter name="time"/>
<parameter name="delta"/>
<parameter name="doclip"/>
<body><![CDATA[
var newTime = time.clone();
var clip_minutes = 60 * this.zoomFactor / 100;
if (newTime.isDate) {
clip_minutes = 60 * 24;
}
var num_hours = this.mEndHour - this.mStartHour;
var hour_width = this.mContentWidth / num_hours;
var minutes_per_pixel = 60 / hour_width;
var minute_shift = minutes_per_pixel * delta;
var isClipped = Math.abs(minute_shift) >= (this.mfClipRatio * clip_minutes);
if (isClipped) {
if (delta > 0) {
if (time.isDate) {
newTime.day++;
} else {
if (doclip) {
newTime.minute -= newTime.minute % clip_minutes;
}
newTime.minute += clip_minutes;
}
} else if (delta < 0) {
if (time.isDate) {
newTime.day--;
} else {
if (doclip) {
newTime.minute -= newTime.minute % clip_minutes;
}
newTime.minute -= clip_minutes;
}
}
}
if (!newTime.isDate) {
if (newTime.hour < this.mStartHour) {
newTime.hour = this.mEndHour - 1;
newTime.day--;
}
if (newTime.hour >= this.mEndHour) {
newTime.hour = this.mStartHour;
newTime.day++;
}
}
return newTime;
]]></body>
</method>
</implementation>
<handlers>
<handler event="mousedown"><![CDATA[
var element = event.target;
this.mMouseX = event.screenX;
var mouseX = event.clientX - element.boxObject.x;
if (mouseX >= this.mMargin) {
if (mouseX <= (this.mMargin + this.mWidth)) {
if (mouseX <= (this.mMargin + this.leftdragWidth)) {
// Move the startdate only...
window.setCursor("w-resize");
this.mDragState = 2;
} else if (mouseX >= (this.mMargin + this.mWidth - (this.rightdragWidth))) {
// Move the enddate only..
window.setCursor("e-resize");
this.mDragState = 3;
} else {
// Move the startdate and the enddate
this.mDragState = 1;
window.setCursor("grab");
}
}
}
]]></handler>
<handler event="mousemove"><![CDATA[
var mouseX = event.screenX;
if (this.mDragState == 1) {
// Move the startdate and the enddate
var delta = mouseX - this.mMouseX;
var newStart = this.moveTime(this.mStartDate, delta, false);
if (newStart.compare(this.mStartDate) != 0) {
newEnd = this.moveTime(this.mEndDate, delta, false);
// We need to adapt this date in case we're dealing with
// an all-day event. This is because setting 'endDate' will
// automatically add one day extra for all-day events.
if (newEnd.isDate) {
newEnd.day--;
}
this.startDate = newStart;
this.endDate = newEnd;
this.mMouseX = mouseX;
this.update();
}
} else if (this.mDragState == 2) {
// Move the startdate only...
var delta = event.screenX - this.mSelectionbar.boxObject.screenX;
var newStart = this.moveTime(this.mStartDate, delta, true);
if (newStart.compare(this.mEndDate) >= 0) {
if (!this.mStartDate.isDate) {
newStart = this.mEndDate;
}
else{
return;
}
}
if (newStart.compare(this.mStartDate) != 0) {
this.startDate = newStart;
this.update();
}
} else if (this.mDragState == 3) {
// Move the enddate only..
var delta = mouseX - (this.mSelectionbar.boxObject.screenX +
this.mSelectionbar.boxObject.width);
var newEnd = this.moveTime(this.mEndDate, delta, true);
if (newEnd.compare(this.mStartDate) < 0) {
newEnd = this.mStartDate;
}
if (newEnd.compare(this.mEndDate) != 0) {
// We need to adapt this date in case we're dealing with
// an all-day event. This is because setting 'endDate' will
// automatically add one day extra for all-day events.
if (newEnd.isDate) {
newEnd.day--;
}
// Don't allow all-day events to be shorter than a single day.
if (!newEnd.isDate || (newEnd.compare(this.startDate) >= 0)) {
this.endDate = newEnd;
this.update();
}
}
}
]]></handler>
<handler event="mouseup"><![CDATA[
this.mDragState = 0;
window.setCursor("auto");
]]></handler>
</handlers>
</binding>
</bindings>