summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-month-view.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/base/content/calendar-month-view.js')
-rw-r--r--comm/calendar/base/content/calendar-month-view.js1242
1 files changed, 1242 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-month-view.js b/comm/calendar/base/content/calendar-month-view.js
new file mode 100644
index 0000000000..f4bd93b02d
--- /dev/null
+++ b/comm/calendar/base/content/calendar-month-view.js
@@ -0,0 +1,1242 @@
+/* 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 calendarNavigationBar, MozElements, MozXULElement */
+
+/* import-globals-from calendar-ui-utils.js */
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+ /**
+ * Implements the Drag and Drop class for the Month Day Box view.
+ *
+ * @augments {MozElements.CalendarDnDContainer}
+ */
+ class CalendarMonthDayBox extends MozElements.CalendarDnDContainer {
+ static get inheritedAttributes() {
+ return {
+ ".calendar-month-week-label": "relation,selected",
+ ".calendar-month-day-label": "relation,selected,text=value",
+ };
+ }
+
+ constructor() {
+ super();
+ this.addEventListener("mousedown", this.onMouseDown);
+ this.addEventListener("dblclick", this.onDblClick);
+ this.addEventListener("click", this.onClick);
+ this.addEventListener("wheel", this.onWheel);
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasConnected) {
+ return;
+ }
+ // this.hasConnected is set to true in super.connectedCallback.
+ super.connectedCallback();
+
+ this.mDate = null;
+ this.mItemHash = {};
+ this.mShowMonthLabel = false;
+
+ this.setAttribute("orient", "vertical");
+
+ let monthDayLabels = document.createElement("h2");
+ monthDayLabels.classList.add("calendar-month-day-box-dates");
+
+ let weekLabel = document.createElement("span");
+ weekLabel.setAttribute("data-label", "week");
+ weekLabel.setAttribute("hidden", "true");
+ weekLabel.style.pointerEvents = "none";
+ weekLabel.classList.add("calendar-month-day-box-week-label", "calendar-month-week-label");
+
+ let dayLabel = document.createElement("span");
+ dayLabel.setAttribute("data-label", "day");
+ dayLabel.style.pointerEvents = "none";
+ dayLabel.classList.add("calendar-month-day-box-date-label", "calendar-month-day-label");
+
+ monthDayLabels.appendChild(weekLabel);
+ monthDayLabels.appendChild(dayLabel);
+
+ this.dayList = document.createElement("ol");
+ this.dayList.classList.add("calendar-month-day-box-list");
+
+ this.appendChild(monthDayLabels);
+ this.appendChild(this.dayList);
+
+ this.initializeAttributeInheritance();
+ }
+
+ get date() {
+ return this.mDate;
+ }
+
+ set date(val) {
+ this.setDate(val);
+ }
+
+ get selected() {
+ let sel = this.getAttribute("selected");
+ if (sel && sel == "true") {
+ return true;
+ }
+
+ return false;
+ }
+
+ set selected(val) {
+ if (val) {
+ this.setAttribute("selected", "true");
+ this.parentNode.setAttribute("selected", "true");
+ } else {
+ this.removeAttribute("selected");
+ this.parentNode.removeAttribute("selected");
+ }
+ }
+
+ get showMonthLabel() {
+ return this.mShowMonthLabel;
+ }
+
+ set showMonthLabel(val) {
+ if (this.mShowMonthLabel == val) {
+ return;
+ }
+ this.mShowMonthLabel = val;
+
+ if (!this.mDate) {
+ return;
+ }
+ if (val) {
+ this.setAttribute("value", cal.dtz.formatter.formatDateWithoutYear(this.mDate));
+ } else {
+ this.setAttribute("value", this.mDate.day);
+ }
+ }
+
+ clear() {
+ // Remove all the old events.
+ this.mItemHash = {};
+ while (this.dayList.lastChild) {
+ this.dayList.lastChild.remove();
+ }
+ }
+
+ setDate(aDate) {
+ this.clear();
+
+ if (this.mDate && aDate && this.mDate.compare(aDate) == 0) {
+ return;
+ }
+
+ this.mDate = aDate;
+
+ if (!aDate) {
+ // Clearing out these attributes isn't strictly necessary but saves some confusion.
+ this.removeAttribute("year");
+ this.removeAttribute("month");
+ this.removeAttribute("week");
+ this.removeAttribute("day");
+ this.removeAttribute("value");
+ return;
+ }
+
+ // Set up DOM attributes for custom CSS coloring.
+ let weekTitle = cal.weekInfoService.getWeekTitle(aDate);
+ this.setAttribute("year", aDate.year);
+ this.setAttribute("month", aDate.month + 1);
+ this.setAttribute("week", weekTitle);
+ this.setAttribute("day", aDate.day);
+
+ if (this.mShowMonthLabel) {
+ this.setAttribute("value", cal.dtz.formatter.formatDateWithoutYear(this.mDate));
+ } else {
+ this.setAttribute("value", aDate.day);
+ }
+ }
+
+ addItem(aItem) {
+ if (aItem.hashId in this.mItemHash) {
+ this.removeItem(aItem);
+ }
+
+ let cssSafeId = cal.view.formatStringForCSSRule(aItem.calendar.id);
+ let box = document.createXULElement("calendar-month-day-box-item");
+ let context = this.getAttribute("item-context") || this.getAttribute("context");
+ box.setAttribute("context", context);
+ box.style.setProperty("--item-backcolor", `var(--calendar-${cssSafeId}-backcolor)`);
+ box.style.setProperty("--item-forecolor", `var(--calendar-${cssSafeId}-forecolor)`);
+
+ let listItemWrapper = document.createElement("li");
+ listItemWrapper.classList.add("calendar-month-day-box-list-item");
+ listItemWrapper.appendChild(box);
+ cal.data.binaryInsertNode(
+ this.dayList,
+ listItemWrapper,
+ aItem,
+ cal.view.compareItems,
+ false,
+ // Access the calendar item from a list item wrapper.
+ wrapper => wrapper.firstChild.item
+ );
+
+ box.calendarView = this.calendarView;
+ box.item = aItem;
+ box.parentBox = this;
+ box.occurrence = aItem;
+
+ this.mItemHash[aItem.hashId] = box;
+ return box;
+ }
+
+ selectItem(aItem) {
+ if (aItem.hashId in this.mItemHash) {
+ this.mItemHash[aItem.hashId].selected = true;
+ }
+ }
+
+ unselectItem(aItem) {
+ if (aItem.hashId in this.mItemHash) {
+ this.mItemHash[aItem.hashId].selected = false;
+ }
+ }
+
+ removeItem(aItem) {
+ if (aItem.hashId in this.mItemHash) {
+ // Delete the list item wrapper.
+ let node = this.mItemHash[aItem.hashId].parentNode;
+ node.remove();
+ delete this.mItemHash[aItem.hashId];
+ }
+ }
+
+ setDropShadow(on) {
+ let existing = this.dayList.querySelector(".dropshadow");
+ if (on) {
+ if (!existing) {
+ // Insert an empty list item.
+ let dropshadow = document.createElement("li");
+ dropshadow.classList.add("dropshadow", "calendar-month-day-box-list-item");
+ this.dayList.insertBefore(dropshadow, this.dayList.firstElementChild);
+ }
+ } else if (existing) {
+ existing.remove();
+ }
+ }
+
+ onDropItem(aItem) {
+ // When item's timezone is different than the default one, the
+ // item might get moved on a day different than the drop day.
+ // Changing the drop day allows to compensate a possible difference.
+
+ // Figure out if the timezones cause a days difference.
+ let start = (
+ aItem[cal.dtz.startDateProp(aItem)] || aItem[cal.dtz.endDateProp(aItem)]
+ ).clone();
+ let dayboxDate = this.mDate.clone();
+ if (start.timezone != dayboxDate.timezone) {
+ let startInDefaultTz = start.clone().getInTimezone(dayboxDate.timezone);
+ start.isDate = true;
+ startInDefaultTz.isDate = true;
+ startInDefaultTz.timezone = start.timezone;
+ let dayDiff = start.subtractDate(startInDefaultTz);
+ // Change the day where to drop the item.
+ dayboxDate.addDuration(dayDiff);
+ }
+
+ return cal.item.moveToDate(aItem, dayboxDate);
+ }
+
+ onMouseDown(event) {
+ event.stopPropagation();
+ if (this.mDate) {
+ this.calendarView.selectedDay = this.mDate;
+ }
+ }
+
+ onDblClick(event) {
+ event.stopPropagation();
+ this.calendarView.controller.createNewEvent();
+ }
+
+ onClick(event) {
+ if (event.button != 0) {
+ return;
+ }
+
+ if (!(event.ctrlKey || event.metaKey)) {
+ this.calendarView.setSelectedItems([]);
+ }
+ }
+
+ onWheel(event) {
+ if (cal.view.getParentNodeOrThisByAttribute(event.target, "data-label", "day") == null) {
+ if (this.dayList.scrollHeight > this.dayList.clientHeight) {
+ event.stopPropagation();
+ }
+ }
+ }
+ }
+
+ customElements.define("calendar-month-day-box", CalendarMonthDayBox);
+
+ /**
+ * The MozCalendarMonthDayBoxItem widget is used as event item in the
+ * Multiweek and Month views of the calendar. It displays the event name,
+ * alarm icon and the category type color.
+ *
+ * @augments {MozElements.MozCalendarEditableItem}
+ */
+ class MozCalendarMonthDayBoxItem extends MozElements.MozCalendarEditableItem {
+ static get inheritedAttributes() {
+ return {
+ ".alarm-icons-box": "flashing",
+ };
+ }
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ // NOTE: This is the same structure as EditableItem, except this has a
+ // time label and we are missing the location-desc.
+ this.appendChild(
+ MozXULElement.parseXULToFragment(`
+ <html:img class="item-type-icon" alt="" />
+ <html:div class="item-time-label"></html:div>
+ <html:div class="event-name-label"></html:div>
+ <html:input class="plain event-name-input"
+ hidden="hidden"
+ placeholder='${cal.l10n.getCalString("newEvent")}' />
+ <html:div class="alarm-icons-box"></html:div>
+ <html:img class="item-classification-icon" />
+ <html:img class="item-recurrence-icon" />
+ <html:div class="calendar-category-box"></html:div>
+ `)
+ );
+ this.timeLabel = this.querySelector(".item-time-label");
+
+ this.classList.add("calendar-color-box", "calendar-item-flex");
+
+ // We have two event listeners for dragstart. This event listener is for the capturing phase
+ // where we are setting up the document.monthDragEvent which will be used in the event listener
+ // in the bubbling phase which is set up in the calendar-editable-item.
+ this.addEventListener(
+ "dragstart",
+ event => {
+ document.monthDragEvent = this;
+ },
+ true
+ );
+
+ this.style.pointerEvents = "auto";
+ this.setAttribute("tooltip", "itemTooltip");
+ this.addEventNameTextboxListener();
+ this.initializeAttributeInheritance();
+ }
+
+ set occurrence(val) {
+ cal.ASSERT(!this.mOccurrence, "Code changes needed to set the occurrence twice", true);
+ this.mOccurrence = val;
+ let displayTime;
+ if (val.isEvent()) {
+ let type;
+ if (!val.startDate.isDate) {
+ let formatter = cal.dtz.formatter;
+ let parentTime = this.parentBox.date.clone();
+ // Convert to the date-time for the start of the day.
+ parentTime.isDate = false;
+ // NOTE: Since this event was placed in this box, then we should be
+ // able to assume that the event starts before or on the same day, and
+ // it ends after or on the same day.
+ let startCompare = val.startDate.compare(parentTime);
+ // Go to the end of the day (midnight).
+ parentTime.day++;
+ let endCompare = val.endDate.compare(parentTime);
+ if (startCompare == -1) {
+ // Starts before this day.
+ switch (endCompare) {
+ case 1: // Ends on a later day.
+ type = "continue";
+ // We have no time to show in this case.
+ break;
+ case 0: // Ends at midnight.
+ case -1: // Ends on this day.
+ type = "end";
+ displayTime = formatter.formatTime(
+ val.endDate.getInTimezone(this.parentBox.date.timezone),
+ // We prefer to show midnight as 24:00 if possible to indicate
+ // that the event ends at the end of this day, rather than the
+ // start of the next day.
+ true
+ );
+ break;
+ }
+ } else {
+ // Starts on this day.
+ if (endCompare == 1) {
+ // Ends on a later day.
+ type = "start";
+ }
+ // Use the same format as ending on the day.
+ displayTime = formatter.formatTime(
+ val.startDate.getInTimezone(this.parentBox.date.timezone)
+ );
+ }
+ }
+ let icon = this.querySelector(".item-type-icon");
+ icon.classList.toggle("rotated-to-read-direction", !!type);
+ switch (type) {
+ case "start":
+ icon.setAttribute("src", "chrome://calendar/skin/shared/event-start.svg");
+ document.l10n.setAttributes(icon, "calendar-editable-item-multiday-event-icon-start");
+ break;
+ case "continue":
+ icon.setAttribute("src", "chrome://calendar/skin/shared/event-continue.svg");
+ document.l10n.setAttributes(
+ icon,
+ "calendar-editable-item-multiday-event-icon-continue"
+ );
+ break;
+ case "end":
+ icon.setAttribute("src", "chrome://calendar/skin/shared/event-end.svg");
+ document.l10n.setAttributes(icon, "calendar-editable-item-multiday-event-icon-end");
+ break;
+ default:
+ icon.removeAttribute("src");
+ icon.removeAttribute("data-l10n-id");
+ icon.setAttribute("alt", "");
+ }
+ }
+
+ if (displayTime) {
+ this.timeLabel.textContent = displayTime;
+ this.timeLabel.hidden = false;
+ } else {
+ this.timeLabel.textContent = "";
+ this.timeLabel.hidden = true;
+ }
+
+ this.setEditableLabel();
+ this.setCSSClasses();
+ }
+
+ get occurrence() {
+ return this.mOccurrence;
+ }
+ }
+
+ customElements.define("calendar-month-day-box-item", MozCalendarMonthDayBoxItem);
+
+ /**
+ * Abstract base class that is used for the month and multiweek calendar view custom elements.
+ *
+ * @implements {calICalendarView}
+ * @augments {MozElements.CalendarBaseView}
+ * @abstract
+ */
+ class CalendarMonthBaseView extends MozElements.CalendarBaseView {
+ ensureInitialized() {
+ if (this.isInitialized) {
+ return;
+ }
+ super.ensureInitialized();
+
+ this.appendChild(
+ MozXULElement.parseXULToFragment(`
+ <html:table class="mainbox monthtable">
+ <html:thead>
+ <html:tr></html:tr>
+ </html:thead>
+ <html:tbody class="monthbody"></html:tbody>
+ </html:table>
+ `)
+ );
+
+ this.addEventListener("wheel", event => {
+ const pixelThreshold = 150;
+ const scrollEnabled = Services.prefs.getBoolPref("calendar.view.mousescroll", true);
+ if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey && scrollEnabled) {
+ // In the month view, the only thing that can be scrolled
+ // is the month the user is in. calendar-base-view takes care of
+ // the shift key, so only move the view when no modifier is pressed.
+ let deltaView = 0;
+ if (event.deltaMode == event.DOM_DELTA_LINE) {
+ 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.moveView(deltaView);
+ }
+ }
+ });
+
+ this.mDateBoxes = null;
+ this.mSelectedDayBox = null;
+
+ this.mShowFullMonth = true;
+ this.mShowWeekNumber = true;
+
+ this.mClickedTime = null;
+
+ let dayHeaderRow = this.querySelector("thead > tr");
+ this.dayHeaders = new Array(7);
+ for (let i = 0; i < 7; i++) {
+ let hdr = document.createXULElement("calendar-day-label");
+ let headerCell = document.createElement("th");
+ headerCell.setAttribute("scope", "col");
+ // NOTE: At the time of implementation, the natural columnheader role is
+ // lost, probably from setting the CSS display of the container table
+ // and row (Bug 1711273).
+ // For now, we restore the role explicitly.
+ headerCell.setAttribute("role", "columnheader");
+ headerCell.appendChild(hdr);
+ this.dayHeaders[i] = hdr;
+ dayHeaderRow.appendChild(headerCell);
+ hdr.weekDay = (i + this.weekStartOffset) % 7;
+ hdr.shortWeekNames = false;
+ hdr.style.gridRow = 1;
+ }
+
+ this.monthbody = this.querySelector(".monthbody");
+ for (let week = 1; week <= 6; week++) {
+ let weekRow = document.createElement("tr");
+ for (let day = 1; day <= 7; day++) {
+ let dayCell = document.createElement("td");
+ let dayContent = document.createXULElement("calendar-month-day-box");
+ dayCell.appendChild(dayContent);
+ weekRow.appendChild(dayCell);
+ // Set the grid row for the element. This is needed to ensure the
+ // elements appear on different lines. We don't set the gridColumn
+ // because some days may become hidden.
+ dayContent.style.gridRow = week + 1;
+ }
+ this.monthbody.appendChild(weekRow);
+ }
+
+ // Set the preference for displaying the week number.
+ this.mShowWeekNumber = Services.prefs.getBoolPref(
+ "calendar.view-minimonth.showWeekNumber",
+ true
+ );
+ }
+
+ // calICalendarView Properties
+
+ get supportsDisjointDates() {
+ return false;
+ }
+
+ get hasDisjointDates() {
+ return false;
+ }
+
+ set selectedDay(day) {
+ if (this.mSelectedDayBox) {
+ this.mSelectedDayBox.selected = false;
+ }
+
+ let realDay = day;
+ if (!realDay.isDate) {
+ realDay = day.clone();
+ realDay.isDate = true;
+ }
+ const box = this.findDayBoxForDate(realDay);
+ if (box) {
+ box.selected = true;
+ this.mSelectedDayBox = box;
+ }
+ this.fireEvent("dayselect", realDay);
+ }
+
+ get selectedDay() {
+ if (this.mSelectedDayBox) {
+ return this.mSelectedDayBox.date.clone();
+ }
+
+ return null;
+ }
+
+ // End calICalendarView Properties
+
+ set selectedDateTime(dateTime) {
+ this.mClickedTime = dateTime;
+ }
+
+ get selectedDateTime() {
+ return cal.dtz.getDefaultStartDate(this.selectedDay);
+ }
+
+ set showFullMonth(showFullMonth) {
+ this.mShowFullMonth = showFullMonth;
+ }
+
+ get showFullMonth() {
+ return this.mShowFullMonth;
+ }
+
+ // This property may be overridden by subclasses if needed.
+ set weeksInView(weeksInView) {}
+
+ get weeksInView() {
+ return 0;
+ }
+
+ // calICalendarView Methods
+
+ setSelectedItems(items, suppressEvent) {
+ if (this.mSelectedItems.length) {
+ for (const item of this.mSelectedItems) {
+ const oldboxes = this.findDayBoxesForItem(item);
+ for (const oldbox of oldboxes) {
+ oldbox.unselectItem(item);
+ }
+ }
+ }
+
+ this.mSelectedItems = items || [];
+
+ if (this.mSelectedItems.length) {
+ for (const item of this.mSelectedItems) {
+ const newboxes = this.findDayBoxesForItem(item);
+ for (const newbox of newboxes) {
+ newbox.selectItem(item);
+ }
+ }
+ }
+
+ if (!suppressEvent) {
+ this.fireEvent("itemselect", this.mSelectedItems);
+ }
+ }
+
+ centerSelectedItems() {}
+
+ showDate(date) {
+ if (date) {
+ this.setDateRange(date.startOfMonth, date.endOfMonth);
+ this.selectedDay = date;
+ } else {
+ this.setDateRange(this.rangeStartDate, this.rangeEndDate);
+ }
+ }
+
+ setDateRange(startDate, endDate) {
+ this.rangeStartDate = startDate;
+ this.rangeEndDate = endDate;
+
+ const viewStart = cal.weekInfoService.getStartOfWeek(startDate.getInTimezone(this.mTimezone));
+
+ const viewEnd = cal.weekInfoService.getEndOfWeek(endDate.getInTimezone(this.mTimezone));
+
+ viewStart.isDate = true;
+ viewStart.makeImmutable();
+ viewEnd.isDate = true;
+ viewEnd.makeImmutable();
+
+ this.mStartDate = viewStart;
+ this.mEndDate = viewEnd;
+
+ // The start and end dates to query calendars with (in CalendarFilteredViewMixin).
+ this.startDate = viewStart;
+ let viewEndPlusOne = viewEnd.clone();
+ viewEndPlusOne.day++;
+ this.endDate = viewEndPlusOne;
+
+ // Check values of tasksInView, workdaysOnly, showCompleted.
+ // See setDateRange comment in calendar-multiday-base-view.js.
+ let toggleStatus = 0;
+
+ if (this.mTasksInView) {
+ toggleStatus |= this.mToggleStatusFlag.TasksInView;
+ }
+ if (this.mWorkdaysOnly) {
+ toggleStatus |= this.mToggleStatusFlag.WorkdaysOnly;
+ }
+ if (this.mShowCompleted) {
+ toggleStatus |= this.mToggleStatusFlag.ShowCompleted;
+ }
+
+ // Update the navigation bar only when changes are related to the current view.
+ if (this.isVisible()) {
+ calendarNavigationBar.setDateRange(startDate, endDate);
+ }
+
+ // Check whether view range has been changed since last call to relayout().
+ if (
+ !this.mViewStart ||
+ !this.mViewEnd ||
+ this.mViewEnd.compare(viewEnd) != 0 ||
+ this.mViewStart.compare(viewStart) != 0 ||
+ this.mToggleStatus != toggleStatus
+ ) {
+ this.relayout();
+ }
+ }
+
+ getDateList() {
+ if (!this.mStartDate || !this.mEndDate) {
+ return [];
+ }
+
+ const results = [];
+ const curDate = this.mStartDate.clone();
+ curDate.isDate = true;
+
+ while (curDate.compare(this.mEndDate) <= 0) {
+ results.push(curDate.clone());
+ curDate.day += 1;
+ }
+ return results;
+ }
+
+ // End calICalendarView Methods
+
+ /**
+ * Set an attribute on the view element, and do re-layout if needed.
+ *
+ * @param {string} attr - The attribute to set.
+ * @param {string} value - The value to set.
+ */
+ setAttribute(attr, value) {
+ const needsRelayout = attr == "context" || attr == "item-context";
+
+ const ret = XULElement.prototype.setAttribute.call(this, attr, value);
+
+ if (needsRelayout) {
+ this.relayout();
+ }
+
+ return ret;
+ }
+
+ /**
+ * Handle preference changes. Typically called by a preference observer.
+ *
+ * @param {object} subject - The subject, a prefs object.
+ * @param {string} topic - The notification topic.
+ * @param {string} preference - The preference to handle.
+ */
+ handlePreference(subject, topic, preference) {
+ subject.QueryInterface(Ci.nsIPrefBranch);
+
+ switch (preference) {
+ case "calendar.previousweeks.inview":
+ this.updateDaysOffPrefs();
+ this.refreshView();
+ break;
+
+ case "calendar.week.start":
+ // Refresh the view so the settings take effect.
+ this.refreshView();
+ break;
+
+ case "calendar.weeks.inview":
+ this.weeksInView = subject.getIntPref(preference);
+ break;
+
+ case "calendar.view-minimonth.showWeekNumber":
+ this.mShowWeekNumber = subject.getBoolPref(preference);
+ if (this.mShowWeekNumber) {
+ this.refreshView();
+ } else {
+ this.hideWeekNumbers();
+ }
+ break;
+
+ default:
+ this.handleCommonPreference(subject, topic, preference);
+ break;
+ }
+ }
+
+ /**
+ * Guarantee that the labels are clipped when an overflow occurs, to
+ * prevent horizontal scrollbars from appearing briefly.
+ */
+ adjustWeekdayLength() {
+ let dayLabels = this.querySelectorAll("calendar-day-label");
+ if (!this.longWeekdayTotalPixels) {
+ let maxDayWidth = 0;
+
+ for (const label of dayLabels) {
+ label.shortWeekNames = false;
+ maxDayWidth = Math.max(maxDayWidth, label.getLongWeekdayPixels());
+ }
+ if (maxDayWidth > 0) {
+ // FIXME: Where does the + 10 come from?
+ this.longWeekdayTotalPixels = maxDayWidth * dayLabels.length + 10;
+ } else {
+ this.longWeekdayTotalPixels = 0;
+ }
+ }
+ let useShortNames = this.longWeekdayTotalPixels > 0.95 * this.clientWidth;
+
+ for (let label of dayLabels) {
+ label.shortWeekNames = useShortNames;
+ }
+ }
+
+ /**
+ * Handle resizing by adjusting the view to the new size.
+ *
+ * @param {Element} viewElement - A calendar view element (calICalendarView).
+ */
+ onResize() {
+ let { width, height } = this.getBoundingClientRect();
+ if (width == this.mWidth && height == this.mHeight) {
+ // Return early if we're still the previous size.
+ return;
+ }
+ this.mWidth = width;
+ this.mHeight = height;
+
+ this.adjustWeekdayLength();
+ }
+
+ /**
+ * Re-render the view.
+ */
+ relayout() {
+ // Adjust headers based on the starting day of the week, if necessary.
+ if (this.dayHeaders[0].weekDay != this.weekStartOffset) {
+ for (let i = 0; i < this.dayHeaders.length; i++) {
+ this.dayHeaders[i].weekDay = (i + this.weekStartOffset) % 7;
+ }
+ }
+
+ if (this.mSelectedItems.length) {
+ this.mSelectedItems = [];
+ }
+
+ if (!this.mStartDate || !this.mEndDate) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ // Days that are not in the main month on display are displayed with
+ // a gray background. Unless the month actually starts on a Sunday,
+ // this means that mStartDate.month is 1 month less than the main month.
+ let mainMonth = this.mStartDate.month;
+ if (this.mStartDate.day != 1) {
+ mainMonth++;
+ mainMonth = mainMonth % 12;
+ }
+
+ const dateBoxes = [];
+
+ // This gets set to true, telling us to collapse the rest of the rows.
+ let finished = false;
+ const dateList = this.getDateList();
+
+ // This allows finding the first column of dayboxes where to set the
+ // week labels, taking into account whether days-off are displayed or not.
+ let weekLabelColumnPos = -1;
+
+ const rows = this.monthbody.children;
+
+ // Iterate through each monthbody row and set up the day-boxes that
+ // are its child nodes. Remember, children is not a normal array,
+ // so don't use the in operator if you don't want extra properties
+ // coming out.
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ // If we've already assigned all of the day-boxes that we need, just
+ // collapse the rest of the rows, otherwise expand them if needed.
+ row.toggleAttribute("hidden", finished);
+ if (finished) {
+ for (let cell of row.cells) {
+ // Clear out the hidden cells for to avoid holding events in memory
+ // for no reason. Also prevents tests failing due to stray event
+ // boxes from months that are no longer displayed.
+ cell.firstElementChild.setDate();
+ }
+ continue;
+ }
+ for (let j = 0; j < row.children.length; j++) {
+ const daybox = row.children[j].firstElementChild;
+ const date = dateList[dateBoxes.length];
+
+ // Remove the attribute "relation" for all the column headers.
+ // Consider only the first row index otherwise it will be
+ // removed again afterwards the correct setting.
+ if (i == 0) {
+ this.dayHeaders[j].removeAttribute("relation");
+ }
+
+ daybox.setAttribute("context", this.getAttribute("context"));
+
+ daybox.setAttribute(
+ "item-context",
+ this.getAttribute("item-context") || this.getAttribute("context")
+ );
+
+ // Set the box-class depending on if this box displays a day in
+ // the month being currently shown or not.
+ let boxClass;
+ if (this.showFullMonth) {
+ boxClass =
+ "calendar-month-day-box-" +
+ (mainMonth == date.month ? "current-month" : "other-month");
+ } else {
+ boxClass = "calendar-month-day-box-current-month";
+ }
+ if (this.mDaysOffArray.some(dayOffNum => dayOffNum == date.weekday)) {
+ boxClass = "calendar-month-day-box-day-off " + boxClass;
+ }
+
+ // Set up label with the week number in the first day of the row.
+ if (this.mShowWeekNumber) {
+ const weekLabel = daybox.querySelector("[data-label='week']");
+ if (weekLabelColumnPos < 0) {
+ const isDayOff = this.mDaysOffArray.includes((j + this.weekStartOffset) % 7);
+ if (this.mDisplayDaysOff || !isDayOff) {
+ weekLabelColumnPos = j;
+ }
+ }
+ // Build and set the label.
+ if (j == weekLabelColumnPos) {
+ weekLabel.removeAttribute("hidden");
+ const weekNumber = cal.weekInfoService.getWeekTitle(date);
+ const weekString = cal.l10n.getCalString("multiweekViewWeek", [weekNumber]);
+ weekLabel.textContent = weekString;
+ } else {
+ weekLabel.hidden = true;
+ }
+ }
+
+ daybox.setAttribute("class", boxClass);
+
+ daybox.calendarView = this;
+ daybox.showMonthLabel = date.day == 1 || date.day == date.endOfMonth.day;
+ daybox.date = date;
+ dateBoxes.push(daybox);
+
+ // If we've now assigned all of our dates, set this to true so we
+ // know we can just collapse the rest of the rows.
+ if (dateBoxes.length == dateList.length) {
+ finished = true;
+ }
+ }
+ }
+
+ // If we're not showing a full month, then add a few extra labels to
+ // help the user orient themselves in the view.
+ if (!this.mShowFullMonth) {
+ dateBoxes[0].showMonthLabel = true;
+ dateBoxes[dateBoxes.length - 1].showMonthLabel = true;
+ }
+
+ // Store these, so that we can access them later.
+ this.mDateBoxes = dateBoxes;
+ this.setDateBoxRelations();
+ this.hideDaysOff();
+
+ this.adjustWeekdayLength();
+
+ // Store the start and end of current view. Next time when
+ // setDateRange is called, it will use mViewStart and mViewEnd to
+ // check if view range has been changed.
+ this.mViewStart = this.mStartDate;
+ this.mViewEnd = this.mEndDate;
+
+ // Store toggle status of current view.
+ let toggleStatus = 0;
+
+ if (this.mTasksInView) {
+ toggleStatus |= this.mToggleStatusFlag.TasksInView;
+ }
+ if (this.mWorkdaysOnly) {
+ toggleStatus |= this.mToggleStatusFlag.WorkdaysOnly;
+ }
+ if (this.mShowCompleted) {
+ toggleStatus |= this.mToggleStatusFlag.ShowCompleted;
+ }
+
+ this.mToggleStatus = toggleStatus;
+ this.refreshItems(true);
+ }
+
+ /**
+ * Marks the box for today and the header for the current day of the week.
+ */
+ setDateBoxRelations() {
+ const today = this.today();
+
+ for (let header of this.dayHeaders) {
+ if (header.weekDay == today.weekday) {
+ header.setAttribute("relation", "today");
+ } else {
+ header.removeAttribute("relation");
+ }
+ }
+
+ for (let daybox of this.mDateBoxes) {
+ // Set up date relations.
+ switch (daybox.mDate.compare(today)) {
+ case -1:
+ daybox.setAttribute("relation", "past");
+ break;
+ case 0:
+ daybox.setAttribute("relation", "today");
+ break;
+ case 1:
+ daybox.setAttribute("relation", "future");
+ break;
+ }
+ }
+ }
+
+ /**
+ * Hide the week numbers.
+ */
+ hideWeekNumbers() {
+ const rows = this.monthbody.children;
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ for (let j = 0; j < row.children.length; j++) {
+ const daybox = row.children[j].firstElementChild;
+ const weekLabel = daybox.querySelector("[data-label='week']");
+ weekLabel.hidden = true;
+ }
+ }
+ }
+
+ /**
+ * Hide the days off.
+ */
+ hideDaysOff() {
+ const rows = this.monthbody.children;
+
+ const lastColNum = rows[0].children.length - 1;
+ for (let colNum = 0; colNum <= lastColNum; colNum++) {
+ const dayForColumn = (colNum + this.weekStartOffset) % 7;
+ const dayOff = this.mDaysOffArray.includes(dayForColumn) && !this.mDisplayDaysOff;
+ // Set the hidden attribute on the parentNode td.
+ this.dayHeaders[colNum].parentNode.toggleAttribute("hidden", dayOff);
+ for (let row of rows) {
+ row.children[colNum].toggleAttribute("hidden", dayOff);
+ }
+ }
+ }
+
+ /**
+ * Return the day box element for a given date.
+ *
+ * @param {calIDateTime} date - A date.
+ * @returns {?Element} A `calendar-month-day-box` element.
+ */
+ findDayBoxForDate(date) {
+ if (!this.mDateBoxes) {
+ return null;
+ }
+ for (const box of this.mDateBoxes) {
+ if (box.mDate.compare(date) == 0) {
+ return box;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the day box elements for a given calendar item.
+ *
+ * @param {calIItemBase} item - A calendar item.
+ * @returns {Element[]} An array of `calendar-month-day-box` elements.
+ */
+ findDayBoxesForItem(item) {
+ let targetDate = null;
+ let finishDate = null;
+ const boxes = [];
+
+ // All our boxes are in default time zone, so we need these times in them too.
+ if (item.isEvent()) {
+ targetDate = item.startDate.getInTimezone(this.mTimezone);
+ finishDate = item.endDate.getInTimezone(this.mTimezone);
+ } else if (item.isTodo()) {
+ // Consider tasks without entry OR due date.
+ if (item.entryDate || item.dueDate) {
+ targetDate = (item.entryDate || item.dueDate).getInTimezone(this.mTimezone);
+ finishDate = (item.dueDate || item.entryDate).getInTimezone(this.mTimezone);
+ }
+ }
+
+ if (!targetDate) {
+ return boxes;
+ }
+
+ if (!finishDate) {
+ const maybeBox = this.findDayBoxForDate(targetDate);
+ if (maybeBox) {
+ boxes.push(maybeBox);
+ }
+ return boxes;
+ }
+
+ if (targetDate.compare(this.mStartDate) < 0) {
+ targetDate = this.mStartDate.clone();
+ }
+
+ if (finishDate.compare(this.mEndDate) > 0) {
+ finishDate = this.mEndDate.clone();
+ finishDate.day++;
+ }
+
+ // Reset the time to 00:00, so that we really get all the boxes.
+ targetDate.isDate = false;
+ targetDate.hour = 0;
+ targetDate.minute = 0;
+ targetDate.second = 0;
+
+ if (targetDate.compare(finishDate) == 0) {
+ // We have also to handle zero length events in particular for
+ // tasks without entry or due date.
+ const box = this.findDayBoxForDate(targetDate);
+ if (box) {
+ boxes.push(box);
+ }
+ }
+
+ while (targetDate.compare(finishDate) == -1) {
+ const box = this.findDayBoxForDate(targetDate);
+
+ // This might not exist if the event spans the view start or end.
+ if (box) {
+ boxes.push(box);
+ }
+ targetDate.day += 1;
+ }
+
+ return boxes;
+ }
+
+ /**
+ * Display a calendar item.
+ *
+ * @param {calIItemBase} item - A calendar item.
+ */
+ doAddItem(item) {
+ this.findDayBoxesForItem(item).forEach(box => box.addItem(item));
+ }
+
+ /**
+ * Remove a calendar item so it is no longer displayed.
+ *
+ * @param {calIItemBase} item - A calendar item.
+ */
+ doRemoveItem(item) {
+ const boxes = this.findDayBoxesForItem(item);
+
+ if (!boxes.length) {
+ return;
+ }
+
+ const oldLength = this.mSelectedItems.length;
+
+ const isNotItem = a => a.hashId != item.hashId;
+ this.mSelectedItems = this.mSelectedItems.filter(isNotItem);
+
+ boxes.forEach(box => box.removeItem(item));
+
+ // If a deleted event was selected, announce that the selection changed.
+ if (oldLength != this.mSelectedItems.length) {
+ this.fireEvent("itemselect", this.mSelectedItems);
+ }
+ }
+
+ // CalendarFilteredViewMixin implementation.
+
+ /**
+ * Removes all items so they are no longer displayed.
+ */
+ clearItems() {
+ for (let dayBox of this.querySelectorAll("calendar-month-day-box")) {
+ dayBox.clear();
+ }
+ }
+
+ /**
+ * Remove all items for a given calendar so they are no longer displayed.
+ *
+ * @param {string} calendarId - The ID of the calendar to remove items from.
+ */
+ removeItemsFromCalendar(calendarId) {
+ if (!this.mDateBoxes) {
+ return;
+ }
+ for (const box of this.mDateBoxes) {
+ for (const id in box.mItemHash) {
+ const node = box.mItemHash[id];
+ const item = node.item;
+
+ if (item.calendar.id == calendarId) {
+ box.removeItem(item);
+ }
+ }
+ }
+ }
+
+ // End of CalendarFilteredViewMixin implementation.
+
+ /**
+ * Make a calendar item flash. Used when an alarm goes off to make the related item flash.
+ *
+ * @param {object} item - The calendar item to flash.
+ * @param {boolean} stop - Whether to stop flashing that's already started.
+ */
+ flashAlarm(item, stop) {
+ if (!this.mStartDate || !this.mEndDate) {
+ return;
+ }
+
+ const showIndicator = Services.prefs.getBoolPref("calendar.alarms.indicator.show", true);
+ const totaltime = Services.prefs.getIntPref("calendar.alarms.indicator.totaltime", 3600);
+
+ if (!stop && (!showIndicator || totaltime < 1)) {
+ // No need to animate if the indicator should not be shown.
+ return;
+ }
+
+ // Make sure the flashing attribute is set or reset on all visible boxes.
+ const boxes = this.findDayBoxesForItem(item);
+ for (const box of boxes) {
+ for (const id in box.mItemHash) {
+ const itemData = box.mItemHash[id];
+
+ if (itemData.item.hasSameIds(item)) {
+ if (stop) {
+ itemData.removeAttribute("flashing");
+ } else {
+ itemData.setAttribute("flashing", "true");
+ }
+ }
+ }
+ }
+
+ if (stop) {
+ // We are done flashing, prevent newly created event boxes from flashing.
+ delete this.mFlashingEvents[item.hashId];
+ } else {
+ // Set up a timer to stop the flashing after the total time.
+ this.mFlashingEvents[item.hashId] = item;
+ setTimeout(() => this.flashAlarm(item, true), totaltime);
+ }
+ }
+ }
+
+ MozElements.CalendarMonthBaseView = CalendarMonthBaseView;
+}