<commit_before>/**
 * 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.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( context ) {

                var unprevented = true;
                var event = {
                    preventDefault: function preventDefault () {
                        unprevented = false;
                    }
                };

                for (var i = 0; i < callbacks.length; i++) {
                    callbacks[i](context, event);
                }

                return unprevented;
            }
        };
    }

    /** Whether an element is hidden */
    function isHidden ( elem ) {
        return elem.offsetWidth === 0 && elem.offsetHeight === 0;
    }

    /** Whether an element matches a selector */
    function matches ( elem, selector ) {
        return elem.msMatchesSelector ?
            elem.msMatchesSelector(selector) :
            elem.matches(selector);
    }

    /**
     * Returns whether an element is focusable
     * @see http://stackoverflow.com/questions/18261595
     */
    function canFocus( elem ) {
        if (
            isHidden(elem) ||
            matches(elem, ":disabled") ||
            elem.hasAttribute("contenteditable")
        ) {
            return false;
        }
        else {
            return matches(elem, "input, a[href], area[href], iframe") ||
                elem.hasAttribute("tabindex");
        }
    }

    /**
     * A small interface for creating and managing a dom element
     */
    function Elem( elem ) {
        this.elem = elem;
    }

    /** Creates a new div */
    Elem.div = function ( parent ) {
        if ( typeof parent === "string" ) {
            parent = document.querySelector(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() {
            this.elem.parentNode.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;
        },

        /** Returns the first descendant that can be focused */
        firstFocusable: function () {
            var items = this.elem.getElementsByTagName("*");
            for (var i = 0; i < items.length; i++) {
                if ( canFocus(items[i]) ) {
                    return items[i];
                }
            }
        },

        /** Returns the last descendant that can be focused */
        lastFocusable: function () {
            var items = this.elem.getElementsByTagName("*");
            for (var i = items.length; i--;) {
                if ( canFocus(items[i]) ) {
                    return items[i];
                }
            }
        },

        /** Sets focus to the first focusable element */
        moveFocus: function () {
            var focusable = this.firstFocusable();
            if ( focusable ) {
                focusable.focus();
            }
        },

        /** Whether this element is visible */
        isVisible: function () {
            return !isHidden(this.elem);
        }
    };

    /** Generates the grey-out effect */
    function buildOverlay( getOption, close ) {
        return Elem.div( getOption("parent") )
            .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( getOption("parent") )
            .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;
        };
    }

    // An observable that is triggered whenever the escape key is pressed
    var escapeKey = observable();

    // An observable that is triggered when the user hits the tab key
    var tabKey = observable();

    /** A global event handler to detect the escape key being pressed */
    document.documentElement.addEventListener(
        'keydown',
        function onKeyPress (event) {
            var keycode = event.which || event.keyCode;

            // If this is the escape key
            if ( keycode === 27 ) {
                escapeKey.trigger();
            }

            // If this is the tab key
            else if ( keycode === 9 ) {
                tabKey.trigger(event);
            }
        }
    );

    /**
     * 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),

            /** Returns whether this modal is currently being shown */
            isVisible: function () {
                return modalElem().isVisible();
            },

            /** 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)
        };

        // Records the currently focused element so state can be returned
        // after the modal closes
        if ( getOption("focus", true) ) {
            var focused;

            iface.afterShow(function focusModal() {
                focused = document.activeElement;
                modalElem().moveFocus();
            });

            iface.afterClose(function returnFocus() {
                // Restore the previously focused element when the modal closes
                focused.focus();
                focused = null;
            });

            // Capture tab key presses and loop them within the modal
            tabKey.watch(function tabKeyPress (event) {
                if ( !modalElem().isVisible() ) {
                    return false;
                }

                var first = modalElem().firstFocusable();
                var last = modalElem().lastFocusable();

                var from = event.shiftKey ? first : last;
                if ( from === document.activeElement ) {
                    (event.shiftKey ? last : first).focus();
                    event.preventDefault();
                }
            });
        }

        // If a user presses the 'escape' key, close the modal.
        // TODO: This is defaulting to false right now, but should default to
        //       `true` in the future. However, that would require a major
        //       version bump as it would allow modals to be closed that
        //       previously may have been locked open.
        if ( getOption("escCloses", false) ) {
            escapeKey.watch(function escapeKeyPress () {
                if ( iface.isVisible() ) {
                    iface.close();
                }
            });
        }

        return iface;
    };

}));
<commit_msg>Expand what is considered focusable<commit_after>/**
 * 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.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( context ) {

                var unprevented = true;
                var event = {
                    preventDefault: function preventDefault () {
                        unprevented = false;
                    }
                };

                for (var i = 0; i < callbacks.length; i++) {
                    callbacks[i](context, event);
                }

                return unprevented;
            }
        };
    }

    /** Whether an element is hidden */
    function isHidden ( elem ) {
        return elem.offsetWidth === 0 && elem.offsetHeight === 0;
    }

    /** Whether an element matches a selector */
    function matches ( elem, selector ) {
        return elem.msMatchesSelector ?
            elem.msMatchesSelector(selector) :
            elem.matches(selector);
    }

    /**
     * Returns whether an element is focusable
     * @see http://stackoverflow.com/questions/18261595
     */
    function canFocus( elem ) {
        if (
            isHidden(elem) ||
            matches(elem, ":disabled") ||
            elem.hasAttribute("contenteditable")
        ) {
            return false;
        }
        else {
            return elem.hasAttribute("tabindex") ||
                matches(
                    elem,
                    "input,select,textarea,button,a[href],area[href],iframe"
                );
        }
    }

    /**
     * A small interface for creating and managing a dom element
     */
    function Elem( elem ) {
        this.elem = elem;
    }

    /** Creates a new div */
    Elem.div = function ( parent ) {
        if ( typeof parent === "string" ) {
            parent = document.querySelector(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() {
            this.elem.parentNode.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;
        },

        /** Returns the first descendant that can be focused */
        firstFocusable: function () {
            var items = this.elem.getElementsByTagName("*");
            for (var i = 0; i < items.length; i++) {
                if ( canFocus(items[i]) ) {
                    return items[i];
                }
            }
        },

        /** Returns the last descendant that can be focused */
        lastFocusable: function () {
            var items = this.elem.getElementsByTagName("*");
            for (var i = items.length; i--;) {
                if ( canFocus(items[i]) ) {
                    return items[i];
                }
            }
        },

        /** Sets focus to the first focusable element */
        moveFocus: function () {
            var focusable = this.firstFocusable();
            if ( focusable ) {
                focusable.focus();
            }
        },

        /** Whether this element is visible */
        isVisible: function () {
            return !isHidden(this.elem);
        }
    };

    /** Generates the grey-out effect */
    function buildOverlay( getOption, close ) {
        return Elem.div( getOption("parent") )
            .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( getOption("parent") )
            .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;
        };
    }

    // An observable that is triggered whenever the escape key is pressed
    var escapeKey = observable();

    // An observable that is triggered when the user hits the tab key
    var tabKey = observable();

    /** A global event handler to detect the escape key being pressed */
    document.documentElement.addEventListener(
        'keydown',
        function onKeyPress (event) {
            var keycode = event.which || event.keyCode;

            // If this is the escape key
            if ( keycode === 27 ) {
                escapeKey.trigger();
            }

            // If this is the tab key
            else if ( keycode === 9 ) {
                tabKey.trigger(event);
            }
        }
    );

    /**
     * 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),

            /** Returns whether this modal is currently being shown */
            isVisible: function () {
                return modalElem().isVisible();
            },

            /** 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)
        };

        // Records the currently focused element so state can be returned
        // after the modal closes
        if ( getOption("focus", true) ) {
            var focused;

            iface.afterShow(function focusModal() {
                focused = document.activeElement;
                modalElem().moveFocus();
            });

            iface.afterClose(function returnFocus() {
                // Restore the previously focused element when the modal closes
                focused.focus();
                focused = null;
            });

            // Capture tab key presses and loop them within the modal
            tabKey.watch(function tabKeyPress (event) {
                if ( !modalElem().isVisible() ) {
                    return false;
                }

                var first = modalElem().firstFocusable();
                var last = modalElem().lastFocusable();

                var from = event.shiftKey ? first : last;
                if ( from === document.activeElement ) {
                    (event.shiftKey ? last : first).focus();
                    event.preventDefault();
                }
            });
        }

        // If a user presses the 'escape' key, close the modal.
        // TODO: This is defaulting to false right now, but should default to
        //       `true` in the future. However, that would require a major
        //       version bump as it would allow modals to be closed that
        //       previously may have been locked open.
        if ( getOption("escCloses", false) ) {
            escapeKey.watch(function escapeKeyPress () {
                if ( iface.isVisible() ) {
                    iface.close();
                }
            });
        }

        return iface;
    };

}));
<|endoftext|>


<commit_before>/* eslint-env jquery */
'use strict';

/**
 * 화면단 UI와 연동 되는 API. APIGateWay 혹은 API 클래스로 클래스명 수정 예정
 * @class MagoFacade
 */
/**
 * mago3d 활성화/비활성화
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeMagoStateAPI(isShow)
{
	var api = new API("changeMagoState");
	api.setMagoEnable(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * render mode
 *
 * @param {Property} renderMode 0 = 호선, 1 = 지번전개
 */
function changeRenderAPI(renderMode)
{
	var api = new API("changeRender");
	api.setRenderMode(renderMode);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * outfitting 표시/비표시
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeOutFittingAPI(isShow)
{
	var api = new API("changeOutFitting");
	api.setShowOutFitting(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * Label 표시/비표시
 *
 * @param {Property} isShow true = 표시, false = 비표시
 */
function changeLabelAPI(isShow)
{
	var api = new API("changeLabel");
	api.setShowLabelInfo(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * boundingBox 표시/비표시
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeBoundingBoxAPI(isShow)
{
	var api = new API("changeBoundingBox");
	api.setShowBoundingBox(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 그림자 표시/비표시
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeShadowAPI(isShow)
{
	var api = new API("changeShadow");
	api.setShowShadow(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * frustum culling 가시 거리
 *
 * @param {Property} frustumFarDistance frustum 거리. 내부적으로는 입력값의 제곱이 사용됨
 */
function changeFrustumFarDistanceAPI(frustumFarDistance)
{
	var api = new API("changefrustumFarDistance");
	api.setFrustumFarDistance(frustumFarDistance);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * highlighting
 *
 * @param {Property} projectId 프로젝트 아이디
 * @param {Property} blockIds block id. 복수개의 경우 , 로 입력
 * @param {Property} objectIds object id. 복수개의 경우 , 로 입력
 */
function changeHighLightingAPI(projectId, blockIds, objectIds)
{
	var api = new API("changeHighLighting");
	api.setProjectId(projectId);
	api.setBlockIds(blockIds);
	api.setObjectIds(objectIds);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * color 변경
 *
 * @param {Property} projectId 프로젝트 아이디
 * @param {Property} blockIds block id. 복수개의 경우 , 로 입력
 * @param {Property} objectIds object id. 복수개의 경우 , 로 입력
 * @param {Property} color R, G, B 색깔을 ',' 로 연결한 string 값을 받음.
 */
function changeColorAPI(projectId, blockIds, objectIds, color)
{
	var api = new API("changeColor");
	api.setProjectId(projectId);
	api.setBlockIds(blockIds);
	api.setObjectIds(objectIds);
	api.setColor(color);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * location and rotation 변경
 *
 * @param {Property} data_key
 * @param {Property} latitude 위도
 * @param {Property} longitude 경도
 * @param {Property} height 높이
 * @param {Property} heading 좌, 우
 * @param {Property} pitch 위, 아래
 * @param {Property} roll 좌, 우 기울기
 */
function changeLocationAndRotationAPI(data_key, latitude, longitude, height, heading, pitch, roll)
{
	var api = new API("changeLocationAndRotation");
	api.setDataKey(data_key);
	api.setLatitude(latitude);
	api.setLongitude(longitude);
	api.setElevation(height);
	api.setHeading(heading);
	api.setPitch(pitch);
	api.setRoll(roll);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 마우스 클릭 객체 이동 대상 변경
 *
 * @param {Property} mouseMoveMode 0 = All, 1 = object, 2 = None
 */
function changeMouseMoveAPI(mouseMoveMode)
{
	var api = new API("changeMouseMove");
	api.setMouseMoveMode(mouseMoveMode);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 이슈 등록 활성화 유무
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeInsertIssueModeAPI(flag)
{
	var api = new API("changeInsertIssueMode");
	api.setIssueInsertEnable(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * object 정보 표시 활성화 유무
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeObjectInfoViewModeAPI(flag)
{
	var api = new API("changeObjectInfoViewMode");
	api.setObjectInfoViewEnable(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * Object Occlusion culling
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 * @param {Property} dataKey
 */
function changeOcclusionCullingAPI(flag, dataKey)
{
	var api = new API("changeOcclusionCulling");
	api.setOcclusionCullingEnable(flag);
	api.setDataKey(dataKey);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 1인칭, 3인칭 모드 개발중...
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeFPVModeAPI(flag)
{
	var api = new API("changeFPVMode");
	api.setFPVMode(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 현재 위치 근처 issue list. false인 경우 clear
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeNearGeoIssueListViewModeAPI(flag)
{
	var api = new API("changeNearGeoIssueListViewMode");
	api.setNearGeoIssueListEnable(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * TODO 이건 위에 이슈 등록 활성화, 비활성화 api로 통합이 가능할거 같음
 * issue 등록 geo 정보 관련 상태 변경
 *
 * @param {Property} insertIssueState 이슈 등록 좌표 상태
 */
function changeInsertIssueStateAPI(insertIssueState)
{
	var api = new API("changeInsertIssueState");
	api.setInsertIssueState(insertIssueState);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * LOD 설정을 변경
 * @param {Property} lod0DistInMeters
 * @param {Property} lod1DistInMeters
 * @param {Property} lod2DistInMeters
 * @param {Property} lod3DistInMeters
 */
function changeLodAPI(lod0DistInMeters, lod1DistInMeters, lod2DistInMeters, lod3DistInMeters)
{
	var api = new API("changeLod");
	api.setLod0DistInMeters(lod0DistInMeters);
	api.setLod1DistInMeters(lod1DistInMeters);
	api.setLod2DistInMeters(lod2DistInMeters);
	api.setLod3DistInMeters(lod3DistInMeters);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * Lighting 설정
 * @param {Property} ambientReflectionCoef
 * @param {Property} diffuseReflectionCoef
 * @param {Property} specularReflectionCoef
 * @param {Property} ambientColor
 * @param {Property} specularColor
 */
function changeLightingAPI(ambientReflectionCoef, diffuseReflectionCoef, specularReflectionCoef, ambientColor, specularColor)
{
	var api = new API("changeLighting");
	api.setAmbientReflectionCoef(ambientReflectionCoef);
	api.setDiffuseReflectionCoef(diffuseReflectionCoef);
	api.setSpecularReflectionCoef(specularReflectionCoef);
	api.setAmbientColor(ambientColor);
	api.setSpecularColor(specularColor);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * SSAD Radius 설정
 * @param {Property} ssaoRadius
 */
function changeSsadRadiusAPI(ssaoRadius)
{
	var api = new API("changeSsadRadius");
	api.setSsaoRadius(ssaoRadius);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * pin image를 그림
 *
 * @param {Property} drawType 이미지를 그리는 유형 0 : DB, 1 : 이슈등록
 * @param {Property} issue_id 이슈 고유키
 * @param {Property} issue_type 이슈 고유키
 * @param {Property} data_key 데이터 고유키
 * @param {Property} latitude 데이터 고유키
 * @param {Property} longitude 데이터 고유키
 * @param {Property} height 데이터 고유키
 */
function drawInsertIssueImageAPI(drawType, issue_id, issue_type, data_key, latitude, longitude, height)
{
	var api = new API("drawInsertIssueImage");
	api.setDrawType(drawType);
	api.setIssueId(issue_id);
	api.setIssueId(issue_type);
	api.setDataKey(data_key);
	api.setLatitude(latitude);
	api.setLongitude(longitude);
	api.setElevation(height);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 마우스를 사용할 수 없는 환경에서 버튼 이벤트로 대체
 * @param {Property} eventType 어떤 마우스 동작을 원하는지를 구분
 */
function mouseMoveAPI(eventType)
{
	if (managerFactory !== null)
	{
		managerFactory.mouseMove(eventType);
	}
}

/**
 * 데이터 검색
 *
 * @param {Property} dataKey 데이터 고유키
 */
function searchDataAPI(dataKey)
{
	var api = new API("searchData");
	api.setDataKey(dataKey);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 데이터를 Rendering
 *
 * @param type new clear 후 새로 그림, append = 추가하여 그림
 * @param dataNameArray target data 이름 배열
 * @param dataUrlArray data를 가져올 url 배열
 * @param
 */
function drawData(type, dataNameArray, dataUrlArray)
{
	if (dataNameArray.length <= 0) { return; }

	var keyMap = new Map();
	var dataCount = 0;
	dataNameArray.forEach(function(dataName, index)
	{
		if (MagoConfig.isDataExist(dataName))
		{
			var data = MagoConfig.getData(dataName);
			keyMap.set(dataName, dataName);
			keyMap.set(CODE.OBJECT_INDEX_FILE_PREFIX + data.data_key, data.data_key);
		}
		else
		{
			console.log("not exist. dataName = " + dataName);
			var objectIndexFilePath = null;
			$.ajax({
				url      : dataUrlArray[index],
				type     : "GET",
				dataType : "json",
				success  : function(serverData)
				{
					MagoConfig.setData(dataName, serverData);
					objectIndexFilePath = serverData.data_key;
					keyMap.set(dataName, dataName);
					keyMap.set(CODE.OBJECT_INDEX_FILE_PREFIX + objectIndexFilePath, objectIndexFilePath);
					managerFactory.loadObjectIndexFile(dataName, objectIndexFilePath);

					// 맨 마지막에는 map에서 지우는 처리를 하러 가자.
					if (type === "new" && dataNameArray.length === (dataCount + 1))
					{
						MagoConfig.clearUnSelectedData(keyMap);
					}
				},
				error: function(request, status, error)
				{
					//alert(JS_MESSAGE["ajax.error.message"]);
					console.log("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);
				}
			});
		}

		dataCount++;
	});
}

<commit_msg>foreach 처리 버그 수정<commit_after>/* eslint-env jquery */
'use strict';

/**
 * 화면단 UI와 연동 되는 API. APIGateWay 혹은 API 클래스로 클래스명 수정 예정
 * @class MagoFacade
 */
/**
 * mago3d 활성화/비활성화
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeMagoStateAPI(isShow)
{
	var api = new API("changeMagoState");
	api.setMagoEnable(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * render mode
 *
 * @param {Property} renderMode 0 = 호선, 1 = 지번전개
 */
function changeRenderAPI(renderMode)
{
	var api = new API("changeRender");
	api.setRenderMode(renderMode);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * outfitting 표시/비표시
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeOutFittingAPI(isShow)
{
	var api = new API("changeOutFitting");
	api.setShowOutFitting(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * Label 표시/비표시
 *
 * @param {Property} isShow true = 표시, false = 비표시
 */
function changeLabelAPI(isShow)
{
	var api = new API("changeLabel");
	api.setShowLabelInfo(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * boundingBox 표시/비표시
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeBoundingBoxAPI(isShow)
{
	var api = new API("changeBoundingBox");
	api.setShowBoundingBox(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 그림자 표시/비표시
 *
 * @param {Property} isShow true = 활성화, false = 비활성화
 */
function changeShadowAPI(isShow)
{
	var api = new API("changeShadow");
	api.setShowShadow(isShow);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * frustum culling 가시 거리
 *
 * @param {Property} frustumFarDistance frustum 거리. 내부적으로는 입력값의 제곱이 사용됨
 */
function changeFrustumFarDistanceAPI(frustumFarDistance)
{
	var api = new API("changefrustumFarDistance");
	api.setFrustumFarDistance(frustumFarDistance);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * highlighting
 *
 * @param {Property} projectId 프로젝트 아이디
 * @param {Property} blockIds block id. 복수개의 경우 , 로 입력
 * @param {Property} objectIds object id. 복수개의 경우 , 로 입력
 */
function changeHighLightingAPI(projectId, blockIds, objectIds)
{
	var api = new API("changeHighLighting");
	api.setProjectId(projectId);
	api.setBlockIds(blockIds);
	api.setObjectIds(objectIds);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * color 변경
 *
 * @param {Property} projectId 프로젝트 아이디
 * @param {Property} blockIds block id. 복수개의 경우 , 로 입력
 * @param {Property} objectIds object id. 복수개의 경우 , 로 입력
 * @param {Property} color R, G, B 색깔을 ',' 로 연결한 string 값을 받음.
 */
function changeColorAPI(projectId, blockIds, objectIds, color)
{
	var api = new API("changeColor");
	api.setProjectId(projectId);
	api.setBlockIds(blockIds);
	api.setObjectIds(objectIds);
	api.setColor(color);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * location and rotation 변경
 *
 * @param {Property} data_key
 * @param {Property} latitude 위도
 * @param {Property} longitude 경도
 * @param {Property} height 높이
 * @param {Property} heading 좌, 우
 * @param {Property} pitch 위, 아래
 * @param {Property} roll 좌, 우 기울기
 */
function changeLocationAndRotationAPI(data_key, latitude, longitude, height, heading, pitch, roll)
{
	var api = new API("changeLocationAndRotation");
	api.setDataKey(data_key);
	api.setLatitude(latitude);
	api.setLongitude(longitude);
	api.setElevation(height);
	api.setHeading(heading);
	api.setPitch(pitch);
	api.setRoll(roll);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 마우스 클릭 객체 이동 대상 변경
 *
 * @param {Property} mouseMoveMode 0 = All, 1 = object, 2 = None
 */
function changeMouseMoveAPI(mouseMoveMode)
{
	var api = new API("changeMouseMove");
	api.setMouseMoveMode(mouseMoveMode);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 이슈 등록 활성화 유무
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeInsertIssueModeAPI(flag)
{
	var api = new API("changeInsertIssueMode");
	api.setIssueInsertEnable(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * object 정보 표시 활성화 유무
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeObjectInfoViewModeAPI(flag)
{
	var api = new API("changeObjectInfoViewMode");
	api.setObjectInfoViewEnable(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * Object Occlusion culling
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 * @param {Property} dataKey
 */
function changeOcclusionCullingAPI(flag, dataKey)
{
	var api = new API("changeOcclusionCulling");
	api.setOcclusionCullingEnable(flag);
	api.setDataKey(dataKey);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 1인칭, 3인칭 모드 개발중...
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeFPVModeAPI(flag)
{
	var api = new API("changeFPVMode");
	api.setFPVMode(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 현재 위치 근처 issue list. false인 경우 clear
 *
 * @param {Property} flag true = 활성화, false = 비활성화
 */
function changeNearGeoIssueListViewModeAPI(flag)
{
	var api = new API("changeNearGeoIssueListViewMode");
	api.setNearGeoIssueListEnable(flag);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * TODO 이건 위에 이슈 등록 활성화, 비활성화 api로 통합이 가능할거 같음
 * issue 등록 geo 정보 관련 상태 변경
 *
 * @param {Property} insertIssueState 이슈 등록 좌표 상태
 */
function changeInsertIssueStateAPI(insertIssueState)
{
	var api = new API("changeInsertIssueState");
	api.setInsertIssueState(insertIssueState);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * LOD 설정을 변경
 * @param {Property} lod0DistInMeters
 * @param {Property} lod1DistInMeters
 * @param {Property} lod2DistInMeters
 * @param {Property} lod3DistInMeters
 */
function changeLodAPI(lod0DistInMeters, lod1DistInMeters, lod2DistInMeters, lod3DistInMeters)
{
	var api = new API("changeLod");
	api.setLod0DistInMeters(lod0DistInMeters);
	api.setLod1DistInMeters(lod1DistInMeters);
	api.setLod2DistInMeters(lod2DistInMeters);
	api.setLod3DistInMeters(lod3DistInMeters);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * Lighting 설정
 * @param {Property} ambientReflectionCoef
 * @param {Property} diffuseReflectionCoef
 * @param {Property} specularReflectionCoef
 * @param {Property} ambientColor
 * @param {Property} specularColor
 */
function changeLightingAPI(ambientReflectionCoef, diffuseReflectionCoef, specularReflectionCoef, ambientColor, specularColor)
{
	var api = new API("changeLighting");
	api.setAmbientReflectionCoef(ambientReflectionCoef);
	api.setDiffuseReflectionCoef(diffuseReflectionCoef);
	api.setSpecularReflectionCoef(specularReflectionCoef);
	api.setAmbientColor(ambientColor);
	api.setSpecularColor(specularColor);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * SSAD Radius 설정
 * @param {Property} ssaoRadius
 */
function changeSsadRadiusAPI(ssaoRadius)
{
	var api = new API("changeSsadRadius");
	api.setSsaoRadius(ssaoRadius);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * pin image를 그림
 *
 * @param {Property} drawType 이미지를 그리는 유형 0 : DB, 1 : 이슈등록
 * @param {Property} issue_id 이슈 고유키
 * @param {Property} issue_type 이슈 고유키
 * @param {Property} data_key 데이터 고유키
 * @param {Property} latitude 데이터 고유키
 * @param {Property} longitude 데이터 고유키
 * @param {Property} height 데이터 고유키
 */
function drawInsertIssueImageAPI(drawType, issue_id, issue_type, data_key, latitude, longitude, height)
{
	var api = new API("drawInsertIssueImage");
	api.setDrawType(drawType);
	api.setIssueId(issue_id);
	api.setIssueId(issue_type);
	api.setDataKey(data_key);
	api.setLatitude(latitude);
	api.setLongitude(longitude);
	api.setElevation(height);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 마우스를 사용할 수 없는 환경에서 버튼 이벤트로 대체
 * @param {Property} eventType 어떤 마우스 동작을 원하는지를 구분
 */
function mouseMoveAPI(eventType)
{
	if (managerFactory !== null)
	{
		managerFactory.mouseMove(eventType);
	}
}

/**
 * 데이터 검색
 *
 * @param {Property} dataKey 데이터 고유키
 */
function searchDataAPI(dataKey)
{
	var api = new API("searchData");
	api.setDataKey(dataKey);
	if (managerFactory !== null)
	{
		managerFactory.callAPI(api);
	}
}

/**
 * 데이터를 Rendering
 *
 * @param type new clear 후 새로 그림, append = 추가하여 그림
 * @param dataNameArray target data 이름 배열
 * @param dataUrlArray data를 가져올 url 배열
 * @param
 */
function drawData(type, dataNameArray, dataUrlArray)
{
	if (dataNameArray.length <= 0) { return; }

	var keyMap = new Map();
	var dataCount = 0;
	dataNameArray.forEach(function(dataName, index)
	{
		if (MagoConfig.isDataExist(dataName))
		{
			var data = MagoConfig.getData(dataName);
			keyMap.set(dataName, dataName);
			keyMap.set(CODE.OBJECT_INDEX_FILE_PREFIX + data.data_key, data.data_key);
			// 맨 마지막에는 map에서 지우는 처리를 하러 가자.
			if (type === "new" && dataNameArray.length === (dataCount + 1))
			{
				MagoConfig.clearUnSelectedData(keyMap);
			}
		}
		else
		{
			console.log("not exist. dataName = " + dataName);
			var objectIndexFilePath = null;
			$.ajax({
				url      : dataUrlArray[index],
				type     : "GET",
				dataType : "json",
				success  : function(serverData)
				{
					MagoConfig.setData(dataName, serverData);
					objectIndexFilePath = serverData.data_key;
					keyMap.set(dataName, dataName);
					keyMap.set(CODE.OBJECT_INDEX_FILE_PREFIX + objectIndexFilePath, objectIndexFilePath);
					managerFactory.loadObjectIndexFile(dataName, objectIndexFilePath);

					// 맨 마지막에는 map에서 지우는 처리를 하러 가자.
					if (type === "new" && dataNameArray.length === (dataCount + 1))
					{
						MagoConfig.clearUnSelectedData(keyMap);
					}
				},
				error: function(request, status, error)
				{
					//alert(JS_MESSAGE["ajax.error.message"]);
					console.log("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);
				}
			});
		}

		dataCount++;
	});
}

<|endoftext|>


<commit_before>/*
 * Copyright (C) 2013 Google Inc., authors, and contributors <see AUTHORS file>
 * Licensed under http://www.apache.org/licenses/LICENSE-2.0 <see LICENSE file>
 * Created By: dan@reciprocitylabs.com
 * Maintained By: dan@reciprocitylabs.com
 */

(function($, CMS, GGRC) {
  var RiskAssessmentsExtension = {},
      _risk_assessments_object_types = ["Program"];
  GGRC.extensions.push(RiskAssessmentsExtension);

  RiskAssessmentsExtension.name = "risk_assessments";

  // Register RA models for use with `infer_object_type`
  RiskAssessmentsExtension.object_type_decision_tree = function() {
    return {
      "risk_assessment": CMS.Models.RiskAssessment
    };
  };

  CMS.Models.Program.attributes.risk_assessments = "CMS.Models.RiskAssessment.stubs";

  // Configure mapping extensions for ggrc_risk_assessments
  RiskAssessmentsExtension.init_mappings = function init_mappings() {
    var Proxy = GGRC.MapperHelpers.Proxy,
        Direct = GGRC.MapperHelpers.Direct,
        Cross = GGRC.MapperHelpers.Cross,
        Multi = GGRC.MapperHelpers.Multi,
        CustomFilter = GGRC.MapperHelpers.CustomFilter,
        Reify = GGRC.MapperHelpers.Reify,
        Search = GGRC.MapperHelpers.Search;

        var mappings = {
          Program: {
            _canonical: {
              risk_assessments: "RiskAssessment"
            },
            risk_assessments: Direct(
              "RiskAssessment", "program", "risk_assessments"),
          },
          RiskAssessment: {
            documents: Proxy(
              "Document", "document", "ObjectDocument", "documentable", "object_documents"),
          },
        };
        new GGRC.Mappings("ggrc_risk_assessments", mappings);
  };

  // Override GGRC.extra_widget_descriptors and GGRC.extra_default_widgets
  // Initialize widgets for risk assessment page
  RiskAssessmentsExtension.init_widgets = function init_widgets() {
    var descriptor = {},
        page_instance = GGRC.page_instance(),
        tree_widgets = GGRC.tree_view.base_widgets_by_type;

    _.each(_risk_assessments_object_types, function (type) {
      if (!type || !tree_widgets[type]) {
        return;
      }
      tree_widgets[type] = tree_widgets[type].concat(["RiskAssessment"]);
    });
    if (page_instance && ~can.inArray(page_instance.constructor.shortName, _risk_assessments_object_types)) {
      descriptor[page_instance.constructor.shortName] = {
        risk_assessments: {
          widget_id: "risk_assessments",
          widget_name: "Risk Assessments",
          content_controller: GGRC.Controllers.TreeView,
          content_controller_options: {
            mapping: "risk_assessments",
            parent_instance: page_instance,
            model: CMS.Models.RiskAssessment,
            show_view: GGRC.mustache_path + "/risk_assessments/tree.mustache",
            draw_children: true,
          }
        }
      };
    }
    new GGRC.WidgetList("ggrc_risk_assessments", descriptor);
  };

  RiskAssessmentsExtension.init_mappings();

})(this.can.$, this.CMS, this.GGRC);
<commit_msg>Hide Documents in Risk Assessment subtree<commit_after>/*
 * Copyright (C) 2013 Google Inc., authors, and contributors <see AUTHORS file>
 * Licensed under http://www.apache.org/licenses/LICENSE-2.0 <see LICENSE file>
 * Created By: dan@reciprocitylabs.com
 * Maintained By: dan@reciprocitylabs.com
 */

(function($, CMS, GGRC) {
  var RiskAssessmentsExtension = {},
      _risk_assessments_object_types = ["Program"];
  GGRC.extensions.push(RiskAssessmentsExtension);

  RiskAssessmentsExtension.name = "risk_assessments";

  // Register RA models for use with `infer_object_type`
  RiskAssessmentsExtension.object_type_decision_tree = function() {
    return {
      "risk_assessment": CMS.Models.RiskAssessment
    };
  };

  CMS.Models.Program.attributes.risk_assessments = "CMS.Models.RiskAssessment.stubs";

  // Configure mapping extensions for ggrc_risk_assessments
  RiskAssessmentsExtension.init_mappings = function init_mappings() {
    var Proxy = GGRC.MapperHelpers.Proxy,
        Direct = GGRC.MapperHelpers.Direct,
        Cross = GGRC.MapperHelpers.Cross,
        Multi = GGRC.MapperHelpers.Multi,
        CustomFilter = GGRC.MapperHelpers.CustomFilter,
        Reify = GGRC.MapperHelpers.Reify,
        Search = GGRC.MapperHelpers.Search;

        var mappings = {
          Program: {
            _canonical: {
              risk_assessments: "RiskAssessment"
            },
            risk_assessments: Direct(
              "RiskAssessment", "program", "risk_assessments"),
          },
          RiskAssessment: {
            documents: Proxy(
              "Document", "document", "ObjectDocument", "documentable", "object_documents"),
          },
        };
        new GGRC.Mappings("ggrc_risk_assessments", mappings);
  };

  // Override GGRC.extra_widget_descriptors and GGRC.extra_default_widgets
  // Initialize widgets for risk assessment page
  RiskAssessmentsExtension.init_widgets = function init_widgets() {
    var descriptor = {},
        page_instance = GGRC.page_instance(),
        tree_widgets = GGRC.tree_view.base_widgets_by_type;

    _.each(_risk_assessments_object_types, function (type) {
      if (!type || !tree_widgets[type]) {
        return;
      }
      tree_widgets[type] = tree_widgets[type].concat(["RiskAssessment"]);
    });
    if (page_instance && ~can.inArray(page_instance.constructor.shortName, _risk_assessments_object_types)) {
      descriptor[page_instance.constructor.shortName] = {
        risk_assessments: {
          widget_id: "risk_assessments",
          widget_name: "Risk Assessments",
          content_controller: GGRC.Controllers.TreeView,
          content_controller_options: {
            mapping: "risk_assessments",
            parent_instance: page_instance,
            model: CMS.Models.RiskAssessment,
            show_view: GGRC.mustache_path + "/risk_assessments/tree.mustache",
            draw_children: false,
          }
        }
      };
    }
    new GGRC.WidgetList("ggrc_risk_assessments", descriptor);
  };

  RiskAssessmentsExtension.init_mappings();

})(this.can.$, this.CMS, this.GGRC);
<|endoftext|>


<commit_before>/**
 * @file In order to load huge volumes of polygon data into memory without
 * breaking Node (due to its low heap-size limits), the package forks a
 * worker process per polygon layer/shapefile. This module contains
 * functions for initializing them and searching them.
 */

'use strict';

const path = require('path');
const childProcess = require( 'child_process' );
const logger = require( 'pelias-logger' ).get( 'wof-pip-service:master' );
const async = require('async');
const _ = require('lodash');
const fs = require('fs');

let requestCount = 0;
// worker processes keyed on layer
const workers = {};

const responseQueue = {};
const wofData = {};

const defaultLayers = [
  'neighbourhood',
  'borough',
  'locality',
  'localadmin',
  'county',
  'macrocounty',
  'macroregion',
  'region',
  'dependency',
  'country',
  'continent',
  'marinearea',
  'ocean'
];

module.exports.create = function createPIPService(datapath, layers, localizedAdminNames, callback) {
  // take the intersection to keep order in decreasing granularity
  // ie - _.intersection([1, 2, 3], [3, 1]) === [1, 3]
  layers = _.intersection(defaultLayers, _.isEmpty(layers) ? defaultLayers : layers);

  // load all workers
  async.forEach(layers, (layer, done) => {
      startWorker(datapath, layer, localizedAdminNames, function (err, worker) {
        workers[layer] = worker;
        done();
      });
    },
    function end() {
      logger.info('PIP Service Loading Completed!!!');

      callback(null, {
        end: killAllWorkers,
        lookup: function (latitude, longitude, search_layers, responseCallback) {
          if (search_layers === undefined) {
            search_layers = layers;
          } else {
            // take the intersection of the valid layers and the layers sent in
            // so that if any layers are manually disabled for development
            // everything still works. this also means invalid layers
            // are silently ignored
            search_layers = _.intersection(layers, search_layers);
          }

          if (search_layers.length === 0) {
            return responseCallback(null, []);
          }

          const id = requestCount++;

          if (responseQueue.hasOwnProperty(id)) {
            const msg = `Tried to create responseQueue item with id ${id} that is already present`;
            logger.error(msg);
            return responseCallback(null, []);
          }

          // bookkeeping object that tracks the progress of the request
          responseQueue[id] = {
            latLon: {latitude: latitude, longitude: longitude},
            // copy of layers to search
            search_layers: search_layers.slice(),
            responseCallback: responseCallback
          };

          // start the chain of worker calls
          searchWorker(id);

        }
      });
    }
  );
};

function killAllWorkers() {
  Object.keys(workers).forEach(function (layer) {
    workers[layer].kill();
  });
}

function startWorker(datapath, layer, localizedAdminNames, callback) {
  const worker = childProcess.fork(path.join(__dirname, 'worker'));

  worker.on('message', msg => {
    if (msg.type === 'loaded') {
      const layerSpecificWofData = JSON.parse(fs.readFileSync(`wof-${layer}-data.json`));
      fs.unlinkSync(`wof-${layer}-data.json`);

      logger.info(`${msg.layer} worker loaded ${_.size(layerSpecificWofData)} features in ${msg.seconds} seconds`);

      // add all layer-specific WOF data to the big WOF data
      _.assign(wofData, layerSpecificWofData);
      callback(null, worker);
    }

    if (msg.type === 'results') {
      handleResults(msg);
    }
  });

  worker.send({
    type: 'load',
    layer: layer,
    datapath: datapath,
    localizedAdminNames: localizedAdminNames
  });
}

function searchWorker(id) {
  const worker = workers[responseQueue[id].search_layers.shift()];

  worker.send({
    type: 'search',
    id: id,
    coords: responseQueue[id].latLon
  });
}

function handleResults(msg) {
  if (!responseQueue.hasOwnProperty(msg.id)) {
    logger.error(`tried to handle results for missing id ${msg.id}`);
    return;
  }

  // first, handle the case where there was a miss
  if (_.isEmpty(msg.results)) {
    if (!_.isEmpty(responseQueue[msg.id].search_layers)) {
      // if there are no results, then call the next layer but only if there are more layer to call
      searchWorker(msg.id);
    } else {
      // no layers left to search, so return an empty array
      responseQueue[msg.id].responseCallback(null, []);
    }

  } else {
    // there was a hit, so find the hierachy and assemble all the pieces
    const results = _.compact(msg.results.Hierarchy[0].map(id => wofData[id]));

    responseQueue[msg.id].responseCallback(null, results);

    delete responseQueue[msg.id];
  }

}
<commit_msg>don't delete worker data file when done<commit_after>/**
 * @file In order to load huge volumes of polygon data into memory without
 * breaking Node (due to its low heap-size limits), the package forks a
 * worker process per polygon layer/shapefile. This module contains
 * functions for initializing them and searching them.
 */

'use strict';

const path = require('path');
const childProcess = require( 'child_process' );
const logger = require( 'pelias-logger' ).get( 'wof-pip-service:master' );
const async = require('async');
const _ = require('lodash');
const fs = require('fs');

let requestCount = 0;
// worker processes keyed on layer
const workers = {};

const responseQueue = {};
const wofData = {};

const defaultLayers = [
  'neighbourhood',
  'borough',
  'locality',
  'localadmin',
  'county',
  'macrocounty',
  'macroregion',
  'region',
  'dependency',
  'country',
  'continent',
  'marinearea',
  'ocean'
];

module.exports.create = function createPIPService(datapath, layers, localizedAdminNames, callback) {
  // take the intersection to keep order in decreasing granularity
  // ie - _.intersection([1, 2, 3], [3, 1]) === [1, 3]
  layers = _.intersection(defaultLayers, _.isEmpty(layers) ? defaultLayers : layers);

  // load all workers
  async.forEach(layers, (layer, done) => {
      startWorker(datapath, layer, localizedAdminNames, function (err, worker) {
        workers[layer] = worker;
        done();
      });
    },
    function end() {
      logger.info('PIP Service Loading Completed!!!');

      callback(null, {
        end: killAllWorkers,
        lookup: function (latitude, longitude, search_layers, responseCallback) {
          if (search_layers === undefined) {
            search_layers = layers;
          } else {
            // take the intersection of the valid layers and the layers sent in
            // so that if any layers are manually disabled for development
            // everything still works. this also means invalid layers
            // are silently ignored
            search_layers = _.intersection(layers, search_layers);
          }

          if (search_layers.length === 0) {
            return responseCallback(null, []);
          }

          const id = requestCount++;

          if (responseQueue.hasOwnProperty(id)) {
            const msg = `Tried to create responseQueue item with id ${id} that is already present`;
            logger.error(msg);
            return responseCallback(null, []);
          }

          // bookkeeping object that tracks the progress of the request
          responseQueue[id] = {
            latLon: {latitude: latitude, longitude: longitude},
            // copy of layers to search
            search_layers: search_layers.slice(),
            responseCallback: responseCallback
          };

          // start the chain of worker calls
          searchWorker(id);

        }
      });
    }
  );
};

function killAllWorkers() {
  Object.keys(workers).forEach(function (layer) {
    workers[layer].kill();
  });
}

function startWorker(datapath, layer, localizedAdminNames, callback) {
  const worker = childProcess.fork(path.join(__dirname, 'worker'));

  worker.on('message', msg => {
    if (msg.type === 'loaded') {
      const layerSpecificWofData = JSON.parse(fs.readFileSync(`wof-${layer}-data.json`));

      logger.info(`${msg.layer} worker loaded ${_.size(layerSpecificWofData)} features in ${msg.seconds} seconds`);

      // add all layer-specific WOF data to the big WOF data
      _.assign(wofData, layerSpecificWofData);
      callback(null, worker);
    }

    if (msg.type === 'results') {
      handleResults(msg);
    }
  });

  worker.send({
    type: 'load',
    layer: layer,
    datapath: datapath,
    localizedAdminNames: localizedAdminNames
  });
}

function searchWorker(id) {
  const worker = workers[responseQueue[id].search_layers.shift()];

  worker.send({
    type: 'search',
    id: id,
    coords: responseQueue[id].latLon
  });
}

function handleResults(msg) {
  if (!responseQueue.hasOwnProperty(msg.id)) {
    logger.error(`tried to handle results for missing id ${msg.id}`);
    return;
  }

  // first, handle the case where there was a miss
  if (_.isEmpty(msg.results)) {
    if (!_.isEmpty(responseQueue[msg.id].search_layers)) {
      // if there are no results, then call the next layer but only if there are more layer to call
      searchWorker(msg.id);
    } else {
      // no layers left to search, so return an empty array
      responseQueue[msg.id].responseCallback(null, []);
    }

  } else {
    // there was a hit, so find the hierachy and assemble all the pieces
    const results = _.compact(msg.results.Hierarchy[0].map(id => wofData[id]));

    responseQueue[msg.id].responseCallback(null, results);

    delete responseQueue[msg.id];
  }

}
<|endoftext|>


<commit_before>
// setup
Elm.Native = Elm.Native || {};
Elm.Native.Graphics = Elm.Native.Graphics || {};
Elm.Native.Graphics.Collage = Elm.Native.Graphics.Collage || {};

// definition
Elm.Native.Graphics.Collage.make = function(localRuntime) {
    'use strict';

    // attempt to short-circuit
    localRuntime.Native = localRuntime.Native || {};
    localRuntime.Native.Graphics = localRuntime.Native.Graphics || {};
    localRuntime.Native.Graphics.Collage = localRuntime.Native.Graphics.Collage || {};
    if ('values' in localRuntime.Native.Graphics.Collage) {
        return localRuntime.Native.Graphics.Collage.values;
    }

    // okay, we cannot short-ciruit, so now we define everything
    var Color = Elm.Native.Color.make(localRuntime);
    var List = Elm.Native.List.make(localRuntime);
    var Transform = Elm.Transform2D.make(localRuntime);

    var Element = Elm.Graphics.Element.make(localRuntime);
    var NativeElement = Elm.Native.Graphics.Element.make(localRuntime);

    function trace(ctx, path) {
        var points = List.toArray(path);
        var i = points.length - 1;
        if (i <= 0) {
            return;
        }
        ctx.moveTo(points[i]._0, points[i]._1);
        while (i--) {
            ctx.lineTo(points[i]._0, points[i]._1);
        }
        if (path.closed) {
            i = points.length - 1;
            ctx.lineTo(points[i]._0, points[i]._1);
        }
    }

    function line(ctx,style,path) {
        (style.dashing.ctor === '[]')
            ? trace(ctx, path)
            : customLineHelp(ctx, style, path);
        ctx.scale(1,-1);
        ctx.stroke();
    }

    function customLineHelp(ctx, style, path) {
        var points = List.toArray(path);
        if (path.closed) {
            points.push(points[0]);
        }
        var pattern = List.toArray(style.dashing);
        var i = points.length - 1;
        if (i <= 0) {
            return;
        }
        var x0 = points[i]._0, y0 = points[i]._1;
        var x1=0, y1=0, dx=0, dy=0, remaining=0, nx=0, ny=0;
        var pindex = 0, plen = pattern.length;
        var draw = true, segmentLength = pattern[0];
        ctx.moveTo(x0,y0);
        while (i--) {
            x1 = points[i]._0; y1 = points[i]._1;
            dx = x1 - x0; dy = y1 - y0;
            remaining = Math.sqrt(dx * dx + dy * dy);
            while (segmentLength <= remaining) {
                x0 += dx * segmentLength / remaining;
                y0 += dy * segmentLength / remaining;
                ctx[draw ? 'lineTo' : 'moveTo'](x0, y0);
                // update starting position
                dx = x1 - x0; dy = y1 - y0;
                remaining = Math.sqrt(dx * dx + dy * dy);
                // update pattern
                draw = !draw;
                pindex = (pindex + 1) % plen;
                segmentLength = pattern[pindex];
            }
            if (remaining > 0) {
                ctx[draw ? 'lineTo' : 'moveTo'](x1, y1);
                segmentLength -= remaining;
            }
            x0 = x1; y0 = y1;
        }
    }

    function drawLine(ctx, style, path) {
        ctx.lineWidth = style.width;

        var cap = style.cap.ctor;
        ctx.lineCap = cap === 'Flat'
            ? 'butt'
            : cap === 'Round'
                ? 'round'
                : 'square';

        var join = style.join.ctor;
        ctx.lineJoin = join === 'Smooth'
            ? 'round'
            : join === 'Sharp'
                ? 'miter'
                : 'bevel';

        ctx.miterLimit = style.join._0 || 10;
        ctx.strokeStyle = Color.toCss(style.color);

        return line(ctx, style, path);
    }

    function texture(redo, ctx, src) {
        var img = new Image();
        img.src = src;
        img.onload = redo;
        return ctx.createPattern(img, 'repeat');
    }

    function gradient(ctx, grad) {
        var g;
        var stops = [];
        if (grad.ctor === 'Linear') {
            var p0 = grad._0, p1 = grad._1;
            g = ctx.createLinearGradient(p0._0, -p0._1, p1._0, -p1._1);
            stops = List.toArray(grad._2);
        } else {
            var p0 = grad._0, p2 = grad._2;
            g = ctx.createRadialGradient(p0._0, -p0._1, grad._1, p2._0, -p2._1, grad._3);
            stops = List.toArray(grad._4);
        }
        var len = stops.length;
        for (var i = 0; i < len; ++i) {
            var stop = stops[i];
            g.addColorStop(stop._0, Color.toCss(stop._1));
        }
        return g;
    }

    function drawShape(redo, ctx, style, path) {
        trace(ctx, path);
        var sty = style.ctor;
        ctx.fillStyle = sty === 'Solid'
            ? Color.toCss(style._0)
            : sty === 'Texture'
                ? texture(redo, ctx, style._0)
                : gradient(ctx, style._0);

        ctx.scale(1,-1);
        ctx.fill();
    }

    function drawImage(redo, ctx, form) {
        var img = new Image();
        img.onload = redo;
        img.src = form._3;
        var w = form._0,
            h = form._1,
            pos = form._2,
            srcX = pos._0,
            srcY = pos._1,
            srcW = w,
            srcH = h,
            destX = -w/2,
            destY = -h/2,
            destW = w,
            destH = h;

        ctx.scale(1,-1);
        ctx.drawImage(img, srcX, srcY, srcW, srcH, destX, destY, destW, destH);
    }

    function renderForm(redo, ctx, form) {
        ctx.save();
        var x = form.x, y = form.y, theta = form.theta, scale = form.scale;
        if (x !== 0 || y !== 0) ctx.translate(x, y);
        if (theta !== 0) ctx.rotate(theta);
        if (scale !== 1) ctx.scale(scale,scale);
        if (form.alpha !== 1) ctx.globalAlpha = ctx.globalAlpha * form.alpha;
        ctx.beginPath();
        var f = form.form;
        switch(f.ctor) {
        case 'FPath' : drawLine(ctx, f._0, f._1); break;
        case 'FImage': drawImage(redo, ctx, f); break;
        case 'FShape':
          if (f._0.ctor === 'Line') {
            f._1.closed = true;
            drawLine(ctx, f._0._0, f._1);
          } else {
            drawShape(redo, ctx, f._0._0, f._1);
          }
        break;
        }
        ctx.restore();
    }

    function formToMatrix(form) {
       var scale = form.scale;
       var matrix = A6( Transform.matrix, scale, 0, 0, scale, form.x, form.y );

       var theta = form.theta
       if (theta !== 0) {
           matrix = A2( Transform.multiply, matrix, Transform.rotation(theta) );
       }

       return matrix;
    }

    function str(n) {
        if (n < 0.00001 && n > -0.00001) return 0;
        return n;
    }

    function makeTransform(w, h, form, matrices) {
        var props = form.form._0.props;
        var m = A6( Transform.matrix, 1, 0, 0, -1,
                    (w - props.width ) / 2,
                    (h - props.height) / 2 );
        var len = matrices.length;
        for (var i = 0; i < len; ++i) {
            m = A2( Transform.multiply, m, matrices[i] );
        }
        m = A2( Transform.multiply, m, formToMatrix(form) );

        return 'matrix(' + str( m[0]) + ', ' + str( m[3]) + ', ' +
                           str(-m[1]) + ', ' + str(-m[4]) + ', ' +
                           str( m[2]) + ', ' + str( m[5]) + ')';
    }

    function stepperHelp(list) {
        var arr = List.toArray(list);
        var i = 0;
        function peekNext() {
            return i < arr.length ? arr[i].form.ctor : '';
        }
        // assumes that there is a next element
        function next() {
            var out = arr[i];
            ++i;
            return out;
        }
        return {
            peekNext:peekNext,
            next:next
        };
    }

    function formStepper(forms) {
        var ps = [stepperHelp(forms)];
        var matrices = [];
        var alphas = [];
        function peekNext() {
            var len = ps.length;
            var formType = '';
            for (var i = 0; i < len; ++i ) {
                if (formType = ps[i].peekNext()) return formType;
            }
            return '';
        }
        // assumes that there is a next element
        function next(ctx) {
            while (!ps[0].peekNext()) {
                ps.shift();
                matrices.pop();
                alphas.shift();
                if (ctx) { ctx.restore(); }
            }
            var out = ps[0].next();
            var f = out.form;
            if (f.ctor === 'FGroup') {
                ps.unshift(stepperHelp(f._1));
                var m = A2(Transform.multiply, f._0, formToMatrix(out));
                ctx.save();
                ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5]);
                matrices.push(m);

                var alpha = (alphas[0] || 1) * out.alpha;
                alphas.unshift(alpha);
                ctx.globalAlpha = alpha;
            }
            return out;
        }
        function transforms() { return matrices; }
        function alpha() { return alphas[0] || 1; }
        return {
            peekNext:peekNext,
            next:next,
            transforms:transforms,
            alpha:alpha
        };
    }

    function makeCanvas(w,h) {
        var canvas = NativeElement.createNode('canvas');
        canvas.style.width  = w + 'px';
        canvas.style.height = h + 'px';
        canvas.style.display = "block";
        canvas.style.position = "absolute";
        canvas.width  = w;
        canvas.height = h;
        return canvas;
    }

    function render(model) {
        var div = NativeElement.createNode('div');
        div.style.overflow = 'hidden';
        div.style.position = 'relative';
        update(div, model, model);
        return div;
    }

    function nodeStepper(w,h,div) {
        var kids = div.childNodes;
        var i = 0;
        function transform(transforms, ctx) {
            ctx.translate(w/2, h/2);
            ctx.scale(1,-1);
            var len = transforms.length;
            for (var i = 0; i < len; ++i) {
                var m = transforms[i];
                ctx.save();
                ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5]);
            }
            return ctx;
        }
        function nextContext(transforms) {
            while (i < kids.length) {
                var node = kids[i];
                if (node.getContext) {
                    node.width = w;
                    node.height = h;
                    node.style.width = w + 'px';
                    node.style.height = h + 'px';
                    ++i;
                    return transform(transforms, node.getContext('2d'));
                }
                div.removeChild(node);
            }
            var canvas = makeCanvas(w,h);
            div.appendChild(canvas);
            // we have added a new node, so we must step our position
            ++i;
            return transform(transforms, canvas.getContext('2d'));
        }
        function addElement(matrices, alpha, form) {
            var kid = kids[i];
            var elem = form.form._0;

            var node = (!kid || kid.getContext)
                ? NativeElement.render(elem)
                : NativeElement.update(kid, kid.oldElement, elem);

            node.style.position = 'absolute';
            node.style.opacity = alpha * form.alpha * elem.props.opacity;
            NativeElement.addTransform(node.style, makeTransform(w, h, form, matrices));
            node.oldElement = elem;
            ++i;
            if (!kid) {
                div.appendChild(node);
            } else if (kid.getContext) {
                div.insertBefore(node, kid);
            }
        }
        function clearRest() {
            while (i < kids.length) {
                div.removeChild(kids[i]);
            }
        }
        return { nextContext:nextContext, addElement:addElement, clearRest:clearRest };
    }

    function update(div, _, model) {
        var w = model.w;
        var h = model.h;

        var forms = formStepper(model.forms);
        var nodes = nodeStepper(w,h,div);
        var ctx = null;
        var formType = '';

        while (formType = forms.peekNext()) {
            // make sure we have context if we need it
            if (ctx === null && formType !== 'FElement') {
                ctx = nodes.nextContext(forms.transforms());
                ctx.globalAlpha = forms.alpha();
            }

            var form = forms.next(ctx);
            // if it is FGroup, all updates are made within formStepper when next is called.
            if (formType === 'FElement') {
                // update or insert an element, get a new context
                nodes.addElement(forms.transforms(), forms.alpha(), form);
                ctx = null;
            } else if (formType !== 'FGroup') {
                renderForm(function() { update(div, model, model); }, ctx, form);
            }
        }
        nodes.clearRest();
        return div;
    }

    function collage(w,h,forms) {
        return A3(Element.newElement, w, h, {
            ctor: 'Custom',
            type: 'Collage',
            render: render,
            update: update,
            model: {w:w, h:h, forms:forms}
      	});
    }

    return localRuntime.Native.Graphics.Collage.values = {
        collage:F3(collage)
    };
};<commit_msg>Fix disappearing HTML elements, Fixes #862<commit_after>
// setup
Elm.Native = Elm.Native || {};
Elm.Native.Graphics = Elm.Native.Graphics || {};
Elm.Native.Graphics.Collage = Elm.Native.Graphics.Collage || {};

// definition
Elm.Native.Graphics.Collage.make = function(localRuntime) {
    'use strict';

    // attempt to short-circuit
    localRuntime.Native = localRuntime.Native || {};
    localRuntime.Native.Graphics = localRuntime.Native.Graphics || {};
    localRuntime.Native.Graphics.Collage = localRuntime.Native.Graphics.Collage || {};
    if ('values' in localRuntime.Native.Graphics.Collage) {
        return localRuntime.Native.Graphics.Collage.values;
    }

    // okay, we cannot short-ciruit, so now we define everything
    var Color = Elm.Native.Color.make(localRuntime);
    var List = Elm.Native.List.make(localRuntime);
    var Transform = Elm.Transform2D.make(localRuntime);

    var Element = Elm.Graphics.Element.make(localRuntime);
    var NativeElement = Elm.Native.Graphics.Element.make(localRuntime);

    function trace(ctx, path) {
        var points = List.toArray(path);
        var i = points.length - 1;
        if (i <= 0) {
            return;
        }
        ctx.moveTo(points[i]._0, points[i]._1);
        while (i--) {
            ctx.lineTo(points[i]._0, points[i]._1);
        }
        if (path.closed) {
            i = points.length - 1;
            ctx.lineTo(points[i]._0, points[i]._1);
        }
    }

    function line(ctx,style,path) {
        (style.dashing.ctor === '[]')
            ? trace(ctx, path)
            : customLineHelp(ctx, style, path);
        ctx.scale(1,-1);
        ctx.stroke();
    }

    function customLineHelp(ctx, style, path) {
        var points = List.toArray(path);
        if (path.closed) {
            points.push(points[0]);
        }
        var pattern = List.toArray(style.dashing);
        var i = points.length - 1;
        if (i <= 0) {
            return;
        }
        var x0 = points[i]._0, y0 = points[i]._1;
        var x1=0, y1=0, dx=0, dy=0, remaining=0, nx=0, ny=0;
        var pindex = 0, plen = pattern.length;
        var draw = true, segmentLength = pattern[0];
        ctx.moveTo(x0,y0);
        while (i--) {
            x1 = points[i]._0; y1 = points[i]._1;
            dx = x1 - x0; dy = y1 - y0;
            remaining = Math.sqrt(dx * dx + dy * dy);
            while (segmentLength <= remaining) {
                x0 += dx * segmentLength / remaining;
                y0 += dy * segmentLength / remaining;
                ctx[draw ? 'lineTo' : 'moveTo'](x0, y0);
                // update starting position
                dx = x1 - x0; dy = y1 - y0;
                remaining = Math.sqrt(dx * dx + dy * dy);
                // update pattern
                draw = !draw;
                pindex = (pindex + 1) % plen;
                segmentLength = pattern[pindex];
            }
            if (remaining > 0) {
                ctx[draw ? 'lineTo' : 'moveTo'](x1, y1);
                segmentLength -= remaining;
            }
            x0 = x1; y0 = y1;
        }
    }

    function drawLine(ctx, style, path) {
        ctx.lineWidth = style.width;

        var cap = style.cap.ctor;
        ctx.lineCap = cap === 'Flat'
            ? 'butt'
            : cap === 'Round'
                ? 'round'
                : 'square';

        var join = style.join.ctor;
        ctx.lineJoin = join === 'Smooth'
            ? 'round'
            : join === 'Sharp'
                ? 'miter'
                : 'bevel';

        ctx.miterLimit = style.join._0 || 10;
        ctx.strokeStyle = Color.toCss(style.color);

        return line(ctx, style, path);
    }

    function texture(redo, ctx, src) {
        var img = new Image();
        img.src = src;
        img.onload = redo;
        return ctx.createPattern(img, 'repeat');
    }

    function gradient(ctx, grad) {
        var g;
        var stops = [];
        if (grad.ctor === 'Linear') {
            var p0 = grad._0, p1 = grad._1;
            g = ctx.createLinearGradient(p0._0, -p0._1, p1._0, -p1._1);
            stops = List.toArray(grad._2);
        } else {
            var p0 = grad._0, p2 = grad._2;
            g = ctx.createRadialGradient(p0._0, -p0._1, grad._1, p2._0, -p2._1, grad._3);
            stops = List.toArray(grad._4);
        }
        var len = stops.length;
        for (var i = 0; i < len; ++i) {
            var stop = stops[i];
            g.addColorStop(stop._0, Color.toCss(stop._1));
        }
        return g;
    }

    function drawShape(redo, ctx, style, path) {
        trace(ctx, path);
        var sty = style.ctor;
        ctx.fillStyle = sty === 'Solid'
            ? Color.toCss(style._0)
            : sty === 'Texture'
                ? texture(redo, ctx, style._0)
                : gradient(ctx, style._0);

        ctx.scale(1,-1);
        ctx.fill();
    }

    function drawImage(redo, ctx, form) {
        var img = new Image();
        img.onload = redo;
        img.src = form._3;
        var w = form._0,
            h = form._1,
            pos = form._2,
            srcX = pos._0,
            srcY = pos._1,
            srcW = w,
            srcH = h,
            destX = -w/2,
            destY = -h/2,
            destW = w,
            destH = h;

        ctx.scale(1,-1);
        ctx.drawImage(img, srcX, srcY, srcW, srcH, destX, destY, destW, destH);
    }

    function renderForm(redo, ctx, form) {
        ctx.save();
        var x = form.x, y = form.y, theta = form.theta, scale = form.scale;
        if (x !== 0 || y !== 0) ctx.translate(x, y);
        if (theta !== 0) ctx.rotate(theta);
        if (scale !== 1) ctx.scale(scale,scale);
        if (form.alpha !== 1) ctx.globalAlpha = ctx.globalAlpha * form.alpha;
        ctx.beginPath();
        var f = form.form;
        switch(f.ctor) {
        case 'FPath' : drawLine(ctx, f._0, f._1); break;
        case 'FImage': drawImage(redo, ctx, f); break;
        case 'FShape':
          if (f._0.ctor === 'Line') {
            f._1.closed = true;
            drawLine(ctx, f._0._0, f._1);
          } else {
            drawShape(redo, ctx, f._0._0, f._1);
          }
        break;
        }
        ctx.restore();
    }

    function formToMatrix(form) {
       var scale = form.scale;
       var matrix = A6( Transform.matrix, scale, 0, 0, scale, form.x, form.y );

       var theta = form.theta
       if (theta !== 0) {
           matrix = A2( Transform.multiply, matrix, Transform.rotation(theta) );
       }

       return matrix;
    }

    function str(n) {
        if (n < 0.00001 && n > -0.00001) return 0;
        return n;
    }

    function makeTransform(w, h, form, matrices) {
        var props = form.form._0.props;
        var m = A6( Transform.matrix, 1, 0, 0, -1,
                    (w - props.width ) / 2,
                    (h - props.height) / 2 );
        var len = matrices.length;
        for (var i = 0; i < len; ++i) {
            m = A2( Transform.multiply, m, matrices[i] );
        }
        m = A2( Transform.multiply, m, formToMatrix(form) );

        return 'matrix(' + str( m[0]) + ', ' + str( m[3]) + ', ' +
                           str(-m[1]) + ', ' + str(-m[4]) + ', ' +
                           str( m[2]) + ', ' + str( m[5]) + ')';
    }

    function stepperHelp(list) {
        var arr = List.toArray(list);
        var i = 0;
        function peekNext() {
            return i < arr.length ? arr[i].form.ctor : '';
        }
        // assumes that there is a next element
        function next() {
            var out = arr[i];
            ++i;
            return out;
        }
        return {
            peekNext:peekNext,
            next:next
        };
    }

    function formStepper(forms) {
        var ps = [stepperHelp(forms)];
        var matrices = [];
        var alphas = [];
        function peekNext() {
            var len = ps.length;
            var formType = '';
            for (var i = 0; i < len; ++i ) {
                if (formType = ps[i].peekNext()) return formType;
            }
            return '';
        }
        // assumes that there is a next element
        function next(ctx) {
            while (!ps[0].peekNext()) {
                ps.shift();
                matrices.pop();
                alphas.shift();
                if (ctx) { ctx.restore(); }
            }
            var out = ps[0].next();
            var f = out.form;
            if (f.ctor === 'FGroup') {
                ps.unshift(stepperHelp(f._1));
                var m = A2(Transform.multiply, f._0, formToMatrix(out));
                ctx.save();
                ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5]);
                matrices.push(m);

                var alpha = (alphas[0] || 1) * out.alpha;
                alphas.unshift(alpha);
                ctx.globalAlpha = alpha;
            }
            return out;
        }
        function transforms() { return matrices; }
        function alpha() { return alphas[0] || 1; }
        return {
            peekNext:peekNext,
            next:next,
            transforms:transforms,
            alpha:alpha
        };
    }

    function makeCanvas(w,h) {
        var canvas = NativeElement.createNode('canvas');
        canvas.style.width  = w + 'px';
        canvas.style.height = h + 'px';
        canvas.style.display = "block";
        canvas.style.position = "absolute";
        canvas.width  = w;
        canvas.height = h;
        return canvas;
    }

    function render(model) {
        var div = NativeElement.createNode('div');
        div.style.overflow = 'hidden';
        div.style.position = 'relative';
        update(div, model, model);
        return div;
    }

    function nodeStepper(w,h,div) {
        var kids = div.childNodes;
        var i = 0;
        function transform(transforms, ctx) {
            ctx.translate(w/2, h/2);
            ctx.scale(1,-1);
            var len = transforms.length;
            for (var i = 0; i < len; ++i) {
                var m = transforms[i];
                ctx.save();
                ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5]);
            }
            return ctx;
        }
        function nextContext(transforms) {
            while (i < kids.length) {
                var node = kids[i];
                if (node.getContext) {
                    node.width = w;
                    node.height = h;
                    node.style.width = w + 'px';
                    node.style.height = h + 'px';
                    ++i;
                    return transform(transforms, node.getContext('2d'));
                }
                div.removeChild(node);
            }
            var canvas = makeCanvas(w,h);
            div.appendChild(canvas);
            // we have added a new node, so we must step our position
            ++i;
            return transform(transforms, canvas.getContext('2d'));
        }
        function addElement(matrices, alpha, form) {
            var kid = kids[i];
            var elem = form.form._0;

            var node = (!kid || kid.getContext)
                ? NativeElement.render(elem)
                : NativeElement.update(kid, kid.oldElement, elem);

            node.style.position = 'absolute';
            node.style.opacity = alpha * form.alpha * elem.props.opacity;
            NativeElement.addTransform(node.style, makeTransform(w, h, form, matrices));
            node.oldElement = elem;
            ++i;
            if (!kid) {
                div.appendChild(node);
            } else {
                div.insertBefore(node, kid);
            }
        }
        function clearRest() {
            while (i < kids.length) {
                div.removeChild(kids[i]);
            }
        }
        return { nextContext:nextContext, addElement:addElement, clearRest:clearRest };
    }

    function update(div, _, model) {
        var w = model.w;
        var h = model.h;

        var forms = formStepper(model.forms);
        var nodes = nodeStepper(w,h,div);
        var ctx = null;
        var formType = '';

        while (formType = forms.peekNext()) {
            // make sure we have context if we need it
            if (ctx === null && formType !== 'FElement') {
                ctx = nodes.nextContext(forms.transforms());
                ctx.globalAlpha = forms.alpha();
            }

            var form = forms.next(ctx);
            // if it is FGroup, all updates are made within formStepper when next is called.
            if (formType === 'FElement') {
                // update or insert an element, get a new context
                nodes.addElement(forms.transforms(), forms.alpha(), form);
                ctx = null;
            } else if (formType !== 'FGroup') {
                renderForm(function() { update(div, model, model); }, ctx, form);
            }
        }
        nodes.clearRest();
        return div;
    }

    function collage(w,h,forms) {
        return A3(Element.newElement, w, h, {
            ctor: 'Custom',
            type: 'Collage',
            render: render,
            update: update,
            model: {w:w, h:h, forms:forms}
      	});
    }

    return localRuntime.Native.Graphics.Collage.values = {
        collage:F3(collage)
    };
};
<|endoftext|>