summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-editable-item.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/base/content/calendar-editable-item.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/calendar-editable-item.js464
1 files changed, 464 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-editable-item.js b/comm/calendar/base/content/calendar-editable-item.js
new file mode 100644
index 0000000000..e45570c995
--- /dev/null
+++ b/comm/calendar/base/content/calendar-editable-item.js
@@ -0,0 +1,464 @@
+/* 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/. */
+
+/* global MozElements, MozXULElement, onMouseOverItem, invokeEventDragSession */
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+ /**
+ * The MozCalendarEditableItem widget is used as a full day event item in the
+ * Day and Week views of the calendar. It displays the event name, alarm icon
+ * and the category type color. It gets displayed in the header container of
+ * the respective view of the calendar.
+ *
+ * @augments MozXULElement
+ */
+ class MozCalendarEditableItem extends MozXULElement {
+ static get inheritedAttributes() {
+ return {
+ ".alarm-icons-box": "flashing",
+ };
+ }
+ constructor() {
+ super();
+
+ this.mOccurrence = null;
+
+ this.mSelected = false;
+
+ this.mCalendarView = null;
+
+ this.addEventListener(
+ "contextmenu",
+ event => {
+ // If the middle/right button was used for click just select the item.
+ if (!this.selected) {
+ this.select(event);
+ }
+ },
+ true
+ );
+
+ this.addEventListener("click", event => {
+ if (event.button != 0 || this.mEditing) {
+ return;
+ }
+
+ // If the left button was used and the item is already selected
+ // and there are no multiple items selected start
+ // the 'single click edit' timeout. Otherwise select the item too.
+ // Also, check if the calendar is readOnly or we are offline.
+
+ if (
+ this.selected &&
+ !(event.ctrlKey || event.metaKey) &&
+ cal.acl.isCalendarWritable(this.mOccurrence.calendar) &&
+ !cal.itip.isInvitation(this.mOccurrence)
+ ) {
+ if (this.editingTimer) {
+ clearTimeout(this.editingTimer);
+ }
+ this.editingTimer = setTimeout(() => this.startEditing(), 350);
+ } else {
+ this.select(event);
+ if (!this.closest("richlistitem")) {
+ event.stopPropagation();
+ }
+ }
+ });
+
+ this.addEventListener("dblclick", event => {
+ if (event.button != 0) {
+ return;
+ }
+
+ event.stopPropagation();
+
+ // Stop 'single click edit' timeout (if started).
+ if (this.editingTimer) {
+ clearTimeout(this.editingTimer);
+ this.editingTimer = null;
+ }
+
+ if (this.calendarView && this.calendarView.controller) {
+ let item = event.ctrlKey ? this.mOccurrence.parentItem : this.mOccurrence;
+ if (Services.prefs.getBoolPref("calendar.events.defaultActionEdit", true)) {
+ this.calendarView.controller.modifyOccurrence(item);
+ return;
+ }
+ this.calendarView.controller.viewOccurrence(item);
+ }
+ });
+
+ this.addEventListener("mouseover", event => {
+ if (this.calendarView && this.calendarView.controller) {
+ event.stopPropagation();
+ onMouseOverItem(event);
+ }
+ });
+
+ // We have two event listeners for dragstart. This event listener is for the bubbling phase.
+ this.addEventListener("dragstart", event => {
+ if (document.monthDragEvent?.localName == "calendar-event-box") {
+ return;
+ }
+ let item = this.occurrence;
+ let isInvitation =
+ item.calendar instanceof Ci.calISchedulingSupport && item.calendar.isInvitation(item);
+ if (
+ !cal.acl.isCalendarWritable(item.calendar) ||
+ !cal.acl.userCanModifyItem(item) ||
+ isInvitation
+ ) {
+ return;
+ }
+ if (!this.selected) {
+ this.select(event);
+ }
+ invokeEventDragSession(item, this);
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.appendChild(
+ MozXULElement.parseXULToFragment(`
+ <html:div class="calendar-item-flex">
+ <html:img class="item-type-icon" alt="" />
+ <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>
+ <html:div class="location-desc"></html:div>
+ <html:div class="calendar-category-box"></html:div>
+ `)
+ );
+
+ this.classList.add("calendar-color-box", "calendar-item-container");
+
+ // 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.
+ this.addEventListener(
+ "dragstart",
+ event => {
+ document.monthDragEvent = this;
+ },
+ true
+ );
+
+ this.style.pointerEvents = "auto";
+ this.setAttribute("tooltip", "itemTooltip");
+ this.setAttribute("tabindex", "-1");
+ this.addEventNameTextboxListener();
+ this.initializeAttributeInheritance();
+ }
+
+ set parentBox(val) {
+ this.mParentBox = val;
+ }
+
+ get parentBox() {
+ return this.mParentBox;
+ }
+
+ set selected(val) {
+ if (val && !this.mSelected) {
+ this.mSelected = true;
+ this.setAttribute("selected", "true");
+ this.focus();
+ } else if (!val && this.mSelected) {
+ this.mSelected = false;
+ this.removeAttribute("selected");
+ this.blur();
+ }
+ }
+
+ get selected() {
+ return this.mSelected;
+ }
+
+ set calendarView(val) {
+ this.mCalendarView = val;
+ }
+
+ get calendarView() {
+ return this.mCalendarView;
+ }
+
+ set occurrence(val) {
+ this.mOccurrence = val;
+ this.setEditableLabel();
+ this.setLocationLabel();
+ this.setCSSClasses();
+ }
+
+ get occurrence() {
+ return this.mOccurrence;
+ }
+
+ get eventNameLabel() {
+ return this.querySelector(".event-name-label");
+ }
+
+ get eventNameTextbox() {
+ return this.querySelector(".event-name-input");
+ }
+
+ addEventNameTextboxListener() {
+ let stopPropagationIfEditing = event => {
+ if (this.mEditing) {
+ event.stopPropagation();
+ }
+ };
+ // While editing, single click positions cursor, so don't propagate.
+ this.eventNameTextbox.onclick = stopPropagationIfEditing;
+ // While editing, double click selects words, so don't propagate.
+ this.eventNameTextbox.ondblclick = stopPropagationIfEditing;
+ // While editing, don't propagate mousedown/up (selects calEvent).
+ this.eventNameTextbox.onmousedown = stopPropagationIfEditing;
+ this.eventNameTextbox.onmouseup = stopPropagationIfEditing;
+ this.eventNameTextbox.onblur = () => {
+ this.stopEditing(true);
+ };
+ this.eventNameTextbox.onkeypress = event => {
+ if (event.key == "Enter") {
+ // Save on enter.
+ this.stopEditing(true);
+ } else if (event.key == "Escape") {
+ // Abort on escape.
+ this.stopEditing(false);
+ }
+ };
+ }
+
+ setEditableLabel() {
+ let label = this.eventNameLabel;
+ let item = this.mOccurrence;
+ label.textContent = item.title
+ ? item.title.replace(/\n/g, " ")
+ : cal.l10n.getCalString("eventUntitled");
+ }
+
+ setLocationLabel() {
+ let locationLabel = this.querySelector(".location-desc");
+ let location = this.mOccurrence.getProperty("LOCATION");
+ let showLocation = Services.prefs.getBoolPref("calendar.view.showLocation", false);
+
+ locationLabel.textContent = showLocation && location ? location : "";
+ locationLabel.hidden = !showLocation || !location;
+ }
+
+ setCSSClasses() {
+ let item = this.mOccurrence;
+ let cssSafeId = cal.view.formatStringForCSSRule(item.calendar.id);
+ this.style.setProperty("--item-backcolor", `var(--calendar-${cssSafeId}-backcolor)`);
+ this.style.setProperty("--item-forecolor", `var(--calendar-${cssSafeId}-forecolor)`);
+ let categoriesBox = this.querySelector(".calendar-category-box");
+
+ let categoriesArray = item.getCategories().map(cal.view.formatStringForCSSRule);
+ // Find the first category with a colour.
+ let firstCategory = categoriesArray.find(
+ category => Services.prefs.getStringPref("calendar.category.color." + category, "") != ""
+ );
+ if (firstCategory) {
+ categoriesBox.hidden = false;
+ categoriesBox.style.backgroundColor = `var(--category-${firstCategory}-color)`;
+ } else {
+ categoriesBox.hidden = true;
+ }
+
+ // Add alarm icons as needed.
+ let alarms = item.getAlarms();
+ if (alarms.length && Services.prefs.getBoolPref("calendar.alarms.indicator.show", true)) {
+ let iconsBox = this.querySelector(".alarm-icons-box");
+ // Set suppressed status on the icons box.
+ iconsBox.toggleAttribute("suppressed", item.calendar.getProperty("suppressAlarms"));
+
+ cal.alarms.addReminderImages(iconsBox, alarms);
+ }
+
+ // Item classification / privacy.
+ let classificationIcon = this.querySelector(".item-classification-icon");
+ if (classificationIcon) {
+ switch (item.privacy) {
+ case "PRIVATE":
+ classificationIcon.setAttribute(
+ "src",
+ "chrome://calendar/skin/shared/icons/private.svg"
+ );
+ // Set the alt attribute.
+ document.l10n.setAttributes(
+ classificationIcon,
+ "calendar-editable-item-privacy-icon-private"
+ );
+ break;
+ case "CONFIDENTIAL":
+ classificationIcon.setAttribute(
+ "src",
+ "chrome://calendar/skin/shared/icons/confidential.svg"
+ );
+ // Set the alt attribute.
+ document.l10n.setAttributes(
+ classificationIcon,
+ "calendar-editable-item-privacy-icon-confidential"
+ );
+ break;
+ default:
+ classificationIcon.removeAttribute("src");
+ classificationIcon.removeAttribute("data-l10n-id");
+ classificationIcon.setAttribute("alt", "");
+ break;
+ }
+ }
+
+ let recurrenceIcon = this.querySelector(".item-recurrence-icon");
+ if (item.parentItem != item && item.parentItem.recurrenceInfo) {
+ if (item.parentItem.recurrenceInfo.getExceptionFor(item.recurrenceId)) {
+ recurrenceIcon.setAttribute(
+ "src",
+ "chrome://messenger/skin/icons/new/recurrence-exception.svg"
+ );
+ document.l10n.setAttributes(
+ recurrenceIcon,
+ "calendar-editable-item-recurrence-exception"
+ );
+ } else {
+ recurrenceIcon.setAttribute("src", "chrome://messenger/skin/icons/new/recurrence.svg");
+ document.l10n.setAttributes(recurrenceIcon, "calendar-editable-item-recurrence");
+ }
+ recurrenceIcon.hidden = false;
+ } else {
+ recurrenceIcon.removeAttribute("src");
+ recurrenceIcon.removeAttribute("data-l10n-id");
+ recurrenceIcon.setAttribute("alt", "");
+ recurrenceIcon.hidden = true;
+ }
+
+ // Event type specific properties.
+ if (item.isEvent() && item.startDate.isDate) {
+ this.setAttribute("allday", "true");
+ }
+ if (item.isTodo()) {
+ let icon = this.querySelector(".item-type-icon");
+ if (cal.item.getProgressAtom(item) === "completed") {
+ icon.setAttribute("src", "chrome://calendar/skin/shared/todo-complete.svg");
+ document.l10n.setAttributes(icon, "calendar-editable-item-todo-icon-completed-task");
+ } else {
+ icon.setAttribute("src", "chrome://calendar/skin/shared/todo.svg");
+ document.l10n.setAttributes(icon, "calendar-editable-item-todo-icon-task");
+ }
+ }
+
+ if (this.calendarView && item.hashId in this.calendarView.mFlashingEvents) {
+ this.setAttribute("flashing", "true");
+ }
+
+ if (alarms.length) {
+ this.setAttribute("alarm", "true");
+ }
+
+ // Priority.
+ if (item.priority > 0 && item.priority < 5) {
+ this.setAttribute("priority", "high");
+ } else if (item.priority > 5 && item.priority < 10) {
+ this.setAttribute("priority", "low");
+ }
+
+ // Status attribute.
+ if (item.status) {
+ this.setAttribute("status", item.status.toUpperCase());
+ }
+
+ // Item class.
+ if (item.hasProperty("CLASS")) {
+ this.setAttribute("itemclass", item.getProperty("CLASS"));
+ }
+
+ // Calendar name.
+ this.setAttribute("calendar", item.calendar.name.toLowerCase());
+
+ // Invitation.
+ if (cal.itip.isInvitation(item)) {
+ this.setAttribute(
+ "invitation-status",
+ cal.itip.getInvitedAttendee(item).participationStatus
+ );
+ }
+ }
+
+ startEditing() {
+ this.editingTimer = null;
+ this.mOriginalTextLabel = this.mOccurrence.title;
+
+ this.eventNameLabel.hidden = true;
+
+ this.mEditing = true;
+
+ this.eventNameTextbox.value = this.mOriginalTextLabel;
+ this.eventNameTextbox.hidden = false;
+ this.eventNameTextbox.focus();
+ }
+
+ get isEditing() {
+ return this.mEditing || false;
+ }
+
+ select(event) {
+ if (!this.calendarView) {
+ return;
+ }
+ let items = this.calendarView.mSelectedItems.slice();
+ if (event.ctrlKey || event.metaKey) {
+ if (this.selected) {
+ let pos = items.indexOf(this.mOccurrence);
+ items.splice(pos, 1);
+ } else {
+ items.push(this.mOccurrence);
+ }
+ } else {
+ items = [this.mOccurrence];
+ }
+ this.calendarView.setSelectedItems(items);
+ }
+
+ stopEditing(saveChanges) {
+ if (!this.mEditing) {
+ return;
+ }
+
+ this.mEditing = false;
+
+ if (saveChanges && this.eventNameTextbox.value != this.mOriginalTextLabel) {
+ this.calendarView.controller.modifyOccurrence(
+ this.mOccurrence,
+ null,
+ null,
+ this.eventNameTextbox.value || cal.l10n.getCalString("eventUntitled")
+ );
+
+ // Note that as soon as we do the modifyItem, this element ceases to exist,
+ // so don't bother trying to modify anything further here! ('this' exists,
+ // because it's being kept alive, but our child content etc. is all gone).
+ return;
+ }
+
+ this.eventNameTextbox.hidden = true;
+ this.eventNameLabel.hidden = false;
+ }
+ }
+
+ MozElements.MozCalendarEditableItem = MozCalendarEditableItem;
+
+ customElements.define("calendar-editable-item", MozCalendarEditableItem);
+}