diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/base/content/calendar-editable-item.js | |
parent | Initial commit. (diff) | |
download | thunderbird-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.js | 464 |
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); +} |