diff --git a/lib/minimap-element.js b/lib/minimap-element.js index 2011244b..be75f72b 100644 --- a/lib/minimap-element.js +++ b/lib/minimap-element.js @@ -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)) } })) } @@ -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) @@ -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}) } } @@ -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() @@ -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()) } @@ -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 + } + } + // ######## #### ######## // ## ## ## ## ## ## // ## ## #### ## ## @@ -1010,58 +1055,65 @@ 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()) } @@ -1069,15 +1121,9 @@ export default class MinimapElement { /** * 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() } diff --git a/spec/helpers/events.js b/spec/helpers/events.js index 8eb91a34..4b0b5b0a 100644 --- a/spec/helpers/events.js +++ b/spec/helpers/events.js @@ -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, @@ -40,15 +39,10 @@ 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) { @@ -56,6 +50,10 @@ function objectCenterCoordinates (obj) { 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) => { @@ -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)) } }) diff --git a/spec/minimap-element-spec.js b/spec/minimap-element-spec.js index f7652834..bbeb47f0 100644 --- a/spec/minimap-element-spec.js +++ b/spec/minimap-element-spec.js @@ -5,7 +5,7 @@ import path from 'path' import Minimap from '../lib/minimap' import MinimapElement from '../lib/minimap-element' import {stylesheet} from './helpers/workspace' -import {mousemove, mousedown, mouseup, mousewheel, touchstart, touchmove} from './helpers/events' +import {mousemove, mousedown, mouseup, mousewheel, touchstart, touchmove, touchend} from './helpers/events' function realOffsetTop (o) { // transform = new WebKitCSSMatrix window.getComputedStyle(o).transform @@ -498,14 +498,14 @@ describe('MinimapElement', () => { beforeEach(() => { originalTop = visibleArea.getBoundingClientRect().top - mousemove(visibleArea, {x: originalLeft + 1, y: scrollTo + 40}) + mousemove(visibleArea, {x: originalLeft + 1, y: scrollTo + 40, btn: 1}) waitsFor(() => { return nextAnimationFrame !== noAnimationFrame }) runs(() => { nextAnimationFrame() }) }) afterEach(() => { - minimapElement.endDrag() + mouseup(visibleArea, {x: originalLeft + 1, y: scrollTo + 40, btn: 1}) }) it( 'scrolls the editor so that the visible area was moved down ' + @@ -569,23 +569,23 @@ describe('MinimapElement', () => { }) describe('dragging the visible area', () => { - let [visibleArea, originalTop] = [] + let [visibleArea, originalTop, originalLeft] = [] beforeEach(() => { visibleArea = minimapElement.visibleArea let o = visibleArea.getBoundingClientRect() - let left = o.left + originalLeft = o.left originalTop = o.top - mousedown(visibleArea, {x: left + 10, y: originalTop + 10}) - mousemove(visibleArea, {x: left + 10, y: originalTop + 50}) + mousedown(visibleArea, {x: originalLeft + 10, y: originalTop + 10}) + mousemove(visibleArea, {x: originalLeft + 10, y: originalTop + 50}) waitsFor(() => { return nextAnimationFrame !== noAnimationFrame }) runs(() => { nextAnimationFrame() }) }) afterEach(() => { - minimapElement.endDrag() + mouseup(visibleArea, {x: originalLeft + 10, y: originalTop + 50}) }) it('scrolls the editor so that the visible area was moved down by 40 pixels', () => { @@ -605,23 +605,23 @@ describe('MinimapElement', () => { }) describe('dragging the visible area using touch events', () => { - let [visibleArea, originalTop] = [] + let [visibleArea, originalTop, originalLeft] = [] beforeEach(() => { visibleArea = minimapElement.visibleArea let o = visibleArea.getBoundingClientRect() - let left = o.left + originalLeft = o.left originalTop = o.top - touchstart(visibleArea, {x: left + 10, y: originalTop + 10}) - touchmove(visibleArea, {x: left + 10, y: originalTop + 50}) + touchstart(visibleArea, {x: originalLeft + 10, y: originalTop + 10}) + touchmove(visibleArea, {x: originalLeft + 10, y: originalTop + 50}) waitsFor(() => { return nextAnimationFrame !== noAnimationFrame }) runs(() => { nextAnimationFrame() }) }) afterEach(() => { - minimapElement.endDrag() + touchend(visibleArea, {x: originalLeft + 10, y: originalTop + 50}) }) it('scrolls the editor so that the visible area was moved down by 40 pixels', () => { @@ -642,6 +642,7 @@ describe('MinimapElement', () => { describe('when the minimap cannot scroll', () => { let [visibleArea, originalTop] = [] + let [top, left] = [] beforeEach(() => { let sample = fs.readFileSync(dir.resolve('seventy.txt')).toString() @@ -656,7 +657,9 @@ describe('MinimapElement', () => { nextAnimationFrame() visibleArea = minimapElement.visibleArea - let {top, left} = visibleArea.getBoundingClientRect() + let o = visibleArea.getBoundingClientRect() + top = o.top + left = o.left originalTop = top mousedown(visibleArea, {x: left + 10, y: top + 10}) @@ -668,7 +671,7 @@ describe('MinimapElement', () => { }) afterEach(() => { - minimapElement.endDrag() + mousemove(visibleArea, {x: left + 10, y: top + 50}) }) it('scrolls based on a ratio adjusted to the minimap height', () => { @@ -686,23 +689,26 @@ describe('MinimapElement', () => { runs(() => { nextAnimationFrame() }) }) + + describe('dragging the visible area', () => { - let [visibleArea, originalTop] = [] + let [originalTop, originalLeft, visibleArea] = [] beforeEach(() => { visibleArea = minimapElement.visibleArea - let {top, left} = visibleArea.getBoundingClientRect() - originalTop = top + let o = visibleArea.getBoundingClientRect() + originalTop = o.top + originalLeft = o.left - mousedown(visibleArea, {x: left + 10, y: top + 10}) - mousemove(visibleArea, {x: left + 10, y: top + 50}) + mousedown(visibleArea, {x: originalLeft + 10, y: originalTop + 10}) + mousemove(visibleArea, {x: originalLeft + 10, y: originalTop + 50}) waitsFor(() => { return nextAnimationFrame !== noAnimationFrame }) runs(() => { nextAnimationFrame() }) }) afterEach(() => { - minimapElement.endDrag() + mouseup(visibleArea, {x: originalLeft + 10, y: originalTop + 50}) }) it('scrolls the editor so that the visible area was moved down by 40 pixels', () => {