summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/widgets/calendar-minimonth.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/base/content/widgets/calendar-minimonth.js')
-rw-r--r--comm/calendar/base/content/widgets/calendar-minimonth.js1055
1 files changed, 1055 insertions, 0 deletions
diff --git a/comm/calendar/base/content/widgets/calendar-minimonth.js b/comm/calendar/base/content/widgets/calendar-minimonth.js
new file mode 100644
index 0000000000..403841e69c
--- /dev/null
+++ b/comm/calendar/base/content/widgets/calendar-minimonth.js
@@ -0,0 +1,1055 @@
+/* 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/. */
+
+/* globals cal MozXULElement */
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+ const lazy = {};
+ ChromeUtils.defineModuleGetter(lazy, "CalDateTime", "resource:///modules/CalDateTime.jsm");
+
+ let dayFormatter = new Services.intl.DateTimeFormat(undefined, { day: "numeric" });
+ let dateFormatter = new Services.intl.DateTimeFormat(undefined, { dateStyle: "long" });
+
+ /**
+ * MiniMonth Calendar: day-of-month grid component.
+ * Displays month name and year above grid of days of month by week rows.
+ * Arrows move forward or back a month or a year.
+ * Clicking on a day cell selects that day.
+ * At site, can provide id, and code to run when value changed by picker.
+ * <calendar-minimonth id="my-date-picker" onchange="myDatePick( this );"/>
+ *
+ * May get/set value in javascript with
+ * document.querySelector("#my-date-picker").value = new Date();
+ *
+ * @implements {calIObserver}
+ * @implements {calICompositeObserver}
+ */
+ class CalendarMinimonth extends MozXULElement {
+ constructor() {
+ super();
+ // Set up custom interfaces.
+ this.calIObserver = this.getCustomInterfaceCallback(Ci.calIObserver);
+ this.calICompositeObserver = this.getCustomInterfaceCallback(Ci.calICompositeObserver);
+
+ let onPreferenceChanged = () => {
+ this.dayBoxes.clear(); // Days have moved, force a refresh of the grid.
+ this.refreshDisplay();
+ };
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "weekStart",
+ "calendar.week.start",
+ 0,
+ onPreferenceChanged
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "showWeekNumber",
+ "calendar.view-minimonth.showWeekNumber",
+ true,
+ onPreferenceChanged
+ );
+ }
+
+ static get inheritedAttributes() {
+ return {
+ ".minimonth-header": "readonly,month,year",
+ ".minimonth-year-name": "value=year",
+ };
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+
+ MozXULElement.insertFTLIfNeeded("calendar/calendar-widgets.ftl");
+
+ const minimonthHeader = `
+ <html:div class="minimonth-header minimonth-month-box"
+ xmlns="http://www.w3.org/1999/xhtml">
+ <div class="minimonth-nav-section">
+ <button class="button icon-button icon-only minimonth-nav-btn today-button"
+ data-l10n-id="calendar-today-button-tooltip"
+ type="button"
+ dir="0">
+ </button>
+ </div>
+ <div class="minimonth-nav-section">
+ <button class="button icon-button icon-only minimonth-nav-btn months-back-button"
+ data-l10n-id="calendar-nav-button-prev-tooltip-month"
+ type="button"
+ dir="-1">
+ </button>
+ <div class="minimonth-nav-item">
+ <input class="minimonth-month-name" tabindex="-1" readonly="true" disabled="disabled" />
+ </div>
+ <button class="button icon-button icon-only minimonth-nav-btn months-forward-button"
+ data-l10n-id="calendar-nav-button-next-tooltip-month"
+ type="button"
+ dir="1">
+ </button>
+ </div>
+ <div class="minimonth-nav-section">
+ <button class="button icon-button icon-only minimonth-nav-btn years-back-button"
+ data-l10n-id="calendar-nav-button-prev-tooltip-year"
+ type="button"
+ dir="-1">
+ </button>
+ <div class="minimonth-nav-item">
+ <input class="yearcell minimonth-year-name" tabindex="-1" readonly="true" disabled="disabled" />
+ </div>
+ <button class="button icon-button icon-only minimonth-nav-btn years-forward-button"
+ data-l10n-id="calendar-nav-button-next-tooltip-year"
+ type="button"
+ dir="1">
+ </button>
+ </div>
+ </html:div>
+ `;
+
+ const minimonthWeekRow = `
+ <html:tr class="minimonth-row-body">
+ <html:th class="minimonth-week" scope="row"></html:th>
+ <html:td class="minimonth-day" tabindex="-1"></html:td>
+ <html:td class="minimonth-day" tabindex="-1"></html:td>
+ <html:td class="minimonth-day" tabindex="-1"></html:td>
+ <html:td class="minimonth-day" tabindex="-1"></html:td>
+ <html:td class="minimonth-day" tabindex="-1"></html:td>
+ <html:td class="minimonth-day" tabindex="-1"></html:td>
+ <html:td class="minimonth-day" tabindex="-1"></html:td>
+ </html:tr>
+ `;
+
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ ${minimonthHeader}
+ <html:div class="minimonth-readonly-header minimonth-month-box"></html:div>
+ <html:table class="minimonth-calendar minimonth-cal-box">
+ <html:tr class="minimonth-row-head">
+ <html:th class="minimonth-row-header-week" scope="col"></html:th>
+ <html:th class="minimonth-row-header" scope="col"></html:th>
+ <html:th class="minimonth-row-header" scope="col"></html:th>
+ <html:th class="minimonth-row-header" scope="col"></html:th>
+ <html:th class="minimonth-row-header" scope="col"></html:th>
+ <html:th class="minimonth-row-header" scope="col"></html:th>
+ <html:th class="minimonth-row-header" scope="col"></html:th>
+ <html:th class="minimonth-row-header" scope="col"></html:th>
+ </html:tr>
+ ${minimonthWeekRow}
+ ${minimonthWeekRow}
+ ${minimonthWeekRow}
+ ${minimonthWeekRow}
+ ${minimonthWeekRow}
+ ${minimonthWeekRow}
+ </html:table>
+ `,
+ ["chrome://calendar/locale/global.dtd"]
+ )
+ );
+ this.initializeAttributeInheritance();
+ this.setAttribute("orient", "vertical");
+
+ // Set up header buttons.
+ this.querySelector(".months-back-button").addEventListener("click", () =>
+ this.advanceMonth(-1)
+ );
+ this.querySelector(".months-forward-button").addEventListener("click", () =>
+ this.advanceMonth(1)
+ );
+ this.querySelector(".years-back-button").addEventListener("click", () =>
+ this.advanceYear(-1)
+ );
+ this.querySelector(".years-forward-button").addEventListener("click", () =>
+ this.advanceYear(1)
+ );
+ this.querySelector(".today-button").addEventListener("click", () => {
+ this.value = new Date();
+ });
+
+ this.dayBoxes = new Map();
+ this.mValue = null;
+ this.mEditorDate = null;
+ this.mExtraDate = null;
+ this.mPixelScrollDelta = 0;
+ this.mObservesComposite = false;
+ this.mToday = null;
+ this.mSelected = null;
+ this.mExtra = null;
+ this.mValue = new Date(); // Default to "today".
+ this.mFocused = null;
+
+ let width = 0;
+ // Start loop from 1 as it is needed to get the first month name string
+ // and avoid extra computation of adding one.
+ for (let i = 1; i <= 12; i++) {
+ let dateString = cal.l10n.getDateFmtString(`month.${i}.name`);
+ width = Math.max(dateString.length, width);
+ }
+ this.querySelector(".minimonth-month-name").style.width = `${width + 1}ch`;
+
+ this.refreshDisplay();
+ if (this.hasAttribute("freebusy")) {
+ this._setFreeBusy(this.getAttribute("freebusy") == "true");
+ }
+
+ // Add event listeners.
+ this.addEventListener("click", event => {
+ if (event.button == 0 && event.target.classList.contains("minimonth-day")) {
+ this.onDayActivate(event);
+ }
+ });
+
+ this.addEventListener("keypress", event => {
+ if (event.target.classList.contains("minimonth-day")) {
+ if (event.altKey || event.metaKey) {
+ return;
+ }
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_LEFT:
+ this.onDayMovement(event, 0, 0, -1);
+ break;
+ case KeyEvent.DOM_VK_RIGHT:
+ this.onDayMovement(event, 0, 0, 1);
+ break;
+ case KeyEvent.DOM_VK_UP:
+ this.onDayMovement(event, 0, 0, -7);
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ this.onDayMovement(event, 0, 0, 7);
+ break;
+ case KeyEvent.DOM_VK_PAGE_UP:
+ if (event.shiftKey) {
+ this.onDayMovement(event, -1, 0, 0);
+ } else {
+ this.onDayMovement(event, 0, -1, 0);
+ }
+ break;
+ case KeyEvent.DOM_VK_PAGE_DOWN:
+ if (event.shiftKey) {
+ this.onDayMovement(event, 1, 0, 0);
+ } else {
+ this.onDayMovement(event, 0, 1, 0);
+ }
+ break;
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.focusDate(this.mValue || this.mExtraDate);
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ case KeyEvent.DOM_VK_HOME: {
+ const today = new Date();
+ this.update(today);
+ this.focusDate(today);
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ }
+ case KeyEvent.DOM_VK_RETURN:
+ this.onDayActivate(event);
+ break;
+ }
+ }
+ });
+
+ this.addEventListener("wheel", event => {
+ const pixelThreshold = 150;
+ let deltaView = 0;
+ if (this.getAttribute("readonly") == "true") {
+ // 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) {
+ const classList = event.target.classList;
+
+ if (
+ classList.contains("years-forward-button") ||
+ classList.contains("yearcell") ||
+ classList.contains("years-back-button")
+ ) {
+ this.advanceYear(deltaView);
+ } else if (!classList.contains("today-button")) {
+ this.advanceMonth(deltaView);
+ }
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ });
+ }
+
+ set value(val) {
+ this.update(val);
+ }
+
+ get value() {
+ return this.mValue;
+ }
+
+ set extra(val) {
+ this.mExtraDate = val;
+ }
+
+ get extra() {
+ return this.mExtraDate;
+ }
+
+ /**
+ * Returns the first (inclusive) date of the minimonth as a calIDateTime object.
+ */
+ get firstDate() {
+ let date = this._getCalBoxNode(1, 1).date;
+ return cal.dtz.jsDateToDateTime(date);
+ }
+
+ /**
+ * Returns the last (exclusive) date of the minimonth as a calIDateTime object.
+ */
+ get lastDate() {
+ let date = this._getCalBoxNode(6, 7).date;
+ let lastDateTime = cal.dtz.jsDateToDateTime(date);
+ lastDateTime.day = lastDateTime.day + 1;
+ return lastDateTime;
+ }
+
+ get mReadOnlyHeader() {
+ return this.querySelector(".minimonth-readonly-header");
+ }
+
+ setBusyDaysForItem(aItem, aState) {
+ let items = aItem.recurrenceInfo
+ ? aItem.getOccurrencesBetween(this.firstDate, this.lastDate)
+ : [aItem];
+ items.forEach(item => this.setBusyDaysForOccurrence(item, aState));
+ }
+
+ parseBoxBusy(aBox) {
+ let boxBusy = {};
+
+ let busyStr = aBox.getAttribute("busy");
+ if (busyStr && busyStr.length > 0) {
+ let calChunks = busyStr.split("\u001A");
+ for (let chunk of calChunks) {
+ let expr = chunk.split("=");
+ boxBusy[expr[0]] = parseInt(expr[1], 10);
+ }
+ }
+
+ return boxBusy;
+ }
+
+ updateBoxBusy(aBox, aBoxBusy) {
+ 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");
+ }
+ }
+
+ removeCalendarFromBoxBusy(aBox, aCalendar) {
+ let boxBusy = this.parseBoxBusy(aBox);
+ if (boxBusy[aCalendar.id]) {
+ delete boxBusy[aCalendar.id];
+ }
+ this.updateBoxBusy(aBox, boxBusy);
+ }
+
+ setBusyDaysForOccurrence(aOccurrence, aState) {
+ if (aOccurrence.getProperty("TRANSP") == "TRANSPARENT") {
+ // Skip transparent events.
+ return;
+ }
+ let start = aOccurrence[cal.dtz.startDateProp(aOccurrence)] || aOccurrence.dueDate;
+ let end = aOccurrence[cal.dtz.endDateProp(aOccurrence)] || start;
+ if (!start) {
+ return;
+ }
+
+ if (start.compare(this.firstDate) < 0) {
+ start = this.firstDate.clone();
+ }
+
+ if (end.compare(this.lastDate) > 0) {
+ end = this.lastDate.clone();
+ end.day++;
+ }
+
+ // We need to compare with midnight of the current day, so reset the
+ // time here.
+ let current = start.clone().getInTimezone(cal.dtz.defaultTimezone);
+ 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++;
+ }
+ }
+
+ // calIObserver methods.
+ calendarsInBatch = new Set();
+
+ onStartBatch(aCalendar) {
+ this.calendarsInBatch.add(aCalendar);
+ }
+
+ onEndBatch(aCalendar) {
+ this.calendarsInBatch.delete(aCalendar);
+ }
+
+ onLoad(aCalendar) {
+ this.getItems(aCalendar);
+ }
+
+ onAddItem(aItem) {
+ if (this.calendarsInBatch.has(aItem.calendar)) {
+ return;
+ }
+
+ this.setBusyDaysForItem(aItem, true);
+ }
+
+ onDeleteItem(aItem) {
+ this.setBusyDaysForItem(aItem, false);
+ }
+
+ onModifyItem(aNewItem, aOldItem) {
+ if (this.calendarsInBatch.has(aNewItem.calendar)) {
+ return;
+ }
+
+ this.setBusyDaysForItem(aOldItem, false);
+ this.setBusyDaysForItem(aNewItem, true);
+ }
+
+ onError(aCalendar, aErrNo, aMessage) {}
+
+ onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "disabled":
+ this.resetAttributesForDate();
+ this.getItems();
+ break;
+ }
+ }
+
+ onPropertyDeleting(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ }
+
+ // End of calIObserver methods.
+ // calICompositeObserver methods.
+
+ onCalendarAdded(aCalendar) {
+ if (!aCalendar.getProperty("disabled")) {
+ this.getItems(aCalendar);
+ }
+ }
+
+ onCalendarRemoved(aCalendar) {
+ if (!aCalendar.getProperty("disabled")) {
+ for (let box of this.dayBoxes.values()) {
+ this.removeCalendarFromBoxBusy(box, aCalendar);
+ }
+ }
+ }
+
+ onDefaultCalendarChanged(aCalendar) {}
+
+ // End calICompositeObserver methods.
+
+ refreshDisplay() {
+ if (!this.mValue) {
+ this.mValue = new Date();
+ }
+ this.setHeader();
+ this.showMonth(this.mValue);
+ this.updateAccessibleLabel();
+ }
+
+ _getCalBoxNode(aRow, aCol) {
+ if (!this.mCalBox) {
+ this.mCalBox = this.querySelector(".minimonth-calendar");
+ }
+ return this.mCalBox.children[aRow].children[aCol];
+ }
+
+ setHeader() {
+ // Reset the headers.
+ let dayList = new Array(7);
+ let longDayList = new Array(7);
+ let tempDate = new Date();
+ let i, j;
+ let useOSFormat;
+ tempDate.setDate(tempDate.getDate() - (tempDate.getDay() - this.weekStart));
+ for (i = 0; i < 7; i++) {
+ // If available, use UILocale days, else operating system format.
+ try {
+ dayList[i] = cal.l10n.getDateFmtString(`day.${tempDate.getDay() + 1}.short`);
+ } catch (e) {
+ dayList[i] = tempDate.toLocaleDateString(undefined, { weekday: "short" });
+ useOSFormat = true;
+ }
+ longDayList[i] = tempDate.toLocaleDateString(undefined, { weekday: "long" });
+ 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.
+ let minLength = dayList.map(name => name.length).reduce((min, len) => Math.min(min, len));
+
+ // 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(dayAbbr => dayAbbr.length > 2)) {
+ for (let endPrefix = 0; endPrefix < minLength; endPrefix++) {
+ let suffix = dayList[0][endPrefix];
+ if (dayList.some(dayAbbr => dayAbbr[endPrefix] != suffix)) {
+ 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++) {
+ let 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);
+ }
+ }
+
+ this._getCalBoxNode(0, 0).hidden = !this.showWeekNumber;
+ for (let column = 1; column < 8; column++) {
+ let node = this._getCalBoxNode(0, column);
+ node.textContent = dayList[column - 1];
+ node.setAttribute("aria-label", longDayList[column - 1]);
+ }
+ }
+
+ showMonth(aDate) {
+ // Use mExtraDate if aDate is null.
+ aDate = new Date(aDate || this.mExtraDate);
+
+ 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
+ let monthChanged = this.mEditorDate && this.mEditorDate.valueOf() != aDate.valueOf();
+ this.mEditorDate = aDate; // Only place mEditorDate is set.
+
+ if (this.mSelected) {
+ this.mSelected.removeAttribute("selected");
+ this.mSelected = null;
+ }
+
+ // Get today's date.
+ let today = new Date();
+
+ if (!monthChanged && this.dayBoxes.size > 0) {
+ this.mSelected = this.getBoxForDate(this.value);
+ if (this.mSelected) {
+ this.mSelected.setAttribute("selected", "true");
+ }
+
+ let todayBox = this.getBoxForDate(today);
+ if (this.mToday != todayBox) {
+ if (this.mToday) {
+ this.mToday.removeAttribute("today");
+ }
+ this.mToday = todayBox;
+ if (this.mToday) {
+ this.mToday.setAttribute("today", "true");
+ }
+ }
+ return;
+ }
+
+ if (this.mToday) {
+ this.mToday.removeAttribute("today");
+ this.mToday = null;
+ }
+
+ if (this.mExtra) {
+ this.mExtra.removeAttribute("extra");
+ this.mExtra = null;
+ }
+
+ // Update the month and year title.
+ this.setAttribute("year", aDate.getFullYear());
+ this.setAttribute("month", aDate.getMonth());
+
+ let miniMonthName = this.querySelector(".minimonth-month-name");
+ let dateString = cal.l10n.getDateFmtString(`month.${aDate.getMonth() + 1}.name`);
+ miniMonthName.setAttribute("value", dateString);
+ miniMonthName.setAttribute("monthIndex", aDate.getMonth());
+ this.mReadOnlyHeader.textContent = dateString + " " + aDate.getFullYear();
+
+ // Update the calendar.
+ let calbox = this.querySelector(".minimonth-calendar");
+ let date = this._getStartDate(aDate);
+
+ if (aDate.getFullYear() == (this.mValue || this.mExtraDate).getFullYear()) {
+ calbox.setAttribute("aria-label", dateString);
+ } else {
+ let monthName = cal.l10n.formatMonth(aDate.getMonth() + 1, "calendar", "monthInYear");
+ let label = cal.l10n.getCalString("monthInYear", [monthName, aDate.getFullYear()]);
+ calbox.setAttribute("aria-label", label);
+ }
+
+ this.dayBoxes.clear();
+ let defaultTz = cal.dtz.defaultTimezone;
+ for (let k = 1; k < 7; k++) {
+ // Set the week number.
+ let firstElement = this._getCalBoxNode(k, 0);
+ firstElement.hidden = !this.showWeekNumber;
+ if (this.showWeekNumber) {
+ let weekNumber = cal.weekInfoService.getWeekTitle(
+ cal.dtz.jsDateToDateTime(date, defaultTz)
+ );
+ let weekTitle = cal.l10n.getCalString("WeekTitle", [weekNumber]);
+ firstElement.textContent = weekNumber;
+ firstElement.setAttribute("aria-label", weekTitle);
+ }
+
+ for (let i = 1; i < 8; i++) {
+ let day = this._getCalBoxNode(k, i);
+ this.setBoxForDate(date, day);
+
+ if (this.getAttribute("readonly") != "true") {
+ day.setAttribute("interactive", "true");
+ }
+
+ if (aDate.getMonth() == date.getMonth()) {
+ day.removeAttribute("othermonth");
+ } else {
+ day.setAttribute("othermonth", "true");
+ }
+
+ // Highlight today.
+ if (this._sameDay(today, date)) {
+ this.mToday = day;
+ day.setAttribute("today", "true");
+ }
+
+ // Highlight the current date.
+ let val = this.value;
+ if (this._sameDay(val, date)) {
+ this.mSelected = day;
+ day.setAttribute("selected", "true");
+ }
+
+ // Highlight the extra date.
+ if (this._sameDay(this.mExtraDate, date)) {
+ this.mExtra = day;
+ day.setAttribute("extra", "true");
+ }
+
+ if (aDate.getMonth() == date.getMonth() && aDate.getFullYear() == date.getFullYear()) {
+ day.setAttribute("aria-label", dayFormatter.format(date));
+ } else {
+ day.setAttribute("aria-label", dateFormatter.format(date));
+ }
+
+ day.removeAttribute("busy");
+
+ day.date = new Date(date);
+ day.textContent = date.getDate();
+ date.setDate(date.getDate() + 1);
+
+ this.resetAttributesForBox(day);
+ }
+ }
+
+ if (!this.mFocused) {
+ this.setFocusedDate(this.mValue || this.mExtraDate);
+ }
+
+ this.fireEvent("monthchange");
+
+ if (this.getAttribute("freebusy") == "true") {
+ this.getItems();
+ }
+ }
+
+ /**
+ * Attention - duplicate!!!!
+ */
+ fireEvent(aEventName) {
+ this.dispatchEvent(new CustomEvent(aEventName, { bubbles: true }));
+ }
+
+ _boxKeyForDate(aDate) {
+ if (aDate instanceof lazy.CalDateTime || aDate instanceof Ci.calIDateTime) {
+ return aDate.getInTimezone(cal.dtz.defaultTimezone).toString().substring(0, 10);
+ }
+ return [
+ aDate.getFullYear(),
+ (aDate.getMonth() + 1).toString().padStart(2, "0"),
+ aDate.getDate().toString().padStart(2, "0"),
+ ].join("-");
+ }
+
+ /**
+ * Fetches the table cell for the given date, or null if the date isn't displayed.
+ *
+ * @param {calIDateTime|Date} aDate
+ * @returns {HTMLTableCellElement|null}
+ */
+ getBoxForDate(aDate) {
+ return this.dayBoxes.get(this._boxKeyForDate(aDate)) ?? null;
+ }
+
+ /**
+ * Stores the table cell for the given date.
+ *
+ * @param {Date} aDate
+ * @param {HTMLTableCellElement} aBox
+ */
+ setBoxForDate(aDate, aBox) {
+ this.dayBoxes.set(this._boxKeyForDate(aDate), aBox);
+ }
+
+ /**
+ * Remove attributes that may have been added to a table cell.
+ *
+ * @param {HTMLTableCellElement} aBox
+ */
+ resetAttributesForBox(aBox) {
+ let allowedAttributes = 0;
+ while (aBox.attributes.length > allowedAttributes) {
+ switch (aBox.attributes[allowedAttributes].nodeName) {
+ case "selected":
+ case "othermonth":
+ case "today":
+ case "extra":
+ case "interactive":
+ case "class":
+ case "tabindex":
+ case "role":
+ case "aria-label":
+ allowedAttributes++;
+ break;
+ default:
+ aBox.removeAttribute(aBox.attributes[allowedAttributes].nodeName);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Remove attributes that may have been added to a table cell, or all table cells.
+ *
+ * @param {Date} [aDate] - If specified, the date of the cell to reset,
+ * otherwise all date cells will be reset.
+ */
+ resetAttributesForDate(aDate) {
+ if (aDate) {
+ let box = this.getBoxForDate(aDate);
+ if (box) {
+ this.resetAttributesForBox(box);
+ }
+ } else {
+ for (let k = 1; k < 7; k++) {
+ for (let i = 1; i < 8; i++) {
+ this.resetAttributesForBox(this._getCalBoxNode(k, i));
+ }
+ }
+ }
+ }
+
+ _setFreeBusy(aFreeBusy) {
+ if (aFreeBusy) {
+ if (!this.mObservesComposite) {
+ cal.view.getCompositeCalendar(window).addObserver(this.calICompositeObserver);
+ this.mObservesComposite = true;
+ this.getItems();
+ }
+ } else if (this.mObservesComposite) {
+ cal.view.getCompositeCalendar(window).removeObserver(this.calICompositeObserver);
+ this.mObservesComposite = false;
+ }
+ }
+
+ removeAttribute(aAttr) {
+ if (aAttr == "freebusy") {
+ this._setFreeBusy(false);
+ }
+ return super.removeAttribute(aAttr);
+ }
+
+ setAttribute(aAttr, aVal) {
+ if (aAttr == "freebusy") {
+ this._setFreeBusy(aVal == "true");
+ }
+ return super.setAttribute(aAttr, aVal);
+ }
+
+ async getItems(aCalendar) {
+ // The minimonth automatically clears extra styles on a month change.
+ // Therefore we only need to fill the minimonth with new info.
+
+ let calendar = aCalendar || cal.view.getCompositeCalendar(window);
+ let filter =
+ calendar.ITEM_FILTER_COMPLETED_ALL |
+ calendar.ITEM_FILTER_CLASS_OCCURRENCES |
+ calendar.ITEM_FILTER_ALL_ITEMS;
+
+ // Get new info.
+ for await (let items of cal.iterate.streamValues(
+ calendar.getItems(filter, 0, this.firstDate, this.lastDate)
+ )) {
+ items.forEach(item => this.setBusyDaysForOccurrence(item, true));
+ }
+ }
+
+ updateAccessibleLabel() {
+ let label;
+ if (this.mValue) {
+ label = dateFormatter.format(this.mValue);
+ } else {
+ label = cal.l10n.getCalString("minimonthNoSelectedDate");
+ }
+ this.setAttribute("aria-label", label);
+ }
+
+ update(aValue) {
+ let changed =
+ this.mValue &&
+ aValue &&
+ (this.mValue.getFullYear() != aValue.getFullYear() ||
+ this.mValue.getMonth() != aValue.getMonth() ||
+ this.mValue.getDate() != aValue.getDate());
+
+ this.mValue = aValue;
+ if (changed) {
+ this.fireEvent("change");
+ }
+ this.showMonth(aValue);
+ if (aValue) {
+ this.setFocusedDate(aValue);
+ }
+ this.updateAccessibleLabel();
+ }
+
+ setFocusedDate(aDate, aForceFocus) {
+ let newFocused = this.getBoxForDate(aDate);
+ if (!newFocused) {
+ return;
+ }
+ if (this.mFocused) {
+ this.mFocused.setAttribute("tabindex", "-1");
+ }
+ this.mFocused = newFocused;
+ this.mFocused.setAttribute("tabindex", "0");
+ // Only actually move the focus if it is already in the calendar box.
+ if (!aForceFocus) {
+ let calbox = this.querySelector(".minimonth-calendar");
+ aForceFocus = calbox.contains(document.commandDispatcher.focusedElement);
+ }
+ if (aForceFocus) {
+ this.mFocused.focus();
+ }
+ }
+
+ focusDate(aDate) {
+ this.showMonth(aDate);
+ this.setFocusedDate(aDate);
+ }
+
+ switchMonth(aMonth) {
+ let newMonth = new Date(this.mEditorDate);
+ newMonth.setMonth(aMonth);
+ this.showMonth(newMonth);
+ }
+
+ switchYear(aYear) {
+ let newMonth = new Date(this.mEditorDate);
+ newMonth.setFullYear(aYear);
+ this.showMonth(newMonth);
+ }
+
+ selectDate(aDate, aMainDate) {
+ 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.
+ let sameMonth = this._sameDay(aMainDate, this.mEditorDate);
+ let 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(aDate);
+ if (this.mSelected) {
+ this.mSelected.removeAttribute("selected");
+ }
+ this.mSelected = day;
+ day.setAttribute("selected", "true");
+ this.mValue = aDate;
+ this.setFocusedDate(aDate);
+ }
+ }
+
+ _getStartDate(aMainDate) {
+ let date = new Date(aMainDate);
+ let firstWeekday = (7 + aMainDate.getDay() - this.weekStart) % 7;
+ date.setDate(date.getDate() - firstWeekday);
+ return date;
+ }
+
+ _getEndDate(aMainDate) {
+ let date = this._getStartDate(aMainDate);
+ let calbox = this.querySelector(".minimonth-calendar");
+ let days = (calbox.children.length - 1) * 7;
+ date.setDate(date.getDate() + days - 1);
+ return date;
+ }
+
+ _sameDay(aDate1, aDate2) {
+ if (
+ aDate1 &&
+ aDate2 &&
+ aDate1.getDate() == aDate2.getDate() &&
+ aDate1.getMonth() == aDate2.getMonth() &&
+ aDate1.getFullYear() == aDate2.getFullYear()
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ advanceMonth(aDir) {
+ let advEditorDate = new Date(this.mEditorDate); // At 1st of month.
+ let advMonth = this.mEditorDate.getMonth() + aDir;
+ advEditorDate.setMonth(advMonth);
+ this.showMonth(advEditorDate);
+ }
+
+ advanceYear(aDir) {
+ let advEditorDate = new Date(this.mEditorDate); // At 1st of month.
+ let advYear = this.mEditorDate.getFullYear() + aDir;
+ advEditorDate.setFullYear(advYear);
+ this.showMonth(advEditorDate);
+ }
+
+ moveDateByOffset(aYears, aMonths, aDays) {
+ const date = new Date(
+ this.mFocused.date.getFullYear() + aYears,
+ this.mFocused.date.getMonth() + aMonths,
+ this.mFocused.date.getDate() + aDays
+ );
+ this.focusDate(date);
+ }
+
+ focusCalendar() {
+ this.mFocused.focus();
+ }
+
+ onDayActivate(aEvent) {
+ // The associated date might change when setting this.value if month changes.
+ const date = aEvent.target.date;
+ if (this.getAttribute("readonly") != "true") {
+ this.value = date;
+ this.fireEvent("select");
+ }
+ this.setFocusedDate(date, true);
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ }
+
+ onDayMovement(event, years, months, days) {
+ this.moveDateByOffset(years, months, days);
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ disconnectedCallback() {
+ if (this.mObservesComposite) {
+ cal.view.getCompositeCalendar(window).removeObserver(this.calICompositeObserver);
+ }
+ }
+ }
+
+ MozXULElement.implementCustomInterface(CalendarMinimonth, [
+ Ci.calIObserver,
+ Ci.calICompositeObserver,
+ ]);
+ customElements.define("calendar-minimonth", CalendarMinimonth);
+}