<?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/. -->

<!--
/* MiniMonth Calendar: day-of-month grid XBL component.
   Displays month name and year above grid of days of month by week rows.
   Arrows move forward or back a month.
   Selecting a month name from month menu moves to that month in same year.
   Selecting a year from year menu moves to same month in selected year.
   Clicking on a day cell calls onchange attribute.
   Changing month via arrows or menus calls onmonthchange attribute.

   At site, can provide id, and code to run when value changed by picker.
     <calendar id="my-date-picker" onchange="myDatePick( this );"/>

   May get/set value in javascript with
     document.getElementById("my-date-picker").value = new Date();

   Use attributes onpopuplisthidden and onmonthchange for working around
   bugs that occur when minimonth is displayed in a popup (as in datepicker):
     Currently (2005.3)
       whenever a child popup is hidden, the parent popup needs to be reshown.
         Use onpopuplisthidden to reshow parent popop (hidePopup, openPopup).
       When title month or year changes, parent popup may need to be reshown.
         Use onmonthchange to reshow parent popop (hidePopup, openPopup).
*/
-->
<!DOCTYPE bindings 
[
    <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
    <!ENTITY % dtd2 SYSTEM "chrome://global/locale/global.dtd" > %dtd2;
]>

<bindings id="xulMiniMonth"
          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="minimonth-header" extends="xul:box">
     <content class="minimonth-month-box" align="center">
         <xul:deck anonid="monthheader" xbl:inherits="selectedIndex=month" class="minimonth-month-name-readonly">
           <xul:text value="&month.1.name;"/>
           <xul:text value="&month.2.name;"/>
           <xul:text value="&month.3.name;"/>
           <xul:text value="&month.4.name;"/>
           <xul:text value="&month.5.name;"/>
           <xul:text value="&month.6.name;"/>
           <xul:text value="&month.7.name;"/>
           <xul:text value="&month.8.name;"/>
           <xul:text value="&month.9.name;"/>
           <xul:text value="&month.10.name;"/>
           <xul:text value="&month.11.name;"/>
           <xul:text value="&month.12.name;"/>
         </xul:deck>
         <xul:text anonid="yearcell" class="minimonth-year-name-readonly" xbl:inherits="value=year"/>
         <xul:spacer flex="1"/>
     </content>
   </binding>

   <binding id="active-minimonth-header" extends="chrome://calendar/content/widgets/minimonth.xml#minimonth-header">
       <content class="minimonth-month-box" align="center">
         <xul:deck anonid="monthheader" xbl:inherits="selectedIndex=month"  class="minimonth-month-name">
           <xul:toolbarbutton label="&month.1.name;" oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.2.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.3.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.4.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.5.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.6.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.7.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.8.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.9.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.10.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.11.name;"  oncommand="showPopupList(event, 'months-popup')"/>
           <xul:toolbarbutton label="&month.12.name;"  oncommand="showPopupList(event, 'months-popup')"/>
         </xul:deck>
         <xul:toolbarbutton anonid="yearcell"
                            class="minimonth-year-name"
                            oncommand="showPopupList(event, 'years-popup')"
                            xbl:inherits="label=year"/>
         <xul:spacer flex="1"/>
         <xul:toolbarbutton anonid="back-button" class="minimonth-nav-btns" dir="-1"
                            oncommand="this.kMinimonth.advanceMonth(parseInt(this.getAttribute('dir'), 10))"
                            tooltiptext="&onemonthbackward.tooltip;"/>
         <xul:toolbarbutton anonid="today-button" class="minimonth-nav-btns" dir="0"
                            oncommand="this.kMinimonth.value = new Date();"
                            tooltiptext="&showToday.tooltip;"/>
         <xul:toolbarbutton anonid="forward-button" class="minimonth-nav-btns" dir="1"
                            oncommand="this.kMinimonth.advanceMonth(parseInt(this.getAttribute('dir'), 10))"
                            tooltiptext="&onemonthforward.tooltip;"/>
       <xul:popupset anonid="minmonth-popupset">
         <xul:menupopup anonid="months-popup" position="after_start"
                        onpopupshowing="event.stopPropagation();"
                        onpopuphidden="firePopupListHidden();">
           <xul:vbox>
             <xul:text class="minimonth-list" value="&month.1.name;" index="0"/>
             <xul:text class="minimonth-list" value="&month.2.name;" index="1"/>
             <xul:text class="minimonth-list" value="&month.3.name;" index="2"/>
             <xul:text class="minimonth-list" value="&month.4.name;" index="3"/>
             <xul:text class="minimonth-list" value="&month.5.name;" index="4"/>
             <xul:text class="minimonth-list" value="&month.6.name;" index="5"/>
             <xul:text class="minimonth-list" value="&month.7.name;" index="6"/>
             <xul:text class="minimonth-list" value="&month.8.name;" index="7"/>
             <xul:text class="minimonth-list" value="&month.9.name;" index="8"/>
             <xul:text class="minimonth-list" value="&month.10.name;" index="9"/>
             <xul:text class="minimonth-list" value="&month.11.name;" index="10"/>
             <xul:text class="minimonth-list" value="&month.12.name;" index="11"/>
           </xul:vbox>
         </xul:menupopup>
         <xul:menupopup anonid="years-popup" position="after_start"
                        onpopupshowing="moveYears('reset', 0); event.stopPropagation();"
                        onpopuphidden="firePopupListHidden();">
           <xul:vbox>
             <xul:autorepeatbutton class="autorepeatbutton-up"
                                   orient="vertical"
                                   oncommand="moveYears('up', 1);"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:text class="minimonth-list"/>
             <xul:autorepeatbutton class="autorepeatbutton-down"
                                   orient="vertical"
                                   oncommand="moveYears('down', 1);"/>
           </xul:vbox>
         </xul:menupopup>
       </xul:popupset>
     </content>
     <implementation>
       <field name="kMinimonth">null</field>
       <field name="mPopup">null</field>
       <field name="mScrollYearsHandler">null</field>
       <field name="mPixelScrollDelta">0</field>
       <constructor><![CDATA[
         this.kMinimonth = getParentNodeOrThis(this, "minimonth");
         document.getAnonymousElementByAttribute(this, "anonid", "back-button").kMinimonth = this.kMinimonth;
         document.getAnonymousElementByAttribute(this, "anonid", "today-button").kMinimonth = this.kMinimonth;
         document.getAnonymousElementByAttribute(this, "anonid", "forward-button").kMinimonth = this.kMinimonth;

         this.mScrollYearsHandler = this.scrollYears.bind(this);
         document.getAnonymousElementByAttribute(this, "anonid", "years-popup")
                 .addEventListener("wheel", this.mScrollYearsHandler, true);
       ]]></constructor>
       <destructor><![CDATA[
         document.getAnonymousElementByAttribute(this, "anonid", "years-popup")
                 .removeEventListener("wheel", this.mScrollYearsHandler, true);
         this.mScrollYearsHandler = null;
       ]]></destructor>

       <method name="showPopupList">
         <parameter name="aEvent"/>
         <parameter name="aPopupAnonId"/>
          <body><![CDATA[
           // Close open popups (if any), to prevent linux crashes
           if (this.mPopup)
               this.mPopup.hidePopup();
           this.mPopup = document.getAnonymousElementByAttribute(this, "anonid", aPopupAnonId);
           this.mPopup.openPopup(aEvent.target, "after_start");
         ]]></body>
       </method>

       <method name="hidePopupList">
         <body><![CDATA[
           // Close open popups (if any)
           let popup = this.mPopup;
           this.mPopup = null;
           if (popup)
               popup.hidePopup();
         ]]></body>
       </method>
       <method name="firePopupListHidden">
         <body><![CDATA[
           if (this.mPopup) {
               this.mPopup = null;
               this.kMinimonth.fireEvent("popuplisthidden");
           }
         ]]></body>
       </method>
       <method name="updateMonthPopup">
         <parameter name="aDate"/>
         <body><![CDATA[
           let months = document.getAnonymousElementByAttribute(this, "anonid", "months-popup").firstChild.childNodes;
           let month = aDate.getMonth();
           for (let i = 0; i < months.length; i++) {
               months[i].setAttribute("current", (i == month) ? "true" : "false");
           }
         ]]></body>
       </method>
       <method name="updateYearPopup">
         <parameter name="aDate"/>
         <body><![CDATA[
           let years = document.getAnonymousElementByAttribute(this, "anonid", "years-popup").firstChild.childNodes;
           let year = new Date(aDate);
           let compFullYear = aDate.getFullYear();
           year.setFullYear(Math.max(1, compFullYear - parseInt(years.length / 2, 10) + 1));
           for (let i = 1; i < years.length - 1; i++) {
               let curfullYear = year.getFullYear();
               years[i].setAttribute("value", curfullYear);
               years[i].setAttribute("current", (curfullYear == compFullYear) ? "true" : "false");
               year.setFullYear(curfullYear + 1);
           }
         ]]></body>
       </method>

       <method name="scrollYears">
         <parameter name="event"/>
         <body><![CDATA[
           let yearPopup = getParentNodeOrThis(event.target, "menupopup");
           const pixelThreshold = 75;
           if (yearPopup) {
               let monthList = yearPopup.getElementsByAttribute("class", "minimonth-list");
               if (monthList && monthList.length > 0) {
                   if (event.deltaMode == event.DOM_DELTA_PAGE) {
                     let dir = (event.deltaY > 0 ? "up" : "down");
                     this.moveYears(dir, Math.abs(event.deltaY) * monthList.length);
                   } else if (event.deltaMode == event.DOM_DELTA_LINE) {
                     let dir = (event.deltaY > 0 ? "up" : "down");
                     this.moveYears(dir, 1);
                   } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
                     this.mPixelScrollDelta += event.deltaY;
                     if (this.mPixelScrollDelta > pixelThreshold) {
                        this.moveYears("down", 1);
                        this.mPixelScrollDelta = 0;
                     } else if (this.mPixelScrollDelta < -pixelThreshold) {
                        this.moveYears("up", 1);
                        this.mPixelScrollDelta = 0;
                     }
                   }

                   event.stopPropagation();
                   event.preventDefault();
               }
           }
         ]]></body>
       </method>

       <method name="moveYears">
         <parameter name="direction"/>
         <parameter name="scrollOffset"/>
         <body><![CDATA[
           // Update the year popup
           let years = document.getAnonymousElementByAttribute(this, "anonid", "years-popup").firstChild.childNodes;
           let current = this.getAttribute("year");
           let offset;
           switch (direction) {
               case "reset":
                   let middleyear = years[Math.floor(years.length / 2)].getAttribute("value");
                   if (current <= (years.length / 2)) {
                       offset = - years[1].getAttribute("value") + 1;
                   } else {
                       offset = current - middleyear;
                   }
                   break;
               case "up":
                   offset = -Math.abs(scrollOffset) || -1;
                   break;
               case "down":
                   offset = Math.abs(scrollOffset) || 1;
                   break;
           }

           // Disable the up arrow when we get to the year 1.
           years[0].disabled = (parseInt(years[1].getAttribute("value"), 10) + offset < 2);

           if (!offset) {
               // No need to loop through when the offset is zero.
               return;
           }

           // Go through all visible years and set the new value. Be sure to
           // skip the autorepeatbuttons.
           for (let i = 1; i < years.length - 1; i++) {
               let value = parseInt(years[i].getAttribute("value"), 10) + offset;
               years[i].setAttribute("value", value);
               years[i].setAttribute("current", value == current ? "true" : "false");
           }
         ]]> </body>
       </method>
    </implementation>
    <!-- ::::::::::::::::: HANDLERS ::::::::::::::::::::::::: -->
    <handlers>
      <handler event="bindingattached" action="this.initialize();"/>
      <!-- handle click from nested months popup and years popup -->
      <handler event="click">
        <![CDATA[
          let element = event.originalTarget;
          let popup = getParentNodeOrThis(element, "menupopup");
          if (popup) {
              let anonid = popup.getAttribute("anonid")
              switch(anonid) {
                  case "months-popup":
                      this.hidePopupList();
                      this.kMinimonth.switchMonth(element.getAttribute("index"));
                      break;
                  case "years-popup":
                      this.hidePopupList();
                      let value = element.getAttribute("value");
                      if (value) {
                          this.kMinimonth.switchYear(value);
                      }
                      break;
              }
          }
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="minimonth" extends="xul:box">
    <resources>
      <stylesheet src="chrome://calendar/skin/widgets/minimonth.css"/>
    </resources>
    <content orient="vertical" xbl:inherits="onchange,onmonthchange,onpopuplisthidden,readonly">
        <xul:minimonth-header anonid="minimonth-header" xbl:inherits="readonly,month,year"/>
        <xul:vbox anonid="minimonth-calendar" class="minimonth-cal-box">
          <xul:hbox class="minimonth-row-head" anonid="minimonth-row-header" equalsize="always">
            <xul:text class="minimonth-row-header" flex="1"/>
            <xul:text class="minimonth-row-header" flex="1"/>
            <xul:text class="minimonth-row-header" flex="1"/>
            <xul:text class="minimonth-row-header" flex="1"/>
            <xul:text class="minimonth-row-header" flex="1"/>
            <xul:text class="minimonth-row-header" flex="1"/>
            <xul:text class="minimonth-row-header" flex="1"/>
          </xul:hbox>
          <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
          </xul:hbox>
          <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
          </xul:hbox>
          <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
          </xul:hbox>
          <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
          </xul:hbox>
          <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
          </xul:hbox>
          <xul:hbox class="minimonth-row-body" equalsize="always" flex="1">
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
            <xul:text class="minimonth-day" flex="1"/>
          </xul:hbox>
        </xul:vbox>
    </content>

    <!-- ::::::::::::::::: INTERFACE ::::::::::::::::::::::::: -->
    <implementation implements="calICompositeObserver calIOperationListener nsIObserver" >
      <property name="value"
                onget="return this.mValue"
                onset="this.update(val)"/>

       <!--returns the first (inclusive) date of the minimonth as a calIDateTime object-->
      <property name="firstDate" readonly="true">
        <getter><![CDATA[
            let calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
            let dt = calbox.childNodes[1].firstChild.date;
            return cal.jsDateToDateTime(dt);
        ]]></getter>
      </property>

       <!--returns the last (exclusive) date of the minimonth as a calIDateTime object-->
      <property name="lastDate" readonly="true">
        <getter><![CDATA[
            let calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
            let dt = calbox.lastChild.lastChild.date;
            let lastDateTime = cal.jsDateToDateTime(dt);
            lastDateTime.day = lastDateTime.day + 1;
            return lastDateTime;
        ]]></getter>
      </property>
      <field name="mDaymap">null</field>
      <field name="mValue">null</field>
      <field name="mEditorDate">null</field>
      <field name="mPixelScrollDelta">0</field>
      <field name="mIsReadOnly">false</field>
      <field name="mObservesComposite">false</field>
      <constructor><![CDATA[
          Components.utils.import("resource://gre/modules/Services.jsm");
          Components.utils.import("resource://gre/modules/Preferences.jsm");

          this.mToday = false;
          this.mSelected = false;
          this.mValue = new Date() // Default to "today"
          // save references for convenience
          if (this.hasAttribute("readonly")) {
              this.mIsReadOnly = this.getAttribute("readonly") == "true";
          }
          this.refreshDisplay( );
          if (this.hasAttribute("freebusy")) {
              this._setFreeBusy(this.getAttribute("freebusy") == "true");
          }

          // Add pref observer for calendar.week.start
          let branch = Services.prefs.getBranch("");
          branch.addObserver("calendar.week.start", this, false);
      ]]></constructor>
      <destructor><![CDATA[
          Components.utils.import("resource://gre/modules/Services.jsm");

          if (this.mObservesComposite == true) {
              getCompositeCalendar().removeObserver(this);
          }

          // Remove pref observer for calendar.week.start
          let branch = Services.prefs.getBranch("");
          branch.removeObserver("calendar.week.start", this, false);
      ]]></destructor>

     <!-- calIOperationListener methods -->
      <method name="onOperationComplete">
        <parameter name="aCalendar"/>
        <parameter name="aStatus"/>
        <parameter name="aOperationType"/>
        <parameter name="aId"/>
        <parameter name="aDetail"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="onGetResult">
        <parameter name="aCalendar"/>
        <parameter name="aStatus"/>
        <parameter name="aItemType"/>
        <parameter name="aDetail"/>
        <parameter name="aCount"/>
        <parameter name="aItems"/>
        <body><![CDATA[
            if (!Components.isSuccessCode(aStatus)) {
                return;
            }
            for each (var item in aItems) {
                this.setBusyDaysForOccurrence(item, true);
            }
        ]]></body>
      </method>

      <method name="setBusyDaysForItem">
        <parameter name="aItem"/>
        <parameter name="aState"/>
        <body><![CDATA[
          var items = [aItem];
          if (aItem.recurrenceInfo) {
              var startDate = this.firstDate;
              var endDate = this.lastDate;
              items = aItem.getOccurrencesBetween(startDate, endDate, {});
          }
          for each (var item in items) {
              this.setBusyDaysForOccurrence(item, aState);
          }
        ]]></body>
      </method>

      <method name="parseBoxBusy">
        <parameter name="aBox"/>
        <body><![CDATA[
          let boxBusy = {};

          let busyStr = aBox.getAttribute("busy");
          if (busyStr && busyStr.length > 0) {
              let calChunks = busyStr.split("\u001A");
              for each (let chunk in calChunks) {
                  let expr = chunk.split("=");
                  boxBusy[expr[0]] = parseInt(expr[1]);
              }
          }

          return boxBusy;
        ]]></body>
      </method>

      <method name="updateBoxBusy">
        <parameter name="aBox"/>
        <parameter name="aBoxBusy"/>
        <body><![CDATA[
          let calChunks = [];

          for (let calId in aBoxBusy) {
              if (aBoxBusy[calId]) {
                  calChunks.push(calId + "=" + aBoxBusy[calId]);
              }
          }

          if (calChunks.length > 0) {
              let busyStr = calChunks.join("\u001A");
              aBox.setAttribute("busy", busyStr);
          } else {
              aBox.removeAttribute("busy");
          }
        ]]></body>
      </method>

      <method name="removeCalendarFromBoxBusy">
        <parameter name="aBox"/>
        <parameter name="aCalendar"/>
        <body><![CDATA[
          let boxBusy = this.parseBoxBusy(aBox);
          if (boxBusy[aCalendar.id]) {
              delete boxBusy[aCalendar.id];
          }
          this.updateBoxBusy(aBox, boxBusy);
        ]]></body>
      </method>

      <method name="setBusyDaysForOccurrence">
        <parameter name="aOccurrence"/>
        <parameter name="aState"/>
        <body><![CDATA[
        if (aOccurrence.getProperty("TRANSP") == "TRANSPARENT") {
          // Skip transparent events
          return;
        }
        let start = aOccurrence[calGetStartDateProp(aOccurrence)] || aOccurrence.dueDate;
        let end = aOccurrence[calGetEndDateProp(aOccurrence)] || start;
        if (!start) {
            return;
        }

        // We need to compare with midnight of the current day, so reset the
        // time here.
        let current = start.clone().getInTimezone(cal.calendarDefaultTimezone());
        current.hour = 0;
        current.minute = 0;
        current.second = 0;

        // Cache the result so the compare isn't called in each iteration.
        let compareResult = (start.compare(end) == 0 ? 1 : 0);

        // Setup the busy days.
        while (current.compare(end) < compareResult) {
            let box = this.getBoxForDate(current);
            if (box) {
                let busyCalendars = this.parseBoxBusy(box);
                if (!busyCalendars[aOccurrence.calendar.id])
                    busyCalendars[aOccurrence.calendar.id] = 0;
                busyCalendars[aOccurrence.calendar.id] += (aState ? 1 : -1);
                this.updateBoxBusy(box, busyCalendars);
            }
            current.day++;
        }
        ]]></body>
      </method>

       <!--calIObserver methods -->
      <method name="onStartBatch">
        <parameter name="aCalendar"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="onEndBatch">
        <parameter name="aCalendar"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="onLoad">
        <parameter name="aCalendar"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="onAddItem">
        <parameter name="aItem"/>
        <body><![CDATA[
            this.setBusyDaysForItem(aItem, true);
        ]]></body>
      </method>

      <method name="onDeleteItem">
        <parameter name="aItem"/>
        <body><![CDATA[
            this.setBusyDaysForItem(aItem, false);
        ]]></body>
      </method>

      <method name="onModifyItem">
        <parameter name="aNewItem"/>
        <parameter name="aOldItem"/>
        <body><![CDATA[
            this.setBusyDaysForItem(aOldItem, false);
            this.setBusyDaysForItem(aNewItem, true);
        ]]></body>
      </method>

      <method name="onError">
        <parameter name="aCalendar"/>
        <parameter name="aErrNo"/>
        <parameter name="aMessage"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="onPropertyChanged">
        <parameter name="aCalendar"/>
        <parameter name="aName"/>
        <parameter name="aValue"/>
        <parameter name="aOldValue"/>
        <body><![CDATA[
            switch (aName) {
                case "disabled":
                    this.resetAttributesForDate();
                    this.getItems();
                    break;
            }
        ]]></body>
      </method>

      <method name="onPropertyDeleting">
        <parameter name="aCalendar"/>
        <parameter name="aName"/>
        <body><![CDATA[
            this.onPropertyChanged(aCalendar, aName, null, null);
        ]]></body>
      </method>

       <!-- calICompositeObserver methods -->
      <method name="onCalendarAdded">
        <parameter name="aCalendar"/>
        <body><![CDATA[
            this.getItems(aCalendar);
        ]]></body>
      </method>
      <method name="onCalendarRemoved">
        <parameter name="aCalendar"/>
        <body><![CDATA[
            for (let day in this.mDayMap) {
                this.removeCalendarFromBoxBusy(this.mDayMap[day], aCalendar);
            }
        ]]></body>
      </method>

      <method name="onDefaultCalendarChanged">
        <parameter name="aCalendar"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <!-- nsIObserver methods -->
      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body><![CDATA[
          // WARNING: In the constructor, only calendar.week.start is observed.
          // If you decide to add further prefs to this observer, you'll need to
          // modify constructor and destructor.
          switch (aData) {
              case "calendar.week.start":
                  this.refreshDisplay(true);
                  break;
          }
        ]]></body>
      </method>

      <method name="refreshDisplay">
        <body><![CDATA[
          // Find out which should be the first day of the week
          this.weekStart = Preferences.get("calendar.week.start", 0);
          if (!this.mValue) {
              this.mValue = new Date();
          }
          this.setHeader();
          this.showMonth(this.mValue);
        ]]></body>
      </method>
      <method name="setHeader">
        <body><![CDATA[
          // Reset the headers
          var header = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-row-header");
          var dayList = new Array(7);
          var tempDate = new Date();
          var i, j;
          var useOSFormat;
          tempDate.setDate(tempDate.getDate() - (tempDate.getDay() - this.weekStart));
          for (i = 0; i < header.childNodes.length; i++) {
              // if available, use UILocale days, else operating system format
              try {
                  dayList[i] = calGetString("dateFormat",
                               "day." + (tempDate.getDay() + 1) + ".short");
              } catch (e) {
                  dayList[i] = tempDate.toLocaleFormat("%a");
                  useOSFormat = true;
              }
              tempDate.setDate(tempDate.getDate() + 1);
          }

          if (useOSFormat) {
              // To keep datepicker popup compact, shrink localized weekday
              // abbreviations down to 1 or 2 chars so each column of week can
              // be as narrow as 2 digits.
              //
              // 1. Compute the minLength of the day name abbreviations.
              var minLength = dayList[0].length;
              for (i = 1; i < dayList.length; i++) {
                  minLength = Math.min(minLength, dayList[i].length);
              }
              // 2. If some day name abbrev. is longer than 2 chars (not Catalan),
              //    and ALL localized day names share same prefix (as in Chinese),
              //    then trim shared "day-" prefix.
              if (dayList.some(function(dayAbbr){ return dayAbbr.length > 2; })) {
                  for (var endPrefix = 0; endPrefix < minLength; endPrefix++) {
                      var c = dayList[0][endPrefix];
                      if (dayList.some(function(dayAbbr) {
                                             return dayAbbr[endPrefix] != c; })) {
                          if (endPrefix > 0) {
                              for (i = 0; i < dayList.length; i++) // trim prefix chars.
                                  dayList[i] = dayList[i].substring(endPrefix);
                          }
                          break;
                      }
                  }
              }
              // 3. trim each day abbreviation to 1 char if unique, else 2 chars.
              for (i = 0; i < dayList.length; i++) {
                  var foundMatch = 1;
                  for (j = 0; j < dayList.length; j++) {
                      if (i != j) {
                          if (dayList[i].substring(0,1) == dayList[j].substring(0,1)) {
                              foundMatch = 2;
                              break;
                          }
                      }
                  }
                  dayList[i] = dayList[i].substring(0,foundMatch)
              }
          }
          for (var column = 0; column < header.childNodes.length; column++) {
              header.childNodes[column].setAttribute( "value", dayList[column]);
          }
        ]]></body>
      </method>
      <method name="showMonth">
        <parameter name="aDate"/>
        <body><![CDATA[
            if (!aDate) {
              aDate = new Date();
            } else {
              aDate = new Date(aDate);
            }
            aDate.setDate(1);
            // We set the hour and minute to something highly unlikely to be the
            // exact change point of DST, so timezones like America/Sao Paulo
            // don't display some days twice.
            aDate.setHours(12);
            aDate.setMinutes(34);
            aDate.setSeconds(0);
            aDate.setMilliseconds(0);
            // Don't fire onmonthchange event upon initialization
            var monthChanged = this.mEditorDate && (this.mEditorDate.valueOf() != aDate.valueOf());
            this.mEditorDate = aDate; // only place mEditorDate is set.

            if (this.mToday) {
              this.mToday.removeAttribute("today");
              this.mToday = null;
            }

            if (this.mSelected) {
              this.mSelected.removeAttribute("selected");
              this.mSelected = null;
            }
            // Update the month and year title
            this.setAttribute("month", aDate.getMonth());
            this.setAttribute("year", aDate.getFullYear());
            if (!this.mIsReadOnly) {
                // Update the month popup
                var header = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-header");
                header.updateYearPopup(aDate);
                header.updateMonthPopup(aDate);
            }
            // Update the calendar
            var calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
            var date = this._getStartDate(aDate);

            // get today's date
            var today = new Date();

            this.mDayMap = {};
            for (var k = 1; k < calbox.childNodes.length; k++) {
                var row = calbox.childNodes[k];
                for (var i = 0; i < 7; i++) {
                    var day = row.childNodes[i];
                    var ymd = date.getFullYear() + "-" +
                              date.getMonth() + "-" +
                              date.getDate();
                    this.mDayMap[ymd] = day;

                    if (!this.mIsReadOnly) {
                        day.setAttribute("interactive", "true");
                    }

                    if (aDate.getMonth() != date.getMonth()) {
                        day.setAttribute("othermonth", "true");
                    } else {
                        day.removeAttribute("othermonth");
                    }

                    // highlight today
                    if (this._sameDay(today, date)) {
                        this.mToday = day;
                        day.setAttribute("today", "true");
                    }

                    // highlight the current date
                    var val = this.value;
                    if (this._sameDay(val, date)) {
                        this.mSelected = day;
                        day.setAttribute("selected", "true");
                    }

                    day.date = new Date(date);
                    day.minimonthParent = this;
                    day.setAttribute("value", date.getDate());
                    date.setDate(date.getDate() + 1);

                    if (monthChanged) {
                        this.resetAttributesForDate(day.date);
                    }
                }
            }
            if (monthChanged) this.fireEvent('monthchange');
            if (this.getAttribute("freebusy") == "true") {
                this.getItems();
            }
        ]]></body>
      </method>
      <!--Attention - duplicate!!!!-->
      <method name="fireEvent">
        <parameter name="aEventName"/>
        <body><![CDATA[
            var event = document.createEvent('Events');
            event.initEvent(aEventName, true, true);
            this.dispatchEvent(event);
        ]]></body>
      </method>

      <method name="getBoxForDate">
        <parameter name="aDate"/>
        <body><![CDATA[
            // aDate is a calIDateTime
            let ymd = [aDate.year, aDate.month, aDate.day].join("-");
            return (ymd in this.mDayMap ? this.mDayMap[ymd] : null);
        ]]></body>
      </method>

      <method name="resetAttributesForDate">
        <parameter name="aDate"/>
        <body><![CDATA[
            function removeForBox(aBox) {
              var allowedAttributes = 0;
              while (aBox.attributes.length > allowedAttributes) {
                switch (aBox.attributes[allowedAttributes].nodeName) {
                  case "selected":
                  case "othermonth":
                  case "today":
                  case "interactive":
                  case "value":
                  case "class":
                  case "flex":
                    allowedAttributes++;
                    break;
                  default:
                    aBox.removeAttribute(aBox.attributes[allowedAttributes].nodeName);
                    break;
                }
              }
            }

            if (aDate) {
              let box = this.getBoxForDate(cal.jsDateToDateTime(aDate, cal.calendarDefaultTimezone()));
              if (box) {
                  removeForBox(box);
              }
            } else {
              var calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
              for (var k = 1; k < calbox.childNodes.length; k++) {
                for (var i = 0; i < 7; i++) {
                    removeForBox(calbox.childNodes[k].childNodes[i]);
                }
              }
            }
          ]]></body>
      </method>
      <method name="_setFreeBusy">
        <parameter name="aFreeBusy"/>
        <body><![CDATA[
            if (aFreeBusy == true) {
                if (this.mObservesComposite == false) {
                    getCompositeCalendar().addObserver(this);
                    this.mObservesComposite = true;
                    this.getItems()
                 }
            } else {
                if (this.mObservesComposite == true) {
                    getCompositeCalendar().removeObserver(this);
                    this.mObservesComposite = false;
                 }
            }
        ]]></body>
      </method>
       <method name="removeAttribute">
        <parameter name="aAttr"/>
        <body><![CDATA[
          if (aAttr == "freebusy") {
              this._setFreeBusy(false);
          }
          // this should be done using lookupMethod(), see bug 286629
          var ret = XULElement.prototype.removeAttribute.call (this, aAttr);
          return ret;
        ]]></body>
      </method>
      <method name="setAttribute">
        <parameter name="aAttr"/>
        <parameter name="aVal"/>
        <body><![CDATA[
          if (aAttr == "freebusy") {
              this._setFreeBusy(aVal == "true");
          }
          // this should be done using lookupMethod(), see bug 286629
          var ret = XULElement.prototype.setAttribute.call (this, aAttr, aVal);
          return ret;
        ]]></body>
      </method>
      <method name="getItems">
        <parameter name="aCalendar"/>
        <body><![CDATA[
          // The minimonth automatically clears extra styles on a month change.
          // Therefore we only need to fill the minimonth with new info.

          let calendar = aCalendar || getCompositeCalendar();
          let filter = calendar.ITEM_FILTER_COMPLETED_ALL |
                       calendar.ITEM_FILTER_CLASS_OCCURRENCES |
                       calendar.ITEM_FILTER_ALL_ITEMS;

          // Get new info
          calendar.getItems(filter,
                            0,
                            this.firstDate,
                            this.lastDate,
                            this);
        ]]></body>
      </method>

      <method name="onSelectDay">
        <parameter name="aDayBox"/>
        <body><![CDATA[
            if (this.mIsReadOnly) {
               return;
            }
            if (this.mSelected) {
              this.mSelected.removeAttribute("selected");
            }
            this.mSelected = aDayBox;
            this.value = aDayBox.date;
            this.fireEvent('select');
          ]]></body>
      </method>

      <method name="update">
        <parameter name="aValue"/>
        <body><![CDATA[
          this.mValue = aValue;
          this.fireEvent('change');
          this.showMonth(aValue);
        ]]></body>
      </method>
      <method name="hidePopupList">
        <body><![CDATA[
          if (!this.mIsReadOnly) {
             var header = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-header");
             header.hidePopupList();
          }
        ]]></body>
      </method>
      <method name="switchMonth">
        <parameter name="aMonth"/>
        <body><![CDATA[
            var newMonth = new Date(this.mEditorDate);
            newMonth.setMonth(aMonth);
            this.showMonth(newMonth);
        ]]></body>
      </method>

      <method name="switchYear">
        <parameter name="aYear"/>
        <body><![CDATA[
            var newMonth = new Date(this.mEditorDate);
            newMonth.setFullYear(aYear);
            this.showMonth(newMonth);
        ]]></body>
      </method>

      <method name="selectDate">
        <parameter name="aDate"/>
        <parameter name="aMainDate"/>
        <body><![CDATA[
            if (!aMainDate || (aDate < this._getStartDate(aMainDate) || aDate > this._getEndDate(aMainDate))) {
                aMainDate = new Date(aDate);
                aMainDate.setDate(1);
            }
            // note, that aMainDate and this.mEditorDate refer to the first day
            // of the corresponding month
            var sameMonth = this._sameDay(aMainDate, this.mEditorDate);
            var sameDate = this._sameDay(aDate,this.mValue);
            if (!sameMonth && !sameDate) {
                // change month and select day
                this.mValue = aDate;
                this.showMonth(aMainDate);
            } else if (!sameMonth) {
                // change month only
                this.showMonth(aMainDate);
            } else if (!sameDate) {
                // select day only
                let day = this.getBoxForDate(cal.jsDateToDateTime(aDate, cal.calendarDefaultTimezone()));
                if (this.mSelected) {
                    this.mSelected.removeAttribute("selected");
                }
                this.mSelected = day;
                day.setAttribute("selected", "true");
                this.mValue = aDate;
            }
         ]]></body>
      </method>

      <method name="_getStartDate">
        <parameter name="aMainDate"/>
        <body><![CDATA[
          var date = new Date(aMainDate);
          var firstWeekday = (7 + aMainDate.getDay() - this.weekStart) % 7;
          date.setDate(date.getDate()-firstWeekday);
          return date;
        ]]></body>
      </method>

      <method name="_getEndDate">
        <parameter name="aMainDate"/>
        <body><![CDATA[
          var date = this._getStartDate(aMainDate);
          var calbox = document.getAnonymousElementByAttribute(this, "anonid", "minimonth-calendar");
          var days = (calbox.childNodes.length - 1) * 7;
          date.setDate(date.getDate() + days - 1);
          return date;
        ]]></body>
      </method>

      <method name="_sameDay">
        <parameter name="aDate1"/>
        <parameter name="aDate2"/>
        <body><![CDATA[
          if (aDate1 && aDate2 &&
             (aDate1.getDate() == aDate2.getDate()) &&
             (aDate1.getMonth() == aDate2.getMonth()) &&
             (aDate1.getFullYear() == aDate2.getFullYear())) {
             return true;
          }
          return false;
        ]]></body>
      </method>
      <method name="advanceMonth">
        <parameter name="aDir"/>
        <body><![CDATA[
          var advEditorDate = new Date(this.mEditorDate); // at 1st of month
          var advMonth = this.mEditorDate.getMonth() + aDir;
          advEditorDate.setMonth(advMonth);
          this.showMonth(advEditorDate);
        ]]></body>
      </method>
    </implementation>
     <handlers>
      <handler event="wheel">
        <![CDATA[
          const pixelThreshold = 150;
          let deltaView = 0;
          if (this.mIsReadOnly) {
            // No scrolling on readonly months
            return;
          }
          if (event.deltaMode == event.DOM_DELTA_LINE ||
              event.deltaMode == event.DOM_DELTA_PAGE) {
              if (event.deltaY != 0) {
                  deltaView = event.deltaY > 0 ? 1 : -1;
              }
          } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
              this.mPixelScrollDelta += event.deltaY;
              if (this.mPixelScrollDelta > pixelThreshold) {
                  deltaView = 1;
                  this.mPixelScrollDelta = 0;
              } else if (this.mPixelScrollDelta < -pixelThreshold) {
                  deltaView = -1;
                  this.mPixelScrollDelta = 0;
              }
          }

          if (deltaView != 0){
              this.advanceMonth(deltaView);
          }

          event.stopPropagation();
          event.preventDefault();
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="minimonth-day" extends="xul:text">
    <handlers>
      <handler event="click" button="0"><![CDATA[
        if (this.minimonthParent.getAttribute("readonly") != "true") {
            this.setAttribute("selected", "true");
            this.minimonthParent.onSelectDay(this)
        }
      ]]></handler>
    </handlers>
  </binding>
</bindings>
