Created
November 7, 2014 17:23
-
-
Save bradberger/bbb25a7d4d3d925b89f4 to your computer and use it in GitHub Desktop.
Alertify JS update with fixes buggy transition support
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (global, undefined) { | |
"use strict"; | |
var document = global.document, | |
Alertify; | |
function getStyleRuleValue(style, selector, sheet) { | |
var sheets = typeof sheet !== "undefined" ? [sheet] : document.styleSheets; | |
for (var i = 0, l = sheets.length; i < l; i++) { | |
sheet = sheets[i]; | |
if (!sheet.cssRules) { | |
continue; | |
} | |
for (var j = 0, k = sheet.cssRules.length; j < k; j++) { | |
var rule = sheet.cssRules[j]; | |
if (rule.selectorText && rule.selectorText.split(",").indexOf(selector) !== -1) { | |
return rule.style[style]; | |
} | |
} | |
} | |
return null; | |
} | |
Alertify = function () { | |
var _alertify = {}, | |
dialogs = {}, | |
isopen = false, | |
keys = {ENTER: 13, ESC: 27, SPACE: 32}, | |
queue = [], | |
$, btnCancel, btnOK, btnReset, btnResetBack, btnFocus, elCallee, elCover, elDialog, elLog, form, input, getTransitionEvent; | |
/** | |
* Markup pieces | |
* @type {Object} | |
*/ | |
dialogs = { | |
buttons: { | |
holder: "<nav class=\"alertify-buttons\">{{buttons}}</nav>", | |
submit: "<button type=\"submit\" class=\"alertify-button alertify-button-ok\" id=\"alertify-ok\">{{ok}}</button>", | |
ok: "<button class=\"alertify-button alertify-button-ok\" id=\"alertify-ok\">{{ok}}</button>", | |
cancel: "<button class=\"alertify-button alertify-button-cancel\" id=\"alertify-cancel\">{{cancel}}</button>" | |
}, | |
input: "<div class=\"alertify-text-wrapper\"><input type=\"text\" class=\"alertify-text\" id=\"alertify-text\"></div>", | |
message: "<p class=\"alertify-message\">{{message}}</p>", | |
log: "<article class=\"alertify-log{{class}}\">{{message}}</article>" | |
}; | |
/** | |
* Return the proper transitionend event | |
* @return {String} Transition type string | |
*/ | |
getTransitionEvent = function () { | |
var t, | |
type, | |
supported = false, | |
el = document.createElement("fakeforelement"), | |
transitions = { | |
"WebkitTransition": "webkitTransitionEnd", | |
"MozTransition": "transitionend", | |
"OTransition": "otransitionend", | |
"transition": "transitionend" | |
}; | |
for (t in transitions) { | |
if (el.style[t] !== undefined) { | |
type = transitions[t]; | |
supported = true; | |
} | |
} | |
return { | |
type: type, | |
supported: supported | |
}; | |
}; | |
/** | |
* Shorthand for document.getElementById() | |
* | |
* @param {String} id A specific element ID | |
* @return {Object} HTML element | |
*/ | |
$ = function (id) { | |
return document.getElementById(id); | |
}; | |
/** | |
* Alertify private object | |
* @type {Object} | |
*/ | |
_alertify = { | |
/** | |
* Labels object | |
* @type {Object} | |
*/ | |
labels: { | |
ok: "OK", | |
cancel: "Cancel" | |
}, | |
/** | |
* Delay number | |
* @type {Number} | |
*/ | |
delay: 5000, | |
/** | |
* Whether buttons are reversed (default is secondary/primary) | |
* @type {Boolean} | |
*/ | |
buttonReverse: false, | |
/** | |
* Which button should be focused by default | |
* @type {String} "ok" (default), "cancel", or "none" | |
*/ | |
buttonFocus: "ok", | |
/** | |
* Set the transition event on load | |
* @type {[type]} | |
*/ | |
transition: undefined, | |
/** | |
* Set the proper button click events | |
* | |
* @param {Function} fn [Optional] Callback function | |
* | |
* @return {undefined} | |
*/ | |
addListeners: function (fn) { | |
var hasOK = (typeof btnOK !== "undefined"), | |
hasCancel = (typeof btnCancel !== "undefined"), | |
hasInput = (typeof input !== "undefined"), | |
val = "", | |
self = this, | |
ok, cancel, common, key, reset; | |
// ok event handler | |
ok = function (event) { | |
if (typeof event.preventDefault !== "undefined") event.preventDefault(); | |
common(event); | |
if (typeof input !== "undefined") val = input.value; | |
if (typeof fn === "function") { | |
if (typeof input !== "undefined") { | |
fn(true, val); | |
} | |
else fn(true); | |
} | |
return false; | |
}; | |
// cancel event handler | |
cancel = function (event) { | |
if (typeof event.preventDefault !== "undefined") event.preventDefault(); | |
common(event); | |
if (typeof fn === "function") fn(false); | |
return false; | |
}; | |
// common event handler (keyup, ok and cancel) | |
common = function (event) { | |
self.hide(); | |
self.unbind(document.body, "keyup", key); | |
self.unbind(btnReset, "focus", reset); | |
if (hasOK) self.unbind(btnOK, "click", ok); | |
if (hasCancel) self.unbind(btnCancel, "click", cancel); | |
}; | |
// keyup handler | |
key = function (event) { | |
var keyCode = event.keyCode; | |
if ((keyCode === keys.SPACE && !hasInput) || (hasInput && keyCode === keys.ENTER)) ok(event); | |
if (keyCode === keys.ESC && hasCancel) cancel(event); | |
}; | |
// reset focus to first item in the dialog | |
reset = function (event) { | |
if (hasInput) input.focus(); | |
else if (!hasCancel || self.buttonReverse) btnOK.focus(); | |
else btnCancel.focus(); | |
}; | |
// handle reset focus link | |
// this ensures that the keyboard focus does not | |
// ever leave the dialog box until an action has | |
// been taken | |
this.bind(btnReset, "focus", reset); | |
this.bind(btnResetBack, "focus", reset); | |
// handle OK click | |
if (hasOK) this.bind(btnOK, "click", ok); | |
// handle Cancel click | |
if (hasCancel) this.bind(btnCancel, "click", cancel); | |
// listen for keys, Cancel => ESC | |
this.bind(document.body, "keyup", key); | |
if (!this.transition.supported) { | |
this.setFocus(); | |
} | |
}, | |
/** | |
* Bind events to elements | |
* | |
* @param {Object} el HTML Object | |
* @param {Event} event Event to attach to element | |
* @param {Function} fn Callback function | |
* | |
* @return {undefined} | |
*/ | |
bind: function (el, event, fn) { | |
if (typeof el.addEventListener === "function") { | |
el.addEventListener(event, fn, false); | |
} else if (el.attachEvent) { | |
el.attachEvent("on" + event, fn); | |
} | |
}, | |
/** | |
* Use alertify as the global error handler (using window.onerror) | |
* | |
* @return {boolean} success | |
*/ | |
handleErrors: function () { | |
if (typeof global.onerror !== "undefined") { | |
var self = this; | |
global.onerror = function (msg, url, line) { | |
self.error("[" + msg + " on line " + line + " of " + url + "]", 0); | |
}; | |
return true; | |
} else { | |
return false; | |
} | |
}, | |
/** | |
* Append button HTML strings | |
* | |
* @param {String} secondary The secondary button HTML string | |
* @param {String} primary The primary button HTML string | |
* | |
* @return {String} The appended button HTML strings | |
*/ | |
appendButtons: function (secondary, primary) { | |
return this.buttonReverse ? primary + secondary : secondary + primary; | |
}, | |
/** | |
* Build the proper message box | |
* | |
* @param {Object} item Current object in the queue | |
* | |
* @return {String} An HTML string of the message box | |
*/ | |
build: function (item) { | |
var html = "", | |
type = item.type, | |
message = item.message, | |
css = item.cssClass || ""; | |
html += "<div class=\"alertify-dialog\">"; | |
html += "<a id=\"alertify-resetFocusBack\" class=\"alertify-resetFocus\" href=\"#\">Reset Focus</a>"; | |
if (_alertify.buttonFocus === "none") html += "<a href=\"#\" id=\"alertify-noneFocus\" class=\"alertify-hidden\"></a>"; | |
// doens't require an actual form | |
if (type === "prompt") html += "<div id=\"alertify-form\">"; | |
html += "<article class=\"alertify-inner\">"; | |
html += dialogs.message.replace("{{message}}", message); | |
if (type === "prompt") html += dialogs.input; | |
html += dialogs.buttons.holder; | |
html += "</article>"; | |
if (type === "prompt") html += "</div>"; | |
html += "<a id=\"alertify-resetFocus\" class=\"alertify-resetFocus\" href=\"#\">Reset Focus</a>"; | |
html += "</div>"; | |
switch (type) { | |
case "confirm": | |
html = html.replace("{{buttons}}", this.appendButtons(dialogs.buttons.cancel, dialogs.buttons.ok)); | |
html = html.replace("{{ok}}", this.labels.ok).replace("{{cancel}}", this.labels.cancel); | |
break; | |
case "prompt": | |
html = html.replace("{{buttons}}", this.appendButtons(dialogs.buttons.cancel, dialogs.buttons.submit)); | |
html = html.replace("{{ok}}", this.labels.ok).replace("{{cancel}}", this.labels.cancel); | |
break; | |
case "alert": | |
html = html.replace("{{buttons}}", dialogs.buttons.ok); | |
html = html.replace("{{ok}}", this.labels.ok); | |
break; | |
default: | |
break; | |
} | |
elDialog.className = "alertify alertify-" + type + " " + css; | |
elCover.className = "alertify-cover"; | |
return html; | |
}, | |
/** | |
* Close the log messages | |
* | |
* @param {Object} elem HTML Element of log message to close | |
* @param {Number} wait [optional] Time (in ms) to wait before automatically hiding the message, if 0 never hide | |
* | |
* @return {undefined} | |
*/ | |
close: function (elem, wait) { | |
// Unary Plus: +"2" === 2 | |
var timer = (wait && !isNaN(wait)) ? +wait : this.delay, | |
self = this, | |
hideElement, transitionDone; | |
// set click event on log messages | |
this.bind(elem, "click", function () { | |
hideElement(elem); | |
}); | |
// Hide the dialog box after transition | |
// This ensure it doens't block any element from being clicked | |
transitionDone = function (event) { | |
event.stopPropagation(); | |
// unbind event so function only gets called once | |
self.unbind(this, self.transition.type, transitionDone); | |
// remove log message | |
elLog.removeChild(this); | |
if (!elLog.hasChildNodes()) elLog.className += " alertify-logs-hidden"; | |
}; | |
// this sets the hide class to transition out | |
// or removes the child if css transitions aren't supported | |
hideElement = function (el) { | |
// ensure element exists | |
if (typeof el !== "undefined" && el.parentNode === elLog) { | |
// whether CSS transition exists | |
el.className += " alertify-log-hide"; | |
if (self.transition.supported) { | |
self.bind(el, self.transition.type, transitionDone); | |
el.className += " alertify-log-hide"; | |
// This is a "just in case" for situations where the transition doesn't fire. | |
var dur = ( | |
getStyleRuleValue("transition-duration", ".alertify-log-hide") || | |
getStyleRuleValue("-webkit-transition-duration", ".alertify-log-hide") || | |
getStyleRuleValue("-moz-transition-duration", ".alertify-log-hide") || | |
getStyleRuleValue("-o-transition-duration", ".alertify-log-hide") || | |
"0" | |
).toLowerCase(), | |
time = parseInt(dur), | |
offset = 1; | |
if (!time || isNaN(time)) { | |
time = 500; | |
} | |
if (dur.indexOf("ms") > -1) { | |
time += offset; | |
} else if (dur.indexOf("s") > -1) { | |
time *= 1000; | |
time += offset; | |
} | |
setTimeout(function () { | |
if (typeof el !== "undefined" && el.parentNode === elLog) { | |
elLog.removeChild(el); | |
} | |
}, time); | |
} else { | |
elLog.removeChild(el); | |
if (!elLog.hasChildNodes()) elLog.className += " alertify-logs-hidden"; | |
} | |
} | |
}; | |
// never close (until click) if wait is set to 0 | |
if (wait === 0) return; | |
// set timeout to auto close the log message | |
setTimeout(function () { | |
hideElement(elem); | |
}, timer); | |
}, | |
/** | |
* Create a dialog box | |
* | |
* @param {String} message The message passed from the callee | |
* @param {String} type Type of dialog to create | |
* @param {Function} fn [Optional] Callback function | |
* @param {String} placeholder [Optional] Default value for prompt input field | |
* @param {String} cssClass [Optional] Class(es) to append to dialog box | |
* | |
* @return {Object} | |
*/ | |
dialog: function (message, type, fn, placeholder, cssClass) { | |
// set the current active element | |
// this allows the keyboard focus to be resetted | |
// after the dialog box is closed | |
elCallee = document.activeElement; | |
// check to ensure the alertify dialog element | |
// has been successfully created | |
var check = function () { | |
if ((elLog && elLog.scrollTop !== null) && (elCover && elCover.scrollTop !== null)) return; | |
else check(); | |
}; | |
// error catching | |
if (typeof message !== "string") throw new Error("message must be a string"); | |
if (typeof type !== "string") throw new Error("type must be a string"); | |
if (typeof fn !== "undefined" && typeof fn !== "function") throw new Error("fn must be a function"); | |
// initialize alertify if it hasn't already been done | |
this.init(); | |
check(); | |
queue.push({type: type, message: message, callback: fn, placeholder: placeholder, cssClass: cssClass}); | |
if (!isopen) this.setup(); | |
return this; | |
}, | |
/** | |
* Extend the log method to create custom methods | |
* | |
* @param {String} type Custom method name | |
* | |
* @return {Function} | |
*/ | |
extend: function (type) { | |
if (typeof type !== "string") throw new Error("extend method must have exactly one parameter"); | |
return function (message, wait) { | |
this.log(message, type, wait); | |
return this; | |
}; | |
}, | |
/** | |
* Hide the dialog and rest to defaults | |
* | |
* @return {undefined} | |
*/ | |
hide: function () { | |
var transitionDone, | |
self = this; | |
// remove reference from queue | |
queue.splice(0, 1); | |
// if items remaining in the queue | |
if (queue.length > 0) this.setup(true); | |
else { | |
isopen = false; | |
// Hide the dialog box after transition | |
// This ensure it doens't block any element from being clicked | |
transitionDone = function (event) { | |
event.stopPropagation(); | |
// unbind event so function only gets called once | |
self.unbind(elDialog, self.transition.type, transitionDone); | |
}; | |
// whether CSS transition exists | |
if (this.transition.supported) { | |
this.bind(elDialog, this.transition.type, transitionDone); | |
elDialog.className = "alertify alertify-hide alertify-hidden"; | |
} else { | |
elDialog.className = "alertify alertify-hide alertify-hidden alertify-isHidden"; | |
} | |
elCover.className = "alertify-cover alertify-cover-hidden"; | |
// set focus to the last element or body | |
// after the dialog is closed | |
elCallee.focus(); | |
} | |
}, | |
/** | |
* Initialize Alertify | |
* Create the 2 main elements | |
* | |
* @return {undefined} | |
*/ | |
init: function () { | |
// ensure legacy browsers support html5 tags | |
document.createElement("nav"); | |
document.createElement("article"); | |
document.createElement("section"); | |
// cover | |
if ($("alertify-cover") == null) { | |
elCover = document.createElement("div"); | |
elCover.setAttribute("id", "alertify-cover"); | |
elCover.className = "alertify-cover alertify-cover-hidden"; | |
document.body.appendChild(elCover); | |
} | |
// main element | |
if ($("alertify") == null) { | |
isopen = false; | |
queue = []; | |
elDialog = document.createElement("section"); | |
elDialog.setAttribute("id", "alertify"); | |
elDialog.className = "alertify alertify-hidden"; | |
document.body.appendChild(elDialog); | |
} | |
// log element | |
if ($("alertify-logs") == null) { | |
elLog = document.createElement("section"); | |
elLog.setAttribute("id", "alertify-logs"); | |
elLog.className = "alertify-logs alertify-logs-hidden"; | |
document.body.appendChild(elLog); | |
} | |
// set tabindex attribute on body element | |
// this allows script to give it focus | |
// after the dialog is closed | |
document.body.setAttribute("tabindex", "0"); | |
// set transition type | |
this.transition = getTransitionEvent(); | |
}, | |
/** | |
* Show a new log message box | |
* | |
* @param {String} message The message passed from the callee | |
* @param {String} type [Optional] Optional type of log message | |
* @param {Number} wait [Optional] Time (in ms) to wait before auto-hiding the log | |
* | |
* @return {Object} | |
*/ | |
log: function (message, type, wait, click) { | |
// check to ensure the alertify dialog element | |
// has been successfully created | |
var check = function () { | |
if (elLog && elLog.scrollTop !== null) return; | |
else check(); | |
}; | |
// initialize alertify if it hasn't already been done | |
this.init(); | |
check(); | |
elLog.className = "alertify-logs"; | |
this.notify(message, type, wait, click); | |
return this; | |
}, | |
/** | |
* Add new log message | |
* If a type is passed, a class name "alertify-log-{type}" will get added. | |
* This allows for custom look and feel for various types of notifications. | |
* | |
* @param {String} message The message passed from the callee | |
* @param {String} type [Optional] Type of log message | |
* @param {Number} wait [Optional] Time (in ms) to wait before auto-hiding | |
* | |
* @return {undefined} | |
*/ | |
notify: function (message, type, wait, click) { | |
var log = document.createElement("article"); | |
log.className = "alertify-log" + ((typeof type === "string" && type !== "") ? " alertify-log-" + type : ""); | |
log.innerHTML = message; | |
// Add the click handler, if specified. | |
if ("function" === typeof click) { | |
this.bind(log, "click", click); | |
} | |
// append child | |
elLog.appendChild(log); | |
// triggers the CSS animation | |
setTimeout(function () { | |
log.className += " alertify-log-show"; | |
}, 50); | |
this.close(log, wait); | |
}, | |
/** | |
* Set properties | |
* | |
* @param {Object} args Passing parameters | |
* | |
* @return {undefined} | |
*/ | |
set: function (args) { | |
var k; | |
// error catching | |
if (typeof args !== "object" && args instanceof Array) throw new Error("args must be an object"); | |
// set parameters | |
for (k in args) { | |
if (args.hasOwnProperty(k)) { | |
this[k] = args[k]; | |
} | |
} | |
}, | |
/** | |
* Common place to set focus to proper element | |
* | |
* @return {undefined} | |
*/ | |
setFocus: function () { | |
if (input) { | |
input.focus(); | |
input.select(); | |
} | |
else btnFocus.focus(); | |
}, | |
/** | |
* Initiate all the required pieces for the dialog box | |
* | |
* @return {undefined} | |
*/ | |
setup: function (fromQueue) { | |
var item = queue[0], | |
self = this, | |
transitionDone; | |
// dialog is open | |
isopen = true; | |
// Set button focus after transition | |
transitionDone = function (event) { | |
event.stopPropagation(); | |
self.setFocus(); | |
// unbind event so function only gets called once | |
self.unbind(elDialog, self.transition.type, transitionDone); | |
}; | |
// whether CSS transition exists | |
if (this.transition.supported && !fromQueue) { | |
this.bind(elDialog, this.transition.type, transitionDone); | |
} | |
// build the proper dialog HTML | |
elDialog.innerHTML = this.build(item); | |
// assign all the common elements | |
btnReset = $("alertify-resetFocus"); | |
btnResetBack = $("alertify-resetFocusBack"); | |
btnOK = $("alertify-ok") || undefined; | |
btnCancel = $("alertify-cancel") || undefined; | |
btnFocus = (_alertify.buttonFocus === "cancel") ? btnCancel : ((_alertify.buttonFocus === "none") ? $("alertify-noneFocus") : btnOK); | |
input = $("alertify-text") || undefined; | |
form = $("alertify-form") || undefined; | |
// add placeholder value to the input field | |
if (typeof item.placeholder === "string" && item.placeholder !== "") input.value = item.placeholder; | |
if (fromQueue) this.setFocus(); | |
this.addListeners(item.callback); | |
}, | |
/** | |
* Unbind events to elements | |
* | |
* @param {Object} el HTML Object | |
* @param {Event} event Event to detach to element | |
* @param {Function} fn Callback function | |
* | |
* @return {undefined} | |
*/ | |
unbind: function (el, event, fn) { | |
if (typeof el.removeEventListener === "function") { | |
el.removeEventListener(event, fn, false); | |
} else if (el.detachEvent) { | |
el.detachEvent("on" + event, fn); | |
} | |
} | |
}; | |
return { | |
alert: function (message, fn, cssClass) { | |
_alertify.dialog(message, "alert", fn, "", cssClass); | |
return this; | |
}, | |
confirm: function (message, fn, cssClass) { | |
_alertify.dialog(message, "confirm", fn, "", cssClass); | |
return this; | |
}, | |
extend: _alertify.extend, | |
init: _alertify.init, | |
log: function (message, type, wait, click) { | |
_alertify.log(message, type, wait, click); | |
return this; | |
}, | |
prompt: function (message, fn, placeholder, cssClass) { | |
_alertify.dialog(message, "prompt", fn, placeholder, cssClass); | |
return this; | |
}, | |
success: function (message, wait, click) { | |
_alertify.log(message, "success", wait, click); | |
return this; | |
}, | |
error: function (message, wait, click) { | |
_alertify.log(message, "error", wait, click); | |
return this; | |
}, | |
set: function (args) { | |
_alertify.set(args); | |
}, | |
labels: _alertify.labels, | |
debug: _alertify.handleErrors | |
}; | |
}; | |
// AMD and window support | |
if (typeof define === "function") { | |
define([], function () { | |
return new Alertify(); | |
}); | |
} else if (typeof global.alertify === "undefined") { | |
global.alertify = new Alertify(); | |
} | |
}(window)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment