alertifyjs
Version:
AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications.
632 lines (583 loc) • 26.4 kB
JavaScript
/**
* Super class for all dialogs
*
* @return {Object} base dialog prototype
*/
var dialog = (function () {
var //holds the list of used keys.
usedKeys = [],
//dummy variable, used to trigger dom reflow.
reflow = null,
//holds body tab index in case it has any.
tabindex = false,
//condition for detecting safari
isSafari = window.navigator.userAgent.indexOf('Safari') > -1 && window.navigator.userAgent.indexOf('Chrome') < 0,
//dialog building blocks
templates = {
dimmer:'<div class="ajs-dimmer"></div>',
/*tab index required to fire click event before body focus*/
modal: '<div class="ajs-modal" tabindex="0"></div>',
dialog: '<div class="ajs-dialog" tabindex="0"></div>',
reset: '<button class="ajs-reset"></button>',
commands: '<div class="ajs-commands"><button class="ajs-pin"></button><button class="ajs-maximize"></button><button class="ajs-close"></button></div>',
header: '<div class="ajs-header"></div>',
body: '<div class="ajs-body"></div>',
content: '<div class="ajs-content"></div>',
footer: '<div class="ajs-footer"></div>',
buttons: { primary: '<div class="ajs-primary ajs-buttons"></div>', auxiliary: '<div class="ajs-auxiliary ajs-buttons"></div>' },
button: '<button class="ajs-button"></button>',
resizeHandle: '<div class="ajs-handle"></div>',
},
//common class names
classes = {
animationIn: 'ajs-in',
animationOut: 'ajs-out',
base: 'alertify',
basic:'ajs-basic',
capture: 'ajs-capture',
closable:'ajs-closable',
fixed: 'ajs-fixed',
frameless:'ajs-frameless',
hidden: 'ajs-hidden',
maximize: 'ajs-maximize',
maximized: 'ajs-maximized',
maximizable:'ajs-maximizable',
modeless: 'ajs-modeless',
movable: 'ajs-movable',
noSelection: 'ajs-no-selection',
noOverflow: 'ajs-no-overflow',
noPadding:'ajs-no-padding',
pin:'ajs-pin',
pinnable:'ajs-pinnable',
prefix: 'ajs-',
resizable: 'ajs-resizable',
restore: 'ajs-restore',
shake:'ajs-shake',
unpinned:'ajs-unpinned',
noTransition:'ajs-no-transition'
};
/**
* Helper: initializes the dialog instance
*
* @return {Number} The total count of currently open modals.
*/
function initialize(instance){
if(!instance.__internal){
//invoke preinit global hook
alertify.defaults.hooks.preinit(instance);
//no need to expose init after this.
delete instance.__init;
//keep a copy of initial dialog settings
if(!instance.__settings){
instance.__settings = copy(instance.settings);
}
//get dialog buttons/focus setup
var setup;
if(typeof instance.setup === 'function'){
setup = instance.setup();
setup.options = setup.options || {};
setup.focus = setup.focus || {};
}else{
setup = {
buttons:[],
focus:{
element:null,
select:false
},
options:{
}
};
}
//initialize hooks object.
if(typeof instance.hooks !== 'object'){
instance.hooks = {};
}
//copy buttons defintion
var buttonsDefinition = [];
if(Array.isArray(setup.buttons)){
for(var b=0;b<setup.buttons.length;b+=1){
var ref = setup.buttons[b],
cpy = {};
for (var i in ref) {
if (ref.hasOwnProperty(i)) {
cpy[i] = ref[i];
}
}
buttonsDefinition.push(cpy);
}
}
var internal = instance.__internal = {
/**
* Flag holding the open state of the dialog
*
* @type {Boolean}
*/
isOpen:false,
/**
* Active element is the element that will receive focus after
* closing the dialog. It defaults as the body tag, but gets updated
* to the last focused element before the dialog was opened.
*
* @type {Node}
*/
activeElement:document.body,
timerIn:undefined,
timerOut:undefined,
buttons: buttonsDefinition,
focus: setup.focus,
options: {
title: undefined,
modal: undefined,
basic:undefined,
frameless:undefined,
defaultFocusOff:undefined,
pinned: undefined,
movable: undefined,
moveBounded:undefined,
resizable: undefined,
autoReset: undefined,
closable: undefined,
closableByDimmer: undefined,
invokeOnCloseOff:undefined,
maximizable: undefined,
startMaximized: undefined,
pinnable: undefined,
transition: undefined,
transitionOff: undefined,
padding:undefined,
overflow:undefined,
onshow:undefined,
onclosing:undefined,
onclose:undefined,
onfocus:undefined,
onmove:undefined,
onmoved:undefined,
onresize:undefined,
onresized:undefined,
onmaximize:undefined,
onmaximized:undefined,
onrestore:undefined,
onrestored:undefined
},
resetHandler:undefined,
beginMoveHandler:undefined,
beginResizeHandler:undefined,
bringToFrontHandler:undefined,
modalClickHandler:undefined,
buttonsClickHandler:undefined,
commandsClickHandler:undefined,
transitionInHandler:undefined,
transitionOutHandler:undefined,
destroy:undefined
};
var elements = {};
//root node
elements.root = document.createElement('div');
//prevent FOUC in case of async styles loading.
elements.root.style.display = 'none';
elements.root.className = classes.base + ' ' + classes.hidden + ' ';
elements.root.innerHTML = templates.dimmer + templates.modal;
//dimmer
elements.dimmer = elements.root.firstChild;
//dialog
elements.modal = elements.root.lastChild;
elements.modal.innerHTML = templates.dialog;
elements.dialog = elements.modal.firstChild;
elements.dialog.innerHTML = templates.reset + templates.commands + templates.header + templates.body + templates.footer + templates.resizeHandle + templates.reset;
//reset links
elements.reset = [];
elements.reset.push(elements.dialog.firstChild);
elements.reset.push(elements.dialog.lastChild);
//commands
elements.commands = {};
elements.commands.container = elements.reset[0].nextSibling;
elements.commands.pin = elements.commands.container.firstChild;
elements.commands.maximize = elements.commands.pin.nextSibling;
elements.commands.close = elements.commands.maximize.nextSibling;
//header
elements.header = elements.commands.container.nextSibling;
//body
elements.body = elements.header.nextSibling;
elements.body.innerHTML = templates.content;
elements.content = elements.body.firstChild;
//footer
elements.footer = elements.body.nextSibling;
elements.footer.innerHTML = templates.buttons.auxiliary + templates.buttons.primary;
//resize handle
elements.resizeHandle = elements.footer.nextSibling;
//buttons
elements.buttons = {};
elements.buttons.auxiliary = elements.footer.firstChild;
elements.buttons.primary = elements.buttons.auxiliary.nextSibling;
elements.buttons.primary.innerHTML = templates.button;
elements.buttonTemplate = elements.buttons.primary.firstChild;
//remove button template
elements.buttons.primary.removeChild(elements.buttonTemplate);
for(var x=0; x < instance.__internal.buttons.length; x+=1) {
var button = instance.__internal.buttons[x];
// add to the list of used keys.
if(usedKeys.indexOf(button.key) < 0){
usedKeys.push(button.key);
}
button.element = elements.buttonTemplate.cloneNode();
button.element.innerHTML = button.text;
if(typeof button.className === 'string' && button.className !== ''){
addClass(button.element, button.className);
}
for(var key in button.attrs){
if(key !== 'className' && button.attrs.hasOwnProperty(key)){
button.element.setAttribute(key, button.attrs[key]);
}
}
if(button.scope === 'auxiliary'){
elements.buttons.auxiliary.appendChild(button.element);
}else{
elements.buttons.primary.appendChild(button.element);
}
}
//make elements pubic
instance.elements = elements;
//save event handlers delegates
internal.resetHandler = delegate(instance, onReset);
internal.beginMoveHandler = delegate(instance, beginMove);
internal.beginResizeHandler = delegate(instance, beginResize);
internal.bringToFrontHandler = delegate(instance, bringToFront);
internal.modalClickHandler = delegate(instance, modalClickHandler);
internal.buttonsClickHandler = delegate(instance, buttonsClickHandler);
internal.commandsClickHandler = delegate(instance, commandsClickHandler);
internal.transitionInHandler = delegate(instance, handleTransitionInEvent);
internal.transitionOutHandler = delegate(instance, handleTransitionOutEvent);
//settings
for(var opKey in internal.options){
if(setup.options[opKey] !== undefined){
// if found in user options
instance.set(opKey, setup.options[opKey]);
}else if(alertify.defaults.hasOwnProperty(opKey)) {
// else if found in defaults options
instance.set(opKey, alertify.defaults[opKey]);
}else if(opKey === 'title' ) {
// else if title key, use alertify.defaults.glossary
instance.set(opKey, alertify.defaults.glossary[opKey]);
}
}
// allow dom customization
if(typeof instance.build === 'function'){
instance.build();
}
//invoke postinit global hook
alertify.defaults.hooks.postinit(instance);
}
//add to the end of the DOM tree.
document.body.appendChild(instance.elements.root);
}
/**
* Helper: maintains scroll position
*
*/
var scrollX, scrollY;
function saveScrollPosition(){
scrollX = getScrollLeft();
scrollY = getScrollTop();
}
function restoreScrollPosition(){
window.scrollTo(scrollX, scrollY);
}
/**
* Helper: adds/removes no-overflow class from body
*
*/
function ensureNoOverflow(){
var requiresNoOverflow = 0;
for(var x=0;x<openDialogs.length;x+=1){
var instance = openDialogs[x];
if(instance.isModal() || instance.isMaximized()){
requiresNoOverflow+=1;
}
}
if(requiresNoOverflow === 0 && document.body.className.indexOf(classes.noOverflow) >= 0){
//last open modal or last maximized one
removeClass(document.body, classes.noOverflow);
preventBodyShift(false);
}else if(requiresNoOverflow > 0 && document.body.className.indexOf(classes.noOverflow) < 0){
//first open modal or first maximized one
preventBodyShift(true);
addClass(document.body, classes.noOverflow);
}
}
var top = '', topScroll = 0;
/**
* Helper: prevents body shift.
*
*/
function preventBodyShift(add){
if(alertify.defaults.preventBodyShift){
if(add && document.documentElement.scrollHeight > document.documentElement.clientHeight ){//&& openDialogs[openDialogs.length-1].elements.dialog.clientHeight <= document.documentElement.clientHeight){
topScroll = scrollY;
top = window.getComputedStyle(document.body).top;
addClass(document.body, classes.fixed);
document.body.style.top = -scrollY + 'px';
} else if(!add) {
scrollY = topScroll;
document.body.style.top = top;
removeClass(document.body, classes.fixed);
restoreScrollPosition();
}
}
}
/**
* Sets the name of the transition used to show/hide the dialog
*
* @param {Object} instance The dilog instance.
*
*/
function updateTransition(instance, value, oldValue){
if(isString(oldValue)){
removeClass(instance.elements.root,classes.prefix + oldValue);
}
addClass(instance.elements.root, classes.prefix + value);
reflow = instance.elements.root.offsetWidth;
}
/**
* Toggles the dialog no transition
*
* @param {Object} instance The dilog instance.
*
* @return {undefined}
*/
function updateTransitionOff(instance){
if (instance.get('transitionOff')) {
// add class
addClass(instance.elements.root, classes.noTransition);
} else {
// remove class
removeClass(instance.elements.root, classes.noTransition);
}
}
/**
* Toggles the dialog display mode
*
* @param {Object} instance The dilog instance.
*
* @return {undefined}
*/
function updateDisplayMode(instance){
if(instance.get('modal')){
//make modal
removeClass(instance.elements.root, classes.modeless);
//only if open
if(instance.isOpen()){
unbindModelessEvents(instance);
//in case a pinned modless dialog was made modal while open.
updateAbsPositionFix(instance);
ensureNoOverflow();
}
}else{
//make modelss
addClass(instance.elements.root, classes.modeless);
//only if open
if(instance.isOpen()){
bindModelessEvents(instance);
//in case pin/unpin was called while a modal is open
updateAbsPositionFix(instance);
ensureNoOverflow();
}
}
}
/**
* Toggles the dialog basic view mode
*
* @param {Object} instance The dilog instance.
*
* @return {undefined}
*/
function updateBasicMode(instance){
if (instance.get('basic')) {
// add class
addClass(instance.elements.root, classes.basic);
} else {
// remove class
removeClass(instance.elements.root, classes.basic);
}
}
/**
* Toggles the dialog frameless view mode
*
* @param {Object} instance The dilog instance.
*
* @return {undefined}
*/
function updateFramelessMode(instance){
if (instance.get('frameless')) {
// add class
addClass(instance.elements.root, classes.frameless);
} else {
// remove class
removeClass(instance.elements.root, classes.frameless);
}
}
/**
* Helper: Brings the modeless dialog to front, attached to modeless dialogs.
*
* @param {Event} event Focus event
* @param {Object} instance The dilog instance.
*
* @return {undefined}
*/
function bringToFront(event, instance){
// Do not bring to front if preceeded by an open modal
var index = openDialogs.indexOf(instance);
for(var x=index+1;x<openDialogs.length;x+=1){
if(openDialogs[x].isModal()){
return;
}
}
// Bring to front by making it the last child.
if(document.body.lastChild !== instance.elements.root){
document.body.appendChild(instance.elements.root);
//also make sure its at the end of the list
openDialogs.splice(openDialogs.indexOf(instance),1);
openDialogs.push(instance);
setFocus(instance);
}
return false;
}
/**
* Helper: reflects dialogs options updates
*
* @param {Object} instance The dilog instance.
* @param {String} option The updated option name.
*
* @return {undefined}
*/
function optionUpdated(instance, option, oldValue, newValue){
switch(option){
case 'title':
instance.setHeader(newValue);
break;
case 'modal':
updateDisplayMode(instance);
break;
case 'basic':
updateBasicMode(instance);
break;
case 'frameless':
updateFramelessMode(instance);
break;
case 'pinned':
updatePinned(instance);
break;
case 'closable':
updateClosable(instance);
break;
case 'maximizable':
updateMaximizable(instance);
break;
case 'pinnable':
updatePinnable(instance);
break;
case 'movable':
updateMovable(instance);
break;
case 'resizable':
updateResizable(instance);
break;
case 'padding':
if(newValue){
removeClass(instance.elements.root, classes.noPadding);
}else if(instance.elements.root.className.indexOf(classes.noPadding) < 0){
addClass(instance.elements.root, classes.noPadding);
}
break;
case 'overflow':
if(newValue){
removeClass(instance.elements.root, classes.noOverflow);
}else if(instance.elements.root.className.indexOf(classes.noOverflow) < 0){
addClass(instance.elements.root, classes.noOverflow);
}
break;
case 'transition':
updateTransition(instance,newValue, oldValue);
break;
case 'transitionOff':
updateTransitionOff(instance);
break;
}
// internal on option updated event
if(typeof instance.hooks.onupdate === 'function'){
instance.hooks.onupdate.call(instance, option, oldValue, newValue);
}
}
/**
* Helper: reflects dialogs options updates
*
* @param {Object} instance The dilog instance.
* @param {Object} obj The object to set/get a value on/from.
* @param {Function} callback The callback function to call if the key was found.
* @param {String|Object} key A string specifying a propery name or a collection of key value pairs.
* @param {Object} value Optional, the value associated with the key (in case it was a string).
* @param {String} option The updated option name.
*
* @return {Object} result object
* The result objects has an 'op' property, indicating of this is a SET or GET operation.
* GET:
* - found: a flag indicating if the key was found or not.
* - value: the property value.
* SET:
* - items: a list of key value pairs of the properties being set.
* each contains:
* - found: a flag indicating if the key was found or not.
* - key: the property key.
* - value: the property value.
*/
function update(instance, obj, callback, key, value){
var result = {op:undefined, items: [] };
if(typeof value === 'undefined' && typeof key === 'string') {
//get
result.op = 'get';
if(obj.hasOwnProperty(key)){
result.found = true;
result.value = obj[key];
}else{
result.found = false;
result.value = undefined;
}
}
else
{
var old;
//set
result.op = 'set';
if(typeof key === 'object'){
//set multiple
var args = key;
for (var prop in args) {
if (obj.hasOwnProperty(prop)) {
if(obj[prop] !== args[prop]){
old = obj[prop];
obj[prop] = args[prop];
callback.call(instance,prop, old, args[prop]);
}
result.items.push({ 'key': prop, 'value': args[prop], 'found':true});
}else{
result.items.push({ 'key': prop, 'value': args[prop], 'found':false});
}
}
} else if (typeof key === 'string'){
//set single
if (obj.hasOwnProperty(key)) {
if(obj[key] !== value){
old = obj[key];
obj[key] = value;
callback.call(instance,key, old, value);
}
result.items.push({'key': key, 'value': value , 'found':true});
}else{
result.items.push({'key': key, 'value': value , 'found':false});
}
} else {
//invalid params
throw new Error('args must be a string or object');
}
}
return result;
}