summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/widgets/mouseoverPreviews.js
blob: 38e5c1e24f17be730bef51770df95d1378143d80 (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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
/* 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/. */

/**
 * Code which generates event and task (todo) preview tooltips/titletips
 *  when the mouse hovers over either the event list, the task list, or
 *  an event or task box in one of the grid views.
 *
 *   (Portions of this code were previously in calendar.js and unifinder.js,
 *   some of it duplicated.)
 */

/* exported onMouseOverItem, showToolTip, getPreviewForItem,
             getEventStatusString, getToDoStatusString */

/* import-globals-from ../calendar-ui-utils.js */

/**
 * PUBLIC: This changes the mouseover preview based on the start and end dates
 * of an occurrence of a (one-time or recurring) calEvent or calToDo.
 * Used by all grid views.
 */

var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");

/**
 * PUBLIC: Displays a tooltip with details when hovering over an item in the views
 *
 * @param   {DOMEvent} occurrenceBoxMouseEvent  the triggering event
 * @returns {boolean} true, if the tooltip is displayed
 */
function onMouseOverItem(occurrenceBoxMouseEvent) {
  if ("occurrence" in occurrenceBoxMouseEvent.currentTarget) {
    // occurrence of repeating event or todo
    let occurrence = occurrenceBoxMouseEvent.currentTarget.occurrence;
    const toolTip = document.getElementById("itemTooltip");
    return showToolTip(toolTip, occurrence);
  }
  return false;
}

/**
 * PUBLIC: Displays a tooltip for a given item
 *
 * @param  {Node}               aTooltip  the node to hold the tooltip
 * @param  {CalIEvent|calIToDo} aItem     the item to create the tooltip for
 * @returns {boolean} true, if the tooltip is displayed
 */
function showToolTip(aToolTip, aItem) {
  if (aItem) {
    let holderBox = getPreviewForItem(aItem);
    if (holderBox) {
      while (aToolTip.lastChild) {
        aToolTip.lastChild.remove();
      }
      aToolTip.appendChild(holderBox);
      return true;
    }
  }
  return false;
}

/**
 * PUBLIC:  Called when a user hovers over a todo element and the text for the
 * mouse over is changed.
 *
 * @param {calIToDo} toDoItem - the item to create the preview for
 * @param {boolean}  aIsTooltip  enabled if used for tooltip composition (default)
 */
function getPreviewForItem(aItem, aIsTooltip = true) {
  if (aItem.isEvent()) {
    return getPreviewForEvent(aItem, aIsTooltip);
  } else if (aItem.isTodo()) {
    return getPreviewForTask(aItem, aIsTooltip);
  }
  return null;
}

/**
 * PUBLIC: Returns the string for status (none), Tentative, Confirmed, or
 * Cancelled for a given event
 *
 * @param   {calIEvent} aEvent The event
 * @returns {string} The string for the status property of the event
 */
function getEventStatusString(aEvent) {
  switch (aEvent.status) {
    // Event status value keywords are specified in RFC2445sec4.8.1.11
    case "TENTATIVE":
      return cal.l10n.getCalString("statusTentative");
    case "CONFIRMED":
      return cal.l10n.getCalString("statusConfirmed");
    case "CANCELLED":
      return cal.l10n.getCalString("eventStatusCancelled");
    default:
      return "";
  }
}

/**
 * PUBLIC: Returns the string for status (none), NeedsAction, InProcess,
 * Cancelled, orCompleted for a given ToDo
 *
 * @param   {calIToDo} aToDo   The ToDo
 * @returns {string} The string for the status property of the event
 */
function getToDoStatusString(aToDo) {
  switch (aToDo.status) {
    // Todo status keywords are specified in RFC2445sec4.8.1.11
    case "NEEDS-ACTION":
      return cal.l10n.getCalString("statusNeedsAction");
    case "IN-PROCESS":
      return cal.l10n.getCalString("statusInProcess");
    case "CANCELLED":
      return cal.l10n.getCalString("todoStatusCancelled");
    case "COMPLETED":
      return cal.l10n.getCalString("statusCompleted");
    default:
      return "";
  }
}

/**
 * PRIVATE: Called when a user hovers over a todo element and the text for the
 * mouse overis changed.
 *
 * @param {calIToDo} toDoItem - the item to create the preview for
 * @param {boolean}  aIsTooltip  enabled if used for tooltip composition (default)
 */
function getPreviewForTask(toDoItem, aIsTooltip = true) {
  if (toDoItem) {
    const vbox = document.createXULElement("vbox");
    vbox.setAttribute("class", "tooltipBox");
    if (aIsTooltip) {
      // tooltip appears above or below pointer, so may have as little as
      // one half the screen height available (avoid top going off screen).
      vbox.style.maxHeight = Math.floor(screen.height / 2);
    } else {
      vbox.setAttribute("flex", "1");
    }
    boxInitializeHeaderTable(vbox);

    let hasHeader = false;

    if (toDoItem.title) {
      boxAppendLabeledText(vbox, "tooltipTitle", toDoItem.title);
      hasHeader = true;
    }

    let location = toDoItem.getProperty("LOCATION");
    if (location) {
      boxAppendLabeledText(vbox, "tooltipLocation", location);
      hasHeader = true;
    }

    // First try to get calendar name appearing in tooltip
    if (toDoItem.calendar.name) {
      let calendarNameString = toDoItem.calendar.name;
      boxAppendLabeledText(vbox, "tooltipCalName", calendarNameString);
    }

    if (toDoItem.entryDate && toDoItem.entryDate.isValid) {
      boxAppendLabeledDateTime(vbox, "tooltipStart", toDoItem.entryDate);
      hasHeader = true;
    }

    if (toDoItem.dueDate && toDoItem.dueDate.isValid) {
      boxAppendLabeledDateTime(vbox, "tooltipDue", toDoItem.dueDate);
      hasHeader = true;
    }

    if (toDoItem.priority && toDoItem.priority != 0) {
      let priorityInteger = parseInt(toDoItem.priority, 10);
      let priorityString;

      // These cut-offs should match calendar-event-dialog.js
      if (priorityInteger >= 1 && priorityInteger <= 4) {
        priorityString = cal.l10n.getCalString("highPriority");
      } else if (priorityInteger == 5) {
        priorityString = cal.l10n.getCalString("normalPriority");
      } else {
        priorityString = cal.l10n.getCalString("lowPriority");
      }
      boxAppendLabeledText(vbox, "tooltipPriority", priorityString);
      hasHeader = true;
    }

    if (toDoItem.status && toDoItem.status != "NONE") {
      let status = getToDoStatusString(toDoItem);
      boxAppendLabeledText(vbox, "tooltipStatus", status);
      hasHeader = true;
    }

    if (
      toDoItem.status != null &&
      toDoItem.percentComplete != 0 &&
      toDoItem.percentComplete != 100
    ) {
      boxAppendLabeledText(vbox, "tooltipPercent", String(toDoItem.percentComplete) + "%");
      hasHeader = true;
    } else if (toDoItem.percentComplete == 100) {
      if (toDoItem.completedDate == null) {
        boxAppendLabeledText(vbox, "tooltipPercent", "100%");
      } else {
        boxAppendLabeledDateTime(vbox, "tooltipCompleted", toDoItem.completedDate);
      }
      hasHeader = true;
    }

    let description = toDoItem.descriptionText;
    if (description) {
      // display wrapped description lines like body of message below headers
      if (hasHeader) {
        boxAppendBodySeparator(vbox);
      }
      boxAppendBody(vbox, description, aIsTooltip);
    }

    return vbox;
  }
  return null;
}

/**
 * PRIVATE: Called when mouse moves over a different, or  when mouse moves over
 * event in event list. The instStartDate is date of instance displayed at event
 * box (recurring or multiday events may be displayed by more than one event box
 * for different days), or null if should compute next instance from now.
 *
 * @param {calIEvent} aEvent - the item to create the preview for
 * @param {boolean}   aIsTooltip   enabled if used for tooltip composition (default)
 */
function getPreviewForEvent(aEvent, aIsTooltip = true) {
  let event = aEvent;
  const vbox = document.createXULElement("vbox");
  vbox.setAttribute("class", "tooltipBox");
  if (aIsTooltip) {
    // tooltip appears above or below pointer, so may have as little as
    // one half the screen height available (avoid top going off screen).
    vbox.maxHeight = Math.floor(screen.height / 2);
  } else {
    vbox.setAttribute("flex", "1");
  }
  boxInitializeHeaderTable(vbox);

  if (event) {
    if (event.title) {
      boxAppendLabeledText(vbox, "tooltipTitle", aEvent.title);
    }

    let location = event.getProperty("LOCATION");
    if (location) {
      boxAppendLabeledText(vbox, "tooltipLocation", location);
    }
    if (!(event.startDate && event.endDate)) {
      // Event may be recurrent event.   If no displayed instance specified,
      // use next instance, or previous instance if no next instance.
      event = getCurrentNextOrPreviousRecurrence(event);
    }
    boxAppendLabeledDateTimeInterval(vbox, "tooltipDate", event);

    // First try to get calendar name appearing in tooltip
    if (event.calendar.name) {
      let calendarNameString = event.calendar.name;
      boxAppendLabeledText(vbox, "tooltipCalName", calendarNameString);
    }

    if (event.status && event.status != "NONE") {
      let statusString = getEventStatusString(event);
      boxAppendLabeledText(vbox, "tooltipStatus", statusString);
    }

    if (event.organizer && event.getAttendees().length > 0) {
      let organizer = event.organizer;
      boxAppendLabeledText(vbox, "tooltipOrganizer", organizer);
    }

    let description = event.descriptionText;
    if (description) {
      boxAppendBodySeparator(vbox);
      // display wrapped description lines, like body of message below headers
      boxAppendBody(vbox, description, aIsTooltip);
    }
    return vbox;
  }
  return null;
}

/**
 * PRIVATE: Append a separator, a thin space between header and body.
 *
 * @param {Node}  vbox  box to which to append separator.
 */
function boxAppendBodySeparator(vbox) {
  const separator = document.createXULElement("separator");
  separator.setAttribute("class", "tooltipBodySeparator");
  vbox.appendChild(separator);
}

/**
 * PRIVATE: Append description to box for body text. Rendered as HTML.
 * Indentation and line breaks are preserved.
 *
 * @param {Node} box - Box to which to append the body.
 * @param {string} textString - Text of the body.
 * @param {boolean} aIsTooltip - True for "tooltip" and false for "conflict-dialog" case.
 */
function boxAppendBody(box, textString, aIsTooltip) {
  let type = aIsTooltip ? "description" : "vbox";
  let xulDescription = document.createXULElement(type);
  xulDescription.setAttribute("class", "tooltipBody");
  if (!aIsTooltip) {
    xulDescription.setAttribute("flex", "1");
  }
  let docFragment = cal.view.textToHtmlDocumentFragment(textString, document);
  xulDescription.appendChild(docFragment);
  box.appendChild(xulDescription);
}

/**
 * PRIVATE: Use dateFormatter to format date and time,
 * and to header table append a row containing localized Label: date.
 *
 * @param {Node}         box            The node to add the date label to
 * @param {string}       labelProperty  The label
 * @param {calIDateTime} date - The datetime object to format and add
 */
function boxAppendLabeledDateTime(box, labelProperty, date) {
  date = date.getInTimezone(cal.dtz.defaultTimezone);
  let formattedDateTime = cal.dtz.formatter.formatDateTime(date);
  boxAppendLabeledText(box, labelProperty, formattedDateTime);
}

/**
 * PRIVATE: Use dateFormatter to format date and time interval,
 * and to header table append a row containing localized Label: interval.
 *
 * @param box               contains header table.
 * @param labelProperty     name of property for localized field label.
 * @param item              the event or task
 */
function boxAppendLabeledDateTimeInterval(box, labelProperty, item) {
  let dateString = cal.dtz.formatter.formatItemInterval(item);
  boxAppendLabeledText(box, labelProperty, dateString);
}

/**
 * PRIVATE: create empty 2-column table for header fields, and append it to box.
 *
 * @param  {Node}  box  The node to create a column table for
 */
function boxInitializeHeaderTable(box) {
  let table = document.createElementNS("http://www.w3.org/1999/xhtml", "table");
  table.setAttribute("class", "tooltipHeaderTable");
  box.appendChild(table);
}

/**
 * PRIVATE: To headers table, append a row containing Label: value, where label
 * is localized text for labelProperty.
 *
 * @param box               box containing headers table
 * @param labelProperty     name of property for localized name of header
 * @param textString        value of header field.
 */
function boxAppendLabeledText(box, labelProperty, textString) {
  let labelText = cal.l10n.getCalString(labelProperty);
  let table = box.querySelector("table");
  let row = document.createElementNS("http://www.w3.org/1999/xhtml", "tr");

  row.appendChild(createTooltipHeaderLabel(labelText));
  row.appendChild(createTooltipHeaderDescription(textString));

  table.appendChild(row);
}

/**
 * PRIVATE: Creates an element for field label (for header table)
 *
 * @param   {string} text  The text to display in the node
 * @returns {Node} The node
 */
function createTooltipHeaderLabel(text) {
  let labelCell = document.createElementNS("http://www.w3.org/1999/xhtml", "th");
  labelCell.setAttribute("class", "tooltipHeaderLabel");
  labelCell.textContent = text;
  return labelCell;
}

/**
 * PRIVATE: Creates an element for field value (for header table)
 *
 * @param   {string} text  The text to display in the node
 * @returns {Node} The node
 */
function createTooltipHeaderDescription(text) {
  let descriptionCell = document.createElementNS("http://www.w3.org/1999/xhtml", "td");
  descriptionCell.setAttribute("class", "tooltipHeaderDescription");
  descriptionCell.textContent = text;
  return descriptionCell;
}

/**
 * PRIVATE: If now is during an occurrence, return the occurrence. If now is
 * before an occurrence, return the next occurrence or otherwise the previous
 * occurrence.
 *
 * @param   {calIEvent}  calendarEvent   The text to display in the node
 * @returns {mixed} Returns a calIDateTime for the detected
 *                                        occurrence or calIEvent, if this is a
 *                                        non-recurring event
 */
function getCurrentNextOrPreviousRecurrence(calendarEvent) {
  if (!calendarEvent.recurrenceInfo) {
    return calendarEvent;
  }

  let dur = calendarEvent.duration.clone();
  dur.isNegative = true;

  // To find current event when now is during event, look for occurrence
  // starting duration ago.
  let probeTime = cal.dtz.now();
  probeTime.addDuration(dur);

  let occ = calendarEvent.recurrenceInfo.getNextOccurrence(probeTime);

  if (!occ) {
    let occs = calendarEvent.recurrenceInfo.getOccurrences(
      calendarEvent.startDate,
      probeTime,
      0,
      {}
    );
    occ = occs[occs.length - 1];
  }
  return occ;
}