diff --git a/.babelrc b/.babelrc index ad94796..a9ce136 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["react-native-stage-0", "react-native"] + "presets": ["react-native"] } diff --git a/README.md b/README.md index 5d21c45..5fb56bc 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ ```sh $ npm install react-native-view-editable --save ``` +or +```sh +$ yarn add react-native-view-editable +``` ### Usage ```javascript diff --git a/__tests__/ViewEditor.js b/__tests__/ViewEditor.js index b6cbeb9..333533f 100644 --- a/__tests__/ViewEditor.js +++ b/__tests__/ViewEditor.js @@ -13,6 +13,37 @@ function getValueForKey(arr, key) { describe('ViewEditor Component', () => { let component = null; const onMoveCallback = jest.fn(); + const onMoveEndCallback = jest.fn(); + const fakeEvent = { + nativeEvent: { + changedTouches: 0, + identifier: 0, + locationX: 0, + locationY: 0, + pageX: 0, + pageY: 0, + target: 0, + timestamp: 0, + touches: { + pageX: 1, + pageY: 1, + locationX: 1, + locationY: 1 + } + } + }; + const fakeGestureState = { + numberActiveTouches: 1, + stateID: 1, + moveX: 1, + moveY: 1, + x0: 1, + y0: 1, + dx: 1, + dy: 1, + vx: 1, + vy: 1 + }; beforeEach(() => { component = renderer.create( @@ -20,6 +51,7 @@ describe('ViewEditor Component', () => { minScale={1} maxScale={20} onMove={onMoveCallback} + onMoveEnd={onMoveEndCallback} > Hello World @@ -32,75 +64,62 @@ describe('ViewEditor Component', () => { const componentJSON = component.toJSON(); expect(componentJSON).toMatchSnapshot(); }); - it('should contain transform prop', () => { - // $FlowFixMe - const componentJSON = component.toJSON(); - expect(componentJSON.props.style.transform).toBeInstanceOf(Array); - }); - it('should add translateX and translateY props correctly', () => { - // $FlowFixMe - const componentJSON = component.toJSON(); - const translateX = getValueForKey(componentJSON.props.style.transform, 'translateX'); - const translateY = getValueForKey(componentJSON.props.style.transform, 'translateY'); + describe('transformation', () => { + it('should contain transform prop', () => { + // $FlowFixMe + const componentJSON = component.toJSON(); + expect(componentJSON.props.style.transform).toBeInstanceOf(Array); + }); + it('should add translateX and translateY props correctly', () => { + // $FlowFixMe + const componentJSON = component.toJSON(); + const translateX = getValueForKey(componentJSON.props.style.transform, 'translateX'); + const translateY = getValueForKey(componentJSON.props.style.transform, 'translateY'); - expect(translateX).toBeDefined(); - expect(translateY).toBeDefined(); + expect(translateX).toBeDefined(); + expect(translateY).toBeDefined(); - expect(translateX).toBe(0); - expect(translateY).toBe(0); - }); - it('should add scale and rotate angle correctly', () => { - // $FlowFixMe - const componentJSON = component.toJSON(); - const rotate = getValueForKey(componentJSON.props.style.transform, 'rotate'); - const scale = getValueForKey(componentJSON.props.style.transform, 'scale'); + expect(translateX).toBe(0); + expect(translateY).toBe(0); + }); + it('should add scale and rotate angle correctly', () => { + // $FlowFixMe + const componentJSON = component.toJSON(); + const rotate = getValueForKey(componentJSON.props.style.transform, 'rotate'); + const scale = getValueForKey(componentJSON.props.style.transform, 'scale'); - expect(rotate).toBeDefined(); - expect(scale).toBeDefined(); + expect(rotate).toBeDefined(); + expect(scale).toBeDefined(); - // expect(rotate).toBe('0deg'); - expect(scale).toBe(1); + // expect(rotate).toBe('0deg'); + expect(scale).toBe(1); + }); }); - it('should call _getTransforms function and return transform values', () => { - // $FlowFixMe - const instance = component.getInstance(); - expect(instance._getTransforms()).toBeInstanceOf(Object); - expect(instance._getTransforms().transform.length).toBe(4); + describe('_getTransforms method', () => { + it('should call _getTransforms function and return Object', () => { + // $FlowFixMe + const instance = component.getInstance(); + expect(instance._getTransforms()).toBeInstanceOf(Object); + }); + it('should return all transformation objects correctly', () => { + const instance = component.getInstance(); + expect(instance._getTransforms().transform.length).toBe(4); + }); }); - it('should call onMove prop on panning', () => { + describe('onMoveEnd prop', () => { // $FlowFixMe - const instance = component.getInstance(); - const fakeEvent = { - nativeEvent: { - changedTouches: 0, - identifier: 0, - locationX: 0, - locationY: 0, - pageX: 0, - pageY: 0, - target: 0, - timestamp: 0, - touches: { - pageX: 1, - pageY: 1, - locationX: 1, - locationY: 1 - } - } - }; - const fakeGestureState = { - numberActiveTouches: 1, - stateID: 1, - moveX: 1, - moveY: 1, - x0: 1, - y0: 1, - dx: 1, - dy: 1, - vx: 1, - vy: 1 - }; - instance._handlePanResponderMove(fakeEvent, fakeGestureState); - expect(onMoveCallback).toHaveBeenCalled(); + it('should be called on touches release', () => { + const instance = component.getInstance(); + instance._handlePanResponderEnd(fakeEvent, fakeGestureState); + expect(onMoveEndCallback).toHaveBeenCalled(); + }) + }); + describe('onMove prop', () => { + it('should call onMove prop on panning', () => { + // $FlowFixMe + const instance = component.getInstance(); + instance._handlePanResponderMove(fakeEvent, fakeGestureState); + expect(onMoveCallback).toHaveBeenCalled(); + }); }); }); diff --git a/__tests__/__snapshots__/common.js.snap b/__tests__/__snapshots__/common.js.snap index 795e2ac..597c5f6 100644 --- a/__tests__/__snapshots__/common.js.snap +++ b/__tests__/__snapshots__/common.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Common utils should generate array for given range 1`] = ` +exports[`Common utils generateArray should generate array for given range 1`] = ` Array [ 0, 1, diff --git a/__tests__/common.js b/__tests__/common.js index 3630347..f37fb21 100644 --- a/__tests__/common.js +++ b/__tests__/common.js @@ -2,9 +2,11 @@ import React from 'react'; import { generateArray } from '../lib/utils'; describe('Common utils', () => { - it('should generate array for given range', () => { - const arr = generateArray(360); - expect(arr.length).toBe(360); - expect(arr).toMatchSnapshot(); + describe('generateArray', () => { + it('should generate array for given range', () => { + const arr = generateArray(360); + expect(arr.length).toBe(360); + expect(arr).toMatchSnapshot(); + }); }); }); diff --git a/__tests__/geometry.js b/__tests__/geometry.js index a9dab7d..dd724cd 100644 --- a/__tests__/geometry.js +++ b/__tests__/geometry.js @@ -15,40 +15,53 @@ describe('Geometry calculations', () => { { pageX: 10, pageY: 5, locationX: 0, locationY: 0 }, { pageX: 4, pageY: 5, locationX: 0, locationY: 0 } ]; - - it('should pow absolute values of two numbers', () => { - const a = -2; - const b = 5; - expect(pow2abs(2, 5)).toBe(9); + describe('pow2abs', () => { + it('should pow absolute values of two numbers', () => { + const a = -2; + const b = 5; + expect(pow2abs(2, 5)).toBe(9); + }); }); - it('Return center of two coordinates', () => { - const result = { - x: 7, - y: 5 - }; - expect(centerTouches(touches)).toEqual(result) + describe('centerTouches', () => { + it('Return center of two coordinates', () => { + const result = { + x: 7, + y: 5 + }; + expect(centerTouches(touches)).toEqual(result) + }); }); - it('should convert number to degree string', () => { - const number = 10; - const result = `${number}deg`; - expect(numberToDegree(number)).toBe(result); + describe('numberToDegree', () => { + it('should convert number to degree string', () => { + const number = 10; + const result = `${number}deg`; + expect(numberToDegree(number)).toBe(result); + }); }); - it('should convert degree string to number', () => { - const degree = '10deg'; - const result = 10; - expect(degreeToNumber(degree)).toBe(result); + describe('degreeToNumber', () => { + it('should convert degree string to number', () => { + const degree = '10deg'; + const result = 10; + expect(degreeToNumber(degree)).toBe(result); + }); }); - it('should return distance of two points(touches)', () => { - const result = 6; - expect(distanceBetweenTouches(touches)).toBe(result); + describe('distanceBetweenTouches', () => { + it('should return distance of two points(touches)', () => { + const result = 6; + expect(distanceBetweenTouches(touches)).toBe(result); + }); }); - it('should convert number to degree', () => { - const angle = 90; - const result = angle * 180 / Math.PI - expect(toDeg(angle)).toBe(result); + describe('toDeg', () => { + it('should convert number to degree', () => { + const angle = 90; + const result = angle * 180 / Math.PI + expect(toDeg(angle)).toBe(result); + }); }); - it('should return positive angle of two points', () => { - const result = 180; - expect(angle(touches)).toBe(result); + describe('angle', () => { + it('should return angle of two points', () => { + const result = 180; + expect(angle(touches)).toBe(result); + }); }); }); diff --git a/lib/ViewEditor/ViewEditor.js b/lib/ViewEditor/ViewEditor.js index 87f91dd..899e5b2 100644 --- a/lib/ViewEditor/ViewEditor.js +++ b/lib/ViewEditor/ViewEditor.js @@ -28,9 +28,14 @@ type Container = { type Props = { children: React.Element<*>, panning: boolean, - minScale: number, - maxScale: number, - onMove: ?(e: Event, g: GestureState) => any + scaleBounds: { + min: number, + max: number, + }, + allowScale: boolean, + allowRotate: boolean, + onMove: ?(e: Event, g: GestureState) => void, + onMoveEnd: ?(e: Event, g: GestureState) => void }; type State = { pan: AnimatedValueXY, @@ -69,14 +74,25 @@ class ViewEditor extends PureComponent { static propTypes = { children: PropTypes.element.isRequired, panning: PropTypes.bool, - minScale: PropTypes.number, onMove: PropTypes.func, - maxScale: PropTypes.number, + onMoveEnd: PropTypes.func, + allowScale: PropTypes.bool, + allowRotate: PropTypes.bool, + scaleBounds: PropTypes.shape({ + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired + }), }; static defaultProps = { panning: true, minScale: 1, maxScale: 10, + allowRotate: true, + allowScale: true, + scaleBounds: { + min: 1, + max: 10 + }, }; // Types @@ -161,22 +177,22 @@ class ViewEditor extends PureComponent { : this.savedValuesBeforeMove.rotate; } - reset() { + _reset = (timing: number, cb?: () => void): void => { Animated.parallel([ Animated.timing(this.state.pan, { toValue: { x: 0, y: 0 }, easing: Easing.linear, - duration: 200 + duration: timing }), Animated.timing(this.state.scale, { toValue: 1, easing: Easing.linear, - duration: 200 + duration: timing }), Animated.timing(this.state.rotate, { toValue: 0, easing: Easing.linear, - duration: 200 + duration: timing }), ]).start(() => { this.transformValues = { @@ -194,9 +210,16 @@ class ViewEditor extends PureComponent { center: null, distance: 0 }; + if (typeof cb === 'function') { + cb(); + } }); } + reset(timing?: number = 200) { + this._reset(timing / 2, () => this._reset(timing / 2)); + } + _onLayout(e: LayoutEvent): void { this._container = { width: e.nativeEvent.layout.width, @@ -213,19 +236,27 @@ class ViewEditor extends PureComponent { // TODO: highlight view } - _updateTransformValues = (transformValues: TransformValues): void => { - const { x, y } = transformValues; - this.state.pan.setOffset({ x, y }); - this.state.pan.setValue({ x: 0, y: 0 }); - this.state.scale.setOffset(transformValues.scale); - this.state.scale.setValue(0); + _updateTransformValues = (): void => { + this.state.pan.flattenOffset(); + // this.state.pan.setValue({ x: 0, y: 0 }); + this.state.scale.flattenOffset(); + this.state.scale.flattenOffset(); + } + + _isScaleCompatable = (scale: number): boolean => { + const { scaleBounds } = this.props; + return (scale >= scaleBounds.min && scale <= scaleBounds.max); + // return (scale < scaleBounds.min || scale > scaleBounds.max); } _handlePanResponderMove = (event: Event, gestureState: GestureState): void => { if (typeof this.props.onMove === 'function') { this.props.onMove(event, gestureState); } - const { minScale, maxScale } = this.props; + const { + allowScale, + allowRotate, + } = this.props; const { numberActiveTouches } = gestureState; if (numberActiveTouches > 1) { this._multiTouch = true; @@ -243,32 +274,35 @@ class ViewEditor extends PureComponent { rotate: prevAngle, distance: prevDistance } = this.savedValuesBeforeMove; - const angleToRotate = angle(event.nativeEvent.touches) - prevAngle; - - const slowDownRotation = 1; // 10x - // Set rotation angle - this.state.rotate.setValue( - (parseFloat(angleToRotate) - prevAngle) / slowDownRotation - ); - // Zoom calculation - const currentDistance = distanceBetweenTouches(event.nativeEvent.touches); - const newScale = ((currentDistance - prevDistance)) / 10; + if (allowRotate) { + const angleToRotate = angle(event.nativeEvent.touches) - prevAngle; - const scaleCalc = this.transformValues.scale + newScale; - if (scaleCalc < minScale || scaleCalc > maxScale) { - return; - } - if (newScale === 0) { - return; + const slowDownRotation = 1; + const destAngle = (parseFloat(angleToRotate) - prevAngle) / slowDownRotation; + // Set rotation angle + this.state.rotate.setValue(destAngle); } - this.state.scale.setValue(newScale); + if (allowScale) { + // Zoom calculation + const currentDistance = distanceBetweenTouches(event.nativeEvent.touches); + const newScale = ((currentDistance - prevDistance)) / 10; + + const scaleCalc = this.transformValues.scale + newScale; + if (!this._isScaleCompatable(scaleCalc)) { + return; + } + this.state.scale.setValue(scaleCalc); + } } - _handlePanResponderEnd = (): void => { - this._updateTransformValues(this.transformValues); + _handlePanResponderEnd = (event: Event, gestureState: GestureState): void => { + this._updateTransformValues(); + if (typeof this.props.onMoveEnd === 'function') { + this.props.onMoveEnd(event, gestureState); + } if (!this._multiTouch) { return; diff --git a/package.json b/package.json index 04ef785..0ac35b7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "commit": "git-cz", "checkall": "npm run test && npm run flow && npm run lint", "flow": "flow", - "test": "jest --verbose --coverage", + "test": "jest --verbose", + "coverage": "npm test -- --coverage", "lint": "eslint ./lib/**/*.js", "semantic-release": "semantic-release pre && npm publish && semantic-release post" },