SeanCocteau
SeanCocteau

Reputation: 1876

Javascript, MouseEvents and Classes

With all the buzz of HTML5 I've began by investigating the Canvas's capabilities along with interaction from Javascript. Unfortunately things haven't been going well due to idiosyncrasies of Javascript and its OO model.

For instance, I figured I could create a wrapper class for my canvas object and effectively box all appropriate methods and properties into it making the development side of things much easier. Unfortunately I'm struggling with the way the mouse handlers are working. In my case, I have the 'DrawArea' class that adds three mouse handlers for drawing rectangles and a 'Draw' routine titled 'Invalidate'. When the mouse events are fired (mouseMove and mouseUp methods), they fail claiming that the 'Invalidate' function is invalid - almost like it is out of context of the method it is being called within. Code below.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
    <script type="text/javascript">

        // Top level variables
        var dWrap;

        // Point Class
        function Point( xPos , yPos ){
            this.X = xPos;
            this.Y = yPos;              
        }

        // Create wrapper class for the draw area
        function DrawArea( da ){
            this.SrcArea = da;
            // Add mouse handlers
            this.SrcArea.addEventListener('mousedown', this.mouseDown, false);
            this.SrcArea.addEventListener('mousemove', this.mouseMove, false);
            this.SrcArea.addEventListener('mouseup', this.mouseUp, false);
            // And draw
            // NOTE: this call works!
            this.Invalidate();
        }

        // Properities
        DrawArea.prototype.ProposedStartPos = undefined;
        DrawArea.prototype.ProposedEndPos = undefined;
        DrawArea.prototype.IsDrawing = false;

        // Mouse Events

        // Handles the mouse down event for new objects
        DrawArea.prototype.mouseDown = function(m) {
            // Flag as drawing
            this.IsDrawing = true;
            // Record the start position
            this.ProposedStartPos = new Point(m.layerX, m.layerY);
        }

        // Handles mouse movement when creating a proposed object
        DrawArea.prototype.mouseMove = function(m) {
            if (this.IsDrawing) {
                // Set the current  end position
                this.ProposedEndPos = new Point(m.layerX, m.layerY);
                // NOTE: this call doesn't work!
                this.Invalidate();
            }
        }

        // Handles the completion of a proposed object
        DrawArea.prototype.mouseUp = function(m) {
            if (this.IsDrawing) {
                // Set the final  end position
                if (m.type != 'mouseout') this.ProposedEndPos = new Point(m.layerX, m.layerY);
                // NOTE: this call doesn't work!
                this.Invalidate();
            }
        }

        // Redraws the source object
        DrawArea.prototype.Invalidate = function() {
            // Obtain    
            if (this.SrcArea.getContext) {
                var context = this.SrcArea.getContext('2d');
                // Clean up
                context.clearRect(0, 0, this.SrcArea.width, this.SrcArea.height);
                context.save();

                // Draw the background
                context.strokeStyle = "#000000";
                context.fillStyle = "#AAAFFF";
                context.beginPath();
                context.rect(0, 0, this.SrcArea.width, this.SrcArea.height);
                context.closePath();
                context.stroke();
                context.fill();

                // Are we drawing any proposed items
                if (this.IsDrawing) {
                    context.strokeStyle = this.ProposedColorStroke;
                    context.fillStyle = this.ProposedColorFill;
                    context.beginPath();
                    context.rect(this.ProposedStartPos.X, this.ProposedStartPos.Y, this.ProposedEndPos.X - this.ProposedStartPos.X, this.ProposedEndPos.Y - this.ProposedStartPos.Y);
                    context.closePath();
                    context.stroke();
                    context.fill();
                }
            }
            // Flush
            context.restore();
        }

        // Initialise the wrapper class
        $(document).ready(function() {              
            // Obtain the canvas and set
            var cWrap = $('#cDrawArea')[0];             
            dWrap = new DrawArea( cWrap );          
        });

Html code...

<body>
    <div id="DrawContainer">        
        <canvas id="cDrawArea" width="800" height="600"></canvas>
    </div>
</body>

What am I missing here and is this a particular efficient and smart way of handling complex objects that will require a lot of behind the scenes code?

Upvotes: 2

Views: 3181

Answers (3)

T.J. Crowder
T.J. Crowder

Reputation: 1075337

This is a common misunderstanding. JavaScript doesn't have classes, and it doesn't have methods. It has functions. Unlike some other languages (Java, C#, C++), this is determined entirely by how a function is called, not where a function is defined. (This is incredibly powerful, but surprising to someone coming from class-based languages.) So this line of code:

this.SrcArea.addEventListener('mousedown', this.mouseDown, false);

...does hook up the function referenced by the mouseDown property, but does nothing to ensure that when that function is called, this is the value you expect.

If you're really using an ECMAScript5-compliant browser (there are some that have canvas but are not completely ES5-compliant), you can use the new Function#bind feature, but again note that this is only about two years old:

// Create wrapper class for the draw area
function DrawArea( da ){
    this.SrcArea = da;
    // Add mouse handlers using ECMAScript5's new `Function#bind`
    this.SrcArea.addEventListener('mousedown', this.mouseDown.bind(this), false);
    this.SrcArea.addEventListener('mousemove', this.mouseMove.bind(this), false);
    this.SrcArea.addEventListener('mouseup', this.mouseUp.bind(this), false);
    // And draw
    // NOTE: this call works!
    this.Invalidate();
}

Alternately, you can do pretty much the same thing yourself using closures:

// Create wrapper class for the draw area
function DrawArea( da ){
    var self = this; // Set up a variable referencing the instance

    this.SrcArea = da;
    // Add mouse handlers - these are closures over the context of this
    // call to the constructor, and have access to the `self` variable
    // above. They just relay the call to the functions on the prototype,
    // but in a way that ensures that `this` is what you expect.
    this.SrcArea.addEventListener('mousedown', function(event) {
        return self.mouseDown(event);
    }, false);
    this.SrcArea.addEventListener('mousemove', function(event) {
        return self.mouseMove(event);
    }, false);
    this.SrcArea.addEventListener('mouseup', function(event) {
        return self.mouseUp(event);
    }, false);
    // And draw
    // NOTE: this call works!
    this.Invalidate();
}

More reading:

Upvotes: 4

pimvdb
pimvdb

Reputation: 154938

The this is not the DrawArea instance in the handler, but the element itself.

You should bind (freeze) the this value with bind. This is the easiest, but is not available in all browsers. There is a shim available, though.

//                                   guarantee the 'this' value inside handler
this.SrcArea.addEventListener('mousedown', this.mouseDown.bind(this), false);

http://jsfiddle.net/KdnZC/

Upvotes: 1

Pointy
Pointy

Reputation: 413976

Try:

        this.SrcArea.addEventListener('mousedown', this.mouseDown.bind(this), false);
        this.SrcArea.addEventListener('mousemove', this.mouseMove.bind(this), false);
        this.SrcArea.addEventListener('mouseup', this.mouseUp.bind(this), false);

The "bind()" method on the Function prototype (should be in any browser with <canvas> I think) returns a function that will force the this value to be the parameter you pass, in this case your wrapper object instance.

If you don't do something like that, then the handler won't have the this you expect.

Upvotes: 3

Related Questions