summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/widgets/calendar-notifications-setting.js
blob: 1f772992c71252132f8aab47bb28c1e765d75a50 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/* 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/. */

"use strict";

/* globals MozXULElement */

// Wrap in a block to prevent leaking to window scope.
{
  var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
  var { PluralForm } = ChromeUtils.importESModule("resource://gre/modules/PluralForm.sys.mjs");

  /**
   * A calendar-notifications-setting provides controls to config notifications
   * times of a calendar.
   *
   * @augments {MozXULElement}
   */
  class CalendarNotificationsSetting extends MozXULElement {
    connectedCallback() {
      MozXULElement.insertFTLIfNeeded("calendar/calendar-widgets.ftl");
    }

    /**
     * @type {string} A string in the form of "PT5M PT0M" to represent the notifications times.
     */
    get value() {
      return [...this._elList.children]
        .map(row => {
          let count = row.querySelector("input").value;
          let unit = row.querySelector(".unit-menu").value;
          let [relation, tag] = row.querySelector(".relation-menu").value.split("-");

          tag = tag == "END" ? "END:" : "";
          relation = relation == "before" ? "-" : "";
          let durTag = unit == "D" ? "P" : "PT";
          return `${tag}${relation}${durTag}${count}${unit}`;
        })
        .join(",");
    }

    set value(value) {
      // An array of notifications times, each item is in the form of [5, "M",
      // "before-start"], i.e. a triple of time, unit and relation.
      let items = [];
      let durations = value?.split(",") || [];
      for (let dur of durations) {
        dur = dur.trim();
        if (!dur) {
          continue;
        }
        let [relation, value] = dur.split(":");
        if (!value) {
          value = relation;
          relation = "START";
        }
        if (value.startsWith("-")) {
          relation = `before-${relation}`;
          value = value.slice(1);
        } else {
          relation = `after-${relation}`;
        }
        let prefix = value.slice(0, 2);
        if (prefix != "PT") {
          prefix = value[0];
        }
        let unit = value.slice(-1);
        if ((prefix == "P" && unit != "D") || (prefix == "PT" && !["M", "H"].includes(unit))) {
          continue;
        }
        value = value.slice(prefix.length, -1);
        items.push([value, unit, relation]);
      }
      this._render(items);
    }

    /**
     * @type {boolean} If true, all form controls should be disabled.
     */
    set disabled(disabled) {
      this._disabled = disabled;
      this._updateDisabled();
    }

    /**
     * Update the disabled attributes of all form controls to this._disabled.
     */
    _updateDisabled() {
      for (let el of this.querySelectorAll("label, input, button, menulist")) {
        el.disabled = this._disabled;
      }
    }

    /**
     * Because form controls can be dynamically added/removed, we bind events to
     * _elButtonAdd and _elList.
     */
    _bindEvents() {
      this._elButtonAdd.addEventListener("click", e => {
        // Add a notification time row.
        this._addNewRow(0, "M", "before-START");
        this._emit();
      });

      this._elList.addEventListener("change", e => {
        if (!HTMLInputElement.isInstance(e.target)) {
          // We only care about change event of input elements.
          return;
        }
        // We don't want this to interfere with the 'change' event emitted by
        // calendar-notifications-setting itself.
        e.stopPropagation();
        this._updateMenuLists();
        this._emit();
      });

      this._elList.addEventListener("command", e => {
        let el = e.target;
        if (el.tagName == "menuitem") {
          this._emit();
        } else if (el.tagName == "button") {
          // Remove a notification time row.
          el.closest("hbox").remove();
          this._updateAddButton();
          this._emit();
        }
      });
    }

    /**
     * Render the layout and the add button, then bind events. This is delayed
     * until the first `set value` call, so that l10n works correctly.
     */
    _renderLayout() {
      this.appendChild(
        MozXULElement.parseXULToFragment(`
          <hbox align="center">
            <label data-l10n-id="calendar-notifications-label"></label>
            <spacer flex="1"></spacer>
            <button class="add-button"
                    data-l10n-id="calendar-add-notification-button"/>
          </hbox>
          <separator class="thin"/>
          <vbox class="calendar-notifications-list indent"></vbox>
          `)
      );
      this._elList = this.querySelector(".calendar-notifications-list");
      this._elButtonAdd = this.querySelector("button");
      this._bindEvents();
    }

    /**
     * Render this_items to a list of rows.
     *
     * @param {Array<[number, string, string]>} items - An array of count, unit and relation.
     */
    _render(items) {
      this._renderLayout();

      // Render a row for each item in this._items.
      items.forEach(([value, unit, relation]) => {
        this._addNewRow(value, unit, relation);
      });
      if (items.length) {
        this._updateMenuLists();
        this._updateDisabled();
      }
    }

    /**
     * Render a notification entry to a row. Each row contains a time input, a
     * unit menulist, a relation menulist and a remove button.
     */
    _addNewRow(value, unit, relation) {
      let fragment = MozXULElement.parseXULToFragment(`
        <hbox class="calendar-notifications-row" align="center">
          <html:input class="size3" value="${value}" type="number" min="0"/>
          <menulist class="unit-menu" crop="none" value="${unit}">
            <menupopup>
              <menuitem value="M"/>
              <menuitem value="H"/>
              <menuitem value="D"/>
            </menupopup>
          </menulist>
          <menulist class="relation-menu" crop="none" value="${relation}">
            <menupopup class="reminder-relation-origin-menupopup">
              <menuitem data-id="reminderCustomOriginBeginBeforeEvent"
                        value="before-START"/>
              <menuitem data-id="reminderCustomOriginBeginAfterEvent"
                        value="after-START"/>
              <menuitem data-id="reminderCustomOriginEndBeforeEvent"
                        value="before-END"/>
              <menuitem data-id="reminderCustomOriginEndAfterEvent"
                        value="after-END"/>
            </menupopup>
          </menulist>
          <button class="remove-button"></button>
        </hbox>
      `);
      this._elList.appendChild(fragment);
      this._updateMenuLists();
      this._updateAddButton();
    }

    /**
     * To prevent a too crowded UI, hide the add button if already have 5 rows.
     */
    _updateAddButton() {
      if (this._elList.childElementCount >= 5) {
        this._elButtonAdd.hidden = true;
      } else {
        this._elButtonAdd.hidden = false;
      }
    }

    /**
     * Iterate all rows, update the plurality of menulist (unit) to the input
     * value (time).
     */
    _updateMenuLists() {
      for (let row of this._elList.children) {
        let input = row.querySelector("input");
        let menulist = row.querySelector(".unit-menu");
        this._updateMenuList(input.value, menulist);
        for (let menuItem of row.querySelectorAll(".relation-menu menuitem")) {
          menuItem.label = cal.l10n.getString("calendar-alarms", menuItem.dataset.id);
        }
      }
    }

    /**
     * Update the plurality of a menulist (unit) options to the input value (time).
     */
    _updateMenuList(length, menu) {
      let getUnitEntry = unit =>
        ({
          M: "unitMinutes",
          H: "unitHours",
          D: "unitDays",
        }[unit] || "unitMinutes");

      for (let menuItem of menu.getElementsByTagName("menuitem")) {
        menuItem.label = PluralForm.get(length, cal.l10n.getCalString(getUnitEntry(menuItem.value)))
          .replace("#1", "")
          .trim();
      }
    }

    /**
     * Emit a change event.
     */
    _emit() {
      this.dispatchEvent(new CustomEvent("change", { detail: this.value }));
    }
  }

  customElements.define("calendar-notifications-setting", CalendarNotificationsSetting);
}