1 <?xml version="1.0"?>
2 <!-- ***** BEGIN LICENSE BLOCK *****
3 - Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 -
5 - The contents of this file are subject to the Mozilla Public License Version
6 - 1.1 (the "License"); you may not use this file except in compliance with
7 - the License. You may obtain a copy of the License at
8 - http://www.mozilla.org/MPL/
9 -
10 - Software distributed under the License is distributed on an "AS IS" basis,
11 - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 - for the specific language governing rights and limitations under the
13 - License.
14 -
15 - The Original Code is Sun Microsystems code.
16 -
17 - The Initial Developer of the Original Code is Sun Microsystems.
18 - Portions created by the Initial Developer are Copyright (C) 2006
19 - the Initial Developer. All Rights Reserved.
20 -
21 - Contributor(s):
22 - Michael Buettner <michael.buettner@sun.com>
23 - Philipp Kewisch <mozilla@kewis.ch>
24 -
25 - Alternatively, the contents of this file may be used under the terms of
26 - either the GNU General Public License Version 2 or later (the "GPL"), or
27 - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 - in which case the provisions of the GPL or the LGPL are applicable instead
29 - of those above. If you wish to allow use of your version of this file only
30 - under the terms of either the GPL or the LGPL, and not to allow others to
31 - use your version of this file under the terms of the MPL, indicate your
32 - decision by deleting the provisions above and replace them with the notice
33 - and other provisions required by the GPL or the LGPL. If you do not delete
34 - the provisions above, a recipient may use your version of this file under
35 - the terms of any one of the MPL, the GPL or the LGPL.
36 -
37 - ***** END LICENSE BLOCK ***** -->
38
39 <!DOCTYPE dialog [
40 <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
41 <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
42 <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/sun-calendar-event-dialog.dtd" > %dtd3;
43 <!ENTITY % calendar-event-dialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %calendar-event-dialogDTD;
44 ]>
45
46 <bindings xmlns="http://www.mozilla.org/xbl"
47 xmlns:xbl="http://www.mozilla.org/xbl"
48 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
49 <binding id="attendees-list">
50 <content>
51 <xul:listbox anonid="listbox" seltype="multiple" rows="-1" flex="1">
52 <xul:listcols>
53 <xul:listcol/>
54 <xul:listcol flex="1"/>
55 </xul:listcols>
56 <xul:listitem anonid="item" class="addressingWidgetItem" allowevents="true">
57 <xul:listcell class="addressingWidgetCell" align="center" pack="center">
58 <xul:image id="attendeeCol1#1" anonid="icon"/>
59 </xul:listcell>
60 <xul:listcell class="addressingWidgetCell">
61 <xul:textbox id="attendeeCol2#1"
62 anonid="input"
63 class="plain textbox-addressingWidget uri-element"
64 type="autocomplete"
65 flex="1"
66 searchSessions="addrbook"
67 timeout="300"
68 maxrows="4"
69 autoFill="true"
70 autoFillAfterMatch="true"
71 forceComplete="true"
72 minResultsForPopup="1"
73 ignoreBlurWhileSearching="true"
74 oninput="this.setAttribute('dirty', 'true');">
75 <xul:image class="person-icon" onclick="this.parentNode.select();"/>
76 </xul:textbox>
77 </xul:listcell>
78 </xul:listitem>
79 </xul:listbox>
80 </content>
81
82 <implementation>
83 <field name="mMaxAttendees">0</field>
84 <field name="mContentHeight">0</field>
85 <field name="mRowHeight">0</field>
86 <field name="mNumColumns">0</field>
87 <field name="mIOService">null</field>
88 <field name="mDirectoryServerObserver">null</field>
89 <field name="mHeaderParser">null</field>
90 <field name="mPrefs">null</field>
91 <field name="mIsOffline">0</field>
92 <field name="mLDAPSession">null</field>
93 <field name="mSessionAdded">0</field>
94 <field name="mOrganizerID">null</field>
95 <field name="mIsReadOnly">false</field>
96 <field name="mIsInvitation">false</field>
97 <field name="mPopupOpen">false</field>
98
99 <constructor><![CDATA[
100 this.mMaxAttendees = 0;
101
102 var self = this;
103 var load = function loadHandler() {
104 self.onLoad();
105 };
106 window.addEventListener("load", load, true);
107 var unload = function attendeeListBinding_unloadHandler() {
108 self.onUnload();
109 };
110 window.addEventListener("unload", unload, true);
111
112 var observer = {
113 observe: function DSO_observe(subject,
114 topic,
115 value) {
116 // catch the exception and ignore it, so that if LDAP setup
117 // fails, the entire window doesn't get horked
118 try {
119 self.setupAutocomplete();
120 }
121 catch (ex) {
122 }
123 }
124 }
125
126 this.mDirectoryServerObserver = observer;
127
128 var component = Components.classes["@mozilla.org/messenger/headerparser;1"];
129 if (component) {
130 this.mHeaderParser = component.getService(Components.interfaces.nsIMsgHeaderParser);
131 }
132
133 // First get the preferences service
134 try {
135 this.mPrefs = Components.classes["@mozilla.org/preferences-service;1"]
136 .getService(Components.interfaces.nsIPrefService)
137 .getBranch(null);
138 this.mPrefs = this.mPrefs.QueryInterface(Components.interfaces.nsIPrefBranch2);
139 } catch (ex) {
140 }
141
142 this.mIOService = Components.classes["@mozilla.org/network/io-service;1"]
143 .getService(Components.interfaces.nsIIOService);
144 this.mIsOffline = this.mIOService.offline;
145 ]]></constructor>
146
147 <method name="onLoad">
148 <body><![CDATA[
149 var listbox =
150 document.getAnonymousElementByAttribute(
151 this, "anonid", "listbox");
152 var template =
153 document.getAnonymousElementByAttribute(
154 this, "anonid", "item");
155
156 // we need to enforce several layout constraints which can't be modelled
157 // with plain xul and css, at least as far as i know.
158 const kStylesheet = "chrome://calendar/content/sun-calendar-event-dialog.css";
159 for each (var stylesheet in document.styleSheets) {
160 if (stylesheet.href == kStylesheet) {
161 // the height of the text blocks contained in the grid items needs
162 // to have the same height as the items of the attendee-list.
163 var height = template.boxObject.height - 1;
164 stylesheet.insertRule(".freebusy-grid { min-height: " + height + "px; }", 0);
165 break;
166 }
167 }
168
169 this.onInitialize();
170
171 // this trigger the continous update chain, which
172 // effectively calls this.onModify() on predefined
173 // time intervals [each second].
174 var self = this;
175 var callback = function() {
176 setTimeout(callback, 1000);
177 self.onModify();
178 }
179 callback();
180 ]]></body>
181 </method>
182
183 <method name="onInitialize">
184 <body><![CDATA[
185 var args = window.arguments[0];
186 var organizer = args.organizer;
187 var attendees = args.attendees;
188 var calendar = args.calendar;
189
190 // set 'mIsReadOnly' if the calendar is read-only
191 if (calendar && calendar.readOnly) {
192 this.mIsReadOnly = true;
193 }
194
195 // assume we're the organizer [in case that the calendar
196 // does not support the concept of identities].
197 this.mIsInvitation = false;
198 this.mOrganizerID = ((organizer && organizer.id) ? organizer.id : "");
199 try {
200 // temporary hack unless all group scheduling features are supported
201 // by the caching facade (calCachedCalendar):
202 var provider = calendar.getProperty("private.wcapCalendar")
203 .QueryInterface(Components.interfaces.calIWcapCalendar);
204 this.mIsInvitation = provider.isInvitation(args.item);
205 if (this.mOrganizerID.length == 0) {
206 this.mOrganizerID = provider.ownerId; // sensible default
207 }
208 }
209 catch (e) {
210 }
211
212 var listbox =
213 document.getAnonymousElementByAttribute(
214 this, "anonid", "listbox");
215 var template =
216 document.getAnonymousElementByAttribute(
217 this, "anonid", "item");
218 template.focus();
219
220 if (this.mIsReadOnly || this.mIsInvitation) {
221 listbox.setAttribute("disabled", "true");
222 }
223
224 // TODO: the organizer should show up in the attendee list, but this information
225 // should be based on the organizer contained in the appropriate field of calIItemBase.
226 // This is currently not supported, since we're still missing calendar identities.
227 if (this.mOrganizerID && this.mOrganizerID != "") {
228 if (!organizer) {
229 organizer = this.createAttendee();
230 organizer.id = this.mOrganizerID;
231 organizer.role = "CHAIR";
232 organizer.participationStatus = "ACCEPTED";
233 } else {
234 if (!organizer.id) {
235 organizer.id = this.mOrganizerID;
236 }
237 if (!organizer.role) {
238 organizer.role = "CHAIR";
239 }
240 if (!organizer.participationStatus) {
241 organizer.participationStatus = "ACCEPTED";
242 }
243 }
244 try {
245 // temporary hack unless all group scheduling features are supported
246 // by the caching facade (calCachedCalendar):
247 var provider = calendar.getProperty("private.wcapCalendar")
248 .QueryInterface(Components.interfaces.calIWcapCalendar);
249 var props = provider.getCalendarProperties("X-S1CS-CALPROPS-COMMON-NAME", {});
250 if (props.length > 0) {
251 if(organizer.commonName.length <= 0) {
252 organizer.commonName = props[0];
253 }
254 }
255 }
256 catch (e) {
257 }
258 this.appendAttendee(organizer, listbox, template, true);
259 }
260
261 var numRowsAdded = 0;
262 if (attendees.length > 0) {
263 for each (var attendee in attendees) {
264 this.appendAttendee(attendee, listbox, template, false);
265 numRowsAdded++;
266 }
267 }
268 if (numRowsAdded == 0) {
269 this.appendAttendee(null, listbox, template, false);
270 }
271
272 // detach the template item from the listbox, but hold the reference.
273 // until this function returns we add at least a single copy of this template back again.
274 listbox.removeChild(template);
275
276 this.addDirectoryServerObserver();
277
278 this.setFocus(this.mMaxAttendees);
279 ]]></body>
280 </method>
281
282 <method name="onUnload">
283 <body><![CDATA[
284 this.removeDirectoryServerObserver();
285 this.releaseAutoCompleteState();
286 this.mIOService = null;
287 this.mLDAPSession = null;
288 ]]></body>
289 </method>
290
291 <!-- appends a new row using an existing attendee structure -->
292 <method name="appendAttendee">
293 <parameter name="aAttendee"/>
294 <parameter name="aParentNode"/>
295 <parameter name="aTemplateNode"/>
296 <parameter name="aDisableIfOrganizer"/>
297 <body><![CDATA[
298 // create a new listbox item and append it to our parent control.
299 var newNode = aTemplateNode.cloneNode(true);
300
301 var input =
302 document.getAnonymousElementByAttribute(
303 newNode, "anonid", "input");
304 var icon =
305 document.getAnonymousElementByAttribute(
306 newNode, "anonid", "icon");
307
308 // We always clone the first row. The problem is that the first row
309 // could be focused. When we clone that row, we end up with a cloned
310 // XUL textbox that has a focused attribute set. Therefore we think
311 // we're focused and don't properly refocus. The best solution to this
312 // would be to clone a template row that didn't really have any presentation,
313 // rather than using the real visible first row of the listbox.
314 // For now we'll just put in a hack that ensures the focused attribute
315 // is never copied when the node is cloned.
316 if (input.getAttribute('focused') != '') {
317 input.removeAttribute('focused');
318 }
319
320 aParentNode.appendChild(newNode);
321
322 // the template could have its fields disabled,
323 // that's why we need to reset their status.
324 input.removeAttribute("disabled");
325 icon.removeAttribute("disabled");
326
327 if (this.mIsReadOnly || this.mIsInvitation) {
328 input.setAttribute("disabled", "true");
329 icon.setAttribute("disabled", "true");
330 }
331
332 // disable the input-field [name <email>] if this attendee
333 // appears to be the organizer.
334 if (aDisableIfOrganizer) {
335 if (aAttendee) {
336 if (this.mOrganizerID && this.mOrganizerID != "") {
337 if (aAttendee.id.toLowerCase() == this.mOrganizerID.toLowerCase()) {
338 input.setAttribute("disabled", "true");
339 }
340 }
341 }
342 }
343
344 this.mMaxAttendees++;
345 var rowNumber = this.mMaxAttendees;
346 if (rowNumber >= 0) {
347 icon.setAttribute("id", "attendeeCol1#" + rowNumber);
348 input.setAttribute("id", "attendeeCol2#" + rowNumber);
349 }
350
351 if (!aAttendee) {
352 aAttendee = this.createAttendee();
353 }
354
355 // construct the display string from common name and/or email address.
356 var inputValue = aAttendee.commonName;
357 var regexp = new RegExp("^mailto:(.*)", "i");
358 if (inputValue && this.mHeaderParser) {
359 var email = aAttendee.id;
360 if (email && email.length) {
361 if (regexp.test(email)) {
362 inputValue += ' <' + RegExp.$1 + '>';
363 } else {
364 inputValue += ' <' + email + '>';
365 }
366 }
367 } else {
368 var email = aAttendee.id;
369 if (email && email.length) {
370 if (regexp.test(email)) {
371 inputValue = RegExp.$1;
372 } else {
373 inputValue = email;
374 }
375 }
376 }
377
378 // remove leading spaces
379 while (inputValue && inputValue[0] == " " ) {
380 inputValue = inputValue.substring(1, inputValue.length);
381 }
382
383 input.setAttribute("value", inputValue);
384 input.value = inputValue;
385 input.attendee = aAttendee;
386 input.setAttribute("dirty", "true");
387
388 if (aAttendee) {
389 if (this.mOrganizerID && this.mOrganizerID != "") {
390 if (aAttendee.id.toLowerCase() == this.mOrganizerID.toLowerCase()) {
391 icon.setAttribute("class", "status-icon");
392 icon.setAttribute("status", aAttendee.participationStatus);
393 return true;
394 }
395 }
396 }
397
398 icon.setAttribute("class", "role-icon");
399 icon.setAttribute("role", aAttendee.role);
400
401 return true;
402 ]]></body>
403 </method>
404
405 <method name="appendNewRow">
406 <parameter name="aSetFocus"/>
407 <body><![CDATA[
408 var listbox =
409 document.getAnonymousElementByAttribute(
410 this, "anonid", "listbox");
411 var listitem1 = this.getListItem(1);
412
413 if (listbox && listitem1) {
414 var newAttendee = this.createAttendee();
415 var nextDummy = this.getNextDummyRow();
416 var newNode = listitem1.cloneNode(true);
417
418 var input =
419 document.getAnonymousElementByAttribute(
420 newNode, "anonid", "input");
421 var icon =
422 document.getAnonymousElementByAttribute(
423 newNode, "anonid", "icon");
424
425 // the template could have its fields disabled,
426 // that's why we need to reset their status.
427 input.removeAttribute("disabled");
428 icon.removeAttribute("disabled");
429
430 if (this.mIsReadOnly || this.mIsInvitation) {
431 input.setAttribute("disabled", "true");
432 icon.setAttribute("disabled", "true");
433 }
434
435 this.mMaxAttendees++;
436 var rowNumber = this.mMaxAttendees;
437 if (rowNumber >= 0) {
438 icon.setAttribute("id", "attendeeCol1#" + rowNumber);
439 input.setAttribute("id", "attendeeCol2#" + rowNumber);
440 }
441
442 input.value = null;
443 input.removeAttribute("value");
444 input.attendee = newAttendee;
445
446 // this copies the autocomplete sessions list from first attendee
447 if (this.mLDAPSession && this.mSessionAdded) {
448 input.syncSessions(document.getElementById('attendeeCol2#1'));
449 }
450
451 // set role and participation status
452 //status.setAttribute("status", newAttendee.participationStatus);
453 //role.setAttribute("role", newAttendee.role);
454 icon.setAttribute("class", "role-icon");
455 icon.setAttribute("role", "REQ-PARTICIPANT");
456
457 // We always clone the first row. The problem is that the first row
458 // could be focused. When we clone that row, we end up with a cloned
459 // XUL textbox that has a focused attribute set. Therefore we think
460 // we're focused and don't properly refocus. The best solution to this
461 // would be to clone a template row that didn't really have any presentation,
462 // rather than using the real visible first row of the listbox.
463 // For now we'll just put in a hack that ensures the focused attribute
464 // is never copied when the node is cloned.
465 if (input.getAttribute('focused') != '') {
466 input.removeAttribute('focused');
467 }
468
469 if (nextDummy) {
470 listbox.replaceChild(newNode, nextDummy);
471 } else {
472 listbox.appendChild(newNode);
473 }
474
475 // focus on new input widget
476 if (aSetFocus) {
477 this.setFocus(this.mMaxAttendees);
478 }
479
480 this.onModify();
481 }
482 ]]></body>
483 </method>
484
485 <property name="attendees">
486 <getter><![CDATA[
487 var attendees = [];
488
489 var inputField;
490 for (var i = 1; inputField = this.getInputElement(i); i++) {
491 var fieldValue = inputField.value;
492 if (fieldValue != "") {
493 // the inputfield already has a reference to the attendee
494 // object, we just need to fill in the name.
495 var attendee = inputField.attendee.clone();
496
497 attendee.role = this.getRoleElement(i).getAttribute("role");
498 //attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
499
500 // break the list of potentially many attendees back into individual names
501 var emailAddresses = {};
502 var names = {};
503 var fullNames = {};
504
505 if (this.mHeaderParser) {
506 this.mHeaderParser.parseHeadersWithArray(fieldValue,
507 emailAddresses,
508 names,
509 fullNames);
510 } else {
511 emailAddresses.value = [ fieldValue ];
512 names.value = [];
513 }
514
515 if (emailAddresses.value.length > 0) {
516 // If the new address has no 'mailto'-prefix but seems
517 // to look like an email-address, we prepend the prefix.
518 // This also allows for non-email-addresses.
519 var email = emailAddresses.value[0];
520 if (email.toLowerCase().indexOf("mailto:") != 0) {
521 if (email.indexOf("@") >= 0) {
522 email = "MAILTO:" + email;
523 }
524 }
525 attendee.id = email;
526 }
527 if (names.value.length > 0) {
528 attendee.commonName = names.value[0];
529 }
530
531 var addAttendee = true;
532 if (this.mOrganizerID &&
533 this.mOrganizerID != "" &&
534 attendee.id.toLowerCase() == this.mOrganizerID.toLowerCase() &&
535 i == 1) {
536 addAttendee = false;
537 }
538
539 // append the attendee object to the list of attendees.
540 if (addAttendee) {
541 attendees.push(attendee);
542 }
543 }
544 }
545
546 return attendees;
547 ]]></getter>
548 </property>
549
550 <property name="organizer">
551 <getter><![CDATA[
552 var inputField;
553 for (var i = 1; inputField = this.getInputElement(i); i++) {
554 var fieldValue = inputField.value;
555 if (fieldValue != "") {
556 // The inputfield already has a reference to the attendee
557 // object, we just need to fill in the name.
558 var attendee = inputField.attendee.clone();
559
560 //attendee.role = this.getRoleElement(i).getAttribute("role");
561 attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
562
563 // break the list of potentially many attendees back into individual names
564 var emailAddresses = {};
565 var names = {};
566 var fullNames = {};
567
568 if (this.mHeaderParser) {
569 this.mHeaderParser.parseHeadersWithArray(fieldValue,
570 emailAddresses,
571 names,
572 fullNames);
573 } else {
574 emailAddresses.value = [ fieldValue ];
575 names.value = [];
576 }
577
578 if (emailAddresses.value.length > 0) {
579 // if the new address has no 'mailto'-prefix but seems
580 // to look like an email-address, we prepend the prefix.
581 // this also allows for non-email-addresses.
582 var email = emailAddresses.value[0];
583 if (email.toLowerCase().indexOf("mailto:") != 0) {
584 if (email.indexOf("@") >= 0) {
585 email = "MAILTO:" + email;
586 }
587 }
588 attendee.id = email;
589 }
590 if (names.value.length > 0) {
591 attendee.commonName = names.value[0];
592 }
593
594 if (this.mOrganizerID && this.mOrganizerID != "" &&
595 attendee.id.toLowerCase() == this.mOrganizerID.toLowerCase()) {
596 return attendee;
597 }
598 }
599 }
600
601 return null;
602 ]]></getter>
603 </property>
604
605 <method name="onModify">
606 <body><![CDATA[
607 var list = [];
608 for (var i = 1; i <= this.mMaxAttendees; i++) {
609 // retrieve the string from the appropriate row
610 var input = this.getInputElement(i);
611 var fieldValue = input.value;
612
613 // parse the string to break this down to individual names and addresses
614 var email = "";
615 var emailAddresses = {};
616 var names = {};
617 var fullNames = {};
618
619 if (this.mHeaderParser) {
620 this.mHeaderParser.parseHeadersWithArray(
621 fieldValue,
622 emailAddresses,
623 names,
624 fullNames);
625 } else {
626 emailAddresses.value = [ fieldValue ];
627 names.value = [];
628 }
629
630 if (emailAddresses.value.length > 0) {
631 // if the new address has no 'mailto'-prefix but seems
632 // to look like an email-address, we prepend the prefix.
633 // this also allows for non-email-addresses.
634 email = emailAddresses.value[0];
635 if (email.toLowerCase().indexOf("mailto:") != 0) {
636 if (email.indexOf("@") >= 0) {
637 email = "MAILTO:" + email;
638 }
639 }
640 }
641
642 var isdirty = false;
643 if (input.hasAttribute("dirty")) {
644 isdirty = input.getAttribute("dirty");
645 }
646 input.removeAttribute("dirty");
647 var entry = {
648 dirty: isdirty,
649 calid: email
650 };
651 list.push(entry);
652 }
653
654 var event = document.createEvent('Events');
655 event.initEvent('modify', true, false);
656 event.details = list;
657 this.dispatchEvent(event);
658 ]]></body>
659 </method>
660
661 <property name="documentSize">
662 <getter><![CDATA[
663 return this.mRowHeight * this.mMaxAttendees;
664 ]]></getter>
665 </property>
666
667 <method name="fitDummyRows">
668 <body><![CDATA[
669 var self = this;
670 var func = function attendees_list_fitDummyRows() {
671 self.calcContentHeight();
672 self.createOrRemoveDummyRows();
673 }
674 setTimeout(func, 0);
675 ]]></body>
676 </method>
677
678 <method name="calcContentHeight">
679 <body><![CDATA[
680 var listbox =
681 document.getAnonymousElementByAttribute(
682 this, "anonid", "listbox");
683 var items = listbox.getElementsByTagNameNS('*', 'listitem');
684 this.mContentHeight = 0;
685 if (items.length > 0) {
686 var i = 0;
687 do {
688 this.mRowHeight = items[i].boxObject.height;
689 ++i;
690 } while (i < items.length && !this.mRowHeight);
691 this.mContentHeight = this.mRowHeight * items.length;
692 }
693 ]]></body>
694 </method>
695
696 <method name="createOrRemoveDummyRows">
697 <body><![CDATA[
698 var listbox =
699 document.getAnonymousElementByAttribute(
700 this, "anonid", "listbox");
701 var listboxHeight = listbox.boxObject.height;
702
703 // remove rows to remove scrollbar
704 var kids = listbox.childNodes;
705 for (var i = kids.length - 1; this.mContentHeight > listboxHeight && i >= 0; --i) {
706 if (kids[i].hasAttribute("_isDummyRow")) {
707 this.mContentHeight -= this.mRowHeight;
708 listbox.removeChild(kids[i]);
709 }
710 }
711
712 // add rows to fill space
713 if (this.mRowHeight) {
714 while (this.mContentHeight + this.mRowHeight < listboxHeight) {
715 this.createDummyItem(listbox);
716 this.mContentHeight += this.mRowHeight;
717 }
718 }
719 ]]></body>
720 </method>
721
722 <method name="createDummyCell">
723 <parameter name="aParent"/>
724 <body><![CDATA[
725 var cell = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "listcell");
726 cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
727 if (aParent) {
728 aParent.appendChild(cell);
729 }
730 return cell;
731 ]]></body>
732 </method>
733
734 <method name="createDummyItem">
735 <parameter name="aParent"/>
736 <body><![CDATA[
737 var titem = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "listitem");
738 titem.setAttribute("_isDummyRow", "true");
739 titem.setAttribute("class", "dummy-row");
740 for (var i = this.numColumns; i > 0; i--) {
741 this.createDummyCell(titem);
742 }
743 if (aParent) {
744 aParent.appendChild(titem);
745 }
746 return titem;
747 ]]></body>
748 </method>
749
750 <!-- gets the next row from the top down -->
751 <method name="getNextDummyRow">
752 <body><![CDATA[
753 var listbox =
754 document.getAnonymousElementByAttribute(
755 this, "anonid", "listbox");
756 var kids = listbox.childNodes;
757 for (var i = 0; i < kids.length; ++i) {
758 if (kids[i].hasAttribute("_isDummyRow")) {
759 return kids[i];
760 }
761 }
762 return null;
763 ]]></body>
764 </method>
765
766 <!-- This method returns the <xul:listitem> at row numer 'aRow' -->
767 <method name="getListItem">
768 <parameter name="aRow"/>
769 <body><![CDATA[
770 var listbox =
771 document.getAnonymousElementByAttribute(
772 this, "anonid", "listbox");
773 if (listbox && aRow > 0) {
774 var listitems = listbox.getElementsByTagNameNS('*', 'listitem');
775 if (listitems && listitems.length >= aRow) {
776 return listitems[aRow - 1];
777 }
778 }
779 return 0;
780 ]]></body>
781 </method>
782
783 <method name="getRowByInputElement">
784 <parameter name="aElement"/>
785 <body><![CDATA[
786 var row = 0;
787 while (aElement && aElement.localName != "listitem") {
788 aElement = aElement.parentNode;
789 }
790 if (aElement) {
791 while (aElement) {
792 if (aElement.localName == "listitem") {
793 ++row;
794 }
795 aElement = aElement.previousSibling;
796 }
797 }
798 return row;
799 ]]></body>
800 </method>
801
802 <!-- This method returns the <xul:textbox> that contains
803 the name of the attendee at row number 'aRow' -->
804 <method name="getInputElement">
805 <parameter name="aRow"/>
806 <body><![CDATA[
807 return document.getElementById("attendeeCol2#" + aRow);
808 ]]></body>
809 </method>
810
811 <method name="getRoleElement">
812 <parameter name="aRow"/>
813 <body><![CDATA[
814 return document.getElementById("attendeeCol1#" + aRow);
815 ]]></body>
816 </method>
817
818 <method name="getStatusElement">
819 <parameter name="aRow"/>
820 <body><![CDATA[
821 return document.getElementById("attendeeCol1#" + aRow);
822 ]]></body>
823 </method>
824
825 <method name="setFocus">
826 <parameter name="aRow"/>
827 <body><![CDATA[
828 var self = this;
829 var set_focus = function() {
830 // do we need to scroll in order to see the selected row?
831 var node = self.getListItem(aRow);
832 var listbox =
833 document.getAnonymousElementByAttribute(
834 self, "anonid", "listbox");
835 var firstVisibleRow = listbox.getIndexOfFirstVisibleRow();
836 var numOfVisibleRows = listbox.getNumberOfVisibleRows();
837 if (aRow <= firstVisibleRow) {
838 listbox.scrollToIndex(aRow - 1);
839 } else {
840 if (aRow - 1 >= (firstVisibleRow + numOfVisibleRows)) {
841 listbox.scrollToIndex(aRow - numOfVisibleRows);
842 }
843 }
844 var input =
845 document.getAnonymousElementByAttribute(
846 node, "anonid", "input");
847 input.focus();
848 }
849 setTimeout(set_focus, 0);
850 ]]></body>
851 </method>
852
853 <property name="firstVisibleRow">
854 <getter><![CDATA[
855 var listbox =
856 document.getAnonymousElementByAttribute(
857 this, "anonid", "listbox");
858 return listbox.getIndexOfFirstVisibleRow();
859 ]]></getter>
860 </property>
861
862 <method name="createAttendee">
863 <body><![CDATA[
864 var attendee = createAttendee();
865 attendee.id = "";
866 attendee.rsvp = true;
867 attendee.role = "REQ-PARTICIPANT";
868 attendee.participationStatus = "NEEDS-ACTION";
869 return attendee;
870 ]]></body>
871 </method>
872
873 <property name="numColumns">
874 <getter><![CDATA[
875 if (!this.mNumColumns) {
876 var listbox =
877 document.getAnonymousElementByAttribute(
878 this, "anonid", "listbox");
879 var listCols = listbox.getElementsByTagNameNS('*', 'listcol');
880 this.mNumColumns = listCols.length;
881 if (!this.mNumColumns) {
882 this.mNumColumns = 1;
883 }
884 }
885 return this.mNumColumns;
886 ]]></getter>
887 </property>
888
889 <property name="ratio">
890 <setter><![CDATA[
891 var listbox =
892 document.getAnonymousElementByAttribute(
893 this, "anonid", "listbox");
894 var rowcount = listbox.getRowCount();
895 listbox.scrollToIndex(Math.floor(rowcount * val));
896 return val;
897 ]]></setter>
898 </property>
899
900 <method name="arrowHit">
901 <parameter name="aElement"/>
902 <parameter name="aDirection"/>
903 <body><![CDATA[
904 var row = this.getRowByInputElement(aElement) + aDirection;
905 if (row) {
906 if (row > this.mMaxAttendees) {
907 this.appendNewRow(true);
908 } else {
909 var input = this.getInputElement(row);
910 if (input.hasAttribute("disabled")) {
911 return;
912 }
913 this.setFocus(row);
914 }
915 var event = document.createEvent('Events');
916 event.initEvent('rowchange', true, false);
917 event.details = row;
918 this.dispatchEvent(event);
919 }
920 ]]></body>
921 </method>
922
923 <method name="deleteHit">
924 <parameter name="aElement"/>
925 <body><![CDATA[
926 // don't delete the row if it's the last one remaining
927 if (this.mMaxAttendees <= 1) {
928 return;
929 }
930
931 var row = this.getRowByInputElement(aElement);
932 this.deleteRow(row);
933 if (row > 1) {
934 row = row - 1;
935 }
936 this.setFocus(row);
937 this.onModify();
938
939 var event = document.createEvent('Events');
940 event.initEvent('rowchange', true, false);
941 event.details = row;
942 this.dispatchEvent(event);
943 ]]></body>
944 </method>
945
946 <method name="deleteRow">
947 <parameter name="aRow"/>
948 <body><![CDATA[
949 // reset id's in order to not break the sequence
950 var maxAttendees = this.mMaxAttendees;
951 this.removeRow(aRow);
952 var numberOfCols = this.numColumns;
953 for (var row = aRow + 1; row <= maxAttendees; row++) {
954 for (var col = 1; col <= numberOfCols; col++) {
955 var colID = "attendeeCol" + col + "#" + row;
956 var elem = document.getElementById(colID);
957 if (elem) {
958 elem.setAttribute("id", "attendeeCol" + col + "#" + (row - 1));
959 }
960 }
961 }
962 ]]></body>
963 </method>
964
965 <method name="removeRow">
966 <parameter name="aRow"/>
967 <body><![CDATA[
968 var listbox =
969 document.getAnonymousElementByAttribute(
970 this, "anonid", "listbox");
971 var nodeToRemove = this.getListItem(aRow);
972 nodeToRemove.parentNode.removeChild(nodeToRemove);
973 this.fitDummyRows();
974 this.mMaxAttendees--;
975 ]]></body>
976 </method>
977
978 <!-- ############################################################################# -->
979 <!-- LDAP support -->
980 <!-- ############################################################################# -->
981
982 <method name="setupAutocomplete">
983 <body><![CDATA[
984 var autocompleteLdap = false;
985 var autocompleteDirectory = null;
986 var prevAutocompleteDirectory = this.mCurrentAutocompleteDirectory;
987 var i;
988
989 try {
990 autocompleteLdap = this.mPrefs.getBoolPref("ldap_2.autoComplete.useDirectory");
991 } catch (ex) {
992 return;
993 }
994 if (autocompleteLdap) {
995 autocompleteDirectory = this.mPrefs.getCharPref(
996 "ldap_2.autoComplete.directoryServer");
997 }
998
999 // Use a temporary to do the setup so that we don't overwrite the
1000 // global, then have some problem and throw an exception, and leave the
1001 // global with a partially setup session. We'll assign the temp
1002 // into the global after we're done setting up the session.
1003 var LDAPSession;
1004 if (this.mLDAPSession) {
1005 LDAPSession = this.mLDAPSession;
1006 } else {
1007 LDAPSession = Components.classes[
1008 "@mozilla.org/autocompleteSession;1?type=ldap"].createInstance()
1009 .QueryInterface(
1010 Components.interfaces.nsILDAPAutoCompleteSession);
1011 }
1012
1013 if (autocompleteDirectory && !this.mIsOffline) {
1014 // Add observer on the directory server we are autocompleting against
1015 // only if current server is different from previous.
1016 // Remove observer if current server is different from previous
1017 this.mCurrentAutocompleteDirectory = autocompleteDirectory;
1018 if (prevAutocompleteDirectory) {
1019 if (prevAutocompleteDirectory != this.mCurrentAutocompleteDirectory) {
1020 this.removeDirectorySettingsObserver(prevAutocompleteDirectory);
1021 this.addDirectorySettingsObserver();
1022 }
1023 } else {
1024 this.addDirectorySettingsObserver();
1025 }
1026
1027 // Fill in the session params if there is a session
1028 if (LDAPSession) {
1029 var serverURL = Components.classes["@mozilla.org/network/ldap-url;1"]
1030 .createInstance(Components.interfaces.nsILDAPURL);
1031 try {
1032 serverURL.spec = this.mPrefs.getComplexValue(
1033 autocompleteDirectory + ".uri",
1034 Components.interfaces.nsISupportsString).data;
1035 } catch (ex) {
1036 dump("ERROR: " + ex + "\n");
1037 }
1038 LDAPSession.serverURL = serverURL;
1039
1040 // Get the login to authenticate as, if there is one
1041 var login = "";
1042 try {
1043 login = this.mPrefs.getComplexValue(
1044 autocompleteDirectory + ".auth.dn",
1045 Components.interfaces.nsISupportsString).data;
1046 } catch (ex) {
1047 // If we don't have this pref, no big deal
1048 }
1049
1050 // Set the LDAP protocol version correctly
1051 var protocolVersion;
1052 try {
1053 protocolVersion = this.mPrefs.getCharPref(
1054 autocompleteDirectory + ".protocolVersion");
1055 } catch (ex) {
1056 // If we don't have this pref, no big deal
1057 }
1058 if (protocolVersion == "2") {
1059 LDAPSession.version =
1060 Components.interfaces.nsILDAPConnection.VERSION2;
1061 }
1062
1063 // Find out if we need to authenticate, and if so, tell the LDAP
1064 // autocomplete session how to prompt for a password. This window
1065 // is being used to parent the authprompter.
1066 LDAPSession.login = login;
1067 if (login != "") {
1068 var windowWatcherSvc = Components.classes[
1069 "@mozilla.org/embedcomp/window-watcher;1"]
1070 .getService(Components.interfaces.nsIWindowWatcher);
1071 var domWin =
1072 window.QueryInterface(Components.interfaces.nsIDOMWindow);
1073 var authPrompter =
1074 windowWatcherSvc.getNewAuthPrompter(domWin);
1075 LDAPSession.authPrompter = authPrompter;
1076 }
1077
1078 // Don't search on non-CJK strings shorter than this
1079 try {
1080 LDAPSession.minStringLength = this.mPrefs.getIntPref(
1081 autocompleteDirectory + ".autoComplete.minStringLength");
1082 } catch (ex) {
1083 // if this pref isn't there, no big deal. Just let
1084 // nsLDAPAutoCompleteSession use its default.
1085 }
1086
1087 // don't search on CJK strings shorter than this
1088 try {
1089 LDAPSession.cjkMinStringLength = this.mPrefs.getIntPref(
1090 autocompleteDirectory + ".autoComplete.cjkMinStringLength");
1091 } catch (ex) {
1092 // If this pref isn't there, no big deal. Just let
1093 // nsLDAPAutoCompleteSession use its default.
1094 }
1095
1096 // We don't try/catch here, because if this fails, we're outta luck
1097 var ldapFormatter = Components.classes[
1098 "@mozilla.org/ldap-autocomplete-formatter;1?type=addrbook"]
1099 .createInstance().QueryInterface(
1100 Components.interfaces.nsIAbLDAPAutoCompFormatter);
1101
1102 // Override autocomplete name format?
1103 try {
1104 ldapFormatter.nameFormat =
1105 this.mPrefs.getComplexValue(autocompleteDirectory +
1106 ".autoComplete.nameFormat",
1107 Components.interfaces.nsISupportsString).data;
1108 } catch (ex) {
1109 // If this pref isn't there, no big deal. Just let
1110 // nsAbLDAPAutoCompFormatter use its default.
1111 }
1112
1113 // override autocomplete mail address format?
1114 try {
1115 ldapFormatter.addressFormat =
1116 this.mPrefs.getComplexValue(autocompleteDirectory +
1117 ".autoComplete.addressFormat",
1118 Components.interfaces.nsISupportsString).data;
1119 } catch (ex) {
1120 // If this pref isn't there, no big deal. Just let
1121 // nsAbLDAPAutoCompFormatter use its default.
1122 }
1123
1124 try {
1125 // Figure out what goes in the comment column, if anything
1126 //
1127 // 0 = none
1128 // 1 = name of addressbook this card came from
1129 // 2 = other per-addressbook format
1130 var showComments = 0;
1131 showComments = this.mPrefs.getIntPref(
1132 "mail.autoComplete.commentColumn");
1133
1134 switch (showComments) {
1135 case 1:
1136 // Use the name of this directory
1137 ldapFormatter.commentFormat = this.mPrefs.getComplexValue(
1138 autocompleteDirectory + ".description",
1139 Components.interfaces.nsISupportsString).data;
1140 break;
1141 case 2:
1142 // Override ldap-specific autocomplete entry?
1143 try {
1144 ldapFormatter.commentFormat =
1145 this.mPrefs.getComplexValue(autocompleteDirectory +
1146 ".autoComplete.commentFormat",
1147 Components.interfaces.nsISupportsString).data;
1148 } catch (innerException) {
1149 // If nothing has been specified, use the ldap
1150 // organization field
1151 ldapFormatter.commentFormat = "[o]";
1152 }
1153 break;
1154 case 0:
1155 default:
1156 // Do nothing
1157 }
1158 } catch (ex) {
1159 // If something went wrong while setting up comments, try and
1160 // proceed anyway
1161 }
1162
1163 // Set the session's formatter, which also happens to
1164 // force a call to the formatter's getAttributes() method
1165 // -- which is why this needs to happen after we've set the
1166 // various formats
1167 LDAPSession.formatter = ldapFormatter;
1168
1169 // Override autocomplete entry formatting?
1170 try {
1171 LDAPSession.outputFormat =
1172 this.mPrefs.getComplexValue(autocompleteDirectory +
1173 ".autoComplete.outputFormat",
1174 Components.interfaces.nsISupportsString).data;
1175 } catch (ex) {
1176 // If this pref isn't there, no big deal. Just let
1177 // nsLDAPAutoCompleteSession use its default.
1178 }
1179
1180 // override default search filter template?
1181 try {
1182 LDAPSession.filterTemplate = this.mPrefs.getComplexValue(
1183 autocompleteDirectory + ".autoComplete.filterTemplate",
1184 Components.interfaces.nsISupportsString).data;
1185 } catch (ex) {
1186 // If this pref isn't there, no big deal. Just let
1187 // nsLDAPAutoCompleteSession use its default
1188 }
1189
1190 // Override default maxHits (currently 100)
1191 try {
1192 LDAPSession.maxHits =
1193 this.mPrefs.getIntPref(autocompleteDirectory +
1194 ".maxHits");
1195 } catch (ex) {
1196 // If this pref isn't there, or is out of range, no big deal.
1197 // Just let nsLDAPAutoCompleteSession use its default.
1198 }
1199
1200 if (!this.mSessionAdded) {
1201 // If we make it here, we know that session initialization has
1202 // succeeded; add the session for all recipients, and
1203 // remember that we've done so
1204 var autoCompleteWidget;
1205 for (i = 1; i <= this.mMaxAttendees; i++) {
1206 autoCompleteWidget = this.getInputElement(i);
1207 if (autoCompleteWidget) {
1208 autoCompleteWidget.addSession(LDAPSession);
1209 // Ldap searches don't insert a default entry with
1210 // the default domain appended to it so reduce the
1211 // minimum results for a popup to 2 in this case.
1212 autoCompleteWidget.minResultsForPopup = 2;
1213 }
1214 }
1215 this.mSessionAdded = true;
1216 }
1217 }
1218 } else {
1219 if (this.mCurrentAutocompleteDirectory) {
1220 // Remove observer on the directory server since we are not doing Ldap
1221 // autocompletion.
1222 this.removeDirectorySettingsObserver(this.mCurrentAutocompleteDirectory);
1223 this.mCurrentAutocompleteDirectory = null;
1224 }
1225 if (this.mLDAPSession && this.mSessionAdded) {
1226 for (var i = 1; i <= this.mMaxAttendees; i++)
1227 this.getInputElement(i).removeSession(this.mLDAPSession);
1228 this.mSessionAdded = false;
1229 }
1230 }
1231
1232 this.mLDAPSession = LDAPSession;
1233 ]]></body>
1234 </method>
1235
1236 <method name="addDirectoryServerObserver">
1237 <body><![CDATA[
1238 if (this.mPrefs) {
1239 this.mPrefs.addObserver(
1240 "ldap_2.autoComplete.useDirectory",
1241 this.mDirectoryServerObserver,
1242 false);
1243 this.mPrefs.addObserver(
1244 "ldap_2.autoComplete.directoryServer",
1245 this.mDirectoryServerObserver,
1246 false);
1247 }
1248 ]]></body>
1249 </method>
1250
1251 <method name="removeDirectoryServerObserver">
1252 <body><![CDATA[
1253 if (this.mPrefs) {
1254 this.mPrefs.removeObserver(
1255 "ldap_2.autoComplete.useDirectory",
1256 this.mDirectoryServerObserver);
1257 this.mPrefs.removeObserver(
1258 "ldap_2.autoComplete.directoryServer",
1259 this.mDirectoryServerObserver);
1260 }
1261 ]]></body>
1262 </method>
1263
1264 <method name="addDirectorySettingsObserver">
1265 <body><![CDATA[
1266 if (this.mPrefs) {
1267 this.mPrefs.addObserver(
1268 this.mCurrentAutocompleteDirectory,
1269 this.mDirectoryServerObserver,
1270 false);
1271 }
1272 ]]></body>
1273 </method>
1274
1275 <method name="removeDirectorySettingsObserver">
1276 <parameter name="aPrefString"/>
1277 <body><![CDATA[
1278 if (this.mPrefs) {
1279 this.mPrefs.removeObserver(
1280 aPrefString,
1281 this.mDirectoryServerObserver);
1282 }
1283 ]]></body>
1284 </method>
1285
1286 <method name="releaseAutoCompleteState">
1287 <body><![CDATA[
1288 if (this.mLDAPSession && this.mSessionAdded) {
1289 for (i = 1; i <= this.mMaxAttendees; i++) {
1290 this.getInputElement(i).removeSession(this.mLDAPSession);
1291 }
1292 }
1293
1294 this.mSessionAdded = false;
1295 this.mLDAPSession = null;
1296 ]]></body>
1297 </method>
1298 </implementation>
1299
1300 <handlers>
1301 <handler event="click" button="0"><![CDATA[
1302 var target = event.originalTarget;
1303 if (target.hasAttribute("role")) {
1304 if (target.hasAttribute("disabled") &&
1305 target.getAttribute("disabled")) {
1306 return;
1307 }
1308 var role = target.getAttribute("role");
1309 if (role == "CHAIR") {
1310 target.setAttribute("role", "REQ-PARTICIPANT");
1311 } else if (role == "REQ-PARTICIPANT") {
1312 target.setAttribute("role", "OPT-PARTICIPANT");
1313 } else if (role == "OPT-PARTICIPANT") {
1314 target.setAttribute("role", "CHAIR");
1315 }
1316 return;
1317 }
1318
1319 if (target.hasAttribute("status")) {
1320 if (target.hasAttribute("disabled") &&
1321 target.getAttribute("disabled")) {
1322 return;
1323 }
1324 var status = target.getAttribute("status");
1325 switch (status) {
1326 case "NEEDS-ACTION":
1327 target.setAttribute("status", "ACCEPTED");
1328 break;
1329 case "ACCEPTED":
1330 target.setAttribute("status", "DECLINED");
1331 break;
1332 case "DECLINED":
1333 target.setAttribute("status", "TENTATIVE");
1334 break;
1335 case "TENTATIVE":
1336 target.setAttribute("status", "ACCEPTED");
1337 break;
1338 }
1339 return;
1340 }
1341
1342 if (this.mIsReadOnly || this.mIsInvitation) {
1343 return;
1344 }
1345
1346 if (target == null ||
1347 (target.localName != "listboxbody" &&
1348 target.localName != "listcell" &&
1349 target.localName != "listitem")) {
1350 return;
1351 }
1352
1353 var lastInput = this.getInputElement(this.mMaxAttendees);
1354 if (lastInput && lastInput.value) {
1355 this.appendNewRow(true);
1356 }
1357 ]]></handler>
1358
1359 <handler event="popupshown"><![CDATA[
1360 this.mPopupOpen = true;
1361 ]]></handler>
1362
1363 <handler event="popuphidden"><![CDATA[
1364 this.mPopupOpen = false;
1365 ]]></handler>
1366
1367 <handler event="keydown"><![CDATA[
1368 if (this.mIsReadOnly || this.mIsInvitation) {
1369 return;
1370 }
1371 if (event.originalTarget.localName == "input") {
1372 switch (event.keyCode) {
1373 case 46:
1374 case 8:
1375 if (!event.originalTarget.value) {
1376 this.deleteHit(event.originalTarget);
1377 }
1378 event.stopPropagation();
1379 break;
1380 case 13:
1381 this.arrowHit(event.originalTarget, 1);
1382 event.stopPropagation();
1383 event.preventDefault();
1384 break;
1385 }
1386 }
1387 ]]></handler>
1388
1389 <handler event="keypress" phase="capturing"><![CDATA[
1390 // In case we're currently showing the autocompletion popup
1391 // don't care about keypress-events and let them go. Otherwise
1392 // this event indicates the user wants to travel between
1393 // the different attendees. In this case we set the focus
1394 // appropriately and stop the event propagation.
1395 if (this.mPopupOpen || this.mIsReadOnly || this.mIsInvitation) {
1396 return;
1397 }
1398 if (event.originalTarget.localName == "input") {
1399 switch (event.keyCode) {
1400 case KeyEvent.DOM_VK_UP:
1401 this.arrowHit(event.originalTarget, -1);
1402 event.stopPropagation();
1403 break;
1404 case KeyEvent.DOM_VK_DOWN:
1405 this.arrowHit(event.originalTarget, 1);
1406 event.stopPropagation();
1407 break;
1408 case KeyEvent.DOM_VK_TAB:
1409 this.arrowHit(event.originalTarget, event.shiftKey ? -1 : +1);
1410 event.stopPropagation();
1411 event.preventDefault();
1412 break;
1413 case KeyEvent.DOM_VK_RETURN:
1414 event.stopPropagation();
1415 event.preventDefault();
1416 break;
1417 }
1418 }
1419 ]]></handler>
1420
1421 <handler event="input"><![CDATA[
1422 this.setupAutocomplete();
1423 ]]></handler>
1424 </handlers>
1425 </binding>
1426
1427 <!-- the 'selection-bar' binding implements the vertical bar that provides
1428 a visual indication for the time range the event is configured for. -->
1429 <binding id="selection-bar">
1430 <content>
1431 <xul:scrollbox anonid="scrollbox" width="0" orient="horizontal" flex="1">
1432 <xul:box class="selection-bar" anonid="selection-bar">
1433 <xul:box class="selection-bar-left" anonid="leftbox"/>
1434 <xul:spacer class="selection-bar-spacer" flex="1"/>
1435 <xul:box class="selection-bar-right" anonid="rightbox"/>
1436 </xul:box>
1437 </xul:scrollbox>
1438 </content>
1439
1440 <implementation>
1441 <field name="mRange">0</field>
1442 <field name="mStartHour">0</field>
1443 <field name="mEndHour">24</field>
1444 <field name="mContentWidth">0</field>
1445 <field name="mHeaderHeight">0</field>
1446 <field name="mRatio">0</field>
1447 <field name="mBaseDate">null</field>
1448 <field name="mStartDate">null</field>
1449 <field name="mEndDate">null</field>
1450 <field name="mMouseX">0</field>
1451 <field name="mMouseY">0</field>
1452 <field name="mDragState">0</field>
1453 <field name="mMargin">0</field>
1454 <field name="mWidth">0</field>
1455 <field name="mForce24Hours">false</field>
1456 <field name="mZoomFactor">100</field>
1457 <!-- constant that defines at which ratio an event is clipped, when moved or resized -->
1458 <field name="mfClipRatio">0.7</field>
1459 <field name="mLeftBox"/>
1460 <field name="mRightBox"/>
1461 <field name="mSelectionbar"/>
1462
1463 <property name="zoomFactor">
1464 <getter><![CDATA[
1465 return this.mZoomFactor;
1466 ]]></getter>
1467 <setter><![CDATA[
1468 this.mZoomFactor = val;
1469 return val;
1470 ]]></setter>
1471 </property>
1472
1473 <property name="force24Hours">
1474 <getter><![CDATA[
1475 return this.mForce24Hours;
1476 ]]></getter>
1477 <setter><![CDATA[
1478 this.mForce24Hours = val;
1479 this.initTimeRange();
1480 this.update();
1481 return val;
1482 ]]></setter>
1483 </property>
1484
1485 <property name="ratio">
1486 <setter><![CDATA[
1487 this.mRatio = val;
1488 this.update();
1489 return val;
1490 ]]></setter>
1491 </property>
1492
1493 <constructor><![CDATA[
1494 this.initTimeRange();
1495
1496 // The basedate is the date/time from which the display
1497 // of the timebar starts. The range is the number of days
1498 // we should be able to show. the start- and enddate
1499 // is the time the event is scheduled for.
1500 this.mRange = Number(this.getAttribute("range"));
1501 this.mSelectionbar =
1502 document.getAnonymousElementByAttribute(
1503 this, "anonid", "selection-bar");
1504 ]]></constructor>
1505
1506 <property name="baseDate">
1507 <setter><![CDATA[
1508 // we need to convert the date/time in question in
1509 // order to calculate with hours that are aligned
1510 // with our timebar display.
1511 var kDefaultTimezone = calendarDefaultTimezone();
1512 this.mBaseDate = val.getInTimezone(kDefaultTimezone);
1513 this.mBaseDate.isDate = true;
1514 this.mBaseDate.makeImmutable();
1515 return val;
1516 ]]></setter>
1517 </property>
1518
1519 <property name="startDate">
1520 <setter><![CDATA[
1521 // currently we *always* set the basedate to be
1522 // equal to the startdate. we'll most probably
1523 // want to change this later.
1524 this.baseDate = val;
1525 // we need to convert the date/time in question in
1526 // order to calculate with hours that are aligned
1527 // with our timebar display.
1528 var kDefaultTimezone = calendarDefaultTimezone();
1529 this.mStartDate = val.getInTimezone(kDefaultTimezone);
1530 this.mStartDate.makeImmutable();
1531 return val;
1532 ]]></setter>
1533 <getter><![CDATA[
1534 return this.mStartDate;
1535 ]]></getter>
1536 </property>
1537
1538 <property name="endDate">
1539 <setter><![CDATA[
1540 // we need to convert the date/time in question in
1541 // order to calculate with hours that are aligned
1542 // with our timebar display.
1543 var kDefaultTimezone = calendarDefaultTimezone();
1544 this.mEndDate = val.getInTimezone(kDefaultTimezone);
1545 if (this.mEndDate.isDate) {
1546 this.mEndDate.day += 1;
1547 }
1548 this.mEndDate.makeImmutable();
1549 return val;
1550 ]]></setter>
1551 <getter><![CDATA[
1552 return this.mEndDate;
1553 ]]></getter>
1554 </property>
1555
1556 <property name="leftdragWidth">
1557 <getter><![CDATA[
1558 if (!this.mLeftBox) {
1559 this.mLeftBox =
1560 document.getAnonymousElementByAttribute(
1561 this, "anonid", "leftbox");
1562 }
1563 return this.mLeftBox.boxObject.width;
1564 ]]></getter>
1565 </property>
1566 <property name="rightdragWidth">
1567 <getter><![CDATA[
1568 if (!this.mRightBox) {
1569 this.mRightBox =
1570 document.getAnonymousElementByAttribute(
1571 this, "anonid", "rightbox");
1572 }
1573 return this.mRightBox.boxObject.width;
1574 ]]></getter>
1575 </property>
1576
1577 <method name="init">
1578 <parameter name="width"/>
1579 <parameter name="height"/>
1580 <body><![CDATA[
1581 this.mContentWidth = width;
1582 this.mHeaderHeight = height + 2;
1583 this.mMargin = 0;
1584 this.update();
1585 ]]></body>
1586 </method>
1587
1588 <!-- given some specific date this method calculates the
1589 corrposonding offset in fractional hours -->
1590 <method name="date2offset">
1591 <parameter name="date"/>
1592 <body><![CDATA[
1593 var num_hours = this.mEndHour - this.mStartHour;
1594 var diff = date.subtractDate(this.mBaseDate);
1595 var offset = diff.days * num_hours;
1596 var hours = (diff.hours - this.mStartHour) + (diff.minutes / 60.0);
1597 if (hours < 0) {
1598 hours = 0;
1599 }
1600 if (hours > num_hours) {
1601 hours = num_hours;
1602 }
1603 offset += hours;
1604 return offset;
1605 ]]></body>
1606 </method>
1607
1608 <method name="update">
1609 <body><![CDATA[
1610 if (!this.mStartDate || !this.mEndDate) {
1611 return;
1612 }
1613
1614 // Calculate the relation of startdate/basedate and enddate/startdate.
1615 var offset = this.mStartDate.subtractDate(this.mBaseDate);
1616 var duration = this.mEndDate.subtractDate(this.mStartDate);
1617
1618 // Calculate how much pixels a single hour and a single day take up.
1619 var num_hours = this.mEndHour - this.mStartHour;
1620 var hour_width = this.mContentWidth / num_hours;
1621
1622 // Calculate the offset in fractional hours that corrospond
1623 // to our start- and end-time.
1624 var start_offset_in_hours = this.date2offset(this.mStartDate);
1625 var end_offset_in_hours = this.date2offset(this.mEndDate);
1626 var duration_in_hours = end_offset_in_hours - start_offset_in_hours;
1627
1628 // Calculate width & margin for the selection bar based on the
1629 // relation of startdate/basedate and enddate/startdate.
1630 // This is a simple conversion from hours to pixels.
1631 this.mWidth = duration_in_hours * hour_width;
1632 var totaldragwidths = this.leftdragWidth + this.rightdragWidth;
1633 if (this.mWidth < totaldragwidths) {
1634 this.mWidth = totaldragwidths;
1635 }
1636 this.mMargin = start_offset_in_hours * hour_width;
1637
1638 // Calculate the difference between content and container in pixels.
1639 // The container is the window showing this control, the content is the
1640 // total number of pixels the selection bar can theoretically take up.
1641 var total_width = this.mContentWidth * this.mRange - this.parentNode.boxObject.width;
1642
1643 // Calculate the current scroll offset.
1644 var offset = Math.floor(total_width * this.mRatio);
1645
1646 // The final margin is the difference between the date-based margin
1647 // and the scroll-based margin.
1648 this.mMargin -= offset;
1649
1650 // Set the styles based on the calculations above for the 'selection-bar'.
1651 var style = "width: " + this.mWidth +
1652 "px; margin-left: " + this.mMargin +
1653 "px; margin-top: " + this.mHeaderHeight + "px;";
1654 this.mSelectionbar.setAttribute("style", style);
1655
1656 var event = document.createEvent('Events');
1657 event.initEvent('timechange', true, false);
1658 event.startDate = this.mStartDate;
1659 event.endDate = this.mEndDate.clone();
1660 if (event.endDate.isDate) {
1661 event.endDate.day--;
1662 }
1663 event.endDate.makeImmutable();
1664 this.dispatchEvent(event);
1665 ]]></body>
1666 </method>
1667
1668 <method name="setWidth">
1669 <parameter name="width"/>
1670 <body><![CDATA[
1671 var scrollbox =
1672 document.getAnonymousElementByAttribute(
1673 this, "anonid", "scrollbox");
1674 scrollbox.setAttribute("width", width);
1675 ]]></body>
1676 </method>
1677
1678 <method name="initTimeRange">
1679 <body><![CDATA[
1680 if (this.force24Hours) {
1681 this.mStartHour = 0;
1682 this.mEndHour = 24;
1683 } else {
1684 this.mStartHour = getPrefSafe("calendar.view.daystarthour", 8);
1685 this.mEndHour = getPrefSafe("calendar.view.dayendhour", 19);
1686 }
1687 ]]></body>
1688 </method>
1689
1690 <method name="moveTime">
1691 <parameter name="time"/>
1692 <parameter name="delta"/>
1693 <parameter name="doclip"/>
1694 <body><![CDATA[
1695 var newTime = time.clone();
1696 var clip_minutes = 60 * this.zoomFactor / 100;
1697 if (newTime.isDate) {
1698 clip_minutes = 60 * 24;
1699 }
1700 var num_hours = this.mEndHour - this.mStartHour;
1701 var hour_width = this.mContentWidth / num_hours;
1702 var minutes_per_pixel = 60 / hour_width;
1703 var minute_shift = minutes_per_pixel * delta;
1704 var isClipped = Math.abs(minute_shift) >= (this.mfClipRatio * clip_minutes);
1705 if (isClipped) {
1706 if (delta > 0) {
1707 if (time.isDate) {
1708 newTime.day++;
1709 } else {
1710 if (doclip) {
1711 newTime.minute -= newTime.minute % clip_minutes;
1712 }
1713 newTime.minute += clip_minutes;
1714 }
1715 } else if (delta < 0) {
1716 if (time.isDate) {
1717 newTime.day--;
1718 } else {
1719 if (doclip) {
1720 newTime.minute -= newTime.minute % clip_minutes;
1721 }
1722 newTime.minute -= clip_minutes;
1723 }
1724 }
1725 }
1726
1727 if (!newTime.isDate) {
1728 if (newTime.hour < this.mStartHour) {
1729 newTime.hour = this.mEndHour - 1;
1730 newTime.day--;
1731 }
1732 if (newTime.hour >= this.mEndHour) {
1733 newTime.hour = this.mStartHour;
1734 newTime.day++;
1735 }
1736 }
1737
1738 return newTime;
1739 ]]></body>
1740 </method>
1741 </implementation>
1742
1743 <handlers>
1744 <handler event="mousedown"><![CDATA[
1745 var element = event.target;
1746 this.mMouseX = event.screenX;
1747 var mouseX = event.clientX - element.boxObject.x;
1748
1749 if (mouseX >= this.mMargin) {
1750 if (mouseX <= (this.mMargin + this.mWidth)) {
1751 if (mouseX <= (this.mMargin + this.leftdragWidth)) {
1752 // Move the startdate only...
1753 window.setCursor("w-resize");
1754 this.mDragState = 2;
1755 } else if (mouseX >= (this.mMargin + this.mWidth - (this.rightdragWidth))) {
1756 // Move the enddate only..
1757 window.setCursor("e-resize");
1758 this.mDragState = 3;
1759 } else {
1760 // Move the startdate and the enddate
1761 this.mDragState = 1;
1762 window.setCursor("-moz-grab");
1763 }
1764 }
1765 }
1766 ]]></handler>
1767
1768 <handler event="mousemove"><![CDATA[
1769 var mouseX = event.screenX;
1770 if (this.mDragState == 1) {
1771 // Move the startdate and the enddate
1772 var delta = mouseX - this.mMouseX;
1773 var newStart = this.moveTime(this.mStartDate, delta, false);
1774 if (newStart.compare(this.mStartDate) != 0) {
1775 newEnd = this.moveTime(this.mEndDate, delta, false);
1776
1777 // We need to adapt this date in case we're dealing with
1778 // an all-day event. This is because setting 'endDate' will
1779 // automatically add one day extra for all-day events.
1780 if (newEnd.isDate) {
1781 newEnd.day--;
1782 }
1783
1784 this.startDate = newStart;
1785 this.endDate = newEnd;
1786 this.mMouseX = mouseX;
1787 this.update();
1788 }
1789 } else if (this.mDragState == 2) {
1790 // Move the startdate only...
1791 var delta = event.screenX - this.mSelectionbar.boxObject.screenX;
1792 var newStart = this.moveTime(this.mStartDate, delta, true);
1793 if (newStart.compare(this.mEndDate) >= 0) {
1794 if (!this.mStartDate.isDate) {
1795 newStart = this.mEndDate;
1796 }
1797 else{
1798 return;
1799 }
1800 }
1801 if (newStart.compare(this.mStartDate) != 0) {
1802 this.startDate = newStart;
1803 this.update();
1804 }
1805 } else if (this.mDragState == 3) {
1806 // Move the enddate only..
1807 var delta = mouseX - (this.mSelectionbar.boxObject.screenX +
1808 this.mSelectionbar.boxObject.width);
1809 var newEnd = this.moveTime(this.mEndDate, delta, true);
1810 if (newEnd.compare(this.mStartDate) < 0) {
1811 newEnd = this.mStartDate;
1812 }
1813 if (newEnd.compare(this.mEndDate) != 0) {
1814 // We need to adapt this date in case we're dealing with
1815 // an all-day event. This is because setting 'endDate' will
1816 // automatically add one day extra for all-day events.
1817 if (newEnd.isDate) {
1818 newEnd.day--;
1819 }
1820
1821 // Don't allow all-day events to be shorter than a single day.
1822 if (!newEnd.isDate || (newEnd.compare(this.startDate) >= 0)) {
1823 this.endDate = newEnd;
1824 this.update();
1825 }
1826 }
1827 }
1828 ]]></handler>
1829
1830 <handler event="mouseup"><![CDATA[
1831 this.mDragState = 0;
1832 window.setCursor("auto");
1833 ]]></handler>
1834 </handlers>
1835 </binding>
1836
1837 </bindings>