UNPKG

alertifyjs

Version:

AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications.

632 lines (583 loc) 26.4 kB
/** * 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; }