diff --git a/src/event/Event.js b/src/event/Event.js index 7f197216..d450221e 100644 --- a/src/event/Event.js +++ b/src/event/Event.js @@ -24,15 +24,15 @@ var Event = Base.extend(/** @lends Event# */{ this.event = event; }, - isPrevented: false, - isStopped: false, + prevented: false, + stopped: false, /** * Cancels the event if it is cancelable, without stopping further * propagation of the event. */ preventDefault: function() { - this.isPrevented = true; + this.prevented = true; this.event.preventDefault(); }, @@ -40,7 +40,7 @@ var Event = Base.extend(/** @lends Event# */{ * Prevents further propagation of the current event. */ stopPropagation: function() { - this.isStopped = true; + this.stopped = true; this.event.stopPropagation(); }, diff --git a/src/item/Item.js b/src/item/Item.js index efe66342..f12016a1 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -2659,7 +2659,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ isDescendant: function(item) { var parent = this; while (parent = parent._parent) { - if (parent == item) + if (parent === item) return true; } return false; diff --git a/src/path/Segment.js b/src/path/Segment.js index fda1fd73..25816a4f 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -351,7 +351,7 @@ var Segment = Base.extend(/** @lends Segment# */{ }, /** - * The curve location that describes this segment's position ont the path. + * The curve location that describes this segment's position on the path. * * @bean * @type CurveLocation diff --git a/src/project/Project.js b/src/project/Project.js index 262ad840..1aa5fa8a 100644 --- a/src/project/Project.js +++ b/src/project/Project.js @@ -709,6 +709,30 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * SVG content */ + removeOn: function(type) { + var sets = this._removeSets; + if (sets) { + // Always clear the drag set on mouseup + if (type === 'mouseup') + sets.mousedrag = null; + var set = sets[type]; + if (set) { + for (var id in set) { + var item = set[id]; + // If we remove this item, we also need to erase it from all + // other sets. + for (var key in sets) { + var other = sets[key]; + if (other && other != set) + delete other[item._id]; + } + item.remove(); + } + sets[type] = null; + } + } + }, + draw: function(ctx, matrix, pixelRatio) { // Increase the _updateVersion before the draw-loop. After that, items // that are visible will have their _updateVersion set to the new value. diff --git a/src/tool/Tool.js b/src/tool/Tool.js index 5f90f71f..0c248870 100644 --- a/src/tool/Tool.js +++ b/src/tool/Tool.js @@ -322,33 +322,11 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{ }, _fireEvent: function(type, event) { - // Handle items marked in removeOn*() calls first,. - var sets = paper.project._removeSets; - if (sets) { - // Always clear the drag set on mouseup - if (type === 'mouseup') - sets.mousedrag = null; - var set = sets[type]; - if (set) { - for (var id in set) { - var item = set[id]; - // If we remove this item, we also need to erase it from all - // other sets. - for (var key in sets) { - var other = sets[key]; - if (other && other != set) - delete other[item._id]; - } - item.remove(); - } - sets[type] = null; - } - } return this.responds(type) && this.emit(type, new ToolEvent(this, type, event)); }, - _handleEvent: function(type, point, event) { + _handleEvent: function(type, event, point) { // Update global reference to this scope. paper = this._scope; // Now handle event callbacks diff --git a/src/view/CanvasView.js b/src/view/CanvasView.js index d925f49d..886a0e55 100644 --- a/src/view/CanvasView.js +++ b/src/view/CanvasView.js @@ -31,7 +31,7 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ * @name CanvasView#initialize * @param {Size} size the size of the canvas to be created */ - initialize: function CanvasView(project, canvas) { + initialize: function(project, canvas) { // Handle canvas argument if (!(canvas instanceof HTMLCanvasElement)) { // See if the arguments describe the view size: @@ -43,8 +43,6 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ canvas = CanvasProvider.getCanvas(size); } this._context = canvas.getContext('2d'); - // Have Item count installed mouse events. - this._eventCounters = {}; this._pixelRatio = 1; /*#*/ if (__options.environment == 'browser') { if (!/^off|false$/.test(PaperScope.getAttribute(canvas, 'hidpi'))) { @@ -144,150 +142,6 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ project._needsUpdate = false; return true; } -}, -new function() { // Item based mouse handling: - var downPoint, - lastPoint, - overPoint, - downItem, - lastItem, - overItem, - dragItem, - dblClick, - clickTime; - - // Returns true if event was stopped, false otherwise, whether handler was - // called or not! - function callEvent(view, type, event, point, target, lastPoint) { - var item = target, - mouseEvent; - - function call(obj, type) { - if (obj.responds(type)) { - // Only produce the event object if we really need it, and then - // reuse it if we're bubbling. - if (!mouseEvent) { - mouseEvent = new MouseEvent(type, event, point, target, - // Calculate delta if lastPoint was passed - lastPoint ? point.subtract(lastPoint) : null); - } - if (obj.emit(type, mouseEvent) && mouseEvent.isStopped) { - // Call preventDefault() on native event if mouse event was - // handled here. - event.preventDefault(); - return true; - } - } else if (type === 'doubleclick') { - // If obj doesn't respond to doubleclick, fall back to click: - return call(obj, 'click'); - } - } - - // Bubble up the parents and call this event until we're told to stop. - while (item) { - if (call(item, type)) - return true; - item = item.getParent(); - } - // Also call event handler on view, if installed. - if (call(view, type)) - return true; - return false; - } - - return /** @lends CanvasView# */{ - /** - * Returns true if event was stopped, false otherwise, whether handler - * was called or not! - */ - _handleEvent: function(type, point, event) { - // Drop out if we don't have any event handlers for this type - if (!this._eventCounters[type]) - return; - // Run the hit-test first - var project = this._project, - hit = project.hitTest(point, { - tolerance: 0, - fill: true, - stroke: true - }), - item = hit && hit.item, - stopped = false; - // Now handle the mouse events - switch (type) { - case 'mousedown': - stopped = callEvent(this, type, event, point, item); - // See if we're clicking again on the same item, within the - // double-click time. Firefox uses 300ms as the max time - // difference: - dblClick = lastItem == item && (Date.now() - clickTime < 300); - downItem = lastItem = item; - downPoint = lastPoint = overPoint = point; - // Only start dragging if none of the mosedown events have - // stopped propagation. - dragItem = !stopped && item; - // Find the first item pu the chain that responds to drag. - // NOTE: Drag event don't bubble - while (dragItem && !dragItem.responds('mousedrag')) - dragItem = dragItem._parent; - break; - case 'mouseup': - // stopping mousup events does not prevent mousedrag / mousemove - // hanlding here, but it does click / doubleclick - stopped = callEvent(this, type, event, point, item, downPoint); - if (dragItem) { - // If the point has changed since the last mousedrag event, - // send another one - if (lastPoint && !lastPoint.equals(point)) - callEvent(this, 'mousedrag', event, point, dragItem, - lastPoint); - // If we end up over another item, send it a mousemove event - // now. Use point as overPoint, so delta is (0, 0) since - // this will be the first mousemove event for this item. - if (item !== dragItem) { - overPoint = point; - callEvent(this, 'mousemove', event, point, item, - overPoint); - } - } - if (!stopped && item && item === downItem) { - clickTime = Date.now(); - callEvent(this, dblClick ? 'doubleclick' : 'click', event, - downPoint, item); - dblClick = false; - } - downItem = dragItem = null; - break; - case 'mousemove': - // Allow both mousedrag and mousemove events to stop mousemove - // events from reaching tools. - if (dragItem) - stopped = callEvent(this, 'mousedrag', event, point, - dragItem, lastPoint); - // TODO: Consider implementing this again? "If we have a - // mousedrag event, do not send mousemove events to any - // item while we're dragging." - // For now, we let other items receive mousemove events even - // during a drag event. - // If we change the overItem, reset overPoint to point so - // delta is (0, 0) - if (!stopped) { - if (item !== overItem) - overPoint = point; - stopped = callEvent(this, type, event, point, item, - overPoint); - } - lastPoint = overPoint = point; - if (item !== overItem) { - callEvent(this, 'mouseleave', event, point, overItem); - overItem = item; - callEvent(this, 'mouseenter', event, point, item); - } - break; - } - return stopped; - } - }; }); /*#*/ if (__options.environment == 'node') { diff --git a/src/view/View.js b/src/view/View.js index b08b70be..5067c657 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -119,6 +119,8 @@ var View = Base.extend(Emitter, /** @lends View# */{ // Items that need the onFrame handler called on them this._frameItems = {}; this._frameItemCount = 0; + // Count the installed events, see _installEvent() / _uninstallEvent(). + this._eventCounters = {}; }, /** @@ -150,7 +152,8 @@ var View = Base.extend(Emitter, /** @lends View# */{ return true; }, - _events: Base.each(['onResize', 'onMouseDown', 'onMouseUp', 'onMouseMove'], + _events: Base.each(['onResize', 'onMouseDown', 'onMouseUp', 'onMouseMove', + 'onMouseDrag', 'onMouseEnter', 'onMouseLeave'], function(name) { this[name] = { install: function(type) { @@ -681,16 +684,23 @@ var View = Base.extend(Emitter, /** @lends View# */{ }, new function() { // Injection scope for mouse events on the browser /*#*/ if (__options.environment == 'browser') { + + /** + * Native event handling, coordinate conversion, focus handling and + * delegation to view and tool objects. + */ + var tool, prevFocus, tempFocus, - dragging = false; + mouseDown = false; function getView(event) { // Get the view from the current event target. var target = DomEvent.getTarget(event); // Some node do not have the getAttribute method, e.g. SVG nodes. - return target.getAttribute && View._viewsById[target.getAttribute('id')]; + return target.getAttribute && View._viewsById[ + target.getAttribute('id')]; } function viewToProject(view, event) { @@ -710,16 +720,33 @@ new function() { // Injection scope for mouse events on the browser } } - function handleMouseMove(view, point, event) { - view._handleEvent('mousemove', point, event); - var tool = view._scope.tool; - if (tool) { - // If there's no onMouseDrag, fire onMouseMove while dragging. - tool._handleEvent(dragging && tool.responds('mousedrag') - ? 'mousedrag' : 'mousemove', point, event); + function handleEvent(type, view, event, point) { + var eventType = type === 'mousemove' && mouseDown ? 'mousedrag' : type, + project = paper.project, + tool = view._scope.tool; + + function handle(obj) { + obj._handleEvent(eventType, event, point); } + + if (!point) + point = viewToProject(view, event); + if (project) + project.removeOn(eventType); + // Always first call the view's mouse handlers, as required by + // CanvasView, and then handle the active tool, if any. + // No need to call view if it doesn't have event handlers for this type. + if (view._eventCounters[type]) + handle(view); + if (tool) + handle(tool); + // In the end we always call update(), which only updates the view if + // anything has changed in the above calls. view.update(); - return tool; + } + + function handleMouseMove(view, event, point) { + handleEvent('mousemove', view, event, point); } // Touch handling inspired by Hammer.js @@ -747,9 +774,9 @@ new function() { // Injection scope for mouse events on the browser var viewEvents = { 'selectstart dragstart': function(event) { - // Only stop this even if we're dragging already, since otherwise no - // text whatsoever can be selected on the page. - if (dragging) + // Only stop this even if we're mouseDown already, since otherwise + // no text whatsoever can be selected on the page. + if (mouseDown) event.preventDefault(); } }; @@ -766,10 +793,12 @@ new function() { // Injection scope for mouse events on the browser // TODO: Remove again after Dec 2016 once it is fixed in Chrome. var offset = DomEvent.getOffset(event, view._element), x = offset.x, - abs = Math.abs(x), - max = 1 << 25; - offset.x = abs - max < abs ? (abs - max) * (x < 0 ? -1 : 1) : x; - handleMouseMove(view, view.viewToProject(offset), event); + abs = Math.abs, + ax = abs(x), + max = 1 << 25, + diff = ax - max; + offset.x = abs(diff) < ax ? diff * (x < 0 ? -1 : 1) : x; + handleMouseMove(view, event, view.viewToProject(offset)); } }, @@ -783,22 +812,14 @@ new function() { // Injection scope for mouse events on the browser viewEvents[mousedown] = function(event) { // Get the view from the event, and store a reference to the view that // should receive keyboard input. - var view = View._focused = getView(event), - point = viewToProject(view, event); - dragging = true; - // Always first call the view's mouse handlers, as required by - // CanvasView, and then handle the active tool, if any. - view._handleEvent('mousedown', point, event); - if (tool = view._scope.tool) - tool._handleEvent('mousedown', point, event); - // In the end we always call update(), which only updates the view if - // anything has changed in the above calls. - view.update(); + var view = View._focused = getView(event); + mouseDown = true; + handleEvent('mousedown', view, event); }; docEvents[mousemove] = function(event) { var view = View._focused; - if (!dragging) { + if (!mouseDown) { // See if we can get the view from the current event target, and // handle the mouse move over it. var target = getView(event); @@ -808,7 +829,7 @@ new function() { // Injection scope for mouse events on the browser // If we switch view, fire one last mousemove in the old view, // to give items the change to receive a mouseleave, etc. if (view !== target) - handleMouseMove(view, viewToProject(view, event), event); + handleMouseMove(view, event); prevFocus = view; view = View._focused = tempFocus = target; } else if (tempFocus && tempFocus === view) { @@ -817,23 +838,15 @@ new function() { // Injection scope for mouse events on the browser updateFocus(); } } - if (view) { - var point = viewToProject(view, event); - if (dragging || view.getBounds().contains(point)) - tool = handleMouseMove(view, point, event); - } + if (view) + handleMouseMove(view, event); }; docEvents[mouseup] = function(event) { var view = View._focused; - if (!view || !dragging) - return; - var point = viewToProject(view, event); - dragging = false; - view._handleEvent('mouseup', point, event); - if (tool) - tool._handleEvent('mouseup', point, event); - view.update(); + if (view && mouseDown) + handleEvent('mouseup', view, event); + mouseDown = false; }; DomEvent.add(document, docEvents); @@ -842,9 +855,84 @@ new function() { // Injection scope for mouse events on the browser load: updateFocus }); - // Flags defining which native events are required by which Paper events - // as required for counting amount of necessary natives events. - // The mapping is native -> virtual + /** + * Higher level event handling, hit-testing, and emitting of normal mouse + * events along with "virtual" events such as mouseenter, mouseleave, + * mousedrag, click, doubleclick, on both the hit-test item and the view, + * with support for bubbling (event-propagation). + */ + + var downPoint, + lastPoint, + downItem, + overItem, + dragItem, + clickItem, + clickTime, + dblClick, + overView, + // Event fallbacks for "virutal" events, e.g. if an item doesn't respond + // to doubleclick, fall back to click: + fallbacks = { + doubleclick: 'click', + mousedrag: 'mousemove' + }; + + // Returns true if event was stopped, false otherwise, whether handler was + // called or not! + function emitEvent(obj, type, event, point, prevPoint, stopItem) { + var target = obj, + mouseEvent; + + function emit(obj, type) { + if (obj.responds(type)) { + // Only produce the event object if we really need it, and then + // reuse it if we're bubbling. + if (!mouseEvent) { + mouseEvent = new MouseEvent( + type, event, point, target, mouseDown, + // Calculate delta if prevPoint was passed + prevPoint ? point.subtract(prevPoint) : null); + } + // Bail out if propagation is stopped + if (obj.emit(type, mouseEvent) && mouseEvent.stopped) + return true; + } else { + var fallback = fallbacks[type]; + if (fallback) + return emit(obj, fallback); + } + } + + // Bubble up the parents and emit this event until we're told to stop. + while (obj && obj !== stopItem) { + if (emit(obj, type)) + return true; + obj = obj._parent; + } + return false; + } + + function emitEvents(view, item, type, event, point, prevPoint) { + // First handle the drag-item and its parents, through bubbling. + return (dragItem && emitEvent(dragItem, type, event, point, + prevPoint) + // Next handle the hit-test item, if it's different from the drag + // item and not a descendant of it (in which case it would already + // have received an event in the call above). Use fallbacks to + // translate mousedrag to mousemove, since drag is handled above. + || item && item !== dragItem && !item.isDescendant(dragItem) + && emitEvent(item, fallbacks[type] || type, event, point, + prevPoint, dragItem) + // Lastly handle the move / drag on the view, if we're still here. + || emitEvent(view, type, event, point, prevPoint)); + } + + /** + * Flags defining which native events are required by which Paper events + * as required for counting amount of necessary natives events. + * The mapping is native -> virtual + */ var mouseFlags = { mousedown: { mousedown: 1, @@ -869,18 +957,87 @@ new function() { // Injection scope for mouse events on the browser return { _viewEvents: viewEvents, - // To be defined in subclasses - _handleEvent: function(/* type, point, event */) {}, + /** + * Returns true if event was stopped, false otherwise, whether handler + * was called or not! + */ + _handleEvent: function(type, event, point) { + // Run the hit-test first + var hit = this._project.hitTest(point, { + tolerance: 0, + fill: true, + stroke: true + }), + item = hit && hit.item, + inView = this.getBounds().contains(point), + stopped = false, + mouse = {}; + // Create a simple lookup object to quickly check for different + // mouse event types. + mouse[type.substr(5)] = true; + // Handle mousemove first, even if this is not actually a mousemove + // event but the mouse has moved since the last event, but do not + // allow it to stop the other events in that case. + var nativeMove = mouse.move || mouse.drag; + moveType = nativeMove && type + || lastPoint && !lastPoint.equals(point) && 'mousemove'; + if (moveType) { + // Handle mouseenter / leave between items, as well as views. + if (item !== overItem) { + if (overItem) + emitEvent(overItem, 'mouseleave', event, point); + if (item) + emitEvent(item, 'mouseenter', event, point); + } + overItem = item; + if (overView && !overView.getBounds().contains(point)) { + emitEvent(overView, 'mouseleave', event, point); + overView = null; + } + if (this !== overView && inView) { + emitEvent(this, 'mouseenter', event, point); + overView = this; + } + if (inView || mouse.drag) + stopped = emitEvents(this, item, moveType, event, point, + lastPoint); + } + if (!nativeMove) { + // Now handle mousedown / mouseup + stopped = emitEvents(this, item, type, event, point, downPoint); + if (mouse.down) { + // See if we're clicking again on the same item, within the + // double-click time. Firefox uses 300ms as the max time + // difference: + dblClick = clickItem === item + && (Date.now() - clickTime < 300); + downItem = clickItem = item; + downPoint = lastPoint = point; + // Only start dragging if the mousedown event has not + // stopped propagation. + dragItem = !stopped && item; + } else if (mouse.up) { + // Emulate click / doubleclick, but only on item, not view + if (!stopped && item && item === downItem) { + clickTime = Date.now(); + emitEvent(item, dblClick ? 'doubleclick' : 'click', + event, point, downPoint); + dblClick = false; + } + downItem = dragItem = null; + } + } + lastPoint = point; + return stopped; + }, _installEvent: function(type) { // If the view requires counting of installed mouse events, // increase the counters now according to mouseFlags var counters = this._eventCounters; - if (counters) { - for (var key in mouseFlags) { - counters[key] = (counters[key] || 0) - + (mouseFlags[key][type] || 0); - } + for (var key in mouseFlags) { + counters[key] = (counters[key] || 0) + + (mouseFlags[key][type] || 0); } }, @@ -888,10 +1045,8 @@ new function() { // Injection scope for mouse events on the browser // If the view requires counting of installed mouse events, // decrease the counters now according to mouseFlags var counters = this._eventCounters; - if (counters) { - for (var key in mouseFlags) - counters[key] -= mouseFlags[key][type] || 0; - } + for (var key in mouseFlags) + counters[key] -= mouseFlags[key][type] || 0; }, statics: {