source: Dev/trunk/src/client/dojox/gfx/canvasWithEvents.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 20.0 KB
Line 
1define(["dojo/_base/lang", "dojo/_base/declare", "dojo/has", "dojo/on", "dojo/aspect", "dojo/touch", "dojo/_base/Color", "dojo/dom",
2                "dojo/dom-geometry", "dojo/_base/window", "./_base","./canvas", "./shape", "./matrix"],
3function(lang, declare, has, on, aspect, touch, Color, dom, domGeom, win, g, canvas, shapeLib, m){
4        function makeFakeEvent(event){
5                // summary:
6                //              Generates a "fake", fully mutable event object by copying the properties from an original host Event
7                //              object to a new standard JavaScript object.
8
9                var fakeEvent = {};
10                for(var k in event){
11                        if(typeof event[k] === "function"){
12                                // Methods (like preventDefault) must be invoked on the original event object, or they will not work
13                                fakeEvent[k] = lang.hitch(event, k);
14                        }
15                        else{
16                                fakeEvent[k] = event[k];
17                        }
18                }
19                return fakeEvent;
20        }
21
22        // Browsers that implement the current (January 2013) WebIDL spec allow Event object properties to be mutated
23        // using Object.defineProperty; some older WebKits (Safari 6-) and at least IE10- do not follow the spec. Direct
24        // mutation is, of course, much faster when it can be done.
25    has.add("dom-mutableEvents", function(){
26        var event = document.createEvent("UIEvents");
27        try {
28            if(Object.defineProperty){
29                Object.defineProperty(event, "type", { value: "foo" });
30            }else{
31                event.type = "foo";
32            }
33            return event.type === "foo";
34        }catch(e){
35            return false;
36        }
37    });
38
39        has.add("MSPointer", navigator.msPointerEnabled);
40        has.add("pointer-events", navigator.pointerEnabled);
41
42        var canvasWithEvents = g.canvasWithEvents = {
43                // summary:
44                //              This the graphics rendering bridge for W3C Canvas compliant browsers which extends
45                //              the basic canvas drawing renderer bridge to add additional support for graphics events
46                //              on Shapes.
47                //              Since Canvas is an immediate mode graphics api, with no object graph or
48                //              eventing capabilities, use of the canvas module alone will only add in drawing support.
49                //              This additional module, canvasWithEvents extends this module with additional support
50                //              for handling events on Canvas.  By default, the support for events is now included
51                //              however, if only drawing capabilities are needed, canvas event module can be disabled
52                //              using the dojoConfig option, canvasEvents:true|false.
53        };
54
55        canvasWithEvents.Shape = declare("dojox.gfx.canvasWithEvents.Shape", canvas.Shape, {
56                _testInputs: function(/* Object */ ctx, /* Array */ pos){
57                        if(this.clip || (!this.canvasFill && this.strokeStyle)){
58                                // pixel-based until a getStrokedPath-like api is available on the path
59                                this._hitTestPixel(ctx, pos);
60                        }else{
61                                this._renderShape(ctx);
62                                var length = pos.length,
63                                        t = this.getTransform();
64
65                                for(var i = 0; i < length; ++i){
66                                        var input = pos[i];
67                                        // already hit
68                                        if(input.target){continue;}
69                                        var x = input.x,
70                                                y = input.y,
71                                                p = t ? m.multiplyPoint(m.invert(t), x, y) : { x: x, y: y };
72                                        input.target = this._hitTestGeometry(ctx, p.x, p.y);
73                                }
74                        }
75                },
76
77                _hitTestPixel: function(/* Object */ ctx, /* Array */ pos){
78                        for(var i = 0; i < pos.length; ++i){
79                                var input = pos[i];
80                                if(input.target){continue;}
81                                var x = input.x,
82                                        y = input.y;
83                                ctx.clearRect(0,0,1,1);
84                                ctx.save();
85                                ctx.translate(-x, -y);
86                                this._render(ctx, true);
87                                input.target = ctx.getImageData(0, 0, 1, 1).data[0] ? this : null;
88                                ctx.restore();
89                        }
90                },
91
92                _hitTestGeometry: function(ctx, x, y){
93                        return ctx.isPointInPath(x, y) ? this : null;
94                },
95
96                _renderFill: function(/* Object */ ctx, /* Boolean */ apply){
97                        // summary:
98                        //              render fill for the shape
99                        // ctx:
100                        //              a canvas context object
101                        // apply:
102                        //              whether ctx.fill() shall be called
103                        if(ctx.pickingMode){
104                                if("canvasFill" in this && apply){
105                                        ctx.fill();
106                                }
107                                return;
108                        }
109                        this.inherited(arguments);
110                },
111
112                _renderStroke: function(/* Object */ ctx){
113                        // summary:
114                        //              render stroke for the shape
115                        // ctx:
116                        //              a canvas context object
117                        // apply:
118                        //              whether ctx.stroke() shall be called
119                        if(this.strokeStyle && ctx.pickingMode){
120                                var c = this.strokeStyle.color;
121                                try{
122                                        this.strokeStyle.color = new Color(ctx.strokeStyle);
123                                        this.inherited(arguments);
124                                }finally{
125                                        this.strokeStyle.color = c;
126                                }
127                        }else{
128                                this.inherited(arguments);
129                        }
130                },
131
132                // events
133
134                getEventSource: function(){
135                        return this.surface.rawNode;
136                },
137
138                on: function(type, listener){
139                        // summary:
140                        //              Connects an event to this shape.
141
142                        var expectedTarget = this.rawNode;
143
144                        // note that event listeners' targets are automatically fixed up in the canvas's addEventListener method
145                        return on(this.getEventSource(), type, function(event){
146                                if(dom.isDescendant(event.target, expectedTarget)){
147                                        listener.apply(expectedTarget, arguments);
148                                }
149                        });
150                },
151
152                connect: function(name, object, method){
153                        // summary:
154                        //              Deprecated. Connects a handler to an event on this shape. Use `on` instead.
155
156                        if(name.substring(0, 2) == "on"){
157                                name = name.substring(2);
158                        }
159                        return this.on(name, method ? lang.hitch(object, method) : lang.hitch(null, object));
160                },
161
162                disconnect: function(handle){
163                        // summary:
164                        //              Deprecated. Disconnects an event handler. Use `handle.remove` instead.
165
166                        handle.remove();
167                }
168        });
169
170        canvasWithEvents.Group = declare("dojox.gfx.canvasWithEvents.Group", [canvasWithEvents.Shape, canvas.Group], {
171                _testInputs: function(/*Object*/ ctx, /*Array*/ pos){
172                        var children = this.children,
173                                t = this.getTransform(),
174                                i,
175                                j,
176                                input;
177
178                        if(children.length === 0){
179                                return;
180                        }
181                        var posbk = [];
182                        for(i = 0; i < pos.length; ++i){
183                                input = pos[i];
184                                // backup position before transform applied
185                                posbk[i] = {
186                                        x: input.x,
187                                        y: input.y
188                                };
189                                if(input.target){continue;}
190                                var x = input.x, y = input.y;
191                                var p = t ? m.multiplyPoint(m.invert(t), x, y) : { x: x, y: y };
192                                input.x = p.x;
193                                input.y = p.y;
194                        }
195                        for(i = children.length - 1; i >= 0; --i){
196                                children[i]._testInputs(ctx, pos);
197                                // does it need more hit tests ?
198                                var allFound = true;
199                                for(j = 0; j < pos.length; ++j){
200                                        if(pos[j].target == null){
201                                                allFound = false;
202                                                break;
203                                        }
204                                }
205                                if(allFound){
206                                        break;
207                                }
208                        }
209                        if(this.clip){
210                                // filter positive hittests against the group clipping area
211                                for(i = 0; i < pos.length; ++i){
212                                        input = pos[i];
213                                        input.x = posbk[i].x;
214                                        input.y = posbk[i].y;
215                                        if(input.target){
216                                                ctx.clearRect(0,0,1,1);
217                                                ctx.save();
218                                                ctx.translate(-input.x, -input.y);
219                                                this._render(ctx, true);
220                                                if(!ctx.getImageData(0, 0, 1, 1).data[0]){
221                                                        input.target = null;
222                                                }
223                                                ctx.restore();
224                                        }
225                                }
226                        }else{
227                                for(i = 0; i < pos.length; ++i){
228                                        pos[i].x = posbk[i].x;
229                                        pos[i].y = posbk[i].y;
230                                }
231                        }
232                }
233
234        });
235
236        canvasWithEvents.Image = declare("dojox.gfx.canvasWithEvents.Image", [canvasWithEvents.Shape, canvas.Image], {
237                _renderShape: function(/* Object */ ctx){
238                        // summary:
239                        //              render image
240                        // ctx:
241                        //              a canvas context object
242                        var s = this.shape;
243                        if(ctx.pickingMode){
244                                ctx.fillRect(s.x, s.y, s.width, s.height);
245                        }else{
246                                this.inherited(arguments);
247                        }
248                },
249                _hitTestGeometry: function(ctx, x, y){
250                        // TODO: improve hit testing to take into account transparency
251                        var s = this.shape;
252                        return x >= s.x && x <= s.x + s.width && y >= s.y && y <= s.y + s.height ? this : null;
253                }
254        });
255
256        canvasWithEvents.Text = declare("dojox.gfx.canvasWithEvents.Text", [canvasWithEvents.Shape, canvas.Text], {
257                _testInputs: function(ctx, pos){
258                        return this._hitTestPixel(ctx, pos);
259                }
260        });
261
262        canvasWithEvents.Rect = declare("dojox.gfx.canvasWithEvents.Rect", [canvasWithEvents.Shape, canvas.Rect], {});
263        canvasWithEvents.Circle = declare("dojox.gfx.canvasWithEvents.Circle", [canvasWithEvents.Shape, canvas.Circle], {});
264        canvasWithEvents.Ellipse = declare("dojox.gfx.canvasWithEvents.Ellipse", [canvasWithEvents.Shape, canvas.Ellipse],{});
265        canvasWithEvents.Line = declare("dojox.gfx.canvasWithEvents.Line", [canvasWithEvents.Shape, canvas.Line],{});
266        canvasWithEvents.Polyline = declare("dojox.gfx.canvasWithEvents.Polyline", [canvasWithEvents.Shape, canvas.Polyline],{});
267        canvasWithEvents.Path = declare("dojox.gfx.canvasWithEvents.Path", [canvasWithEvents.Shape, canvas.Path],{});
268        canvasWithEvents.TextPath = declare("dojox.gfx.canvasWithEvents.TextPath", [canvasWithEvents.Shape, canvas.TextPath],{});
269
270        // When events are dispatched using on.emit, certain properties of these events (like target) get overwritten by
271        // the DOM. The only real way to deal with this at the moment, short of never using any standard event properties,
272        // is to store this data out-of-band and fix up the event object passed to the listener by wrapping the listener.
273        // The out-of-band data is stored here.
274        var fixedEventData = null;
275
276        canvasWithEvents.Surface = declare("dojox.gfx.canvasWithEvents.Surface", canvas.Surface, {
277                constructor: function(){
278                        this._elementUnderPointer = null;
279                },
280
281                fixTarget: function(listener){
282                        // summary:
283                        //              Corrects the `target` properties of the event object passed to the actual listener.
284                        // listener: Function
285                        //              An event listener function.
286
287                        var surface = this;
288
289                        return function(event){
290                                var k;
291                                if(fixedEventData){
292                                        if(has("dom-mutableEvents")){
293                                                Object.defineProperties(event, fixedEventData);
294                                        }else{
295                                                event = makeFakeEvent(event);
296                                                for(k in fixedEventData){
297                                                        event[k] = fixedEventData[k].value;
298                                                }
299                                        }
300                                }else{
301                                        // non-synthetic events need to have target correction too, but since there is no out-of-band
302                                        // data we need to figure out the target ourselves
303                                        var canvas = surface.getEventSource(),
304                                                target = canvas._dojoElementFromPoint(
305                                                        // touch events may not be fixed at this point, so clientX/Y may not be set on the
306                                                        // event object
307                                                        (event.changedTouches ? event.changedTouches[0] : event).pageX,
308                                                        (event.changedTouches ? event.changedTouches[0] : event).pageY
309                                                );
310                                        if(has("dom-mutableEvents")){
311                                                Object.defineProperties(event, {
312                                                        target: {
313                                                                value: target,
314                                                                configurable: true,
315                                                                enumerable: true
316                                                        },
317                                                        gfxTarget: {
318                                                                value: target.shape,
319                                                                configurable: true,
320                                                                enumerable: true
321                                                        }
322                                                });
323                                        }else{
324                                                event = makeFakeEvent(event);
325                                                event.target = target;
326                                                event.gfxTarget = target.shape;
327                                        }
328                                }
329
330                                // fixTouchListener in dojo/on undoes target changes by copying everything from changedTouches even
331                                // if the value already exists on the event; of course, this canvas implementation currently only
332                                // supports one pointer at a time. if we wanted to make sure all the touches arrays' targets were
333                                // updated correctly as well, we could support multi-touch and this workaround would not be needed
334                                if(has("touch")){
335                                        // some standard properties like clientX/Y are not provided on the main touch event object,
336                                        // so copy them over if we need to
337                                        if(event.changedTouches && event.changedTouches[0]){
338                                                var changedTouch = event.changedTouches[0];
339                                                for(k in changedTouch){
340                                                        if(!event[k]){
341                                                                if(has("dom-mutableEvents")){
342                                                                        Object.defineProperty(event, k, {
343                                                                                value: changedTouch[k],
344                                                                                configurable: true,
345                                                                                enumerable: true
346                                                                        });
347                                                                }else{
348                                                                        event[k] = changedTouch[k];
349                                                                }
350                                                        }
351                                                }
352                                        }
353                                        event.corrected = event;
354                                }
355
356                                return listener.call(this, event);
357                        };
358                },
359
360                _checkPointer: function(event){
361                        // summary:
362                        //              Emits enter/leave/over/out events in response to the pointer entering/leaving the inner elements
363                        //              within the canvas.
364
365                        function emit(types, target, relatedTarget){
366                                // summary:
367                                //              Emits multiple synthetic events defined in `types` with the given target `target`.
368
369                                var oldBubbles = event.bubbles;
370
371                                for(var i = 0, type; (type = types[i]); ++i){
372                                        // targets get reset when the event is dispatched so we need to give information to fixTarget to
373                                        // restore the target on the dispatched event through a back channel
374                                        fixedEventData = {
375                                                target: { value: target, configurable: true, enumerable: true},
376                                                gfxTarget: { value: target.shape, configurable: true, enumerable: true },
377                                                relatedTarget: { value: relatedTarget, configurable: true, enumerable: true }
378                                        };
379
380                                        // bubbles can be set directly, though.
381                                        Object.defineProperty(event, "bubbles", {
382                                                value: type.bubbles,
383                                                configurable: true,
384                                                enumerable: true
385                                        });
386
387                                        on.emit(canvas, type.type, event);
388                                        fixedEventData = null;
389                                }
390
391                                Object.defineProperty(event, "bubbles", { value: oldBubbles, configurable: true, enumerable: true });
392                        }
393
394                        // Types must be arrays because hash map order is not guaranteed but we must fire in order to match normal
395                        // event behaviour
396                        var TYPES = {
397                                        out: [
398                                                { type: "mouseout", bubbles: true },
399                                                { type: "MSPointerOut", bubbles: true },
400                                                { type: "pointerout", bubbles: true },
401                                                { type: "mouseleave", bubbles: false },
402                                                { type: "dojotouchout", bubbles: true}
403                                        ],
404                                        over: [
405                                                { type: "mouseover", bubbles: true },
406                                                { type: "MSPointerOver", bubbles: true },
407                                                { type: "pointerover", bubbles: true },
408                                                { type: "mouseenter", bubbles: false },
409                                                { type: "dojotouchover", bubbles: true}
410                                        ]
411                                },
412                                elementUnderPointer = event.target,
413                                oldElementUnderPointer = this._elementUnderPointer,
414                                canvas = this.getEventSource();
415
416                        if(oldElementUnderPointer !== elementUnderPointer){
417                                if(oldElementUnderPointer && oldElementUnderPointer !== canvas){
418                                        emit(TYPES.out, oldElementUnderPointer, elementUnderPointer);
419                                }
420
421                                this._elementUnderPointer = elementUnderPointer;
422
423                                if(elementUnderPointer && elementUnderPointer !== canvas){
424                                        emit(TYPES.over, elementUnderPointer, oldElementUnderPointer);
425                                }
426                        }
427                },
428
429                getEventSource: function(){
430                        return this.rawNode;
431                },
432
433                on: function(type, listener){
434                        // summary:
435                        //              Connects an event to this surface.
436
437                        return on(this.getEventSource(), type, listener);
438                },
439
440                connect: function(/*String*/ name, /*Object*/ object, /*Function|String*/ method){
441                        // summary:
442                        //              Deprecated. Connects a handler to an event on this surface. Use `on` instead.
443                        // name: String
444                        //              The event name
445                        // object: Object
446                        //              The object that method will receive as "this".
447                        // method: Function
448                        //              A function reference, or name of a function in context.
449
450                        if(name.substring(0, 2) == "on"){
451                                name = name.substring(2);
452                        }
453                        return this.on(name, method ? lang.hitch(object, method) : object);
454                },
455
456                disconnect: function(handle){
457                        // summary:
458                        //              Deprecated. Disconnects a handler. Use `handle.remove` instead.
459
460                        handle.remove();
461                },
462
463                _initMirrorCanvas: function(){
464                        // summary:
465                        //              Initialises a mirror canvas used for event hit detection.
466
467                        this._initMirrorCanvas = function(){};
468
469                        var canvas = this.getEventSource(),
470                                mirror = this.mirrorCanvas = canvas.ownerDocument.createElement("canvas");
471
472                        mirror.width = 1;
473                        mirror.height = 1;
474                        mirror.style.position = "absolute";
475                        mirror.style.left = mirror.style.top = "-99999px";
476                        canvas.parentNode.appendChild(mirror);
477
478                        var moveEvt = "mousemove";
479                        if(has("pointer-events")){
480                                moveEvt = "pointermove";
481                        }else if(has("MSPointer")){
482                                moveEvt = "MSPointerMove";
483                        }else if(has("touch")){
484                                moveEvt = "touchmove";
485                        }
486                        on(canvas, moveEvt, lang.hitch(this, "_checkPointer"));
487                },
488
489                destroy: function(){
490                        if(this.mirrorCanvas){
491                                this.mirrorCanvas.parentNode.removeChild(this.mirrorCanvas);
492                                this.mirrorCanvas = null;
493                        }
494                        this.inherited(arguments);
495                }
496        });
497
498        canvasWithEvents.createSurface = function(parentNode, width, height){
499                // summary:
500                //              creates a surface (Canvas)
501                // parentNode: Node
502                //              a parent node
503                // width: String
504                //              width of surface, e.g., "100px"
505                // height: String
506                //              height of surface, e.g., "100px"
507
508                if(!width && !height){
509                        var pos = domGeom.position(parentNode);
510                        width  = width  || pos.w;
511                        height = height || pos.h;
512                }
513                if(typeof width === "number"){
514                        width = width + "px";
515                }
516                if(typeof height === "number"){
517                        height = height + "px";
518                }
519
520                var surface = new canvasWithEvents.Surface(),
521                        parent = dom.byId(parentNode),
522                        canvas = parent.ownerDocument.createElement("canvas");
523
524                canvas.width  = g.normalizedLength(width);      // in pixels
525                canvas.height = g.normalizedLength(height);     // in pixels
526
527                parent.appendChild(canvas);
528                surface.rawNode = canvas;
529                surface._parent = parent;
530                surface.surface = surface;
531
532                g._base._fixMsTouchAction(surface);
533
534                // any event handler added to the canvas needs to have its target fixed.
535                var oldAddEventListener = canvas.addEventListener,
536                        oldRemoveEventListener = canvas.removeEventListener,
537                        listeners = [];
538
539                var addEventListenerImpl = function(type, listener, useCapture){
540                        surface._initMirrorCanvas();
541
542                        var actualListener = surface.fixTarget(listener);
543                        listeners.push({ original: listener, actual: actualListener });
544                        oldAddEventListener.call(this, type, actualListener, useCapture);
545                };
546                var removeEventListenerImpl = function(type, listener, useCapture){
547                        for(var i = 0, record; (record = listeners[i]); ++i){
548                                if(record.original === listener){
549                                        oldRemoveEventListener.call(this, type, record.actual, useCapture);
550                                        listeners.splice(i, 1);
551                                        break;
552                                }
553                        }
554                };
555                try{
556                        Object.defineProperties(canvas, {
557                                addEventListener: {
558                                        value: addEventListenerImpl,
559                                        enumerable: true,
560                                        configurable: true
561                                },
562                                removeEventListener: {
563                                        value: removeEventListenerImpl
564                                }
565                        });
566                }catch(e){
567                        // Object.defineProperties fails on iOS 4-5. "Not supported on DOM objects").
568                        canvas.addEventListener = addEventListenerImpl;
569                        canvas.removeEventListener = removeEventListenerImpl;
570                }
571
572
573                canvas._dojoElementFromPoint = function(x, y){
574                        // summary:
575                        //              Returns the shape under the given (x, y) coordinate.
576                        // evt:
577                        //              mouse event
578
579                        if(!surface.mirrorCanvas){
580                                return this;
581                        }
582
583                        var surfacePosition = domGeom.position(this, true);
584
585                        // use canvas-relative positioning
586                        x -= surfacePosition.x;
587                        y -= surfacePosition.y;
588
589                        var mirror = surface.mirrorCanvas,
590                                ctx = mirror.getContext("2d"),
591                                children = surface.children;
592
593                        ctx.clearRect(0, 0, mirror.width, mirror.height);
594                        ctx.save();
595                        ctx.strokeStyle = "rgba(127,127,127,1.0)";
596                        ctx.fillStyle = "rgba(127,127,127,1.0)";
597                        ctx.pickingMode = true;
598
599                        // TODO: Make inputs non-array
600                        var inputs = [ { x: x, y: y } ];
601
602                        // process the inputs to find the target.
603                        for(var i = children.length - 1; i >= 0; i--){
604                                children[i]._testInputs(ctx, inputs);
605
606                                if(inputs[0].target){
607                                        break;
608                                }
609                        }
610                        ctx.restore();
611                        return inputs[0] && inputs[0].target ? inputs[0].target.rawNode : this;
612                };
613
614
615                return surface; // dojox/gfx.Surface
616        };
617
618        var Creator = {
619                createObject: function(){
620                        // summary:
621                        //              Creates a synthetic, partially-interoperable Element object used to uniquely identify the given
622                        //              shape within the canvas pseudo-DOM.
623
624                        var shape = this.inherited(arguments),
625                                listeners = {};
626
627                        shape.rawNode = {
628                                shape: shape,
629                                ownerDocument: shape.surface.rawNode.ownerDocument,
630                                parentNode: shape.parent ? shape.parent.rawNode : null,
631                                addEventListener: function(type, listener){
632                                        var listenersOfType = listeners[type] = (listeners[type] || []);
633                                        for(var i = 0, record; (record = listenersOfType[i]); ++i){
634                                                if(record.listener === listener){
635                                                        return;
636                                                }
637                                        }
638
639                                        listenersOfType.push({
640                                                listener: listener,
641                                                handle: aspect.after(this, "on" + type, shape.surface.fixTarget(listener), true)
642                                        });
643                                },
644                                removeEventListener: function(type, listener){
645                                        var listenersOfType = listeners[type];
646                                        if(!listenersOfType){
647                                                return;
648                                        }
649                                        for(var i = 0, record; (record = listenersOfType[i]); ++i){
650                                                if(record.listener === listener){
651                                                        record.handle.remove();
652                                                        listenersOfType.splice(i, 1);
653                                                        return;
654                                                }
655                                        }
656                                }
657                        };
658                        return shape;
659                }
660        };
661
662        canvasWithEvents.Group.extend(Creator);
663        canvasWithEvents.Surface.extend(Creator);
664
665        return canvasWithEvents;
666});
Note: See TracBrowser for help on using the repository browser.