├── .DS_Store ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── demo.gif ├── example ├── App.tsx ├── components │ ├── Editor │ │ ├── Editor.tsx │ │ └── types.d.ts │ ├── IconButton │ │ ├── IconButton.tsx │ │ ├── SvgIcon │ │ │ ├── SvgIcon.tsx │ │ │ └── types.d.ts │ │ ├── index.ts │ │ └── types.d.ts │ ├── Slider │ │ ├── Slider.tsx │ │ └── types.d.ts │ └── index.tsx ├── constants.ts ├── context │ ├── ExampleContextProvider.ts │ └── types.d.ts ├── hooks │ └── useExampleContext.ts └── types.d.ts ├── package.json ├── src ├── components │ ├── cornerComponent │ │ ├── CornerComponent.tsx │ │ ├── styles.tsx │ │ └── types.d.ts │ ├── customTextInput │ │ ├── CustomTextInput.tsx │ │ └── styles.tsx │ ├── dragTextEditor │ │ ├── DragTextEditor.tsx │ │ ├── index.ts │ │ └── types.d.ts │ ├── resizerSnapPoint │ │ ├── ResizerSnapPoint.tsx │ │ ├── styles.tsx │ │ └── types.d.ts │ └── rotationSnapPoint │ │ ├── RotationSnapPoint.tsx │ │ ├── styles.tsx │ │ └── types.d.ts ├── constants.ts ├── context │ ├── external.ts │ ├── internal.ts │ └── types.d.ts ├── hooks │ └── index.ts ├── index.ts └── utils │ └── calculate.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneskarpuz/react-native-drag-text-editor/0d007b0b6fc2e996c2bb9a35c3c1d7c979a8ea12/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@react-native-community', 'prettier'], 4 | rules: { 5 | 'no-console': ['error', { allow: ['warn', 'error'] }], 6 | 'prettier/prettier': 'error', 7 | }, 8 | parserOptions: { 9 | requireConfigFile: false, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: Bug 5 | labels: bug, enhancement 6 | assignees: eneskarpuz 7 | 8 | --- 9 | 10 | # Bug 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. 13 | 14 | ## Device info 15 | - Device: [e.g. iPhone6] 16 | - Version [e.g. 22] 17 | 18 | ## Environment info 19 | 20 | 23 | 24 | | Library | Version | 25 | | ------------------------------- | ------- | 26 | | react-native-drag-text-editor | x.x.x | 27 | | react-native | x.x.x | 28 | 29 | 30 | ## Steps To Reproduce 31 | Steps to reproduce the behavior: 32 | 1. Go to '...' 33 | 2. Click on '....' 34 | 3. Scroll down to '....' 35 | 4. See error 36 | 37 | Describe what you expected to happen: 38 | 39 | 1. I would expect '...' 40 | 41 | ## Reproducible sample code 42 | 43 | 46 | 47 | ## Additional context 48 | Add any other context about the problem here. 49 | 50 | 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log 4 | 5 | # Runtime data 6 | tmp 7 | build 8 | dist 9 | .DS_Store 10 | 11 | # Dependency directory 12 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log 4 | 5 | # Dependency directory 6 | node_modules 7 | .DS_Store 8 | 9 | # Runtime data 10 | tmp -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "arrowParens": "avoid", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "useTabs": false 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 eneskarpuz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Drag Text Editor 2 | 3 | [![reanimated](https://img.shields.io/github/package-json/dependency-version/eneskarpuz/react-native-drag-text-editor/dev/react-native-reanimated?label=Reanimated%20v2&style=flat-square)](https://www.npmjs.com/package/react-native-drag-text-editor) [![npm](https://img.shields.io/npm/l/react-native-drag-text-editor?style=flat-square)](https://www.npmjs.com/package/react-native-drag-text-editor) [![npm](https://img.shields.io/badge/types-included-blue?style=flat-square)](https://www.npmjs.com/package/react-native-drag-text-editor) [![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-4630EB.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/) 4 | 5 | 6 | 7 | 📝 60FPS Draggable, Rotatable, Resizeable Text Input Component written in Typescript and Reanimated 2 8 | 9 | ![React Native Drag Text Editor](./demo.gif) 10 | 11 | ## Features 12 | 13 | - Powered with Reanimated v2. 14 | - Compatible with Expo. 15 | - Written in TypeScript. 16 | 17 | ## Getting Started 18 | 19 | Check out [the documentation website](https://eneskarpuz.github.io/react-native-drag-text-editor/). 20 | 21 | ## License 22 | 23 | [MIT](./LICENSE) 24 | 25 | ## Support 26 | 27 | You can drop a star if this project helped you out. 💫 28 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneskarpuz/react-native-drag-text-editor/0d007b0b6fc2e996c2bb9a35c3c1d7c979a8ea12/demo.gif -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useMemo, useRef, useState} from 'react'; 2 | import {View, StyleSheet} from 'react-native'; 3 | import {useAnimatedStyle, useSharedValue} from 'react-native-reanimated'; 4 | import {DragTextEditor, DragTextRef} from 'react-native-drag-text-editor'; 5 | import {ExampleContextProvider} from './context/ExampleContextProvider'; 6 | import {Editor, IconButton} from './components'; 7 | import {defaultTextConfig, ICONS} from './constants'; 8 | import {DragTextArrayProps} from './types.d'; 9 | import {GestureHandlerRootView} from 'react-native-gesture-handler'; 10 | 11 | const App = () => { 12 | const [texts, addText] = useState([ 13 | {key: 0, ...defaultTextConfig}, 14 | ]); 15 | const DragTextEditorRef = useRef([]); 16 | const [activeIndex, setActiveIndex] = useState(0); 17 | const [textValue, onChangeText] = useState(texts[activeIndex].text); 18 | const sharedSliderValue = useSharedValue(texts[activeIndex].fontSize); 19 | 20 | const DefaultTextConfig = {key: texts.length, ...defaultTextConfig}; 21 | 22 | const editTextsArray = useCallback( 23 | (prop: string, value: number | boolean | string) => { 24 | const tmpTextsArray = [...texts]; 25 | tmpTextsArray[activeIndex][prop] = value; 26 | addText(tmpTextsArray); 27 | }, 28 | [texts, activeIndex], 29 | ); 30 | 31 | const editFontSize = useCallback( 32 | (value: number) => { 33 | texts[activeIndex].fontSize = value; 34 | }, 35 | [texts], 36 | ); 37 | 38 | const addNewText = () => { 39 | DragTextEditorRef.current[activeIndex]?.setBorderStatus(false); 40 | addText(texts => [...texts, DefaultTextConfig]); 41 | }; 42 | 43 | const manageActiveStatus = (_index: number) => { 44 | texts[activeIndex].fontSize = sharedSliderValue.value; 45 | texts[activeIndex].text = textValue; 46 | onChangeText(texts[_index].text); 47 | setActiveIndex(_index); 48 | sharedSliderValue.value = texts[_index].fontSize; 49 | texts 50 | .filter(el => el.key !== _index) 51 | .map((el, i) => DragTextEditorRef.current[el.key].setBorderStatus(false)); 52 | }; 53 | 54 | const exampleContextValues = useMemo( 55 | () => ({ 56 | sharedSliderValue, 57 | }), 58 | [sharedSliderValue], 59 | ); 60 | 61 | const animatedTextStyle = useAnimatedStyle( 62 | () => ({ 63 | fontSize: sharedSliderValue.value, 64 | }), 65 | [sharedSliderValue], 66 | ); 67 | 68 | const activeStyleHandler = useCallback( 69 | (index: number) => { 70 | if (index == activeIndex) { 71 | return animatedTextStyle; 72 | } else { 73 | return {fontSize: texts[index].fontSize}; 74 | } 75 | }, 76 | [texts, animatedTextStyle, activeIndex], 77 | ); 78 | 79 | const _cornerComponents = [ 80 | { 81 | side: 'TR', 82 | customCornerComponent: () => ( 83 | editTextsArray('visible', false)} 85 | iconName={ICONS.CLOSE_ICON} 86 | /> 87 | ), 88 | }, 89 | { 90 | side: 'TL', 91 | customCornerComponent: () => ( 92 | addNewText()} iconName={ICONS.TAB_ICON} /> 93 | ), 94 | }, 95 | ]; 96 | 97 | const _rotateComponent = { 98 | side: 'bottom', 99 | customRotationComponent: () => , 100 | }; 101 | const _resizerSnapPoints = ['right', 'left']; 102 | 103 | return ( 104 | 105 | 106 | 107 | {texts.map((item, index) => ( 108 | (DragTextEditorRef.current[index] = ref)} 114 | onItemActive={() => manageActiveStatus(index)} 115 | externalBorderStyles={_exampleStyles.externalBorder} 116 | placeholder={'Placeholder'} 117 | cornerComponents={_cornerComponents} 118 | resizerSnapPoints={_resizerSnapPoints} 119 | rotationComponent={_rotateComponent} 120 | externalTextStyles={[ 121 | {color: item.color}, 122 | activeStyleHandler(index), 123 | ]}> 124 | ))} 125 | 126 | editTextsArray(prop, value)} 128 | editFontSize={value => editFontSize(value)} 129 | addNewText={() => addNewText()} 130 | /> 131 | 132 | 133 | ); 134 | }; 135 | 136 | const _exampleStyles = StyleSheet.create({ 137 | gestureRootStyles: { 138 | ...StyleSheet.absoluteFillObject, 139 | }, 140 | container: { 141 | backgroundColor: '#fff', 142 | flex: 3, 143 | }, 144 | externalBorder: { 145 | borderStyle: 'dashed', 146 | borderColor: 'gray', 147 | }, 148 | editorContainer: { 149 | flex: 1, 150 | flexDirection: 'column', 151 | backgroundColor: 'white', 152 | padding: 20, 153 | }, 154 | addTextButton: { 155 | backgroundColor: '#ddd', 156 | padding: 10, 157 | borderRadius: 10, 158 | width: '50%', 159 | alignSelf: 'center', 160 | justifyContent: 'center', 161 | alignItems: 'center', 162 | }, 163 | fontOptions: { 164 | margin: 2, 165 | flex: 1, 166 | paddingVertical: 10, 167 | paddingHorizontal: 6, 168 | borderRadius: 10, 169 | }, 170 | colorOptions: { 171 | margin: 4, 172 | flex: 1, 173 | padding: 20, 174 | borderRadius: 10, 175 | }, 176 | }); 177 | export default App; 178 | -------------------------------------------------------------------------------- /example/components/Editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity, View, Text, StyleSheet} from 'react-native'; 3 | import Slider from '../Slider/Slider'; 4 | import {fonts, textColors} from '../../constants'; 5 | import {useExampleContext} from '../../hooks/useExampleContext'; 6 | import {EditorTypes} from './types'; 7 | 8 | const Editor = ({editTextsArray, editFontSize, addNewText}: EditorTypes) => { 9 | const {sharedSliderValue} = useExampleContext(); 10 | 11 | return ( 12 | 13 | 14 | {textColors.map((item, index) => ( 15 | editTextsArray('color', item)} 17 | style={[_exampleStyles.colorOptions, {backgroundColor: item}]} 18 | key={index}> 19 | ))} 20 | 21 | 22 | {fonts.map((item, index) => ( 23 | editTextsArray('fontFamily', item)} 25 | style={_exampleStyles.fontOptions} 26 | key={index}> 27 | {item} 28 | 29 | ))} 30 | 31 | editFontSize(sharedSliderValue.value)}> 32 | addNewText()} 34 | style={_exampleStyles.addTextButton}> 35 | Add Text 36 | 37 | 38 | ); 39 | }; 40 | 41 | const _exampleStyles = StyleSheet.create({ 42 | gestureRootStyles: { 43 | ...StyleSheet.absoluteFillObject, 44 | }, 45 | editorContainer: { 46 | flex: 1, 47 | flexDirection: 'column', 48 | backgroundColor: 'white', 49 | padding: 20, 50 | }, 51 | addTextButton: { 52 | backgroundColor: '#ddd', 53 | padding: 10, 54 | borderRadius: 10, 55 | width: '50%', 56 | alignSelf: 'center', 57 | justifyContent: 'center', 58 | alignItems: 'center', 59 | }, 60 | fontOptions: { 61 | margin: 2, 62 | flex: 1, 63 | paddingVertical: 10, 64 | paddingHorizontal: 6, 65 | borderRadius: 10, 66 | }, 67 | colorOptions: { 68 | margin: 4, 69 | flex: 1, 70 | padding: 20, 71 | borderRadius: 10, 72 | }, 73 | }); 74 | 75 | export default Editor; 76 | -------------------------------------------------------------------------------- /example/components/Editor/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface EditorTypes { 2 | editTextsArray: ( 3 | prop: string, 4 | value: number | boolean | string | any, 5 | ) => void; 6 | editFontSize: (value: number) => void; 7 | addNewText: () => void; 8 | } 9 | -------------------------------------------------------------------------------- /example/components/IconButton/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity, StyleSheet} from 'react-native'; 3 | import SvgIcon from './SvgIcon/SvgIcon'; 4 | import {IconButtonProps} from './types'; 5 | 6 | const IconButton = ({onPress, iconName}: IconButtonProps) => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | 20 | const IconButtonStyles = StyleSheet.create({ 21 | buttonStyles: { 22 | padding: 5, 23 | marginTop: -3, 24 | marginLeft: -3, 25 | marginRight: -3, 26 | backgroundColor: 'white', 27 | borderWidth: 0.5, 28 | borderColor: '#ccc', 29 | justifyContent: 'center', 30 | alignItems: 'center', 31 | borderRadius: 20, 32 | }, 33 | }); 34 | 35 | export default IconButton; 36 | -------------------------------------------------------------------------------- /example/components/IconButton/SvgIcon/SvgIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, {Path} from 'react-native-svg'; 3 | import {SvgIconProps} from './types'; 4 | 5 | const SvgIcon = ({stroke, d, width, height, fill, viewBox}: SvgIconProps) => { 6 | return ( 7 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default SvgIcon; 19 | -------------------------------------------------------------------------------- /example/components/IconButton/SvgIcon/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface SvgIconProps { 2 | stroke?: string; 3 | d: string; 4 | width: number; 5 | height: number; 6 | fill?: string; 7 | viewBox: string; 8 | } 9 | -------------------------------------------------------------------------------- /example/components/IconButton/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './IconButton'; -------------------------------------------------------------------------------- /example/components/IconButton/types.d.ts: -------------------------------------------------------------------------------- 1 | import {ICONS} from '../../constants'; 2 | 3 | export interface IconButtonProps { 4 | onPress?: () => void; 5 | iconName: ICONS; 6 | } 7 | -------------------------------------------------------------------------------- /example/components/Slider/Slider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import { 4 | GestureHandlerRootView, 5 | PanGestureHandler, 6 | } from 'react-native-gesture-handler'; 7 | import Animated, { 8 | useAnimatedGestureHandler, 9 | useAnimatedStyle, 10 | runOnJS, 11 | } from 'react-native-reanimated'; 12 | import {useExampleContext} from '../../hooks/useExampleContext'; 13 | import {SliderTypes} from './types'; 14 | 15 | const Slider = ({onEnded}: SliderTypes) => { 16 | const {sharedSliderValue} = useExampleContext(); 17 | 18 | const slideHandler = useAnimatedGestureHandler({ 19 | onStart: (_ev: any, ctx: any) => { 20 | ctx.startX = sharedSliderValue.value; 21 | }, 22 | onActive: (_ev, ctx) => { 23 | sharedSliderValue.value = ctx.startX + _ev.translationX; 24 | }, 25 | onEnd: () => { 26 | runOnJS(onEnded)(); 27 | }, 28 | }); 29 | const animatedSliderStyle = useAnimatedStyle( 30 | () => ({ 31 | transform: [{translateX: sharedSliderValue.value}], 32 | }), 33 | [], 34 | ); 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | const sliderStyle = StyleSheet.create({ 54 | sliderDefault: { 55 | backgroundColor: 'black', 56 | width: 20, 57 | height: 20, 58 | borderRadius: 20, 59 | position: 'absolute', 60 | bottom: -8, 61 | }, 62 | sliderInactive: { 63 | height: 3, 64 | width: '80%', 65 | alignSelf: 'center', 66 | backgroundColor: '#ccc', 67 | display: 'flex', 68 | }, 69 | sliderContainer: { 70 | height: '5%', 71 | width: '100%', 72 | padding: 20, 73 | justifyContent: 'center', 74 | alignItems: 'center', 75 | backgroundColor: '#fff', 76 | display: 'flex', 77 | }, 78 | }); 79 | export default Slider; 80 | -------------------------------------------------------------------------------- /example/components/Slider/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface SliderTypes{ 2 | onEnded:()=>void; 3 | } -------------------------------------------------------------------------------- /example/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Editor } from './Editor/Editor'; 2 | export { default as Slider } from './Slider/Slider'; 3 | export { default as IconButton } from './IconButton/IconButton'; 4 | -------------------------------------------------------------------------------- /example/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ICONS { 2 | TAB_ICON = 'M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6', 3 | ROTATE_ICON = 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99', 4 | CLOSE_ICON = 'M6 18L18 6M6 6l12 12', 5 | } 6 | 7 | export const fonts = [ 8 | 'Poppins-Medium', 9 | 'Poppins-Bold', 10 | ]; 11 | 12 | export const textColors = [ 13 | '#e7eff6', 14 | '#ddd', 15 | '#63ace5', 16 | '#4b86b4', 17 | '#2a4d69', 18 | '#63ace5', 19 | ]; 20 | 21 | export const defConfig = { 22 | color: 'black', 23 | visible: true, 24 | fontSize: 35, 25 | fontFamily: 'Poppins-Bold', 26 | text:'' 27 | }; 28 | -------------------------------------------------------------------------------- /example/context/ExampleContextProvider.ts: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react'; 2 | import {ExampleContextTypes} from './types.d'; 3 | 4 | export const ExampleContext = createContext(null); 5 | 6 | export const ExampleContextProvider = ExampleContext.Provider; 7 | -------------------------------------------------------------------------------- /example/context/types.d.ts: -------------------------------------------------------------------------------- 1 | import Animated from 'react-native-reanimated'; 2 | 3 | export interface ExampleContextTypes { 4 | sharedSliderValue: Animated.SharedValue; 5 | } 6 | -------------------------------------------------------------------------------- /example/hooks/useExampleContext.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {ExampleContext} from '../context/ExampleContextProvider' 3 | 4 | export const useExampleContext = () => { 5 | const context = useContext(ExampleContext); 6 | 7 | if (context === null) { 8 | throw "'useExampleContext' cannot be used out of the Example!"; 9 | } 10 | 11 | return context; 12 | }; -------------------------------------------------------------------------------- /example/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface DragTextArrayProps { 2 | visible: boolean; 3 | fontSize: number; 4 | fontFamily: string; 5 | color: string; 6 | key: number; 7 | text: string; 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-drag-text-editor", 3 | "version": "1.0.5", 4 | "description": "Draggable, Resizeable, Rotatable Text Input Component", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "typescript": "tsc --noEmit", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "\"react", 13 | "native\"", 14 | "ios", 15 | "android", 16 | "reanimated", 17 | "drag-n-drop", 18 | "\"text", 19 | "editor\"", 20 | "\"text\"", 21 | "\"photo", 22 | "editor\"", 23 | "\"image", 24 | "editor\"", 25 | "\"image", 26 | "manipulation\"", 27 | "\"no", 28 | "code\"" 29 | ], 30 | "author": "Enes Karpuz", 31 | "license": "MIT", 32 | "peerDependencies": { 33 | "react": "*", 34 | "react-native": "*", 35 | "react-native-gesture-handler": "^2.5.0", 36 | "react-native-reanimated": "^2.9.1" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.12.9", 40 | "@babel/runtime": "^7.12.5", 41 | "@react-native-community/eslint-config": "^3.1.0", 42 | "@types/jest": "^28.1.6", 43 | "@types/react": "^18.0.15", 44 | "@types/react-native": "^0.69.3", 45 | "@types/react-test-renderer": "^18.0.0", 46 | "babel-jest": "^26.6.3", 47 | "eslint": "^8.23.0", 48 | "jest": "^26.6.3", 49 | "metro-react-native-babel-preset": "^0.70.3", 50 | "react-test-renderer": "18.0.0", 51 | "typescript": "^4.7.4", 52 | "react-native-gesture-handler": ">=1.10.1", 53 | "react-native-reanimated": ">=2.2.0" 54 | }, 55 | "dependencies": { 56 | "eslint-config-prettier": "^8.5.0", 57 | "eslint-plugin-prettier": "^4.2.1", 58 | "prettier": "^2.7.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/cornerComponent/CornerComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react'; 2 | import { StyleProp, ViewStyle } from 'react-native'; 3 | import Animated, { useAnimatedStyle } from 'react-native-reanimated'; 4 | import { CORNERS } from '../../constants'; 5 | import { useRNDTInternal } from '../../hooks'; 6 | import styles from './styles'; 7 | import { CornerComponentProps } from './types'; 8 | 9 | const CornerComponent: FC = ({ 10 | side, 11 | customCornerComponent, 12 | }) => { 13 | var { borderStatus } = useRNDTInternal(); 14 | 15 | const cornerComponentStyleSelector: { 16 | [key in CORNERS]: StyleProp; 17 | } = { 18 | TR: styles.TR, 19 | TL: styles.TL, 20 | BR: styles.BR, 21 | BL: styles.BL, 22 | }; 23 | const cornerComponentStyle = cornerComponentStyleSelector[side as CORNERS]; 24 | 25 | const borderStatusStyle = useAnimatedStyle(() => ({ 26 | display: borderStatus.value ? 'flex' : 'none', 27 | })); 28 | 29 | return ( 30 | 31 | {customCornerComponent()} 32 | 33 | ); 34 | }; 35 | 36 | export default memo(CornerComponent); 37 | -------------------------------------------------------------------------------- /src/components/cornerComponent/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const cornerButtonDefaults = StyleSheet.create({ 4 | defaults: { 5 | zIndex: 100, 6 | elevation: 0.01, 7 | alignItems: 'center', 8 | position: 'absolute', 9 | }, 10 | }); 11 | 12 | const styles = StyleSheet.create({ 13 | TL: { 14 | ...cornerButtonDefaults.defaults, 15 | top: -8, 16 | left: -8, 17 | }, 18 | TR: { 19 | ...cornerButtonDefaults.defaults, 20 | top: -8, 21 | right: -8, 22 | }, 23 | BR: { 24 | ...cornerButtonDefaults.defaults, 25 | bottom: -8, 26 | right: -8, 27 | }, 28 | BL: { 29 | ...cornerButtonDefaults.defaults, 30 | bottom: -8, 31 | left: -8, 32 | }, 33 | }); 34 | 35 | export default styles; 36 | -------------------------------------------------------------------------------- /src/components/cornerComponent/types.d.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { CORNERS } from '../../constants'; 3 | 4 | export interface CornerComponentProps { 5 | /** 6 | * Preffered resizers [ 'left', 'right', 'top', 'bottom' ] 7 | * @type Array 8 | */ 9 | side: CORNERS | string; 10 | /** 11 | * Preffered resizers [ 'left', 'right', 'top', 'bottom' ] 12 | * @type Array 13 | */ 14 | customCornerComponent?: PropsWithChildren; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/customTextInput/CustomTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { FC } from 'react'; 3 | import { TextInput } from 'react-native'; 4 | import Animated from 'react-native-reanimated'; 5 | import { PLACEHOLDER } from '../../constants'; 6 | import { useRNDTExternal, useRNDTInternal } from '../../hooks'; 7 | import styles from './styles'; 8 | const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); 9 | 10 | const CustomTextInput: FC = () => { 11 | var { 12 | externalTextStyles, 13 | placeholder, 14 | onChangeText, 15 | onBlur, 16 | value, 17 | blurOnSubmit, 18 | defaultTextValue, 19 | } = useRNDTExternal(); 20 | const { isResize } = useRNDTInternal(); 21 | return ( 22 | <> 23 | (isResize.value = false)} 36 | style={[styles.textStyles, externalTextStyles]} 37 | placeholder={placeholder ? placeholder : PLACEHOLDER} 38 | /> 39 | 40 | ); 41 | }; 42 | 43 | export default memo(CustomTextInput); 44 | -------------------------------------------------------------------------------- /src/components/customTextInput/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | textStyles: { 5 | width: '90%', 6 | alignSelf: 'flex-start', 7 | margin: 10, 8 | }, 9 | }); 10 | 11 | export default styles; 12 | -------------------------------------------------------------------------------- /src/components/dragTextEditor/DragTextEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | memo, 3 | forwardRef, 4 | Ref, 5 | useCallback, 6 | useImperativeHandle, 7 | useMemo, 8 | useState, 9 | } from 'react'; 10 | import { Keyboard, StyleSheet, TouchableOpacity } from 'react-native'; 11 | import Animated, { 12 | useAnimatedGestureHandler, 13 | useSharedValue, 14 | useAnimatedStyle, 15 | runOnJS, 16 | } from 'react-native-reanimated'; 17 | import { PanGestureHandler } from 'react-native-gesture-handler'; 18 | import RotationSnapPoint from '../rotationSnapPoint/RotationSnapPoint'; 19 | import ResizerSnapPoint from '../resizerSnapPoint/ResizerSnapPoint'; 20 | import CustomTextInput from '../customTextInput/CustomTextInput'; 21 | import { RNDTInternalProvider } from '../../context/internal'; 22 | import { RNDTExternalProvider } from '../../context/external'; 23 | import { 24 | defBoxWidth, 25 | defInputCoverZIndex, 26 | defRotationAngle, 27 | defX, 28 | defY, 29 | radian, 30 | SIDES, 31 | textInputLayoutDefaults, 32 | } from '../../constants'; 33 | import CornerComponent from '../cornerComponent/CornerComponent'; 34 | import { CornerComponentProps } from '../cornerComponent/types.d'; 35 | import { DragTextPropTypes, DragTextRef } from './types'; 36 | 37 | const DragText= forwardRef( 38 | ( 39 | { 40 | rotationComponent, 41 | resizerSnapPoints, 42 | cornerComponents, 43 | externalTextStyles, 44 | externalBorderStyles, 45 | blurOnSubmit, 46 | placeholder, 47 | value, 48 | defaultTextValue, 49 | visible = true, 50 | onChangeText, 51 | onBlur, 52 | onItemActive, 53 | }:DragTextPropTypes, 54 | ref: Ref 55 | ) => { 56 | const x = useSharedValue(defX); 57 | const y = useSharedValue(defY); 58 | const boxWidth = useSharedValue(defBoxWidth); 59 | const rotationAngle = useSharedValue(defRotationAngle); 60 | const [inputCoverZIndex, _setInputCoverZIndex] = 61 | useState(defInputCoverZIndex); 62 | const isResize = useSharedValue(false); 63 | const borderStatus = useSharedValue(true); 64 | const textInputLayout = textInputLayoutDefaults; 65 | 66 | const onStartRoutine = () => { 67 | Keyboard.dismiss(); 68 | onItemActive?.(); 69 | }; 70 | 71 | const onEndRoutine = () => { 72 | _setInputCoverZIndex(defInputCoverZIndex); 73 | }; 74 | 75 | const dragHandler = useAnimatedGestureHandler({ 76 | onStart: (_ev: any, ctx: any) => { 77 | runOnJS(onStartRoutine)(); 78 | borderStatus.value = true; 79 | ctx.startX = x.value; 80 | ctx.startY = y.value; 81 | }, 82 | onActive: (_ev: any, ctx: any) => { 83 | if (borderStatus.value) { 84 | y.value = ctx.startY + _ev.translationY; 85 | x.value = ctx.startX + _ev.translationX; 86 | } 87 | }, 88 | onEnd: () => { 89 | runOnJS(onEndRoutine)(); 90 | borderStatus.value = false; 91 | }, 92 | }); 93 | 94 | useImperativeHandle(ref, () => ({ 95 | setBorderStatus: param => { 96 | borderStatus.value = param; 97 | _setInputCoverZIndex(defInputCoverZIndex); 98 | }, 99 | })); 100 | 101 | const externalContextVariables = useMemo( 102 | () => ({ 103 | resizerSnapPoints, 104 | cornerComponents, 105 | externalTextStyles, 106 | externalBorderStyles, 107 | rotationComponent, 108 | placeholder, 109 | onChangeText, 110 | defaultTextValue, 111 | blurOnSubmit, 112 | value, 113 | onBlur, 114 | }), 115 | [ 116 | resizerSnapPoints, 117 | cornerComponents, 118 | externalTextStyles, 119 | externalBorderStyles, 120 | rotationComponent, 121 | placeholder, 122 | onChangeText, 123 | defaultTextValue, 124 | blurOnSubmit, 125 | value, 126 | onBlur, 127 | ] 128 | ); 129 | 130 | const internalContextVariables = useMemo( 131 | () => ({ 132 | x, 133 | y, 134 | boxWidth, 135 | rotationAngle, 136 | isResize, 137 | textInputLayout, 138 | borderStatus, 139 | }), 140 | [x, y, boxWidth, isResize, textInputLayout, rotationAngle, borderStatus] 141 | ); 142 | 143 | const animatedDragStyles = useAnimatedStyle( 144 | () => ({ 145 | transform: [{ translateX: x.value }, { translateY: y.value }], 146 | width: boxWidth.value, 147 | display: visible ? 'flex' : 'none', 148 | }), 149 | [x, y, boxWidth, visible] 150 | ); 151 | 152 | const animatedRotationStyles = useAnimatedStyle( 153 | () => ({ 154 | transform: [{ rotateZ: rotationAngle.value + radian }], 155 | borderWidth: borderStatus.value ? 1 : 0, 156 | }), 157 | [rotationAngle, borderStatus] 158 | ); 159 | 160 | const _cornerComponent = useCallback( 161 | ({ side, customCornerComponent }: CornerComponentProps, i: number) => ( 162 | 167 | ), 168 | [] 169 | ); 170 | 171 | const _resizerSnapPoint = useCallback( 172 | (sides: SIDES | string, i: number) => ( 173 | 174 | ), 175 | [] 176 | ); 177 | 178 | return ( 179 | 180 | 181 | 182 | 183 | 190 | {rotationComponent && ( 191 | 197 | )} 198 | {cornerComponents?.map(_cornerComponent)} 199 | {resizerSnapPoints?.map(_resizerSnapPoint)} 200 | _setInputCoverZIndex(0)} 202 | style={[styles.inputCoverStyle, { zIndex: inputCoverZIndex }]} 203 | /> 204 | 205 | 206 | 207 | 208 | 209 | 210 | ); 211 | } 212 | ); 213 | 214 | const styles = StyleSheet.create({ 215 | dragContainer: { 216 | position: 'absolute', 217 | }, 218 | rotationStyles: { 219 | justifyContent: 'center', 220 | alignItems: 'center', 221 | borderWidth: 1, 222 | borderStyle: 'dashed', 223 | borderColor: '#777', 224 | }, 225 | inputCoverStyle: { 226 | width: '100%', 227 | height: '100%', 228 | position: 'absolute', 229 | elevation: 5, 230 | }, 231 | }); 232 | 233 | export default memo(DragText); 234 | -------------------------------------------------------------------------------- /src/components/dragTextEditor/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DragTextEditor } from './DragTextEditor'; 2 | -------------------------------------------------------------------------------- /src/components/dragTextEditor/types.d.ts: -------------------------------------------------------------------------------- 1 | import { CornerComponentProps } from '../cornerComponent/types'; 2 | import { StyleProp, TextStyle, ViewStyle } from 'react-native'; 3 | import { rotationComponentPropTypes } from '../rotationSnapPoint/types'; 4 | import { SIDES } from '../../constants'; 5 | 6 | export interface DragTextRef { 7 | /** 8 | * Change border visibility 9 | * @param borderStatus border visibility 10 | */ 11 | setBorderStatus: (value: boolean) => void; 12 | } 13 | 14 | export interface DragTextPropTypes { 15 | // TextInput Types 16 | onChangeText?: ((text: string) => void) | undefined; 17 | blurOnSubmit?: boolean | undefined; 18 | value?: string | undefined; 19 | onBlur?: 20 | | ((e: NativeSyntheticEvent) => void) 21 | | undefined; 22 | 23 | onItemActive?: () => void; 24 | /** 25 | * Component visibility 26 | * @type boolean | undefined 27 | */ 28 | visible?: boolean; 29 | /** 30 | * Custom corner component props 31 | * [{ side:'TL', customCornerComponent:customComponent }] 32 | * @type Array 33 | */ 34 | cornerComponents?: Array; 35 | /** 36 | * Custom rotation component props 37 | * [{ side:'bottom', customRotationComponent:customComponent }] 38 | * @type rotationComponentPropTypes 39 | */ 40 | rotationComponent?: rotationComponentPropTypes; 41 | /** 42 | * Resizer snap points [ 'left', 'right', 'top', 'bottom' ] 43 | * @type Array 44 | */ 45 | resizerSnapPoints?: Array; 46 | /** 47 | * External text styles 48 | * @type StyleProp 49 | */ 50 | externalTextStyles?: StyleProp; 51 | /** 52 | * External border styles 53 | * @type StyleProp 54 | */ 55 | externalBorderStyles?: StyleProp; 56 | placeholder?: string; 57 | defaultTextValue?: string; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/resizerSnapPoint/ResizerSnapPoint.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useState } from 'react'; 2 | import Animated, { 3 | runOnJS, 4 | useAnimatedGestureHandler, 5 | useAnimatedStyle, 6 | } from 'react-native-reanimated'; 7 | import { PanGestureHandler } from 'react-native-gesture-handler'; 8 | import styles from './styles'; 9 | import { useRNDTInternal } from '../../hooks'; 10 | import { ResizerSnapPointProps } from './types.d'; 11 | import { handleLeft, handleRight } from '../../utils/calculate'; 12 | import { Keyboard, StyleProp, View, ViewStyle } from 'react-native'; 13 | import { SIDES, textInputLayoutOffset } from '../../constants'; 14 | 15 | const ResizerSnapPoint: FC = ({ side }) => { 16 | const { boxWidth, x, y, textInputLayout, isResize, borderStatus } = 17 | useRNDTInternal(); 18 | 19 | const [widthEnabled, setWidthEnabled] = useState(true); 20 | 21 | const handleLimiters = useCallback(() => { 22 | if (boxWidth.value <= textInputLayout.width) { 23 | setWidthEnabled(false); 24 | } else { 25 | setWidthEnabled(true); 26 | } 27 | }, [boxWidth, textInputLayout.width]); 28 | 29 | const handleResizers = (params: any) => { 30 | 'worklet'; 31 | switch (side) { 32 | case SIDES.LEFT: 33 | [boxWidth.value, x.value] = handleLeft(params); 34 | break; 35 | case SIDES.RIGHT: 36 | boxWidth.value = handleRight(params); 37 | break; 38 | case undefined: 39 | break; 40 | } 41 | isResize.value = false; 42 | runOnJS(handleLimiters)(); 43 | }; 44 | 45 | const resizeHandler = useAnimatedGestureHandler( 46 | { 47 | onStart: (_ev: any, ctx: any) => { 48 | runOnJS(Keyboard.dismiss)(); 49 | ctx.boxW = boxWidth.value + textInputLayoutOffset; 50 | ctx.startX = x.value; 51 | ctx.startY = y.value; 52 | }, 53 | onActive: (_ev, ctx) => { 54 | handleResizers({ _ev, ctx }); 55 | }, 56 | onFinish: _ev => { 57 | runOnJS(setWidthEnabled)(true); 58 | }, 59 | }, 60 | [] 61 | ); 62 | 63 | /** 64 | * resizeFieldStyles: transparent snap point with bigger 65 | * touchable field than visible white snap points 66 | */ 67 | const resizeFieldStyles: { [key in SIDES]: StyleProp } = { 68 | left: styles.leftResizerField, 69 | right: styles.rightResizerField, 70 | }; 71 | const resizeFieldStyle = resizeFieldStyles[side as SIDES]; 72 | 73 | /** resizeStyles: visible white snap point */ 74 | const resizeStyles: { [key in SIDES]: StyleProp } = { 75 | left: styles.leftResizer, 76 | right: styles.rightResizer, 77 | }; 78 | const resizeStyle = resizeStyles[side as SIDES]; 79 | 80 | const borderStatusStyle = useAnimatedStyle(() => ({ 81 | display: borderStatus.value ? 'flex' : 'none', 82 | })); 83 | 84 | return ( 85 | 86 | 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | export default ResizerSnapPoint; 94 | -------------------------------------------------------------------------------- /src/components/resizerSnapPoint/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const resizeDefaults = StyleSheet.create({ 4 | defaults: { 5 | borderWidth: 1, 6 | borderStyle: 'solid', 7 | borderColor: '#ddd', 8 | backgroundColor: 'white', 9 | position: 'absolute', 10 | borderRadius: 25, 11 | }, 12 | fieldDefaults: { 13 | zIndex: 200, 14 | elevation: 0.1, 15 | width: 35, 16 | height: 35, 17 | backgroundColor: 'transparent', 18 | position: 'absolute', 19 | }, 20 | }); 21 | const styles = StyleSheet.create({ 22 | rightResizerField: { 23 | ...resizeDefaults.fieldDefaults, 24 | right: -17, 25 | }, 26 | leftResizerField: { 27 | ...resizeDefaults.fieldDefaults, 28 | left: -17, 29 | }, 30 | bottomResizerField: { 31 | ...resizeDefaults.fieldDefaults, 32 | bottom: -17, 33 | }, 34 | topResizerField: { 35 | ...resizeDefaults.fieldDefaults, 36 | top: -17, 37 | }, 38 | rightResizer: { 39 | ...resizeDefaults.defaults, 40 | width: 10, 41 | height: 25, 42 | marginTop: 5, 43 | right: 12, 44 | }, 45 | leftResizer: { 46 | ...resizeDefaults.defaults, 47 | width: 10, 48 | height: 25, 49 | marginTop: 5, 50 | left: 12, 51 | }, 52 | bottomResizer: { 53 | ...resizeDefaults.defaults, 54 | width: 25, 55 | height: 10, 56 | marginLeft: 5, 57 | bottom: 12, 58 | }, 59 | topResizer: { 60 | ...resizeDefaults.defaults, 61 | width: 25, 62 | height: 10, 63 | marginLeft: 5, 64 | top: 12, 65 | }, 66 | }); 67 | 68 | export default styles; 69 | -------------------------------------------------------------------------------- /src/components/resizerSnapPoint/types.d.ts: -------------------------------------------------------------------------------- 1 | import { SIDES } from '../../constants'; 2 | 3 | export type ResizerSnapPointProps = { 4 | side: SIDES | string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/rotationSnapPoint/RotationSnapPoint.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo, useCallback } from 'react'; 2 | import Animated, { 3 | runOnJS, 4 | useAnimatedGestureHandler, 5 | useAnimatedStyle, 6 | } from 'react-native-reanimated'; 7 | import { PanGestureHandler } from 'react-native-gesture-handler'; 8 | import { 9 | rotation, 10 | getCenters, 11 | toDegree, 12 | toRadian, 13 | inRange, 14 | } from '../../utils/calculate'; 15 | import styles from './styles'; 16 | import { useRNDTInternal } from '../../hooks'; 17 | import { ROTATION_SNAP_POINTS } from '../../constants'; 18 | import { Keyboard, StyleProp, ViewStyle } from 'react-native'; 19 | import { rotationComponentPropTypes } from './types'; 20 | 21 | const RotationSnapPoint: FC = ({ 22 | side, 23 | customRotationComponent, 24 | }) => { 25 | const internals = useRNDTInternal(); 26 | 27 | const rotationFixer = useCallback(() => { 28 | 'worklet'; 29 | const rotationAngleInDegree = toDegree(internals.rotationAngle.value); 30 | if (inRange(rotationAngleInDegree, 30, -30)) { 31 | internals.rotationAngle.value = 0; 32 | } 33 | if (inRange(rotationAngleInDegree, 150, 50)) { 34 | internals.rotationAngle.value = toRadian(90); 35 | } 36 | if (inRange(rotationAngleInDegree, -150, -50)) { 37 | internals.rotationAngle.value = toRadian(-90); 38 | } 39 | }, [internals.rotationAngle]); 40 | 41 | const rotateHandler = useAnimatedGestureHandler({ 42 | onStart: (_ev: any, ctx: any) => { 43 | runOnJS(Keyboard.dismiss)(); 44 | ctx.centers = getCenters(internals); 45 | }, 46 | onActive: (_ev, ctx) => { 47 | internals.rotationAngle.value = rotation(_ev, ctx); 48 | }, 49 | onEnd: () => { 50 | rotationFixer(); 51 | }, 52 | }); 53 | 54 | const borderStatusStyle = useAnimatedStyle(() => ({ 55 | display: internals.borderStatus.value ? 'flex' : 'none', 56 | })); 57 | 58 | const rotationStylesSelector: { 59 | [key in ROTATION_SNAP_POINTS]: StyleProp; 60 | } = { 61 | left: styles.left, 62 | bottom: styles.bottom, 63 | top: styles.top, 64 | }; 65 | const rotationStyle = rotationStylesSelector[side as ROTATION_SNAP_POINTS]; 66 | 67 | return ( 68 | 69 | 70 | {customRotationComponent()} 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default memo(RotationSnapPoint); 77 | -------------------------------------------------------------------------------- /src/components/rotationSnapPoint/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const rotationDefaults = StyleSheet.create({ 4 | defaults: { 5 | position: 'absolute', 6 | justifyContent: 'center', 7 | alignItems: 'center', 8 | alignSelf: 'center', 9 | }, 10 | }); 11 | 12 | const styles = StyleSheet.create({ 13 | left: { 14 | ...rotationDefaults.defaults, 15 | left: -8, 16 | top: -8, 17 | }, 18 | top: { 19 | ...rotationDefaults.defaults, 20 | top: -40, 21 | }, 22 | bottom: { 23 | ...rotationDefaults.defaults, 24 | bottom: -40, 25 | }, 26 | }); 27 | 28 | export default styles; 29 | -------------------------------------------------------------------------------- /src/components/rotationSnapPoint/types.d.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { ROTATION_SNAP_POINTS } from '../../constants'; 3 | 4 | export type rotationComponentPropTypes = { 5 | side: ROTATION_SNAP_POINTS | string; 6 | customRotationComponent: PropsWithChildren; 7 | }; 8 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ROTATION_SNAP_POINTS { 2 | LEFT = 'left', 3 | BOTTOM = 'bottom', 4 | TOP = 'top', 5 | } 6 | 7 | export enum CORNERS { 8 | TR = 'TR', 9 | TL = 'TL', 10 | BR = 'BR', 11 | BL = 'BL', 12 | } 13 | 14 | export enum SIDES { 15 | RIGHT = 'right', 16 | LEFT = 'left', 17 | } 18 | 19 | export const defX = 30; 20 | export const defY = 100; 21 | export const defBoxWidth = 240; 22 | export const defRotationAngle = 0; 23 | export const PLACEHOLDER = 24 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod incididunt.'; 25 | export const defInputCoverZIndex = 1000; 26 | 27 | export const textInputLayoutDefaults = { 28 | width: 100, 29 | height: 100, 30 | }; 31 | 32 | export const radian = 'rad'; 33 | export const textInputLayoutOffset = 5; 34 | -------------------------------------------------------------------------------- /src/context/external.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { RNDTExternalContextVariables } from './types'; 3 | 4 | export const RNDTExternalContext = 5 | createContext(null); 6 | 7 | export const RNDTExternalProvider = RNDTExternalContext.Provider; 8 | -------------------------------------------------------------------------------- /src/context/internal.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { RNDTInternalContextVariables } from './types'; 3 | 4 | export const RNDTInternalContext = 5 | createContext(null); 6 | 7 | export const RNDTInternalProvider = RNDTInternalContext.Provider; 8 | -------------------------------------------------------------------------------- /src/context/types.d.ts: -------------------------------------------------------------------------------- 1 | import Animated from 'react-native-reanimated'; 2 | import { StyleProp, TextStyle, ViewStyle } from 'react-native'; 3 | import { SIDES } from '../constants'; 4 | type textInputLayoutTypes = { 5 | height: number; 6 | width: number; 7 | }; 8 | 9 | export interface RNDTExternalContextVariables { 10 | // TextInput Types 11 | onChangeText?: ((text: string) => void) | undefined; 12 | onBlur?: 13 | | ((e: NativeSyntheticEvent) => void) 14 | | undefined; 15 | defaultTextValue?: string; 16 | blurOnSubmit?: boolean | undefined; 17 | placeholder?: string; 18 | value?: string | undefined; 19 | /** 20 | * Resizer Snap Points [ 'left', 'right', 'top', 'bottom' ] 21 | * @type Array 22 | */ 23 | resizerSnapPoints?: Array; 24 | /** 25 | * External text styles 26 | * @type StyleProp 27 | */ 28 | externalTextStyles?: StyleProp; 29 | /** 30 | * External border styles 31 | * @type StyleProp 32 | */ 33 | externalBorderStyles?: StyleProp; 34 | } 35 | 36 | export interface RNDTInternalContextVariables { 37 | /** 38 | * Border display status 39 | * @type Animated.SharedValue 40 | */ 41 | borderStatus: Animated.SharedValue; 42 | /** 43 | * customTextInput layout width and height 44 | * @type textInputLayoutTypes 45 | */ 46 | textInputLayout: textInputLayoutTypes; 47 | /** 48 | * Component resizing status 49 | * @type Animated.SharedValue 50 | */ 51 | isResize: Animated.SharedValue; 52 | /** 53 | * Rotation angle 54 | * @type Animated.SharedValue 55 | */ 56 | rotationAngle: Animated.SharedValue; 57 | /** 58 | * X value 59 | * @type Animated.SharedValue 60 | */ 61 | x: Animated.SharedValue; 62 | /** 63 | * Y value 64 | * @type Animated.SharedValue 65 | */ 66 | y: Animated.SharedValue; 67 | /** 68 | * Box width 69 | * @type Animated.SharedValue 70 | */ 71 | boxWidth: Animated.SharedValue; 72 | } 73 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { RNDTInternalContext } from '../context/internal'; 3 | import { RNDTExternalContext } from '../context/external'; 4 | 5 | export const useRNDTExternal = () => { 6 | const context = useContext(RNDTExternalContext); 7 | 8 | if (context === null) { 9 | throw "'useRNDT' cannot be used out of the RNDT!"; 10 | } 11 | 12 | return context; 13 | }; 14 | 15 | export const useRNDTInternal = () => { 16 | const context = useContext(RNDTInternalContext); 17 | 18 | if (context === null) { 19 | throw "'useRNDT' cannot be used out of the RNDT!"; 20 | } 21 | 22 | return context; 23 | }; 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { DragTextEditor } from './components/dragTextEditor'; 2 | export type { DragTextRef } from './components/dragTextEditor/types'; -------------------------------------------------------------------------------- /src/utils/calculate.ts: -------------------------------------------------------------------------------- 1 | export const rotation = (event: any, ctx: any): number => { 2 | 'worklet'; 3 | let radian = Math.atan2( 4 | event.absoluteX - ctx.centers.x, 5 | event.absoluteY - ctx.centers.y 6 | ); 7 | const rotationAngleInRad = -radian; 8 | return rotationAngleInRad; 9 | }; 10 | 11 | /** 12 | * @private 13 | * @param {number} number The number to check. 14 | * @param {number} start The start of the range. 15 | * @param {number} end The end of the range. 16 | * @returns {boolean} Returns `true` if `number` is in the range, else `false`. 17 | */ 18 | export const inRange = ( 19 | number: number, 20 | start: number, 21 | end: number 22 | ): boolean => { 23 | 'worklet'; 24 | return number >= Math.min(start, end) && number < Math.max(start, end); 25 | }; 26 | 27 | /** 28 | * Convert degrees to radians 29 | * @private 30 | * @param {number} degree The degree to convert. 31 | * @returns {number} 32 | */ 33 | export const toRadian = (degree: number): number => { 34 | 'worklet'; 35 | const radian = degree * (Math.PI / 180); 36 | return radian; 37 | }; 38 | 39 | /** 40 | * Convert radians to degrees 41 | * @private 42 | * @param {number} radian The radian to convert. 43 | * @returns {number} 44 | */ 45 | export const toDegree = (radian: number): number => { 46 | 'worklet'; 47 | const degree = radian * (180 / Math.PI); 48 | return degree; 49 | }; 50 | 51 | export const getCenters = (internals: any) => { 52 | 'worklet'; 53 | const x = internals.x.value + internals.boxWidth.value / 2; 54 | const y = internals.y.value; 55 | return { x, y }; 56 | }; 57 | 58 | export const handleLeft = (params: any) => { 59 | 'worklet'; 60 | const x = params.ctx.startX + params._ev.translationX; 61 | const boxWidth = params.ctx.boxW - params._ev.translationX; 62 | return [boxWidth, x]; 63 | }; 64 | 65 | export const handleBottom = (params: any) => { 66 | 'worklet'; 67 | const boxHeight = params.ctx.boxH + params._ev.translationY; 68 | return boxHeight; 69 | }; 70 | 71 | export const handleRight = (params: any) => { 72 | 'worklet'; 73 | const boxWidth = params.ctx.boxW + params._ev.translationX; 74 | return boxWidth; 75 | }; 76 | 77 | export const handleTop = (params: any) => { 78 | 'worklet'; 79 | const y = params.ctx.startY + params._ev.translationY; 80 | const boxHeight = params.ctx.boxH - params._ev.translationY; 81 | return [y, boxHeight]; 82 | }; 83 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "react-native-drag-text-editor": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "skipLibCheck": true, 23 | "strict": true, 24 | "target": "esnext" 25 | }, 26 | "include": ["src"], 27 | "exclude": ["example"] 28 | } 29 | --------------------------------------------------------------------------------