Skip to content

Commit

Permalink
Fixes #383 Fix touch scroll support
Browse files Browse the repository at this point in the history
TouchEvents and MouseEvents have a different interface, which leads to
incorrect processing of touch scrolling on the minimap.

Mapping both event types to a unified interface resolves this issue.
  • Loading branch information
TimoStaudinger committed Dec 19, 2015
1 parent d157b36 commit cfb509e
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 83 deletions.
126 changes: 86 additions & 40 deletions lib/minimap-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ export default class MinimapElement {
}))

this.subscriptions.add(this.subscribeTo(this.getFrontCanvas(), {
'mousedown': (e) => { this.mousePressedOverCanvas(e) }
'mousedown': (e) => { this.canvasPressed(this.extractMouseEventData(e)) },
'touchstart': (e) => { this.canvasPressed(this.extractTouchEventData(e)) }
}))
}

Expand All @@ -413,8 +414,8 @@ export default class MinimapElement {
this.visibleArea.classList.add('minimap-visible-area')
this.shadowRoot.appendChild(this.visibleArea)
this.visibleAreaSubscription = this.subscribeTo(this.visibleArea, {
'mousedown': (e) => { this.startDrag(e) },
'touchstart': (e) => { this.startDrag(e) }
'mousedown': (e) => { this.startDrag(this.extractMouseEventData(e)) },
'touchstart': (e) => { this.startDrag(this.extractTouchEventData(e)) }
})

this.subscriptions.add(this.visibleAreaSubscription)
Expand Down Expand Up @@ -928,17 +929,19 @@ export default class MinimapElement {
/**
* Callback triggered when the mouse is pressed on the MinimapElement canvas.
*
* @param {MouseEvent} e the mouse event object
* @param {number} y the vertical coordinate of the event
* @param {boolean} isLeftMouse was the left mouse button pressed?
* @param {boolean} isMiddleMouse was the middle mouse button pressed?
* @access private
*/
mousePressedOverCanvas (e) {
canvasPressed ({y, isLeftMouse, isMiddleMouse}) {
if (this.minimap.isStandAlone()) { return }
if (e.which === 1) {
this.leftMousePressedOverCanvas(e)
} else if (e.which === 2) {
this.middleMousePressedOverCanvas(e)
if (isLeftMouse) {
this.canvasLeftMousePressed(y)
} else if(isMiddleMouse){
this.canvasMiddleMousePressed(y)
let {top, height} = this.visibleArea.getBoundingClientRect()
this.startDrag({which: 2, pageY: top + height / 2}) // ugly hack
this.startDrag({y: top + height / 2, isLeftMouse: false, isMiddleMouse: true})
}
}

Expand All @@ -951,9 +954,9 @@ export default class MinimapElement {
* @param {HTMLElement} e.target the source of the event
* @access private
*/
leftMousePressedOverCanvas ({pageY, target}) {
let y = pageY - target.getBoundingClientRect().top
let row = Math.floor(y / this.minimap.getLineHeight()) + this.minimap.getFirstVisibleScreenRow()
canvasLeftMousePressed (y) {
let deltaY = y - this.getBoundingClientRect().top
let row = Math.floor(deltaY / this.minimap.getLineHeight()) + this.minimap.getFirstVisibleScreenRow()

let textEditor = this.minimap.getTextEditor()

Expand All @@ -978,11 +981,11 @@ export default class MinimapElement {
* @param {number} e.pageY the mouse y position in page
* @access private
*/
middleMousePressedOverCanvas ({pageY}) {
canvasMiddleMousePressed (y) {
let {top: offsetTop} = this.getBoundingClientRect()
let y = pageY - offsetTop - this.minimap.getTextEditorScaledHeight() / 2
let deltaY = y - offsetTop - this.minimap.getTextEditorScaledHeight() / 2

let ratio = y / (this.minimap.getVisibleHeight() - this.minimap.getTextEditorScaledHeight())
let ratio = deltaY / (this.minimap.getVisibleHeight() - this.minimap.getTextEditorScaledHeight())

this.minimap.setTextEditorScrollTop(ratio * this.minimap.getTextEditorMaxScrollTop())
}
Expand All @@ -998,6 +1001,48 @@ export default class MinimapElement {
this.getTextEditorElement().component.onMouseWheel(e)
}

/**
* A method that extracts data from a `MouseEvent` which can then be used to
* process clicks and drags of the minimap.
*
* Used together with `extractTouchEventData` to provide a unified interface
* for `MouseEvent`s and `TouchEvent`s.
*
* @param {MouseEvent} mouseEvent the mouse event object
* @access private
*/
extractMouseEventData (mouseEvent) {
return {
x: mouseEvent.pageX,
y: mouseEvent.pageY,
isLeftMouse: mouseEvent.which === 1,
isMiddleMouse: mouseEvent.which === 2
}
}

/**
* A method that extracts data from a `TouchEvent` which can then be used to
* process clicks and drags of the minimap.
*
* Used together with `extractMouseEventData` to provide a unified interface
* for `MouseEvent`s and `TouchEvent`s.
*
* @param {TouchEvent} touchEvent the touch event object
* @access private
*/
extractTouchEventData (touchEvent) {
// Use the first touch on the target area. Other touches will be ignored in
// case of multi-touch.
let touch = touchEvent.changedTouches[0]

return {
x: touch.pageX,
y: touch.pageY,
isLeftMouse: true, // Touch is treated like a left mouse button click
isMiddleMouse: false
}
}

// ######## #### ########
// ## ## ## ## ## ##
// ## ## #### ## ##
Expand All @@ -1010,74 +1055,75 @@ export default class MinimapElement {
* A method triggered when the mouse is pressed over the visible area that
* starts the dragging gesture.
*
* @param {MouseEvent} e the mouse event object
* @param {number} y the vertical coordinate of the event
* @param {boolean} isLeftMouse was the left mouse button pressed?
* @param {boolean} isMiddleMouse was the middle mouse button pressed?
* @access private
*/
startDrag (e) {
let {which, pageY} = e
startDrag ({y, isLeftMouse, isMiddleMouse}) {
if (!this.minimap) { return }
if (which !== 1 && which !== 2 && !(e.touches != null)) { return }
if (!isLeftMouse && !isMiddleMouse) { return }

let {top} = this.visibleArea.getBoundingClientRect()
let {top: offsetTop} = this.getBoundingClientRect()

let dragOffset = pageY - top
let dragOffset = y - top

let initial = {dragOffset, offsetTop}

let mousemoveHandler = (e) => this.drag(e, initial)
let mouseupHandler = (e) => this.endDrag(e, initial)
let mousemoveHandler = (e) => this.drag(this.extractMouseEventData(e), initial)
let mouseupHandler = (e) => this.endDrag()

let touchmoveHandler = (e) => this.drag(this.extractTouchEventData(e), initial)
let touchendHandler = (e) => this.endDrag()

document.body.addEventListener('mousemove', mousemoveHandler)
document.body.addEventListener('mouseup', mouseupHandler)
document.body.addEventListener('mouseleave', mouseupHandler)

document.body.addEventListener('touchmove', mousemoveHandler)
document.body.addEventListener('touchend', mouseupHandler)
document.body.addEventListener('touchmove', touchmoveHandler)
document.body.addEventListener('touchend', touchendHandler)
document.body.addEventListener('touchcancel', touchendHandler)

this.dragSubscription = new Disposable(function () {
document.body.removeEventListener('mousemove', mousemoveHandler)
document.body.removeEventListener('mouseup', mouseupHandler)
document.body.removeEventListener('mouseleave', mouseupHandler)

document.body.removeEventListener('touchmove', mousemoveHandler)
document.body.removeEventListener('touchend', mouseupHandler)
document.body.removeEventListener('touchmove', touchmoveHandler)
document.body.removeEventListener('touchend', touchendHandler)
document.body.removeEventListener('touchcancel', touchendHandler)
})
}

/**
* The method called during the drag gesture.
*
* @param {MouseEvent} e the mouse event object
* @param {Object} initial
* @param {number} y the vertical coordinate of the event
* @param {boolean} isLeftMouse was the left mouse button pressed?
* @param {boolean} isMiddleMouse was the middle mouse button pressed?
* @param {number} initial.dragOffset the mouse offset within the visible
* area
* @param {number} initial.offsetTop the MinimapElement offset at the moment
* of the drag start
* @access private
*/
drag (e, initial) {
drag ({y, isLeftMouse, isMiddleMouse}, initial) {
if (!this.minimap) { return }
if (e.which !== 1 && e.which !== 2 && !(e.touches != null)) { return }
let y = e.pageY - initial.offsetTop - initial.dragOffset
if (!isLeftMouse && !isMiddleMouse) { return }
let deltaY = y - initial.offsetTop - initial.dragOffset

let ratio = y / (this.minimap.getVisibleHeight() - this.minimap.getTextEditorScaledHeight())
let ratio = deltaY / (this.minimap.getVisibleHeight() - this.minimap.getTextEditorScaledHeight())

this.minimap.setTextEditorScrollTop(ratio * this.minimap.getTextEditorMaxScrollTop())
}

/**
* The method that ends the drag gesture.
*
* @param {MouseEvent} e the mouse event object
* @param {Object} initial
* @param {number} initial.dragOffset the mouse offset within the visible
* area
* @param {number} initial.offsetTop the MinimapElement offset at the moment
* of the drag start
* @access private
*/
endDrag (e, initial) {
endDrag () {
if (!this.minimap) { return }
this.dragSubscription.dispose()
}
Expand Down
50 changes: 28 additions & 22 deletions spec/helpers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ function mouseEvent (type, properties) {
}

function touchEvent (type, touches) {
let firstTouch = touches[0]

let properties = {
let event = new UIEvent(type, {
bubbles: true,
cancelable: true,
view: window,
Expand All @@ -40,22 +39,21 @@ function touchEvent (type, touches) {
shiftKey: false,
metaKey: false,
relatedTarget: undefined
}
})
event.touches = event.changedTouches = event.targetTouches = touches

let e = new Event(type, properties)
e.pageX = firstTouch.pageX
e.pageY = firstTouch.pageY
e.clientX = firstTouch.clientX
e.clientY = firstTouch.clientY
e.touches = e.targetTouches = e.changedTouches = touches
return e
return event
}

function objectCenterCoordinates (obj) {
let {top, left, width, height} = obj.getBoundingClientRect()
return {x: left + width / 2, y: top + height / 2}
}

function exists (value) {
return (typeof value !== 'undefined' && value !== null)
}

module.exports = {objectCenterCoordinates, mouseEvent}

;['mousedown', 'mousemove', 'mouseup', 'click'].forEach((key) => {
Expand All @@ -82,20 +80,28 @@ module.exports.mousewheel = function (obj, deltaX = 0, deltaY = 0) {
}

;['touchstart', 'touchmove', 'touchend'].forEach((key) => {
module.exports[key] = function (obj, {x, y, cx, cy} = {}) {
if (!((typeof x !== 'undefined' && x !== null) && (typeof y !== 'undefined' && y !== null))) {
let o = objectCenterCoordinates(obj)
x = o.x
y = o.y
module.exports[key] = function (obj, touches) {
if(!Array.isArray(touches)) {
touches = [touches]
}

if (!((typeof cx !== 'undefined' && cx !== null) && (typeof cy !== 'undefined' && cy !== null))) {
cx = x
cy = y
}
touches.forEach((touch) => {
if(!exists(touch.target)) {
touch.target = obj;
}

if (!(exists(touch.pageX) && exists(touch.pageY))) {
let o = objectCenterCoordinates(obj)
touch.pageX = exists(touch.x) ? touch.x : o.x;
touch.pageY = exists(touch.y) ? touch.y : o.y;
}

if (!(exists(touch.clientX) && exists(touch.clientY))) {
touch.clientX = touch.pageX
touch.clientY = touch.pageY
}
})

obj.dispatchEvent(touchEvent(key, [
{pageX: x, pageY: y, clientX: cx, clientY: cy}
]))
obj.dispatchEvent(touchEvent(key, touches))
}
})
Loading

0 comments on commit cfb509e

Please sign in to comment.