Related
I am doing the d3 azimuthal equal-area projection in react-native, i used this example for this. its working fine but im updating rotate values using panGestureHandler this is also working but it's not smooth and it's take time to update map.
this the repo of this.
this is the code where i update rotate values:
const countryPaths = useMemo(() => {
const clipAngle = 150;
const projection = d3
.geoAzimuthalEqualArea()
// .rotate([0, -90])
.rotate([rotateX, rotateY])
.fitSize([mapExtent, mapExtent], {
type: 'FeatureCollection',
features: COUNTRIES,
})
.clipAngle(clipAngle)
.translate([dimensions.width / 2, mapExtent / 2]);
const geoPath = d3.geoPath().projection(projection);
const windowPaths = COUNTRIES.map(geoPath);
return windowPaths;
}, [dimensions, rotateX, rotateY]);
here is my complete code
App.js
import React, {useState, useMemo, useEffect, useRef} from 'react';
import {
StyleSheet,
View,
Dimensions,
Animated,
PanResponder,
Text,
SafeAreaView,
} from 'react-native';
import Map from './components/Map';
import COLORS from './constants/Colors';
import movingAverage from './functions/movingAverage';
import * as d3 from 'd3';
import covidData_raw from './assets/data/who_data.json';
export default function App(props) {
const dimensions = Dimensions.get('window');
const [stat, setStat] = useState('avg_confirmed');
const [date, setDate] = useState('2020-04-24');
//Data Manipulation
const covidData = useMemo(() => {
const countriesAsArray = Object.keys(covidData_raw).map((key) => ({
name: key,
data: covidData_raw[key],
}));
const windowSize = 7;
const countriesWithAvg = countriesAsArray.map((country) => ({
name: country.name,
data: [...movingAverage(country.data, windowSize)],
}));
const onlyCountriesWithData = countriesWithAvg.filter(
(country) => country.data.findIndex((d, _) => d[stat] >= 10) != -1,
);
return onlyCountriesWithData;
}, []);
const maxY = useMemo(() => {
return d3.max(covidData, (country) => d3.max(country.data, (d) => d[stat]));
}, [stat]);
const colorize = useMemo(() => {
const colorScale = d3
.scaleSequentialSymlog(d3.interpolateReds)
.domain([0, maxY]);
return colorScale;
});
return (
<SafeAreaView>
<View>
<Map
dimensions={dimensions}
data={covidData}
date={date}
colorize={colorize}
stat={stat}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.primary,
alignItems: 'center',
justifyContent: 'center',
},
rotateView: {
width: 300,
height: 300,
backgroundColor: 'black',
shadowOpacity: 0.2,
},
});
map.js
import React, {useMemo, useState, useEffect} from 'react';
import {StyleSheet, View, Animated, PanResponder} from 'react-native';
//LIBRARIES
import Svg, {G, Path, Circle} from 'react-native-svg';
import * as d3 from 'd3';
import {
PanGestureHandler,
PinchGestureHandler,
State,
} from 'react-native-gesture-handler';
//CONSTANTS
import {COUNTRIES} from '../constants/CountryShapes';
import COLORS from '../constants/Colors';
//COMPONENTS
import Button from './Button';
const Map = (props) => {
const [countryList, setCountryList] = useState([]);
const [translateX, setTranslateX] = useState(0);
const [translateY, setTranslateY] = useState(0);
const [lastTranslateX, setLastTranslateX] = useState(0);
const [lastTranslateY, setLastTranslateY] = useState(0);
const [buttonOpacity, _] = useState(new Animated.Value(0));
const [scale, setScale] = useState(1);
const [prevScale, setPrevScale] = useState(1);
const [lastScaleOffset, setLastScaleOffset] = useState(0);
const [rotateX, setrotateX] = useState();
const [rotateY, setrotateY] = useState();
const {dimensions, data, date, colorize, stat} = props;
//Gesture Handlers
const panStateHandler = (event) => {
if (event.nativeEvent.oldState === State.UNDETERMINED) {
setLastTranslateX(translateX);
setLastTranslateY(translateY);
}
if (event.nativeEvent.oldState === State.ACTIVE) {
Animated.timing(buttonOpacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}).start();
}
};
const panGestureHandler = (event) => {
console.log('event', event.nativeEvent);
setrotateX(event.nativeEvent.x);
setrotateX(event.nativeEvent.y);
setTranslateX(-event.nativeEvent.translationX / scale + lastTranslateX);
setTranslateY(-event.nativeEvent.translationY / scale + lastTranslateY);
};
const pinchStateHandler = (event) => {
if (event.nativeEvent.oldState === State.UNDETERMINED) {
setLastScaleOffset(-1 + scale);
}
if (event.nativeEvent.oldState === State.ACTIVE) {
Animated.timing(buttonOpacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}).start();
}
};
const pinchGestureHandler = (event) => {
if (
event.nativeEvent.scale + lastScaleOffset >= 1 &&
event.nativeEvent.scale + lastScaleOffset <= 5
) {
setPrevScale(scale);
setScale(event.nativeEvent.scale + lastScaleOffset);
setTranslateX(
translateX -
(event.nativeEvent.focalX / scale -
event.nativeEvent.focalX / prevScale),
);
setTranslateY(
translateY -
(event.nativeEvent.focalY / scale -
event.nativeEvent.focalY / prevScale),
);
}
};
//Initialize Map Transforms
const initializeMap = () => {
setTranslateX(0);
setTranslateY(0);
setScale(1);
setPrevScale(1);
setLastScaleOffset(0);
Animated.timing(buttonOpacity, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}).start();
};
//Create Map Paths
const mapExtent = useMemo(() => {
return dimensions.width > dimensions.height / 2
? dimensions.height / 2
: dimensions.width;
}, [dimensions]);
const countryPaths = useMemo(() => {
const clipAngle = 150;
const projection = d3
.geoAzimuthalEqualArea()
// .rotate([0, -90])
.rotate([rotateX, rotateY])
.fitSize([mapExtent, mapExtent], {
type: 'FeatureCollection',
features: COUNTRIES,
})
.clipAngle(clipAngle)
.translate([dimensions.width / 2, mapExtent / 2]);
const geoPath = d3.geoPath().projection(projection);
const windowPaths = COUNTRIES.map(geoPath);
return windowPaths;
}, [dimensions, rotateX, rotateY]);
useEffect(() => {
setCountryList(
countryPaths.map((path, i) => {
const curCountry = COUNTRIES[i].properties.name;
const isCountryNameInData = data.some(
(country) => country.name === curCountry,
);
const curCountryData = isCountryNameInData
? data.find((country) => country.name === curCountry)['data']
: null;
const isDataAvailable = isCountryNameInData
? curCountryData.some((data) => data.date === date)
: false;
const dateIndex = isDataAvailable
? curCountryData.findIndex((x) => x.date === date)
: null;
return (
<Path
key={COUNTRIES[i].properties.name}
d={path}
stroke={COLORS.greyLight}
strokeOpacity={0.3}
strokeWidth={0.6}
fill={
isDataAvailable
? colorize(curCountryData[dateIndex][stat])
: COLORS.greyLight
}
opacity={isDataAvailable ? 1 : 0.4}
/>
);
}),
);
}, [rotateX, rotateY]);
return (
<View>
<PanGestureHandler
onGestureEvent={(e) => panGestureHandler(e)}
onHandlerStateChange={(e) => panStateHandler(e)}>
<PinchGestureHandler
onGestureEvent={(e) => pinchGestureHandler(e)}
onHandlerStateChange={(e) => pinchStateHandler(e)}>
<Svg
width={dimensions.width}
height={dimensions.height / 2}
style={styles.svg}>
<G
// transform={`scale(${scale}) translate(${-translateX},${-translateY})`}
>
<Circle
cx={dimensions.width / 2}
cy={mapExtent / 2}
r={mapExtent / 2}
fill={COLORS.lightPrimary}
/>
{countryList.map((x) => x)}
</G>
</Svg>
</PinchGestureHandler>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
svg: {},
rotateView: {
width: 100,
height: 400,
backgroundColor: 'black',
shadowOffset: {height: 1, width: 1},
shadowOpacity: 0.2,
},
});
export default Map;
how i fixed this issue is :
I cange countries json to countries-110m.json';
delete the rotateX, rotateY and replace by translateX translateY
new rotate code is: .rotate([-translateX, translateY])
if any doubts please check my complete source code from Github
On dataView true dataViewModel is rendered
{popUps.dataView && popupsFactory.dataView}
Rendering of model is very slow, which have and table with dataSource around 10k-20k or more.
/* eslint-disable react/prop-types */
/* eslint-disable indent */
/* eslint-disable linebreak-style */
import React, { useState, useEffect, useRef } from 'react'
import { Modal, Select, Table, Input, Button, Icon, Tooltip } from 'antd'
//import Highlighter from 'react-highlight-words'
import { connect } from 'react-redux'
import actionDispatcher from 'client/app-actions'
import { isFiltered } from 'client/routes/app/constants/deck-layers'
//import TableLayout from './Table'
import styled from './styled-component'
//import './style.css'
const { Option } = Select
const dataViewModal = (props) => {
const {
visibility
} = props
const [layerId, setLayerId] = useState('')
const [tableColumns, setTableColumns] = useState(null)
const [tableData, setTableData] = useState(null)
const [searchText, setSearchText] = useState('')
const [searchedColumn, setSearchedColumn] = useState('')
const [filteredInfo, setFilteredInfo] = useState(null)
const [sortedInfo, setSortedInfo] = useState(null)
const [pagination, setPagination] = useState({ pageSize: 20, current: 1 })
const [currentDataSource, setCurrentDataSource] = useState(null)
const searchInput = useRef(null)
useEffect(() => {
// console.log(currentDataSource)
}, [currentDataSource])
/**
* initialize layer
*/
useEffect(() => {
setLayerId(props.layers.length ? props.layers[0].uniqueKey : null)
}, [props.layers])
/**
* whenerver layerId changes clear the sort and filter for the columns
*/
useEffect(() => {
clearAll()
}, [layerId])
const tempLayer = props.layers.find(item => item.uniqueKey === layerId)
useEffect(() => {
const layer = props.layers.find(item => item.uniqueKey === layerId)
if (layer?.props?.coordinates) {
showMapDataToUser(layer, false)
} else {
setTableColumns(null)
setTableData(null)
}
}, [tempLayer, sortedInfo, filteredInfo])
const showMapDataToUser = (layer, exportToCsv) => {
const filteredColumnSet = layer.columnSet.filter(
item => item.variableAdded
)
const showDataToUser = []
const showColumns = []
filteredColumnSet.forEach((item, index) => {
if (item.showToUser) {
showColumns.push(`${item.displayName}`)
}
})
showDataToUser.push([showColumns])
layer.props.coordinates.forEach((it) => {
const showData = []
filteredColumnSet.forEach((item, index) => {
if (item.showToUser) {
if (it.geometry &&
it.geometry.data[layer.props.columnSet + isFiltered]) {
if (showColumns.includes('Network GMP')) {
if (item.columnName.toUpperCase() === 'OLYMPUS_ID') {
const splitLatLong = it.geometry.data[index].split('_')
if (splitLatLong.length > 2
&& !isNaN(splitLatLong[splitLatLong.length - 1])
&& !isNaN(splitLatLong[splitLatLong.length - 2])) {
splitLatLong.splice(-2, 2)
}
const splitLatLongString = splitLatLong.join('_')
showData.push(`${splitLatLongString}`.replace(/,/g, ';'))
} else if (item.columnName.toUpperCase() === 'RETAINABILITY_KPI' && !isNaN(it.geometry.data[index])) {
const calculativeDCR = 100 - (it.geometry.data[index])
showData.push(`${calculativeDCR}`.replace(/,/g, ';'))
} else {
showData.push(`${it.geometry.data[index]}`.replace(/,/g, ';'))
}
} else {
showData.push(`${it.geometry.data[index]}`.replace(/,/g, ';'))
}
} else if (it[layer.props.columnSet + isFiltered]) {
if (showColumns.includes('Network GMP')) {
if (item.columnName.toUpperCase() === 'OLYMPUS_ID') {
const splitLatLong = it[index].split('_')
if (splitLatLong.length > 2
&& !isNaN(splitLatLong[splitLatLong.length - 1])
&& !isNaN(splitLatLong[splitLatLong.length - 2])) {
splitLatLong.splice(-2, 2)
}
const splitLatLongString = splitLatLong.join('_')
showData.push(`${splitLatLongString}`.replace(/,/g, ';'))
} else if (item.columnName.toUpperCase() === 'RETAINABILITY_KPI' && !isNaN(item[index])) {
const calculativeDCR = 100 - (item[index])
showData.push(`${calculativeDCR}`.replace(/,/g, ';'))
} else {
showData.push(`${it[index]}`.replace(/,/g, ';'))
}
} else {
showData.push(`${it[index]}`.replace(/,/g, ';'))
}
}
}
})
if (showData.length) {
showDataToUser.push([showData])
}
})
//-------------------------------------------
if (!exportToCsv) {
//create table data
const columnNames = filteredColumnSet.filter(col => col.showToUser).map(col => col.columnName)
const data = showDataToUser.slice(1).map((dataSet, index) => {
//items[0]: array converted to obj
const itemsObj = { key: index + 1 }
dataSet[0].forEach((item, i) => {
itemsObj[columnNames[i]] = item
})
return itemsObj
})
// console.log(data)
setTableData(data)
//
//create table columns
const sortedInfoNew = sortedInfo || {}
const filteredInfoNew = filteredInfo || {}
const columns = []
columns.push({
title: 'No.',
dataIndex: 'key',
width: 70,
// fixed: 'left',
key: 'key',
})
const len = filteredColumnSet.filter(col => col.showToUser).length
filteredColumnSet.filter(col => col.showToUser).forEach((item, index) => {
if (item.showToUser) {
// console.log(item)
const columnObj = index === 0 ? ({
title: item.displayName,
width: 150,
// fixed: 'left',
displayName: item.displayName,
dataIndex: item.columnName,
key: item.columnName,
sorter: item.columnName ? (a, b) => sortByColumnType(a, b, item) : null,
sortDirections: ['descend', 'ascend'],
sortOrder: sortedInfoNew.columnKey === item.columnName && sortedInfoNew.order,
filteredValue: filteredInfoNew[item.columnName] || null, // to clear search
...getColumnSearchProps(item.columnName)
})
: index === len - 1 ?
({
title: () => columnTitle(item, data),
width: 150,
//fixed: 'right',
displayName: item.displayName,
dataIndex: item.columnName,
key: item.columnName,
filters: getFilters(data, item.columnName),
sorter: item.columnName ? (a, b) => sortByColumnType(a, b, item) : null,
onFilter: (value, record) => record[item.columnName].indexOf(value) === 0,
sortDirections: ['descend', 'ascend'],
sortOrder: sortedInfoNew.columnKey === item.columnName && sortedInfoNew.order,
filteredValue: filteredInfoNew[item.columnName] || null,
}) :
({
title: () => columnTitle(item, data),
width: 150,
displayName: item.displayName,
dataIndex: item.columnName,
key: item.columnName,
filters: getFilters(data, item.columnName),
sorter: item.columnName ? (a, b) => sortByColumnType(a, b, item) : null,
onFilter: (value, record) => record[item.columnName].indexOf(value) === 0,
sortDirections: ['descend', 'ascend'],
sortOrder: sortedInfoNew.columnKey === item.columnName && sortedInfoNew.order,
filteredValue: filteredInfoNew[item.columnName] || null,
})
columns.push(columnObj)
}
})
// console.log(columns)
setTableColumns(columns)
//
}
//-------------------------------------------
// console.log(showDataToUser)
return showDataToUser
}
const getFilters = (data, columnName) => {
const uniqueValuesSet = new Set(data.map(dataObj => dataObj[columnName]))
const filters = Array.from(uniqueValuesSet).map(value => ({ text: value, value }))
return filters
}
const sortByColumnType = (a, b, item) => {
if (item.columnType === 'NUMERIC') {
return a[item.columnName] - b[item.columnName]
} else if (item.columnType === 'TEXT') {
if (!a[item.columnName]) {
// console.log(item, a)
}
return a[item.columnName]?.localeCompare(b[item.columnName])
}
// console.log(item)
return null
}
/**
* set to initial values
*/
const clearAll = () => {
setFilteredInfo(null)
setSortedInfo(null)
setPagination({
pageSize: 20,
current: 1
})
setCurrentDataSource(null)
}
function getColumnSearchProps(dataIndex) {
return {
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 8 }}>
<Input
ref={searchInput}
placeholder={`Search ${dataIndex}`}
value={selectedKeys && selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => handleSearch(selectedKeys, confirm, dataIndex)}
style={{ width: 188, marginBottom: 8, display: 'block' }}
/>
<Button
type="primary"
onClick={() => handleSearch(selectedKeys, confirm, dataIndex)}
icon="search"
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button onClick={() => handleReset(clearFilters)} size="small" style={{ width: 90 }}>
Reset
</Button>
</div>
),
filterIcon: filtered => <Icon type="search" style={{ color: filtered ? '#1890ff' : undefined }} />,
onFilter: (value, record) =>
record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: (visible) => {
if (visible) {
setTimeout(() => searchInput.current.select())
}
},
// render: text =>
// searchedColumn === dataIndex ? (
// <Highlighter
// highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
// searchWords={[searchText]}
// autoEscape
// textToHighlight={text.toString()}
// />
// ) : (
// text
// ),
}
}
function handleSearch(selectedKeys, confirm, dataIndex) {
confirm()
setSearchText(selectedKeys[0])
setSearchedColumn(dataIndex)
}
function handleReset(clearFilters) {
clearFilters()
setSearchText('')
}
const handleCancel = () => {
props.updatePopUpMode('dataView')
}
const handleChange = (value) => {
// console.log(`selected ${value}`)
setLayerId(value)
}
function onChange(pager, filters, sorter, extra) {
// console.log(pager, filters, sorter, extra)
setFilteredInfo(filters)
setSortedInfo(sorter)
setPagination(pager)
setCurrentDataSource(extra.currentDataSource)
}
const handleDownloadLayer = () => {
//const layer = props.layers.find(item => item.uniqueKey === layerId)
if (tableColumns?.length) {
//export in csv
const showDataToUser = []
showDataToUser.push(tableColumns.map(({ displayName }) => displayName).slice(1)) // don't include key column ref: line 138
const dataSource = currentDataSource || tableData
showDataToUser.push(...dataSource.map(item => tableColumns.map(({ key }) => (item[key])).slice(1))) // don't include key column values ref: line 138
let csvContent = ''
showDataToUser.forEach((row) => {
csvContent += row.join(',')
csvContent += '\n'
})
const encodedUri = URL.createObjectURL(new Blob([csvContent], { type: 'text/plain' }))
const a = document.createElement('a')
a.href = encodedUri
a.target = '_Blank'
a.download = 'mapdata.csv'
a.click()
//export in csv
}
}
const selectedLayer = props.layers.find(layer => layer.uniqueKey === layerId)
const columnTitle = (item, data) => {
const columnData = data.map(dataObj => dataObj[item.columnName])
// console.log(columnData)
const nullColumnData = columnData.filter(value => value === 'null')
const nullPercent = (nullColumnData.length / columnData.length) * 100
return (
<div>
<Tooltip placement="top" title={item.displayName}>
<styled.columnDisplayName>
{item.displayName}
</styled.columnDisplayName>
</Tooltip>
<div style={{ fontSize: '10px', color: '#ff5c00a1' }}>
{Math.round(nullPercent)}% null rows
</div>
</div>
)
}
const LayerNames = () => (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Select defaultValue={selectedLayer?.uniqueKey} style={{ width: 120 }} onChange={handleChange}>
{
props.layers.map(item =>
<Option value={item.uniqueKey}>{item.displayName}</Option>
)
}
</Select>
</div>
)
return (
<styled.modelWrapper
width="98%"
style={{ top: '20px' }}
title={<LayerNames/>}
visible={visibility}
onCancel={handleCancel}
footer={null}
>
<div className="Model Content" style={{ height: '590px' }}>
<styled.modalContentHeader>
<div style={{ float: 'right' }}>
<Button onClick={handleDownloadLayer}>Export to CSV</Button>
</div>
</styled.modalContentHeader>
<styled.tableWrapper style={{
overflow: 'auto',
height: '88%'
}}
>
{!tableData?.length ?
'NO DATA'
:
<styled.table
className="table-striped-rows"
columns={tableColumns}
dataSource={tableData}
pagination={pagination}
size="small"
onChange={onChange}
scroll={{ y: 410, x: 1340 }}
rowClassName={(record, index) => (index % 2 === 0 ? 'table-row-light' : 'table-row-dark')}
/>
}
</styled.tableWrapper>
</div>
</styled.modelWrapper>
)
}
const mapStateToProps = (store, ownProps) => {
const appViewState = store.appStore.appView
const layers = store.layerStore.layers
return {
selectedLayer: layers.length && layers.find(it => it.id === appViewState.variableSelectLayerId),
visibility: appViewState.popups.dataView,
layers
}
}
const mapDispatchToProps = {
updatePopUpMode: actionDispatcher.updatePopUpMode,
updateCoordinateDataFilter: actionDispatcher.updateCoordinateDataFilter
}
const DataView = connect(
mapStateToProps,
mapDispatchToProps
)(dataViewModal)
export default DataView
All operations like sorting, filtering, and pagination change are very slow. Filters take too long to render. Is there something I can do to optimize code. I tried to do calculation part of table dataSource before rendering and then passing it as a prop from Redux store, but that didn't work either.
Hello everyone we are building an app for a company. My task is to create an image crop feature for profile pictures. I am using the react-easy-crop library. I have implemented nearly all the features the only missing thing is zooming in to a certain pixel. Our company doesn't want users to be able to zoom in less than 600 X 600 pixels. What I meany by that is when users zoom in the image size can't be lower than 600 x 600 pixels. here is my code structure (Be aware this not the full code I have reduced it for complexity issues)
import React, { useEffect, useRef, useState, FC, useCallback, SetStateAction } from 'react';
import Cropper from 'react-easy-crop';
import Resizer from 'react-image-file-resizer';
import { Text, Button, DragAndDrop } from '#shared/components';
import { Modal } from '#shared/components/Modal/lazy';
import { getOrientation } from 'get-orientation';
import { COLOR_NAMES } from '#src/settings/colors.constants';
import { Icon } from '#src/shared/components/icon';
import { Centered } from '#src/shared/components/layout/centered';
import { useDispatch } from 'react-redux';
import { Flex } from 'rebass';
import { Dispatch } from 'redux';
import { ZoomAndApplyContainer, CropContainer } from '#app/shared/components/crop-image/styled';
import { FileValue } from '#shared/components/upload-single-document/interfaces';
import { UploadSingleImageProps } from '#app/shared/components/upload-single-image/interfaces';
import { CoverPicEditComponent, ImageUploadContainer, PicEditComponent } from '#app/shared/components/upload-single-image/styled';
import { MODAL_NAMES, MODAL_TYPES } from '#app/shared/store/constants/modal.constants';
import { ShowModalAC, HideModalAC } from '#app/shared/store/actions/modal.actions';
import { NumberOfBytesInOneMB, TOASTER_APPEARANCES } from '#app/shared/constants';
import { SetToastsActionCreator } from '#app/shared/store/actions/toast.actions';
import { validateFileType } from '#app/utils/validations';
import { PRIMARY } from '../button/button.constants';
import { FormikFieldErrorMessage } from '../formik-field/styled';
const readFile: any = (file: any) =>
new Promise((resolve: any) => {
const reader: any = new FileReader();
reader.addEventListener('load', () => resolve(reader.result), false);
reader.readAsDataURL(file);
});
const createImage: any = (url: any) =>
new Promise((resolve: any, reject: any) => {
const image: any = new Image();
image.addEventListener('load', () => resolve(image));
image.addEventListener('error', (error: any) => reject(error));
image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
image.src = url;
});
const getRadianAngle: any = (degreeValue: any) => (degreeValue * Math.PI) / 180;
const ORIENTATION_TO_ANGLE: any = {
3: 180,
6: 90,
8: -90,
};
/**
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
* #param {File} image - Image File url
* #param {Object} pixelCrop - pixelCrop Object provided by react-easy-crop
* #param {number} rotation - optional rotation parameter
*/
const getCroppedImg: any = async (imageSrc: any, pixelCrop: any, rotation: any = 0) => {
const image: any = await createImage(imageSrc);
const canvas: any = document.createElement('canvas');
const ctx: any = canvas.getContext('2d');
const maxSize: any = Math.max(image.width, image.height);
const safeArea: any = 2 * ((maxSize / 2) * Math.sqrt(2));
// set each dimensions to double largest dimension to allow for a safe area for the
// image to rotate in without being clipped by canvas context
canvas.width = safeArea;
canvas.height = safeArea;
// translate canvas context to a central location on image to allow rotating around the center.
ctx.translate(safeArea / 2, safeArea / 2);
ctx.rotate(getRadianAngle(rotation));
ctx.translate(-safeArea / 2, -safeArea / 2);
// draw rotated image and store data.
ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5);
const data: any = ctx.getImageData(0, 0, safeArea, safeArea);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image with correct offsets for x,y crop values.
ctx.putImageData(data, Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y));
// As Base64 string
// return canvas.toDataURL('image/jpeg');
// As a blob
return new Promise((resolve: any) => {
canvas.toBlob((file: any) => {
resolve(file);
}, 'image/jpeg');
});
};
const getRotatedImage: any = async (imageSrc: any, rotation: number = 0) => {
const image: any = await createImage(imageSrc);
const canvas: any = document.createElement('canvas');
const ctx: any = canvas.getContext('2d');
const orientationChanged: boolean = rotation === 90 || rotation === -90 || rotation === 270 || rotation === -270;
if (orientationChanged) {
canvas.width = image.height;
canvas.height = image.width;
} else {
canvas.width = image.width;
canvas.height = image.height;
}
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate((rotation * Math.PI) / 180);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
return new Promise((resolve: any) => {
canvas.toBlob((file: any) => {
resolve(URL.createObjectURL(file));
}, 'image/jpeg');
});
};
export const UploadSingleImage: FC<UploadSingleImageProps> = ({
setFieldValue,
setFieldTouched,
name,
extensionName,
width = '600',
height = '600',
errorMessage,
isDisabled,
fileId,
extension,
title,
validationRules,
isCoverPhoto,
customContainerCss,
onChange,
editIconName = 'edit',
onUploaded,
}: UploadSingleImageProps) => {
const [value, setValue]: [FileValue, React.Dispatch<SetStateAction<FileValue>>] = useState(null);
const [imgSrc, setImgSrc]: any = useState(null);
const [maxZoom, setMaxZoom]: any = useState(1);
const [rotation, setRotation]: any = useState(0);
const [crop, setCrop]: any = useState({ x: 0, y: 0 });
const [imageSendingFail, setImageSendingFail]: [boolean, React.Dispatch<SetStateAction<boolean>>] = useState(true);
const [zoom, setZoom]: any = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels]: any = useState(null);
const showCroppedImage: any = useCallback(async () => {
try {
const cropedImage: any = await getCroppedImg(imgSrc, croppedAreaPixels, rotation);
Resizer.imageFileResizer(
cropedImage,
600,
600,
'JPEG',
100,
0,
(file: any) => {
onChange(file, setValue);
dispatch(HideModalAC(MODAL_NAMES.IMAGE_CROP_MODAL));
setImgSrc(null);
},
'blob'
);
} catch (e) {
console.error(e);
}
}, [imgSrc, croppedAreaPixels, rotation]);
const imageInput: React.MutableRefObject<HTMLInputElement> = useRef();
const dispatch: Dispatch = useDispatch();
const toast: any = useCallback((toasts: any) => dispatch(SetToastsActionCreator(toasts)), [dispatch]);
const onCropComplete: any = useCallback((croppedArea: any, croppedAreaPixel: any) => {
setCroppedAreaPixels(croppedAreaPixel);
}, []);
const handleFileDrop: any = (e: any) => {
const files: any = e.dataTransfer.files;
if (files && files.length === 1) {
validateImage(files[0]);
}
};
const onFileChange: any = async (e: any) => {
if (e.target.files && e.target.files.length === 1) {
const file: any = e.target.files[0];
validateImage(file);
}
};
const onClick: any = (e: any) => {
setZoom(1);
e.target.value = '';
};
const validateImage: (file: File) => void = async (file: any) => {
setImageSendingFail(false);
// const imageDataUrl: any = await readFile(file);
// setImgSrc(imageDataUrl);
if (setFieldTouched) {
setFieldTouched(name);
}
if (validateFileType(toast, validationRules?.fileTypes, file)) {
let imageDataUrl: any = await readFile(file);
const img: any = createImage(imageDataUrl);
if (!validationRules || validateImg(toast, validationRules, img, file)) {
const orientation: any = await getOrientation(file);
const rotationPortion: any = ORIENTATION_TO_ANGLE[orientation];
if (rotation) {
imageDataUrl = await getRotatedImage(imageDataUrl, rotationPortion);
}
setImgSrc(imageDataUrl);
dispatch(ShowModalAC(MODAL_NAMES.IMAGE_CROP_MODAL));
} else {
imageInput.current.value = '';
setImageSendingFail(true);
}
}
};
useEffect(() => {
if (fileId && extension) {
setValue({ fileId, extension });
}
}, [fileId, extension]);
useEffect(() => {
if (setFieldValue) {
setFieldValue(name, value?.fileId);
setFieldValue(extensionName, value?.extension);
}
if (onUploaded && value?.fileId) {
onUploaded(name, value);
}
}, [value?.fileId, value?.extension]);
return (
<Flex justifyContent={'center'} alignItems={'center'} flexDirection={'column'} css={{ height: '100%' }} name={name}>
{imgSrc && (
<Modal bodyCss={{ padding: 0 }} bodyHeight='90%' width={'70%'} height={'85%'} borderRadius={'4px'} modalId={MODAL_NAMES.IMAGE_CROP_MODAL} headingText={'Resmi Düzenle'} type={MODAL_TYPES.NORMAL}>
<CropContainer>
<div style={{ width: '80%', height: '70%', position: 'relative' }}>
<Cropper
image={imgSrc}
crop={crop}
zoom={zoom}
rotation={rotation}
aspect={1 / 1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
restrictPosition={false}
onRotationChange={setRotation}
minZoom={0.5}
maxZoom={maxZoom}
onMediaLoaded={(imageSize: any) => {
if (imageSize.naturalWidth > 600) {
setMaxZoom(600 / Math.max(imageSize.height, imageSize.width));
} else {
setMaxZoom(1);
}
console.log(imageSize);
}}
/>
</div>
<ZoomAndApplyContainer>
<input type='range' value={zoom} min={0.5} max={maxZoom} step={0.05} onChange={(e: any) => setZoom(e.target.value)} />
<input type='range' value={rotation} min={0} max={360} step={10} onChange={(e: any) => setRotation(e.target.value)} />
<Button button={PRIMARY} onClick={showCroppedImage}>
Upload
</Button>
</ZoomAndApplyContainer>
</CropContainer>
</Modal>
)}
<DragAndDrop handleDrop={handleFileDrop}>
<ImageUploadContainer customContainerCss={customContainerCss} url={fileId && extension && `${fileId}${extension}`} width={width} height={height} errorMessage={errorMessage}>
{value?.fileId && value?.extension && isCoverPhoto && !isDisabled && (
<CoverPicEditComponent>
<label htmlFor={name}>
<Icon margin={'auto'} name={'upload-white'} />
<Text color={COLOR_NAMES.REMAX_WHITE} customWeight={1}>
Yeni kapak fotoğrafı yükle
</Text>
</label>
</CoverPicEditComponent>
)}
{value?.fileId && value?.extension && !isCoverPhoto && !isDisabled && (
<PicEditComponent className='edit-icon-container' htmlFor={name}>
<Icon name={editIconName} />
</PicEditComponent>
)}
{!(value?.fileId && value?.extension) && (
<Centered css={{ flexDirection: 'column' }}>
<Text customSize={[2, 3, 3]} lineHeight={1} customWeight={1} color={COLOR_NAMES.REMAX_TEXT_GREY_7f7f7f} css={{ textAlign: 'center', width: '145px', paddingBottom: '12px' }}>
{title}
</Text>
<label htmlFor={name}>
<Text customSize={[2, 3, 3]} customWeight={1} color={COLOR_NAMES.REMAX_BLUE_SELECTED_1451EF} textDecoration={'underline'}>
Dosya Seç
</Text>
</label>
</Centered>
)}
<input id={name} ref={imageInput} name={name} type='file' onChange={onFileChange} onClick={onClick} />
</ImageUploadContainer>
</DragAndDrop>
{/* Eğer resim yok ve upload işlemi fail olduysa hata mesajı basılsın */}
{!value?.fileId && imageSendingFail && errorMessage && <FormikFieldErrorMessage elipsis={true}>{errorMessage}</FormikFieldErrorMessage>}
</Flex>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
here is my solution for the problem: this callback inside the cropper
onMediaLoaded={(imageSize: any) => {
if (imageSize.naturalWidth > 600) {
setMaxZoom(600 / Math.max(imageSize.height, imageSize.width));
} else {
setMaxZoom(1);
}
}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
but this code doesn't ensure that I have a max zoom restriction that produces 600 x 600 in the end. (It works for some images but some others not. I would really appreciate any suggestion thanks in advance.
const onCropComplete: any = useCallback((croppedArea: any, croppedAreaPixel: any) => {
if(croppedAreaPixel.width < requiredWidth || croppedAreaPixel.height< requiredHeight){
/** do something here to disallow zoom
* this is a workaround
*/
}
setCroppedAreaPixels(croppedAreaPixel);
}, []);
I have an Ul component which contains an array of objects with different values in it. Each item called TestCase in this list has a button which makes a restcall and updates its object. However not all TestItems need to be updated. Only those whose button are clicked. The state of this array is stored in a parent continer component called TestCaseContainer. My button will updates the state accordingly to the effected TestItem in the array, however. This causes the whole list to rerender. How can I only have the changed TestItems rendered, instead of having the entire ul rendered every time an element is updated. I read about using useMemo so the component can memoize the passed down props, however I don't know how to implement this properly.
How can I stop all the rerenders?
Regression.js - Holds all the state
const Testing = forwardRef((props,ref) => {
const templateTestItem = {id:0,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
const testCaseRef = useRef()
const [isRun, setIsRun] = useState(false)
const [testItems, setTestItems] = useState([ templateTestItem])
const [stats,setStats] = useState(null)
const addTestItem = () => {
const newIndex = testItems.length
// console.log(newIndex)
const templateTestItem = {id:newIndex,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
setTestItems([...testItems, templateTestItem])
}
const addUploadCases = (cases) => {
setTestItems([])
const UploadedItems = cases.map((item,index)=>{
return{
id:index,
formData:{
date:item['date'],
env:item['env'],
assetClass:item['asset_class'],
metric:item['metric'],
nodeLevel:item['node_level'],
nodeName:item['node_name'],
testName:item['test_name'],
dataType:item['dataType'],
tradeId:item['tradeId']
},
results:[]
}
})
setTestItems(UploadedItems)
}
const runAllTests = () => {
testCaseRef.current.runAll()
}
const clearTestCases = () => {
// console.log('Clear Test cases')
setTestItems([])
if (testItems.length == 0) {
setTestItems([templateTestItem])
}
}
const extractAllResults =()=>{
testCaseRef.current.extractAllResults()
}
const updateTestResults = useCallback( (result, index) => {
console.log('Index:', index)
setTestItems(prevObjs=>(prevObjs.map((item)=>{
let updatedItem = { ...item, results: result }
if(item.id==index) return updatedItem
return item
})))
},[])
return (
<div style={{ 'backgroundColor': '#1b2829', 'display': 'flex', }} className={styles.dashboard}>
<Grid>
<Row stretched style={{}} className={styles.buttonConsole}>
{<ButtonConsole addTest={addTestItem} addUploadCases={addUploadCases} runAllTests={runAllTests} clearTestCases={clearTestCases} extractAllResults={extractAllResults} />}
</Row>
<Row centered>
<TestRunStats stats={stats}/>
</Row>
<Row style={{ 'display': 'flex', 'flex-direction': 'column' }} ><TestCaseContainer countTestRunStats={countTestRunStats} updateTestResults={updateTestResults} isRun={isRun} ref={testCaseRef} testItems={testItems} /> </Row>
{/*
<Row></Row>
<Row></Row> */}
</Grid>
</div>
);
})
TestContainer.js
const TestCaseContainer = forwardRef((props, ref) => {
const testCaseRef = useRef([])
useImperativeHandle(ref, () => ({
extractAllResults: async () => {
const data = {
data:[],
summary:[]
}
testCaseRef.current.forEach(async (item, index) => {
try {
const workbook = item.extractAllResults()
const summary = workbook['summary']
workbook['data'].forEach(testData => {
data['data'].push(testData)
})
data['summary'].push(summary)
} catch (err) {
console.log(err)
}
})
await axios.post('http://localhost:9999/api/downloadresults', data).then(res => {
console.log('res', res)
const byteCharacters = atob(res.data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'application/vnd.ms-excel' });
saveAs(blob, 'TestResults.xlsx')
})
},
runAll: () => {
testCaseRef.current.forEach(async (item, index) => {
await item.runAll()
})
}
}));
const runTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
let testKey = key
console.log('FEtCHING', testKey)
try {
const params = {
nodeName,
date,
env,
nodeLevel,
assetClass,
metric,
dataType,
tradeId,
testName
}
const endpoint ={
sensitivities:'sensitivities'
}
if (metric == 'DELTA_SENSITIVITIES') {
const result = await axios.get('example.net/api/sensitivities', { params, }).then(response => {
console.log('response.data', response.data)
return response.data
})
if (result.data == 'none') {
toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
} else if (result.data != 'none') {
// setTestResult(result)
props.updateTestResults(result, testKey)
// updateTestResults(false,testKey,'isLoading')
toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
// setTestResult(result.data)
} else {
await axios.get(`http://localhost:9999/api/metric/${metric}`, { params, }).then(response => {
if (response.data != 'none') {
props.updateTestResults(response.data, testKey)
toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
} else {
toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
})
}
} catch (error) {
toast.error(`${date}-${metric}-${nodeName} failed queried! -${error
}`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
}
return (
<Segment style={{ 'display': 'flex', 'width': 'auto', 'height': '100vh' }} className={styles.testCaseContainer}>
<div style={{ 'display': 'flex', }}>
</div>
<ul style={{overflowY:'auto',height:'100%'}} className='testItemContainer'>
{
// memoTestTwo
// testList
props.testItems.map((item, index) => {
let testName
if (item['formData']['testName'] == '') {
testName = `testRun-${index}`
} else {
testName = item['formData']['testName']
}
return <TestCase testResult={item['results']} runTestCase={runTestCase} isRun={props.isRun} ref={el => (testCaseRef.current[index] = el)} testKey={index} key={index} date={item['formData']['date']} env={item['formData']['env']} assetClass={item['formData']['assetClass']} metric={item['formData']['metric']} nodeLevel={item['formData']['nodeLevel']} nodeName={item['formData']['nodeName']} testName={testName} dataType={item['formData']['dataType']} tradeId={item['formData']['tradeId']} hierarchy={hierarchy} />
})
}
</ul>
</Segment>
)
})
TestCase.js - the individual item rendered from mapping!
const TestCase = forwardRef((props, ref) => {
const [isLoading, setIsLoading] = useState(false)
const inputRefs = useRef()
const outputRefs = useRef()
useImperativeHandle(ref, () => ({
extractAllResults: () => {
return outputRefs.current.extractAllResults();
},
runAll: () => {
inputRefs.current.runAll()
},
}));
const runSingleTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
setIsLoading(true)
await props.runTestCase(date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key)
setIsLoading(false)
}
const convertDate = (date) => {
if (date) {
const newDate = date.split('/')[2] + '-' + date.split('/')[0] + '-' + date.split('/')[1]
return newDate
} else {
return date
}
}
return (
<Segment color='green' style={{ 'display': 'flex', 'flexDirection': 'column', }}>
<div style={{ 'display': 'flex', 'justify-content': 'space-between' }}>
<div style={{ 'display': 'flex', 'height': '30px' }}>
<Button
// onClick={props.deleteSingleTest(props.testKey)}
icon="close"
inverted
size="tiny"
color='red'
></Button>
</div>
<RegressionInput runSingleTestCase={runSingleTestCase} isRun={props.isRun} testKey={props.testKey} ref={inputRefs} nodeNames={props.hierarchy} runTestCase={props.runTestCase} date={convertDate(props.date)} testName={props.testName} env={props.env} assetClass={props.assetClass} metric={props.metric} nodeLevel={props.nodeLevel} nodeName={props.nodeName} dataType={props.dataType} tradeId={props.tradeId} />
<TestCheck pass={props.testResult ? props.testResult['CHECK'] : null} />
</div>
{
isLoading ? (<Loading type={'circle'} style={{ 'display': 'flex', 'flexDirecton': 'column', 'justify-content': 'center', 'align-items': 'center', 'marginTop': '50' }} inline />) : (
<RegressionOutput ref={outputRefs} testName={props.testName} testResult={props.testResult} />
)
}
</Segment>
)
})
This article might help you understand React rendering behavior better:
Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior
React's default behavior is that when a parent component renders, React will recursively render all child components inside of it!
To change that behavior you can wrap some of your components in React.memo(). So React will do a shallow compare on the props object and only re-render that if one of the top level properties of the props object has changed.
That's not always possible or recommended, especially if you are using props.children.
const TestItem = React.memo(({id,value}) => {
console.log(`Rendering TestItem ${id}...`);
return(
<div>TestItem {id}. Value: {value}</div>
);
});
const App = () => {
console.log("Rendering App...");
const [items,setItems] = React.useState([
{ id: 1, value: "INITIAL VALUE" },
{ id: 2, value: "INITIAL VALUE" },
{ id: 3, value: "INITIAL VALUE" },
]);
const testItems = items.map((item,index) =>
<TestItem key={index} id={item.id} value={item.value}/>
);
const updateTest = (index) => {
console.clear();
setItems((prevState) => {
const newArray = Array.from(prevState);
newArray[index].value = "NEW VALUE";
return newArray
});
};
return(
<React.Fragment>
<div>App</div>
<button onClick={()=>{updateTest(0)}}>Update Test 1</button>
<button onClick={()=>{updateTest(1)}}>Update Test 2</button>
<button onClick={()=>{updateTest(2)}}>Update Test 3</button>
<div>
{testItems}
</div>
</React.Fragment>
);
};
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Without the React.memo() call. Every re-render of the App component would trigger a re-render in all of the TestItem component that it renders.
I am using react-input-trigger library https://github.com/abinavseelan/react-input-trigger, to implement #mentions functionality (like twitter).
What I want to happen is for the list of available options to display when '#' is pressed.
What is currently happening is that I have to type '#' plus a character before the list will display. It is so close to what I need but I can't for the life of me figure out this part...!
This is my first question on stack overflow and I tried really hard to find an answer to this question but I couldn't find anything that solved this particular issue, apologies if I missed something.
My current code looks like this:
import React, { Component } from 'react';
import InputTrigger from 'react-input-trigger';
import { TextArea, Dropdown, Step, H3 } from "../../styled-components/styled-components"
class CreateMessage extends Component {
state = {
top: null,
left: null,
showSuggestor: false,
startPosition: null,
text: null,
currentSelection: 0,
textareaValue: "",
properties: [{name: "property1", type: "string"}, {name: "property2", type: "string"}, {name: "property3", type: "string"}, {name: "property4", type: "string"}]
}
toggleSuggestor = (metaInformation) => {
const { hookType, cursor } = metaInformation;
if (hookType === 'start') {
this.setState({
showSuggestor: true,
left: cursor.left,
top: cursor.top + 20,
startPosition: cursor.selectionStart,
});
}
if (hookType === 'cancel') {
this.setState({
showSuggestor: false,
left: null,
top: null,
text: null,
startPosition: null,
});
}
}
handleInput = (metaInformation) => {
this.setState({ text: metaInformation.text });
}
handleKeyDown = (event) => {
const { which } = event;
const { currentSelection } = this.state;
const keyInputs = {
down: 40,
up: 38,
enter: 13
}
let properties = this.state.properties.map(( p ) => p)
if (which === keyInputs.down ) {
event.preventDefault();
this.setState({
currentSelection: (currentSelection + 1) % properties.length,
});
}
if (which === keyInputs.up ) { // 40 is the character code of the up arrow
event.preventDefault();
this.setState({
currentSelection: (currentSelection - 1) % properties.length,
});
}
if (which === keyInputs.enter) { // 13 is the character code for enter
event.preventDefault();
const { currentSelection, startPosition, textareaValue } = this.state;
const property = properties[currentSelection];
const newText = `${textareaValue.slice(0, startPosition - 1)}{ ${property.name} }${textareaValue.slice(startPosition + property.name.length, textareaValue.length)}`
this.setState({
showSuggestor: false,
left: null,
top: null,
text: null,
startPosition: null,
textareaValue: newText,
});
this.endHandler();
}
}
handleTextareaInput = (event) => {
const { value } = event.target;
this.setState({ textareaValue: value })
}
render() {
let properties = this.state.properties.map(( p ) => p )
return (
<Step>
<H3>Enter Message:</H3>
<div style={{ position: 'relative' }} id="message-container" onKeyDown={this.handleKeyDown}>
<InputTrigger
trigger={{ keyCode: 50, shiftKey: true }}
onStart={(metaData) => { this.toggleSuggestor(metaData); }}
onCancel={(metaData) => { this.toggleSuggestor(metaData); }}
onType={(metaData) => { this.handleInput(metaData); }}
endTrigger={(endHandler) => { this.endHandler = endHandler; }}
>
<TextArea id="event-message" onChange={this.handleTextareaInput} value={this.state.textareaValue} />
</InputTrigger>
<Dropdown id="dropdown" style={{ display: this.state.showSuggestor ? "block" : "none", top: this.state.top, left: this.state.left }}>
{
properties
.filter(property => property.name.indexOf(this.state.text) !== -1)
.map((property, index) => (
<div
key={index}
className='dropdown-item'
style={{ padding: '10px 20px', background: index === this.state.currentSelection ? '#eee' : '' }}
>
{ property.name }
</div>
))
}
</Dropdown>
</div>
</Step>
);
}
}
export default CreateMessage;