diff options
Diffstat (limited to 'comm/calendar/base/modules/utils/calPrintUtils.jsm')
-rw-r--r-- | comm/calendar/base/modules/utils/calPrintUtils.jsm | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/comm/calendar/base/modules/utils/calPrintUtils.jsm b/comm/calendar/base/modules/utils/calPrintUtils.jsm new file mode 100644 index 0000000000..ad7b022f3c --- /dev/null +++ b/comm/calendar/base/modules/utils/calPrintUtils.jsm @@ -0,0 +1,616 @@ +/* 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/. */ + +/** + * Helpers for printing. + * + * This file detects when printing starts, and if it's the calendar that is + * being printed, injects calendar-print.js into the printing UI. + * + * Also contains the code for formatting the to-be-printed document as chosen + * by the user. + */ + +// NOTE: This module should not be loaded directly, it is available when +// including calUtils.jsm under the cal.print namespace. + +const EXPORTED_SYMBOLS = ["calprint"]; + +const lazy = {}; +ChromeUtils.defineModuleGetter(lazy, "cal", "resource:///modules/calendar/calUtils.jsm"); + +var calprint = { + ensureInitialized() { + // Deliberate no-op. By calling this function from outside, you've ensured + // the observer has been added. + }, + + async draw(document, type, startDate, endDate, filter, notDueTasks) { + lazy.cal.view.colorTracker.addColorsToDocument(document); + + let listContainer = document.getElementById("list-container"); + while (listContainer.lastChild) { + listContainer.lastChild.remove(); + } + let monthContainer = document.getElementById("month-container"); + while (monthContainer.lastChild) { + monthContainer.lastChild.remove(); + } + let weekContainer = document.getElementById("week-container"); + while (weekContainer.lastChild) { + weekContainer.lastChild.remove(); + } + + let taskContainer = document.getElementById("task-container"); + while (taskContainer.lastChild) { + taskContainer.lastChild.remove(); + } + document.getElementById("tasks-list-box").hidden = true; + + switch (type) { + case "list": + await listView.draw(document, startDate, endDate, filter, notDueTasks); + break; + case "monthGrid": + await monthGridView.draw(document, startDate, endDate, filter, notDueTasks); + break; + case "weekPlanner": + await weekPlannerView.draw(document, startDate, endDate, filter, notDueTasks); + break; + } + }, +}; + +/** + * Serializes the given item by setting marked nodes to the item's content. + * Has some expectations about the DOM document (in CSS-selector-speak), all + * following nodes MUST exist. + * + * - #item-template will be cloned and filled, and modified: + * - .item-interval gets the time interval of the item. + * - .item-title gets the item title + * - .category-color-box gets a 2px solid border in category color + * - .calendar-color-box gets background color of the calendar + * + * @param document The DOM Document to set things on + * @param item The item to serialize + * @param dayContainer The DOM Node to insert the container in + */ +function addItemToDaybox(document, item, boxDate, dayContainer) { + // Clone our template + let itemNode = document.getElementById("item-template").content.firstElementChild.cloneNode(true); + itemNode.removeAttribute("id"); + itemNode.item = item; + + // Fill in details of the item + let itemInterval = getItemIntervalString(item, boxDate); + itemNode.querySelector(".item-interval").textContent = itemInterval; + itemNode.querySelector(".item-title").textContent = item.title; + + // Fill in category details + let categoriesArray = item.getCategories(); + if (categoriesArray.length > 0) { + let cssClassesArray = categoriesArray.map(lazy.cal.view.formatStringForCSSRule); + itemNode.style.borderInlineEnd = `2px solid var(--category-${cssClassesArray[0]}-color)`; + } + + // Fill in calendar color + let cssSafeId = lazy.cal.view.formatStringForCSSRule(item.calendar.id); + itemNode.style.color = `var(--calendar-${cssSafeId}-forecolor)`; + itemNode.style.backgroundColor = `var(--calendar-${cssSafeId}-backcolor)`; + + // Add it to the day container in the right order + lazy.cal.data.binaryInsertNode(dayContainer, itemNode, item, lazy.cal.view.compareItems); +} + +/** + * Serializes the given item by setting marked nodes to the item's + * content. Should be used for tasks with no start and due date. Has + * some expectations about the DOM document (in CSS-selector-speak), + * all following nodes MUST exist. + * + * - Nodes will be added to #task-container. + * - #task-list-box will have the "hidden" attribute removed. + * - #task-template will be cloned and filled, and modified: + * - .task-checkbox gets the "checked" attribute set, if completed + * - .task-title gets the item title. + * + * @param document The DOM Document to set things on + * @param item The item to serialize + */ +function addItemToDayboxNodate(document, item) { + let taskContainer = document.getElementById("task-container"); + let taskNode = document.getElementById("task-template").content.firstElementChild.cloneNode(true); + taskNode.item = item; + + let taskListBox = document.getElementById("tasks-list-box"); + if (taskListBox.hasAttribute("hidden")) { + let tasksTitle = document.getElementById("tasks-title"); + taskListBox.removeAttribute("hidden"); + tasksTitle.textContent = lazy.cal.l10n.getCalString("tasksWithNoDueDate"); + } + + // Fill in details of the task + if (item.isCompleted) { + taskNode.querySelector(".task-checkbox").setAttribute("checked", "checked"); + } + + taskNode.querySelector(".task-title").textContent = item.title; + + const collator = new Intl.Collator(); + lazy.cal.data.binaryInsertNode( + taskContainer, + taskNode, + item, + collator.compare, + node => node.item.title + ); +} + +/** + * Get time interval string for the given item. Returns an empty string for all-day items. + * + * @param aItem The item providing the interval + * @returns The string describing the interval + */ +function getItemIntervalString(aItem, aBoxDate) { + // omit time label for all-day items + let formatter = lazy.cal.dtz.formatter; + let startDate = aItem[lazy.cal.dtz.startDateProp(aItem)]; + let endDate = aItem[lazy.cal.dtz.endDateProp(aItem)]; + if ((startDate && startDate.isDate) || (endDate && endDate.isDate)) { + return ""; + } + + // check for tasks without start and/or due date + if (!startDate || !endDate) { + return formatter.formatItemTimeInterval(aItem); + } + + let defaultTimezone = lazy.cal.dtz.defaultTimezone; + startDate = startDate.getInTimezone(defaultTimezone); + endDate = endDate.getInTimezone(defaultTimezone); + let start = startDate.clone(); + let end = endDate.clone(); + start.isDate = true; + end.isDate = true; + if (start.compare(end) == 0) { + // Events that start and end in the same day. + return formatter.formatTimeInterval(startDate, endDate); + } + // Events that span two or more days. + let compareStart = aBoxDate.compare(start); + let compareEnd = aBoxDate.compare(end); + if (compareStart == 0) { + return "\u21e4 " + formatter.formatTime(startDate); // unicode '⇤' + } else if (compareStart > 0 && compareEnd < 0) { + return "\u21ff"; // unicode '↔' + } else if (compareEnd == 0) { + return "\u21e5 " + formatter.formatTime(endDate); // unicode '⇥' + } + return ""; +} + +/** + * Gets items from the composite calendar for printing. + * + * @param {calIDateTime} startDate + * @param {calIDateTime} endDate + * @param {integer} filter - calICalendar ITEM_FILTER flags + * @param {boolean} notDueTasks - if true, include tasks with no due date + * @returns {Promise<calIItemBase[]>} + */ +async function getItems(startDate, endDate, filter, notDueTasks) { + let window = Services.wm.getMostRecentWindow("mail:3pane"); + let compositeCalendar = lazy.cal.view.getCompositeCalendar(window); + + let itemList = []; + for await (let items of lazy.cal.iterate.streamValues( + compositeCalendar.getItems(filter, 0, startDate, endDate) + )) { + if (!notDueTasks) { + items = items.filter(i => !i.isTodo() || i.entryDate || i.dueDate); + } + itemList = itemList.concat(items); + } + return itemList; +} + +/** + * A simple list of calendar items. + */ +let listView = { + /** + * Create the list view. + * + * @param {HTMLDocument} document + * @param {calIDateTime} startDate - the first day of the months to be displayed + * @param {calIDateTime} endDate - the first day of the month AFTER the + * months to be displayed + * @param {integer} filter - calICalendar ITEM_FILTER flags + * @param {boolean} notDueTasks - if true, include tasks with no due date + */ + async draw(document, startDate, endDate, filter, notDueTasks) { + let container = document.getElementById("list-container"); + let listItemTemplate = document.getElementById("list-item-template"); + + // Get and sort items. + let items = await getItems(startDate, endDate, filter, notDueTasks); + items.sort((a, b) => { + let start_a = a[lazy.cal.dtz.startDateProp(a)]; + if (!start_a) { + return -1; + } + let start_b = b[lazy.cal.dtz.startDateProp(b)]; + if (!start_b) { + return 1; + } + return start_a.compare(start_b); + }); + + // Display the items. + for (let item of items) { + let itemNode = listItemTemplate.content.firstElementChild.cloneNode(true); + + let setupTextRow = function (classKey, propValue, prefixKey) { + if (propValue) { + let prefix = lazy.cal.l10n.getCalString(prefixKey); + itemNode.querySelector("." + classKey + "key").textContent = prefix; + itemNode.querySelector("." + classKey).textContent = propValue; + } else { + let row = itemNode.querySelector("." + classKey + "row"); + if ( + row.nextSibling.nodeType == row.nextSibling.TEXT_NODE || + row.nextSibling.nodeType == row.nextSibling.CDATA_SECTION_NODE + ) { + row.nextSibling.remove(); + } + row.remove(); + } + }; + + let itemStartDate = item[lazy.cal.dtz.startDateProp(item)]; + let itemEndDate = item[lazy.cal.dtz.endDateProp(item)]; + if (itemStartDate || itemEndDate) { + // This is a task with a start or due date, format accordingly + let prefixWhen = lazy.cal.l10n.getCalString("htmlPrefixWhen"); + itemNode.querySelector(".intervalkey").textContent = prefixWhen; + + let startNode = itemNode.querySelector(".dtstart"); + let dateString = lazy.cal.dtz.formatter.formatItemInterval(item); + startNode.setAttribute("title", itemStartDate ? itemStartDate.icalString : "none"); + startNode.textContent = dateString; + } else { + let row = itemNode.querySelector(".intervalrow"); + row.remove(); + if ( + row.nextSibling && + (row.nextSibling.nodeType == row.nextSibling.TEXT_NODE || + row.nextSibling.nodeType == row.nextSibling.CDATA_SECTION_NODE) + ) { + row.nextSibling.remove(); + } + } + + let itemTitle = item.isCompleted + ? lazy.cal.l10n.getCalString("htmlTaskCompleted", [item.title]) + : item.title; + setupTextRow("summary", itemTitle, "htmlPrefixTitle"); + + setupTextRow("location", item.getProperty("LOCATION"), "htmlPrefixLocation"); + setupTextRow("description", item.getProperty("DESCRIPTION"), "htmlPrefixDescription"); + + container.appendChild(itemNode); + } + + // Set the page title. + endDate.day--; + document.title = lazy.cal.dtz.formatter.formatInterval(startDate, endDate); + }, +}; + +/** + * A layout with one calendar month per page. + */ +let monthGridView = { + dayTable: {}, + + /** + * Create the month grid view. + * + * @param {HTMLDocument} document + * @param {calIDateTime} startDate - the first day of the months to be displayed + * @param {calIDateTime} endDate - the first day of the month AFTER the + * months to be displayed + * @param {integer} filter - calICalendar ITEM_FILTER flags + * @param {boolean} notDueTasks - if true, include tasks with no due date + */ + async draw(document, startDate, endDate, filter, notDueTasks) { + let container = document.getElementById("month-container"); + + // Draw the month grid(s). + let current = startDate.clone(); + do { + container.appendChild(this.drawMonth(document, current)); + current.month += 1; + } while (current.compare(endDate) < 0); + + // Extend the date range to include adjacent days that will be printed. + startDate = lazy.cal.weekInfoService.getStartOfWeek(startDate); + // Get the end of the week containing the last day of the month, not the + // week containing the first day of the next month. + endDate.day--; + endDate = lazy.cal.weekInfoService.getEndOfWeek(endDate); + endDate.day++; // Add a day to include items from the last day. + + // Get and display the items. + let items = await getItems(startDate, endDate, filter, notDueTasks); + let defaultTimezone = lazy.cal.dtz.defaultTimezone; + for (let item of items) { + let itemStartDate = + item[lazy.cal.dtz.startDateProp(item)] || item[lazy.cal.dtz.endDateProp(item)]; + let itemEndDate = + item[lazy.cal.dtz.endDateProp(item)] || item[lazy.cal.dtz.startDateProp(item)]; + + if (!itemStartDate && !itemEndDate) { + addItemToDayboxNodate(document, item); + continue; + } + itemStartDate = itemStartDate.getInTimezone(defaultTimezone); + itemEndDate = itemEndDate.getInTimezone(defaultTimezone); + + let boxDate = itemStartDate.clone(); + boxDate.isDate = true; + for (boxDate; boxDate.compare(itemEndDate) < (itemEndDate.isDate ? 0 : 1); boxDate.day++) { + let boxDateString = boxDate.icalString; + if (boxDateString in this.dayTable) { + for (let dayBox of this.dayTable[boxDateString]) { + addItemToDaybox(document, item, boxDate, dayBox.querySelector(".items")); + } + } + } + } + + // Set the page title. + let months = container.querySelectorAll("table"); + if (months.length == 1) { + document.title = months[0].querySelector(".month-title").textContent; + } else { + document.title = + months[0].querySelector(".month-title").textContent + + " – " + + months[months.length - 1].querySelector(".month-title").textContent; + } + }, + + /** + * Create one month from the template. + * + * @param {HTMLDocument} document + * @param {calIDateTime} startOfMonth - the first day of the month + */ + drawMonth(document, startOfMonth) { + let monthTemplate = document.getElementById("month-template"); + let month = monthTemplate.content.firstElementChild.cloneNode(true); + + // Set up the month title + let monthName = lazy.cal.l10n.formatMonth(startOfMonth.month + 1, "calendar", "monthInYear"); + let monthTitle = lazy.cal.l10n.getCalString("monthInYear", [monthName, startOfMonth.year]); + month.rows[0].cells[0].firstElementChild.textContent = monthTitle; + + // Set up the weekday titles + let weekStart = Services.prefs.getIntPref("calendar.week.start", 0); + for (let i = 0; i < 7; i++) { + let dayNumber = ((i + weekStart) % 7) + 1; + month.rows[1].cells[i].firstElementChild.textContent = lazy.cal.l10n.getDateFmtString( + `day.${dayNumber}.Mmm` + ); + } + + // Set up each week + let endOfMonthView = lazy.cal.weekInfoService.getEndOfWeek(startOfMonth.endOfMonth); + let startOfMonthView = lazy.cal.weekInfoService.getStartOfWeek(startOfMonth); + let mainMonth = startOfMonth.month; + + for ( + let weekStart = startOfMonthView; + weekStart.compare(endOfMonthView) < 0; + weekStart.day += 7 + ) { + month.tBodies[0].appendChild(this.drawWeek(document, weekStart, mainMonth)); + } + + return month; + }, + + /** + * Create one week from the template. + * + * @param {HTMLDocument} document + * @param {calIDateTime} startOfWeek - the first day of the week + * @param {number} mainMonth - the month that this week is being added to + * (for marking days that are in adjacent months) + */ + drawWeek(document, startOfWeek, mainMonth) { + const weekdayMap = [ + "d0sundaysoff", + "d1mondaysoff", + "d2tuesdaysoff", + "d3wednesdaysoff", + "d4thursdaysoff", + "d5fridaysoff", + "d6saturdaysoff", + ]; + + let weekTemplate = document.getElementById("month-week-template"); + let week = weekTemplate.content.firstElementChild.cloneNode(true); + + // Set up day numbers for all days in this week + let date = startOfWeek.clone(); + for (let i = 0; i < 7; i++) { + let dayBox = week.cells[i]; + dayBox.querySelector(".day-title").textContent = date.day; + + let weekDay = date.weekday; + let dayOffPrefName = "calendar.week." + weekdayMap[weekDay]; + if (Services.prefs.getBoolPref(dayOffPrefName, false)) { + dayBox.classList.add("day-off"); + } + + if (date.month != mainMonth) { + dayBox.classList.add("out-of-month"); + } + + if (date.icalString in this.dayTable) { + this.dayTable[date.icalString].push(dayBox); + } else { + this.dayTable[date.icalString] = [dayBox]; + } + date.day++; + } + + return week; + }, +}; + +/** + * A layout with seven days per page. The week layout is NOT aware of the + * start-of-week preferences. It always begins on a Monday. + */ +let weekPlannerView = { + dayTable: {}, + + /** + * Create the week planner view. + * + * @param {HTMLDocument} document + * @param {calIDateTime} startDate - the Monday of the first week to be displayed + * @param {calIDateTime} endDate - the Monday AFTER the last week to be displayed + * @param {integer} filter - calICalendar ITEM_FILTER flags + * @param {boolean} notDueTasks - if true, include tasks with no due date + */ + async draw(document, startDate, endDate, filter, notDueTasks) { + let container = document.getElementById("week-container"); + + // Draw the week grid(s). + for (let current = startDate.clone(); current.compare(endDate) < 0; current.day += 7) { + container.appendChild(this.drawWeek(document, current)); + } + + // Get and display the items. + let items = await getItems(startDate, endDate, filter, notDueTasks); + let defaultTimezone = lazy.cal.dtz.defaultTimezone; + for (let item of items) { + let itemStartDate = + item[lazy.cal.dtz.startDateProp(item)] || item[lazy.cal.dtz.endDateProp(item)]; + let itemEndDate = + item[lazy.cal.dtz.endDateProp(item)] || item[lazy.cal.dtz.startDateProp(item)]; + + if (!itemStartDate && !itemEndDate) { + addItemToDayboxNodate(document, item); + continue; + } + itemStartDate = itemStartDate.getInTimezone(defaultTimezone); + itemEndDate = itemEndDate.getInTimezone(defaultTimezone); + + let boxDate = itemStartDate.clone(); + boxDate.isDate = true; + for (boxDate; boxDate.compare(itemEndDate) < (itemEndDate.isDate ? 0 : 1); boxDate.day++) { + let boxDateString = boxDate.icalString; + if (boxDateString in this.dayTable) { + addItemToDaybox(document, item, boxDate, this.dayTable[boxDateString]); + } + } + } + + // Set the page title. + let weeks = container.querySelectorAll("table"); + if (weeks.length == 1) { + document.title = lazy.cal.l10n.getCalString("singleLongCalendarWeek", [weeks[0].number]); + } else { + document.title = lazy.cal.l10n.getCalString("severalLongCalendarWeeks", [ + weeks[0].number, + weeks[weeks.length - 1].number, + ]); + } + }, + + /** + * Create one week from the template. + * + * @param {HTMLDocument} document + * @param {calIDateTime} monday - the Monday of the week + */ + drawWeek(document, monday) { + // In the order they appear on the page. + const weekdayMap = [ + "d1mondaysoff", + "d2tuesdaysoff", + "d3wednesdaysoff", + "d4thursdaysoff", + "d5fridaysoff", + "d6saturdaysoff", + "d0sundaysoff", + ]; + + let weekTemplate = document.getElementById("week-template"); + let week = weekTemplate.content.firstElementChild.cloneNode(true); + + // Set up the week number title + week.number = lazy.cal.weekInfoService.getWeekTitle(monday); + week.querySelector(".week-title").textContent = lazy.cal.l10n.getCalString("WeekTitle", [ + week.number, + ]); + + // Set up the day boxes + let currentDate = monday.clone(); + for (let i = 0; i < 7; i++) { + let day = week.rows[1].cells[i]; + + let titleNode = day.querySelector(".day-title"); + titleNode.textContent = lazy.cal.dtz.formatter.formatDateLong(currentDate); + + this.dayTable[currentDate.icalString] = day.querySelector(".items"); + + if (Services.prefs.getBoolPref("calendar.week." + weekdayMap[i], false)) { + day.classList.add("day-off"); + } + + currentDate.day++; + } + + return week; + }, +}; + +Services.obs.addObserver( + { + async observe(subDialogWindow) { + if (!subDialogWindow.location.href.startsWith("chrome://global/content/print.html?")) { + return; + } + + await new Promise(resolve => + subDialogWindow.document.addEventListener("print-settings", resolve, { once: true }) + ); + + if ( + subDialogWindow.PrintEventHandler.activeCurrentURI != + "chrome://calendar/content/printing-template.html" + ) { + return; + } + + Services.scriptloader.loadSubScript( + "chrome://calendar/content/widgets/calendar-minimonth.js", + subDialogWindow + ); + Services.scriptloader.loadSubScript( + "chrome://calendar/content/calendar-print.js", + subDialogWindow + ); + }, + }, + "subdialog-loaded" +); |