<commit_before>/**
 * jQuery Image Stream Widget
 * @author Stephen Koo
 * @version 0.1.1
 **/
(function ($) {
    "use strict";

    $.widget("remotebio.imageStream", {

        // Default options
        options: {
            // The array of image path definitions, ordered from highest resolution to lowest (though
            // Image Stream wouldn't know the difference).
            // Each definition is either in the form of a callback function--which takes a zero-indexed frame number
            // as an integer and returns a string--or an explicit array of strings.
            sequences: [],
            // The number of frames in the animation. Must be specified if using a callback function for
            // sequence definition, otherwise defaults to the length of the first explicit list.
            numFrames: null,
            // The size of the container. This value should be an object with numeric "width" and "height" properties.
            // Defaults to the CSS size of the element right before initialization of Image Stream.
            containerSize: null,
            // Size of the images inside the container. Same format as containerSize.
            // Defaults to the size of the container element after the above defaults are applied.
            size: null,
            // Offset of the images inside the container. This value should be an boject with numeric "x" and "y"
            // properties.
            offset: {x: 0, y: 0},
            // The frame to display first (zero-indexed).
            frame: 0,
            // The speed of the animation in frames per second.
            fps: 12,
            // The maximum number of images to keep in buffer at any given time. Can be arbitrarily large.
            maxBufferSize: 20,
            // The minimum number of images to buffer before starting animation on play.
            minBufferSize: 10,
            // If true, will apply adaptive streaming, otherwise, stay on the same sequence throughout animation.
            adaptive: true,
            // Callback function that returns the index of the sequence to switch to given a new image width after resize.
            // Default always uses first sequence in sequences array.
            sequenceByWidth: function (imageWidth) {
                return 0;
            },
            // ADVANCED: Number of milliseconds to wait after resizing the images to detect need for sequence change.
            resizeWait: 500
        },

        _create: function () {
            // Prepare internal (private) state object
            this._state = {
                "$bufferContainer": $("

").hide().appendTo(this.element),
                "$currentImage": $(""),
                "bufferSize": 0,
                "sequence": Infinity,
                "buffer": {},
                "playing": false,
                "aniTimer": null,
                "resizeTimer": null,
                "$canvas": null,
                "ctx": null
            };
            this._state.$canvas = $("<canvas></canvas").appendTo(this.element);
            this._state.ctx = this._state.$canvas.get(0).getContext('2d');

            // get current size of container if not specified in options
            if (this.options.containerSize === null) {
                this.options.containerSize = {
                    width: this.element.css("width"),
                    height: this.element.css("height")
                };
            }
            // default image size matches the container size
            if (this.options.size === null) {
                this.options.size = this.options.containerSize;
            }

            // Process the sequence definitions
            var numSequences = this.options.sequences.length;
            for (var i = 0; i < numSequences; i++) {
                var sequence = this.options.sequences[i];

                if (typeof sequence === 'function') {
                    // naive test
                    var testPath = sequence(0);
                    if (typeof testPath !== 'string') {
                        throw new Error("The callback function must accept an integer as argument and return a string.");
                    }
                    if (typeof this.options.numFrames !== 'number') {
                        throw new Error("numFrames must be specified if using a callback function for sequence definition.");
                    }
                } else if (sequence instanceof Array) {
                    if (this.options.numFrames == null) {
                        this.options.numFrames = sequence.length;
                    } else if (sequence.length < this.options.numFrames) {
                        throw new Error("The explicit array of images (sequences[" + i + "]) is smaller than the specified number of frames.");
                    }
                    // normalize into a function as well TODO: confirm that this works
                    this.options.sequences[i] = function getPath(frameNumber) {
                        return getPath.images[frameNumber];
                    };
                    this.options.sequences[i].images = sequence;
                } else {
                    throw new Error("Sequence must be an array of image paths or a callback function.");
                }

            }

            // Initialize sizes and load first image
            this._updateSequence();
            this._updateContainerSize();
            this._resetFrame();
        },

        _setOption: function(key, value) {
            if (value === undefined || value === null) {
                return this.options[key];
            }
            switch (key) {
            case "size":
                this.options[key] = value;
                this._refreshImage();
                // Reset timer to update sequence (if not in the middle of animation)
                if (!this._state.playing) {
                    window.clearTimeout(this._state.resizeTimer);
                    var self = this;
                    this._state.resizeTimer = window.setTimeout(function() {
                        self._updateSequence(null, true); // prevent downgrade
                    }, this.options.resizeWait);
                }
                break;
            case "offset":
                this.options[key] = value;
                this._refreshImage();
                break;
            case "frame":
                if (value !== this.options.frame && value >= 0 && value < this.options.numFrames) {
                    if (this._state.playing) {
                        this.pause();
                    }
                    this.options[key] = value;
                    this._updateSequence();
                    this._resetFrame();
                }
                break;
            case "containerSize":
                this.options[key] = value;
                this._updateContainerSize();
                this._refreshImage;
                break;
            case "numFrames":
                // First confirm that the sequences can be extended as specified.
                var numSequences = this.options.sequences.length;
                for (var i = 0; i < numSequences; i++) {
                    var sequence = this.options.sequences[i];
                    if (sequence.hasOwnProperty('images') && sequence.images.length < value) {
                        throw new Error("explicit array of images not big enough for new numFrames.");
                    }
                }
                this.options[key] = value;
                break;
            case "sequences":
                throw new Error("You cannot modify the sequences definitions after initializing the image stream (yet).");
                break;
            default:
                this.options[key] = value;
                break;
            }
        },

        _updateSequence: function(newSequence, preventDowngrade) {
            // Calculate new sequence id based on width if none provided
            if (newSequence === undefined || newSequence === null) {
                newSequence = this.options.sequenceByWidth(this.options.size.width);
            }
            // Bound sequence id
            if (newSequence >= this.options.sequences.length) {
                newSequence = this.options.sequences.length - 1;
            } else if (newSequence < 0) {
                newSequence = 0;
            }
            // No need to let resolution decrease if better resolution image already loaded
            if (preventDowngrade && this._state.sequence < newSequence) {
                return false;
            }
            if (this._state.sequence === newSequence) {
                return false; // just return false if unchanged
            }

            this._state.sequence = newSequence;

            if (!this._state.playing) {
                this._resetFrame();
            }
            return true;
        },

        /*
         * Reset the buffer and reload the current frame from scratch.
         */
        _resetFrame: function() {
            // reset buffer
            this._state.$bufferContainer.empty();
            this._state.bufferSize = 0;
            this._state.buffer = new Object();
            // load image
            this._loadImage(this.options.frame);
            this._joinImageLoad(this.options.frame, function() {
                this._displayFrame();
            });
        },

        /*
         * Display the current frame.
         * Assumes that the image for the current frame has already been loaded.
         */
        _displayFrame: function() {
            // move image element out of buffer into $currentImage
            var $img = this._state.buffer[this.options.frame];
            delete this._state.buffer[this.options.frame];
            this._state.$currentImage.remove(); // remove last displayed image from document
            this._state.$currentImage = $img;
            this._state.bufferSize--;    // decrement bufferSize
            // refresh with new image
            this._refreshImage();
            // trigger frameChange event
            this.element.trigger("frameChange", this.options.frame);
        },

        /*
         * Fires a request to preload an image and places the new image element in the buffer.
         */
        _loadImage: function(seqNum) {
            if (this._state.buffer[seqNum]) {
                return; // if already in buffer, ignore load request.
            }
            var $img = $(new Image());

            // Set "done" flag when image is done loading
            $img.on("load", function() {
                $img.data("done", true);
            });
            // Preload image
            // force some recalcitrant browsers to actually load the images by setting a size
            $img.attr("src", this.options.sequences[this._state.sequence](seqNum)).css({height:"1px", width:"1px"}).data("done", false);

            // "cache" in a container
            this._state.$bufferContainer.append($img);
            this._state.buffer[seqNum] = $img;
            this._state.bufferSize++;
        },

        /*
         * Call the callback immediately if the given frame has already been loaded,
         * or bind it to be called when the image is loaded.
         *
         * The callback receives an additional argument representing whether the callback was delayed,
         * which is "true" in the latter case outlined above and false in the former.
         */
        _joinImageLoad: function(seqNum, callback) {
            var $img = this._state.buffer[seqNum];

            if ($img) {
                if ($img.data("done") === true) {
                    callback.call(this, false /*isDelayed*/);
                } else {
                    var self = this;
                    $img.on("load", function() {
                        callback.call(self, true);
                    });
                }
            } else {
                throw new Error("loadImage has not been called on " + seqNum + " yet.");
            }
        },

        /*
         * Update the container element and canvas size.
         * Canvas will get erased if resized so _refreshImage should typically be called after this.
         */
        _updateContainerSize: function() {
            this._state.$canvas.attr(this.options.containerSize);
            this.element.css(this.options.containerSize);
        },

        /*
         * Redraw the current image based on the currently specified offset and size.
         */
        _refreshImage: function() {
            var offset = this.options.offset;
            var size = this.options.size;
            this._state.ctx.drawImage(this._state.$currentImage[0], offset.x, offset.y, size.width, size.height);
        },

        /* Public Methods */

        /*
         * Play through the specified range of images as an animation.
         * If no range is specified, defaults are:
         *      from -  the current frame, or the beginning if current frame is last frame
         *      to - the last frame in the sequence
         */
        play: function(from, to) {
            this.element.trigger("play");
            this._state.playing = true;
            // validation and defaults
            if (from === undefined || from === null) {
                var nextFrame = this.options.frame + 1;
                from = (nextFrame >= this.options.numFrames) ? 0 : nextFrame;
            }
            if (to === undefined || to === null) {
                to = this.options.numFrames - 1;
            }

            // the meat
            var lastTime = new Date().getTime();
            this.options.frame = from;

            // Fire off loading for the first maxBufferSize images.
            this.element.trigger("buffering");
            var bufferEnd = Math.min(from + this.options.maxBufferSize - 1, to);
            for (var i = from; i <= bufferEnd; i++) {
                this._loadImage(i);
            }
            waitBuffer.call(this, from, Math.min(from + this.options.minBufferSize - 1, to));

            // Helper functions
            /* Calls stepAnimation only after all the images in the specified range have been loaded. */
            function waitBuffer(waitStart, waitEnd) {
                if (waitStart === waitEnd) {
                    this._joinImageLoad(waitEnd, stepAnimation);
                } else {
                    this._joinImageLoad(waitStart, function() {
                        waitBuffer.call(this, waitStart + 1, waitEnd); // recursively wait for the next image
                    });
                }
            }
            /* Displays the current frame in the animation, continues firing off buffer requests, and then recurses after the next image or the buffer is loaded. */
            function stepAnimation(isDelayed) {
                this._displayFrame();
                if (this.options.frame < to) {
                    // fire off loading if buffer is under maxBufferSize
                    bufferEnd = Math.min(to, this.options.frame + this.options.maxBufferSize - this._state.bufferSize);
                    for (var i = this.options.frame + this._state.bufferSize + 1; i <= bufferEnd; i++) {
                        this._loadImage(i);
                    }
                    // Calculate time left in the current frame interval
                    var elapsed = (new Date().getTime() - lastTime);
                    var frameInterval = 1000 / this.options.fps;
                    var timeLeft = (elapsed > frameInterval) ? 0 : frameInterval - elapsed;
                    // Wait for time for next frame
                    var self = this;
                    this._state.aniTimer = window.setTimeout(function() {
                        lastTime = new Date().getTime();
                        self.options.frame++;
                        if (isDelayed) {
                            // if hitting end of buffer, wait until it grows to minBufferSize if can't switch resolution,
                            // or switch to lower resolution and wait until all previously buffered higher resolution images are done loading
                            self.element.trigger("buffering");
                            if (self.options.adaptive && self._updateSequence(self._state.sequence + 1)) {
                                waitBuffer.call(self, self.options.frame, self.options.frame + self._state.bufferSize - 1);
                            } else {
                                waitBuffer.call(self, self.options.frame, Math.min(self.options.frame + self.options.minBufferSize - 1, to));
                            }
                        } else {
                            self._joinImageLoad(self.options.frame, stepAnimation);
                        }
                    }, timeLeft);
                } else {
                    this.pause();
                }
            }
        },

        /*
         * Stop any currently playing animation.
         */
        pause: function() {
            window.clearTimeout(this._state.aniTimer);
            this._state.playing = false;
            this.element.trigger("pause");
            this._updateSequence();
        },

        /* A few convenience methods for moving around images */
        next: function() {
            this._setOption("frame", this.options.frame + 1);
        },

        previous: function() {
            this._setOption("frame", this.options.frame - 1);
        },

        frame: function(seqNum) {
            this._setOption("frame", seqNum);
        }
    });

})(jQuery);
<commit_msg>actually gauges download speed for adaptive streaming<commit_after>/**
 * jQuery Image Stream Widget
 * @author Stephen Koo
 * @version 0.1.1
 **/
(function ($) {
    "use strict";

    $.widget("remotebio.imageStream", {

        // Default options
        options: {
            // The array of image path definitions, ordered from highest resolution to lowest (though
            // Image Stream wouldn't know the difference).
            // Each definition is either in the form of a callback function--which takes a zero-indexed frame number
            // as an integer and returns a string--or an explicit array of strings.
            sequences: [],
            // Average file sizes of the images for each sequence, in bytes.
            bytes: [],
            // Path to placeholder image, which is displayed when frame is set to -1.
            placeholder: "/placeholder.png",
            // The number of frames in the animation. Must be specified if using a callback function for
            // sequence definition, otherwise defaults to the length of the first explicit list.
            numFrames: null,
            // The size of the container. This value should be an object with numeric "width" and "height" properties.
            // Defaults to the CSS size of the element right before initialization of Image Stream.
            containerSize: null,
            // Size of the images inside the container. Same format as containerSize.
            // Defaults to the size of the container element after the above defaults are applied.
            size: null,
            // Offset of the images inside the container. This value should be an object with numeric "x" and "y"
            // properties.
            offset: {x: 0, y: 0},
            // The frame to display first (zero-indexed).
            frame: 0,
            // The speed of the animation in frames per second.
            fps: 12,
            // The maximum number of images to keep in buffer at any given time. Can be arbitrarily large.
            maxBufferSize: 20,
            // The minimum number of images to buffer before starting animation on play.
            minBufferSize: 10,
            // If true, will apply adaptive streaming, otherwise, stay on the same sequence throughout animation.
            adaptive: true,
            // Callback function that returns the index of the sequence to switch to given a new image width after resize.
            // Default always uses first sequence in sequences array.
            sequenceByWidth: function (imageWidth) {
                return 0;
            },
            // ADVANCED: Number of milliseconds to wait after resizing the images to detect need for sequence change.
            resizeWait: 500
        },

        _create: function () {
            // Prepare internal (private) state object
            this._state = {
                "$bufferContainer": $("

").hide().appendTo(this.element),
                "$currentImage": $(""),
                "bufferSize": 0,
                "sequence": Infinity,
                "buffer": {},
                "playing": false,
                "aniTimer": null,
                "resizeTimer": null,
                "$canvas": null,
                "ctx": null,
                "dlSpeed": Infinity,
            };
            this._state.$canvas = $("<canvas></canvas").appendTo(this.element);
            this._state.ctx = this._state.$canvas.get(0).getContext('2d');

            // get current size of container if not specified in options
            if (this.options.containerSize === null) {
                this.options.containerSize = {
                    width: this.element.css("width"),
                    height: this.element.css("height")
                };
            }
            // default image size matches the container size
            if (this.options.size === null) {
                this.options.size = this.options.containerSize;
            }

            // Process the sequence definitions
            var numSequences = this.options.sequences.length;
            for (var i = 0; i < numSequences; i++) {
                var sequence = this.options.sequences[i];

                if (typeof sequence === 'function') {
                    // naive test
                    var testPath = sequence(0);
                    if (typeof testPath !== 'string') {
                        throw new Error("The callback function must accept an integer as argument and return a string.");
                    }
                    if (typeof this.options.numFrames !== 'number') {
                        throw new Error("numFrames must be specified if using a callback function for sequence definition.");
                    }
                } else if (sequence instanceof Array) {
                    if (this.options.numFrames == null) {
                        this.options.numFrames = sequence.length;
                    } else if (sequence.length < this.options.numFrames) {
                        throw new Error("The explicit array of images (sequences[" + i + "]) is smaller than the specified number of frames.");
                    }
                    // normalize into a function as well TODO: confirm that this works
                    this.options.sequences[i] = function getPath(frameNumber) {
                        return getPath.images[frameNumber];
                    };
                    this.options.sequences[i].images = sequence;
                } else {
                    throw new Error("Sequence must be an array of image paths or a callback function.");
                }

            }

            // Initialize sizes and load first image
            this._updateSequence();
            this._updateContainerSize();
            this._resetFrame();
        },

        _setOption: function(key, value) {
            if (value === undefined || value === null) {
                return this.options[key];
            }
            switch (key) {
            case "size":
                this.options[key] = value;
                this._refreshImage();
                // Reset timer to update sequence (if not in the middle of animation)
                if (!this._state.playing) {
                    window.clearTimeout(this._state.resizeTimer);
                    var self = this;
                    this._state.resizeTimer = window.setTimeout(function() {
                        self._updateSequence(null, true); // prevent downgrade
                    }, this.options.resizeWait);
                }
                break;
            case "offset":
                this.options[key] = value;
                this._refreshImage();
                break;
            case "frame":
                if (value !== this.options.frame && value >= -1 && value < this.options.numFrames) {
                    if (this._state.playing) {
                        this.pause();
                    }
                    this.options[key] = value;
                    this._updateSequence();
                    this._resetFrame();
                }
                break;
            case "containerSize":
                this.options[key] = value;
                this._updateContainerSize();
                this._refreshImage;
                break;
            case "numFrames":
                // First confirm that the sequences can be extended as specified.
                var numSequences = this.options.sequences.length;
                for (var i = 0; i < numSequences; i++) {
                    var sequence = this.options.sequences[i];
                    if (sequence.hasOwnProperty('images') && sequence.images.length < value) {
                        throw new Error("explicit array of images not big enough for new numFrames.");
                    }
                }
                this.options[key] = value;
                break;
            case "bytes":
            case "sequences":
                throw new Error("You cannot modify \"" + key + "\" after initialization.");
                break;
            default:
                this.options[key] = value;
                break;
            }
        },

        _updateSequence: function(newSequence, preventDowngrade) {
            // Calculate new sequence id based on width if none provided
            if (newSequence === undefined || newSequence === null) {
                newSequence = this.options.sequenceByWidth(this.options.size.width);
            }
            // Bound sequence id
            if (newSequence >= this.options.sequences.length) {
                newSequence = this.options.sequences.length - 1;
            } else if (newSequence < 0) {
                newSequence = 0;
            }
            // No need to let resolution decrease if better resolution image already loaded
            if (preventDowngrade && this._state.sequence < newSequence) {
                return false;
            }
            if (this._state.sequence === newSequence) {
                return false; // just return false if unchanged
            }

            // console.log("update to", newSequence);
            this._state.sequence = newSequence;

            if (!this._state.playing) {
                this._resetFrame();
            }
            return true;
        },

        /*
         * Reset the buffer and reload the current frame from scratch.
         */
        _resetFrame: function() {
            // reset buffer
            this._state.$bufferContainer.empty();
            this._state.bufferSize = 0;
            this._state.buffer = new Object();
            // load image
            this._loadImage(this.options.frame);
            this._joinImageLoad(this.options.frame, function() {
                this._displayFrame();
            });
        },

        /*
         * Display the current frame.
         * Assumes that the image for the current frame has already been loaded.
         */
        _displayFrame: function() {
            // move image element out of buffer into $currentImage
            var $img = this._state.buffer[this.options.frame];
            delete this._state.buffer[this.options.frame];
            this._state.$currentImage.remove(); // remove last displayed image from document
            this._state.$currentImage = $img;
            this._state.bufferSize--;    // decrement bufferSize
            // refresh with new image
            this._refreshImage();
            // trigger frameChange event
            this.element.trigger("frameChange", this.options.frame);
        },

        /*
         * Fires a request to preload an image and places the new image element in the buffer.
         */
        _loadImage: function(seqNum) {
            var state = this._state;
            var options = this.options;

            if (state.buffer[seqNum]) {
                return; // if already in buffer, ignore load request.
            }

            var $img = $(new Image());

            // Set "done" flag when image is done loading
            var startTime = new Date().getTime(); // we don't need anything better than 15 ms precision
            var seq = state.sequence; // save current sequence
            $img.on("load", function() {
                state.dlSpeed = options.bytes[seq] / (new Date().getTime() - startTime);
                $img.data("done", true);
            });
            // Preload image
            if (seqNum === -1) {
                $img.attr("src", options.placeholder);
            } else {
                $img.attr("src", options.sequences[seq](seqNum));
            }
            // force some recalcitrant browsers to actually load the images by setting a size
            $img.css({height:"1px", width:"1px"}).data("done", false);

            // "cache" in a container
            state.$bufferContainer.append($img);
            state.buffer[seqNum] = $img;
            state.bufferSize++;
        },

        /*
         * Call the callback immediately if the given frame has already been loaded,
         * or bind it to be called when the image is loaded.
         *
         * The callback receives an additional argument representing whether the callback was delayed,
         * which is "true" in the latter case outlined above and false in the former.
         */
        _joinImageLoad: function(seqNum, callback) {
            var $img = this._state.buffer[seqNum];

            if ($img) {
                if ($img.data("done") === true) {
                    callback.call(this, false);
                } else {
                    var self = this;
                    $img.on("load", function() {
                        callback.call(self, true);
                    });
                }
            } else {
                throw new Error("loadImage has not been called on " + seqNum + " yet.");
            }
        },

        /*
         * Update the container element and canvas size.
         * Canvas will get erased if resized so _refreshImage should typically be called after this.
         */
        _updateContainerSize: function() {
            this._state.$canvas.attr(this.options.containerSize);
            this.element.css(this.options.containerSize);
        },

        /*
         * Redraw the current image based on the currently specified offset and size.
         */
        _refreshImage: function() {
            var offset = this.options.offset;
            var size = this.options.size;
            this._state.ctx.clearRect(0, 0, this._state.$canvas.width(), this._state.$canvas.height());
            this._state.ctx.drawImage(this._state.$currentImage[0], offset.x, offset.y, size.width, size.height);
        },

        /* Public Methods */

        /*
         * Play through the specified range of images as an animation.
         * If no range is specified, defaults are:
         *      from -  the current frame, or the beginning if current frame is last frame
         *      to - the last frame in the sequence
         */
        play: function(from, to) {
            var state, options, element, self,
                nextFrame, lastTime, frameInterval, speeds, numSpeeds, i, bufferEnd;

            state = this._state;
            options = this.options;
            element = this.element;
            self = this;

            // validation and defaults
            if (from === undefined || from === null) {
                nextFrame = options.frame + 1;
                from = (nextFrame >= options.numFrames) ? 0 : nextFrame;
            }
            if (to === undefined || to === null) {
                to = options.numFrames - 1;
            }

            // the meat
            element.trigger("play");
            state.playing = true;
            options.frame = from;
            lastTime = new Date().getTime();
            frameInterval = 1000 / options.fps;

            // Compute download speeds necessary based on current fps
            speeds = [];
            numSpeeds = options.bytes.length
            for (i = 0; i < numSpeeds; i++) {
                speeds.push(options.bytes[i] / frameInterval);
                // console.log(i, speeds[i]*(7.62939e-3), "Mbps");
            }

            // Fire off loading for the first maxBufferSize images.
            element.trigger("buffering");
            bufferEnd = Math.min(from + options.maxBufferSize - 1, to);
            for (i = from; i <= bufferEnd; i++) {
                this._loadImage(i);
            }
            waitBuffer(from, Math.min(from + options.minBufferSize - 1, to));

            // Helper functions
            /* Calls stepAnimation only after all the images in the specified range have been loaded. */
            function waitBuffer(waitStart, waitEnd) {
                if (waitStart === waitEnd) {
                    self._joinImageLoad(waitEnd, function () {
                        stepAnimation(false);
                    });
                } else {
                    self._joinImageLoad(waitStart, function() {
                        waitBuffer(waitStart + 1, waitEnd); // recursively wait for the next image
                    });
                }
            }
            /* Displays the current frame in the animation, continues firing off buffer requests, and then recurses after the next image or the buffer is loaded. */
            function stepAnimation(isDelayed) {
                var bufferEnd, i;

                self._displayFrame();
                if (options.frame < to) {
                    // fire off loaders to mabntain buffer
                    bufferEnd = Math.min(to, options.frame + options.maxBufferSize - state.bufferSize);
                    for (i = options.frame + state.bufferSize + 1; i <= bufferEnd; i++) {
                        self._loadImage(i);
                    }

                    // Wait until it's time for the next frame.
                    state.aniTimer = window.setTimeout(function() {
                        var idx;

                        options.frame++;
                        lastTime = new Date().getTime();

                        // Adaptive streaming computations
                        if (options.adaptive) {
                            if (numSpeeds) {
                                // console.log(state.dlSpeed*(7.62949e-3) + " Mbps");
                                // Determine appropriate resolution by download speed
                                for (idx = 0; idx < numSpeeds; idx++) {
                                    if (state.dlSpeed > speeds[idx]) break;
                                }

                                // resolution could be better XOR last image load delayed
                                if ((idx < state.sequence) !== isDelayed) {
                                    self._updateSequence(Math.max(options.sequenceByWidth(options.size.width), idx));
                                }

                            } else {
                                // dumb method:
                                // bump down resolution if hit end of buffer
                                if (isDelayed) self._updateSequence(state.sequence + 1);
                            }
                        }

                        self._joinImageLoad(options.frame, stepAnimation);

                    }, Math.max(0, frameInterval - (new Date().getTime() - lastTime)));
                } else {
                    self.pause();
                }
            }
        },

        /*
         * Stop any currently playing animation.
         */
        pause: function() {
            window.clearTimeout(this._state.aniTimer);
            this._state.playing = false;
            this.element.trigger("pause");
            this._updateSequence();
        },

        /* A few convenience methods for moving around images */
        next: function() {
            this._setOption("frame", this.options.frame + 1);
        },

        previous: function() {
            this._setOption("frame", this.options.frame - 1);
        },

        frame: function(seqNum) {
            this._setOption("frame", seqNum);
        }
    });

})(jQuery);
<|endoftext|>


<commit_before>// Current detail wallet address
var currentDetailAddress = 0;

// Manage view state
// 0 = list addresses
// 1 = show single address
// 2 = show node stats
var viewState = 0;

// Init Transactions table
var TransT = $("#TransT").DataTable();

// Draw a row in transactions table
function drawTransRow(a, b, c, d, e, f, g, h) {
    var x = moment.unix(a);
    var z = "";

    if (h == e) {
        z = '

';
    } else {
        z = '

';
    }

    var t  = moment(x).format("HH:mm D MMM YYYY");

    //var TransT = $("#TransT").DataTable();
    TransT.row.add([c, d, t, e, z, f, b, g, c, e, f]);
}

// Draws all addresses in wallet file to page.
function drawAddresses(addresses) {
    console.log(addresses);

    viewState = 0;

    // Clear list first
    $('#walletlist').empty();

    // Loop all wallets, and present them
    var addressIndex = 0;
    $.each(addresses, function() {

        var thisAddress = this[0];
        var thisBalance = this[3];

        $('#walletlist').append(
            '

'
            + addressIndex
            + '
'
            + thisAddress +
            '
'
            + thisBalance +
            '

'
        );

        // Increment address index id
        addressIndex += 1;
    });

    // Remove dimmer
    $('.dimmer').hide();
}

// Gets detail about the running node
function getNodeInfo(hideDimmer = false) {

    viewState = 2;

    // Dimmer does not show for auto refreshes.
    if(hideDimmer == false) {
        $('.dimmer').show();
    }

    $('#show-wallet').hide();
    $('#show-all-wallets').hide();
    $('#show-node-stats').show();

    /*
    {
      "status": "ok",
      "unmined": 14009974.73883562,
      "network": "qrl testnet",
      "block_reward": 0.84203832,
      "emission": 6990025.26116438,
      "stake_validators": 5,
      "epoch": 0,
      "block_time": 50110414.432299145,
      "version": "alpha/0.27a",
      "staked_percentage_emission": 70.82,
      "blockheight": 30,
      "nodes": 6,
      "block_time_variance": 1503311097.9484582,
      "network_uptime": 8594.410389661789
    }
    */
    $.ajax({
        url: 'http://localhost:8080/api/stats',
        dataType: 'json',
        type: "GET",
        success: function(data) {
            $('.dimmer').hide();

            $('#network').text(':' + data.network);
            var x = moment.duration(data.network_uptime,'seconds').format("d[d] h[h] mm[min]");
            $('#uptime').text(x);
            $('#nodes').text(data.nodes);
            var x = moment.duration(data.block_time_variance,'seconds');
            x = Math.round(x/10)/100;
            $('#variance').text(x + 's');
            var x = moment.duration(data.block_time,'seconds').format("s[s]");
            $('#blocktime').text(x);
            $('#blockheight').text(data.blockheight);
            $('#validators').text(data.stake_validators);
            $('#PCemission').text(data.staked_percentage_emission + '%');
            $('#epoch').text(data.epoch);
            var x = data.emission;
            x = (Math.round(x * 10000)) / 10000;
            $('#emission').text(x);
            var x = data.unmined;
            x = (Math.round(x * 10000)) / 10000;
            $('#unmined').text(x);
            $('#reward').text(data.block_reward);
            $('#nodeversion').text(data.version);
        },
        error: function(data) {
            $('.dimmer').hide();
        }
    });

    // Mempool transactions
    $.ajax({
        url: 'http://localhost:8888/webwallet-mempool',
        dataType: 'text',
        type: "GET",
        success: function(data) {
            $('#mempooltransactions').text(data);
        },
        error: function(data) {
            $('.dimmer').hide();
        }
    });

    // Sync status
    $.ajax({
        url: 'http://localhost:8888/webwallet-sync',
        dataType: 'text',
        type: "GET",
        success: function(data) {
            $('#syncstatus').text(data);
        },
        error: function(data) {
            $('.dimmer').hide();
        }
    });

}

// Draws all addresses in wallet file to page.
function drawAddresses(addresses) {
    console.log(addresses);

    viewState = 0;

    // Clear list first
    $('#walletlist').empty();

    // Loop all wallets, and present them
    var addressIndex = 0;
    $.each(addresses, function() {

        var thisAddress = this[0];
        var thisBalance = this[3];

        $('#walletlist').append(
            '

'
            + addressIndex
            + '
'
            + thisAddress +
            '
'
            + thisBalance +
            '

'
        );

        // Increment address index id
        addressIndex += 1;
    });

    // Remove dimmer
    $('.dimmer').hide();
}

// Queries a list of all addresses from wallet, and passing to draw function to rendor to screen
function getAddresses(hideDimmer = false) {

    // Dimmer does not show for auto refreshes.
    if(hideDimmer == false) {
        $('.dimmer').show();
    }

    $('#show-wallet').hide();
    $('#show-node-stats').hide();
    $('#show-all-wallets').show();

    $.ajax({
        url: 'http://localhost:8888/webwallet-addresses',
        dataType: 'json',
        type: "GET",
        success: function(data) {
            drawAddresses(data);
        },
        error: function(data) {
            drawAddresses(data);
        }
    });
}

// Creates a new XMSS 8000sig address in local wallet, then refreshes addresses on page.
function createNewAddress() {
    $('.dimmer').show();
    $.ajax({
        url: 'http://localhost:8888/webwallet-create-new-address',
        dataType: 'json',
        type: "GET",
        success: function(data) {
            getAddresses();
        },
        error: function(data) {
           getAddresses();
        }
    });
}

// Draws address detail to page
function drawAddress(addresses, showAddressId, addressDetail, usdvalue) {
    //console.log(addresses);
    //console.log(addressDetail);

    viewState = 1;

    // Clear list first
    $('#walletdetail').empty();

    // Get some detail about this address
    // For now we're only using the address, and signatures
    var addressIndex = 0;
    var thisAddress = '';
    var sigSplit;
    $.each(addresses, function() {

        if(addressIndex == showAddressId) {
            currentDetailAddress = showAddressId;

            thisAddress = this[0];
            var thisBalance = this[3];
            var thisType = this[2];
            var thisNonce = this[4];
            var thisSigs = this[5];

            // Split up signature result
            // signatures left: 7993 (7993/8000)
            sigSplit = thisSigs.split('(')[1]; // 7993/8000)
            sigSplit = sigSplit.split(')')[0]; // 7993/8000
            sigSplit = sigSplit.split('/'); // [0] = 7993, [1] = 8000
        }

        // Increment address index id
        addressIndex += 1;
    });

    // Show address
    $('#addressHeading').text(thisAddress);

    // Only show these details if we get a successful reply from the API
    if(addressDetail.status == "ok") {
        $('#balance').text(addressDetail.state.balance);
        $('#nonce').text(addressDetail.state.nonce);
        $('#transactions').text(addressDetail.state.transactions);
        $('#sigsremaining').text(sigSplit[0]);

        TransT.clear();
        _.each(addressDetail.transactions, function(object) {

            var txHashLink = ''+object.txhash+'';

            drawTransRow(object.timestamp, object.amount, txHashLink, object.block, object.txfrom, object.txto, object.fee, addressDetail.state.address);
        });
        TransT.columns.adjust().draw(true);

        // Attempt to get USD Value of wallet
        $.ajax({
            url: 'http://cors-anywhere.herokuapp.com/https://www.folio.ninja/api/v1/quote?base=QRL&quote=USD&amount=' + addressDetail.state.balance,
            dataType: 'json',
            jsonpCallback: 'callback',
            success: function(folioNinjaReply) {
                $('#usdvalue').text("$" + folioNinjaReply.response.quote);
            }
        });

    } else {
        TransT.clear();
        $('#balance').text("0");
        $('#nonce').text("0");
        $('#transactions').text("0");
        $('#sigsremaining').text(sigSplit[0]);
        $('#usdvalue').text("0");
    }

    // Remove dimmer
    $('.dimmer').hide();
}

// Step 2 - grab details from local api
function showAddressStep2(addresses, addressIndex) {
    // Get wallet address
    var counter = 0;
    var thisAddress = '';
    $.each(addresses, function() {
        if(addressIndex == counter) {
            thisAddress = this[0];
        }
        counter += 1;
    });

    console.log(thisAddress);

    $.ajax({
        url: 'http://localhost:8080/api/address/' + thisAddress,
        success: function(addressDetail) {
            drawAddress(addresses, addressIndex, addressDetail);
        },
        error: function(addressDetail) {
            drawAddress(addresses, addressIndex, addressDetail);
        }
    });
}

// Gets details about a single address, and passes to draw function to rendor to screen.
function showAddress(addressIndex, hideDimmer = false) {

    // Dimmer does not show for auto refreshes.
    // Also don't reset data
    if(hideDimmer == false) {
        $('.dimmer').show();

        // Clear to and amount onload
        $('#to').val("");
        $('#amount').val("");
        $('#addressHeading').text("");
        $('#balance').text("");
        $('#nonce').text("");
        $('#transactions').text("");
        $('#sigsremaining').text("");
        $('#usdvalue').text("");
    }

    // Change view
    $('#show-all-wallets').hide();
    $('#show-node-stats').hide();
    $('#show-wallet').show();

    // Get addresses then draw
    $.ajax({
        url: 'http://localhost:8888/webwallet-addresses',
        dataType: 'json',
        type: "GET",
        success: function(addresses) {
            showAddressStep2(addresses, addressIndex);
        },
        error: function(addresses) {
            showAddressStep2(addresses, addressIndex);
        }
    });
}

// Modal to show txn result to screen
function drawTxnResult(txnResult) {

    // Refresh wallet page to update values
    showAddress(currentDetailAddress);

    // Show modal with results
    if(txnResult.status == "fail") {
        $('#domModalTitle').text('Transaction Failed!');
        $('#domModalBody').html('Error: '+txnResult.message);

        $('#domModal').modal('show');
    } else {
        $('#domModalTitle').text('Transaction Successful');
        $('#domModalBody').html('TXN Hash: '+txnResult.txnhash+'
'+
                                'From: ' + txnResult.from+'
'+
                                'To: ' + txnResult.to+'
'+
                                'Amount: ' + txnResult.amount+' Qaunta
'
                                );

        $('#domModal').modal('show');
    }

    // Hide loading pane
    $('.dimmer').hide();
}

// Creates a transaction in the network
function sendQuanta() {

    $('.dimmer').show();

    var from = currentDetailAddress;
    var to = $('#to').val();
    var amount = $('#amount').val();

    console.log("Send from index "+from+", to: "+ to + ", amount:"+amount)

    $.ajax({
        url: 'http://localhost:8888/webwallet-send',
        dataType: 'json',
        contentType: 'application/json',
        type: "POST",
        data: JSON.stringify( { "from": from, "to": to, "amount": amount } ),
        processData: false,
        success: function(data) {
            drawTxnResult(data);
        },
        error: function(data) {
            drawTxnResult(data);
        }
    });
}

// Show addresses on ready
$( document ).ready(function() {
    // Hide the detail pane
    $('#show-wallet').hide();
    $('#show-node-stats').hide();

    // Rendor addresses to screen.
    getAddresses();
});

// 20 second refresh
window.setInterval(function() {
    // Refresh addresses
    if(viewState == 0) {
        getAddresses(true);
    }

    // Refresh individual address view
    if(viewState == 1) {
        showAddress(currentDetailAddress, true);
    }

    // Refresh node stats
    if(viewState == 2) {
        getNodeInfo(true);
    }
}, 20000);
<commit_msg>Removing duplicate function, and fixing transactions table when empty<commit_after>// Current detail wallet address
var currentDetailAddress = 0;

// Manage view state
// 0 = list addresses
// 1 = show single address
// 2 = show node stats
var viewState = 0;

// Init Transactions table
var TransT = $("#TransT").DataTable();

// Draw a row in transactions table
function drawTransRow(a, b, c, d, e, f, g, h) {
    var x = moment.unix(a);
    var z = "";

    if (h == e) {
        z = '

';
    } else {
        z = '

';
    }

    var t  = moment(x).format("HH:mm D MMM YYYY");

    //var TransT = $("#TransT").DataTable();
    TransT.row.add([c, d, t, e, z, f, b, g, c, e, f]);
}

// Gets detail about the running node
function getNodeInfo(hideDimmer = false) {

    viewState = 2;

    // Dimmer does not show for auto refreshes.
    if(hideDimmer == false) {
        $('.dimmer').show();
    }

    $('#show-wallet').hide();
    $('#show-all-wallets').hide();
    $('#show-node-stats').show();

    /*
    {
      "status": "ok",
      "unmined": 14009974.73883562,
      "network": "qrl testnet",
      "block_reward": 0.84203832,
      "emission": 6990025.26116438,
      "stake_validators": 5,
      "epoch": 0,
      "block_time": 50110414.432299145,
      "version": "alpha/0.27a",
      "staked_percentage_emission": 70.82,
      "blockheight": 30,
      "nodes": 6,
      "block_time_variance": 1503311097.9484582,
      "network_uptime": 8594.410389661789
    }
    */
    $.ajax({
        url: 'http://localhost:8080/api/stats',
        dataType: 'json',
        type: "GET",
        success: function(data) {
            $('.dimmer').hide();

            $('#network').text(':' + data.network);
            var x = moment.duration(data.network_uptime,'seconds').format("d[d] h[h] mm[min]");
            $('#uptime').text(x);
            $('#nodes').text(data.nodes);
            var x = moment.duration(data.block_time_variance,'seconds');
            x = Math.round(x/10)/100;
            $('#variance').text(x + 's');
            var x = moment.duration(data.block_time,'seconds').format("s[s]");
            $('#blocktime').text(x);
            $('#blockheight').text(data.blockheight);
            $('#validators').text(data.stake_validators);
            $('#PCemission').text(data.staked_percentage_emission + '%');
            $('#epoch').text(data.epoch);
            var x = data.emission;
            x = (Math.round(x * 10000)) / 10000;
            $('#emission').text(x);
            var x = data.unmined;
            x = (Math.round(x * 10000)) / 10000;
            $('#unmined').text(x);
            $('#reward').text(data.block_reward);
            $('#nodeversion').text(data.version);
        },
        error: function(data) {
            $('.dimmer').hide();
        }
    });

    // Mempool transactions
    $.ajax({
        url: 'http://localhost:8888/webwallet-mempool',
        dataType: 'text',
        type: "GET",
        success: function(data) {
            $('#mempooltransactions').text(data);
        },
        error: function(data) {
            $('.dimmer').hide();
        }
    });

    // Sync status
    $.ajax({
        url: 'http://localhost:8888/webwallet-sync',
        dataType: 'text',
        type: "GET",
        success: function(data) {
            $('#syncstatus').text(data);
        },
        error: function(data) {
            $('.dimmer').hide();
        }
    });

}

// Draws all addresses in wallet file to page.
function drawAddresses(addresses) {
    console.log(addresses);

    viewState = 0;

    // Clear list first
    $('#walletlist').empty();

    // Loop all wallets, and present them
    var addressIndex = 0;
    $.each(addresses, function() {

        var thisAddress = this[0];
        var thisBalance = this[3];

        $('#walletlist').append(
            '

'
            + addressIndex
            + '
'
            + thisAddress +
            '
'
            + thisBalance +
            '

'
        );

        // Increment address index id
        addressIndex += 1;
    });

    // Remove dimmer
    $('.dimmer').hide();
}

// Queries a list of all addresses from wallet, and passing to draw function to rendor to screen
function getAddresses(hideDimmer = false) {

    // Dimmer does not show for auto refreshes.
    if(hideDimmer == false) {
        $('.dimmer').show();
    }

    $('#show-wallet').hide();
    $('#show-node-stats').hide();
    $('#show-all-wallets').show();

    $.ajax({
        url: 'http://localhost:8888/webwallet-addresses',
        dataType: 'json',
        type: "GET",
        success: function(data) {
            drawAddresses(data);
        },
        error: function(data) {
            drawAddresses(data);
        }
    });
}

// Creates a new XMSS 8000sig address in local wallet, then refreshes addresses on page.
function createNewAddress() {
    $('.dimmer').show();
    $.ajax({
        url: 'http://localhost:8888/webwallet-create-new-address',
        dataType: 'json',
        type: "GET",
        success: function(data) {
            getAddresses();
        },
        error: function(data) {
           getAddresses();
        }
    });
}

// Draws address detail to page
function drawAddress(addresses, showAddressId, addressDetail, usdvalue) {
    //console.log(addresses);
    //console.log(addressDetail);

    viewState = 1;

    // Clear list first
    $('#walletdetail').empty();

    // Get some detail about this address
    // For now we're only using the address, and signatures
    var addressIndex = 0;
    var thisAddress = '';
    var sigSplit;
    $.each(addresses, function() {

        if(addressIndex == showAddressId) {
            currentDetailAddress = showAddressId;

            thisAddress = this[0];
            var thisBalance = this[3];
            var thisType = this[2];
            var thisNonce = this[4];
            var thisSigs = this[5];

            // Split up signature result
            // signatures left: 7993 (7993/8000)
            sigSplit = thisSigs.split('(')[1]; // 7993/8000)
            sigSplit = sigSplit.split(')')[0]; // 7993/8000
            sigSplit = sigSplit.split('/'); // [0] = 7993, [1] = 8000
        }

        // Increment address index id
        addressIndex += 1;
    });

    // Show address
    $('#addressHeading').text(thisAddress);

    // Only show these details if we get a successful reply from the API
    if(addressDetail.status == "ok") {
        $('#balance').text(addressDetail.state.balance);
        $('#nonce').text(addressDetail.state.nonce);
        $('#transactions').text(addressDetail.state.transactions);
        $('#sigsremaining').text(sigSplit[0]);

        TransT.clear();
        _.each(addressDetail.transactions, function(object) {

            var txHashLink = ''+object.txhash+'';

            drawTransRow(object.timestamp, object.amount, txHashLink, object.block, object.txfrom, object.txto, object.fee, addressDetail.state.address);
        });
        TransT.columns.adjust().draw(true);

        // Attempt to get USD Value of wallet
        $.ajax({
            url: 'http://cors-anywhere.herokuapp.com/https://www.folio.ninja/api/v1/quote?base=QRL&quote=USD&amount=' + addressDetail.state.balance,
            dataType: 'json',
            jsonpCallback: 'callback',
            success: function(folioNinjaReply) {
                $('#usdvalue').text("$" + folioNinjaReply.response.quote);
            }
        });

    } else {
        console.log("clear");
        TransT.clear();
        TransT.columns.adjust().draw(true);
        $('#balance').text("0");
        $('#nonce').text("0");
        $('#transactions').text("0");
        $('#sigsremaining').text(sigSplit[0]);
        $('#usdvalue').text("0");
    }

    // Remove dimmer
    $('.dimmer').hide();
}

// Step 2 - grab details from local api
function showAddressStep2(addresses, addressIndex) {
    // Get wallet address
    var counter = 0;
    var thisAddress = '';
    $.each(addresses, function() {
        if(addressIndex == counter) {
            thisAddress = this[0];
        }
        counter += 1;
    });

    console.log(thisAddress);

    $.ajax({
        url: 'http://localhost:8080/api/address/' + thisAddress,
        success: function(addressDetail) {
            drawAddress(addresses, addressIndex, addressDetail);
        },
        error: function(addressDetail) {
            drawAddress(addresses, addressIndex, addressDetail);
        }
    });
}

// Gets details about a single address, and passes to draw function to rendor to screen.
function showAddress(addressIndex, hideDimmer = false) {

    // Dimmer does not show for auto refreshes.
    // Also don't reset data
    if(hideDimmer == false) {
        $('.dimmer').show();

        // Clear to and amount onload
        $('#to').val("");
        $('#amount').val("");
        $('#addressHeading').text("");
        $('#balance').text("");
        $('#nonce').text("");
        $('#transactions').text("");
        $('#sigsremaining').text("");
        $('#usdvalue').text("");
    }

    // Change view
    $('#show-all-wallets').hide();
    $('#show-node-stats').hide();
    $('#show-wallet').show();

    // Get addresses then draw
    $.ajax({
        url: 'http://localhost:8888/webwallet-addresses',
        dataType: 'json',
        type: "GET",
        success: function(addresses) {
            showAddressStep2(addresses, addressIndex);
        },
        error: function(addresses) {
            showAddressStep2(addresses, addressIndex);
        }
    });
}

// Modal to show txn result to screen
function drawTxnResult(txnResult) {

    // Refresh wallet page to update values
    showAddress(currentDetailAddress);

    // Show modal with results
    if(txnResult.status == "fail") {
        $('#domModalTitle').text('Transaction Failed!');
        $('#domModalBody').html('Error: '+txnResult.message);

        $('#domModal').modal('show');
    } else {
        $('#domModalTitle').text('Transaction Successful');
        $('#domModalBody').html('TXN Hash: '+txnResult.txnhash+'
'+
                                'From: ' + txnResult.from+'
'+
                                'To: ' + txnResult.to+'
'+
                                'Amount: ' + txnResult.amount+' Qaunta
'
                                );

        $('#domModal').modal('show');
    }

    // Hide loading pane
    $('.dimmer').hide();
}

// Creates a transaction in the network
function sendQuanta() {

    $('.dimmer').show();

    var from = currentDetailAddress;
    var to = $('#to').val();
    var amount = $('#amount').val();

    console.log("Send from index "+from+", to: "+ to + ", amount:"+amount)

    $.ajax({
        url: 'http://localhost:8888/webwallet-send',
        dataType: 'json',
        contentType: 'application/json',
        type: "POST",
        data: JSON.stringify( { "from": from, "to": to, "amount": amount } ),
        processData: false,
        success: function(data) {
            drawTxnResult(data);
        },
        error: function(data) {
            drawTxnResult(data);
        }
    });
}

// Show addresses on ready
$( document ).ready(function() {
    // Hide the detail pane
    $('#show-wallet').hide();
    $('#show-node-stats').hide();

    // Rendor addresses to screen.
    getAddresses();
});

// 20 second refresh
window.setInterval(function() {
    // Refresh addresses
    if(viewState == 0) {
        getAddresses(true);
    }

    // Refresh individual address view
    if(viewState == 1) {
        showAddress(currentDetailAddress, true);
    }

    // Refresh node stats
    if(viewState == 2) {
        getNodeInfo(true);
    }
}, 20000);
<|endoftext|>


<commit_before>// ==========================================================================
// Project:   MySystem.DiagramView
// Copyright: @2011 Concord Consortium
// ==========================================================================
/*globals MySystem RaphaelViews */

sc_require('views/terminal');

/** @class

  Display class for displaying a MySystem diagram (a set of nodes and links) as a collection.
  Is a drop target for nodes from the NodesPaletteView, invokes statechart's 'addNode' action upon drop.

  @extends SC.View
*/
MySystem.DiagramView = RaphaelViews.RaphaelCollectionView.extend(
/** @scope MySystem.DiagramView.prototype */ {

  exampleView: MySystem.NodeView,

  // This tels the collection view to deselect everything
  // if the nothing is clicked on
  allowDeselectAll: YES,

  selectedLinks: [],

  _isDragging: NO,

  // SC.DropTarget
  //   The methods below are all part o the SC.DropTarget protocol
  //   and must be implemented to function as a drop target.
  isDropTarget: YES,
  dragStarted: function(drag, evt) {},
  dragEntered: function(drag, evt) {},
  dragUpdated: function(drag, evt) {},
  dragExited: function(drag, evt) {},
  dragEnded: function(drag, evt) {},
  acceptDragOperation: function(drag, op) {
    return YES;
  },

  computeDragOperations: function () {
    return SC.DRAG_LINK;      // TODO this happens to work, but are we using the right semantics?
  },

  performDragOperation: function (drag, op) {
    var canvasOffset = this.get('canvasView').$().offset(),

        // The numbers at the end are to account for the difference in size of the AddButtonView
        // compared to the Node view.  Mostly likely those could be computed.

        newNodeX = drag.location.x - drag.ghostOffset.x - canvasOffset.left + 17,
        newNodeY = drag.location.y - drag.ghostOffset.y - canvasOffset.top  + 9,

        newNodeAttributes = {
          title:    drag.data.title,
          image:    drag.data.image,
          x:        newNodeX,
          y:        newNodeY,
          nodeType: drag.data.uuid
        };

    // Create the node
    MySystem.statechart.sendEvent('addNode', newNodeAttributes);

    // De-select other diagram objects and select
    return SC.DRAG_COPY;
  },

  mouseDown: function (evt) {
    var handledBySuper = sc_super();

    if (handledBySuper) {
      this._dragX = evt.pageX;
      this._dragY = evt.pageY;
      this.invokeLast(this._startDrag);
    }

    return handledBySuper;
  },

  mouseDragged: function (evt) {
    this._drag(evt);
    return sc_super();
  },

  mouseUp: function (evt) {
    // don't forget to handle the last mouse movement!
    this._drag(evt);

    // just to be sure we don't drag views around in case we get sent more mouseDragged events
    this._isDragging = NO;
    this._dragMap = [];
    this._draggedViews = [];

    return sc_super();
  },

  _startDrag: function () {
    var containerView  = this.get('containerView') || this,
        $canvasView    = this.get('canvasView').$();

    this._isDragging = YES;

    this._dragFrame = {
      left:   0,
      top:    0,
      right:  $canvasView.innerWidth(),
      bottom: $canvasView.innerHeight()
    };

    this._draggedViews = containerView.get('childViews').filter(function (view) {
      return view.get('isSelected') && view.get('content').kindOf(MySystem.Node);
    });

    this._dragMap = this._draggedViews.map( function (view) {
      return {
        x:      view.get('x'),
        y:      view.get('y'),
        width:  view.get('bodyWidth') + view.get('borderWidth') / 2,
        height: view.get('bodyHeight') + view.get('borderWidth') / 2
      };
    });
  },

  _drag: function (evt) {
    var dx = evt.pageX - this._dragX,
        dy = evt.pageY - this._dragY;

    if (!this._isDragging) {
      return NO;
    }

    // clear the select properties of mouseDownInfo
    // TODO: change the conditions, probably the nodes shouldn't move eitehr until
    //   these conditiosn are met
    if(Math.abs(dx) > 5 || Math.abs(dy) > 5){
      this.mouseDownInfo.shouldReselect = NO;
      this.mouseDownInfo.shouldSelect = NO;
    }

    this._dragX = evt.pageX;
    this._dragY = evt.pageY;

    this._updateDragMap(dx, dy);
    this._adjustDraggedViews();
  },

  _updateDragMap: function (dx, dy) {
    for (var i = 0; i < this._dragMap.length; i++) {
      this._dragMap[i].x += dx;
      this._dragMap[i].y += dy;
    }
  },

  _adjustDraggedViews: function () {
    var i, x, y, view, width, height;

    for (i = 0; i < this._draggedViews.length; i++) {
      x      = this._dragMap[i].x;
      y      = this._dragMap[i].y;
      width  = this._dragMap[i].width;
      height = this._dragMap[i].height;

      if (x          < this._dragFrame.left)   x = this._dragFrame.left;
      if (x + width  > this._dragFrame.right)  x = this._dragFrame.right - width;
      if (y          < this._dragFrame.top)    y = this._dragFrame.top;
      if (y + height > this._dragFrame.bottom) y = this._dragFrame.bottom - height;

      view = this._draggedViews[i];

      view.beginPropertyChanges();
      view.set('x', x);
      view.set('y', y);
      view.endPropertyChanges();
    }
  }

});
<commit_msg>Allow keyboard delete in diagram view.<commit_after>// ==========================================================================
// Project:   MySystem.DiagramView
// Copyright: @2011 Concord Consortium
// ==========================================================================
/*globals MySystem RaphaelViews */

sc_require('views/terminal');

/** @class

  Display class for displaying a MySystem diagram (a set of nodes and links) as a collection.
  Is a drop target for nodes from the NodesPaletteView, invokes statechart's 'addNode' action upon drop.

  @extends SC.View
*/
MySystem.DiagramView = RaphaelViews.RaphaelCollectionView.extend(
/** @scope MySystem.DiagramView.prototype */ {

  exampleView: MySystem.NodeView,

  // This tels the collection view to deselect everything
  // if the nothing is clicked on
  allowDeselectAll: YES,

  // Allow keyboard delete operations:
  acceptsFirstResponder: YES,
  canDeleteContent: YES,

  selectedLinks: [],

  _isDragging: NO,

  // SC.DropTarget
  //   The methods below are all part o the SC.DropTarget protocol
  //   and must be implemented to function as a drop target.
  isDropTarget: YES,
  dragStarted: function(drag, evt) {},
  dragEntered: function(drag, evt) {},
  dragUpdated: function(drag, evt) {},
  dragExited: function(drag, evt) {},
  dragEnded: function(drag, evt) {},
  acceptDragOperation: function(drag, op) {
    return YES;
  },

  computeDragOperations: function () {
    return SC.DRAG_LINK;      // TODO this happens to work, but are we using the right semantics?
  },

  performDragOperation: function (drag, op) {
    var canvasOffset = this.get('canvasView').$().offset(),

        // The numbers at the end are to account for the difference in size of the AddButtonView
        // compared to the Node view.  Mostly likely those could be computed.

        newNodeX = drag.location.x - drag.ghostOffset.x - canvasOffset.left + 17,
        newNodeY = drag.location.y - drag.ghostOffset.y - canvasOffset.top  + 9,

        newNodeAttributes = {
          title:    drag.data.title,
          image:    drag.data.image,
          x:        newNodeX,
          y:        newNodeY,
          nodeType: drag.data.uuid
        };

    // Create the node
    MySystem.statechart.sendEvent('addNode', newNodeAttributes);

    // De-select other diagram objects and select
    return SC.DRAG_COPY;
  },

  mouseDown: function (evt) {
    var handledBySuper = sc_super();

    if (handledBySuper) {
      this._dragX = evt.pageX;
      this._dragY = evt.pageY;
      this.invokeLast(this._startDrag);
    }

    return handledBySuper;
  },

  mouseDragged: function (evt) {
    this._drag(evt);
    return sc_super();
  },

  mouseUp: function (evt) {
    // don't forget to handle the last mouse movement!
    this._drag(evt);

    // just to be sure we don't drag views around in case we get sent more mouseDragged events
    this._isDragging = NO;
    this._dragMap = [];
    this._draggedViews = [];

    return sc_super();
  },

  _startDrag: function () {
    var containerView  = this.get('containerView') || this,
        $canvasView    = this.get('canvasView').$();

    this._isDragging = YES;

    this._dragFrame = {
      left:   0,
      top:    0,
      right:  $canvasView.innerWidth(),
      bottom: $canvasView.innerHeight()
    };

    this._draggedViews = containerView.get('childViews').filter(function (view) {
      return view.get('isSelected') && view.get('content').kindOf(MySystem.Node);
    });

    this._dragMap = this._draggedViews.map( function (view) {
      return {
        x:      view.get('x'),
        y:      view.get('y'),
        width:  view.get('bodyWidth') + view.get('borderWidth') / 2,
        height: view.get('bodyHeight') + view.get('borderWidth') / 2
      };
    });
  },

  _drag: function (evt) {
    var dx = evt.pageX - this._dragX,
        dy = evt.pageY - this._dragY;

    if (!this._isDragging) {
      return NO;
    }

    // clear the select properties of mouseDownInfo
    // TODO: change the conditions, probably the nodes shouldn't move eitehr until
    //   these conditiosn are met
    if(Math.abs(dx) > 5 || Math.abs(dy) > 5){
      this.mouseDownInfo.shouldReselect = NO;
      this.mouseDownInfo.shouldSelect = NO;
    }

    this._dragX = evt.pageX;
    this._dragY = evt.pageY;

    this._updateDragMap(dx, dy);
    this._adjustDraggedViews();
  },

  _updateDragMap: function (dx, dy) {
    for (var i = 0; i < this._dragMap.length; i++) {
      this._dragMap[i].x += dx;
      this._dragMap[i].y += dy;
    }
  },

  _adjustDraggedViews: function () {
    var i, x, y, view, width, height;

    for (i = 0; i < this._draggedViews.length; i++) {
      x      = this._dragMap[i].x;
      y      = this._dragMap[i].y;
      width  = this._dragMap[i].width;
      height = this._dragMap[i].height;

      if (x          < this._dragFrame.left)   x = this._dragFrame.left;
      if (x + width  > this._dragFrame.right)  x = this._dragFrame.right - width;
      if (y          < this._dragFrame.top)    y = this._dragFrame.top;
      if (y + height > this._dragFrame.bottom) y = this._dragFrame.bottom - height;

      view = this._draggedViews[i];

      view.beginPropertyChanges();
      view.set('x', x);
      view.set('y', y);
      view.endPropertyChanges();
    }
  }

});
<|endoftext|>


<commit_before>/**
 * Display a nice easy to use multiselect list
 * @Version: 1.0
 * @Author: Patrick Springstubbe
 * @Contact: @JediNobleclem
 * @Website: springstubbe.us
 * @Source: https://github.com/nobleclem/jQuery-MultiSelect
 *
 * Usage:
 *     $('select[multiple]').multiselect();
 *     $('select[multiple]').multiselect({ placeholder: 'Select options' });
 *
 **/
(function($){
    var msCounter = 1;
    $.fn.multiselect = function( options ){
        var defaults = {
            placeholder   : '',    // text to use in dummy input
            columns       : 1,     // how many columns should be use to show options
            maxWidth      : null,  // maximum width of option overlay (or selector)
            minHeight     : 200,   // minimum height of option overlay
            maxHeight     : null,  // maximum height of option overlay
            showCheckbox  : true,  // display the checkbox to the user

            // @NOTE: these are for future development
            minSelect     : false, // minimum number of items that can be selected
            maxSelect     : false, // maximum number of items that can be selected
            groupSelect   : false, // select entire optgroup
            selectAllText : false, // add select all option
            searchOptions : false, // enable option search/filtering
        }
        options = $.extend( defaults, options );

        var func = {
            addOption: function( container, option )
            {
                container.text( $(option).text() );
                container.prepend(
                    $('<input type="checkbox" value="" title="" />')
                        .val( $(option).val() )
                        .attr( 'title', $(option).text() )
                        .attr( 'id', 'ms-opt-'+ msCounter )
                );

                if( $(option).attr( 'selected' ) ) {
                    container.addClass('selected');
                    container.find( 'input[type="checkbox"]' ).attr( 'checked', 'checked' );
                }

                var label = $('').attr( 'for', 'ms-opt-'+ msCounter );
                container.wrapInner( label );

                if( !options.showCheckbox ) {
                    container.find('input[id="ms-opt-'+ msCounter +'"]').hide();
                }

                msCounter = msCounter + 1;
            },

            ieVersion: function()
            {
                var myNav = navigator.userAgent.toLowerCase();
                return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false;
            }
        };

        // menuize each list
        return this.each(function(){
            // make sure this is a select list
            if( this.nodeName != 'SELECT' ) {
                return false;
            }

            // add option container
            $(this).after('
<button>None Selected</button>

');
            var placeholder = $(this).next('.ms-options-wrap').find('> button:first-child');
            var optionsWrap = $(this).next('.ms-options-wrap').find('> .ms-options');
            var optionsList = optionsWrap.find('> ul');
            var hasOptGroup = $(this).find('optgroup').length ? true : false;

            var maxWidth = null;
            if( typeof options.width == 'number' ) {
                optionsWrap.parent().css( 'position', 'relative' );
                maxWidth = options.width;
            }
            else if( typeof options.width == 'string' ) {
                $( options.width ).css( 'position', 'relative' );
                maxWidth = '100%';
            }
            else {
                optionsWrap.parent().css( 'position', 'relative' );
            }

            var maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
            if( options.maxHeight ) {
                maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
                maxHeight = maxHeight < options.minHeight ? options.minHeight : maxheight;
            }

            maxHeight = maxHeight < options.minHeight ? options.minHeight : maxHeight;

            optionsWrap.css({
                maxWidth : maxWidth,
                minHeight: options.minHeight,
                maxHeight: maxHeight,
                overflow : 'auto'
            }).hide();

            // isolate options scroll
            // @source: https://github.com/nobleclem/jQuery-IsolatedScroll
            optionsWrap.bind( 'touchmove mousewheel DOMMouseScroll', function ( e ) {
                if( ($(this).outerHeight() < $(this)[0].scrollHeight) ) {
                    var e0 = e.originalEvent,
                        delta = e0.wheelDelta || -e0.detail;

                    if( ($(this).outerHeight() + $(this)[0].scrollTop) > $(this)[0].scrollHeight ) {
                        e.preventDefault();
                        this.scrollTop += ( delta < 0 ? 1 : -1 );
                    }
                }
            });

            // hide options menus if click happens off of the list placeholder button
            $(window).off('click.ms-hideopts').on('click.ms-hideopts', function( event ){
                if( !$(event.target).closest('.ms-options-wrap').length ) {
                    $('.ms-options-wrap > .ms-options:visible').hide();
                }
            });

            // disable button action
            placeholder.bind('mousedown',function(){
                // hide other menus before showing this one
                $('.ms-options-wrap > .ms-options:visible').each(function(){
                    if( $(this).parent().prev()[0] != optionsWrap.parent().prev()[0] ) {
                        $(this).hide();
                    }
                });

                // show/hide options
                optionsWrap.toggle();

                // recalculate height
                if( optionsWrap.is(':visible') ) {
                    optionsWrap.css( 'maxHeight', '' );

                    var maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
                    if( options.maxHeight ) {
                        maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
                        maxHeight = maxHeight < options.minHeight ? options.minHeight : maxheight;
                    }
                    maxHeight = maxHeight < options.minHeight ? options.minHeight : maxHeight;

                    optionsWrap.css( 'maxHeight', maxHeight );
                }
            }).click(function( event ){ event.preventDefault(); });

            // add placeholder copy
            if( options.placeholder ) {
                placeholder.text( options.placeholder );
            }

            // hide native select list
            $(this).hide();

            // add options to wrapper
            $(this).children().each(function(){
                var container = $('

');

                // add group of items
                if( this.nodeName == 'OPTGROUP' ) {
                    container.addClass('optgroup');
                    container.append(''+ $(this).attr('label') +'');
                    container.find('> .label').css({
                        clear: 'both'
                    });
                    container.append('

');

                    $(this).children('option').each(function(){
                        var gContainer = $('

');
                        func.addOption( gContainer, this );

                        container.find('> ul').append( gContainer );
                    });
                }
                else if( this.nodeName == 'OPTION' ) {
                    func.addOption( container, this );
                }
                else {
                    // bad option
                    return true;
                }

                optionsList.append( container );
            });

            // COLUMNIZE
            if( hasOptGroup ) {
                // float non grouped options
                optionsList.find('> li:not(.optgroup)').css({
                    float: 'left',
                    width: (100 / options.columns) +'%'
                });

                // add CSS3 column styles
                optionsList.find('li.optgroup').css({
                    clear: 'both'
                }).find('> ul').css({
                    'column-count'        : options.columns,
                    'column-gap'          : 0,
                    '-webkit-column-count': options.columns,
                    '-webkit-column-gap'  : 0,
                    '-moz-column-count'   : options.columns,
                    '-moz-column-gap'     : 0
                });

                // for crappy IE versions float grouped options
                if( func.ieVersion() && func.ieVersion() < 10 ) {
                    optionsList.find('li.optgroup > ul > li').css({
                        float: 'left',
                        width: (100 / options.columns) +'%'
                    });
                }
            }
            else {
                // add CSS3 column styles
                optionsList.css({
                    'column-count'        : options.columns,
                    'column-gap'          : 0,
                    '-webkit-column-count': options.columns,
                    '-webkit-column-gap'  : 0,
                    '-moz-column-count'   : options.columns,
                    '-moz-column-gap'     : 0
                });

                // for crappy IE versions float grouped options
                if( func.ieVersion() && func.ieVersion() < 10 ) {
                    optionsList.find('> li').css({
                        float: 'left',
                        width: (100 / options.columns) +'%'
                    });
                }
            }

            // BIND SELECT ACTION
            optionsWrap.find('input[type="checkbox"]').click(function(){
                $(this).closest( 'li' ).toggleClass( 'selected' );

                var select = optionsWrap.parent().prev();

                // toggle clicked option
                select.find('option[value="'+ $(this).val() +'"]').attr(
                    'selected', $(this).is(':checked')
                ).closest('select').trigger('change');

                // get selected options
                var selOpts = [];
                select.find('option[selected="selected"]').each(function(){
                    selOpts.push( $(this).text() );
                });

                // UPDATE PLACEHOLDER TEXT WITH OPTIONS SELECTED
                placeholder.text( selOpts.join( ', ' ) );
                var copy = placeholder.clone().css({
                    display   : 'inline',
                    width     : 'auto',
                    visibility: 'hidden'
                }).appendTo( optionsWrap.parent() );

                // if copy is larger than button width use "# selected"
                if( copy.width() > placeholder.width() ) {
                    placeholder.text( selOpts.length +' selected' );
                }
                // if options selected then use those
                else if( selOpts.length ) {
                    placeholder.text( selOpts.join( ', ' ) );
                }
                // replace placeholder text
                else {
                    placeholder.text( options.placeholder );
                }

                // remove dummy element
                copy.remove();

            }).each(function( idx ){
                if( $(this).css('display').match(/block$/) ) {
                    var checkboxWidth = $(this).outerWidth();
                        checkboxWidth = checkboxWidth ? checkboxWidth : 15;

                    $(this).closest('label').css(
                        'padding-left',
                        (parseInt( $(this).closest('label').css('padding-left') ) * 2) + checkboxWidth
                    );
                }

                if( $(this).is(':checked') ) {
                    $(this).trigger('click');
                }
            });
        });
    };
}(jQuery));
<commit_msg>Fixed issues around preselected options.<commit_after>/**
 * Display a nice easy to use multiselect list
 * @Version: 1.1
 * @Author: Patrick Springstubbe
 * @Contact: @JediNobleclem
 * @Website: springstubbe.us
 * @Source: https://github.com/nobleclem/jQuery-MultiSelect
 *
 * Usage:
 *     $('select[multiple]').multiselect();
 *     $('select[multiple]').multiselect({ placeholder: 'Select options' });
 *
 **/
(function($){
    var msCounter = 1;
    $.fn.multiselect = function( options ){
        var defaults = {
            placeholder   : '',    // text to use in dummy input
            columns       : 1,     // how many columns should be use to show options
            maxWidth      : null,  // maximum width of option overlay (or selector)
            minHeight     : 200,   // minimum height of option overlay
            maxHeight     : null,  // maximum height of option overlay
            showCheckbox  : true,  // display the checkbox to the user

            // @NOTE: these are for future development
            minSelect     : false, // minimum number of items that can be selected
            maxSelect     : false, // maximum number of items that can be selected
            groupSelect   : false, // select entire optgroup
            selectAllText : false, // add select all option
            searchOptions : false, // enable option search/filtering
        }
        options = $.extend( defaults, options );

        var func = {
            addOption: function( container, option )
            {
                container.text( $(option).text() );
                container.prepend(
                    $('<input type="checkbox" value="" title="" />')
                        .val( $(option).val() )
                        .attr( 'title', $(option).text() )
                        .attr( 'id', 'ms-opt-'+ msCounter )
                );

                if( $(option).attr( 'selected' ) ) {
                    container.addClass('selected');
                    container.find( 'input[type="checkbox"]' ).attr( 'checked', 'checked' );
                }

                var label = $('').attr( 'for', 'ms-opt-'+ msCounter );
                container.wrapInner( label );

                if( !options.showCheckbox ) {
                    container.find('input[id="ms-opt-'+ msCounter +'"]').hide();
                }

                msCounter = msCounter + 1;
            },

            ieVersion: function()
            {
                var myNav = navigator.userAgent.toLowerCase();
                return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false;
            }
        };

        // menuize each list
        return this.each(function(){
            // make sure this is a select list
            if( this.nodeName != 'SELECT' ) {
                return false;
            }

            // add option container
            $(this).after('
<button>None Selected</button>

');
            var placeholder = $(this).next('.ms-options-wrap').find('> button:first-child');
            var optionsWrap = $(this).next('.ms-options-wrap').find('> .ms-options');
            var optionsList = optionsWrap.find('> ul');
            var hasOptGroup = $(this).find('optgroup').length ? true : false;

            var maxWidth = null;
            if( typeof options.width == 'number' ) {
                optionsWrap.parent().css( 'position', 'relative' );
                maxWidth = options.width;
            }
            else if( typeof options.width == 'string' ) {
                $( options.width ).css( 'position', 'relative' );
                maxWidth = '100%';
            }
            else {
                optionsWrap.parent().css( 'position', 'relative' );
            }

            var maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
            if( options.maxHeight ) {
                maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
                maxHeight = maxHeight < options.minHeight ? options.minHeight : maxheight;
            }

            maxHeight = maxHeight < options.minHeight ? options.minHeight : maxHeight;

            optionsWrap.css({
                maxWidth : maxWidth,
                minHeight: options.minHeight,
                maxHeight: maxHeight,
                overflow : 'auto'
            }).hide();

            // isolate options scroll
            // @source: https://github.com/nobleclem/jQuery-IsolatedScroll
            optionsWrap.bind( 'touchmove mousewheel DOMMouseScroll', function ( e ) {
                if( ($(this).outerHeight() < $(this)[0].scrollHeight) ) {
                    var e0 = e.originalEvent,
                        delta = e0.wheelDelta || -e0.detail;

                    if( ($(this).outerHeight() + $(this)[0].scrollTop) > $(this)[0].scrollHeight ) {
                        e.preventDefault();
                        this.scrollTop += ( delta < 0 ? 1 : -1 );
                    }
                }
            });

            // hide options menus if click happens off of the list placeholder button
            $(window).off('click.ms-hideopts').on('click.ms-hideopts', function( event ){
                if( !$(event.target).closest('.ms-options-wrap').length ) {
                    $('.ms-options-wrap > .ms-options:visible').hide();
                }
            });

            // disable button action
            placeholder.bind('mousedown',function(){
                // hide other menus before showing this one
                $('.ms-options-wrap > .ms-options:visible').each(function(){
                    if( $(this).parent().prev()[0] != optionsWrap.parent().prev()[0] ) {
                        $(this).hide();
                    }
                });

                // show/hide options
                optionsWrap.toggle();

                // recalculate height
                if( optionsWrap.is(':visible') ) {
                    optionsWrap.css( 'maxHeight', '' );

                    var maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
                    if( options.maxHeight ) {
                        maxHeight = ($(window).height() - optionsWrap.offset().top - 20);
                        maxHeight = maxHeight < options.minHeight ? options.minHeight : maxheight;
                    }
                    maxHeight = maxHeight < options.minHeight ? options.minHeight : maxHeight;

                    optionsWrap.css( 'maxHeight', maxHeight );
                }
            }).click(function( event ){ event.preventDefault(); });

            // add placeholder copy
            if( options.placeholder ) {
                placeholder.text( options.placeholder );
            }

            // hide native select list
            //$(this).hide();

            // add options to wrapper
            $(this).children().each(function(){
                var container = $('

');

                // add group of items
                if( this.nodeName == 'OPTGROUP' ) {
                    container.addClass('optgroup');
                    container.append(''+ $(this).attr('label') +'');
                    container.find('> .label').css({
                        clear: 'both'
                    });
                    container.append('

');

                    $(this).children('option').each(function(){
                        var gContainer = $('

');
                        func.addOption( gContainer, this );

                        container.find('> ul').append( gContainer );
                    });
                }
                else if( this.nodeName == 'OPTION' ) {
                    func.addOption( container, this );
                }
                else {
                    // bad option
                    return true;
                }

                optionsList.append( container );
            });

            // COLUMNIZE
            if( hasOptGroup ) {
                // float non grouped options
                optionsList.find('> li:not(.optgroup)').css({
                    float: 'left',
                    width: (100 / options.columns) +'%'
                });

                // add CSS3 column styles
                optionsList.find('li.optgroup').css({
                    clear: 'both'
                }).find('> ul').css({
                    'column-count'        : options.columns,
                    'column-gap'          : 0,
                    '-webkit-column-count': options.columns,
                    '-webkit-column-gap'  : 0,
                    '-moz-column-count'   : options.columns,
                    '-moz-column-gap'     : 0
                });

                // for crappy IE versions float grouped options
                if( func.ieVersion() && func.ieVersion() < 10 ) {
                    optionsList.find('li.optgroup > ul > li').css({
                        float: 'left',
                        width: (100 / options.columns) +'%'
                    });
                }
            }
            else {
                // add CSS3 column styles
                optionsList.css({
                    'column-count'        : options.columns,
                    'column-gap'          : 0,
                    '-webkit-column-count': options.columns,
                    '-webkit-column-gap'  : 0,
                    '-moz-column-count'   : options.columns,
                    '-moz-column-gap'     : 0
                });

                // for crappy IE versions float grouped options
                if( func.ieVersion() && func.ieVersion() < 10 ) {
                    optionsList.find('> li').css({
                        float: 'left',
                        width: (100 / options.columns) +'%'
                    });
                }
            }

            // BIND SELECT ACTION
            optionsWrap.find('input[type="checkbox"]').click(function(){
                $(this).closest( 'li' ).toggleClass( 'selected' );

                var select = optionsWrap.parent().prev();

                // toggle clicked option
                select.find('option[value="'+ $(this).val() +'"]').attr(
                    'selected', $(this).is(':checked')
                ).closest('select').trigger('change');

                updatePlaceholderText();
            }).each(function( idx ){
                if( $(this).css('display').match(/block$/) ) {
                    var checkboxWidth = $(this).outerWidth();
                        checkboxWidth = checkboxWidth ? checkboxWidth : 15;

                    $(this).closest('label').css(
                        'padding-left',
                        (parseInt( $(this).closest('label').css('padding-left') ) * 2) + checkboxWidth
                    );
                }
            });

            // update selected placeholder text
            var updatePlaceholderText = function(){
                var select = optionsWrap.parent().prev();

                // get selected options
                var selOpts = [];
                select.find('option[selected="selected"]').each(function(){
                    selOpts.push( $(this).text() );
                });

                // UPDATE PLACEHOLDER TEXT WITH OPTIONS SELECTED
                placeholder.text( selOpts.join( ', ' ) );
                var copy = placeholder.clone().css({
                    display   : 'inline',
                    width     : 'auto',
                    visibility: 'hidden'
                }).appendTo( optionsWrap.parent() );

                // if copy is larger than button width use "# selected"
                if( copy.width() > placeholder.width() ) {
                    placeholder.text( selOpts.length +' selected' );
                }
                // if options selected then use those
                else if( selOpts.length ) {
                    placeholder.text( selOpts.join( ', ' ) );
                }
                // replace placeholder text
                else {
                    placeholder.text( options.placeholder );
                }

                // remove dummy element
                copy.remove();
            };

            updatePlaceholderText();
        });
    };
}(jQuery));
<|endoftext|>