Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
/*
 * Copyright (C) 2011 Ursa Project LLC (http://ursaj.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
(function () {
    // Store this window context.
    var $win = window;

    // Initialize namespace.
    var $ns = window.hfs = window.hfs || {};

    if ($ns.Uploader)
        return; // Escape duplicate initialization.

    //
    // Utility functions.
    //

    /**
     * Safe shortcut for console logging.
     */
    function log(/* ... $args */) {
        HfsUploader.log.apply(HfsUploader, arguments);
    }

    /**
     * Validate constructor has been called with 'new' keyword instead of direct function call.
     *
     * @param $obj This object to validate constructor call for.
     */
    function validateConstructorCall($obj) {
        if ($obj === $ns || $obj === $win || $obj === window)
            throw new HfsException('Constructor should be called with \'new\' keyword.');
    }

    /**
     * Apply modifications to the target object.
     *
     * @param $target Object to apply modifications to.
     * param $args Sequence of objects to apply to the target.
     */
    function apply($target/*, ... $args */) {
        var $key;

        $target = $target || {};

        for (var $i = arguments.length - 1; $i >= 1; $i--) {
            var $src = arguments[$i];

            if (!$src)
                continue;

            for ($key in $src) {
                if (!$src.hasOwnProperty($key))
                    continue;

                $target[$key] = $src[$key];

                if (typeof $src[$key] == 'undefined')
                    delete $target[$key];
            }
        }

        return $target;
    }

    /**
     * If the string contains control characters or quote characters or backslash characters,
     * then we must also replace the offending characters with safe escape sequences.
     */
    var $escape = /['\\\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;

    /** Table of character substitutions. */
    var $meta = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '\'': '\\\'', '\\': '\\\\' };

    /**
     * Quote string into serialized string view.
     *
     * @param $str String to quote.
     * @return {string} Serialized string view.
     * @see https://github.com/douglascrockford/JSON-js/blob/master/json2.js
     */
    function quote($str) {
        $escape.lastIndex = 0; // Rewind regular expression.

        return '\'' + $str.replace($escape, function ($s) {
            var $char = $meta[$s];

            return typeof $char === 'string' ? $char :
                '\\u' + (0x10000 + $s.charCodeAt(0)).toString(16).substr(1);
        }) + '\'';
    }

    /**
     * Serialize JS object into serialized string view.
     *
     * @param $obj Object to serialize.
     * @return {string} Serialized string view.
     * @see https://github.com/douglascrockford/JSON-js/blob/master/json2.js
     */
    function serialize($obj) {
        switch (typeof $obj) {
            case 'undefined':
            case 'boolean':
            case 'number':
                return String($obj);

            case 'string':
                return quote($obj);

            case 'object':
                if (!$obj)
                    return 'null';

                var $partial = [];

                if ($obj instanceof Array || Object.prototype.toString.apply($obj) === '[object Array]') {
                    for (var $i = 0, $len = $obj.length; $i < $len; $i++)
                        $partial[$i] = serialize($obj[$i]);

                    return '[' + $partial.join(', ') + ']';
                }

                for (var $key in $obj)
                    if (Object.prototype.hasOwnProperty.call($obj, $key))
                        $partial.push(quote($key) + ': ' + serialize($obj[$key]));

                return '{' + $partial.join(', ') + '}';

            default:
                return 'null';
        }
    }

    //
    // Classes definitions.
    //

    /**
     * Constructs HFS general exception.
     *
     * @param $msg Exception message.
     * @param $cause Exception cause.
     */
    function HfsException($msg, $cause) {
        validateConstructorCall(this);

        if (arguments.length >= 1)
            this.msg = $msg;

        if (arguments.length >= 2)
            this.cause = $cause;
    }

    HfsException.prototype = {
        /** Exception message. */
        msg: undefined,

        /** Exception cause (if any). */
        cause: undefined,

        /**
         * @return String representation of the object.
         */
        toString: function () {
            return 'HfsException [msg=' + this.msg + ', cause=' + this.cause + ']';
        }
    };

    /**
     * Constructs HFS Upload instance.
     *
     * @param $cfg Upload configuration.
     * @private Don't publish this constructor in HFS namespace.
     */
    function HfsUpload($cfg) {
        validateConstructorCall(this);

        // Override all Upload properties with values from configuration.
        apply(this, $cfg);

        // Collection of callback listeners.
        this.$listeners = [];

        // Register new upload in the active uploads.
        HfsUpload.registry[this.id()] = this;
    }

    /** Prefix to generate frame ID from HFS upload ID. */
    var $FRAME_PREFIX = 'frame-';

    HfsUpload.prototype = {
        /** {Array} Names of the uploaded files. */
        names: undefined,

        /** Hide frame flag. */
        hideFrame: true,

        /** Fail this upload if frame has been loaded but completion has not been called within this delay. */
        completeDelay: 200,

        /**
         * Upload ID (auto-generated String ID).
         *
         * @return {String} Upload ID.
         */
        id: function () {
            var $id = Math.random().toFixed(8).substr(2);

            this.id = function () {
                return $id;
            };

            return $id;
        },

        /**
         * Auto-generated frame element.
         *
         * @return {HTMLElement} Frame element.
         */
        frame: function () {
            var $frame = document.createElement('iframe');

            $frame.id = $FRAME_PREFIX + this.id();
            $frame.src = 'about:blank';
            $frame.name = $frame.id;

            if (this.hideFrame)
                $frame.style.cssText = 'position: absolute; top: -1000px; left: -1000px; width: 1px; height: 1px;';

            HfsUploader.addEventListener($frame, 'load', this.loadCallback());

            this.frame = function () {
                return $frame;
            };

            return $frame;
        },

        /**
         * Frame load callback.
         */
        loadCallback: function () {
            // Save callback real scope.
            var $this = this;

            // DOM element callback.
            var $cb = function ($event) {
                if (this != $this.frame())
                    throw new HfsException('Failed to handle unexpected element: ' + this);

                if ($event.target != $this.frame())
                    throw new HfsException('Failed to handle unexpected element: ' + $event.target);

                $this.load($event);
            };

            // Persist generated callback.
            this.loadCallback = function () {
                return $cb;
            };

            return $cb;
        },

        /**
         * Callback on frame loading.
         *
         * @param $event DOM event on frame loading.
         */
        load: function ($event) {
            if ($event == null)
                return; // Ignore calls without DOM event.

            try {
                if ('about:blank' == this.frame().contentWindow.location.href)
                    return; // Skip initial frame loading.
            }
            catch ($e) {
                // No-op: ignore any exceptions, e.g. restricted access.
                log('Failed to validate frame URI.', $e);
            }

            var $this = this;

            // Add used frame cleanup, use delay for cases frame scripts would do anything.
            setTimeout(function () {
                $this.complete(-1, 'Frame loaded, but upload completion has not been fired.');
            }, this.completeDelay);
        },

        /**
         * Completes this upload.
         *
         * @param $code Completion code: zero on success, otherwise - failure.
         * @param $value Completion value, result description in most cases.
         */
        complete: function ($code, $value) {
            var $args = Array.prototype.slice.call(arguments, 0);

            // Allow to call this method only once.
            this.complete = function () {
                log('Completion callback has already been called.', $args);
            };

            // Make new callbacks be called instantly.
            this.onComplete = function ($callback, $scope) {
                $callback.apply($scope || this, $args);
            };

            // Cleanup frame.
            var $frame = this.frame();

            if ($frame.parentNode && this.hideFrame)
                $frame.parentNode.removeChild($frame);

            // Notify callbacks (if any).
            for (var $i = 0, $size = this.$listeners.length; $i < $size; $i++) {
                var $cb = this.$listeners[$i];

                try {
                    $cb[0].apply($cb[1] || this, $args);
                }
                catch ($e) {
                    log('Failed to notify upload callback.', $e, $cb[0], $cb[1], $args);
                }
            }

            // Cleanup listeners collection.
            this.$listeners.splice(0, this.$listeners.length);

            // Unregister this upload from the active uploads.
            HfsUpload.registry[this.id()] = undefined;
            delete HfsUpload.registry[this.id()];
        },

        /**
         * Add completion callback for this upload.
         *
         * @param $callback Callback function for upload completion: function($code, $value) { ... }, where
         *     $code - completion code: zero on success, otherwise - failure,
         *     $value - completion value, result description in most cases.
         * @param $scope Callback scope to use. If not provided, this upload will be used as a scope.
         */
        onComplete: function ($callback, $scope) {
            this.$listeners[this.$listeners.length] = [$callback, $scope];
        },

        /**
         * @return String representation of the object.
         */
        toString: function () {
            return 'HFS Upload [names=' + this.names + ', hideFrame=' + this.hideFrame +
                ', completeDelay=' + this.completeDelay + ']';
        }
    };

    /** Provide active uploads manipulations as static members. */
    apply(HfsUpload, {
        /** Registry of active uploads. */
        registry: {},

        /**
         * Get active upload associated with specified frame.
         *
         * @param $frameId Frame DOM element ID to get associated upload for.
         * @return {HfsUpload} Active upload or 'undefined' if no active upload associated with this frame.
         */
        getUpload: function ($frameId) {
            $frameId += ''; // Escape string.
            var $len = $FRAME_PREFIX.length;

            if ($frameId.substr(0, $len) == $FRAME_PREFIX)
                return this.registry[$frameId.substr($len)];

            return null;
        }
    });

    /**
     * Constructs HFS Uploader instance.
     *
     * @param $cfg Uploader configuration.
     */
    function HfsUploader($cfg) {
        validateConstructorCall(this);

        // Override all Uploader properties with values from configuration.
        apply(this, $cfg);

        // Validate configuration.
        if (typeof this.uploadUri != 'string' || this.uploadUri.length == 0)
            throw new HfsException('Failed to configure HFS Uploader (invalid upload URI): ' + this.uploadUri);

        this.el = HfsUploader.resolveElement(this.el); // Resolve upload control element.

        this.appendInput();
    }

    HfsUploader.prototype = {
        /** Upload control element to open file's selection dialog for (configurable '#element-id' or DOM element). */
        el: undefined,

        /**
         * Uploads handler to process new uploads.
         *
         * @param $upload Started upload object.
         */
        handler: function ($upload) {
            $ns.Uploader.log('>>> UPLOAD STARTED. Override HFS uploader \'handler\' to provide your logic.', $upload);

            $upload.onComplete(function ($code, $value) {
                $ns.Uploader.log('>>> UPLOAD COMPLETE.', $code, $value);
            }, null);
        },

        /** Upload handler scope. */
        scope: undefined,

        /** Upload URI to send uploaded files to (String). */
        uploadUri: undefined,

        /** Upload control element styles to apply (configurable key->value style properties). */
        style: undefined,

        /** Allow multiple select for uploads. */
        multiple: false,

        /** Debug mode: show upload frame(s) and keep them even after upload completes. */
        debug: false,

        /**
         * Uploader ID (auto-generated String ID).
         *
         * @return {String} Uploader ID.
         */
        id: function () {
            var $id = Math.random().toFixed(8).substr(2);

            this.id = function () {
                return $id;
            };

            return $id;
        },

        /**
         * Auto-generated file input element.
         *
         * @return {HTMLElement} File input element.
         */
        input: function () {
            var $input = document.createElement('input');

            $input.id = 'file-input-' + this.id();
            $input.name = 'file[]';
            $input.type = 'file';
            $input.multiple = this.multiple;

            HfsUploader.addEventListener($input, 'change', this.changeCallback());

            this.input = function () {
                return $input;
            };

            return $input;
        },

        /**
         * Create upload form element.
         *
         * @return {HTMLElement} Upload form element.
         */
        form: function () {
            var $form = document.createElement('form');

            $form.id = 'form-' + this.id();
            $form.method = 'POST';
            $form.enctype = 'multipart/form-data';
            $form.action = this.uploadUri;

            this.form = function () {
                return $form;
            };

            return $form;
        },

        /**
         * Append file input element to upload control one.
         */
        appendInput: function () {
            // Apply styles to upload control element.
            apply(this.el.style, this.style, {
                display: 'inline-block',
                overflow: 'hidden',
                position: 'relative',
                verticalAlign: 'bottom'
            });

            // Apply styles to file input element.
            this.input().style.cssText = '' +
                ' position: absolute; top: -100px; right: -100px; z-index: 2; font-size: 300px;' +
                ' background: pink; opacity: 0; -moz-opacity: 0; filter: alpha(opacity:0); ';

            // Append input element.
            this.form().appendChild(this.input());
            this.el.appendChild(this.form());
        },

        /**
         * Callback on file selection.
         *
         * @param $event DOM event on file selection.
         */
        select: function ($event) {
            if ($event == null)
                return; // Ignore calls without DOM event.

            var $files = this.input().files;

            if ($files.length == 0)
                return; // Ignore canceled selections.

            var $names = [];

            for (var $i = 0; $i < $files.length; $i++)
                $names[$i] = $files[$i].name;

            var $upload = new HfsUpload(apply({}, {
                names: $names
            }, this.debug ? { hideFrame: false } : {}));

            var $form = this.form();
            var $frame = $upload.frame();

            this.el.ownerDocument.body.appendChild($frame);

            $form.target = $frame.name;
            $form.submit();
            $form.reset();
            $form.target = '_blank';

            if (this.handler)
                this.handler.call(this.scope || this, $upload);
            else
                log('Failed to notify upload handler (not found).', this, $upload);
        },

        /**
         * Input file DOM element 'onchange' callback.
         *
         * @return {Function} Input file DOM element 'onchange' callback.
         * @private
         */
        changeCallback: function () {
            // Save callback real scope.
            var $this = this;

            // DOM element callback.
            var $cb = function ($event) {
                if (this != $this.input())
                    throw new HfsException('Failed to handle unexpected element: ' + this);

                if ($event.target != $this.input())
                    throw new HfsException('Failed to handle unexpected element: ' + $event.target);

                $this.select($event);
            };

            // Persist generated callback.
            this.changeCallback = function () {
                return $cb;
            };

            return $cb;
        },

        /**
         * @return String representation of the object.
         */
        toString: function () {
            return 'HFS Uploader [el=' + this.el + ', target=' + this.target + ', uploadUri=' + this.uploadUri + ']';
        }
    };

    /** Provide DOM manipulation methods as static members. */
    apply(HfsUploader, {
        /**
         * Completes this upload, should be called from the uploading frame.
         *
         * @param $code {int} Completion code: zero on success, otherwise - failure.
         * @param $value Completion value, result description in most cases.
         * @param $frame Frame window object ('window' variable in the frame context).
         */
        complete: function ($code, $value, $frame) {
            if (typeof $frame == 'string' /* Frame ID, correct (parent) window scope. */) {
                var $upload = HfsUpload.getUpload($frame);

                if ($upload)
                    $upload.complete($code, $value /* Evaluated in correct scope. */);
                else
                    log('Failed to resolve active upload for the frame ID.', $frame, HfsUpload.registry);
            }
            else {
                var $frameId = this.parentFrameId($frame);

                if (!$frameId)
                    throw new HfsException('Failed to resolve parent frame ID for page.', $frame);

                // Execute completion in the correct (parent) window scope.
                $win.eval('window.hfs.Uploader.complete(' +
                    $code + ', ' + this.serialize($value) + ', \'' + $frameId + '\');');
            }
        },

        /**
         * Serialize JS object into JSON string.
         *
         * @param $obj JS object to serialize.
         * @return {String} Serialized JSON string.
         */
        serialize: function ($obj) {
            return serialize($obj);
        },

        /**
         * Resolve the frame element ID corresponding to passed frame window object.
         *
         * @param $frame Frame window object ('window' variable in the frame context).
         * @return {String} The frame DOM element ID corresponding to passed frame window object
         *     or 'null' if frame element cannot be resolved.
         */
        parentFrameId: function ($frame) {
            try {
                var $frames = $win.document.getElementsByTagName('iframe');

                for (var $i = 0; $i < $frames.length; $i++)
                    if ($frames[$i].contentWindow == $frame)
                        return $frames[$i].id;
            }
            catch ($e) {
                log('Failed to resolve parent frame.', $e);
            }

            return null;
        },

        /**
         * Resolve DOM element from its description.
         *
         * @param $el DOM element description.
         * @return DOM element resolved from its description or descriptor itself.
         */
        resolveElement: function ($el) {
            if (typeof $el == 'string' && $el.length > 0 && $el[0] == '#')
                $el = document.getElementById($el.substr(1));

            return $el;
        },

        /**
         * Add event listener to DOM element.
         *
         * @param $el DOM element to add event listener to.
         * @param $event Event name to listen.
         * @param $callback Callback handler for events.
         * @return {*}
         */
        addEventListener: function ($el, $event, $callback) {
            if ($el.addEventListener)
                return $el.addEventListener($event, $callback, false);
            else if ($el.attachEvent)
                return $el.attachEvent('on' + $event, $callback);
            else
                log('Failed to attach event to DOM element.', $el, $event, $callback);
        },

        /**
         * Console logging.
         */
        log: function (/* ... $args */) {
            if (console && console.log && console.log.apply)
                console.log.apply(console, arguments);
        }
    });

    //
    // Publish classes within HFS namespace.
    //

    $ns.Exception = HfsException;
    $ns.Uploader = HfsUploader;
})();
New to GrepCode? Check out our FAQ X