UNPKG

dojox

Version:

Dojo eXtensions, a rollup of many useful sub-projects and varying states of maturity – from very stable and robust, to alpha and experimental. See individual projects contain README files for details.

664 lines (581 loc) 20.4 kB
define(["dojo/_base/lang", "dojo/_base/declare", "dojo/has", "dojo/on", "dojo/aspect", "dojo/touch", "dojo/_base/Color", "dojo/dom", "dojo/dom-geometry", "dojo/_base/window", "./_base","./canvas", "./shape", "./matrix"], function(lang, declare, has, on, aspect, touch, Color, dom, domGeom, win, g, canvas, shapeLib, m){ function makeFakeEvent(event){ // summary: // Generates a "fake", fully mutable event object by copying the properties from an original host Event // object to a new standard JavaScript object. var fakeEvent = {}; for(var k in event){ if(typeof event[k] === "function"){ // Methods (like preventDefault) must be invoked on the original event object, or they will not work fakeEvent[k] = lang.hitch(event, k); } else{ fakeEvent[k] = event[k]; } } return fakeEvent; } // Browsers that implement the current (January 2013) WebIDL spec allow Event object properties to be mutated // using Object.defineProperty; some older WebKits (Safari 6-) and at least IE10- do not follow the spec. Direct // mutation is, of course, much faster when it can be done. has.add("dom-mutableEvents", function(){ var event = document.createEvent("UIEvents"); try { if(Object.defineProperty){ Object.defineProperty(event, "type", { value: "foo" }); }else{ event.type = "foo"; } return event.type === "foo"; }catch(e){ return false; } }); var canvasWithEvents = g.canvasWithEvents = { // summary: // This the graphics rendering bridge for W3C Canvas compliant browsers which extends // the basic canvas drawing renderer bridge to add additional support for graphics events // on Shapes. // Since Canvas is an immediate mode graphics api, with no object graph or // eventing capabilities, use of the canvas module alone will only add in drawing support. // This additional module, canvasWithEvents extends this module with additional support // for handling events on Canvas. By default, the support for events is now included // however, if only drawing capabilities are needed, canvas event module can be disabled // using the dojoConfig option, canvasEvents:true|false. }; canvasWithEvents.Shape = declare("dojox.gfx.canvasWithEvents.Shape", canvas.Shape, { _testInputs: function(/* Object */ ctx, /* Array */ pos){ if(this.clip || (!this.canvasFill && this.strokeStyle)){ // pixel-based until a getStrokedPath-like api is available on the path this._hitTestPixel(ctx, pos); }else{ this._renderShape(ctx); var length = pos.length, t = this.getTransform(); for(var i = 0; i < length; ++i){ var input = pos[i]; // already hit if(input.target){continue;} var x = input.x, y = input.y, p = t ? m.multiplyPoint(m.invert(t), x, y) : { x: x, y: y }; input.target = this._hitTestGeometry(ctx, p.x, p.y); } } }, _hitTestPixel: function(/* Object */ ctx, /* Array */ pos){ for(var i = 0; i < pos.length; ++i){ var input = pos[i]; if(input.target){continue;} var x = input.x, y = input.y; ctx.clearRect(0,0,1,1); ctx.save(); ctx.translate(-x, -y); this._render(ctx, true); input.target = ctx.getImageData(0, 0, 1, 1).data[0] ? this : null; ctx.restore(); } }, _hitTestGeometry: function(ctx, x, y){ return ctx.isPointInPath(x, y) ? this : null; }, _renderFill: function(/* Object */ ctx, /* Boolean */ apply){ // summary: // render fill for the shape // ctx: // a canvas context object // apply: // whether ctx.fill() shall be called if(ctx.pickingMode){ if("canvasFill" in this && apply){ ctx.fill(); } return; } this.inherited(arguments); }, _renderStroke: function(/* Object */ ctx){ // summary: // render stroke for the shape // ctx: // a canvas context object // apply: // whether ctx.stroke() shall be called if(this.strokeStyle && ctx.pickingMode){ var c = this.strokeStyle.color; try{ this.strokeStyle.color = new Color(ctx.strokeStyle); this.inherited(arguments); }finally{ this.strokeStyle.color = c; } }else{ this.inherited(arguments); } }, // events getEventSource: function(){ return this.surface.rawNode; }, on: function(type, listener){ // summary: // Connects an event to this shape. var expectedTarget = this.rawNode; // note that event listeners' targets are automatically fixed up in the canvas's addEventListener method return on(this.getEventSource(), type, function(event){ if(dom.isDescendant(event.target, expectedTarget)){ listener.apply(expectedTarget, arguments); } }); }, connect: function(name, object, method){ // summary: // Deprecated. Connects a handler to an event on this shape. Use `on` instead. if(name.substring(0, 2) == "on"){ name = name.substring(2); } return this.on(name, method ? lang.hitch(object, method) : lang.hitch(null, object)); }, disconnect: function(handle){ // summary: // Deprecated. Disconnects an event handler. Use `handle.remove` instead. handle.remove(); } }); canvasWithEvents.Group = declare("dojox.gfx.canvasWithEvents.Group", [canvasWithEvents.Shape, canvas.Group], { _testInputs: function(/*Object*/ ctx, /*Array*/ pos){ var children = this.children, t = this.getTransform(), i, j, input; if(children.length === 0){ return; } var posbk = []; for(i = 0; i < pos.length; ++i){ input = pos[i]; // backup position before transform applied posbk[i] = { x: input.x, y: input.y }; if(input.target){continue;} var x = input.x, y = input.y; var p = t ? m.multiplyPoint(m.invert(t), x, y) : { x: x, y: y }; input.x = p.x; input.y = p.y; } for(i = children.length - 1; i >= 0; --i){ children[i]._testInputs(ctx, pos); // does it need more hit tests ? var allFound = true; for(j = 0; j < pos.length; ++j){ if(pos[j].target == null){ allFound = false; break; } } if(allFound){ break; } } if(this.clip){ // filter positive hittests against the group clipping area for(i = 0; i < pos.length; ++i){ input = pos[i]; input.x = posbk[i].x; input.y = posbk[i].y; if(input.target){ ctx.clearRect(0,0,1,1); ctx.save(); ctx.translate(-input.x, -input.y); this._render(ctx, true); if(!ctx.getImageData(0, 0, 1, 1).data[0]){ input.target = null; } ctx.restore(); } } }else{ for(i = 0; i < pos.length; ++i){ pos[i].x = posbk[i].x; pos[i].y = posbk[i].y; } } } }); canvasWithEvents.Image = declare("dojox.gfx.canvasWithEvents.Image", [canvasWithEvents.Shape, canvas.Image], { _renderShape: function(/* Object */ ctx){ // summary: // render image // ctx: // a canvas context object var s = this.shape; if(ctx.pickingMode){ ctx.fillRect(s.x, s.y, s.width, s.height); }else{ this.inherited(arguments); } }, _hitTestGeometry: function(ctx, x, y){ // TODO: improve hit testing to take into account transparency var s = this.shape; return x >= s.x && x <= s.x + s.width && y >= s.y && y <= s.y + s.height ? this : null; } }); canvasWithEvents.Text = declare("dojox.gfx.canvasWithEvents.Text", [canvasWithEvents.Shape, canvas.Text], { _testInputs: function(ctx, pos){ return this._hitTestPixel(ctx, pos); } }); canvasWithEvents.Rect = declare("dojox.gfx.canvasWithEvents.Rect", [canvasWithEvents.Shape, canvas.Rect], {}); canvasWithEvents.Circle = declare("dojox.gfx.canvasWithEvents.Circle", [canvasWithEvents.Shape, canvas.Circle], {}); canvasWithEvents.Ellipse = declare("dojox.gfx.canvasWithEvents.Ellipse", [canvasWithEvents.Shape, canvas.Ellipse],{}); canvasWithEvents.Line = declare("dojox.gfx.canvasWithEvents.Line", [canvasWithEvents.Shape, canvas.Line],{}); canvasWithEvents.Polyline = declare("dojox.gfx.canvasWithEvents.Polyline", [canvasWithEvents.Shape, canvas.Polyline],{}); canvasWithEvents.Path = declare("dojox.gfx.canvasWithEvents.Path", [canvasWithEvents.Shape, canvas.Path],{}); canvasWithEvents.TextPath = declare("dojox.gfx.canvasWithEvents.TextPath", [canvasWithEvents.Shape, canvas.TextPath],{}); // When events are dispatched using on.emit, certain properties of these events (like target) get overwritten by // the DOM. The only real way to deal with this at the moment, short of never using any standard event properties, // is to store this data out-of-band and fix up the event object passed to the listener by wrapping the listener. // The out-of-band data is stored here. var fixedEventData = null; canvasWithEvents.Surface = declare("dojox.gfx.canvasWithEvents.Surface", canvas.Surface, { constructor: function(){ this._elementUnderPointer = null; }, fixTarget: function(listener){ // summary: // Corrects the `target` properties of the event object passed to the actual listener. // listener: Function // An event listener function. var surface = this; return function(event){ var k; if(fixedEventData){ if(has("dom-mutableEvents")){ Object.defineProperties(event, fixedEventData); }else{ event = makeFakeEvent(event); for(k in fixedEventData){ event[k] = fixedEventData[k].value; } } }else{ // non-synthetic events need to have target correction too, but since there is no out-of-band // data we need to figure out the target ourselves var canvas = surface.getEventSource(), target = canvas._dojoElementFromPoint( // touch events may not be fixed at this point, so clientX/Y may not be set on the // event object (event.changedTouches ? event.changedTouches[0] : event).pageX, (event.changedTouches ? event.changedTouches[0] : event).pageY ); if(has("dom-mutableEvents")){ Object.defineProperties(event, { target: { value: target, configurable: true, enumerable: true }, gfxTarget: { value: target.shape, configurable: true, enumerable: true } }); }else{ event = makeFakeEvent(event); event.target = target; event.gfxTarget = target.shape; } } // fixTouchListener in dojo/on undoes target changes by copying everything from changedTouches even // if the value already exists on the event; of course, this canvas implementation currently only // supports one pointer at a time. if we wanted to make sure all the touches arrays' targets were // updated correctly as well, we could support multi-touch and this workaround would not be needed if(has("touch")){ // some standard properties like clientX/Y are not provided on the main touch event object, // so copy them over if we need to if(event.changedTouches && event.changedTouches[0]){ var changedTouch = event.changedTouches[0]; for(k in changedTouch){ if(!event[k]){ if(has("dom-mutableEvents")){ Object.defineProperty(event, k, { value: changedTouch[k], configurable: true, enumerable: true }); }else{ event[k] = changedTouch[k]; } } } } event.corrected = event; } return listener.call(this, event); }; }, _checkPointer: function(event){ // summary: // Emits enter/leave/over/out events in response to the pointer entering/leaving the inner elements // within the canvas. function emit(types, target, relatedTarget){ // summary: // Emits multiple synthetic events defined in `types` with the given target `target`. var oldBubbles = event.bubbles; for(var i = 0, type; (type = types[i]); ++i){ // targets get reset when the event is dispatched so we need to give information to fixTarget to // restore the target on the dispatched event through a back channel fixedEventData = { target: { value: target, configurable: true, enumerable: true}, gfxTarget: { value: target.shape, configurable: true, enumerable: true }, relatedTarget: { value: relatedTarget, configurable: true, enumerable: true } }; // bubbles can be set directly, though. Object.defineProperty(event, "bubbles", { value: type.bubbles, configurable: true, enumerable: true }); on.emit(canvas, type.type, event); fixedEventData = null; } Object.defineProperty(event, "bubbles", { value: oldBubbles, configurable: true, enumerable: true }); } // Types must be arrays because hash map order is not guaranteed but we must fire in order to match normal // event behaviour var TYPES = { out: [ { type: "mouseout", bubbles: true }, { type: "MSPointerOut", bubbles: true }, { type: "pointerout", bubbles: true }, { type: "mouseleave", bubbles: false }, { type: "dojotouchout", bubbles: true} ], over: [ { type: "mouseover", bubbles: true }, { type: "MSPointerOver", bubbles: true }, { type: "pointerover", bubbles: true }, { type: "mouseenter", bubbles: false }, { type: "dojotouchover", bubbles: true} ] }, elementUnderPointer = event.target, oldElementUnderPointer = this._elementUnderPointer, canvas = this.getEventSource(); if(oldElementUnderPointer !== elementUnderPointer){ if(oldElementUnderPointer && oldElementUnderPointer !== canvas){ emit(TYPES.out, oldElementUnderPointer, elementUnderPointer); } this._elementUnderPointer = elementUnderPointer; if(elementUnderPointer && elementUnderPointer !== canvas){ emit(TYPES.over, elementUnderPointer, oldElementUnderPointer); } } }, getEventSource: function(){ return this.rawNode; }, on: function(type, listener){ // summary: // Connects an event to this surface. return on(this.getEventSource(), type, listener); }, connect: function(/*String*/ name, /*Object*/ object, /*Function|String*/ method){ // summary: // Deprecated. Connects a handler to an event on this surface. Use `on` instead. // name: String // The event name // object: Object // The object that method will receive as "this". // method: Function // A function reference, or name of a function in context. if(name.substring(0, 2) == "on"){ name = name.substring(2); } return this.on(name, method ? lang.hitch(object, method) : object); }, disconnect: function(handle){ // summary: // Deprecated. Disconnects a handler. Use `handle.remove` instead. handle.remove(); }, _initMirrorCanvas: function(){ // summary: // Initialises a mirror canvas used for event hit detection. this._initMirrorCanvas = function(){}; var canvas = this.getEventSource(), mirror = this.mirrorCanvas = canvas.ownerDocument.createElement("canvas"); mirror.width = 1; mirror.height = 1; mirror.style.position = "absolute"; mirror.style.left = mirror.style.top = "-99999px"; canvas.parentNode.appendChild(mirror); var moveEvt = "mousemove"; if(has("pointer-events")){ moveEvt = "pointermove"; }else if(has("MSPointer")){ moveEvt = "MSPointerMove"; }else if(has("touch-events")){ moveEvt = "touchmove"; } on(canvas, moveEvt, lang.hitch(this, "_checkPointer")); }, destroy: function(){ if(this.mirrorCanvas){ this.mirrorCanvas.parentNode.removeChild(this.mirrorCanvas); this.mirrorCanvas = null; } this.inherited(arguments); } }); canvasWithEvents.createSurface = function(parentNode, width, height){ // summary: // creates a surface (Canvas) // parentNode: Node // a parent node // width: String // width of surface, e.g., "100px" // height: String // height of surface, e.g., "100px" if(!width && !height){ var pos = domGeom.position(parentNode); width = width || pos.w; height = height || pos.h; } if(typeof width === "number"){ width = width + "px"; } if(typeof height === "number"){ height = height + "px"; } var surface = new canvasWithEvents.Surface(), parent = dom.byId(parentNode), canvas = parent.ownerDocument.createElement("canvas"); canvas.width = g.normalizedLength(width); // in pixels canvas.height = g.normalizedLength(height); // in pixels parent.appendChild(canvas); surface.rawNode = canvas; surface._parent = parent; surface.surface = surface; g._base._fixMsTouchAction(surface); // any event handler added to the canvas needs to have its target fixed. var oldAddEventListener = canvas.addEventListener, oldRemoveEventListener = canvas.removeEventListener, listeners = []; var addEventListenerImpl = function(type, listener, useCapture){ surface._initMirrorCanvas(); var actualListener = surface.fixTarget(listener); listeners.push({ original: listener, actual: actualListener }); oldAddEventListener.call(this, type, actualListener, useCapture); }; var removeEventListenerImpl = function(type, listener, useCapture){ for(var i = 0, record; (record = listeners[i]); ++i){ if(record.original === listener){ oldRemoveEventListener.call(this, type, record.actual, useCapture); listeners.splice(i, 1); break; } } }; try{ Object.defineProperties(canvas, { addEventListener: { value: addEventListenerImpl, enumerable: true, configurable: true }, removeEventListener: { value: removeEventListenerImpl } }); }catch(e){ // Object.defineProperties fails on iOS 4-5. "Not supported on DOM objects"). canvas.addEventListener = addEventListenerImpl; canvas.removeEventListener = removeEventListenerImpl; } canvas._dojoElementFromPoint = function(x, y){ // summary: // Returns the shape under the given (x, y) coordinate. // evt: // mouse event if(!surface.mirrorCanvas){ return this; } var surfacePosition = domGeom.position(this, true); // use canvas-relative positioning x -= surfacePosition.x; y -= surfacePosition.y; var mirror = surface.mirrorCanvas, ctx = mirror.getContext("2d"), children = surface.children; ctx.clearRect(0, 0, mirror.width, mirror.height); ctx.save(); ctx.strokeStyle = "rgba(127,127,127,1.0)"; ctx.fillStyle = "rgba(127,127,127,1.0)"; ctx.pickingMode = true; // TODO: Make inputs non-array var inputs = [ { x: x, y: y } ]; // process the inputs to find the target. for(var i = children.length - 1; i >= 0; i--){ children[i]._testInputs(ctx, inputs); if(inputs[0].target){ break; } } ctx.restore(); return inputs[0] && inputs[0].target ? inputs[0].target.rawNode : this; }; return surface; // dojox/gfx.Surface }; var Creator = { createObject: function(){ // summary: // Creates a synthetic, partially-interoperable Element object used to uniquely identify the given // shape within the canvas pseudo-DOM. var shape = this.inherited(arguments), listeners = {}; shape.rawNode = { shape: shape, ownerDocument: shape.surface.rawNode.ownerDocument, parentNode: shape.parent ? shape.parent.rawNode : null, addEventListener: function(type, listener){ var listenersOfType = listeners[type] = (listeners[type] || []); for(var i = 0, record; (record = listenersOfType[i]); ++i){ if(record.listener === listener){ return; } } listenersOfType.push({ listener: listener, handle: aspect.after(this, "on" + type, shape.surface.fixTarget(listener), true) }); }, removeEventListener: function(type, listener){ var listenersOfType = listeners[type]; if(!listenersOfType){ return; } for(var i = 0, record; (record = listenersOfType[i]); ++i){ if(record.listener === listener){ record.handle.remove(); listenersOfType.splice(i, 1); return; } } } }; return shape; } }; canvasWithEvents.Group.extend(Creator); canvasWithEvents.Surface.extend(Creator); return canvasWithEvents; });