/** * Modal dialog prompts * @module Ink.UI.Modal_1 * @version 1 */ Ink.createModule('Ink.UI.Modal', '1', ['Ink.UI.Common_1','Ink.Dom.Event_1','Ink.Dom.Css_1','Ink.Dom.Element_1','Ink.Dom.Selector_1','Ink.Util.Array_1'], function(Common, Event, Css, InkElement, Selector, InkArray ) { 'use strict'; var opacitySupported = (function (div) { div.style.opacity = 'invalid'; return div.style.opacity !== 'invalid'; }(InkElement.create('div', {style: 'opacity: 1'}))); /** * @class Ink.UI.Modal * @constructor * @version 1 * @param {String|DOMElement} selector Element or ID * @param {Object} [options] Options object, containing: * @param {String} [options.width] Default/Initial width. Ex: '600px' * @param {String} [options.height] Default/Initial height. Ex: '400px' * @param {String} [options.shadeClass] Custom class to be added to the div.ink-shade * @param {String} [options.modalClass] Custom class to be added to the div.ink-modal * @param {String} [options.trigger] CSS Selector for target elements that will trigger the Modal. * @param {Boolean} [options.autoDisplay] Displays the Modal automatically when constructed. * @param {String} [options.markup] Markup to be placed in the Modal when created * @param {Function} [options.onShow] Callback function to run when the Modal is opened. * @param {Function} [options.onDismiss] Callback function to run when the Modal is closed. Return `false` to cancel dismissing the Modal. * @param {Boolean} [options.closeOnClick] Flag to close the modal when clicking outside of it. * @param {Boolean} [options.closeOnEscape] Determines if the Modal should close when "Esc" key is pressed. Defaults to true. * @param {Boolean} [options.responsive] Determines if the Modal should behave responsively (adapt to smaller viewports). * @param {String} [options.triggerEvent] (advanced) Trigger's event to be listened. Defaults to 'click'. * * @sample Ink_UI_Modal_1.html */ function upName(dimension) { // omg IE var firstCharacter = dimension.match(/^./)[0]; return firstCharacter.toUpperCase() + dimension.replace(/^./, ''); } function maxName(dimension) { return 'max' + upName(dimension); } var openModals = []; function Modal() { Common.BaseUIComponent.apply(this, arguments); } Modal._name = 'Modal_1'; Modal._optionDefinition = { /** * Width, height and markup really optional, as they can be obtained by the element */ width: ['String', undefined], height: ['String', undefined], /** * To add extra classes */ shadeClass: ['String', undefined], modalClass: ['String', undefined], /** * Optional trigger properties */ trigger: ['String', undefined], triggerEvent: ['String', 'click'], autoDisplay: ['Boolean', true], /** * Remaining options */ markup: ['String', undefined], onShow: ['Function', undefined], onDismiss: ['Function', undefined], closeOnClick: ['Boolean', false], closeOnEscape: ['Boolean', true], responsive: ['Boolean', true] }; Modal.prototype = { _init: function () { this._handlers = { click: Ink.bindEvent(this._onShadeClick, this), keyDown: Ink.bindEvent(this._onKeyDown, this), resize: Ink.bindEvent(this._onResize, this) }; this._wasDismissed = false; /** * Modal Markup */ if( this._element ){ this._markupMode = Css.hasClassName(this._element,'ink-modal'); // Check if the full modal comes from the markup } else { this._markupMode = false; } if( !this._markupMode ){ this._modalShadow = document.createElement('div'); this._modalShadowStyle = this._modalShadow.style; this._modalDiv = document.createElement('div'); this._modalDivStyle = this._modalDiv.style; if( !!this._element ){ this._options.markup = this._element.innerHTML; } /** * Not in full markup mode, let's set the classes and css configurations */ Css.addClassName( this._modalShadow,'ink-shade' ); Css.addClassName( this._modalDiv,'ink-modal ink-space' ); /** * Applying the main css styles */ // this._modalDivStyle.position = 'absolute'; this._modalShadow.appendChild( this._modalDiv); document.body.appendChild( this._modalShadow ); } else { this._modalDiv = this._element; this._modalDivStyle = this._modalDiv.style; this._modalShadow = this._modalDiv.parentNode; this._modalShadowStyle = this._modalShadow.style; this._contentContainer = Selector.select(".modal-body", this._modalDiv)[0]; if( !this._contentContainer){ throw new Error('Ink.UI.Modal: Missing div with class "modal-body"'); } this._options.markup = this._contentContainer.innerHTML; } if( !this._markupMode ){ this.setContentMarkup(this._options.markup); } if( typeof this._options.shadeClass === 'string' ){ Css.addClassName(this._modalShadow, this._options.shadeClass); } if( typeof this._options.modalClass === 'string' ){ Css.addClassName(this._modalDiv, this._options.modalClass); } if( this._options.trigger ) { var triggerElements = Common.elsOrSelector(this._options.trigger, ''); Event.observeMulti(triggerElements, this._options.triggerEvent, Ink.bindEvent(this.open, this)); } else if ( this._options.autoDisplay.toString() === "true" ) { this.open(); } }, /** * Responsible for repositioning the modal * * @method _reposition * @private */ _reposition: function(){ this._modalDivStyle.marginTop = (-InkElement.elementHeight(this._modalDiv)/2) + 'px'; this._modalDivStyle.marginLeft = (-InkElement.elementWidth(this._modalDiv)/2) + 'px'; }, /** * Responsible for resizing the modal * * @method _onResize * @param {Boolean|Event} runNow Its executed in the begining to resize/reposition accordingly to the viewport. But usually it's an event object. * @private */ _onResize: function( runNow ){ if( typeof runNow === 'boolean' ){ this._timeoutResizeFunction.call(this); } else if( !this._resizeTimeout && (runNow && typeof runNow === 'object') ){ this._resizeTimeout = setTimeout(Ink.bind(this._timeoutResizeFunction, this),250); } }, /** * Timeout Resize Function * * @method _timeoutResizeFunction * @private */ _timeoutResizeFunction: function(){ /** * Getting the current viewport size */ var isPercentage = { width: ('' + this._options.width).indexOf('%') !== -1, height: ('' + this._options.height).indexOf('%') !== -1 }; var currentViewport = { height: InkElement.viewportHeight(), width: InkElement.viewportWidth() }; InkArray.forEach(['height', 'width'], Ink.bind(function (dimension) { // Not used for percentage measurements if (isPercentage[dimension]) { return; } if (currentViewport[dimension] > this.originalStatus[dimension]) { this._modalDivStyle[dimension] = this._modalDivStyle[maxName(dimension)]; } else { this._modalDivStyle[dimension] = Math.round(currentViewport[dimension] * 0.9) + 'px'; } }, this)); this._resizeContainer(); this._reposition(); this._resizeTimeout = undefined; }, /** * Handle clicks on the shade element. * * @method _onShadeClick * @param {Event} ev * @private */ _onShadeClick: function(ev) { var tgtEl = Event.element(ev); if (Css.hasClassName(tgtEl, 'ink-close') || Css.hasClassName(tgtEl, 'ink-dismiss') || InkElement.findUpwardsBySelector(tgtEl, '.ink-close,.ink-dismiss') || ( this._options.closeOnClick && (!InkElement.descendantOf(this._shadeElement, tgtEl) || (tgtEl === this._shadeElement)) ) ) { var alertsInTheModal = Selector.select('.ink-alert', this._shadeElement), alertsLength = alertsInTheModal.length; for( var i = 0; i < alertsLength; i++ ){ if( InkElement.descendantOf(alertsInTheModal[i], tgtEl) ){ return; } } this.dismiss(); // Only stop the event if this dismisses this modal if (this._wasDismissed) { Event.stop(ev); } } }, /** * Responsible for handling the escape key pressing. * * @method _onKeyDown * @param {Event} ev * @private */ _onKeyDown: function(ev) { if (ev.keyCode !== 27 || this._wasDismissed) { return; } if (this._options.closeOnEscape.toString() === 'true' && openModals[openModals.length - 1] === this) { this.dismiss(); if (this._wasDismissed) { Event.stop(ev); } } }, /** * Responsible for setting the size of the modal (and position) based on the viewport. * * @method _resizeContainer * @private */ _resizeContainer: function() { var containerHeight = InkElement.elementHeight(this._modalDiv); this._modalHeader = Selector.select('.modal-header',this._modalDiv)[0]; if( this._modalHeader ){ containerHeight -= InkElement.elementHeight(this._modalHeader); } this._modalFooter = Selector.select('.modal-footer',this._modalDiv)[0]; if( this._modalFooter ){ containerHeight -= InkElement.elementHeight(this._modalFooter); } this._contentContainer.style.height = containerHeight + 'px'; if( containerHeight !== InkElement.elementHeight(this._contentContainer) ){ this._contentContainer.style.height = ~~(containerHeight - (InkElement.elementHeight(this._contentContainer) - containerHeight)) + 'px'; } if( this._markupMode ){ return; } }, /************** * PUBLIC API * **************/ /** * Opens this Modal. * Use this if you created the modal with `autoOpen: false` * to open the modal when you want to. * @method open * @param {Event} [event] (internal) In case its fired by the internal trigger. */ open: function(event) { if( event ){ Event.stop(event); } var elem = (document.compatMode === "CSS1Compat") ? document.documentElement : document.body; this._resizeTimeout = null; Css.addClassName( this._modalShadow,'ink-shade' ); this._modalShadowStyle.display = this._modalDivStyle.display = 'block'; setTimeout(Ink.bind(function() { Css.addClassName( this._modalShadow, 'visible' ); Css.addClassName( this._modalDiv, 'visible' ); }, this), 100); /** * Fallback to the old one */ this._contentElement = this._modalDiv; this._shadeElement = this._modalShadow; if( !this._markupMode ){ /** * Setting the content of the modal */ this.setContentMarkup( this._options.markup ); } /** * If any size has been user-defined, let's set them as max-width and max-height */ var isPercentage = { width: ('' + this._options.width).indexOf('%') !== -1, height: ('' + this._options.height).indexOf('%') !== -1 }; InkArray.forEach(['width', 'height'], Ink.bind(function (dimension) { if (this._options[dimension] !== undefined) { this._modalDivStyle[dimension] = this._options[dimension]; if (!isPercentage[dimension]) { this._modalDivStyle[maxName(dimension)] = InkElement['element' + upName(dimension)](this._modalDiv) + 'px'; } } else { this._modalDivStyle[maxName(dimension)] = InkElement['element' + upName(dimension)](this._modalDiv) + 'px'; } if (isPercentage[dimension] && parseInt(elem['client' + maxName(dimension)], 10) <= parseInt(this._modalDivStyle[dimension], 10) ) { this._modalDivStyle[dimension] = Math.round(parseInt(elem['client' + maxName(dimension)], 10) * 0.9) + 'px'; } }, this)); this.originalStatus = { viewportHeight: InkElement.elementHeight(elem), viewportWidth: InkElement.elementWidth(elem), height: InkElement.elementHeight(this._modalDiv), width: InkElement.elementWidth(this._modalDiv) }; /** * Let's 'resize' it: */ if( this._options.responsive.toString() === 'true' ) { this._onResize(true); Event.observe( window,'resize',this._handlers.resize ); } else { this._resizeContainer(); this._reposition(); } if (this._options.onShow) { this._options.onShow(this); } // subscribe events Event.observe(this._shadeElement, 'click', this._handlers.click); if (this._options.closeOnEscape.toString() === 'true') { Event.observe(document, 'keydown', this._handlers.keyDown); } this._wasDismissed = false; openModals.push(this); Css.addClassName(document.documentElement, 'ink-modal-open'); }, /** * Closes the modal * * @method dismiss * @public */ dismiss: function() { if (this._wasDismissed) { /* Already dismissed. WTF IE. */ return; } if (this._options.onDismiss) { var ret = this._options.onDismiss(this); if (ret === false) { return; } } this._wasDismissed = true; if( this._options.responsive ){ Event.stopObserving(window, 'resize', this._handlers.resize); } // this._modalShadow.parentNode.removeChild(this._modalShadow); if( !this._markupMode ){ this._modalShadow.parentNode.removeChild(this._modalShadow); this.destroy(); } else { Css.removeClassName( this._modalDiv, 'visible' ); Css.removeClassName( this._modalShadow, 'visible' ); this._waitForFade(this._modalShadow, Ink.bind(function () { this._modalShadowStyle.display = 'none'; }, this)); } openModals = InkArray.remove(openModals, InkArray.keyValue(this, openModals), 1); if (openModals.length === 0) { // Document level stuff now there are no modals in play. var htmlEl = document.documentElement; // Remove the class from the HTML element. Css.removeClassName(htmlEl, 'ink-modal-open'); } }, /** * Utility function to listen to the onTransmissionEnd event, or wait using setTimeouts * * Specific to this._element */ _waitForFade: function (elem, callback) { if (!opacitySupported) { return callback(); } var transitionEndEventNames = [ 'transitionEnd', 'oTransitionEnd', 'webkitTransitionEnd']; var classicName; var evName; for (var i = 0, len = transitionEndEventNames.length; i < len; i++) { evName = transitionEndEventNames[i]; classicName = 'on' + evName.toLowerCase(); if (classicName in elem) { Event.observeOnce(elem, evName, callback); return; } } var fadeChecker = function () { if( +Css.getStyle(elem, 'opacity') > 0 ){ setTimeout(fadeChecker, 250); } else { callback(); } }; setTimeout(fadeChecker, 500); }, /** * Removes the modal from the DOM * * @method destroy * @public */ destroy: function() { Common.unregisterInstance(this._instanceId); }, /** * Returns the content DOM element * * @method getContentElement * @return {DOMElement} Modal main cointainer. * @public */ getContentElement: function() { return this._contentContainer; }, /** * Replaces the content markup * * @method setContentMarkup * @param {String} contentMarkup * @public */ setContentMarkup: function(contentMarkup) { if( !this._markupMode ){ this._modalDiv.innerHTML = [contentMarkup].join(''); this._contentContainer = Selector.select(".modal-body",this._modalDiv); if( !this._contentContainer.length ){ // throw 'Missing div with class "modal-body"'; var tempHeader = Selector.select(".modal-header",this._modalDiv); var tempFooter = Selector.select(".modal-footer",this._modalDiv); InkArray.each(tempHeader, InkElement.remove); InkArray.each(tempFooter, InkElement.remove); var body = document.createElement('div'); Css.addClassName(body,'modal-body'); body.innerHTML = this._modalDiv.innerHTML; this._modalDiv.innerHTML = ''; var toAdd = tempHeader.concat([body]).concat(tempFooter); InkArray.each(toAdd, Ink.bindMethod(this._modalDiv, 'appendChild')); this._contentContainer = Selector.select(".modal-body",this._modalDiv); } this._contentContainer = this._contentContainer[0]; } else { this._contentContainer.innerHTML = contentMarkup; } this._contentElement = this._modalDiv; this._resizeContainer(); } }; Common.createUIComponent(Modal, { elementIsOptional: true }); return Modal; });