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