1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  */
 27 
 28 /**
 29  * Creates the status view.
 30  * @class
 31  * This class represents the status view.
 32  * 
 33  * @param    {DwtControl}    parent        the parent
 34  * @param    {String}        className     the class name
 35  * @param    {constant}      posStyle      the position style
 36  * @param    {String}        id            the id
 37  * 
 38  * @extends		DwtControl
 39  */
 40 ZmStatusView = function(parent, className, posStyle, id) {
 41 
 42     DwtControl.call(this, {parent:parent, className:(className || "ZmStatus"), posStyle:posStyle, id:id});
 43 
 44     this._toast = this._standardToast = new ZmToast(this, ZmId.TOAST);
 45     this._statusQueue = [];
 46 };
 47 
 48 ZmStatusView.prototype = new DwtControl;
 49 ZmStatusView.prototype.constructor = ZmStatusView;
 50 
 51 
 52 // Constants
 53 /**
 54  * Defines the "informational" status level.
 55  */
 56 ZmStatusView.LEVEL_INFO             = 1;    // informational
 57 /**
 58  * Defines the "warning" status level.
 59  */
 60 ZmStatusView.LEVEL_WARNING          = 2;    // warning
 61 /**
 62  * Defines the "critical" status level.
 63  */
 64 ZmStatusView.LEVEL_CRITICAL         = 3;    // critical
 65 
 66 ZmStatusView.MSG_PARAMS = ["msg", "level", "detail", "transitions", "toast", "force", "dismissCallback", "finishCallback"];
 67 
 68 // Public methods
 69 
 70 ZmStatusView.prototype.toString =
 71 function() {
 72     return "ZmStatusView";
 73 };
 74 
 75 /**
 76  * Displays a status message.
 77  * 
 78  * @param {String}    msg the message
 79  * @param {constant}    [level]         the level (see {@link ZmStatusView}<code>.LEVEL_</code> constants) 
 80  * @param {String}    [detail]         the details
 81  * @param {String}    [transitions] the transitions (see {@link ZmToast})
 82  * @param {String}    [toast]     the toast control
 83  * @param {boolean}    [force]        force any displayed toasts out of the way
 84  * @param {AjxCallback}    [dismissCallback]    callback to run when the toast is dismissed (by another message using [force], or explicitly calling ZmStatusView.prototype.dismiss())
 85  * @param {AjxCallback}    [finishCallback]     callback to run when the toast finishes its transitions by itself (not when dismissed)
 86  */
 87 ZmStatusView.prototype.setStatusMsg =
 88 function(params) {
 89     params = Dwt.getParams(arguments, ZmStatusView.MSG_PARAMS);
 90     if (typeof params == "string") {
 91         params = { msg: params };
 92     }
 93     var work = {
 94         msg: params.msg,
 95         level: params.level || ZmStatusView.LEVEL_INFO,
 96         detail: params.detail,
 97         date: new Date(),
 98         transitions: params.transitions,
 99         toast: params.toast || this._standardToast,
100         dismissCallback: (params.dismissCallback instanceof AjxCallback) ? params.dismissCallback : null,
101         finishCallback: (params.finishCallback instanceof AjxCallback) ? params.finishCallback : null,
102 		dismissed: false
103     };
104 
105 	if (params.force) { // We want to dismiss ALL messages in the queue and display the new message
106 		for (var i=0; i<this._statusQueue.length; i++) {
107 			this._statusQueue[i].dismissed = true; // Dismiss all messages in the queue in turn, calling their dismissCallbacks along the way
108 		}
109 	}
110     // always push so we know one is active
111     this._statusQueue.push(work);
112     if (!this._toast.isPoppedUp()) {
113         this._updateStatusMsg();
114     } else if (params.force) {
115         this.dismissStatusMsg();
116     }
117 };
118 
119 ZmStatusView.prototype.nextStatus =
120 function() {
121     if (this._statusQueue.length > 0) {
122         this._updateStatusMsg();
123         return true;
124     }
125     return false;
126 };
127 
128 ZmStatusView.prototype.dismissStatusMsg =
129 function(all) {
130 	if (all) {
131 		for (var i=0; i<this._statusQueue.length; i++) {
132 			this._statusQueue[i].dismissed = true; // Dismiss all messages in the queue in turn, calling their dismissCallbacks along the way
133 		}
134 	}
135     this._toast.dismiss();
136 };
137 
138 // Protected methods
139 
140 ZmStatusView.prototype._updateStatusMsg =
141 function() {
142     var work = this._statusQueue.shift();
143     if (!work) { return; }
144 	if (work.dismissed) { // If preemptively dismissed, just run the callback and proceed to the next msg
145 		if (work.dismissCallback)
146 			work.dismissCallback.run();
147 		this.nextStatus();
148 	} else {
149 		this._toast = work.toast;
150 		this._toast.popup(work);
151 	}
152 };
153 
154 
155 //
156 // ZmToast
157 //
158 
159 /**
160  * Creates the "toaster".
161  * @class
162  * This class represents the "toaster".
163  * 
164  * @extends	DwtComposite
165  */
166 ZmToast = function(parent, id) {
167     if (arguments.length == 0) { return; }
168 
169     DwtComposite.call(this, {parent:parent.shell, className:"ZToast", posStyle:Dwt.ABSOLUTE_STYLE, id:id});
170     this._statusView = parent;
171     this._createHtml();
172 
173     this._funcs = {};
174     this._funcs["position"] = AjxCallback.simpleClosure(this.__position, this);
175     this._funcs["show"] = AjxCallback.simpleClosure(this.__show, this);
176     this._funcs["hide"] = AjxCallback.simpleClosure(this.__hide, this);
177     this._funcs["pause"] = AjxCallback.simpleClosure(this.__pause, this);
178     this._funcs["hold"] = AjxCallback.simpleClosure(this.__hold, this);
179     this._funcs["idle"] = AjxCallback.simpleClosure(this.__idle, this);
180     this._funcs["fade"] = AjxCallback.simpleClosure(this.__fade, this);
181     this._funcs["fade-in"] = this._funcs["fade"];
182     this._funcs["fade-out"] = this._funcs["fade"];
183     this._funcs["slide"] = AjxCallback.simpleClosure(this.__slide, this);
184     this._funcs["slide-in"] = this._funcs["slide"];
185     this._funcs["slide-out"] = this._funcs["slide"];
186     this._funcs["next"] = AjxCallback.simpleClosure(this.transition, this);
187 }
188 ZmToast.prototype = new DwtComposite;
189 
190 ZmToast.prototype.constructor = ZmToast;
191 ZmToast.prototype.toString =
192 function() {
193     return "ZmToast";
194 };
195 
196 ZmToast.prototype.role = 'alert';
197 ZmToast.prototype.isFocusable = true;
198 
199 // Constants
200 /**
201  * Defines the "fade" transition.
202  */
203 ZmToast.FADE = { type: "fade" };
204 /**
205  * Defines the "fade-in" transition.
206  */
207 ZmToast.FADE_IN = { type: "fade-in" };
208 /**
209  * Defines the "fade-out" transition.
210  */
211 ZmToast.FADE_OUT = { type: "fade-out" };
212 /**
213  * Defines the "slide" transition.
214  */
215 ZmToast.SLIDE = { type: "slide" };
216 /**
217  * Defines the "slide-in" transition.
218  */
219 ZmToast.SLIDE_IN = { type: "slide-in" };
220 /**
221  * Defines the "slide-out" transition.
222  */
223 ZmToast.SLIDE_OUT = { type: "slide-out" };
224 /**
225  * Defines the "pause" transition.
226  */
227 ZmToast.PAUSE = { type: "pause" };
228 /**
229  * Defines the "hold" transition.
230  */
231 ZmToast.HOLD = { type: "hold" };
232 /**
233  * Defines the "idle" transition.
234  */
235 ZmToast.IDLE = {type: "idle" };
236 /**
237  * Defines the "show" transition.
238  */
239 ZmToast.SHOW = {type: "show" };
240 
241 //ZmToast.DEFAULT_TRANSITIONS = [ ZmToast.FADE_IN, ZmToast.PAUSE, ZmToast.FADE_OUT ];
242 ZmToast.DEFAULT_TRANSITIONS = [ ZmToast.SLIDE_IN, ZmToast.PAUSE, ZmToast.SLIDE_OUT ];
243 
244 ZmToast.DEFAULT_STATE = {};
245 ZmToast.DEFAULT_STATE["position"] = { location: "C" }; // center
246 ZmToast.DEFAULT_STATE["pause"] = { duration: 1200 };
247 ZmToast.DEFAULT_STATE["hold"] = {};
248 ZmToast.DEFAULT_STATE["fade"] = { duration: 100, multiplier: 1 };
249 ZmToast.DEFAULT_STATE["fade-in"] = { start: 0, end: 99, step: 10, duration: 200, multiplier: 1 };
250 ZmToast.DEFAULT_STATE["fade-out"] = { start: 99, end: 0, step: -10, duration: 200, multiplier: 1 };
251 ZmToast.DEFAULT_STATE["slide"] = { duration: 100, multiplier: 1 };
252 ZmToast.DEFAULT_STATE["slide-in"] = { start: -40, end: 0, step: 1, duration: 100, multiplier: 1 };
253 ZmToast.DEFAULT_STATE["slide-out"] = { start: 0, end: -40, step: -1, duration: 100, multiplier: 1 };
254 
255 ZmToast.LEVEL_RE = /\b(ZToastCrit|ZToastWarn|ZToastInfo)\b/g;
256 ZmToast.DISMISSABLE_STATES = [ZmToast.HOLD];
257 
258 // Data
259 
260 ZmToast.prototype.TEMPLATE = "share.Widgets#ZToast";
261 
262 
263 // Public methods
264 
265 ZmToast.prototype.dispose =
266 function() {
267     this._textEl = null;
268     this._iconEl = null;
269     this._detailEl = null;
270     DwtComposite.prototype.dispose.call(this);
271 };
272 
273 ZmToast.prototype.popup =
274 function(work) {
275     this.__clear();
276     this._poppedUp = true;
277     this._dismissed = false;
278     this._dismissCallback = work.dismissCallback;
279     this._finishCallback = work.finishCallback;
280 
281     var icon, className, label;
282 
283     switch (work.level) {
284     case ZmStatusView.LEVEL_CRITICAL:
285         className = "ZToastCrit";
286         icon = "Critical";
287         label = AjxMsg.criticalMsg;
288         break;
289 
290     case ZmStatusView.LEVEL_WARNING:
291         className = "ZToastWarn";
292         icon = "Warning";
293         label = AjxMsg.warningMsg;
294         break;
295 
296     case ZmStatusView.LEVEL_INFO:
297     default:
298         className = "ZToastInfo";
299         icon = "Success";
300         label = AjxMsg.infoMsg;
301         break;
302     }
303 
304     // setup display
305     var el = this.getHtmlElement();
306     Dwt.delClass(el, ZmToast.LEVEL_RE, className);
307 
308     if (this._iconEl) {
309         this._iconEl.innerHTML = AjxImg.getImageHtml({
310             imageName: icon, altText: label
311         });
312     }
313 
314     if (this._textEl) {
315         // we use and add a dedicated SPAN to make sure that we trigger all
316         // screen readers
317         var span = document.createElement('SPAN');
318         span.innerHTML = work.msg || "";
319 
320         Dwt.removeChildren(this._textEl);
321         this._textEl.appendChild(span);
322     }
323 
324     // get transitions
325     var location = appCtxt.getSkinHint("toast", "location");
326     var transitions =
327         (work.transitions || appCtxt.getSkinHint("toast", "transitions") ||
328          ZmToast.DEFAULT_TRANSITIONS);
329 
330     transitions = [].concat( {type:"position", location:location}, transitions, {type:"hide"} );
331 
332     // start animation
333     this._transitions = transitions;
334     this.transition();
335 };
336 
337 ZmToast.prototype.popdown =
338 function() {
339     this.__clear();
340     this.setLocation(Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE);
341     this._poppedUp = false;
342 
343     if (!this._dismissed) {
344         if (this._finishCallback)
345             this._finishCallback.run();
346     }
347 
348     this._dismissed = false;
349 };
350 
351 ZmToast.prototype.isPoppedUp =
352 function() {
353     return this._poppedUp;
354 };
355 
356 ZmToast.prototype.transition =
357 function() {
358 
359     if (this._pauseTimer) {
360         clearTimeout(this._pauseTimer);
361         this._pauseTimer = null;
362     }
363     if (this._held) {
364         this._held = false;
365     }
366 
367     var transition = this._transitions && this._transitions.shift();
368     if (!transition) {
369         this._poppedUp = false;
370         if (!this._statusView.nextStatus()) {
371             this.popdown();
372         }
373         return;
374     }
375 
376     var state = this._state = this._createState(transition);
377 
378     this.setLocation(state.x, state.y);
379 
380     this._funcs[transition.type || "next"]();
381 };
382 
383 // Protected methods
384 
385 ZmToast.prototype._createHtml =
386 function(templateId) {
387     var data = { id: this._htmlElId };
388     this._createHtmlFromTemplate(templateId || this.TEMPLATE, data);
389     this.setZIndex(Dwt.Z_TOAST);
390     var el = this.getHtmlElement();
391 
392     el.setAttribute('aria-live', 'assertive');
393     el.setAttribute('aria-relevant', 'additions');
394     el.setAttribute('aria-atomic', true);
395 };
396 
397 ZmToast.prototype._createHtmlFromTemplate =
398 function(templateId, data) {
399     DwtComposite.prototype._createHtmlFromTemplate.call(this, templateId, data);
400     this._textEl = document.getElementById(data.id+"_text");
401     this._iconEl = document.getElementById(data.id+"_icon");
402     this._detailEl = document.getElementById(data.id+"_detail");
403 };
404 
405 ZmToast.prototype._createState =
406 function(transition) {
407     var state = AjxUtil.createProxy(transition);
408     var defaults = ZmToast.DEFAULT_STATE[state.type];
409     for (var name in defaults) {
410         if (!state[name]) {
411             state[name] = defaults[name];
412         }
413     }
414 
415     switch (state.type) {
416         case "fade-in":
417             this.setOpacity(0);
418             this.setLocation(null, 0);
419             state.value = state.start;
420             break;
421         case "fade-out":
422         case "fade":
423             this.setLocation(null, 0);
424             state.value = state.start;
425             break;
426         case "slide-in":
427         case "slide-out":
428         case "slide":{
429             this.setLocation(null, -36);
430             this.setOpacity(100);
431             state.value = state.start;
432             break;
433         }
434     }
435     return state;
436 };
437 
438 // Private methods
439 
440 ZmToast.prototype.__clear =
441 function() {
442     clearTimeout(this._actionId);
443     clearInterval(this._actionId);
444     this._actionId = -1;
445 };
446 
447 // transition handlers
448 
449 ZmToast.prototype.__position =
450 function() {
451     var location = this._state.location || "C";
452     var containerId = "skin_container_toast"; // Skins may specify an optional element with this id. Toasts will then be placed relative to this element, rather than to the the zshell
453 
454     var container = Dwt.byId(containerId) || this.shell.getHtmlElement();
455     
456     var bsize = Dwt.getSize(container);
457     var tsize = this.getSize();
458 
459     var x = (bsize.x - tsize.x) / 2;
460     var y = (bsize.y - tsize.y) / 2;
461 
462     switch (location.toUpperCase()) {
463         case 'N': y = 0-tsize.y; break;
464         case 'S': y = bsize.y - tsize.y; break;
465         case 'E': x = bsize.x - tsize.x; break;
466         case 'W': x = 0; break;
467         case 'NE': x = bsize.x - tsize.x; y = 0; break;
468         case 'NW': x = 0; y = 0; break;
469         case 'SE': x = bsize.x - tsize.x; y = bsize.y - tsize.y; break;
470         case 'SW': x = 0; y = bsize.y - tsize.y; break;
471         case 'C': default: /* nothing to do */ break;
472     }
473 
474     var offset = Dwt.toWindow(container);
475     x += offset.x;
476     y += offset.y;
477     this.setLocation(x, y);
478 
479     this._funcs["next"]();
480 };
481 
482 ZmToast.prototype.__show =
483 function() {
484     this.setVisible(true);
485     this.setVisibility(true);
486     this._funcs["next"]();
487 };
488 
489 ZmToast.prototype.__hide =
490 function() {
491     this.setLocation(Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE);
492     if (this._textEl) {
493 		Dwt.removeChildren(this._textEl);
494     }
495     if (this._iconEl) {
496 		Dwt.removeChildren(this._iconEl);
497     }
498     this._funcs["next"]();
499 };
500 
501 ZmToast.prototype.__pause =
502 function() {
503     if (this._dismissed && ZmToast.__mayDismiss(ZmToast.PAUSE)) {
504         this._funcs["next"]();
505     } else {
506         this._pauseTimer = setTimeout(this._funcs["next"], this._state.duration);
507     }
508 };
509 
510 
511 /**
512  * Hold the toast in place until dismiss() is called. If dismiss() was already called before this function (ie. during fade/slide in), continue immediately
513  */
514 ZmToast.prototype.__hold =
515 function() {
516     if (this._dismissed && ZmToast.__mayDismiss(ZmToast.HOLD)!=-1) {
517         this._funcs["next"]();
518     } else {
519         this._held = true;
520     }
521 };
522 
523 ZmToast.__mayDismiss =
524 function(state) {
525     return AjxUtil.indexOf(ZmToast.DISMISSABLE_STATES, state)!=-1;
526 };
527 
528 /**
529  * Dismiss (continue) a held or paused toast (Given that ZmToast.DISMISSABLE_STATES agrees). If not yet held or paused, those states will be skipped when they occur
530  */
531 ZmToast.prototype.dismiss =
532 function() {
533     if (!this._dismissed && this._poppedUp) {
534         var doDismiss = (this._pauseTimer && ZmToast.__mayDismiss(ZmToast.PAUSE)) || 
535             (this._held && ZmToast.__mayDismiss(ZmToast.HOLD));
536         if (doDismiss) {
537             this._funcs["next"]();
538         }
539         this._dismissed = true;
540         if (this._dismissCallback instanceof AjxCallback) {
541             this._dismissCallback.run();
542         }
543     }
544 };
545 
546 ZmToast.prototype.__idle =
547 function() {
548     if (!this._idleTimer) {
549         this._idleTimer = new DwtIdleTimer(0, new AjxCallback(this, this.__idleCallback));
550     } else {
551         this._idleTimer.resurrect(0);
552     }
553 };
554 
555 ZmToast.prototype.__idleCallback =
556 function(idle) {
557     if (!idle) {
558         this.transition();
559         this._idleTimer.kill();
560     }
561 };
562 
563 ZmToast.prototype.__move =
564 function() {
565     // TODO
566     this._funcs["next"]();
567 };
568 
569 ZmToast.prototype.__fade =
570 function() {
571     var opacity = this._state.value;
572     var step = this._state.step;
573 
574     // NOTE: IE8 and earlier are slow re-rendering when adjusting
575     //       opacity. So we try to do it using filters.
576     if (AjxEnv.isIE && !AjxEnv.isIE9up) {
577         try {
578             var el = this.getHtmlElement();
579             el.style.visibility = step > 0 ? "hidden" : "visible";
580 
581             var duration = this._state.duration / 1000;
582             el.style.filter = "progid:DXImageTransform.Microsoft.Fade(duration="+duration+",overlap=1.0)";
583 
584             el.filters[0].Apply();
585             el.style.visibility = step > 0 ? "visible" : "hidden";
586             el.filters[0].Play();
587         }
588         catch (e) {
589             DBG.println("error: "+e);
590         }
591         setTimeout(this._funcs["next"], 0);
592         return;
593     }
594 
595     var isOver = step > 0 ? opacity >= this._state.end : opacity <= this._state.end;
596     if (isOver) {
597         opacity = this._state.end;
598     }
599 
600     this.setOpacity(opacity);
601 
602     if (isOver) {
603         this.__clear();
604         setTimeout(this._funcs["next"], 0);
605         return;
606     }
607 
608     if (this._actionId == -1) {
609         var duration = this._state.duration;
610         var delta = duration / Math.abs(step);
611         this._actionId = setInterval(this._funcs["fade"], delta);
612     }
613 
614     this._state.value += step;
615     this._state.step *= this._state.multiplier;
616 };
617 
618 ZmToast.prototype.__slide =
619 function() {
620     var top = this._state.value;
621     var step = this._state.step;
622 
623     var isOver = step > 0 ? top >= this._state.end : top <= this._state.end;
624     if (isOver) {
625         top = this._state.end;
626     }
627 
628     //this.setOpacity(opacity);
629     this.setLocation(null, top);
630     //el.style.top = top+'px';
631 
632 
633     if (isOver) {
634         this.__clear();
635         setTimeout(this._funcs["next"], 0);
636         return;
637     }
638 
639     if (this._actionId == -1) {
640         var duration = this._state.duration;
641         var delta = duration / Math.abs(step);
642         this._actionId = setInterval(this._funcs["slide"], delta);
643     }
644 
645     this._state.value += step;
646     this._state.step *= this._state.multiplier;
647 };
648