Bug 1558384 - Use CSS variables for calendar item colours rather than dynamically adding CSS rules. r=philipp DONTBUILD
authorGeoff Lankow <geoff@darktrojan.net>
Wed, 26 Jun 2019 10:27:39 +0200
changeset 35955 b58f19c12f2270fd3d828f68882477a1e1cd0304
parent 35954 8a22b144d02d572bcb0f3f9937c03d9000a7c785
child 35956 0fdada2dff357b49d6a3aaabfc4684cc536953f1
push id392
push userclokep@gmail.com
push dateMon, 02 Sep 2019 20:17:19 +0000
reviewersphilipp
bugs1558384
Bug 1558384 - Use CSS variables for calendar item colours rather than dynamically adding CSS rules. r=philipp DONTBUILD
calendar/base/content/agenda-listbox.js
calendar/base/content/calendar-chrome-startup.js
calendar/base/content/calendar-month-view.xml
calendar/base/content/calendar-multiday-view.xml
calendar/base/content/calendar-view-core.xml
calendar/base/content/calendar-views-utils.js
calendar/base/content/widgets/calendar-list-tree.js
calendar/base/modules/utils/calViewUtils.jsm
calendar/base/themes/common/calendar-views.css
calendar/providers/gdata/content/gdata-calendar-creation.js
--- a/calendar/base/content/agenda-listbox.js
+++ b/calendar/base/content/agenda-listbox.js
@@ -307,22 +307,19 @@
                 titlebox.textContent = aItem.title;
             } else {
                 durationbox.textContent += " " + aItem.title;
             }
             this.refreshColor();
         }
 
         refreshColor() {
-            let calcolor = (this.mOccurrence &&
-                this.mOccurrence.calendar.getProperty("color")) ||
-                "#a8c2e1";
-
             let imagebox = this.querySelector(".agenda-calendar-image");
-            imagebox.setAttribute("style", "background-color: " + calcolor + ";");
+            let cssSafeId = cal.view.formatStringForCSSRule(this.mOccurrence.calendar.id);
+            imagebox.style.backgroundColor = `var(--calendar-${cssSafeId}-backcolor)`;
         }
 
         get occurrence() {
             return this.mOccurrence;
         }
     }
 
 
--- a/calendar/base/content/calendar-chrome-startup.js
+++ b/calendar/base/content/calendar-chrome-startup.js
@@ -48,16 +48,19 @@ async function commonInitCalendar() {
     // Start alarm service
     Cc["@mozilla.org/calendar/alarm-service;1"].getService(Ci.calIAlarmService).startup();
     document.getElementById("calsidebar_splitter").addEventListener("command", onCalendarViewResize);
     window.addEventListener("resize", onCalendarViewResize, true);
 
     // Set up the category colors
     categoryManagement.initCategories();
 
+    // Set calendar color CSS on this window
+    cal.view.colorTracker.registerWindow(window);
+
     // Set up window pref observers
     calendarWindowPrefs.init();
 
     // Set up the available modifiers for each platform.
     let keys = document.querySelectorAll("#calendar-keys > key");
     let platform = AppConstants.platform;
     for (let key of keys) {
         if (key.hasAttribute("modifiers-" + platform)) {
--- a/calendar/base/content/calendar-month-view.xml
+++ b/calendar/base/content/calendar-month-view.xml
@@ -14,17 +14,16 @@
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="calendar-month-day-box-item" extends="chrome://calendar/content/calendar-view-core.xml#calendar-editable-item">
     <content mousethrough="never" tooltip="itemTooltip">
       <xul:vbox flex="1">
         <xul:hbox>
           <xul:box anonid="event-container"
                    class="calendar-color-box"
-                   xbl:inherits="calendar-uri,calendar-id"
                    flex="1">
             <xul:box class="calendar-event-selection" orient="horizontal" flex="1">
               <xul:stack anonid="eventbox"
                          class="calendar-event-box-container"
                          xbl:inherits="readonly,flashing,alarm,allday,priority,progress,status,calendar,categories"
                          flex="1">
                 <xul:hbox anonid="event-detail-box"
                           class="calendar-event-details">
@@ -233,22 +232,23 @@
 
       <method name="addItem">
         <parameter name="aItem"/>
         <body><![CDATA[
             if (aItem.hashId in this.mItemHash) {
                 this.deleteItem(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.setAttribute("calendar-uri", aItem.calendar.uri.spec);
-            box.setAttribute("calendar-id", aItem.calendar.id);
+            box.style.setProperty("--item-backcolor", `var(--calendar-${cssSafeId}-backcolor)`);
+            box.style.setProperty("--item-forecolor", `var(--calendar-${cssSafeId}-forecolor)`);
 
             cal.data.binaryInsertNode(this, box, aItem, cal.view.compareItems, false);
 
             box.calendarView = this.calendarView;
             box.item = aItem;
             box.parentBox = this;
             box.occurrence = aItem;
 
--- a/calendar/base/content/calendar-multiday-view.xml
+++ b/calendar/base/content/calendar-multiday-view.xml
@@ -2033,17 +2033,17 @@
      -  An individual event box, to be inserted into a column.
     -->
   <binding id="calendar-event-box" extends="chrome://calendar/content/calendar-view-core.xml#calendar-editable-item">
     <content mousethrough="never" tooltip="itemTooltip">
       <xul:box xbl:inherits="orient,width,height" flex="1">
         <xul:box anonid="event-container"
                  class="calendar-color-box"
                  xbl:inherits="orient,readonly,flashing,alarm,allday,priority,progress,
-                               status,calendar,categories,calendar-uri,calendar-id,todoType"
+                               status,calendar,categories,todoType"
                  flex="1">
           <xul:box class="calendar-event-selection" orient="horizontal" flex="1">
             <xul:stack anonid="eventbox"
                        align="stretch"
                        class="calendar-event-box-container"
                        flex="1"
                        xbl:inherits="context,parentorient=orient,readonly,flashing,alarm,allday,priority,progress,status,calendar,categories">
               <xul:hbox class="calendar-event-details"
--- a/calendar/base/content/calendar-view-core.xml
+++ b/calendar/base/content/calendar-view-core.xml
@@ -16,17 +16,16 @@
   <binding id="calendar-editable-item">
     <content mousethrough="never"
              tooltip="itemTooltip"
              tabindex="-1">
       <xul:vbox flex="1">
         <xul:hbox>
           <xul:box anonid="event-container"
                    class="calendar-color-box"
-                   xbl:inherits="calendar-uri,calendar-id"
                    flex="1">
             <xul:box class="calendar-event-selection" orient="horizontal" flex="1">
               <xul:stack anonid="eventbox"
                          class="calendar-event-box-container"
                          flex="1"
                          xbl:inherits="readonly,flashing,alarm,allday,priority,progress,status,calendar,categories">
                 <xul:hbox class="calendar-event-details">
                   <xul:vbox align="left" flex="1" xbl:inherits="context">
@@ -160,18 +159,19 @@
             locationLabel.value = showLocation && location ? location : "";
             setBooleanAttribute(locationLabel, "hidden", !showLocation || !location);
         ]]></body>
       </method>
 
       <method name="setCSSClasses">
         <body><![CDATA[
             let item = this.mOccurrence;
-            this.setAttribute("calendar-uri", item.calendar.uri.spec);
-            this.setAttribute("calendar-id", item.calendar.id);
+            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 categoriesArray = item.getCategories({});
             if (categoriesArray.length > 0) {
                 let cssClassesArray = categoriesArray.map(cal.view.formatStringForCSSRule);
                 this.setAttribute("categories", cssClassesArray.join(" "));
             }
 
             // Add alarm icons as needed.
             let alarms = item.getAlarms({});
--- a/calendar/base/content/calendar-views-utils.js
+++ b/calendar/base/content/calendar-views-utils.js
@@ -1,14 +1,14 @@
 /* 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/. */
 
 /* exported switchToView, getSelectedDay, scheduleMidnightUpdate,
- *          updateStyleSheetForViews, observeViewDaySelect, toggleOrientation,
+ *          observeViewDaySelect, toggleOrientation,
  *          toggleWorkdaysOnly, toggleTasksInView, toggleShowCompletedInView,
  *          goToDate, getLastCalendarView, deleteSelectedEvents,
  *          editSelectedEvents, selectAllEvents
  */
 
 /* import-globals-from calendar-chrome-startup.js */
 /* import-globals-from calendar-item-editing.js */
 
@@ -389,50 +389,16 @@ function getViewStyleSheet() {
                 break;
             }
         }
     }
     return getViewStyleSheet.sheet;
 }
 
 /**
- * Updates the view stylesheet to contain rules that give all boxes with class
- * .calendar-color-box and an attribute calendar-id="<id of the calendar>" the
- * background color of the specified calendar.
- *
- * @param calendar     The calendar to update the stylesheet for.
- */
-function updateStyleSheetForViews(calendar) {
-    if (!updateStyleSheetForViews.ruleCache) {
-        updateStyleSheetForViews.ruleCache = {};
-    }
-    let ruleCache = updateStyleSheetForViews.ruleCache;
-
-    if (!(calendar.id in ruleCache)) {
-        // We haven't create a rule for this calendar yet, do so now.
-        let sheet = getViewStyleSheet();
-        let ruleString = '.calendar-color-box[calendar-id="' + calendar.id + '"] {} ';
-
-        try {
-            let ruleIndex = sheet.insertRule(ruleString, sheet.cssRules.length);
-            ruleCache[calendar.id] = sheet.cssRules[ruleIndex];
-        } catch (ex) {
-            sheet.ownerNode.addEventListener("load",
-                                             () => updateStyleSheetForViews(calendar),
-                                             { once: true });
-            return;
-        }
-    }
-
-    let color = calendar.getProperty("color") || "#A8C2E1";
-    ruleCache[calendar.id].style.backgroundColor = color;
-    ruleCache[calendar.id].style.color = cal.view.getContrastingTextColor(color);
-}
-
-/**
  * Category preferences observer. Used to update the stylesheets for category
  * colors.
  *
  * Note we need to keep the categoryPrefBranch variable outside of
  * initCategories since branch observers only live as long as the branch object
  * is alive, and out of categoryManagement to avoid cyclic references.
  */
 var categoryPrefBranch;
--- a/calendar/base/content/widgets/calendar-list-tree.js
+++ b/calendar/base/content/widgets/calendar-list-tree.js
@@ -212,20 +212,16 @@
                 onAddItem(item) {},
                 onModifyItem(newItem, oldItem) {},
                 onDeleteItem(deletedItem) {},
                 onError(calendar, errNo, message) {},
 
                 onPropertyChanged(calendar, name, value, oldValue) {
                     switch (name) {
                         case "color":
-                            // TODO See other TODO in this file about updateStyleSheetForViews
-                            if ("updateStyleSheetForViews" in window) {
-                                updateStyleSheetForViews(calendar);
-                            }
                             this.listTree.updateCalendarColor(calendar);
                             // Fall through, update item in any case
                         case "name":
                         case "currentStatus":
                         case "readOnly":
                         case "disabled":
                             this.listTree.updateCalendar(calendar);
                             // Fall through, update commands in any cases.
@@ -466,25 +462,16 @@
 
             if (!composite.defaultCalendar ||
                 calendar.id == composite.defaultCalendar.id) {
                 this.tree.view.selection.select(this.mCalendarList.length - 1);
             }
 
             this.updateCalendarColor(calendar);
 
-            // TODO This should be done only once outside of this custom element, but to
-            // do that right, we need to have an easy way to register an observer for
-            // all calendar properties. This could be the calendar manager that
-            // holds an observer on every calendar anyway, which would then use the
-            // global observer service which clients can register with.
-            if ("updateStyleSheetForViews" in window) {
-                updateStyleSheetForViews(calendar);
-            }
-
             // Watch the calendar for changes, i.e color.
             calendar.addObserver(this.calObserver);
 
             // Adding a calendar causes the sort order to be changed.
             this.sortOrderChanged();
 
             // Re-assign defaultCalendar, sometimes it is not the right one after
             // removing and adding a calendar.
--- a/calendar/base/modules/utils/calViewUtils.jsm
+++ b/calendar/base/modules/utils/calViewUtils.jsm
@@ -282,8 +282,83 @@ var calview = {
         if (cmp != 0) {
             return cmp;
         }
 
         cmp = (a.title > b.title) - (a.title < b.title);
         return cmp;
     }
 };
+
+/**
+ * Adds CSS variables for each calendar to registered windows for coloring
+ * UI elements. Automatically tracks calendar creation, changes, and deletion.
+ */
+calview.colorTracker = {
+    calendars: null,
+    windows: new Set(),
+    QueryInterface: cal.generateQI([
+        Ci.calICalendarManagerObserver,
+        Ci.calIObserver
+    ]),
+
+    // The only public method. Deregistration is not required.
+    registerWindow(aWindow) {
+        if (this.calendars === null) {
+            let manager = cal.getCalendarManager();
+            this.calendars = new Set(manager.getCalendars({}));
+            manager.addObserver(this);
+            manager.addCalendarObserver(this);
+        }
+
+        this.windows.add(aWindow);
+        aWindow.addEventListener("unload", () => this.windows.delete(aWindow));
+        for (let calendar of this.calendars) {
+            this._addToWindow(aWindow, calendar);
+        }
+    },
+    _addToWindow(aWindow, aCalendar) {
+        let cssSafeId = cal.view.formatStringForCSSRule(aCalendar.id);
+        let style = aWindow.document.documentElement.style;
+        let backColor = aCalendar.getProperty("color") || "#a8c2e1";
+        let foreColor = calview.getContrastingTextColor(backColor);
+        style.setProperty(`--calendar-${cssSafeId}-backcolor`, backColor);
+        style.setProperty(`--calendar-${cssSafeId}-forecolor`, foreColor);
+    },
+    _removeFromWindow(aWindow, aCalendar) {
+        let cssSafeId = cal.view.formatStringForCSSRule(aCalendar.id);
+        let style = aWindow.document.documentElement.style;
+        style.removeProperty(`--calendar-${cssSafeId}-backcolor`);
+        style.removeProperty(`--calendar-${cssSafeId}-forecolor`);
+    },
+
+    // calICalendarManagerObserver methods
+    onCalendarRegistered(aCalendar) {
+        this.calendars.add(aCalendar);
+        for (let window of this.windows) {
+            this._addToWindow(window, aCalendar);
+        }
+    },
+    onCalendarUnregistering(aCalendar) {
+        this.calendars.delete(aCalendar);
+        for (let window of this.windows) {
+            this._removeFromWindow(window, aCalendar);
+        }
+    },
+    onCalendarDeleting(aCalendar) {},
+
+    // calIObserver methods
+    onStartBatch() {},
+    onEndBatch() {},
+    onLoad() {},
+    onAddItem(aItem) {},
+    onModifyItem(aNewItem, aOldItem) {},
+    onDeleteItem(aDeletedItem) {},
+    onError(aCalendar, aErrNo, aMessage) {},
+    onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
+        if (aName == "color") {
+            for (let window of this.windows) {
+                this._addToWindow(window, aCalendar);
+            }
+        }
+    },
+    onPropertyDeleting(aCalendar, aName) {},
+};
--- a/calendar/base/themes/common/calendar-views.css
+++ b/calendar/base/themes/common/calendar-views.css
@@ -705,16 +705,18 @@ calendar-month-day-box-item {
 }
 
 .calendar-color-box {
     /* This rule should be adopted if the alarm image size is changed */
     min-height: 13px;
     background-image: linear-gradient(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.01) 50%, rgba(0, 0, 0, 0.05));
     border: 1px solid transparent;
     border-radius: 2px;
+    background-color: var(--item-backcolor);
+    color: var(--item-forecolor);
 }
 
 calendar-month-day-box calendar-month-day-box-item[allday="true"] .calendar-color-box {
     border-color: rgba(0, 0, 0, 0.5);
     box-shadow: inset -1px -1px 0 rgba(255, 255, 255, 0.7), inset 1px 1px 0 rgba(255, 255, 255, 0.7);
 }
 
 .calendar-month-day-box-item-label {
--- a/calendar/providers/gdata/content/gdata-calendar-creation.js
+++ b/calendar/providers/gdata/content/gdata-calendar-creation.js
@@ -233,20 +233,16 @@ var { monkeyPatch } = ChromeUtils.import
 
     document.addEventListener("DOMContentLoaded", () => {
         // Older versions of Lightning don't set the onselect attribute at all.
         let calendarFormat = document.getElementById("calendar-format");
         if (!calendarFormat.hasAttribute("onselect")) {
             calendarFormat.setAttribute("onselect", "gdataSelectProvider(this.value)");
         }
 
-        if (!("updateStyleSheetForViews" in window)) {
-            window.updateStyleSheetForViews = function() {};
-        }
-
         if (document.getElementById("gdata-session").pageIndex == -1) {
             let wizard = document.documentElement;
             wizard._initPages();
         }
     });
 
     let gdataSessionPage = document.getElementById("gdata-session");
     gdataSessionPage.addEventListener("pageshow", () => {