diff --git a/src/MultiSlider/MultiSliderHandle.js b/src/MultiSlider/MultiSliderHandle.js index aefa11c..b53594e 100644 --- a/src/MultiSlider/MultiSliderHandle.js +++ b/src/MultiSlider/MultiSliderHandle.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { noop } from '../util/functions'; +import normalizeEvent from '../util/normalize-event'; import './MultiSliderHandle.css'; @@ -12,6 +13,7 @@ class MultiSliderHandle extends React.PureComponent { } startInteraction(e) { + e = normalizeEvent(e); this.props.onStart(e, this.props.property); } @@ -26,6 +28,7 @@ class MultiSliderHandle extends React.PureComponent {
); diff --git a/src/Pad/stories/index.stories.js b/src/Pad/stories/index.stories.js index 50c858b..585f011 100644 --- a/src/Pad/stories/index.stories.js +++ b/src/Pad/stories/index.stories.js @@ -2,7 +2,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import State from '../../Husk/Husk'; +import Husk from '../../Husk/Husk'; import Pad from '../Pad'; import PadGrid from '../PadGrid'; diff --git a/src/Position/Position.js b/src/Position/Position.js index d72c33f..c0c7561 100644 --- a/src/Position/Position.js +++ b/src/Position/Position.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { noop } from '../util/functions'; +import normalizeEvent, { isTouchEnabledDevice } from '../util/normalize-event'; /* Component: Position @@ -24,14 +25,23 @@ class Position extends React.PureComponent { componentDidMount() { document.addEventListener('mousemove', this.onChange); document.addEventListener('mouseup', this.onEnd); + if (isTouchEnabledDevice()) { + document.addEventListener('touchmove', this.onChange); + document.addEventListener('touchend', this.onEnd); + } } componentWillUnmount() { document.removeEventListener('mousemove', this.onChange); document.removeEventListener('mouseup', this.onEnd); + if (isTouchEnabledDevice()) { + document.removeEventListener('touchmove', this.onChange); + document.removeEventListener('touchend', this.onEnd); + } } onChange(e) { + e = normalizeEvent(e); this.props.onChange( { x: e.clientX, @@ -44,6 +54,7 @@ class Position extends React.PureComponent { } onEnd(e) { + e = normalizeEvent(e); this.props.onEnd( { x: e.clientX, diff --git a/src/Position/stories/index.stories.js b/src/Position/stories/index.stories.js index 5caacb5..1ca9703 100644 --- a/src/Position/stories/index.stories.js +++ b/src/Position/stories/index.stories.js @@ -32,16 +32,17 @@ storiesOf('Position', module) this.state = { interacting: false }; + this.startInteraction = this.startInteraction.bind(this); + } + + startInteraction(e) { + this.setState({ interacting: true }); + e.preventDefault(); } render() { return ( -
{ - this.setState({ interacting: true }); - e.preventDefault(); - }} - > +

Hold down the mouse here and move it to read its coordinates:{' '} {this.state.x}: {this.state.y} diff --git a/src/Surface/Surface.js b/src/Surface/Surface.js index b4f05a3..c91725a 100644 --- a/src/Surface/Surface.js +++ b/src/Surface/Surface.js @@ -5,6 +5,7 @@ import { scaleLinear } from 'd3-scale'; import Position from '../Position/Position'; import { noop } from '../util/functions'; +import normalizeEvent, { isTouchEnabledDevice } from '../util/normalize-event'; import './Surface.css'; @@ -22,7 +23,6 @@ const initial_state = { for the X and Y coordinates in percentages (interval [0..100]). */ - class Surface extends React.Component { constructor(props) { super(props); @@ -42,6 +42,7 @@ class Surface extends React.Component { } start(e) { + e = normalizeEvent(e); this.setState({ interacting: true }); @@ -54,6 +55,7 @@ class Surface extends React.Component { } end(e) { + e = normalizeEvent(e); this.setState({ interacting: false }); @@ -75,6 +77,7 @@ class Surface extends React.Component { } insert(e) { + e = normalizeEvent(e); this.props.onInsert( this.scale({ x: e.clientX, @@ -92,6 +95,8 @@ class Surface extends React.Component { let events = {}; if (passive) { events['onDoubleClick'] = this.insert; + } else if (isTouchEnabledDevice()) { + events['onTouchMove'] = this.start; } else { events['onMouseDownCapture'] = this.start; } diff --git a/src/util/normalize-event.js b/src/util/normalize-event.js new file mode 100644 index 0000000..46486b5 --- /dev/null +++ b/src/util/normalize-event.js @@ -0,0 +1,85 @@ +const proxiedValues = ['clientX', 'clientY', 'pageX', 'pageY', 'screenX', 'screenY']; + +const proxiedFunctions = ['preventDefault', 'stopPropagation', 'stopImmediatePropagation']; + +var EventProxy = { + apply: function(obj, thisArg, argumentList) { + return obj.apply(obj, argumentList); + }, + + get: function(obj, prop) { + if (obj.changedTouches && obj.changedTouches[0] && proxiedValues.indexOf(prop) > -1) { + return obj.changedTouches[0][prop]; + } + + if ( + obj.detail && + obj.detail.changedTouches && + obj.detail.changedTouches[0] && + proxiedValues.indexOf(prop) > -1 + ) { + return obj.detail.changedTouches[0][prop]; + } + + if (prop == 'original') { + if (obj.detail && obj.detail.original) { + return obj.detail.original; + } + return obj; + } + + if (obj.type === 'touchend' && prop === 'target') { + let clientX, clientY; + if (obj.changedTouches && obj.changedTouches[0]) { + clientX = obj.changedTouches[0].clientX; + clientY = obj.changedTouches[0].clientY; + } else if (obj.detail && obj.detail.changedTouches && obj.detail.changedTouches[0]) { + clientX = obj.detail.changedTouches[0].clientX; + clientY = obj.detail.changedTouches[0].clientY; + } + return document.elementFromPoint(clientX, clientY); + } + + if (proxiedFunctions.indexOf(prop) > -1) { + if (obj.detail && obj.detail.original && obj.detail.original[prop]) { + return obj.detail.original[prop].bind(obj); + } + return obj[prop].bind(obj); + } + + return obj[prop]; + } +}; + +let __isTouchEnabledDevice; +function isTouchEnabledDevice() { + return __isTouchEnabledDevice !== undefined + ? __isTouchEnabledDevice + : (__isTouchEnabledDevice = (function() { + // we rely on Proxy for events that come from touch-enabled devices, + // so if we don't have it we need to assume this is not such a device. + if (!window.Proxy) { + return false; + } + try { + new CustomEvent('longtap'); + } catch (e) { + return false; + } + /* + As per https://developer.mozilla.org/en-US/docs/Web/API/Navigator/maxTouchPoints + a device should expose how many touch points are supported; unfortunately + mobile Safari does not support it, so we have to also taste for ontouchstart for iPads. + */ + return ( + window.ontouchstart !== undefined || + navigator.maxTouchPoints || + navigator.msMaxTouchPoints + ); + })()); +} + +const normalizeEvent = e => (isTouchEnabledDevice() ? new Proxy(e, EventProxy) : e); + +export default normalizeEvent; +export { isTouchEnabledDevice };