blob: d4221233337ddc2b5ec2bcf89ef09155a5b507fd [file] [log] [blame]
Nils Diewaldab4d3ca2015-04-17 01:48:43 +00001/*global define*/
2(function (global, undefined) {
3 "use strict";
4
5 var document = global.document,
6 Alertify;
7
8 Alertify = function () {
9
10 var _alertify = {},
11 dialogs = {},
12 isopen = false,
13 keys = { ENTER: 13, ESC: 27, SPACE: 32 },
14 queue = [],
15 $, btnCancel, btnOK, btnReset, btnResetBack, btnFocus, elCallee, elCover, elDialog, elLog, form, input, getTransitionEvent;
16
17 /**
18 * Markup pieces
19 * @type {Object}
20 */
21 dialogs = {
22 buttons : {
23 holder : "<nav class=\"alertify-buttons\">{{buttons}}</nav>",
24 submit : "<button type=\"submit\" class=\"alertify-button alertify-button-ok\" id=\"alertify-ok\">{{ok}}</button>",
25 ok : "<button class=\"alertify-button alertify-button-ok\" id=\"alertify-ok\">{{ok}}</button>",
26 cancel : "<button class=\"alertify-button alertify-button-cancel\" id=\"alertify-cancel\">{{cancel}}</button>"
27 },
28 input : "<div class=\"alertify-text-wrapper\"><input type=\"text\" class=\"alertify-text\" id=\"alertify-text\"></div>",
29 message : "<p class=\"alertify-message\">{{message}}</p>",
30 log : "<article class=\"alertify-log{{class}}\">{{message}}</article>"
31 };
32
33 /**
34 * Return the proper transitionend event
35 * @return {String} Transition type string
36 */
37 getTransitionEvent = function () {
38 var t,
39 type,
40 supported = false,
41 el = document.createElement("fakeelement"),
42 transitions = {
43 "WebkitTransition" : "webkitTransitionEnd",
44 "MozTransition" : "transitionend",
45 "OTransition" : "otransitionend",
46 "transition" : "transitionend"
47 };
48
49 for (t in transitions) {
50 if (el.style[t] !== undefined) {
51 type = transitions[t];
52 supported = true;
53 break;
54 }
55 }
56
57 return {
58 type : type,
59 supported : supported
60 };
61 };
62
63 /**
64 * Shorthand for document.getElementById()
65 *
66 * @param {String} id A specific element ID
67 * @return {Object} HTML element
68 */
69 $ = function (id) {
70 return document.getElementById(id);
71 };
72
73 /**
74 * Alertify private object
75 * @type {Object}
76 */
77 _alertify = {
78
79 /**
80 * Labels object
81 * @type {Object}
82 */
83 labels : {
84 ok : "OK",
85 cancel : "Cancel"
86 },
87
88 /**
89 * Delay number
90 * @type {Number}
91 */
92 delay : 5000,
93
94 /**
95 * Whether buttons are reversed (default is secondary/primary)
96 * @type {Boolean}
97 */
98 buttonReverse : false,
99
100 /**
101 * Which button should be focused by default
102 * @type {String} "ok" (default), "cancel", or "none"
103 */
104 buttonFocus : "ok",
105
106 /**
107 * Set the transition event on load
108 * @type {[type]}
109 */
110 transition : undefined,
111
112 /**
113 * Set the proper button click events
114 *
115 * @param {Function} fn [Optional] Callback function
116 *
117 * @return {undefined}
118 */
119 addListeners : function (fn) {
120 var hasOK = (typeof btnOK !== "undefined"),
121 hasCancel = (typeof btnCancel !== "undefined"),
122 hasInput = (typeof input !== "undefined"),
123 val = "",
124 self = this,
125 ok, cancel, common, key, reset;
126
127 // ok event handler
128 ok = function (event) {
129 if (typeof event.preventDefault !== "undefined") event.preventDefault();
130 common(event);
131 if (typeof input !== "undefined") val = input.value;
132 if (typeof fn === "function") {
133 if (typeof input !== "undefined") {
134 fn(true, val);
135 }
136 else fn(true);
137 }
138 return false;
139 };
140
141 // cancel event handler
142 cancel = function (event) {
143 if (typeof event.preventDefault !== "undefined") event.preventDefault();
144 common(event);
145 if (typeof fn === "function") fn(false);
146 return false;
147 };
148
149 // common event handler (keyup, ok and cancel)
150 common = function (event) {
151 self.hide();
152 self.unbind(document.body, "keyup", key);
153 self.unbind(btnReset, "focus", reset);
154 if (hasOK) self.unbind(btnOK, "click", ok);
155 if (hasCancel) self.unbind(btnCancel, "click", cancel);
156 };
157
158 // keyup handler
159 key = function (event) {
160 var keyCode = event.keyCode;
161 if ((keyCode === keys.SPACE && !hasInput) || (hasInput && keyCode === keys.ENTER)) ok(event);
162 if (keyCode === keys.ESC && hasCancel) cancel(event);
163 };
164
165 // reset focus to first item in the dialog
166 reset = function (event) {
167 if (hasInput) input.focus();
168 else if (!hasCancel || self.buttonReverse) btnOK.focus();
169 else btnCancel.focus();
170 };
171
172 // handle reset focus link
173 // this ensures that the keyboard focus does not
174 // ever leave the dialog box until an action has
175 // been taken
176 this.bind(btnReset, "focus", reset);
177 this.bind(btnResetBack, "focus", reset);
178 // handle OK click
179 if (hasOK) this.bind(btnOK, "click", ok);
180 // handle Cancel click
181 if (hasCancel) this.bind(btnCancel, "click", cancel);
182 // listen for keys, Cancel => ESC
183 this.bind(document.body, "keyup", key);
184 if (!this.transition.supported) {
185 this.setFocus();
186 }
187 },
188
189 /**
190 * Bind events to elements
191 *
192 * @param {Object} el HTML Object
193 * @param {Event} event Event to attach to element
194 * @param {Function} fn Callback function
195 *
196 * @return {undefined}
197 */
198 bind : function (el, event, fn) {
199 if (typeof el.addEventListener === "function") {
200 el.addEventListener(event, fn, false);
201 } else if (el.attachEvent) {
202 el.attachEvent("on" + event, fn);
203 }
204 },
205
206 /**
207 * Use alertify as the global error handler (using window.onerror)
208 *
209 * @return {boolean} success
210 */
211 handleErrors : function () {
212 if (typeof global.onerror !== "undefined") {
213 var self = this;
214 global.onerror = function (msg, url, line) {
215 self.error("[" + msg + " on line " + line + " of " + url + "]", 0);
216 };
217 return true;
218 } else {
219 return false;
220 }
221 },
222
223 /**
224 * Append button HTML strings
225 *
226 * @param {String} secondary The secondary button HTML string
227 * @param {String} primary The primary button HTML string
228 *
229 * @return {String} The appended button HTML strings
230 */
231 appendButtons : function (secondary, primary) {
232 return this.buttonReverse ? primary + secondary : secondary + primary;
233 },
234
235 /**
236 * Build the proper message box
237 *
238 * @param {Object} item Current object in the queue
239 *
240 * @return {String} An HTML string of the message box
241 */
242 build : function (item) {
243 var html = "",
244 type = item.type,
245 message = item.message,
246 css = item.cssClass || "";
247
248 html += "<div class=\"alertify-dialog\">";
249 html += "<a id=\"alertify-resetFocusBack\" class=\"alertify-resetFocus\" href=\"#\">Reset Focus</a>";
250
251 if (_alertify.buttonFocus === "none") html += "<a href=\"#\" id=\"alertify-noneFocus\" class=\"alertify-hidden\"></a>";
252
253 // doens't require an actual form
254 if (type === "prompt") html += "<div id=\"alertify-form\">";
255
256 html += "<article class=\"alertify-inner\">";
257 html += dialogs.message.replace("{{message}}", message);
258
259 if (type === "prompt") html += dialogs.input;
260
261 html += dialogs.buttons.holder;
262 html += "</article>";
263
264 if (type === "prompt") html += "</div>";
265
266 html += "<a id=\"alertify-resetFocus\" class=\"alertify-resetFocus\" href=\"#\">Reset Focus</a>";
267 html += "</div>";
268
269 switch (type) {
270 case "confirm":
271 html = html.replace("{{buttons}}", this.appendButtons(dialogs.buttons.cancel, dialogs.buttons.ok));
272 html = html.replace("{{ok}}", this.labels.ok).replace("{{cancel}}", this.labels.cancel);
273 break;
274 case "prompt":
275 html = html.replace("{{buttons}}", this.appendButtons(dialogs.buttons.cancel, dialogs.buttons.submit));
276 html = html.replace("{{ok}}", this.labels.ok).replace("{{cancel}}", this.labels.cancel);
277 break;
278 case "alert":
279 html = html.replace("{{buttons}}", dialogs.buttons.ok);
280 html = html.replace("{{ok}}", this.labels.ok);
281 break;
282 default:
283 break;
284 }
285
286 elDialog.className = "alertify alertify-" + type + " " + css;
287 elCover.className = "alertify-cover";
288 return html;
289 },
290
291 /**
292 * Close the log messages
293 *
294 * @param {Object} elem HTML Element of log message to close
295 * @param {Number} wait [optional] Time (in ms) to wait before automatically hiding the message, if 0 never hide
296 *
297 * @return {undefined}
298 */
299 close : function (elem, wait) {
300 // Unary Plus: +"2" === 2
301 var timer = (wait && !isNaN(wait)) ? +wait : this.delay,
302 self = this,
303 hideElement, transitionDone;
304
305 // set click event on log messages
306 this.bind(elem, "click", function () {
307 hideElement(elem);
308 });
309 // Hide the dialog box after transition
310 // This ensure it doens't block any element from being clicked
311 transitionDone = function (event) {
312 event.stopPropagation();
313 // unbind event so function only gets called once
314 self.unbind(this, self.transition.type, transitionDone);
315 // remove log message
316 elLog.removeChild(this);
317 if (!elLog.hasChildNodes()) elLog.className += " alertify-logs-hidden";
318 };
319 // this sets the hide class to transition out
320 // or removes the child if css transitions aren't supported
321 hideElement = function (el) {
322 // ensure element exists
323 if (typeof el !== "undefined" && el.parentNode === elLog) {
324 // whether CSS transition exists
325 if (self.transition.supported) {
326 self.bind(el, self.transition.type, transitionDone);
327 el.className += " alertify-log-hide";
328 } else {
329 elLog.removeChild(el);
330 if (!elLog.hasChildNodes()) elLog.className += " alertify-logs-hidden";
331 }
332 }
333 };
334 // never close (until click) if wait is set to 0
335 if (wait === 0) return;
336 // set timeout to auto close the log message
337 setTimeout(function () { hideElement(elem); }, timer);
338 },
339
340 /**
341 * Create a dialog box
342 *
343 * @param {String} message The message passed from the callee
344 * @param {String} type Type of dialog to create
345 * @param {Function} fn [Optional] Callback function
346 * @param {String} placeholder [Optional] Default value for prompt input field
347 * @param {String} cssClass [Optional] Class(es) to append to dialog box
348 *
349 * @return {Object}
350 */
351 dialog : function (message, type, fn, placeholder, cssClass) {
352 // set the current active element
353 // this allows the keyboard focus to be resetted
354 // after the dialog box is closed
355 elCallee = document.activeElement;
356 // check to ensure the alertify dialog element
357 // has been successfully created
358 var check = function () {
359 if ((elLog && elLog.scrollTop !== null) && (elCover && elCover.scrollTop !== null)) return;
360 else check();
361 };
362 // error catching
363 if (typeof message !== "string") throw new Error("message must be a string");
364 if (typeof type !== "string") throw new Error("type must be a string");
365 if (typeof fn !== "undefined" && typeof fn !== "function") throw new Error("fn must be a function");
366 // initialize alertify if it hasn't already been done
367 this.init();
368 check();
369
370 queue.push({ type: type, message: message, callback: fn, placeholder: placeholder, cssClass: cssClass });
371 if (!isopen) this.setup();
372
373 return this;
374 },
375
376 /**
377 * Extend the log method to create custom methods
378 *
379 * @param {String} type Custom method name
380 *
381 * @return {Function}
382 */
383 extend : function (type) {
384 if (typeof type !== "string") throw new Error("extend method must have exactly one paramter");
385 return function (message, wait) {
386 this.log(message, type, wait);
387 return this;
388 };
389 },
390
391 /**
392 * Hide the dialog and rest to defaults
393 *
394 * @return {undefined}
395 */
396 hide : function () {
397 var transitionDone,
398 self = this;
399 // remove reference from queue
400 queue.splice(0,1);
401 // if items remaining in the queue
402 if (queue.length > 0) this.setup(true);
403 else {
404 isopen = false;
405 // Hide the dialog box after transition
406 // This ensure it doens't block any element from being clicked
407 transitionDone = function (event) {
408 event.stopPropagation();
409 // unbind event so function only gets called once
410 self.unbind(elDialog, self.transition.type, transitionDone);
411 };
412 // whether CSS transition exists
413 if (this.transition.supported) {
414 this.bind(elDialog, this.transition.type, transitionDone);
415 elDialog.className = "alertify alertify-hide alertify-hidden";
416 } else {
417 elDialog.className = "alertify alertify-hide alertify-hidden alertify-isHidden";
418 }
419 elCover.className = "alertify-cover alertify-cover-hidden";
420 // set focus to the last element or body
421 // after the dialog is closed
422 elCallee.focus();
423 }
424 },
425
426 /**
427 * Initialize Alertify
428 * Create the 2 main elements
429 *
430 * @return {undefined}
431 */
432 init : function () {
433 // ensure legacy browsers support html5 tags
434 document.createElement("nav");
435 document.createElement("article");
436 document.createElement("section");
437 // cover
438 if ($("alertify-cover") == null) {
439 elCover = document.createElement("div");
440 elCover.setAttribute("id", "alertify-cover");
441 elCover.className = "alertify-cover alertify-cover-hidden";
442 document.body.appendChild(elCover);
443 }
444 // main element
445 if ($("alertify") == null) {
446 isopen = false;
447 queue = [];
448 elDialog = document.createElement("section");
449 elDialog.setAttribute("id", "alertify");
450 elDialog.className = "alertify alertify-hidden";
451 document.body.appendChild(elDialog);
452 }
453 // log element
454 if ($("alertify-logs") == null) {
455 elLog = document.createElement("section");
456 elLog.setAttribute("id", "alertify-logs");
457 elLog.className = "alertify-logs alertify-logs-hidden";
458 document.body.appendChild(elLog);
459 }
460 // set tabindex attribute on body element
461 // this allows script to give it focus
462 // after the dialog is closed
463 document.body.setAttribute("tabindex", "0");
464 // set transition type
465 this.transition = getTransitionEvent();
466 },
467
468 /**
469 * Show a new log message box
470 *
471 * @param {String} message The message passed from the callee
472 * @param {String} type [Optional] Optional type of log message
473 * @param {Number} wait [Optional] Time (in ms) to wait before auto-hiding the log
474 *
475 * @return {Object}
476 */
477 log : function (message, type, wait) {
478 // check to ensure the alertify dialog element
479 // has been successfully created
480 var check = function () {
481 if (elLog && elLog.scrollTop !== null) return;
482 else check();
483 };
484 // initialize alertify if it hasn't already been done
485 this.init();
486 check();
487
488 elLog.className = "alertify-logs";
489 this.notify(message, type, wait);
490 return this;
491 },
492
493 /**
494 * Add new log message
495 * If a type is passed, a class name "alertify-log-{type}" will get added.
496 * This allows for custom look and feel for various types of notifications.
497 *
498 * @param {String} message The message passed from the callee
499 * @param {String} type [Optional] Type of log message
500 * @param {Number} wait [Optional] Time (in ms) to wait before auto-hiding
501 *
502 * @return {undefined}
503 */
504 notify : function (message, type, wait) {
505 var log = document.createElement("article");
506 log.className = "alertify-log" + ((typeof type === "string" && type !== "") ? " alertify-log-" + type : "");
507 log.innerHTML = message;
508 // append child
509 elLog.appendChild(log);
510 // triggers the CSS animation
511 setTimeout(function() { log.className = log.className + " alertify-log-show"; }, 50);
512 this.close(log, wait);
513 },
514
515 /**
516 * Set properties
517 *
518 * @param {Object} args Passing parameters
519 *
520 * @return {undefined}
521 */
522 set : function (args) {
523 var k;
524 // error catching
525 if (typeof args !== "object" && args instanceof Array) throw new Error("args must be an object");
526 // set parameters
527 for (k in args) {
528 if (args.hasOwnProperty(k)) {
529 this[k] = args[k];
530 }
531 }
532 },
533
534 /**
535 * Common place to set focus to proper element
536 *
537 * @return {undefined}
538 */
539 setFocus : function () {
540 if (input) {
541 input.focus();
542 input.select();
543 }
544 else btnFocus.focus();
545 },
546
547 /**
548 * Initiate all the required pieces for the dialog box
549 *
550 * @return {undefined}
551 */
552 setup : function (fromQueue) {
553 var item = queue[0],
554 self = this,
555 transitionDone;
556
557 // dialog is open
558 isopen = true;
559 // Set button focus after transition
560 transitionDone = function (event) {
561 event.stopPropagation();
562 self.setFocus();
563 // unbind event so function only gets called once
564 self.unbind(elDialog, self.transition.type, transitionDone);
565 };
566 // whether CSS transition exists
567 if (this.transition.supported && !fromQueue) {
568 this.bind(elDialog, this.transition.type, transitionDone);
569 }
570 // build the proper dialog HTML
571 elDialog.innerHTML = this.build(item);
572 // assign all the common elements
573 btnReset = $("alertify-resetFocus");
574 btnResetBack = $("alertify-resetFocusBack");
575 btnOK = $("alertify-ok") || undefined;
576 btnCancel = $("alertify-cancel") || undefined;
577 btnFocus = (_alertify.buttonFocus === "cancel") ? btnCancel : ((_alertify.buttonFocus === "none") ? $("alertify-noneFocus") : btnOK),
578 input = $("alertify-text") || undefined;
579 form = $("alertify-form") || undefined;
580 // add placeholder value to the input field
581 if (typeof item.placeholder === "string" && item.placeholder !== "") input.value = item.placeholder;
582 if (fromQueue) this.setFocus();
583 this.addListeners(item.callback);
584 },
585
586 /**
587 * Unbind events to elements
588 *
589 * @param {Object} el HTML Object
590 * @param {Event} event Event to detach to element
591 * @param {Function} fn Callback function
592 *
593 * @return {undefined}
594 */
595 unbind : function (el, event, fn) {
596 if (typeof el.removeEventListener === "function") {
597 el.removeEventListener(event, fn, false);
598 } else if (el.detachEvent) {
599 el.detachEvent("on" + event, fn);
600 }
601 }
602 };
603
604 return {
605 alert : function (message, fn, cssClass) { _alertify.dialog(message, "alert", fn, "", cssClass); return this; },
606 confirm : function (message, fn, cssClass) { _alertify.dialog(message, "confirm", fn, "", cssClass); return this; },
607 extend : _alertify.extend,
608 init : _alertify.init,
609 log : function (message, type, wait) { _alertify.log(message, type, wait); return this; },
610 prompt : function (message, fn, placeholder, cssClass) { _alertify.dialog(message, "prompt", fn, placeholder, cssClass); return this; },
611 success : function (message, wait) { _alertify.log(message, "success", wait); return this; },
612 error : function (message, wait) { _alertify.log(message, "error", wait); return this; },
613 set : function (args) { _alertify.set(args); },
614 labels : _alertify.labels,
615 debug : _alertify.handleErrors
616 };
617 };
618
619 // AMD and window support
620 if (typeof define === "function") {
621 define([], function () { return new Alertify(); });
622 } else if (typeof global.alertify === "undefined") {
623 global.alertify = new Alertify();
624 }
625
626}(this));