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",