<commit_before>/*
 * @copyright Adam Benda, 2016
 */

/**
 * @class
 * @classdesc Ball
 */

/**
 *
 * @param {Matter.Engine} engine -
 * @param {number} x - x coordinate
 * @param {number} y - y coordinate
 * @param {number} r - radius
 * @param {string} textureFile - png image containing texture
 * @param {string} id - identifier of this ball. Player has id=0
 * @returns {Ball}
 */
var Ball = function (engine, x, y, r, textureFile, id) {
    this.id = Math.round(Math.random() * 1000);
    this.engine = engine;
    this.world = engine.world;
    this.initX = x * this.engine.render.options.width;
    this.initY = y * this.engine.render.options.height;
    this.initR = r * this.engine.render.options.width;
    this.textureFile = textureFile;
    this.createBody(
            this.initX,
            this.initY,
            this.initR
            );
    Matter.Composite.add(this.world, this.body);

    this.distracted = 0;
    this.controlled = false;

    this.timeHooked = new Date();
    this.timeUnhooked = new Date();
};

Ball.prototype.toggleHook = function () {
    var world = this.world;

    if (!this.hooked) {
        //find a nearest
        //todo: to a separate function
        var target = null;
        var shortestDistance = 99999999;
        for (var i = 0; i < world.bodies.length; i++) {
            var body = world.bodies[i];
            if (body.isStatic) {

                //Check if current speed vector is compatible with
                //vector of rotation hooked to this body
                var centripetalVect = Matter.Vector.create(
                        body.position.x - this.body.position.x,
                        body.position.y - this.body.position.y
                        );
                var angle = Matter.Vector.dot(centripetalVect, this.body.velocity) /
                        (Matter.Vector.magnitude(centripetalVect) * Matter.Vector.magnitude(this.body.velocity));

                if ((Math.abs(angle) > 100 / Matter.Vector.magnitude(centripetalVect))) {
                    //not compatible vector
                    continue;
                }

                var thisDistance = Matter.Vector.magnitudeSquared(
                        Matter.Vector.sub(body.position, this.body.position)
                        );
                if (shortestDistance > thisDistance) {
                    shortestDistance = thisDistance;
                    target = body;
                }
            }
        }

        if (target === null) {
            //no target available
            return false;
        }

        this.rope = Matter.Constraint.create({bodyA: this.body, bodyB: target, stiffness: 4});
        this.body.onRopeGravity = 1;
        Matter.World.add(world, this.rope);
        //this.rope.render.strokeStyle = "#ff0000";
        this.hooked = true;
        this.timeHooked = new Date();
        return true;
    } else {
        Matter.World.remove(world, this.rope);

        this.body.onRopeGravity = 0;
        this.timeUnhooked = new Date();
        this.hooked = false;
    }

};

Ball.prototype.checkBoundaries = function () {
    if (this.hooked) {
        //if we are hooked up its ok
        return true;
    }

    if (
            this.engine.render.bounds.max.y < this.body.position.y - this.initR ||
            this.engine.render.bounds.min.x > this.body.position.x + this.initR ||
            this.engine.render.bounds.max.x < this.body.position.x - this.initR
            ) {
        return this.killed();

    }
    return true;
};

Ball.prototype.killed = function () {
    if (this.lives !== undefined) {
        this.lives--;
        if (this.lives < 0) {
            this.gamestate.gameOverSignal();
            return false;
        }
    }

    //unhook
    if (this.hooked) {
        this.toggleHook(this.world);
    }

    Matter.Composite.remove(this.world, this.body);
    if (this.lives === undefined || this.lives >= 0) {
        this.createBody(this.initX, this.initY, this.initR);
        Matter.Composite.add(this.world, this.body);
    }
    this.gamestate.ballKilledSignal(this.id);
    return true;
};

Ball.prototype.createBody = function (x, y, r) {
    this.body = Matter.Bodies.circle(x, y, r);
    this.body.force.x = 0;
    this.body.force.y = 0.1;
    this.body.frictionAir = 0;
    this.body.friction = 0;
    Matter.Body.setAngularVelocity(this.body, 0.01);
    Matter.Body.setMass(this.body, 5);
    if (this.textureFile && this.textureFile.length && this.textureFile.length > 0) {
        this.body.render.sprite.texture = this.textureFile;
    }
    this.body.parentBall = this;

    /**
     * Disable effect of ordinary gravity and apply my special rope gravity
     * @param {number} timeDiff - in ms
     * @param {number} gx - gravity x part (world.gravity.x * gravity.scale)
     * @param {number} gy - gravity y part
     * @returns {undefined}
     */
    this.body.applyRopeGravity = function (timeDiff, gx, gy) {

        //this is Ball.body
        if (this.onRopeGravity > 0) {
            //negating effect of _bodiesApplyGravity
            this.force.x -= gx * this.mass;
            this.force.y -= gy * this.mass;
            var addForce = timeDiff / (100 * this.parentBall.rope.length);
            //smaller bound -> faster

            //add force in the direction of our move
            var nVel = Matter.Vector.normalise(this.velocity);
            this.force.x += nVel.x * addForce;
            this.force.y += nVel.y * addForce;
        }

    };
};

/**
 * Violently destroy the hook = by collision with another object
 * This Ball has to be unhookable some time depending on current speed
 * @returns {undefined}
 */
Ball.prototype.destroyHook = function () {
    if (this.hooked) {
        this.toggleHook();
    }
};

/**
 * Control this ball if not controlled by a player
 * @returns {undefined}
 */
Ball.prototype.ai = function () {
    if (this.controlled) {
        return;
    }

    //Basic "AI"
    if (this.hooked) {
        var q = new Date() - this.timeHooked;
        if (q > 1000) { //todo: vector not outside
            //unhook
            if (this.body.velocity.y < 0 && Math.abs(this.body.velocity.y) > Math.abs(this.body.velocity.x)) {
                this.toggleHook();
            }
        }
    } else {
        //try to hook
        var q = new Date() - this.timeUnhooked;
        if (q > 500) {
            this.toggleHook();
        }
    }
};<commit_msg>Lives 3,2,1 end instead of 3,2,1,0,end<commit_after>/*
 * @copyright Adam Benda, 2016
 */

/**
 * @class
 * @classdesc Ball
 */

/**
 *
 * @param {Matter.Engine} engine -
 * @param {number} x - x coordinate
 * @param {number} y - y coordinate
 * @param {number} r - radius
 * @param {string} textureFile - png image containing texture
 * @param {string} id - identifier of this ball. Player has id=0
 * @returns {Ball}
 */
var Ball = function (engine, x, y, r, textureFile, id) {
    this.id = Math.round(Math.random() * 1000);
    this.engine = engine;
    this.world = engine.world;
    this.initX = x * this.engine.render.options.width;
    this.initY = y * this.engine.render.options.height;
    this.initR = r * this.engine.render.options.width;
    this.textureFile = textureFile;
    this.createBody(
            this.initX,
            this.initY,
            this.initR
            );
    Matter.Composite.add(this.world, this.body);

    this.distracted = 0;
    this.controlled = false;

    this.timeHooked = new Date();
    this.timeUnhooked = new Date();
};

Ball.prototype.toggleHook = function () {
    var world = this.world;

    if (!this.hooked) {
        //find a nearest
        //todo: to a separate function
        var target = null;
        var shortestDistance = 99999999;
        for (var i = 0; i < world.bodies.length; i++) {
            var body = world.bodies[i];
            if (body.isStatic) {

                //Check if current speed vector is compatible with
                //vector of rotation hooked to this body
                var centripetalVect = Matter.Vector.create(
                        body.position.x - this.body.position.x,
                        body.position.y - this.body.position.y
                        );
                var angle = Matter.Vector.dot(centripetalVect, this.body.velocity) /
                        (Matter.Vector.magnitude(centripetalVect) * Matter.Vector.magnitude(this.body.velocity));

                if ((Math.abs(angle) > 100 / Matter.Vector.magnitude(centripetalVect))) {
                    //not compatible vector
                    continue;
                }

                var thisDistance = Matter.Vector.magnitudeSquared(
                        Matter.Vector.sub(body.position, this.body.position)
                        );
                if (shortestDistance > thisDistance) {
                    shortestDistance = thisDistance;
                    target = body;
                }
            }
        }

        if (target === null) {
            //no target available
            return false;
        }

        this.rope = Matter.Constraint.create({bodyA: this.body, bodyB: target, stiffness: 4});
        this.body.onRopeGravity = 1;
        Matter.World.add(world, this.rope);
        //this.rope.render.strokeStyle = "#ff0000";
        this.hooked = true;
        this.timeHooked = new Date();
        return true;
    } else {
        Matter.World.remove(world, this.rope);

        this.body.onRopeGravity = 0;
        this.timeUnhooked = new Date();
        this.hooked = false;
    }

};

Ball.prototype.checkBoundaries = function () {
    if (this.hooked) {
        //if we are hooked up its ok
        return true;
    }

    if (
            this.engine.render.bounds.max.y < this.body.position.y - this.initR ||
            this.engine.render.bounds.min.x > this.body.position.x + this.initR ||
            this.engine.render.bounds.max.x < this.body.position.x - this.initR
            ) {
        return this.killed();

    }
    return true;
};

Ball.prototype.killed = function () {
    if (this.lives !== undefined) {
        this.lives--;
        if (this.lives <= 0) {
            this.gamestate.gameOverSignal();
            return false;
        }
    }

    //unhook
    if (this.hooked) {
        this.toggleHook(this.world);
    }

    Matter.Composite.remove(this.world, this.body);
    if (this.lives === undefined || this.lives >= 0) {
        this.createBody(this.initX, this.initY, this.initR);
        Matter.Composite.add(this.world, this.body);
    }
    this.gamestate.ballKilledSignal(this.id);
    return true;
};

Ball.prototype.createBody = function (x, y, r) {
    this.body = Matter.Bodies.circle(x, y, r);
    this.body.force.x = 0;
    this.body.force.y = 0.1;
    this.body.frictionAir = 0;
    this.body.friction = 0;
    Matter.Body.setAngularVelocity(this.body, 0.01);
    Matter.Body.setMass(this.body, 5);
    if (this.textureFile && this.textureFile.length && this.textureFile.length > 0) {
        this.body.render.sprite.texture = this.textureFile;
    }
    this.body.parentBall = this;

    /**
     * Disable effect of ordinary gravity and apply my special rope gravity
     * @param {number} timeDiff - in ms
     * @param {number} gx - gravity x part (world.gravity.x * gravity.scale)
     * @param {number} gy - gravity y part
     * @returns {undefined}
     */
    this.body.applyRopeGravity = function (timeDiff, gx, gy) {

        //this is Ball.body
        if (this.onRopeGravity > 0) {
            //negating effect of _bodiesApplyGravity
            this.force.x -= gx * this.mass;
            this.force.y -= gy * this.mass;
            var addForce = timeDiff / (100 * this.parentBall.rope.length);
            //smaller bound -> faster

            //add force in the direction of our move
            var nVel = Matter.Vector.normalise(this.velocity);
            this.force.x += nVel.x * addForce;
            this.force.y += nVel.y * addForce;
        }

    };
};

/**
 * Violently destroy the hook = by collision with another object
 * This Ball has to be unhookable some time depending on current speed
 * @returns {undefined}
 */
Ball.prototype.destroyHook = function () {
    if (this.hooked) {
        this.toggleHook();
    }
};

/**
 * Control this ball if not controlled by a player
 * @returns {undefined}
 */
Ball.prototype.ai = function () {
    if (this.controlled) {
        return;
    }

    //Basic "AI"
    if (this.hooked) {
        var q = new Date() - this.timeHooked;
        if (q > 1000) { //todo: vector not outside
            //unhook
            if (this.body.velocity.y < 0 && Math.abs(this.body.velocity.y) > Math.abs(this.body.velocity.x)) {
                this.toggleHook();
            }
        }
    } else {
        //try to hook
        var q = new Date() - this.timeUnhooked;
        if (q > 500) {
            this.toggleHook();
        }
    }
};<|endoftext|>


<commit_before>/* jshint browser: true */

( function( window, $ )
{
    "use strict";

    /*
     *  Represenets an editor
     *  @constructor
     *  @param {DOMNode} element - The TEXTAREA element to add the Wysiwyg to.
     *  @param {object} userOptions - The default options selected by the user.
     */
    function Wysiwyg( element, userOptions ) {

        // This calls the $ function, with the element as a parameter and
        // returns the jQuery object wrapper for element. It also assigns the
        // jQuery object wrapper to the property $editor on `this`.
        this.selectedRange = null;
        this.editor = $( element );
        var editor = $( element );
        var defaults = {
            hotKeys: {
            "Ctrl+b meta+b": "bold",
            "Ctrl+i meta+i": "italic",
            "Ctrl+u meta+u": "underline",
            "Ctrl+z": "undo",
            "Ctrl+y meta+y meta+shift+z": "redo",
            "Ctrl+l meta+l": "justifyleft",
            "Ctrl+r meta+r": "justifyright",
            "Ctrl+e meta+e": "justifycenter",
            "Ctrl+j meta+j": "justifyfull",
            "Shift+tab": "outdent",
            "tab": "indent"
            },
            toolbarSelector: "[data-role=editor-toolbar]",
            commandRole: "edit",
            activeToolbarClass: "btn-info",
            selectionMarker: "edit-focus-marker",
            selectionColor: "darkgrey",
            dragAndDropImages: true,
            keypressTimeout: 200,
            fileUploadError: function( reason, detail ) { console.log( "File upload error", reason, detail ); }
        };

        var options = $.extend( true, {}, defaults, userOptions );
        var toolbarBtnSelector = "a[data-" + options.commandRole + "],button[data-" + options.commandRole + "],input[type=button][data-" + options.commandRole + "]";
        this.bindHotkeys( editor, options, toolbarBtnSelector );

        if ( options.dragAndDropImages ) {
            this.initFileDrops( editor, options, toolbarBtnSelector );
        }

        this.bindToolbar( editor, $( options.toolbarSelector ), options, toolbarBtnSelector );

        editor.attr( "contenteditable", true )
            .on( "mouseup keyup mouseout", function() {
                this.saveSelection();
                this.updateToolbar( editor, toolbarBtnSelector, options );
            }.bind( this ) );

        $( window ).bind( "touchend", function( e ) {
            var isInside = ( editor.is( e.target ) || editor.has( e.target ).length > 0 ),
            currentRange = this.getCurrentRange(),
            clear = currentRange && ( currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset );

            if ( !clear || isInside ) {
                this.saveSelection();
                this.updateToolbar( editor, toolbarBtnSelector, options );
            }
        } );
     }

     Wysiwyg.prototype.readFileIntoDataUrl = function( fileInfo ) {
        var loader = $.Deferred(),
        fReader = new FileReader();

        fReader.onload = function( e ) {
            loader.resolve( e.target.result );
        };

        fReader.onerror = loader.reject;
        fReader.onprogress = loader.notify;
        fReader.readAsDataURL( fileInfo );
        return loader.promise();
     };

     Wysiwyg.prototype.cleanHtml = function( o ) {
        var self = this;
        if ( $( self ).data( "wysiwyg-html-mode" ) === true ) {
            $( self ).html( $( self ).text() );
            $( self ).attr( "contenteditable", true );
            $( self ).data( "wysiwyg-html-mode", false );
        }

        // Strip the images with src="data:image/.." out;
        if ( o === true && $( self ).parent().is( "form" ) ) {
            var gGal = $( self ).html;
            if ( $( gGal ).has( "img" ).length ) {
                var gImages = $( "img", $( gGal ) );
                var gResults = [];
                var gEditor = $( self ).parent();
                $.each( gImages, function( i, v ) {
                    if ( $( v ).attr( "src" ).match( /^data:image\/.*$/ ) ) {
                        gResults.push( gImages[ i ] );
                        $( gEditor ).prepend( "<input value='" + $( v ).attr( "src" ) + "' type='hidden' name='postedimage/" + i + "' />" );
                        $( v ).attr( "src", "postedimage/" + i );
                    }
                } );
            }
        }

        var html = $( self ).html();
        return html && html.replace( /(
|\s|

<\/div>| )*$/, "" );
     };

     Wysiwyg.prototype.updateToolbar = function( editor, toolbarBtnSelector, options ) {
        if ( options.activeToolbarClass ) {
            $( options.toolbarSelector ).find( toolbarBtnSelector ).each( function() {
                var self =  $( this );
                var commandArr = self.data( options.commandRole ).split( " " );
                var command = commandArr[ 0 ];

                // If the command has an argument and its value matches this button. == used for string/number comparison
                if ( commandArr.length > 1 && document.queryCommandEnabled( command ) && document.queryCommandValue( command ) === commandArr[ 1 ] ) {
                    self.addClass( options.activeToolbarClass );
                }

                // Else if the command has no arguments and it is active
                else if ( commandArr.length === 1 && document.queryCommandEnabled( command ) && document.queryCommandState( command ) ) {
                    self.addClass( options.activeToolbarClass );
                }

                // Else the command is not active
                else {
                    self.removeClass( options.activeToolbarClass );
                }
            } );
        }
     };

     Wysiwyg.prototype.execCommand = function( commandWithArgs, valueArg, editor, options, toolbarBtnSelector ) {
        var commandArr = commandWithArgs.split( " " ),
            command = commandArr.shift(),
            args = commandArr.join( " " ) + ( valueArg || "" );

        var parts = commandWithArgs.split( "-" );

        if ( parts.length === 1 ) {
            document.execCommand( command, false, args );
        } else if ( parts[ 0 ] === "format" && parts.length === 2 ) {
            document.execCommand( "formatBlock", false, parts[ 1 ] );
        }

        ( editor ).trigger( "change" );
        this.updateToolbar( editor, toolbarBtnSelector, options );
     };

     Wysiwyg.prototype.bindHotkeys = function( editor, options, toolbarBtnSelector ) {
        var self = this;
        $.each( options.hotKeys, function( hotkey, command ) {
            if(!command) return;

            $( editor ).keydown( hotkey, function( e ) {
                if ( editor.attr( "contenteditable" ) && $( editor ).is( ":visible" ) ) {
                    e.preventDefault();
                    e.stopPropagation();
                    self.execCommand( command, null, editor, options, toolbarBtnSelector );
                }
            } ).keyup( hotkey, function( e ) {
                if ( editor.attr( "contenteditable" ) && $( editor ).is( ":visible" ) ) {
                    e.preventDefault();
                    e.stopPropagation();
                }
            } );
        } );

        editor.keyup( function() { editor.trigger( "change" ); } );
     };

     Wysiwyg.prototype.getCurrentRange = function() {
        var sel, range;
        if ( window.getSelection ) {
            sel = window.getSelection();
            if ( sel.getRangeAt && sel.rangeCount ) {
                range = sel.getRangeAt( 0 );
            }
        } else if ( document.selection ) {
            range = document.selection.createRange();
        }

        return range;
     };

     Wysiwyg.prototype.saveSelection = function() {
        this.selectedRange = this.getCurrentRange();
     };

     Wysiwyg.prototype.restoreSelection = function() {
        var selection;
        if ( window.getSelection || document.createRange ) {
            selection = window.getSelection();
            if ( this.selectedRange ) {
                try {
                    selection.removeAllRanges();
                }
                catch ( ex ) {
                    document.body.createTextRange().select();
                    document.selection.empty();
                }
                selection.addRange( this.selectedRange );
            }
        } else if ( document.selection && this.selectedRange ) {
            this.selectedRange.select();
        }
     };

     // Adding Toggle HTML based on the work by @jd0000, but cleaned up a little to work in this context.
     Wysiwyg.prototype.toggleHtmlEdit = function( editor ) {
        if ( editor.data( "wysiwyg-html-mode" ) !== true ) {
            var oContent = editor.html();
            var editorPre = $( "
" );
            $( editorPre ).append( document.createTextNode( oContent ) );
            $( editorPre ).attr( "contenteditable", true );
            $( editor ).html( " " );
            $( editor ).append( $( editorPre ) );
            $( editor ).attr( "contenteditable", false );
            $( editor ).data( "wysiwyg-html-mode", true );
            $( editorPre ).focus();
        } else {
            $( editor ).html( $( editor ).text() );
            $( editor ).attr( "contenteditable", true );
            $( editor ).data( "wysiwyg-html-mode", false );
            $( editor ).focus();
        }
     };

     Wysiwyg.prototype.insertFiles = function( files, options, editor, toolbarBtnSelector ) {
        var self = this;
        editor.focus();
        $.each( files, function( idx, fileInfo ) {
            if ( /^image\//.test( fileInfo.type ) ) {
                $.when( self.readFileIntoDataUrl( fileInfo ) ).done( function( dataUrl ) {
                    self.execCommand( "insertimage", dataUrl, editor, options, toolbarBtnSelector );
                    editor.trigger( "image-inserted" );
                } ).fail( function( e ) {
                    options.fileUploadError( "file-reader", e );
                } );
            } else {
                options.fileUploadError( "unsupported-file-type", fileInfo.type );
            }
        } );
     };

     Wysiwyg.prototype.markSelection = function( input, color, options ) {
        this.restoreSelection(  );
        if ( document.queryCommandSupported( "hiliteColor" ) ) {
            document.execCommand( "hiliteColor", false, color || "transparent" );
        }
        this.saveSelection(  );
        input.data( options.selectionMarker, color );
     };

     Wysiwyg.prototype.bindToolbar = function( editor, toolbar, options, toolbarBtnSelector ) {
        var self = this;
        toolbar.find( toolbarBtnSelector ).click( function() {
            self.restoreSelection(  );
            editor.focus();

            if ( editor.data( options.commandRole ) === "html" ) {
                self.toggleHtmlEdit( editor );
            } else {
                self.execCommand( $( this ).data( options.commandRole ), null, editor, options, toolbarBtnSelector );
            }

            self.saveSelection(  );
        } );

        toolbar.find( "[data-toggle=dropdown]" ).click( this.restoreSelection(  ) );

        toolbar.find( "input[type=text][data-" + options.commandRole + "]" ).on( "webkitspeechchange change", function() {
            var newValue = this.value;  // Ugly but prevents fake double-calls due to selection restoration
            this.value = "";
            self.restoreSelection(  );
            if ( newValue ) {
                editor.focus();
                self.execCommand( $( this ).data( options.commandRole ), newValue, editor, options, toolbarBtnSelector );
            }
            self.saveSelection(  );
        } ).on( "focus", function() {
            var input = $( this );
            if ( !input.data( options.selectionMarker ) ) {
                self.markSelection( input, options.selectionColor, options );
                input.focus();
            }
        } ).on( "blur", function() {
            var input = $( this );
            if ( input.data( options.selectionMarker ) ) {
                self.markSelection( input, false, options );
            }
        } );
        toolbar.find( "input[type=file][data-" + options.commandRole + "]" ).change( function() {
            self.restoreSelection(  );
            if ( this.type === "file" && this.files && this.files.length > 0 ) {
                self.insertFiles( this.files, options, editor, toolbarBtnSelector );
            }
            self.saveSelection(  );
            this.value = "";
        } );
     };

     Wysiwyg.prototype.initFileDrops = function( editor, options, toolbarBtnSelector ) {
         var self = this;
        editor.on( "dragenter dragover", false ).on( "drop", function( e ) {
            var dataTransfer = e.originalEvent.dataTransfer;
            e.stopPropagation();
            e.preventDefault();
            if ( dataTransfer && dataTransfer.files && dataTransfer.files.length > 0 ) {
                self.insertFiles( dataTransfer.files, options, editor, toolbarBtnSelector );
            }
        } );
     };

     /*
      *  Represenets an editor
      *  @constructor
      *  @param {object} userOptions - The default options selected by the user.
      */

     $.fn.wysiwyg = function( userOptions ) {
        var wysiwyg = new Wysiwyg( this, userOptions );
     };

} )( window, window.jQuery );
<commit_msg>Fix issue #122<commit_after>/* jshint browser: true */

( function( window, $ )
{
    "use strict";

    /*
     *  Represenets an editor
     *  @constructor
     *  @param {DOMNode} element - The TEXTAREA element to add the Wysiwyg to.
     *  @param {object} userOptions - The default options selected by the user.
     */
    function Wysiwyg( element, userOptions ) {

        // This calls the $ function, with the element as a parameter and
        // returns the jQuery object wrapper for element. It also assigns the
        // jQuery object wrapper to the property $editor on `this`.
        this.selectedRange = null;
        this.editor = $( element );
        var editor = $( element );
        var defaults = {
            hotKeys: {
            "Ctrl+b meta+b": "bold",
            "Ctrl+i meta+i": "italic",
            "Ctrl+u meta+u": "underline",
            "Ctrl+z": "undo",
            "Ctrl+y meta+y meta+shift+z": "redo",
            "Ctrl+l meta+l": "justifyleft",
            "Ctrl+r meta+r": "justifyright",
            "Ctrl+e meta+e": "justifycenter",
            "Ctrl+j meta+j": "justifyfull",
            "Shift+tab": "outdent",
            "tab": "indent"
            },
            toolbarSelector: "[data-role=editor-toolbar]",
            commandRole: "edit",
            activeToolbarClass: "btn-info",
            selectionMarker: "edit-focus-marker",
            selectionColor: "darkgrey",
            dragAndDropImages: true,
            keypressTimeout: 200,
            fileUploadError: function( reason, detail ) { console.log( "File upload error", reason, detail ); }
        };

        var options = $.extend( true, {}, defaults, userOptions );
        var toolbarBtnSelector = "a[data-" + options.commandRole + "],button[data-" + options.commandRole + "],input[type=button][data-" + options.commandRole + "]";
        this.bindHotkeys( editor, options, toolbarBtnSelector );

        if ( options.dragAndDropImages ) {
            this.initFileDrops( editor, options, toolbarBtnSelector );
        }

        this.bindToolbar( editor, $( options.toolbarSelector ), options, toolbarBtnSelector );

        editor.attr( "contenteditable", true )
            .on( "mouseup keyup mouseout", function() {
                this.saveSelection();
                this.updateToolbar( editor, toolbarBtnSelector, options );
            }.bind( this ) );

        $( window ).bind( "touchend", function( e ) {
            var isInside = ( editor.is( e.target ) || editor.has( e.target ).length > 0 ),
            currentRange = this.getCurrentRange(),
            clear = currentRange && ( currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset );

            if ( !clear || isInside ) {
                this.saveSelection();
                this.updateToolbar( editor, toolbarBtnSelector, options );
            }
        } );
     }

     Wysiwyg.prototype.readFileIntoDataUrl = function( fileInfo ) {
        var loader = $.Deferred(),
        fReader = new FileReader();

        fReader.onload = function( e ) {
            loader.resolve( e.target.result );
        };

        fReader.onerror = loader.reject;
        fReader.onprogress = loader.notify;
        fReader.readAsDataURL( fileInfo );
        return loader.promise();
     };

     Wysiwyg.prototype.cleanHtml = function( o ) {
        var self = this;
        if ( $( self ).data( "wysiwyg-html-mode" ) === true ) {
            $( self ).html( $( self ).text() );
            $( self ).attr( "contenteditable", true );
            $( self ).data( "wysiwyg-html-mode", false );
        }

        // Strip the images with src="data:image/.." out;
        if ( o === true && $( self ).parent().is( "form" ) ) {
            var gGal = $( self ).html;
            if ( $( gGal ).has( "img" ).length ) {
                var gImages = $( "img", $( gGal ) );
                var gResults = [];
                var gEditor = $( self ).parent();
                $.each( gImages, function( i, v ) {
                    if ( $( v ).attr( "src" ).match( /^data:image\/.*$/ ) ) {
                        gResults.push( gImages[ i ] );
                        $( gEditor ).prepend( "<input value='" + $( v ).attr( "src" ) + "' type='hidden' name='postedimage/" + i + "' />" );
                        $( v ).attr( "src", "postedimage/" + i );
                    }
                } );
            }
        }

        var html = $( self ).html();
        return html && html.replace( /(
|\s|

<\/div>| )*$/, "" );
     };

     Wysiwyg.prototype.updateToolbar = function( editor, toolbarBtnSelector, options ) {
        if ( options.activeToolbarClass ) {
            $( options.toolbarSelector ).find( toolbarBtnSelector ).each( function() {
                var self =  $( this );
                var commandArr = self.data( options.commandRole ).split( " " );
                var command = commandArr[ 0 ];

                // If the command has an argument and its value matches this button. == used for string/number comparison
                if ( commandArr.length > 1 && document.queryCommandEnabled( command ) && document.queryCommandValue( command ) === commandArr[ 1 ] ) {
                    self.addClass( options.activeToolbarClass );
                }

                // Else if the command has no arguments and it is active
                else if ( commandArr.length === 1 && document.queryCommandEnabled( command ) && document.queryCommandState( command ) ) {
                    self.addClass( options.activeToolbarClass );
                }

                // Else the command is not active
                else {
                    self.removeClass( options.activeToolbarClass );
                }
            } );
        }
     };

     Wysiwyg.prototype.execCommand = function( commandWithArgs, valueArg, editor, options, toolbarBtnSelector ) {
        var commandArr = commandWithArgs.split( " " ),
            command = commandArr.shift(),
            args = commandArr.join( " " ) + ( valueArg || "" );

        var parts = commandWithArgs.split( "-" );

        if ( parts.length === 1 ) {
            document.execCommand( command, false, args );
        } else if ( parts[ 0 ] === "format" && parts.length === 2 ) {
            document.execCommand( "formatBlock", false, parts[ 1 ] );
        }

        ( editor ).trigger( "change" );
        this.updateToolbar( editor, toolbarBtnSelector, options );
     };

     Wysiwyg.prototype.bindHotkeys = function( editor, options, toolbarBtnSelector ) {
        var self = this;
        $.each( options.hotKeys, function( hotkey, command ) {
            if(!command) return;

            $( editor ).keydown( hotkey, function( e ) {
                if ( editor.attr( "contenteditable" ) && $( editor ).is( ":visible" ) ) {
                    e.preventDefault();
                    e.stopPropagation();
                    self.execCommand( command, null, editor, options, toolbarBtnSelector );
                }
            } ).keyup( hotkey, function( e ) {
                if ( editor.attr( "contenteditable" ) && $( editor ).is( ":visible" ) ) {
                    e.preventDefault();
                    e.stopPropagation();
                }
            } );
        } );

        editor.keyup( function() { editor.trigger( "change" ); } );
     };

     Wysiwyg.prototype.getCurrentRange = function() {
        var sel, range;
        if ( window.getSelection ) {
            sel = window.getSelection();
            if ( sel.getRangeAt && sel.rangeCount ) {
                range = sel.getRangeAt( 0 );
            }
        } else if ( document.selection ) {
            range = document.selection.createRange();
        }

        return range;
     };

     Wysiwyg.prototype.saveSelection = function() {
        this.selectedRange = this.getCurrentRange();
     };

     Wysiwyg.prototype.restoreSelection = function() {
        var selection;
        if ( window.getSelection || document.createRange ) {
            selection = window.getSelection();
            if ( this.selectedRange ) {
                try {
                    selection.removeAllRanges();
                }
                catch ( ex ) {
                    document.body.createTextRange().select();
                    document.selection.empty();
                }
                selection.addRange( this.selectedRange );
            }
        } else if ( document.selection && this.selectedRange ) {
            this.selectedRange.select();
        }
     };

     // Adding Toggle HTML based on the work by @jd0000, but cleaned up a little to work in this context.
     Wysiwyg.prototype.toggleHtmlEdit = function( editor ) {
        if ( editor.data( "wysiwyg-html-mode" ) !== true ) {
            var oContent = editor.html();
            var editorPre = $( "
" );
            $( editorPre ).append( document.createTextNode( oContent ) );
            $( editorPre ).attr( "contenteditable", true );
            $( editor ).html( " " );
            $( editor ).append( $( editorPre ) );
            $( editor ).attr( "contenteditable", false );
            $( editor ).data( "wysiwyg-html-mode", true );
            $( editorPre ).focus();
        } else {
            $( editor ).html( $( editor ).text() );
            $( editor ).attr( "contenteditable", true );
            $( editor ).data( "wysiwyg-html-mode", false );
            $( editor ).focus();
        }
     };

     Wysiwyg.prototype.insertFiles = function( files, options, editor, toolbarBtnSelector ) {
        var self = this;
        editor.focus();
        $.each( files, function( idx, fileInfo ) {
            if ( /^image\//.test( fileInfo.type ) ) {
                $.when( self.readFileIntoDataUrl( fileInfo ) ).done( function( dataUrl ) {
                    self.execCommand( "insertimage", dataUrl, editor, options, toolbarBtnSelector );
                    editor.trigger( "image-inserted" );
                } ).fail( function( e ) {
                    options.fileUploadError( "file-reader", e );
                } );
            } else {
                options.fileUploadError( "unsupported-file-type", fileInfo.type );
            }
        } );
     };

     Wysiwyg.prototype.markSelection = function( input, color, options ) {
        this.restoreSelection(  );
        if ( document.queryCommandSupported( "hiliteColor" ) ) {
            document.execCommand( "hiliteColor", false, color || "transparent" );
        }
        this.saveSelection(  );
        input.data( options.selectionMarker, color );
     };

     Wysiwyg.prototype.bindToolbar = function( editor, toolbar, options, toolbarBtnSelector ) {
        var self = this;
        toolbar.find( toolbarBtnSelector ).click( function() {
            self.restoreSelection(  );
            editor.focus();

            if ( editor.data( options.commandRole ) === "html" ) {
                self.toggleHtmlEdit( editor );
            } else {
                self.execCommand( $( this ).data( options.commandRole ), null, editor, options, toolbarBtnSelector );
            }

            self.saveSelection(  );
        } );

        toolbar.find( "[data-toggle=dropdown]" ).click( this.restoreSelection(  ) );

        toolbar.find( "input[type=text][data-" + options.commandRole + "]" ).on( "webkitspeechchange change", function() {
            var newValue = this.value;  // Ugly but prevents fake double-calls due to selection restoration
            this.value = "";
            self.restoreSelection(  );
            if ( newValue ) {
                editor.focus();
                self.execCommand( $( this ).data( options.commandRole ), newValue, editor, options, toolbarBtnSelector );
            }
            self.saveSelection(  );
        } ).on( "focus", function() {
            var input = $( this );
            if ( !input.data( options.selectionMarker ) ) {
                self.markSelection( input, options.selectionColor, options );
                input.focus();
            }
        } ).on( "blur", function() {
            var input = $( this );
            if ( input.data( options.selectionMarker ) ) {
                self.markSelection( input, false, options );
            }
        } );
        toolbar.find( "input[type=file][data-" + options.commandRole + "]" ).change( function() {
            self.restoreSelection(  );
            if ( this.type === "file" && this.files && this.files.length > 0 ) {
                self.insertFiles( this.files, options, editor, toolbarBtnSelector );
            }
            self.saveSelection(  );
            this.value = "";
        } );
     };

     Wysiwyg.prototype.initFileDrops = function( editor, options, toolbarBtnSelector ) {
         var self = this;
        editor.on( "dragenter dragover", false ).on( "drop", function( e ) {
            var dataTransfer = e.originalEvent.dataTransfer;
            e.stopPropagation();
            e.preventDefault();
            if ( dataTransfer && dataTransfer.files && dataTransfer.files.length > 0 ) {
                self.insertFiles( dataTransfer.files, options, editor, toolbarBtnSelector );
            }
        } );
     };

     /*
      *  Represenets an editor
      *  @constructor
      *  @param {object} userOptions - The default options selected by the user.
      */

     $.fn.wysiwyg = function( userOptions ) {
        var wysiwyg = new Wysiwyg( this, userOptions );
        return this;
     };

     $.fn.cleanHtml = function() {
        return Wysiwyg.prototype.cleanHtml.apply(this);
     };

} )( window, window.jQuery );
<|endoftext|>


<commit_before>(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // We're using AMD, e.g. require.js. Register as an anonymous module.
        define(['jquery', 'raphael'], factory);
    } else {
        // We're using plain javascript imports, namespace everything in the Autn namespace.
        (function(scope, namespace){
            for (var key, words = namespace.split('.'); key = words.shift();) {
                scope = (scope[key] || (scope[key] = {}));
            }
        })(window, 'autn.vis.util');

        autn.vis.util.wordWrap = factory(jQuery, Raphael);
    }
}(function ($, Raphael) {
    var sizeMap = {};

    function measureWord(textEl, word, family, size) {
        // font-size differences don't actually make much difference, mainly just to whitespace, may consider
        // just linearly scaling them
        var key = family + ':' + word;
        var existing = sizeMap[key];
        if (existing) {
            return {width: existing[0] * size, height: existing[1] * size};
        }

        if (curText !== word) { textEl.attr('text', word); curText = word; }
        if (curFontFamily !== family) { textEl.attr('font-family', family); curFontFamily = family; }

        var bounds = textEl.getBBox();

        // 1/40
        var wFrac = bounds.width * 0.025;
        var wHeight = bounds.height * 0.025;
        sizeMap[key] = [wFrac, wHeight];
        return {width: wFrac * size, height: wHeight * size};
    }

    function fastLineBreak(textInput, textEl, maxWidth, padPC, fontFamily, fontSize, minFontSize, maxHeight) {
        var bestSize = fontSize, linesWhichFit;
        var lines = fastTryTextLayout(textInput, textEl, maxWidth, padPC, fontFamily, fontSize, maxHeight, minFontSize === fontSize);
        var biggestFitting = Number.MIN_VALUE;

        if (lines.fit) {
            biggestFitting = fontSize;
        } else {
            bestSize = binaryChop(minFontSize, fontSize - 1, function(fontSize) {
                lines = fastTryTextLayout(textInput, textEl, maxWidth, padPC, fontFamily, fontSize, maxHeight, minFontSize === fontSize);
                if (lines.fit && fontSize > biggestFitting) {
                    linesWhichFit = lines;
                    biggestFitting = fontSize;
                }
                return lines.fit;
            });
        }

        return {
            fit: biggestFitting > 0,
            text: (linesWhichFit || lines).text,
            'font-size': bestSize
        };
    }

    function fastTryTextLayout(text, textEl, maxWidth, padPC, fontFamily, fontSize, maxHeight, earlyTermination) {
        var spaceSize = measureWord(textEl, '\u00A0', fontFamily, fontSize);
        var spaceW = spaceSize.width;
        var aveCharH = spaceSize.height;

        if (padPC) {
            var pad = fontSize * padPC;
            maxWidth -= pad;
            maxHeight -= pad;
        }

        var cursorX = 0, cursorY = 0, lines = [], line = '';
        var fit = true;

        for (var ii = 0; ii < text.length; ++ii) {
            var word = text[ii];
            var wordW = measureWord(textEl, word, fontFamily, fontSize).width;

            if (wordW > maxWidth) {
                fit = false;
                if (earlyTermination) {
                    return {fit: false};
                }
                if (line) {
                    lines.push(line);
                    cursorY += aveCharH;
                }
                lines.push(word);
                cursorY += aveCharH;
                cursorX = 0;
                line = '';
            }
            else if (!line) {
                cursorX = wordW;
                line = word;
            }
            else if (cursorX + spaceW + wordW > maxWidth) {
                lines.push(line);
                cursorY += aveCharH;
                cursorX = wordW;
                line = word;
            }
            else {
                cursorX += spaceW + wordW;
                line += '\u00A0' + word;
            }

            if (cursorY > maxHeight) {
                fit = false;
                if (earlyTermination) {
                    return {fit: false};
                }
            }
        }

        if (line) {
            lines.push(line);
            cursorY += aveCharH;

            if (cursorY > maxHeight) {
                fit = false;
                if (earlyTermination) {
                    return {fit: false};
                }
            }
        }

        return {fit: fit, text: lines.join('\n')};
    }

    // should return highest integer value which passes testFn, or lowest value otherwise
    function binaryChop(low, high, testFn) {
        while (low < high) {
            var mid = ((low + high) >> 1) + 1;
            if (testFn(mid)) {
                low = mid;
            }
            else {
                high = mid - 1;
            }
        }

        return low;
    }

    function lineBreak(textInput, textEl, maxWidth, padPC, fontSize, minFontSize, maxHeight) {
        if (tryTextLayout(textInput, textEl, maxWidth, padPC, fontSize, maxHeight, true)) {
            return true;
        }

        var lastFontTested;
        var lastFontFit;
        var bestSize = binaryChop(minFontSize, fontSize - 1, function(fontSize){
            lastFontTested = fontSize;
            textEl.attr('font-size', fontSize);
            lastFontFit = tryTextLayout(textInput, textEl, maxWidth, padPC, fontSize, maxHeight, true);
            return lastFontFit;
        });

        if (bestSize !== lastFontTested || !lastFontFit) {
            var fit = tryTextLayout(textInput, textEl, maxWidth, padPC, bestSize, maxHeight, false);

            // Sometimes things don't fit when tested a second time
            return bestSize > minFontSize || fit;
        }

        return true;
    }

    function tryTextLayout(text, textEl, maxWidth, padPC, fontSize, maxHeight, earlyTermination) {
        var tmpText = '';

        if (padPC) {
            var pad = fontSize * padPC;
            maxWidth -= pad;
            maxHeight -= pad;
        }

        var onNewLine = true, fits = true;
        for (var ii = 0; ii < text.length; ++ii) {
            var testText = tmpText + (onNewLine ? text[ii] : ' ' + text[ii]);
            textEl.attr('text', testText);
            var bbox = textEl.getBBox();
            var widthFits = (bbox.width <= maxWidth);
            var heightFits = (bbox.height <= maxHeight);

            if (!heightFits) {
                fits = false;
                if (earlyTermination) {
                    return false;
                }
            }

            if (widthFits) {
                tmpText = testText;
                onNewLine = false;
                continue;
            }

            if (!onNewLine) {
                testText = tmpText + '\n' + text[ii];
                textEl.attr('text', testText);
                bbox = textEl.getBBox();
                widthFits = (bbox.width <=  maxWidth);
                heightFits = (bbox.height <= maxHeight);

                if (!heightFits) {
                    fits = false;
                    if (earlyTermination) {
                        return false;
                    }
                }

                if (widthFits) {
                    onNewLine = false;
                    tmpText = testText;
                    continue;
                }
            }

            fits = false;
            if (earlyTermination) {
                return false;
            }

            tmpText = testText + '\n';
            onNewLine = true;
        }
        textEl.attr('text', tmpText);
        return fits;
    }

    var layoutEl, curFontSize, curText, curFontFamily, curPaper;

    Raphael.eve.on('raphael.clear', function() {
        if (curPaper === this) {
            sizeMap = {};

            if (layoutEl) {
                layoutEl.remove();
                layoutEl = null;
            }
        }
    });

    var fastRender = Raphael.vml || /[&?]fast=true/i.test(window.location.search);

    return fastRender ? function(paper, font, maxWidth, text, padPC, fontSize, minFontSize, maxHeight, textEl) {
        var terms = text.split(' ');

        if (!layoutEl) {
            layoutEl = paper.text(0,0,terms[0]).attr({'font-family': font, 'font-size': 40}).hide();
            curText = text;
            curFontFamily = font;
            curPaper = paper;
        }

        var lineAttrs = fastLineBreak(terms, layoutEl, maxWidth, padPC ? padPC + 0.15 : 0, font, fontSize, minFontSize, maxHeight);

        if (textEl) {
            textEl.attr(lineAttrs);
        }

        return {
            fit: lineAttrs.fit,
            text: lineAttrs.text,
            fontSize: lineAttrs['font-size']
        };
    } : function(paper, font, maxWidth, text, padPC, fontSize, minFontSize, maxHeight, textEl) {
        if (!textEl) {
            // if you call .remove(), the removed flag is set,
            // but if you just do paper.clear(), the removed flag remains unset
            if (!layoutEl) {
                layoutEl = paper.text(0,0,text).attr({'font-family': font, 'font-size': fontSize}).hide();
                curText = text;
                curFontFamily = font;
                curPaper = paper;
            }
            else {
                if (curText !== text) { layoutEl.attr('text', text); curText = text; }
                if (curFontSize !== fontSize) { layoutEl.attr('font-size', fontSize); curFontSize = font; }
                if (curFontFamily !== font) { layoutEl.attr('font-family', font); curFontFamily = font; }
            }
            textEl = layoutEl;
        }

        var fit = lineBreak(text.split(' '), textEl, maxWidth, padPC, fontSize, minFontSize, maxHeight);

        if (textEl === layoutEl) {
            // for speed, only return the text and fontSize if requested to do so
            return {
                fit: fit,
                text: textEl.attr('text'),
                fontSize: textEl.attr('font-size')
            };
        }

        return { fit: fit };
    };
}));
<commit_msg>[Visualizers] (WIP CCUK-1647, CCUK-1656)  The fast render window flag doesn't really make sense in a packaged context<commit_after>(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // We're using AMD, e.g. require.js. Register as an anonymous module.
        define(['jquery', 'raphael'], factory);
    } else {
        // We're using plain javascript imports, namespace everything in the Autn namespace.
        (function(scope, namespace){
            for (var key, words = namespace.split('.'); key = words.shift();) {
                scope = (scope[key] || (scope[key] = {}));
            }
        })(window, 'autn.vis.util');

        autn.vis.util.wordWrap = factory(jQuery, Raphael);
    }
}(function ($, Raphael) {
    var sizeMap = {};

    function measureWord(textEl, word, family, size) {
        // font-size differences don't actually make much difference, mainly just to whitespace, may consider
        // just linearly scaling them
        var key = family + ':' + word;
        var existing = sizeMap[key];
        if (existing) {
            return {width: existing[0] * size, height: existing[1] * size};
        }

        if (curText !== word) { textEl.attr('text', word); curText = word; }
        if (curFontFamily !== family) { textEl.attr('font-family', family); curFontFamily = family; }

        var bounds = textEl.getBBox();

        // 1/40
        var wFrac = bounds.width * 0.025;
        var wHeight = bounds.height * 0.025;
        sizeMap[key] = [wFrac, wHeight];
        return {width: wFrac * size, height: wHeight * size};
    }

    function fastLineBreak(textInput, textEl, maxWidth, padPC, fontFamily, fontSize, minFontSize, maxHeight) {
        var bestSize = fontSize, linesWhichFit;
        var lines = fastTryTextLayout(textInput, textEl, maxWidth, padPC, fontFamily, fontSize, maxHeight, minFontSize === fontSize);
        var biggestFitting = Number.MIN_VALUE;

        if (lines.fit) {
            biggestFitting = fontSize;
        } else {
            bestSize = binaryChop(minFontSize, fontSize - 1, function(fontSize) {
                lines = fastTryTextLayout(textInput, textEl, maxWidth, padPC, fontFamily, fontSize, maxHeight, minFontSize === fontSize);
                if (lines.fit && fontSize > biggestFitting) {
                    linesWhichFit = lines;
                    biggestFitting = fontSize;
                }
                return lines.fit;
            });
        }

        return {
            fit: biggestFitting > 0,
            text: (linesWhichFit || lines).text,
            'font-size': bestSize
        };
    }

    function fastTryTextLayout(text, textEl, maxWidth, padPC, fontFamily, fontSize, maxHeight, earlyTermination) {
        var spaceSize = measureWord(textEl, '\u00A0', fontFamily, fontSize);
        var spaceW = spaceSize.width;
        var aveCharH = spaceSize.height;

        if (padPC) {
            var pad = fontSize * padPC;
            maxWidth -= pad;
            maxHeight -= pad;
        }

        var cursorX = 0, cursorY = 0, lines = [], line = '';
        var fit = true;

        for (var ii = 0; ii < text.length; ++ii) {
            var word = text[ii];
            var wordW = measureWord(textEl, word, fontFamily, fontSize).width;

            if (wordW > maxWidth) {
                fit = false;
                if (earlyTermination) {
                    return {fit: false};
                }
                if (line) {
                    lines.push(line);
                    cursorY += aveCharH;
                }
                lines.push(word);
                cursorY += aveCharH;
                cursorX = 0;
                line = '';
            }
            else if (!line) {
                cursorX = wordW;
                line = word;
            }
            else if (cursorX + spaceW + wordW > maxWidth) {
                lines.push(line);
                cursorY += aveCharH;
                cursorX = wordW;
                line = word;
            }
            else {
                cursorX += spaceW + wordW;
                line += '\u00A0' + word;
            }

            if (cursorY > maxHeight) {
                fit = false;
                if (earlyTermination) {
                    return {fit: false};
                }
            }
        }

        if (line) {
            lines.push(line);
            cursorY += aveCharH;

            if (cursorY > maxHeight) {
                fit = false;
                if (earlyTermination) {
                    return {fit: false};
                }
            }
        }

        return {fit: fit, text: lines.join('\n')};
    }

    // should return highest integer value which passes testFn, or lowest value otherwise
    function binaryChop(low, high, testFn) {
        while (low < high) {
            var mid = ((low + high) >> 1) + 1;
            if (testFn(mid)) {
                low = mid;
            }
            else {
                high = mid - 1;
            }
        }

        return low;
    }

    function lineBreak(textInput, textEl, maxWidth, padPC, fontSize, minFontSize, maxHeight) {
        if (tryTextLayout(textInput, textEl, maxWidth, padPC, fontSize, maxHeight, true)) {
            return true;
        }

        var lastFontTested;
        var lastFontFit;
        var bestSize = binaryChop(minFontSize, fontSize - 1, function(fontSize){
            lastFontTested = fontSize;
            textEl.attr('font-size', fontSize);
            lastFontFit = tryTextLayout(textInput, textEl, maxWidth, padPC, fontSize, maxHeight, true);
            return lastFontFit;
        });

        if (bestSize !== lastFontTested || !lastFontFit) {
            var fit = tryTextLayout(textInput, textEl, maxWidth, padPC, bestSize, maxHeight, false);

            // Sometimes things don't fit when tested a second time
            return bestSize > minFontSize || fit;
        }

        return true;
    }

    function tryTextLayout(text, textEl, maxWidth, padPC, fontSize, maxHeight, earlyTermination) {
        var tmpText = '';

        if (padPC) {
            var pad = fontSize * padPC;
            maxWidth -= pad;
            maxHeight -= pad;
        }

        var onNewLine = true, fits = true;
        for (var ii = 0; ii < text.length; ++ii) {
            var testText = tmpText + (onNewLine ? text[ii] : ' ' + text[ii]);
            textEl.attr('text', testText);
            var bbox = textEl.getBBox();
            var widthFits = (bbox.width <= maxWidth);
            var heightFits = (bbox.height <= maxHeight);

            if (!heightFits) {
                fits = false;
                if (earlyTermination) {
                    return false;
                }
            }

            if (widthFits) {
                tmpText = testText;
                onNewLine = false;
                continue;
            }

            if (!onNewLine) {
                testText = tmpText + '\n' + text[ii];
                textEl.attr('text', testText);
                bbox = textEl.getBBox();
                widthFits = (bbox.width <=  maxWidth);
                heightFits = (bbox.height <= maxHeight);

                if (!heightFits) {
                    fits = false;
                    if (earlyTermination) {
                        return false;
                    }
                }

                if (widthFits) {
                    onNewLine = false;
                    tmpText = testText;
                    continue;
                }
            }

            fits = false;
            if (earlyTermination) {
                return false;
            }

            tmpText = testText + '\n';
            onNewLine = true;
        }
        textEl.attr('text', tmpText);
        return fits;
    }

    var layoutEl, curFontSize, curText, curFontFamily, curPaper;

    Raphael.eve.on('raphael.clear', function() {
        if (curPaper === this) {
            sizeMap = {};

            if (layoutEl) {
                layoutEl.remove();
                layoutEl = null;
            }
        }
    });

    return Raphael.vml ? function(paper, font, maxWidth, text, padPC, fontSize, minFontSize, maxHeight, textEl) {
        var terms = text.split(' ');

        if (!layoutEl) {
            layoutEl = paper.text(0,0,terms[0]).attr({'font-family': font, 'font-size': 40}).hide();
            curText = text;
            curFontFamily = font;
            curPaper = paper;
        }

        var lineAttrs = fastLineBreak(terms, layoutEl, maxWidth, padPC ? padPC + 0.15 : 0, font, fontSize, minFontSize, maxHeight);

        if (textEl) {
            textEl.attr(lineAttrs);
        }

        return {
            fit: lineAttrs.fit,
            text: lineAttrs.text,
            fontSize: lineAttrs['font-size']
        };
    } : function(paper, font, maxWidth, text, padPC, fontSize, minFontSize, maxHeight, textEl) {
        if (!textEl) {
            // if you call .remove(), the removed flag is set,
            // but if you just do paper.clear(), the removed flag remains unset
            if (!layoutEl) {
                layoutEl = paper.text(0,0,text).attr({'font-family': font, 'font-size': fontSize}).hide();
                curText = text;
                curFontFamily = font;
                curPaper = paper;
            }
            else {
                if (curText !== text) { layoutEl.attr('text', text); curText = text; }
                if (curFontSize !== fontSize) { layoutEl.attr('font-size', fontSize); curFontSize = font; }
                if (curFontFamily !== font) { layoutEl.attr('font-family', font); curFontFamily = font; }
            }
            textEl = layoutEl;
        }

        var fit = lineBreak(text.split(' '), textEl, maxWidth, padPC, fontSize, minFontSize, maxHeight);

        if (textEl === layoutEl) {
            // for speed, only return the text and fontSize if requested to do so
            return {
                fit: fit,
                text: textEl.attr('text'),
                fontSize: textEl.attr('font-size')
            };
        }

        return { fit: fit };
    };
}));
<|endoftext|>


<commit_before>'use strict'

import uuid from 'node-uuid'
import DialogCollection from '../dialog/dialog-collection'
import RESTSession from '../rest-session'
import consts from '../constants'
import { post } from '../utils'

class ConnectorBot extends DialogCollection {
  constructor (options) {
    super()
    this.options = {
      endpoint: process.env.ENDPOINT,
      appId: process.env.APP_ID || '',
      appSecret: process.env.APP_SECRET || '',
      defaultDialogId: '/',
      minSendDelay: 100
    }
    this.configure(this.options)
  }

  configure (options) {
    if (options) {
      for (let key in options) {
        if (options.hasOwnProperty(key)) {
          this.options[key] = options[key]
        }
      }
    }
  }

  verify (options) {
    this.configure(options)
    return (req, res, next) => {
      let authorized = false
      const isSecure = req.headers['x-forwarded-proto'] === 'https' || req.headers['x-arr-ssl']
      if (isSecure && this.options.appId && this.options.appSecret) {
        if (req.headers.hasOwnProperty('authorization')) {
          const tmp = req.headers['authorization'].split(' ')
          const buf = new Buffer(tmp[1], 'base64')
          const cred = buf.toString().split(':')
          if (cred[0] === this.options.appId && cred[1] === this.options.appSecret) {
            authorized = true
          }
        }
      } else {
        authorized = true
      }
      if (authorized) {
        next()
      } else {
        res.send(401)
      }
    }
  }

  listen (dialogId, dialogArgs) {
    return (req, res) => {
      if (req.body) {
        console.log('req.body exists ...')
        console.log('Message type: ' + req.body.type)
        this.dispatchMessage(null, req.body, { dialogId, dialogArgs }, res)
      } else {
        let requestData = ''
        req.on('data', (chunk) => requestData += chunk)
        req.on('end', () => {
          try {
            const msg = JSON.parse(requestData)
            console.log(msg)
            console.log('Message type: ' + msg.type)
            this.dispatchMessage(null, msg, { dialogId, dialogArgs }, res)
          } catch (e) {
            console.error(e)
            this.emit('error', new Error('Invalid message'))
            res.send(400)
          }
        })
      }
    }
  }

  beginDialog (address, dialogId, dialogArgs) {
    const message = address
    message.type = 'Message'
    if (!message.from) {
      message.from = this.options.defaultFrom
    }
    if (!message.to || !message.from) {
      throw new Error('Invalid address passed to ConnectorBot.beginDialog()')
    }
    if (!this.hasDialog(dialogId)) {
      throw new Error('Invalid dialog passed to ConnectorBot.beginDialog()')
    }
    this.dispatchMessage(message.to.id, message, { dialogId, dialogArgs })
  }

  dispatchMessage (userId, message, options, res) {
    try {
      if (!message || !message.type) {
        this.emit('error', new Error('Invalid message'))
        return res ? res.send(400) : null
      }
      if (!userId) {
        if (message.from && message.from.id) {
          userId = message.from.id
        } else {
          this.emit('error', new Error('Invalid message'))
          return res ? res.send(400) : null
        }
      }

      let sessionId = null

      if (message.botConversationData && message.botConversationData[consts.Data.SessionId]) {
        sessionId = message.botConversationData[consts.Data.SessionId]
      } else {
        sessionId = uuid.v1()
        message.botConversationData = message.botConversationData || {}
        message.botConversationData[consts.Data.SessionId] = sessionId
      }

      this.emit(message.type, message)

      if (message.type === 'Message') {
        const ses = new RESTSession({
          localizer: this.options.localizer,
          minSendDelay: this.options.minSendDelay,
          dialogs: this,
          dialogId: options.dialogId || this.options.defaultDialogId,
          dialogArgs: options.dialogArgs || this.options.defaultDialogArgs
        })
        ses.on('send', (_reply) => {
          let reply = _reply || {}
          reply.botConversationData = message.botConversationData
          if (reply.text && !reply.language && message.language) {
            reply.language = message.language
          }

          const data = {
            userData: ses.userData,
            conversationData: ses.conversationData,
            perUserConversationData: ses.perUserInConversationData
          }

          data.perUserConversationData[consts.Data.SessionState] = ses.sessionState

          this.saveData(userId, sessionId, data, reply, (err) => {
            if (err) return this.emit('error', err)

            let endpoint = null

            if (ses.message.to.channelId === 'emulator') {
              endpoint = this.options.endpoint || 'http://localhost:9000'
            } else {
              endpoint = this.options.endpoint || 'https://api.ourapidomain.com'
            }

            if (res) {
              this.emit('reply', reply)
              res.send(200, reply)
              res = null
            } else if (ses.message.conversationId) {
              reply.from = ses.message.to
              reply.to = ses.message.replyTo ? ses.message.replyTo : ses.message.from
              reply.replyToMessageId = ses.message.id
              reply.conversationId = ses.message.conversationId
              reply.channelConversationId = ses.message.channelConversationId
              reply.channelMessageId = ses.message.channelMessageId
              reply.participants = ses.message.participants
              reply.totalParticipants = ses.message.totalParticipants

              if (!reply.language && ses.message.language) {
                reply.language = ses.message.language
              }

              this.emit('reply', reply)

              post(this.options, endpoint, '/bot/v1.0/messages', reply, (err, response) => {
                if (err) {
                  console.error(err)
                  this.emit('error', err)
                } else if (response.statusCode >= 400) {
                  console.error(response.statusMessage)
                }
              })
            } else {
              reply.from = ses.message.from
              reply.to = ses.message.to
              this.emit('send', reply)
              post(this.options, endpoint, '/bot/v1.0/messages', reply, (err, response) => {
                if (err) {
                  console.error(err)
                  this.emit('error', err)
                } else if (response.statusCode >= 400) {
                  console.error(response.statusMessage)
                }
              })
            }
          })
        })
        ses.on('error', (err) => {
          console.error(err, ses.message)
          this.emit('error', err, ses.message)
          return res ? res.send(500) : null
        })

        ses.on('quit', () => this.emit('quit', ses.message))

        this.getData(userId, sessionId, message, (err, data) => {
          if (!err) {
            let sessionState = null
            ses.userData = data.userData || {}
            ses.conversationData = data.conversationData || {}
            ses.perUserInConversationData = data.perUserConversationData || {}
            if (ses.perUserInConversationData.hasOwnProperty(consts.Data.SessionState)) {
              sessionState = ses.perUserInConversationData[consts.Data.SessionState]
              delete ses.perUserInConversationData[consts.Data.SessionState]
            }
            if (options.replyToDialogId) {
              if (sessionState && sessionState.callstack[sessionState.callstack.length - 1].id === options.replyToDialogId) {
                ses.dispatch(sessionState, message)
              }
            } else {
              ses.dispatch(sessionState, message)
            }
          } else {
            console.error(err, message)
            this.emit('error', err, message)
          }
        })
      } else if (res) {
        let msg = null
        switch (message.type) {
          case 'BotAddedToConversation':
            msg = this.options.groupWelcomeMessage
            break
          case 'UserAddedToConversation':
            msg = this.options.userWelcomeMessage
            break
          case 'EndOfConversation':
            msg = this.options.goodbyeMessage
            break
        }
        res.send(msg ? { type: message.type, text: msg } : {})
      }
    } catch (e) {
      console.error(e)
      const error = e instanceof Error ? e : new Error(e.toString())
      this.emit('error', error)
      return res ? res.send(500) : null
    }
  }

  getData (userId, sessionId, msg, callback) {
    const botPath = `/${this.options.appId}`
    const userPath = `${botPath}/users/${userId}`
    const convoPath = `${botPath}/conversations/${sessionId}`
    const perUserConvoPath = `${botPath}/conversations/${sessionId}/users/${userId}`

    let ops = 3
    const data = {}

    function load (id, field, store, botData) {
      data[field] = botData
      if (store) {
        store.get(id, (err, item) => {
          if (callback) {
            if (!err) {
              data[field] = item
              if (--ops === 0) {
                callback(null, data)
              }
            } else {
              callback(err, null)
              callback = null
            }
          }
        })
      } else if (callback && --ops === 0) {
        callback(null, data)
      }
    }

    load(userPath, 'userData', this.options.userStore, msg.botUserData)
    load(convoPath, 'conversationData', this.options.conversationStore, msg.botConversationData)
    load(perUserConvoPath, 'perUserConversationData', this.options.perUserInConversationStore, msg.botPerUserInConversationData)
  }

  saveData (userId, sessionId, data, msg, callback) {
    const botPath = `/${this.options.appId}`
    const userPath = `${botPath}/users/${userId}`
    const convoPath = `${botPath}/conversations/${sessionId}`
    const perUserConvoPath = `${botPath}/conversations/${sessionId}/users/${userId}`

    let ops = 3

    function save (id, field, store, botData) {
      if (store) {
        store.save(id, botData, (err) => {
          if (callback) {
            if (!err && --ops === 0) {
              callback(null)
            } else {
              callback(err)
              callback = null
            }
          }
        })
      } else {
        msg[field] = botData
        if (callback && --ops === 0) {
          callback(null)
        }
      }
    }

    save(userPath, 'botUserData', this.options.userStore, data.userData)
    save(convoPath, 'botConversationData', this.options.conversationStore, data.conversationData)
    save(perUserConvoPath, 'botPerUserInConversationData', this.options.perUserInConversationStore, data.perUserConversationData)
  }
}

export default ConnectorBot
<commit_msg>Debug 2<commit_after>'use strict'

import uuid from 'node-uuid'
import DialogCollection from '../dialog/dialog-collection'
import RESTSession from '../rest-session'
import consts from '../constants'
import { post } from '../utils'

class ConnectorBot extends DialogCollection {
  constructor (options) {
    super()
    this.options = {
      endpoint: process.env.ENDPOINT,
      appId: process.env.APP_ID || '',
      appSecret: process.env.APP_SECRET || '',
      defaultDialogId: '/',
      minSendDelay: 100
    }
    this.configure(this.options)
  }

  configure (options) {
    if (options) {
      for (let key in options) {
        if (options.hasOwnProperty(key)) {
          this.options[key] = options[key]
        }
      }
    }
  }

  verify (options) {
    this.configure(options)
    return (req, res, next) => {
      let authorized = false
      const isSecure = req.headers['x-forwarded-proto'] === 'https' || req.headers['x-arr-ssl']
      if (isSecure && this.options.appId && this.options.appSecret) {
        if (req.headers.hasOwnProperty('authorization')) {
          const tmp = req.headers['authorization'].split(' ')
          const buf = new Buffer(tmp[1], 'base64')
          const cred = buf.toString().split(':')
          if (cred[0] === this.options.appId && cred[1] === this.options.appSecret) {
            authorized = true
          }
        }
      } else {
        authorized = true
      }
      if (authorized) {
        next()
      } else {
        res.send(401)
      }
    }
  }

  listen (dialogId, dialogArgs) {
    return (req, res) => {
      if (req.body) {
        console.log('req.body exists ...')
        console.log('Message type: ' + req.body.type)
        this.dispatchMessage(null, req.body, { dialogId, dialogArgs }, res)
      } else {
        let requestData = ''
        req.on('data', (chunk) => requestData += chunk)
        req.on('end', () => {
          try {
            console.log('******')
            console.log(requestData)
            console.log('******')
            const msg = JSON.parse(requestData)
            console.log(msg)
            console.log('Message type: ' + msg.type)
            this.dispatchMessage(null, msg, { dialogId, dialogArgs }, res)
          } catch (e) {
            console.error(e.stack)
            this.emit('error', new Error('Invalid message'))
            res.send(400)
          }
        })
      }
    }
  }

  beginDialog (address, dialogId, dialogArgs) {
    const message = address
    message.type = 'Message'
    if (!message.from) {
      message.from = this.options.defaultFrom
    }
    if (!message.to || !message.from) {
      throw new Error('Invalid address passed to ConnectorBot.beginDialog()')
    }
    if (!this.hasDialog(dialogId)) {
      throw new Error('Invalid dialog passed to ConnectorBot.beginDialog()')
    }
    this.dispatchMessage(message.to.id, message, { dialogId, dialogArgs })
  }

  dispatchMessage (userId, message, options, res) {
    try {
      if (!message || !message.type) {
        this.emit('error', new Error('Invalid message'))
        return res ? res.send(400) : null
      }
      if (!userId) {
        if (message.from && message.from.id) {
          userId = message.from.id
        } else {
          this.emit('error', new Error('Invalid message'))
          return res ? res.send(400) : null
        }
      }

      let sessionId = null

      if (message.botConversationData && message.botConversationData[consts.Data.SessionId]) {
        sessionId = message.botConversationData[consts.Data.SessionId]
      } else {
        sessionId = uuid.v1()
        message.botConversationData = message.botConversationData || {}
        message.botConversationData[consts.Data.SessionId] = sessionId
      }

      this.emit(message.type, message)

      if (message.type === 'Message') {
        const ses = new RESTSession({
          localizer: this.options.localizer,
          minSendDelay: this.options.minSendDelay,
          dialogs: this,
          dialogId: options.dialogId || this.options.defaultDialogId,
          dialogArgs: options.dialogArgs || this.options.defaultDialogArgs
        })
        ses.on('send', (_reply) => {
          let reply = _reply || {}
          reply.botConversationData = message.botConversationData
          if (reply.text && !reply.language && message.language) {
            reply.language = message.language
          }

          const data = {
            userData: ses.userData,
            conversationData: ses.conversationData,
            perUserConversationData: ses.perUserInConversationData
          }

          data.perUserConversationData[consts.Data.SessionState] = ses.sessionState

          this.saveData(userId, sessionId, data, reply, (err) => {
            if (err) return this.emit('error', err)

            let endpoint = null

            if (ses.message.to.channelId === 'emulator') {
              endpoint = this.options.endpoint || 'http://localhost:9000'
            } else {
              endpoint = this.options.endpoint || 'https://api.ourapidomain.com'
            }

            if (res) {
              this.emit('reply', reply)
              res.send(200, reply)
              res = null
            } else if (ses.message.conversationId) {
              reply.from = ses.message.to
              reply.to = ses.message.replyTo ? ses.message.replyTo : ses.message.from
              reply.replyToMessageId = ses.message.id
              reply.conversationId = ses.message.conversationId
              reply.channelConversationId = ses.message.channelConversationId
              reply.channelMessageId = ses.message.channelMessageId
              reply.participants = ses.message.participants
              reply.totalParticipants = ses.message.totalParticipants

              if (!reply.language && ses.message.language) {
                reply.language = ses.message.language
              }

              this.emit('reply', reply)

              post(this.options, endpoint, '/bot/v1.0/messages', reply, (err, response) => {
                if (err) {
                  console.error(err)
                  this.emit('error', err)
                } else if (response.statusCode >= 400) {
                  console.error(response.statusMessage)
                }
              })
            } else {
              reply.from = ses.message.from
              reply.to = ses.message.to
              this.emit('send', reply)
              post(this.options, endpoint, '/bot/v1.0/messages', reply, (err, response) => {
                if (err) {
                  console.error(err)
                  this.emit('error', err)
                } else if (response.statusCode >= 400) {
                  console.error(response.statusMessage)
                }
              })
            }
          })
        })
        ses.on('error', (err) => {
          console.error(err, ses.message)
          this.emit('error', err, ses.message)
          return res ? res.send(500) : null
        })

        ses.on('quit', () => this.emit('quit', ses.message))

        this.getData(userId, sessionId, message, (err, data) => {
          if (!err) {
            let sessionState = null
            ses.userData = data.userData || {}
            ses.conversationData = data.conversationData || {}
            ses.perUserInConversationData = data.perUserConversationData || {}
            if (ses.perUserInConversationData.hasOwnProperty(consts.Data.SessionState)) {
              sessionState = ses.perUserInConversationData[consts.Data.SessionState]
              delete ses.perUserInConversationData[consts.Data.SessionState]
            }
            if (options.replyToDialogId) {
              if (sessionState && sessionState.callstack[sessionState.callstack.length - 1].id === options.replyToDialogId) {
                ses.dispatch(sessionState, message)
              }
            } else {
              ses.dispatch(sessionState, message)
            }
          } else {
            console.error(err, message)
            this.emit('error', err, message)
          }
        })
      } else if (res) {
        let msg = null
        switch (message.type) {
          case 'BotAddedToConversation':
            msg = this.options.groupWelcomeMessage
            break
          case 'UserAddedToConversation':
            msg = this.options.userWelcomeMessage
            break
          case 'EndOfConversation':
            msg = this.options.goodbyeMessage
            break
        }
        res.send(msg ? { type: message.type, text: msg } : {})
      }
    } catch (e) {
      console.error(e)
      const error = e instanceof Error ? e : new Error(e.toString())
      this.emit('error', error)
      return res ? res.send(500) : null
    }
  }

  getData (userId, sessionId, msg, callback) {
    const botPath = `/${this.options.appId}`
    const userPath = `${botPath}/users/${userId}`
    const convoPath = `${botPath}/conversations/${sessionId}`
    const perUserConvoPath = `${botPath}/conversations/${sessionId}/users/${userId}`

    let ops = 3
    const data = {}

    function load (id, field, store, botData) {
      data[field] = botData
      if (store) {
        store.get(id, (err, item) => {
          if (callback) {
            if (!err) {
              data[field] = item
              if (--ops === 0) {
                callback(null, data)
              }
            } else {
              callback(err, null)
              callback = null
            }
          }
        })
      } else if (callback && --ops === 0) {
        callback(null, data)
      }
    }

    load(userPath, 'userData', this.options.userStore, msg.botUserData)
    load(convoPath, 'conversationData', this.options.conversationStore, msg.botConversationData)
    load(perUserConvoPath, 'perUserConversationData', this.options.perUserInConversationStore, msg.botPerUserInConversationData)
  }

  saveData (userId, sessionId, data, msg, callback) {
    const botPath = `/${this.options.appId}`
    const userPath = `${botPath}/users/${userId}`
    const convoPath = `${botPath}/conversations/${sessionId}`
    const perUserConvoPath = `${botPath}/conversations/${sessionId}/users/${userId}`

    let ops = 3

    function save (id, field, store, botData) {
      if (store) {
        store.save(id, botData, (err) => {
          if (callback) {
            if (!err && --ops === 0) {
              callback(null)
            } else {
              callback(err)
              callback = null
            }
          }
        })
      } else {
        msg[field] = botData
        if (callback && --ops === 0) {
          callback(null)
        }
      }
    }

    save(userPath, 'botUserData', this.options.userStore, data.userData)
    save(convoPath, 'botConversationData', this.options.conversationStore, data.conversationData)
    save(perUserConvoPath, 'botPerUserInConversationData', this.options.perUserInConversationStore, data.perUserConversationData)
  }
}

export default ConnectorBot
<|endoftext|>