/* 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. * * * 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 = `
`; const minimonthWeekRow = ` `; this.appendChild( MozXULElement.parseXULToFragment( ` ${minimonthHeader} ${minimonthWeekRow} ${minimonthWeekRow} ${minimonthWeekRow} ${minimonthWeekRow} ${minimonthWeekRow} ${minimonthWeekRow} `, ["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); }