/** * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (function (root, factory) { "use strict"; if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.webphone_api.picoModal = factory(); } }(this, function () { /** * A self-contained modal library */ "use strict"; /** Returns whether a value is a dom node */ function isNode(value) { if ( typeof Node === "object" ) { return value instanceof Node; } else { return value && typeof value === "object" && typeof value.nodeType === "number"; } } /** Returns whether a value is a string */ function isString(value) { return typeof value === "string"; } /** * Generates observable objects that can be watched and triggered */ function observable() { var callbacks = []; return { watch: callbacks.push.bind(callbacks), trigger: function( modal ) { var unprevented = true; var event = { preventDefault: function preventDefault () { unprevented = false; } }; for (var i = 0; i < callbacks.length; i++) { callbacks[i](modal, event); } return unprevented; } }; } /** * A small interface for creating and managing a dom element */ function Elem( elem ) { this.elem = elem; } /** * Creates a new div */ Elem.div = function ( parent ) { var elem = document.createElement('div'); (parent || document.body).appendChild(elem); return new Elem(elem); }; Elem.prototype = { /** Creates a child of this node */ child: function () { return Elem.div(this.elem); }, /** Applies a set of styles to an element */ stylize: function(styles) { styles = styles || {}; if ( typeof styles.opacity !== "undefined" ) { styles.filter = "alpha(opacity=" + (styles.opacity * 100) + ")"; } for (var prop in styles) { if (styles.hasOwnProperty(prop)) { this.elem.style[prop] = styles[prop]; } } return this; }, /** Adds a class name */ clazz: function (clazz) { this.elem.className += " " + clazz; return this; }, /** Sets the HTML */ html: function (content) { if ( isNode(content) ) { this.elem.appendChild( content ); } else { this.elem.innerHTML = content; } return this; }, /** Adds a click handler to this element */ onClick: function(callback) { this.elem.addEventListener('click', callback); return this; }, /** Removes this element from the DOM */ destroy: function() { document.body.removeChild(this.elem); }, /** Hides this element */ hide: function() { this.elem.style.display = "none"; }, /** Shows this element */ show: function() { this.elem.style.display = "block"; }, /** Sets an attribute on this element */ attr: function ( name, value ) { this.elem.setAttribute(name, value); return this; }, /** Executes a callback on all the ancestors of an element */ anyAncestor: function ( predicate ) { var elem = this.elem; while ( elem ) { if ( predicate( new Elem(elem) ) ) { return true; } else { elem = elem.parentNode; } } return false; } }; /** Generates the grey-out effect */ function buildOverlay( getOption, close ) { return Elem.div() .clazz("pico-overlay") .clazz( getOption("overlayClass", "") ) .stylize({ display: "none", position: "fixed", top: "0px", left: "0px", height: "100%", width: "100%", zIndex: 10000 }) .stylize(getOption('overlayStyles', { opacity: 0.5, background: "#000" })) .onClick(function () { if ( getOption('overlayClose', true) ) { close(); } }); } /** Builds the content of a modal */ function buildModal( getOption, close ) { var width = getOption('width', 'auto'); if ( typeof width === "number" ) { width = "" + width + "px"; } var elem = Elem.div() .clazz("pico-content") .clazz( getOption("modalClass", "") ) .stylize({ display: 'none', position: 'fixed', zIndex: 10001, left: "50%", top: "50px", width: width, '-ms-transform': 'translateX(-50%)', '-moz-transform': 'translateX(-50%)', '-webkit-transform': 'translateX(-50%)', '-o-transform': 'translateX(-50%)', 'transform': 'translateX(-50%)' }) .stylize(getOption('modalStyles', { backgroundColor: "white", padding: "20px", borderRadius: "5px" })) .html( getOption('content') ) .attr("role", "dialog") .onClick(function (event) { var isCloseClick = new Elem(event.target) .anyAncestor(function (elem) { return /\bpico-close\b/.test(elem.elem.className); }); if ( isCloseClick ) { close(); } }); return elem; } /** Builds the close button */ function buildClose ( elem, getOption ) { if ( getOption('closeButton', true) ) { return elem.child() .html( getOption('closeHtml', "×") ) .clazz("pico-close") .clazz( getOption("closeClass") ) .stylize( getOption('closeStyles', { borderRadius: "2px", cursor: "pointer", height: "15px", width: "15px", position: "absolute", top: "5px", right: "5px", fontSize: "16px", textAlign: "center", lineHeight: "15px", background: "#CCC" }) ); } } /** Builds a method that calls a method and returns an element */ function buildElemAccessor( builder ) { return function () { return builder().elem; }; } /** * Displays a modal */ return function picoModal(options) { if ( isString(options) || isNode(options) ) { options = { content: options }; } var afterCreateEvent = observable(); var beforeShowEvent = observable(); var afterShowEvent = observable(); var beforeCloseEvent = observable(); var afterCloseEvent = observable(); /** * Returns a named option if it has been explicitly defined. Otherwise, * it returns the given default value */ function getOption ( opt, defaultValue ) { var value = options[opt]; if ( typeof value === "function" ) { value = value( defaultValue ); } return value === undefined ? defaultValue : value; } /** Hides this modal */ function forceClose () { shadowElem().hide(); modalElem().hide(); afterCloseEvent.trigger(iface); } /** Gracefully hides this modal */ function close () { if ( beforeCloseEvent.trigger(iface) ) { forceClose(); } } /** Wraps a method so it returns the modal interface */ function returnIface ( callback ) { return function () { callback.apply(this, arguments); return iface; }; } // The constructed dom nodes var built; /** Builds a method that calls a method and returns an element */ function build ( name ) { if ( !built ) { var modal = buildModal(getOption, close); built = { modal: modal, overlay: buildOverlay(getOption, close), close: buildClose(modal, getOption) }; afterCreateEvent.trigger(iface); } return built[name]; } var modalElem = build.bind(window, 'modal'); var shadowElem = build.bind(window, 'overlay'); var closeElem = build.bind(window, 'close'); var iface = { /** Returns the wrapping modal element */ modalElem: buildElemAccessor(modalElem), /** Returns the close button element */ closeElem: buildElemAccessor(closeElem), /** Returns the overlay element */ overlayElem: buildElemAccessor(shadowElem), /** Builds the dom without showing the modal */ buildDom: returnIface(build), /** Shows this modal */ show: function () { if ( beforeShowEvent.trigger(iface) ) { shadowElem().show(); closeElem(); modalElem().show(); afterShowEvent.trigger(iface); } return this; }, /** Hides this modal */ close: returnIface(close), /** * Force closes this modal. This will not call beforeClose * events and will just immediately hide the modal */ forceClose: returnIface(forceClose), /** Destroys this modal */ destroy: function () { modalElem = modalElem().destroy(); shadowElem = shadowElem().destroy(); closeElem = undefined; }, /** * Updates the options for this modal. This will only let you * change options that are re-evaluted regularly, such as * `overlayClose`. */ options: function ( opts ) { options = opts; }, /** Executes after the DOM nodes are created */ afterCreate: returnIface(afterCreateEvent.watch), /** Executes a callback before this modal is closed */ beforeShow: returnIface(beforeShowEvent.watch), /** Executes a callback after this modal is shown */ afterShow: returnIface(afterShowEvent.watch), /** Executes a callback before this modal is closed */ beforeClose: returnIface(beforeCloseEvent.watch), /** Executes a callback after this modal is closed */ afterClose: returnIface(afterCloseEvent.watch) }; return iface; }; }));