diff --git a/components/DropDownMenu.js b/components/DropDownMenu.js index c31cc1c3..b86bfb73 100644 --- a/components/DropDownMenu.js +++ b/components/DropDownMenu.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import { Button } from './Button'; import { Icon } from './Icon'; -import { Text } from './Text'; +import { Text, Title } from './Text'; import { View } from './View'; import { TouchableOpacity } from './TouchableOpacity'; @@ -51,7 +51,14 @@ class DropDownMenu extends Component { * Prop definition overrides style. */ visibleOptions: React.PropTypes.number, - style: React.PropTypes.object, + /** + * Header property + */ + header: React.PropTypes.string, + style: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), }; static DEFAULT_VISIBLE_OPTIONS = 8; @@ -144,7 +151,12 @@ class DropDownMenu extends Component { return selectedOption ? ( @@ -185,7 +197,7 @@ class DropDownMenu extends Component { render() { const { collapsed } = this.state; - const { titleProperty, options, style } = this.props; + const { titleProperty, options, style, header, } = this.props; const button = this.renderSelectedOption(); if (_.size(options) === 0 || !button) { @@ -206,6 +218,7 @@ class DropDownMenu extends Component { + {header && {header}} { + return ( + + ); + }, + } + + timingDriver = new TimingDriver(); + imageRefs = new Map(); + + constructor(props) { + super(props); + this.renderPage = this.renderPage.bind(this); + this.onIndexSelected = this.onIndexSelected.bind(this); + this.collapseDescription = this.collapseDescription.bind(this); + this.expandDescription = this.expandDescription.bind(this); + this.onDescriptionScroll = this.onDescriptionScroll.bind(this); + this.renderTitle = this.renderTitle.bind(this); + this.renderDescription = this.renderDescription.bind(this); + this.onViewTransformed = this.onViewTransformed.bind(this); + this.onSingleTapConfirmed = this.onSingleTapConfirmed.bind(this); + this.resetSurroundingImageTransformations = this.resetSurroundingImageTransformations.bind(this); + this.getImageTransformer = this.getImageTransformer.bind(this); + this.updateImageSwitchingStatus = this.updateImageSwitchingStatus.bind(this); + this.setImagePreviewMode = this.setImagePreviewMode.bind(this); + this.setGalleryMode = this.setGalleryMode.bind(this); + this.state = { + selectedIndex: this.props.selectedIndex || 0, + collapsed: true, + mode: IMAGE_GALLERY_MODE, + imageSwitchingEnabled: true, + }; + } + + onIndexSelected(newIndex) { + const { onIndexSelected } = this.props; + this.setState({ + selectedIndex: newIndex, + }, () => { + if (_.isFunction(onIndexSelected)) { + onIndexSelected(newIndex); + } + InteractionManager.runAfterInteractions(() => { + // After swipe interaction finishes, we'll have new selected index in state + // And we're resetting surrounding image transformations, + // So that images aren't left zoomed in when user swipes to next/prev image + this.resetSurroundingImageTransformations(); + }); + }); + } + + getSelectedIndex() { + const { selectedIndex } = this.state; + + return selectedIndex; + } + + collapseDescription() { + LayoutAnimation.easeInEaseOut(); + this.setState({ collapsed: false }); + } + + expandDescription() { + LayoutAnimation.easeInEaseOut(); + this.setState({ collapsed: true }); + } + + onDescriptionScroll(event) { + const { collapsed } = this.state; + const offsetY = event.nativeEvent.contentOffset.y; + if (offsetY > 0 && collapsed) { + this.collapseDescription(); + } + if (offsetY < 0 && !collapsed) { + this.expandDescription(); + } + } + + updateImageSwitchingStatus() { + const { imageSwitchingEnabled, selectedIndex } = this.state; + + const imageTransformer = this.getImageTransformer(selectedIndex); + if (!imageTransformer) { + return; + } + + const translationSpace = imageTransformer.getAvailableTranslateSpace(); + if (!translationSpace) { + return; + } + + const imageBoundaryReached = (translationSpace.right <= 0 || translationSpace.left <= 0); + + if (imageSwitchingEnabled !== imageBoundaryReached) { + // We want to allow switching between gallery images only if + // the image is at its left of right boundary. This happens if the + // image is fully zoomed out, or if the image is zoomed in but the + // user moved it to one of its boundaries. + this.setState({ + imageSwitchingEnabled: imageBoundaryReached, + }); + } + } + + setImagePreviewMode() { + const { onModeChanged } = this.props; + + this.setState({ mode: IMAGE_PREVIEW_MODE }); + + this.timingDriver.runTimer(1, () => { + if (_.isFunction(onModeChanged)) { + onModeChanged(IMAGE_PREVIEW_MODE); + } + }); + } + + setGalleryMode() { + const { onModeChanged } = this.props; + + this.setState({ mode: IMAGE_GALLERY_MODE }); + + this.timingDriver.runTimer(0, () => { + if (_.isFunction(onModeChanged)) { + onModeChanged(IMAGE_GALLERY_MODE); + } + }); + } + + onViewTransformed(event) { + const { mode } = this.state; + + if (event.scale > 1.0 && mode === IMAGE_GALLERY_MODE) { + // If controls are visible and image is transformed, + // We should switch to image preview mode + this.setImagePreviewMode(); + } else if (mode === IMAGE_PREVIEW_MODE) { + this.updateImageSwitchingStatus(); + } + } + + onSingleTapConfirmed() { + const { mode } = this.state; + + if (mode === IMAGE_PREVIEW_MODE) { + // If controls are not visible and user taps on image + // We should switch to gallery mode + this.setGalleryMode(); + } else { + this.setImagePreviewMode(); + } + } + + renderDescription(description) { + const { collapsed } = this.state; + const { style } = this.props; + + if (!description) return; + + const descriptionIcon = collapsed ? : ; + + const descriptionText = ( + + {description} + + ); + + return ( + + + {description.length >= DESCRIPTION_LENGTH_TRIM_LIMIT ? descriptionIcon : null} + + + {descriptionText} + + + ); + } + + renderTitle(title) { + const { style } = this.props; + + return ( + + {title} + + ); + } + + getImageTransformer(page) { + const { data } = this.props; + if (page >= 0 && page < data.length) { + const ref = this.imageRefs.get(page); + if (ref) { + return ref.getViewTransformerInstance(); + } + } + return null; + } + + resetImageTransformer(transformer) { + transformer.updateTransform({ scale: 1, translateX: 0, translateY: 0 }); + } + + resetSurroundingImageTransformations() { + const { selectedIndex } = this.state; + let transformer = this.getImageTransformer(selectedIndex - 1); + if (transformer) { + this.resetImageTransformer(transformer); + } + transformer = this.getImageTransformer(selectedIndex + 1); + if (transformer) { + this.resetImageTransformer(transformer); + } + } + + renderPage(page, pageId) { + const { style } = this.props; + const { imageSwitchingEnabled } = this.state; + const image = _.get(page, 'source.uri'); + const title = _.get(page, 'title'); + const description = _.get(page, 'description'); + + if (!image) { + return; + } + + return ( + + { this.imageRefs.set(pageId, ref); })} + /> + { this.renderTitle(title) } + { this.renderDescription(description) } + + ); + } + + render() { + const { data, renderOverlay, renderPlaceholder, style } = this.props; + const { imageSwitchingEnabled, selectedIndex } = this.state; + + return ( + + + + ); + } +} + +const StyledImageGallery = connectStyle('shoutem.ui.ImageGallery')(ImageGallery); + +export { + StyledImageGallery as ImageGallery, +}; diff --git a/components/ImagePreview.js b/components/ImagePreview.js index 47618a94..3486fa82 100644 --- a/components/ImagePreview.js +++ b/components/ImagePreview.js @@ -21,7 +21,10 @@ const propTypes = { width: PropTypes.number, height: PropTypes.number, source: Image.propTypes.source, - style: PropTypes.object, + style: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]), }; const CLOSE_ICON_NAME = 'clear'; diff --git a/components/InlineGallery.js b/components/InlineGallery.js index bee3748e..ccd122eb 100644 --- a/components/InlineGallery.js +++ b/components/InlineGallery.js @@ -27,7 +27,10 @@ class InlineGallery extends Component { // Initially selected page in gallery selectedIndex: PropTypes.number, // Style, applied to Image component - style: PropTypes.object, + style: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]), // Prop that reduces page size by pageMargin, allowing 'sneak peak' of next page // Defaults to false showNextPage: PropTypes.bool, diff --git a/components/ListView.js b/components/ListView.js index a4c86bc1..3f8c01e2 100644 --- a/components/ListView.js +++ b/components/ListView.js @@ -66,9 +66,14 @@ class ListDataSource { class ListView extends React.Component { static propTypes = { autoHideHeader: React.PropTypes.bool, - style: React.PropTypes.object, - data: React.PropTypes.array, + style: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), + data: React.PropTypes.oneOfType([ + React.PropTypes.object, React.PropTypes.array]), loading: React.PropTypes.bool, + rowHasChanged: React.PropTypes.func, onLoadMore: React.PropTypes.func, onRefresh: React.PropTypes.func, getSectionId: React.PropTypes.func, @@ -77,6 +82,7 @@ class ListView extends React.Component { renderFooter: React.PropTypes.func, renderSectionHeader: React.PropTypes.func, scrollDriver: React.PropTypes.object, + onEndReachedThreshold: React.PropTypes.number, // TODO(Braco) - add render separator }; @@ -91,7 +97,7 @@ class ListView extends React.Component { this.listDataSource = new ListDataSource({ - rowHasChanged: (r1, r2) => r1 !== r2, + rowHasChanged: props.rowHasChanged || ((r1, r2) => r1 !== r2), sectionHeaderHasChanged: props.renderSectionHeader ? (s1, s2) => s1 !== s2 : undefined, getSectionHeaderData: (dataBlob, sectionId) => props.getSectionId(dataBlob[sectionId][0]), }, props.getSectionId); @@ -147,7 +153,7 @@ class ListView extends React.Component { // configuration // default load more threshold - mappedProps.onEndReachedThreshold = 40; + mappedProps.onEndReachedThreshold = props.onEndReachedThreshold || 40; // React native warning // NOTE: In react 0.23 it can't be set to false mappedProps.enableEmptySections = true; diff --git a/components/NavigationBar/NavigationBar.js b/components/NavigationBar/NavigationBar.js index b2cc42f2..effc8e54 100644 --- a/components/NavigationBar/NavigationBar.js +++ b/components/NavigationBar/NavigationBar.js @@ -58,7 +58,11 @@ class NavigationBar extends Component { leftComponent: React.PropTypes.node, centerComponent: React.PropTypes.node, rightComponent: React.PropTypes.node, - style: React.PropTypes.object, + style: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), + customStyle: React.PropTypes.object, id: React.PropTypes.string, }; @@ -72,6 +76,7 @@ class NavigationBar extends Component { rightComponent, centerComponent, style, + customStyle = {}, id, } = this.props; @@ -79,12 +84,12 @@ class NavigationBar extends Component { setStatusBarStyle(backgroundColor); // Key must be set to render new screen NavigationBar return ( - + - - {leftComponent} - {centerComponent} - {rightComponent} + + {leftComponent} + {centerComponent} + {rightComponent} ); diff --git a/components/PageIndicators.js b/components/PageIndicators.js index ff401ee7..194e6a34 100644 --- a/components/PageIndicators.js +++ b/components/PageIndicators.js @@ -20,7 +20,10 @@ class PageIndicators extends Component { // will be rendered. Defaults to 10 maxCount: PropTypes.number, // Style prop used to override default (theme) styling - style: PropTypes.object, + style: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]), }; static defaultProps = { diff --git a/components/RichMedia/RichMedia.js b/components/RichMedia/RichMedia.js index 3abc24b3..31b469b3 100644 --- a/components/RichMedia/RichMedia.js +++ b/components/RichMedia/RichMedia.js @@ -104,7 +104,10 @@ RichMedia.defaultProps = { RichMedia.propTypes = { body: PropTypes.string, onError: PropTypes.func, - style: PropTypes.object, + style: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]), openUrl: PropTypes.func, renderElement: PropTypes.func, renderText: PropTypes.func, diff --git a/components/Spinner.js b/components/Spinner.js index 9f35b168..43e6d043 100644 --- a/components/Spinner.js +++ b/components/Spinner.js @@ -21,7 +21,10 @@ function Spinner({ style }) { } Spinner.propTypes = { - style: React.PropTypes.object, + style: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), }; const StyledSpinner = connectStyle('shoutem.ui.Spinner', { diff --git a/components/TextInput.js b/components/TextInput.js index 527148c9..68f7b13e 100644 --- a/components/TextInput.js +++ b/components/TextInput.js @@ -26,7 +26,10 @@ class TextInput extends Component { TextInput.propTypes = { ...RNTextInput.propTypes, - style: React.PropTypes.object, + style: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), }; const AnimatedTextInput = connectAnimation(TextInput); diff --git a/components/Video/Video.js b/components/Video/Video.js index e090909a..6f308c42 100644 --- a/components/Video/Video.js +++ b/components/Video/Video.js @@ -19,7 +19,10 @@ const propTypes = { source: PropTypes.shape({ uri: PropTypes.string, }), - style: PropTypes.object, + style: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]), }; const defaultProps = { diff --git a/package.json b/package.json index 5170b3c4..acf6f6c9 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "@shoutem/ui", - "version": "0.12.0", + "version": "0.12.0-lug", "description": "Styleable set of components for React Native applications", "dependencies": { "@shoutem/animation": "^0.10.1", - "@shoutem/theme": "^0.9.0", + "@shoutem/theme": "git+https://github.com/Lughino/theme.git#0.9.0-lug", "babel-plugin-transform-decorators-legacy": "^1.3.4", "buffer": "^4.5.1", "events": "1.1.0",