├── .editorconfig
├── .eslintignore
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── __tests__
├── View-test.js
└── __snapshots__
│ └── View-test.js.snap
├── assets
├── animations
│ └── loadingDots.json
├── images
│ ├── emptyList.svg
│ ├── image-fallback.png
│ ├── pin-dark@3x.png
│ ├── pin-light@3x.png
│ └── transparent.png
└── index.js
├── babel.config.js
├── components
├── ActionSheet
│ ├── ActionSheet.js
│ ├── ActionSheetOption.js
│ └── index.js
├── AnimatedScrollingText.js
├── Button.js
├── Card.js
├── CategoryPicker
│ ├── Category.js
│ ├── CategoryPicker.js
│ ├── index.js
│ └── shapes.js
├── DateTimePicker.js
├── Divider.js
├── DropDownMenu
│ ├── DropDownMenu.js
│ ├── DropDownModal.js
│ └── index.js
├── EmptyListImage.js
├── EmptyStateView.js
├── FormGroup.js
├── GridRow.js
├── HorizontalPager
│ ├── HorizontalPager.js
│ ├── Page.js
│ └── index.js
├── Icon
│ ├── Icon.js
│ ├── Icon.web.js
│ ├── assets
│ │ ├── about.svg
│ │ ├── activity.svg
│ │ ├── add-event.svg
│ │ ├── add-friend.svg
│ │ ├── add-to-cart.svg
│ │ ├── add-to-favorites-off.svg
│ │ ├── add-to-favorites-on.svg
│ │ ├── address.svg
│ │ ├── attach-media.svg
│ │ ├── back.svg
│ │ ├── books.svg
│ │ ├── call.svg
│ │ ├── cam-switch.svg
│ │ ├── camera.svg
│ │ ├── cart.svg
│ │ ├── checkbox-off.svg
│ │ ├── checkbox-on.svg
│ │ ├── checkbox-rectangle-off.svg
│ │ ├── checkbox-rectangle-on.svg
│ │ ├── checkmark-oval.svg
│ │ ├── checkmark-round.svg
│ │ ├── clear-text.svg
│ │ ├── clock.svg
│ │ ├── close.svg
│ │ ├── comment.svg
│ │ ├── comments.svg
│ │ ├── deals.svg
│ │ ├── delete.svg
│ │ ├── directions.svg
│ │ ├── down-arrow.svg
│ │ ├── download.svg
│ │ ├── drop-down.svg
│ │ ├── edit.svg
│ │ ├── email.svg
│ │ ├── equalizer.svg
│ │ ├── error-rectangle.svg
│ │ ├── error.svg
│ │ ├── events.svg
│ │ ├── exit-to-app.svg
│ │ ├── eye-crossed.svg
│ │ ├── eye.svg
│ │ ├── facebook-logo.svg
│ │ ├── facebook.svg
│ │ ├── filters.svg
│ │ ├── folder.svg
│ │ ├── forward-10.svg
│ │ ├── forward-30.svg
│ │ ├── forward-5.svg
│ │ ├── forward.svg
│ │ ├── friends.svg
│ │ ├── full-screen.svg
│ │ ├── gallery.svg
│ │ ├── garbage-can.svg
│ │ ├── gift.svg
│ │ ├── github.svg
│ │ ├── hang-up.svg
│ │ ├── history.svg
│ │ ├── home.svg
│ │ ├── index.js
│ │ ├── instagram.svg
│ │ ├── laptop.svg
│ │ ├── left-arrow.svg
│ │ ├── like-heart.svg
│ │ ├── like.svg
│ │ ├── link.svg
│ │ ├── linkedin.svg
│ │ ├── location-pins.svg
│ │ ├── lock.svg
│ │ ├── loyalty-card.svg
│ │ ├── maps.svg
│ │ ├── mic-off.svg
│ │ ├── mic.svg
│ │ ├── minus-button.svg
│ │ ├── missing.svg
│ │ ├── more-horizontal.svg
│ │ ├── music-note.svg
│ │ ├── music-video.svg
│ │ ├── my-location.svg
│ │ ├── news.svg
│ │ ├── notifications.svg
│ │ ├── page.svg
│ │ ├── pause.svg
│ │ ├── photo.svg
│ │ ├── pin.svg
│ │ ├── play.svg
│ │ ├── playlist-play.svg
│ │ ├── plus-button.svg
│ │ ├── podcasts.svg
│ │ ├── products.svg
│ │ ├── queue.svg
│ │ ├── radio.svg
│ │ ├── radiobutton-off.svg
│ │ ├── radiobutton-on.svg
│ │ ├── receipt.svg
│ │ ├── refresh.svg
│ │ ├── repeat.svg
│ │ ├── replay-10.svg
│ │ ├── replay-30.svg
│ │ ├── replay-5.svg
│ │ ├── replay.svg
│ │ ├── restaurant-menu.svg
│ │ ├── restaurant.svg
│ │ ├── restore.svg
│ │ ├── right-arrow.svg
│ │ ├── rss-feed.svg
│ │ ├── rsvp.svg
│ │ ├── search.svg
│ │ ├── settings.svg
│ │ ├── share-android.svg
│ │ ├── share.svg
│ │ ├── sidebar.svg
│ │ ├── skip-next.svg
│ │ ├── sleep.svg
│ │ ├── social-wall.svg
│ │ ├── speed-meter.svg
│ │ ├── stamp.svg
│ │ ├── stop.svg
│ │ ├── take-a-photo.svg
│ │ ├── tiktok.svg
│ │ ├── trophy.svg
│ │ ├── turn-off.svg
│ │ ├── tweet.svg
│ │ ├── uber.svg
│ │ ├── unfriend.svg
│ │ ├── up-arrow.svg
│ │ ├── user-profile.svg
│ │ ├── users.svg
│ │ ├── video-cam-off.svg
│ │ ├── video-cam.svg
│ │ ├── video-chat.svg
│ │ ├── video-recording.svg
│ │ ├── web.svg
│ │ └── wheelchair.svg
│ ├── index.js
│ └── services
│ │ ├── icons.js
│ │ └── index.js
├── Image.js
├── ImageBackground.js
├── ImageGallery.js
├── ImageGalleryOverlay.js
├── ImagePreview.js
├── InlineDropDownMenu
│ ├── InlineDropDownMenu.js
│ ├── InlineDropDownMenuItem.js
│ └── index.js
├── InlineGallery.js
├── Lightbox.js
├── LinearGradient.js
├── ListView.js
├── LoadingContainer.js
├── LoadingIndicator.js
├── NumberInput.js
├── Overlay.js
├── PageIndicators.js
├── ProgressRings
│ ├── AnimatedProgressRing.js
│ ├── ProgressRing.js
│ ├── ProgressRings.js
│ ├── const.js
│ ├── index.js
│ └── svgCalculations.js
├── RNHapticFeedback
│ ├── index.js
│ └── index.web.js
├── Row.js
├── Screen.js
├── ScrollView
│ ├── ScrollDriverProvider.js
│ ├── ScrollView.js
│ └── index.js
├── SearchField.js
├── ShareButton.js
├── Spinner.js
├── Switch.js
├── TabMenu
│ ├── TabMenu.js
│ ├── TabMenuItem.js
│ ├── const.js
│ └── index.js
├── Text.js
├── TextInput.js
├── Tile.js
├── ToastMessage
│ ├── README.md
│ ├── components
│ │ ├── ActionToast.js
│ │ ├── BaseToast.js
│ │ ├── ErrorToast.js
│ │ ├── InfoToast.js
│ │ ├── SuccessToast.js
│ │ ├── ToastProgressBar.js
│ │ └── index.js
│ ├── index.js
│ └── toastMessage.js
├── Touchable.js
├── TouchableNativeFeedback.js
├── TouchableOpacity.js
├── Video
│ ├── Video.js
│ ├── VideoSourceReader.js
│ └── index.js
├── View.js
└── YearRangePicker
│ ├── YearRangePicker.js
│ ├── YearRangePickerButton.js
│ ├── YearRangePickerModal.js
│ └── index.js
├── const.js
├── fonts
├── OFL.txt
├── Rubik-Black.ttf
├── Rubik-BlackItalic.ttf
├── Rubik-Bold.ttf
├── Rubik-BoldItalic.ttf
├── Rubik-Italic.ttf
├── Rubik-Light.ttf
├── Rubik-LightItalic.ttf
├── Rubik-Medium.ttf
├── Rubik-MediumItalic.ttf
├── Rubik-Regular.ttf
└── rubicon-icon-font.ttf
├── helpers
├── device-selector.js
├── index.js
└── keyboard.js
├── hooks
├── index.js
└── useColorAndPercentageInterpolation.js
├── html
├── Html.js
├── README.md
├── components
│ ├── AttachmentRenderer.js
│ ├── Gallery.js
│ ├── IframeRenderer.js
│ ├── Image.js
│ ├── SimpleHtml.js
│ └── VideoRenderer.js
├── elements
│ ├── A.js
│ ├── Block.js
│ ├── Br.js
│ ├── Img.js
│ ├── Inline.js
│ ├── Text.js
│ ├── Video.js
│ ├── Virtual.js
│ └── list
│ │ ├── Li.js
│ │ ├── Ol.js
│ │ ├── Ul.js
│ │ ├── helpers
│ │ ├── pickLiChildElements.js
│ │ └── renderItems.js
│ │ ├── index.js
│ │ └── prefix
│ │ ├── Bullet.js
│ │ └── Number.js
├── index.js
└── services
│ ├── Dimensions.js
│ ├── DomVisitors.js
│ ├── ElementRegistry.js
│ ├── HTMLElementModels.js
│ ├── HtmlParser.js
│ ├── getEmptyObjectKeys.js
│ └── isValidVideoFormat.js
├── index.js
├── init.js
├── package-lock.json
├── package.json
├── scripts
└── add-native-deps.js
├── services
├── index.js
├── platform.js
├── unavailableInWeb.js
└── variableResolver.js
├── theme.js
├── web
└── style
│ └── Alert.scss
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | # All files
5 | [*]
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | charset = utf-8
9 |
10 | # JS files
11 | [*.js]
12 | indent_style = space
13 | indent_size = 2
14 |
15 | # JSON files
16 | [*.json]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | # Java files
21 | [*.java]
22 | indent_style = space
23 | indent_size = 4
24 |
25 | # Objective C files
26 | [*{.h,.m,.mm}]
27 | indent_style = space
28 | indent_size = 2
29 |
30 | [*.md]
31 | trim_trailing_whitespace = false
32 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/dist/*
2 | **/node_modules/*
3 | **/server.js
4 | **/webpack.config*.js
5 | **/test-utils/setup.js
6 | **/App.test.js
7 | **/__tests__/**
8 | **/_tests_/**
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | *.tgz
4 |
5 | # IDE files and folders
6 | .vscode
7 | jsconfig.json
8 |
9 | # Mac OS X folder attributes file
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 |
4 | # Ignore test files
5 | *_tests_
6 |
7 | # Ignore local/config files
8 | .editorconfig
9 | .npmignore
10 | .gitignore
11 |
12 | # Ignore previous builds
13 | *.tgz
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD License
2 |
3 | For @shoutem/ui software
4 |
5 | Copyright (c) 2016-present, Shoutem. All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions are met:
9 |
10 | * Redistributions of source code must retain the above copyright
11 | notice, this list of conditions and the following disclaimer.
12 |
13 | * Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in the
15 | documentation and/or other materials provided with the distribution.
16 |
17 | * Neither the name of the Shoutem nor the
18 | names of its contributors may be used to endorse or promote products
19 | derived from this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY
25 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
28 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shoutem UI
2 |
3 | Shoutem UI is a set of styleable components that enables you to build beautiful React Native applications for iOS and Android. All of our components are built to be both composable and [customizable](http://github.com/shoutem/theme). Each component has a predefined style that is compatible with the rest of the Shoutem UI, which makes it possible to build complex components that look great without the need to manually define complex styles.
4 |
5 | ## Install
6 |
7 | These instructions are valid for React Native 0.60.0 and higher. If you're running a lower version, please use v1.X.X.
8 |
9 | ```
10 | $ npm install --save @shoutem/ui
11 | ```
12 |
13 | We have a `postinstall` script which will add `@shoutem/ui`'s native dependencies to your root `package.json` in order to support autolinking and pod installation.
14 |
15 | Optional: Link the font files to your iOS and Android projects using `react-native-asset`:
16 |
17 | ```
18 | $ npx react-native-asset node_modules/@shoutem/ui/fonts
19 | ```
20 |
21 | Not doing this will result in dismissable red screen errors about unknown font names, as well as the default system fonts being used on iOS and Android when a Shoutem UI font is meant to be used, so we suggest running this step.
22 |
23 | ## Docs
24 |
25 | All the documentation is available on the [Developer portal](http://shoutem.github.io/docs/ui-toolkit/introduction).
26 |
27 | ## Community
28 |
29 | Join [our community](https://www.facebook.com/groups/shoutem.community/) on Facebook. Also, feel free to ask a question on Stack Overflow using ["shoutem" tag](http://stackoverflow.com/tags/shoutem).
30 |
31 | ## UI Toolkit
32 |
33 | Shoutem UI is a part of the [Shoutem UI Toolkit](https://shoutem.github.io/docs/ui-toolkit/introduction) that enables you to build professional looking React Native apps with ease.
34 |
35 | It consists of three libraries:
36 |
37 | - [@shoutem/ui](https://github.com/shoutem/ui): beautiful and customizable UI components
38 | - [@shoutem/theme](https://github.com/shoutem/theme): “CSS-way” of styling entire app
39 | - [@shoutem/animation](https://github.com/shoutem/animation): declarative way of applying ready-made animations
40 |
41 | ## License
42 |
43 | [The BSD License](https://opensource.org/licenses/BSD-3-Clause)
44 | Copyright (c) 2016-present, [Shoutem](http://shoutem.github.io)
45 |
--------------------------------------------------------------------------------
/__tests__/View-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { View } from '../components/View';
4 |
5 | test('View renders correctly', () => {
6 | const tree = renderer.create(
7 | ,
8 | ).toJSON();
9 |
10 | expect(tree).toMatchSnapshot();
11 | });
12 |
13 | test('View renders children vertically with vertical styleName', () => {
14 | const tree = renderer.create(
15 |
16 |
17 |
18 |
19 | );
20 | });
21 |
22 | test('View centers children horizontally with h-center styleName', () => {
23 | const tree = renderer.create(
24 |
25 |
26 |
27 |
28 | );
29 | });
30 |
31 | test('View renders children horizontally with horizontal styleName', () => {
32 | const tree = renderer.create(
33 |
34 |
35 |
36 |
37 | );
38 | });
39 |
40 | test('View centers children vertically with v-center styleName', () => {
41 | const tree = renderer.create(
42 |
43 |
44 |
45 |
46 | );
47 | });
48 |
49 | test('View fills parent with fill-parent styleName', () => {
50 | const tree = renderer.create(
51 |
52 |
53 |
54 | );
55 | });
56 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/View-test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`View renders correctly 1`] = `
4 |
15 | `;
16 |
--------------------------------------------------------------------------------
/assets/images/image-fallback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/image-fallback.png
--------------------------------------------------------------------------------
/assets/images/pin-dark@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/pin-dark@3x.png
--------------------------------------------------------------------------------
/assets/images/pin-light@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/pin-light@3x.png
--------------------------------------------------------------------------------
/assets/images/transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/transparent.png
--------------------------------------------------------------------------------
/assets/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | export const animations = {
3 | loadingDots: require('./animations/loadingDots.json'),
4 | };
5 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['module:metro-react-native-babel-preset'],
5 | plugins: ['@babel/plugin-proposal-class-properties'],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/components/ActionSheet/ActionSheet.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef } from 'react';
2 | import ActionSheetNative from 'react-native-actions-sheet';
3 | import _ from 'lodash';
4 | import PropTypes from 'prop-types';
5 | import { connectStyle } from '@shoutem/theme';
6 | import { View } from '../View';
7 | import ActionSheetOption, { optionPropType } from './ActionSheetOption';
8 |
9 | const ActionSheet = ({
10 | active = false,
11 | cancelOptions = undefined,
12 | confirmOptions = undefined,
13 | style,
14 | onDismiss = undefined,
15 | ...otherProps
16 | }) => {
17 | const actionSheetRef = useRef(null);
18 |
19 | const hasConfirmOptions = useMemo(() => !_.isEmpty(confirmOptions), [
20 | confirmOptions,
21 | ]);
22 | const hasCancelOptions = useMemo(() => !_.isEmpty(cancelOptions), [
23 | cancelOptions,
24 | ]);
25 |
26 | useEffect(() => {
27 | if (active) {
28 | actionSheetRef.current?.show();
29 | } else {
30 | actionSheetRef.current?.hide();
31 | }
32 | }, [active]);
33 |
34 | return (
35 |
42 |
43 | {hasConfirmOptions && (
44 |
45 | {_.map(confirmOptions, option => (
46 |
51 | ))}
52 |
53 | )}
54 | {hasCancelOptions && (
55 |
56 |
57 | {_.map(cancelOptions, option => (
58 |
64 | ))}
65 |
66 |
67 | )}
68 |
69 |
70 | );
71 | };
72 |
73 | ActionSheet.propTypes = {
74 | style: PropTypes.object.isRequired,
75 | active: PropTypes.bool,
76 | cancelOptions: PropTypes.arrayOf(optionPropType),
77 | confirmOptions: PropTypes.arrayOf(optionPropType),
78 | onDismiss: PropTypes.func,
79 | };
80 |
81 | ActionSheet.defaultProps = {
82 | active: false,
83 | cancelOptions: undefined,
84 | confirmOptions: undefined,
85 | onDismiss: undefined,
86 | };
87 |
88 | export default connectStyle('shoutem.ui.ActionSheet')(ActionSheet);
89 |
--------------------------------------------------------------------------------
/components/ActionSheet/ActionSheetOption.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connectStyle } from '@shoutem/theme';
4 | import { isIos } from '../../services';
5 | import { Text } from '../Text';
6 | import { TouchableOpacity } from '../TouchableOpacity';
7 |
8 | export const optionPropType = PropTypes.shape({
9 | title: PropTypes.string,
10 | onPress: PropTypes.func,
11 | });
12 |
13 | function ActionSheetOption({ style, option, cancelOption, nativeStyle }) {
14 | const { title, onPress } = option;
15 |
16 | const useIosTextColor = nativeStyle && isIos;
17 |
18 | return (
19 |
24 |
31 | {title}
32 |
33 |
34 | );
35 | }
36 |
37 | ActionSheetOption.propTypes = {
38 | style: PropTypes.object.isRequired,
39 | cancelOption: PropTypes.bool,
40 | nativeStyle: PropTypes.bool,
41 | option: optionPropType,
42 | };
43 |
44 | ActionSheetOption.defaultProps = {
45 | option: undefined,
46 | cancelOption: false,
47 | nativeStyle: false,
48 | };
49 |
50 | export default connectStyle('shoutem.ui.ActionSheetOption')(ActionSheetOption);
51 |
--------------------------------------------------------------------------------
/components/ActionSheet/index.js:
--------------------------------------------------------------------------------
1 | export { default as ActionSheet } from './ActionSheet';
2 |
--------------------------------------------------------------------------------
/components/Button.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { TouchableOpacity } from 'react-native';
3 | import { connectAnimation } from '@shoutem/animation';
4 | import { connectStyle } from '@shoutem/theme';
5 |
6 | class Button extends PureComponent {
7 | render() {
8 | const { style } = this.props;
9 | const { underlayColor, ...otherStyle } = style;
10 |
11 | return (
12 |
17 | );
18 | }
19 | }
20 |
21 | Button.propTypes = {
22 | ...TouchableOpacity.propTypes,
23 | };
24 |
25 | const AnimatedButton = connectAnimation(Button);
26 | const StyledButton = connectStyle('shoutem.ui.Button')(AnimatedButton);
27 | export { StyledButton as Button };
28 |
--------------------------------------------------------------------------------
/components/Card.js:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { connectAnimation } from '@shoutem/animation';
3 | import { connectStyle } from '@shoutem/theme';
4 |
5 | const AnimatedCard = connectAnimation(View);
6 | const Card = connectStyle('shoutem.ui.Card')(AnimatedCard);
7 |
8 | export { Card };
9 |
--------------------------------------------------------------------------------
/components/CategoryPicker/Category.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connectStyle } from '@shoutem/theme';
4 | import { Text } from '../Text';
5 | import { TouchableOpacity } from '../TouchableOpacity';
6 | import { categoryShape } from './shapes';
7 |
8 | function Category({ category, style, isSelected, onPress }) {
9 | const textStyle = useMemo(
10 | () => [style.category, !isSelected && style.selectedCategory],
11 |
12 | [isSelected, style.category, style.selectedCategory],
13 | );
14 |
15 | function handlePress() {
16 | if (isSelected) {
17 | return;
18 | }
19 |
20 | if (onPress) {
21 | onPress(category);
22 | }
23 | }
24 |
25 | return (
26 |
27 | {category.name}
28 |
29 | );
30 | }
31 |
32 | Category.propTypes = {
33 | category: categoryShape.isRequired,
34 | isSelected: PropTypes.bool,
35 | style: PropTypes.object,
36 | onPress: PropTypes.func,
37 | };
38 |
39 | Category.defaultProps = {
40 | style: {},
41 | isSelected: false,
42 | onPress: undefined,
43 | };
44 |
45 | export default React.memo(connectStyle('shoutem.ui.Category')(Category));
46 |
--------------------------------------------------------------------------------
/components/CategoryPicker/CategoryPicker.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { FlatList } from 'react-native';
3 | import _ from 'lodash';
4 | import PropTypes from 'prop-types';
5 | import { connectStyle } from '@shoutem/theme';
6 | import { View } from '../View';
7 | import Category from './Category';
8 | import { categoryShape } from './shapes';
9 |
10 | export function CategoryPicker({
11 | categories,
12 | onCategorySelected,
13 | style,
14 | selectedCategory,
15 | }) {
16 | const renderItem = useCallback(
17 | ({ item: category }) => (
18 |
24 | ),
25 | [selectedCategory.id, onCategorySelected],
26 | );
27 |
28 | if (_.size(categories) < 2) {
29 | return null;
30 | }
31 |
32 | return (
33 |
34 |
42 |
43 | );
44 | }
45 |
46 | CategoryPicker.propTypes = {
47 | categories: PropTypes.arrayOf(categoryShape),
48 | selectedCategory: categoryShape,
49 | style: PropTypes.object,
50 | onCategorySelected: PropTypes.func,
51 | };
52 |
53 | CategoryPicker.defaultProps = {
54 | categories: [],
55 | style: {},
56 | selectedCategory: undefined,
57 | onCategorySelected: undefined,
58 | };
59 |
60 | export default React.memo(
61 | connectStyle('shoutem.ui.CategoryPicker')(CategoryPicker),
62 | );
63 |
--------------------------------------------------------------------------------
/components/CategoryPicker/index.js:
--------------------------------------------------------------------------------
1 | export { default as CategoryPicker } from './CategoryPicker';
2 |
--------------------------------------------------------------------------------
/components/CategoryPicker/shapes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export const categoryShape = PropTypes.shape({
4 | id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
5 | name: PropTypes.string.isRequired,
6 | description: PropTypes.string,
7 | });
8 |
--------------------------------------------------------------------------------
/components/Divider.js:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { connectAnimation } from '@shoutem/animation';
3 | import { connectStyle } from '@shoutem/theme';
4 |
5 | const AnimatedDivider = connectAnimation(View);
6 | const Divider = connectStyle('shoutem.ui.Divider')(AnimatedDivider);
7 |
8 | export { Divider };
9 |
--------------------------------------------------------------------------------
/components/DropDownMenu/index.js:
--------------------------------------------------------------------------------
1 | export { DropDownMenu } from './DropDownMenu';
2 | export { DropDownModal } from './DropDownModal';
3 |
--------------------------------------------------------------------------------
/components/EmptyListImage.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import autoBindReact from 'auto-bind/react';
3 | import _ from 'lodash';
4 | import PropTypes from 'prop-types';
5 | import { connectStyle } from '@shoutem/theme';
6 | import { Text, Title, View } from '@shoutem/ui';
7 | import EmptyList from '../assets/images/emptyList.svg';
8 |
9 | class EmptyListImage extends PureComponent {
10 | constructor(props) {
11 | super(props);
12 |
13 | autoBindReact(this);
14 | }
15 |
16 | resolveImage() {
17 | const { image } = this.props;
18 |
19 | if (image && !_.isFunction(image)) {
20 | // eslint-disable-next-line no-console
21 | console.warn(`Image must be an SVG file imported as a React component.`);
22 | } else if (image && _.isFunction(image)) {
23 | return image;
24 | }
25 |
26 | return EmptyList;
27 | }
28 |
29 | render() {
30 | const {
31 | message,
32 | title,
33 | imageStyle,
34 | titleStyle,
35 | messageStyle,
36 | style,
37 | } = this.props;
38 | const EmptyStateImage = this.resolveImage();
39 |
40 | // SVG cannot receive array of style elements on Web
41 | const resolvedImageStyle = {
42 | ...style.image,
43 | ...imageStyle,
44 | };
45 |
46 | const resolvedTitleStyle = {
47 | ...style.title,
48 | ...titleStyle,
49 | };
50 |
51 | const resolvedTextStyle = {
52 | ...style.message,
53 | ...messageStyle,
54 | };
55 |
56 | return (
57 |
58 |
59 |
60 | {title}
61 |
62 |
63 | {message}
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | EmptyListImage.propTypes = {
71 | style: PropTypes.object.isRequired,
72 | image: PropTypes.func,
73 | imageStyle: PropTypes.object,
74 | message: PropTypes.string,
75 | messageStyle: PropTypes.object,
76 | title: PropTypes.string,
77 | titleStyle: PropTypes.object,
78 | };
79 |
80 | EmptyListImage.defaultProps = {
81 | image: undefined,
82 | imageStyle: undefined,
83 | message: "We couldn't find anything to show...",
84 | messageStyle: undefined,
85 | title: "It's empty in here",
86 | titleStyle: undefined,
87 | };
88 |
89 | const StyledView = connectStyle('shoutem.ui.EmptyListImage')(EmptyListImage);
90 |
91 | export { StyledView as EmptyListImage };
92 |
--------------------------------------------------------------------------------
/components/EmptyStateView.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import autoBindReact from 'auto-bind/react';
3 | import PropTypes from 'prop-types';
4 | import { connectStyle } from '@shoutem/theme';
5 | import { Button } from './Button';
6 | import { Icon } from './Icon';
7 | import { Subtitle, Text } from './Text';
8 | import { View } from './View';
9 |
10 | class EmptyStateView extends PureComponent {
11 | constructor(props) {
12 | super(props);
13 |
14 | autoBindReact(this);
15 | }
16 |
17 | onRetry() {
18 | const { onRetry } = this.props;
19 |
20 | onRetry();
21 | }
22 |
23 | renderRetryButton() {
24 | const { retryButtonTitle } = this.props;
25 |
26 | // Show retry button at the bottom only if there is an onRetry action passed
27 | return (
28 |
29 |
32 |
33 | );
34 | }
35 |
36 | render() {
37 | const { icon, message, onRetry, ...otherProps } = this.props;
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 | {message}
46 |
47 | {onRetry && this.renderRetryButton()}
48 |
49 | );
50 | }
51 | }
52 |
53 | EmptyStateView.propTypes = {
54 | icon: PropTypes.string,
55 | message: PropTypes.string,
56 | retryButtonTitle: PropTypes.string,
57 | onRetry: PropTypes.func,
58 | };
59 |
60 | EmptyStateView.defaultProps = {
61 | icon: 'error',
62 | message: undefined,
63 | retryButtonTitle: 'TRY AGAIN',
64 | onRetry: undefined,
65 | };
66 |
67 | const StyledView = connectStyle('shoutem.ui.EmptyStateView')(EmptyStateView);
68 |
69 | export { StyledView as EmptyStateView };
70 |
--------------------------------------------------------------------------------
/components/FormGroup.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connectAnimation } from '@shoutem/animation';
3 | import { connectStyle } from '@shoutem/theme';
4 | import { View } from './View';
5 |
6 | class FormGroup extends PureComponent {
7 | render() {
8 | const { children, ...otherProps } = this.props;
9 |
10 | return {children};
11 | }
12 | }
13 |
14 | FormGroup.propTypes = {
15 | ...View.propTypes,
16 | };
17 |
18 | FormGroup.defaultProps = {
19 | ...View.defaultProps,
20 | };
21 |
22 | const AnimatedFormGroup = connectAnimation(FormGroup);
23 | const StyledFormGroup = connectStyle('shoutem.ui.FormGroup')(AnimatedFormGroup);
24 |
25 | export { StyledFormGroup as FormGroup };
26 |
--------------------------------------------------------------------------------
/components/HorizontalPager/Page.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View } from '../View';
4 |
5 | /**
6 | * A HorizontalPager page. This component is used in
7 | * HorizontalPager in order to prevent unnecessary
8 | * rendering of pages that are not currently visible.
9 | */
10 | export class Page extends Component {
11 | shouldComponentUpdate(nextProps) {
12 | return nextProps.isActive;
13 | }
14 |
15 | render() {
16 | const { width, style, children } = this.props;
17 |
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | }
24 | }
25 |
26 | Page.propTypes = {
27 | isActive: PropTypes.bool.isRequired,
28 | width: PropTypes.number.isRequired,
29 | children: PropTypes.node,
30 | style: PropTypes.object,
31 | };
32 |
33 | Page.defaultProps = {
34 | children: undefined,
35 | style: {},
36 | };
37 |
--------------------------------------------------------------------------------
/components/HorizontalPager/index.js:
--------------------------------------------------------------------------------
1 | export { HorizontalPager } from './HorizontalPager';
2 |
--------------------------------------------------------------------------------
/components/Icon/Icon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connectStyle } from '@shoutem/theme';
4 | import { getIcon } from './services';
5 |
6 | function Icon({ name, style, ...otherProps }) {
7 | const { color, width, height, ...otherStyle } = style;
8 |
9 | const NamedIcon = getIcon(name);
10 |
11 | if (!NamedIcon) {
12 | // eslint-disable-next-line no-console
13 | console.warn(`Icon with name '${name}' not found within the provided set`);
14 |
15 | return null;
16 | }
17 |
18 | return (
19 |
26 | );
27 | }
28 |
29 | Icon.propTypes = {
30 | name: PropTypes.string.isRequired,
31 | style: PropTypes.object.isRequired,
32 | };
33 |
34 | export default connectStyle('shoutem.ui.Icon')(Icon);
35 |
--------------------------------------------------------------------------------
/components/Icon/Icon.web.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connectStyle } from '@shoutem/theme';
4 | import { View } from '../View';
5 | import { getIcon } from './services';
6 |
7 | function Icon({ name, style, ...otherProps }) {
8 | const { color, width, height, ...otherStyle } = style;
9 |
10 | const NamedIcon = getIcon(name);
11 |
12 | if (!NamedIcon) {
13 | // eslint-disable-next-line no-console
14 | console.warn(`Icon with name '${name}' not found within the provided set`);
15 |
16 | return null;
17 | }
18 |
19 | return (
20 | // Sometimes, in web, svg icons are not resized as expected, unless svg is wrapped inside View.
21 |
22 |
29 |
30 | );
31 | }
32 |
33 | Icon.propTypes = {
34 | name: PropTypes.string.isRequired,
35 | style: PropTypes.object.isRequired,
36 | };
37 |
38 | export default connectStyle('shoutem.ui.Icon')(Icon);
39 |
--------------------------------------------------------------------------------
/components/Icon/assets/about.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/activity.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/add-event.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/add-friend.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/add-to-cart.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/add-to-favorites-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/add-to-favorites-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/address.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/attach-media.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/back.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/books.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/call.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/cam-switch.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/camera.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/cart.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/checkbox-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/checkbox-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/checkbox-rectangle-off.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/checkbox-rectangle-on.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/checkmark-round.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/clear-text.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/clock.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/comment.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/comments.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/deals.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/delete.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/directions.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/down-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/download.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/drop-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/edit.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/email.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/equalizer.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/error-rectangle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/events.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/exit-to-app.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/eye-crossed.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/eye.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/facebook-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/filters.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/components/Icon/assets/folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/forward-10.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/forward-30.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/components/Icon/assets/forward-5.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/forward.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/friends.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/full-screen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/gallery.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/garbage-can.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/gift.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/components/Icon/assets/hang-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/history.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/home.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/instagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/laptop.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/left-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/like-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/like.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/linkedin.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/location-pins.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/lock.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/loyalty-card.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/maps.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/mic-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/mic.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/minus-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/missing.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/more-horizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/music-note.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/music-video.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/my-location.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/news.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/notifications.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/page.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/pause.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/photo.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/pin.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/play.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/playlist-play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/plus-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/podcasts.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/products.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/queue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/radio.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/radiobutton-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/radiobutton-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/receipt.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/repeat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/replay-10.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/replay-30.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/components/Icon/assets/replay-5.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/replay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/assets/restaurant-menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/restaurant.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/restore.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/Icon/assets/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/rss-feed.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/rsvp.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/share-android.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/share.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/sidebar.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/skip-next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/social-wall.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/speed-meter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/stamp.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/take-a-photo.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/components/Icon/assets/tiktok.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/trophy.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/turn-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/tweet.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/uber.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/unfriend.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/up-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/user-profile.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/users.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/video-cam-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/video-cam.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/video-chat.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/video-recording.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Icon/assets/web.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/components/Icon/assets/wheelchair.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/Icon/index.js:
--------------------------------------------------------------------------------
1 | export { default as Icon } from './Icon';
2 | export { registerIcons } from './services';
3 |
--------------------------------------------------------------------------------
/components/Icon/services/icons.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import { defaultConfig } from '../assets';
3 |
4 | const Icons = {};
5 |
6 | function addNewIcon(config) {
7 | const hasName = _.has(config, 'name');
8 | const hasIcon = _.has(config, 'icon');
9 |
10 | if (!hasName || !hasIcon) {
11 | // eslint-disable-next-line no-console
12 | console.warn(`"name" and "icon" keys are required in icon config.`);
13 | return;
14 | }
15 |
16 | const { name, icon } = config;
17 |
18 | if (_.hasIn(Icons, name)) {
19 | // eslint-disable-next-line no-console
20 | console.warn(
21 | `Icon with name "${name}" already exists. Please try using another name.`,
22 | );
23 | return;
24 | }
25 |
26 | if (!_.isString(name)) {
27 | // eslint-disable-next-line no-console
28 | console.warn(`Icon name must be a string.`);
29 | return;
30 | }
31 |
32 | if (!_.isFunction(icon)) {
33 | // eslint-disable-next-line no-console
34 | console.warn(`Icon must be an SVG file imported as a React component.`);
35 | return;
36 | }
37 |
38 | try {
39 | Icons[name] = icon;
40 | } catch (error) {
41 | // eslint-disable-next-line no-console
42 | console.warn(`Icon "${name}" could not be added: ${error.message}`);
43 | }
44 | }
45 |
46 | export function registerIcons(config) {
47 | const isArray = _.isArray(config);
48 | const isObject = _.isObject(config);
49 |
50 | if (!isObject) {
51 | // eslint-disable-next-line no-console
52 | console.warn('Icon config must be an object or an array of objects.');
53 | return;
54 | }
55 |
56 | if (isArray) {
57 | _.forEach(config, element => {
58 | addNewIcon(element);
59 | });
60 | } else {
61 | addNewIcon(config);
62 | }
63 | }
64 |
65 | registerIcons(defaultConfig);
66 |
67 | export function getIcon(name) {
68 | return Icons[name];
69 | }
70 |
--------------------------------------------------------------------------------
/components/Icon/services/index.js:
--------------------------------------------------------------------------------
1 | export { getIcon, registerIcons } from './icons';
2 |
--------------------------------------------------------------------------------
/components/InlineDropDownMenu/InlineDropDownMenuItem.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Animated, Dimensions, Pressable } from 'react-native';
3 | import autoBindReact from 'auto-bind/react';
4 | import PropTypes from 'prop-types';
5 | import { connectStyle } from '@shoutem/theme';
6 | import { Text } from '../Text';
7 |
8 | const window = Dimensions.get('window');
9 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
10 |
11 | class InlineDropDownMenuItem extends PureComponent {
12 | constructor(props) {
13 | super(props);
14 |
15 | autoBindReact(this);
16 |
17 | this.animatedValue = new Animated.Value(0);
18 | }
19 |
20 | componentDidMount() {
21 | const { index } = this.props;
22 |
23 | Animated.timing(this.animatedValue, {
24 | toValue: 1,
25 | useNativeDriver: true,
26 | duration: 300 + index * 15,
27 | }).start();
28 | }
29 |
30 | handlePress() {
31 | const { onItemPressed, item } = this.props;
32 |
33 | if (onItemPressed) {
34 | onItemPressed(item);
35 | }
36 | }
37 |
38 | render() {
39 | const { isSelected, selectedDescriptor, item, style } = this.props;
40 |
41 | const resolvedText = isSelected
42 | ? `${item.title} (${selectedDescriptor})`
43 | : item.title;
44 | const textStyle = isSelected ? 'muted' : '';
45 |
46 | return (
47 |
64 | {resolvedText}
65 |
66 | );
67 | }
68 | }
69 |
70 | InlineDropDownMenuItem.propTypes = {
71 | style: PropTypes.isRequired,
72 | index: PropTypes.number,
73 | isSelected: PropTypes.bool,
74 | item: PropTypes.object,
75 | selectedDescriptor: PropTypes.string,
76 | onItemPressed: PropTypes.func,
77 | };
78 |
79 | InlineDropDownMenuItem.defaultProps = {
80 | index: undefined,
81 | isSelected: false,
82 | item: undefined,
83 | selectedDescriptor: undefined,
84 | onItemPressed: undefined,
85 | };
86 |
87 | const StyledComponent = connectStyle('shoutem.ui.InlineDropDownMenuItem')(
88 | InlineDropDownMenuItem,
89 | );
90 |
91 | export { StyledComponent as InlineDropDownMenuItem };
92 |
--------------------------------------------------------------------------------
/components/InlineDropDownMenu/index.js:
--------------------------------------------------------------------------------
1 | export { InlineDropDownMenu } from './InlineDropDownMenu';
2 | export { InlineDropDownMenuItem } from './InlineDropDownMenuItem';
3 |
--------------------------------------------------------------------------------
/components/Lightbox.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import RNLightbox from 'react-native-lightbox-v2';
3 | import { connectAnimation } from '@shoutem/animation';
4 | import { connectStyle } from '@shoutem/theme';
5 |
6 | // Exporting as class because Animated.createAnimatedComponent
7 | // doesn't support stateless functional components
8 | class Lightbox extends PureComponent {
9 | render() {
10 | return ;
11 | }
12 | }
13 |
14 | const AnimatedLightbox = connectAnimation(Lightbox);
15 | const StyledLightbox = connectStyle('shoutem.ui.Lightbox')(AnimatedLightbox);
16 |
17 | export { StyledLightbox as Lightbox };
18 |
--------------------------------------------------------------------------------
/components/LinearGradient.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import RNLinearGradient from 'react-native-linear-gradient';
3 | import _ from 'lodash';
4 | import PropTypes from 'prop-types';
5 | import { connectAnimation } from '@shoutem/animation';
6 | import { connectStyle } from '@shoutem/theme';
7 |
8 | const RNLinearGradientPropsKeys = ['start', 'end', 'colors', 'locations'];
9 |
10 | class LinearGradient extends PureComponent {
11 | render() {
12 | const { children, style } = this.props;
13 |
14 | const styleWithOmissions = _.omit(style, RNLinearGradientPropsKeys);
15 | const linearGradientProps = {
16 | ...this.props,
17 | ..._.pick(style, RNLinearGradientPropsKeys),
18 | };
19 |
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | }
26 | }
27 |
28 | LinearGradient.propTypes = {
29 | style: PropTypes.object.isRequired,
30 | children: PropTypes.node,
31 | };
32 |
33 | LinearGradient.defaultProps = {
34 | children: undefined,
35 | };
36 |
37 | const AnimatedLinearGradient = connectAnimation(LinearGradient);
38 | const StyledLinearGradient = connectStyle('shoutem.ui.LinearGradient')(
39 | AnimatedLinearGradient,
40 | );
41 |
42 | export { StyledLinearGradient as LinearGradient };
43 |
--------------------------------------------------------------------------------
/components/LoadingContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState } from 'react';
2 | import LottieView from 'lottie-react-native';
3 | import PropTypes from 'prop-types';
4 | import { connectStyle } from '@shoutem/theme';
5 | import { View } from '@shoutem/ui';
6 | import { animations } from '../assets';
7 |
8 | function LoadingContainer({
9 | animation,
10 | children,
11 | loading,
12 | animationScale,
13 | style,
14 | }) {
15 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
16 |
17 | function onLayoutChange(event) {
18 | const { width, height } = event.nativeEvent.layout;
19 |
20 | if (width === 0 && height === 0) {
21 | return;
22 | }
23 |
24 | setDimensions({ width, height });
25 | }
26 |
27 | const lottieViewDimensions = useMemo(
28 | () => ({
29 | width: dimensions.width * animationScale,
30 | height: dimensions.height * animationScale,
31 | }),
32 | [animationScale, dimensions],
33 | );
34 |
35 | if (!children) {
36 | return null;
37 | }
38 |
39 | return (
40 |
41 | {!loading && {children}}
42 | {loading && (
43 |
44 |
51 |
52 | )}
53 |
54 | );
55 | }
56 |
57 | LoadingContainer.propTypes = {
58 | style: PropTypes.object.isRequired,
59 | animation: PropTypes.object,
60 | animationScale: PropTypes.number,
61 | children: PropTypes.oneOfType([
62 | PropTypes.object,
63 | PropTypes.func,
64 | PropTypes.node,
65 | ]),
66 | loading: PropTypes.bool,
67 | };
68 |
69 | LoadingContainer.defaultProps = {
70 | animationScale: 1,
71 | children: undefined,
72 | animation: animations.loadingDots,
73 | loading: false,
74 | };
75 |
76 | const StyledLoadingContainer = connectStyle('shoutem.ui.LoadingContainer')(
77 | LoadingContainer,
78 | );
79 | export { StyledLoadingContainer as LoadingContainer };
80 |
--------------------------------------------------------------------------------
/components/LoadingIndicator.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connectStyle } from '@shoutem/theme';
3 | import { Spinner } from './Spinner';
4 | import { View } from './View';
5 |
6 | /**
7 | * Renders a loading indicator (spinner) that fits into available space (container)
8 | */
9 | class LoadingIndicator extends PureComponent {
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | const StyledLoadingIndicator = connectStyle('shoutem.ui.LoadingIndicator')(
22 | LoadingIndicator,
23 | );
24 |
25 | export { StyledLoadingIndicator as LoadingIndicator };
26 |
--------------------------------------------------------------------------------
/components/Overlay.js:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { connectAnimation } from '@shoutem/animation';
3 | import { connectStyle } from '@shoutem/theme';
4 |
5 | const AnimatedOverlay = connectAnimation(View);
6 | const Overlay = connectStyle('shoutem.ui.Overlay')(AnimatedOverlay);
7 |
8 | export { Overlay };
9 |
--------------------------------------------------------------------------------
/components/ProgressRings/AnimatedProgressRing.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/require-default-props */
2 | import React from 'react';
3 | import _ from 'lodash';
4 | import PropTypes from 'prop-types';
5 | import { DEFAULT_PROGRESS_COLORS } from '../../const';
6 | import { useColorAndPercentageInterpolation } from '../../hooks';
7 | import { PROGRESS_RING_DEFAULT_PROPS } from './const';
8 | import ProgressRing from './ProgressRing';
9 |
10 | const AnimatedProgressRing = props => {
11 | const {
12 | progressPercentage = PROGRESS_RING_DEFAULT_PROPS.progressPercentage,
13 | progressColors = DEFAULT_PROGRESS_COLORS,
14 | animationConfig = {
15 | toValue: progressPercentage,
16 | duration: 1000 * (progressPercentage / 100), // 100% will animate for 1s, 50% for 0.5s etc.
17 | useNativeDriver: true,
18 | },
19 | } = props;
20 |
21 | const {
22 | interpolatedColor,
23 | interpolatedPercentage,
24 | } = useColorAndPercentageInterpolation(
25 | progressColors,
26 | progressPercentage,
27 | animationConfig,
28 | );
29 |
30 | return (
31 |
36 | );
37 | };
38 |
39 | AnimatedProgressRing.propTypes = {
40 | ..._.omit(ProgressRing.propTypes, ['color']),
41 | progressColors: PropTypes.arrayOf(PropTypes.string),
42 | };
43 |
44 | export default AnimatedProgressRing;
45 |
--------------------------------------------------------------------------------
/components/ProgressRings/ProgressRings.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { View } from 'react-native';
3 | import { G, Svg } from 'react-native-svg';
4 | import _ from 'lodash';
5 | import PropTypes from 'prop-types';
6 | import { connectStyle } from '@shoutem/theme';
7 | import AnimatedProgressRing from './AnimatedProgressRing';
8 | import ProgressRing from './ProgressRing';
9 |
10 | /**
11 | * Component rendering given number of rings, which are filled depending on given
12 | * percentage values.
13 | * It is possible to either specify definite color for each ring by passing ring.color prop,
14 | * or give and array of ring.progressColors and component will then interpolate between given
15 | * colors, based on given percentage value.
16 | */
17 | const ProgressRings = ({ rings, size, rotation, children, style }) => {
18 | const renderRings = useMemo(
19 | () =>
20 | rings.map((ring, index) => {
21 | const ResolvedRingComponent = ring.progressColors
22 | ? AnimatedProgressRing
23 | : ProgressRing;
24 |
25 | return (
26 |
32 | );
33 | }),
34 | [rings, size],
35 | );
36 |
37 | return (
38 |
39 |
44 | {children && {children}}
45 |
46 | );
47 | };
48 |
49 | ProgressRings.propTypes = {
50 | rings: PropTypes.arrayOf(
51 | // Omitting color to stop errors, but implementation has to define either color or progressColors,
52 | // depending on which component they're using - ProgressRing or AnimatedProgressRing, respectively.
53 | PropTypes.shape({ ..._.omit(ProgressRing.propTypes, ['index', 'color']) }),
54 | ).isRequired,
55 | children: PropTypes.func,
56 | rotation: PropTypes.number,
57 | size: PropTypes.number,
58 | style: PropTypes.object,
59 | };
60 |
61 | ProgressRings.defaultProps = {
62 | children: undefined,
63 | size: 80,
64 | rotation: 0,
65 | style: {},
66 | };
67 |
68 | export default connectStyle('shoutem.ui.ProgressRings')(ProgressRings);
69 |
--------------------------------------------------------------------------------
/components/ProgressRings/const.js:
--------------------------------------------------------------------------------
1 | export const PROGRESS_RING_DEFAULT_PROPS = {
2 | progressLineWidth: 10,
3 | backgroundLineWidth: 10,
4 | progressLineCap: 'round',
5 | backgroundLineCap: 'round',
6 | progressPercentage: 0.1, // 0.1 so that it shows tiny fill indicator, indicating 0%, better UX/UI than empty.
7 | arcSweepAngle: 360,
8 | size: 80,
9 | color: '#000',
10 | };
11 |
--------------------------------------------------------------------------------
/components/ProgressRings/index.js:
--------------------------------------------------------------------------------
1 | export { default as AnimatedProgressRing } from './AnimatedProgressRing';
2 | export * from './const';
3 | export { default as ProgressRing } from './ProgressRing';
4 | export { default as ProgressRings } from './ProgressRings';
5 |
--------------------------------------------------------------------------------
/components/ProgressRings/svgCalculations.js:
--------------------------------------------------------------------------------
1 | const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
2 | const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
3 | return {
4 | x: centerX + radius * Math.cos(angleInRadians),
5 | y: centerY + radius * Math.sin(angleInRadians),
6 | };
7 | };
8 |
9 | export const circlePath = (x, y, radius, startAngle, endAngle) => {
10 | const start = polarToCartesian(x, y, radius, endAngle * 0.9999999);
11 | const end = polarToCartesian(x, y, radius, startAngle);
12 | const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
13 | return [
14 | 'M',
15 | start.x,
16 | start.y,
17 | 'A',
18 | radius,
19 | radius,
20 | 0,
21 | largeArcFlag,
22 | 0,
23 | end.x,
24 | end.y,
25 | ].join(' ');
26 | };
27 |
--------------------------------------------------------------------------------
/components/RNHapticFeedback/index.js:
--------------------------------------------------------------------------------
1 | export { default as ReactNativeHapticFeedback } from 'react-native-haptic-feedback';
2 |
--------------------------------------------------------------------------------
/components/RNHapticFeedback/index.web.js:
--------------------------------------------------------------------------------
1 | const ReactNativeHapticFeedback = {
2 | trigger: () => {},
3 | };
4 |
5 | export { ReactNativeHapticFeedback };
6 |
--------------------------------------------------------------------------------
/components/Row.js:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { connectAnimation } from '@shoutem/animation';
3 | import { connectStyle } from '@shoutem/theme';
4 |
5 | const AnimatedRow = connectAnimation(View);
6 | const Row = connectStyle('shoutem.ui.Row')(AnimatedRow);
7 |
8 | export { Row };
9 |
--------------------------------------------------------------------------------
/components/Screen.js:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { connectAnimation } from '@shoutem/animation';
3 | import { connectStyle } from '@shoutem/theme';
4 |
5 | const AnimatedScreen = connectAnimation(View);
6 | const Screen = connectStyle('shoutem.ui.Screen')(AnimatedScreen);
7 |
8 | export { Screen };
9 |
--------------------------------------------------------------------------------
/components/ScrollView/index.js:
--------------------------------------------------------------------------------
1 | export { ScrollView } from './ScrollView';
2 |
--------------------------------------------------------------------------------
/components/SearchField.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-multi-comp */
2 | import React, { useEffect, useState } from 'react';
3 | import { ActivityIndicator } from 'react-native';
4 | import PropTypes from 'prop-types';
5 | import { connectAnimation } from '@shoutem/animation';
6 | import { connectStyle } from '@shoutem/theme';
7 | import { Button } from './Button';
8 | import { Icon } from './Icon';
9 | import { TextInput } from './TextInput';
10 | import { View } from './View';
11 |
12 | const ClearButton = ({ style, onPress }) => (
13 |
16 | );
17 |
18 | ClearButton.propTypes = {
19 | style: PropTypes.object.isRequired,
20 | onPress: PropTypes.func.isRequired,
21 | };
22 |
23 | /**
24 | * A component that allows the user to enter a search query.
25 | * It has a search icon, placeholder and a button that clears the current query.
26 | *
27 | */
28 | const SearchField = ({ onChangeText, loading, style, ...otherProps }) => {
29 | const [text, setText] = useState(otherProps.defaultValue);
30 |
31 | useEffect(() => {
32 | onChangeText(text);
33 | // eslint-disable-next-line react-hooks/exhaustive-deps
34 | }, [text]);
35 |
36 | return (
37 |
41 |
42 |
50 | {!!text && !loading && (
51 | setText('')} style={style} />
52 | )}
53 | {loading && }
54 |
55 | );
56 | };
57 |
58 | SearchField.propTypes = {
59 | style: PropTypes.object.isRequired,
60 | defaultValue: PropTypes.string,
61 | loading: PropTypes.bool,
62 | onChangeText: PropTypes.func,
63 | };
64 |
65 | SearchField.defaultProps = {
66 | loading: false,
67 | onChangeText: undefined,
68 | defaultValue: '',
69 | };
70 |
71 | const AnimatedSearchField = connectAnimation(SearchField);
72 | const StyledSearchField = connectStyle('shoutem.ui.SearchField')(
73 | AnimatedSearchField,
74 | );
75 |
76 | export { StyledSearchField as SearchField };
77 |
--------------------------------------------------------------------------------
/components/ShareButton.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Platform, Share } from 'react-native';
3 | import autoBindReact from 'auto-bind/react';
4 | import PropTypes from 'prop-types';
5 | import { connectStyle } from '@shoutem/theme';
6 | import { unavailableInWeb } from '../services';
7 | import { Button } from './Button';
8 | import { Icon } from './Icon';
9 |
10 | /**
11 | * The ShareButton is a virtual component that wraps a button with a share icon.
12 | * It puts the sharing logic in one place. It's used in the navigation bar in the toolkit,
13 | * but it can be reused anywhere.
14 | *
15 | * It should have the style of its underlying button. That's why it's not connected to style
16 | * or animation.
17 | */
18 | class ShareButton extends PureComponent {
19 | constructor(props) {
20 | super(props);
21 |
22 | autoBindReact(this);
23 | }
24 |
25 | onShare() {
26 | const { title, message, url } = this.props;
27 |
28 | return Share.share({
29 | title,
30 | // URL property isn't supported on Android, so we are
31 | // including it as the message for now.
32 | message: Platform.OS === 'android' ? url : message,
33 | url,
34 | });
35 | }
36 |
37 | render() {
38 | const { animationName, iconProps, url, ...otherProps } = this.props;
39 |
40 | if (!url) {
41 | return null;
42 | }
43 |
44 | return (
45 |
52 | );
53 | }
54 | }
55 |
56 | ShareButton.propTypes = {
57 | // Animation name for share icon
58 | animationName: PropTypes.string,
59 | // Additional props for Icon component
60 | iconProps: PropTypes.object,
61 | // Message to share
62 | message: PropTypes.string,
63 | // Title
64 | title: PropTypes.string,
65 | // Url to share
66 | url: PropTypes.string,
67 | };
68 |
69 | ShareButton.defaultProps = {
70 | animationName: undefined,
71 | iconProps: undefined,
72 | message: undefined,
73 | title: undefined,
74 | url: undefined,
75 | };
76 |
77 | const StyledShareButton = connectStyle(
78 | 'shoutem.ui.ShareButton',
79 | undefined,
80 | undefined,
81 | { virtual: true },
82 | )(ShareButton);
83 |
84 | export { StyledShareButton as ShareButton };
85 |
--------------------------------------------------------------------------------
/components/Spinner.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { ActivityIndicator } from 'react-native';
3 | import PropTypes from 'prop-types';
4 | import { connectStyle } from '@shoutem/theme';
5 |
6 | class Spinner extends PureComponent {
7 | render() {
8 | const { style } = this.props;
9 | const { color, size, ...otherStyle } = style;
10 |
11 | return (
12 |
18 | );
19 | }
20 | }
21 |
22 | Spinner.propTypes = {
23 | style: PropTypes.object.isRequired,
24 | };
25 |
26 | const StyledSpinner = connectStyle('shoutem.ui.Spinner', {
27 | size: 'small',
28 | })(Spinner);
29 |
30 | export { StyledSpinner as Spinner };
31 |
--------------------------------------------------------------------------------
/components/Switch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch as RNSwitch } from 'react-native';
3 | import PropTypes from 'prop-types';
4 | import { connectStyle } from '@shoutem/theme';
5 | import { isWeb } from '../services/platform';
6 | import { View } from './View';
7 |
8 | const Switch = ({ value, onValueChange, style }) => {
9 | const webColors = {
10 | trackColor: style.track?.false,
11 | activeThumbColor: style.thumb,
12 | activeTrackColor: style.track.true,
13 | };
14 |
15 | return (
16 |
17 |
25 |
26 | );
27 | };
28 |
29 | Switch.propTypes = {
30 | onValueChange: PropTypes.func.isRequired,
31 | style: PropTypes.object,
32 | value: PropTypes.bool,
33 | };
34 |
35 | Switch.defaultProps = {
36 | value: false,
37 | style: {},
38 | };
39 |
40 | const StyledSwitch = connectStyle('shoutem.ui.Switch')(Switch);
41 |
42 | export { StyledSwitch as Switch };
43 |
--------------------------------------------------------------------------------
/components/TabMenu/TabMenuItem.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { LayoutAnimation } from 'react-native';
3 | import autoBindReact from 'auto-bind/react';
4 | import PropTypes from 'prop-types';
5 | import { connectStyle } from '@shoutem/theme';
6 | import { Text } from '../Text';
7 | import { TouchableOpacity } from '../TouchableOpacity';
8 | import { View } from '../View';
9 | import { optionShape } from './const';
10 |
11 | class TabMenuItem extends PureComponent {
12 | constructor(props) {
13 | super(props);
14 |
15 | autoBindReact(this);
16 |
17 | this.state = {
18 | baseWidth: 0,
19 | };
20 | }
21 |
22 | handleItemPressed() {
23 | const { onItemPressed, item } = this.props;
24 |
25 | onItemPressed(item);
26 | }
27 |
28 | handleTextLayout({
29 | nativeEvent: {
30 | layout: { width },
31 | },
32 | }) {
33 | LayoutAnimation.easeInEaseOut();
34 | this.setState({ baseWidth: width });
35 | }
36 |
37 | handleContainerLayout({
38 | nativeEvent: {
39 | layout: { width },
40 | },
41 | }) {
42 | const { onLayoutMeasured } = this.props;
43 |
44 | onLayoutMeasured(width);
45 | }
46 |
47 | render() {
48 | const { style, isSelected, item } = this.props;
49 | const { baseWidth } = this.state;
50 |
51 | return (
52 |
57 |
61 | {item.title}
62 |
63 | {isSelected && }
64 |
65 | );
66 | }
67 | }
68 |
69 | TabMenuItem.propTypes = {
70 | style: PropTypes.object.isRequired,
71 | onLayoutMeasured: PropTypes.func.isRequired,
72 | isSelected: PropTypes.bool,
73 | item: optionShape,
74 | onItemPressed: PropTypes.func,
75 | };
76 |
77 | TabMenuItem.defaultProps = {
78 | isSelected: false,
79 | item: undefined,
80 | onItemPressed: undefined,
81 | };
82 |
83 | const StyledTabMenuItem = connectStyle('shoutem.ui.TabMenuItem')(TabMenuItem);
84 |
85 | export { StyledTabMenuItem as TabMenuItem };
86 |
--------------------------------------------------------------------------------
/components/TabMenu/const.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export const optionShape = PropTypes.shape({
4 | title: PropTypes.string,
5 | value: PropTypes.any,
6 | });
7 |
--------------------------------------------------------------------------------
/components/TabMenu/index.js:
--------------------------------------------------------------------------------
1 | export { TabMenu } from './TabMenu';
2 |
--------------------------------------------------------------------------------
/components/Text.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Text as RNText } from 'react-native';
3 | import { TextPropTypes } from 'deprecated-react-native-prop-types';
4 | import { connectAnimation } from '@shoutem/animation';
5 | import { connectStyle } from '@shoutem/theme';
6 |
7 | class Text extends PureComponent {
8 | render() {
9 | return ;
10 | }
11 | }
12 |
13 | Text.propTypes = {
14 | ...TextPropTypes,
15 | };
16 |
17 | const AnimatedText = connectAnimation(Text);
18 | const StyledText = connectStyle('shoutem.ui.Text')(AnimatedText);
19 | const Heading = connectStyle('shoutem.ui.Heading')(AnimatedText);
20 | const Title = connectStyle('shoutem.ui.Title')(AnimatedText);
21 | const Subtitle = connectStyle('shoutem.ui.Subtitle')(AnimatedText);
22 | const Caption = connectStyle('shoutem.ui.Caption')(AnimatedText);
23 |
24 | export { Caption, Heading, Subtitle, StyledText as Text, Title };
25 |
--------------------------------------------------------------------------------
/components/Tile.js:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { connectAnimation } from '@shoutem/animation';
3 | import { connectStyle } from '@shoutem/theme';
4 |
5 | const AnimatedTile = connectAnimation(View);
6 | const Tile = connectStyle('shoutem.ui.Tile')(AnimatedTile);
7 |
8 | export { Tile };
9 |
--------------------------------------------------------------------------------
/components/ToastMessage/components/ActionToast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connectStyle } from '@shoutem/theme';
3 | import BaseToast from './BaseToast';
4 |
5 | function ActionToast({ style, isVisible, props }) {
6 | return (
7 |
13 | );
14 | }
15 |
16 | ActionToast.propTypes = BaseToast.propTypes;
17 |
18 | export default connectStyle('shoutem.ui.ActionToast')(ActionToast);
19 |
--------------------------------------------------------------------------------
/components/ToastMessage/components/ErrorToast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connectStyle } from '@shoutem/theme';
3 | import BaseToast from './BaseToast';
4 |
5 | function ErrorToast({ style, props, isVisible }) {
6 | return (
7 |
13 | );
14 | }
15 |
16 | ErrorToast.propTypes = BaseToast.propTypes;
17 |
18 | export default connectStyle('shoutem.ui.ErrorToast')(ErrorToast);
19 |
--------------------------------------------------------------------------------
/components/ToastMessage/components/InfoToast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connectStyle } from '@shoutem/theme';
3 | import BaseToast from './BaseToast';
4 |
5 | function InfoToast({ style, props, isVisible }) {
6 | return (
7 |
8 | );
9 | }
10 |
11 | InfoToast.propTypes = BaseToast.propTypes;
12 |
13 | export default connectStyle('shoutem.ui.InfoToast')(InfoToast);
14 |
--------------------------------------------------------------------------------
/components/ToastMessage/components/SuccessToast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connectStyle } from '@shoutem/theme';
3 | import BaseToast from './BaseToast';
4 |
5 | function SuccessToast({ style, props, isVisible }) {
6 | return (
7 |
13 | );
14 | }
15 |
16 | SuccessToast.propTypes = BaseToast.propTypes;
17 |
18 | export default connectStyle('shoutem.ui.SuccessToast')(SuccessToast);
19 |
--------------------------------------------------------------------------------
/components/ToastMessage/components/ToastProgressBar.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef } from 'react';
2 | import { Animated, View } from 'react-native';
3 | import PropTypes from 'prop-types';
4 | import { connectStyle } from '@shoutem/theme';
5 |
6 | function ToastProgressBar({
7 | duration,
8 | style,
9 | onProgressComplete,
10 | color,
11 | visible,
12 | }) {
13 | const progressValue = useRef(new Animated.Value(0)).current;
14 |
15 | const handleAnimationEnd = useCallback(() => {
16 | progressValue.setValue(0);
17 | if (onProgressComplete) {
18 | onProgressComplete();
19 | }
20 | }, [onProgressComplete, progressValue]);
21 |
22 | useEffect(() => {
23 | if (!visible) {
24 | progressValue.setValue(0);
25 | return;
26 | }
27 |
28 | Animated.timing(progressValue, {
29 | toValue: 1,
30 | duration,
31 | useNativeDriver: false,
32 | }).start(handleAnimationEnd);
33 | }, [visible, duration, handleAnimationEnd]);
34 |
35 | return (
36 |
37 |
49 |
50 | );
51 | }
52 |
53 | ToastProgressBar.propTypes = {
54 | style: PropTypes.object.isRequired,
55 | visible: PropTypes.bool.isRequired,
56 | color: PropTypes.string,
57 | duration: PropTypes.number,
58 | onProgressComplete: PropTypes.func,
59 | };
60 |
61 | ToastProgressBar.defaultProps = {
62 | color: '#00AADF',
63 | duration: 4000,
64 | onProgressComplete: undefined,
65 | };
66 |
67 | export default connectStyle('shoutem.ui.ToastProgressBar')(ToastProgressBar);
68 |
--------------------------------------------------------------------------------
/components/ToastMessage/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as ActionToast } from './ActionToast';
2 | export { default as BaseToast } from './BaseToast';
3 | export { default as ErrorToast } from './ErrorToast';
4 | export { default as InfoToast } from './InfoToast';
5 | export { default as SuccessToast } from './SuccessToast';
6 | export { default as ToastProgressBar } from './ToastProgressBar';
7 |
--------------------------------------------------------------------------------
/components/ToastMessage/index.js:
--------------------------------------------------------------------------------
1 | export { BaseToast } from './components';
2 | export { default as Toast } from './toastMessage';
3 |
--------------------------------------------------------------------------------
/components/Touchable.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Platform } from 'react-native';
3 | import PropTypes from 'prop-types';
4 | import { connectStyle } from '@shoutem/theme';
5 | import { TouchableNativeFeedback } from './TouchableNativeFeedback';
6 | import { TouchableOpacity } from './TouchableOpacity';
7 | import { View } from './View';
8 |
9 | /**
10 | * A universal touchable component with a platform specific feedback. This
11 | * component displays a TouchableOpacity on iOS, and a TouchableNativeFeedback
12 | * on Android.
13 | */
14 | class Touchable extends PureComponent {
15 | render() {
16 | const { children, style, styleName } = this.props;
17 | const { touchableNativeFeedback, touchableOpacity, ...otherStyle } = style;
18 |
19 | if (Platform.OS === 'android') {
20 | return (
21 |
25 |
26 | {children}
27 |
28 |
29 | );
30 | }
31 |
32 | return (
33 |
40 | {children}
41 |
42 | );
43 | }
44 | }
45 |
46 | Touchable.propTypes = {
47 | ...TouchableOpacity.propTypes,
48 | ...TouchableNativeFeedback.propTypes,
49 | style: PropTypes.object.isRequired,
50 | };
51 |
52 | const StyledTouchable = connectStyle('shoutem.ui.Touchable', {
53 | touchableNativeFeedback: {},
54 | touchableOpacity: {},
55 | })(Touchable);
56 |
57 | export { StyledTouchable as Touchable };
58 |
--------------------------------------------------------------------------------
/components/TouchableNativeFeedback.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { TouchableNativeFeedback as RNTouchableNativeFeedback } from 'react-native';
3 | import PropTypes from 'prop-types';
4 | import { connectStyle } from '@shoutem/theme';
5 |
6 | class TouchableNativeFeedback extends PureComponent {
7 | render() {
8 | const { children, style } = this.props;
9 | const { background, useForeground, ...otherStyle } = style;
10 |
11 | return (
12 |
18 | {children}
19 |
20 | );
21 | }
22 | }
23 |
24 | TouchableNativeFeedback.propTypes = {
25 | ...RNTouchableNativeFeedback.propTypes,
26 | style: PropTypes.shape({
27 | background: PropTypes.object,
28 | useForeground: PropTypes.bool,
29 | }).isRequired,
30 | };
31 |
32 | const StyledTouchableNativeFeedback = connectStyle(
33 | 'shoutem.ui.TouchableNativeFeedback',
34 | )(TouchableNativeFeedback);
35 |
36 | export { StyledTouchableNativeFeedback as TouchableNativeFeedback };
37 |
--------------------------------------------------------------------------------
/components/TouchableOpacity.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { TouchableOpacity as RNTouchableOpacity } from 'react-native';
3 | import { connectStyle } from '@shoutem/theme';
4 |
5 | class TouchableOpacity extends PureComponent {
6 | render() {
7 | const { children, style } = this.props;
8 | const { activeOpacity, ...otherStyle } = style;
9 |
10 | return (
11 |
16 | {children}
17 |
18 | );
19 | }
20 | }
21 |
22 | TouchableOpacity.propTypes = {
23 | ...RNTouchableOpacity.propTypes,
24 | };
25 |
26 | const StyledTouchableOpacity = connectStyle('shoutem.ui.TouchableOpacity')(
27 | TouchableOpacity,
28 | );
29 |
30 | export { StyledTouchableOpacity as TouchableOpacity };
31 |
--------------------------------------------------------------------------------
/components/Video/Video.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { View } from 'react-native';
3 | import { WebView } from 'react-native-webview';
4 | import PropTypes from 'prop-types';
5 | import { connectAnimation } from '@shoutem/animation';
6 | import { connectStyle } from '@shoutem/theme';
7 | import VideoSourceReader from './VideoSourceReader';
8 |
9 | function getSource(sourceReader, poster) {
10 | const url = sourceReader.getUrl();
11 |
12 | if (sourceReader.isEmbeddableVideo()) {
13 | return {
14 | uri: url,
15 | };
16 | }
17 |
18 | const HTML = `
19 |
22 | `;
23 |
24 | return {
25 | html: HTML,
26 | };
27 | }
28 |
29 | /**
30 | * Renders a Video based on the source type
31 | * if source is an url to a web player the
32 | * it is displayed in a WebView, if not
33 | * a Video HTML element is displayed in the
34 | * WebView.
35 | *
36 | * @returns {*}
37 | */
38 | class Video extends PureComponent {
39 | constructor(props) {
40 | super(props);
41 |
42 | const { source, playerParams } = props;
43 | this.sourceReader = new VideoSourceReader(source.uri, playerParams);
44 | }
45 |
46 | render() {
47 | const { width, height = '100%', style, poster } = this.props;
48 |
49 | return (
50 |
51 |
59 |
60 | );
61 | }
62 | }
63 |
64 | Video.propTypes = {
65 | style: PropTypes.object.isRequired,
66 | height: PropTypes.number,
67 | // `playerParams` currently only works for Youtube
68 | playerParams: PropTypes.object,
69 | poster: PropTypes.string,
70 | source: PropTypes.shape({
71 | uri: PropTypes.string,
72 | }),
73 | width: PropTypes.number,
74 | };
75 |
76 | Video.defaultProps = {
77 | width: undefined,
78 | height: undefined,
79 | playerParams: { showinfo: 0 },
80 | source: undefined,
81 | poster: undefined,
82 | };
83 |
84 | const AnimatedVideo = connectAnimation(Video);
85 | const StyledVideo = connectStyle('shoutem.ui.Video')(AnimatedVideo);
86 |
87 | export { StyledVideo as Video };
88 |
--------------------------------------------------------------------------------
/components/Video/VideoSourceReader.js:
--------------------------------------------------------------------------------
1 | import { stringify } from 'qs';
2 |
3 | function getYouTubeVideoId(url) {
4 | // eslint-disable-next-line no-useless-escape
5 | const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\??v?=?))([^#&\?]*).*/;
6 | const match = url.match(regExp);
7 |
8 | if (match && match[7].length === 11) {
9 | return match[7];
10 | }
11 |
12 | return false;
13 | }
14 |
15 | function getVimeoVideoId(url) {
16 | // TODO(Vladimir) - find a shorter regex that covers all of our usecases, remove eslint-disable
17 | // The eslint line length rule is disabled so we can use our old battle-tested regex for vimeo
18 | // eslint-disable-next-line no-useless-escape
19 | const regExp = /https?:\/\/(?:[\w]+\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/; // eslint-disable-line max-len
20 | const match = url.match(regExp);
21 |
22 | if (match && match[3]) {
23 | return match[3];
24 | }
25 |
26 | return false;
27 | }
28 |
29 | function getYouTubeEmbedUrl(id, playerParams) {
30 | const serializedParams = stringify(playerParams);
31 | return `https://www.youtube.com/embed/${id}?${serializedParams}`;
32 | }
33 |
34 | function getVimeoEmbedUrl(id) {
35 | return `https://player.vimeo.com/video/${id}?title=0&byline=0&portrait=0`;
36 | }
37 |
38 | /**
39 | * Reads the video source and provides the video
40 | * url in embedded form if necessary
41 | */
42 |
43 | export default class VideoSourceReader {
44 | constructor(source, playerParams) {
45 | this.source = source;
46 | this.playerParams = playerParams;
47 | this.isYouTube = !!getYouTubeVideoId(source);
48 | this.isVimeo = !!getVimeoVideoId(source);
49 | }
50 |
51 | isEmbeddableVideo() {
52 | return this.isYouTube || this.isVimeo;
53 | }
54 |
55 | getUrl() {
56 | if (this.isYouTube) {
57 | return getYouTubeEmbedUrl(
58 | getYouTubeVideoId(this.source),
59 | this.playerParams,
60 | );
61 | }
62 | if (this.isVimeo) {
63 | return getVimeoEmbedUrl(getVimeoVideoId(this.source));
64 | }
65 |
66 | return this.source;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/components/Video/index.js:
--------------------------------------------------------------------------------
1 | export { Video } from './Video';
2 |
--------------------------------------------------------------------------------
/components/View.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { View as RNView } from 'react-native';
3 | import { ViewPropTypes } from 'deprecated-react-native-prop-types';
4 | import PropTypes from 'prop-types';
5 | import { connectAnimation } from '@shoutem/animation';
6 | import { connectStyle } from '@shoutem/theme';
7 | import { LinearGradient } from './LinearGradient';
8 |
9 | class View extends PureComponent {
10 | render() {
11 | const { children, style } = this.props;
12 | const { backgroundGradient, ...viewStyle } = style;
13 |
14 | let gradient = null;
15 | if (backgroundGradient) {
16 | gradient = (
17 |
18 | );
19 | }
20 |
21 | return (
22 |
23 | {gradient}
24 | {children}
25 |
26 | );
27 | }
28 | }
29 |
30 | View.propTypes = {
31 | ...ViewPropTypes,
32 | style: PropTypes.object.isRequired,
33 | };
34 |
35 | const AnimatedView = connectAnimation(View);
36 | const StyledView = connectStyle('shoutem.ui.View')(AnimatedView);
37 |
38 | export { StyledView as View };
39 |
--------------------------------------------------------------------------------
/components/YearRangePicker/YearRangePickerButton.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Animated } from 'react-native';
3 | import autoBindReact from 'auto-bind/react';
4 | import PropTypes from 'prop-types';
5 | import { connectStyle } from '@shoutem/theme';
6 | import { Icon } from '../Icon';
7 | import { Text } from '../Text';
8 | import { TouchableOpacity } from '../TouchableOpacity';
9 |
10 | const AnimatedIcon = Animated.createAnimatedComponent(Icon);
11 |
12 | class YearRangePickerButton extends PureComponent {
13 | constructor(props) {
14 | super(props);
15 |
16 | autoBindReact(this);
17 |
18 | this.dropDownIconValue = new Animated.Value(0);
19 | }
20 |
21 | componentDidUpdate(prevProps) {
22 | const { collapsed } = this.props;
23 | const { collapsed: prevCollapsed } = prevProps;
24 |
25 | if (collapsed !== prevCollapsed) {
26 | const toValue = collapsed ? 1 : 0;
27 |
28 | Animated.timing(this.dropDownIconValue, {
29 | toValue,
30 | useNativeDriver: true,
31 | duration: 300,
32 | }).start();
33 | }
34 | }
35 |
36 | render() {
37 | const { style, tooltip, onPress } = this.props;
38 |
39 | return (
40 |
41 | {tooltip}
42 |
56 |
57 | );
58 | }
59 | }
60 |
61 | YearRangePickerButton.propTypes = {
62 | style: PropTypes.object.isRequired,
63 | collapsed: PropTypes.bool,
64 | tooltip: PropTypes.string,
65 | onPress: PropTypes.func,
66 | };
67 |
68 | YearRangePickerButton.defaultProps = {
69 | collapsed: undefined,
70 | tooltip: undefined,
71 | onPress: undefined,
72 | };
73 |
74 | export default connectStyle('shoutem.ui.YearRangePickerButton')(
75 | YearRangePickerButton,
76 | );
77 |
--------------------------------------------------------------------------------
/components/YearRangePicker/index.js:
--------------------------------------------------------------------------------
1 | export { default as YearRangePicker } from './YearRangePicker';
2 |
--------------------------------------------------------------------------------
/const.js:
--------------------------------------------------------------------------------
1 | import { Platform, StatusBar } from 'react-native';
2 | import {
3 | HOME_INDICATOR_PADDING,
4 | NAVIGATION_BAR_HEIGHT,
5 | NAVIGATION_HEADER_HEIGHT,
6 | NOTCH_AREA_HEIGHT,
7 | } from './helpers';
8 |
9 | const STATUS_BAR_OFFSET =
10 | Platform.OS === 'android' ? -StatusBar.currentHeight : 0;
11 |
12 | // TODO: Deprecate and remove exports from here
13 | // Currently leaving for backwards compatibility
14 | export {
15 | HOME_INDICATOR_PADDING,
16 | NAVIGATION_BAR_HEIGHT,
17 | NAVIGATION_HEADER_HEIGHT,
18 | NOTCH_AREA_HEIGHT,
19 | STATUS_BAR_OFFSET,
20 | };
21 |
22 | export const DEFAULT_PROGRESS_COLORS = [
23 | '#FF0000', // Red
24 | '#FF4500', // Orange-Red
25 | '#FFA500', // Orange
26 | '#FFD700', // Yellow
27 | '#ADFF2F', // Yellow-Green
28 | '#90EE90', // Light Green
29 | ];
30 |
--------------------------------------------------------------------------------
/fonts/Rubik-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Black.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-BlackItalic.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Bold.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-BoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Italic.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Light.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-LightItalic.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Medium.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-MediumItalic.ttf
--------------------------------------------------------------------------------
/fonts/Rubik-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Regular.ttf
--------------------------------------------------------------------------------
/fonts/rubicon-icon-font.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/rubicon-icon-font.ttf
--------------------------------------------------------------------------------
/helpers/device-selector.js:
--------------------------------------------------------------------------------
1 | import { Platform, StatusBar } from 'react-native';
2 | import DeviceInfo from 'react-native-device-info';
3 | import { initialWindowMetrics } from 'react-native-safe-area-context';
4 |
5 | export const NAVIGATION_HEADER_HEIGHT = 64;
6 |
7 | export const isNotchedAndroid =
8 | Platform.OS === 'android' && DeviceInfo.hasNotch();
9 |
10 | export const NAVIGATION_BAR_HEIGHT = Platform.select({
11 | ios: NAVIGATION_HEADER_HEIGHT + initialWindowMetrics?.insets?.top,
12 | android: isNotchedAndroid
13 | ? NAVIGATION_HEADER_HEIGHT + StatusBar.currentHeight
14 | : NAVIGATION_HEADER_HEIGHT,
15 | default: NAVIGATION_HEADER_HEIGHT,
16 | });
17 |
18 | export const HOME_INDICATOR_PADDING =
19 | Platform.OS === 'ios' ? initialWindowMetrics?.insets?.bottom : 0;
20 |
21 | export const NOTCH_AREA_HEIGHT = Platform.select({
22 | ios: initialWindowMetrics?.insets?.top,
23 | android: isNotchedAndroid ? StatusBar.currentHeight : 0,
24 | default: 0,
25 | });
26 |
27 | export const Device = {
28 | isNotchedAndroid,
29 | HOME_INDICATOR_PADDING,
30 | NOTCH_AREA_HEIGHT,
31 | NAVIGATION_HEADER_HEIGHT,
32 | };
33 |
--------------------------------------------------------------------------------
/helpers/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | Device,
3 | HOME_INDICATOR_PADDING,
4 | isNotchedAndroid,
5 | NAVIGATION_BAR_HEIGHT,
6 | NAVIGATION_HEADER_HEIGHT,
7 | NOTCH_AREA_HEIGHT,
8 | } from './device-selector';
9 | export { calculateKeyboardOffset, default as Keyboard } from './keyboard';
10 |
--------------------------------------------------------------------------------
/helpers/keyboard.js:
--------------------------------------------------------------------------------
1 | import { Platform, StatusBar } from 'react-native';
2 | import { isNotchedAndroid, NAVIGATION_BAR_HEIGHT } from './device-selector';
3 |
4 | export function calculateKeyboardOffset(extraOffset = 0) {
5 | const resolvedOffset = NAVIGATION_BAR_HEIGHT + extraOffset;
6 |
7 | if (Platform.OS === 'ios' || isNotchedAndroid) {
8 | return resolvedOffset;
9 | }
10 |
11 | if (Platform.OS === 'android' && !isNotchedAndroid) {
12 | return StatusBar.currentHeight + resolvedOffset;
13 | }
14 |
15 | return resolvedOffset;
16 | }
17 |
18 | // TODO: Deprecate and remove Keyboard.calculateKeyboardOffset
19 | // Replace with direct function call. It's cleaner for any practical use of this
20 | // because we will usually import Keyboard from react-native alongside this, so
21 | // we're forced to rename imports.
22 | export default { calculateKeyboardOffset };
23 |
--------------------------------------------------------------------------------
/hooks/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | useColorAndPercentageInterpolation,
3 | useColorInterpolation,
4 | } from './useColorAndPercentageInterpolation';
5 |
--------------------------------------------------------------------------------
/html/README.md:
--------------------------------------------------------------------------------
1 | # Html
2 |
3 | ## Html component
4 | Transforms the raw HTML to the React Native components tree.
5 | Accepts these props:
6 |
7 | - `body` A raw HTML.
8 | - `renderElement(element, style, defaultRenderElement)` A function to override default `renderElement`. If this function returns `undefined` the default `renderElement` will be used to render element.
9 | - `style` A Html style.
10 |
11 | Register an element with the static `Html.registerElement(elementTag, Component)` method.
12 | Registered elements will be handled with the default `renderElement`.
13 |
14 | ### Components
15 | Components in the `html/components` folder are base implementation of common HTML elements that contain specific logic.
16 | Wrap those components with your own element implementation, and reduce the element attributes to the component props.
17 |
18 | For example, there are different ways to implement the HTML image gallery but most the most cases you can use the same React Native implementation.
19 |
20 | ## Element components
21 | Elements are React Native components used to render a parsed element.
22 | The Element will by default receives these props:
23 |
24 | - `element` An object describing element, contains `tag`, `attributes`, `childElements`, `parent` props.
25 | - `style` The [Shoutem theme](https://github.com/shoutem/theme) style passed in the Html instance style.
26 | - `renderElement` A function used to render an element with registered element component.
27 |
28 |
--------------------------------------------------------------------------------
/html/components/AttachmentRenderer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import PropTypes from 'prop-types';
4 | import { resolveDimensions } from '../services/Dimensions';
5 | import Image from './Image';
6 |
7 | const AttachmentRenderer = ({ id, type, style, attachments }) => {
8 | if (!attachments) {
9 | return null;
10 | }
11 |
12 | if (type === 'image') {
13 | const image = _.find(attachments, { id });
14 |
15 | if (image && image.src) {
16 | const source = { uri: image.src };
17 | const imageSize = { width: image.width, height: image.height };
18 | const { height, width } = resolveDimensions(imageSize, style);
19 | const resolvedStyle = { alignSelf: 'center', height, width };
20 |
21 | return ;
22 | }
23 | }
24 |
25 | return null;
26 | };
27 |
28 | AttachmentRenderer.propTypes = {
29 | attachments: PropTypes.oneOf([PropTypes.array, PropTypes.object]),
30 | id: PropTypes.any,
31 | style: PropTypes.any,
32 | type: PropTypes.any,
33 | };
34 |
35 | AttachmentRenderer.defaultProps = {
36 | id: undefined,
37 | type: undefined,
38 | attachments: undefined,
39 | style: undefined,
40 | };
41 |
42 | export default AttachmentRenderer;
43 |
--------------------------------------------------------------------------------
/html/components/Gallery.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import autoBindReact from 'auto-bind/react';
3 | import PropTypes from 'prop-types';
4 | import { InlineGallery } from '../../components/InlineGallery';
5 |
6 | /**
7 | * Use to render a HTML gallery component.
8 | * Style interface correspond to InlineGallery from @shoutem/ui.
9 | */
10 | export default class Gallery extends PureComponent {
11 | constructor(props) {
12 | super(props);
13 |
14 | autoBindReact(this);
15 |
16 | this.state = {
17 | selectedIndex: 0,
18 | };
19 | }
20 |
21 | onIndexSelected(selectedIndex) {
22 | this.setState({ selectedIndex });
23 | }
24 |
25 | handlePhotoPress() {
26 | const { data, handlePhotoPress } = this.props;
27 | const { selectedIndex } = this.state;
28 |
29 | if (!handlePhotoPress) {
30 | // eslint-disable-next-line no-console
31 | console.warn('There is no "handlePhotoPress" handler for Gallery photo.');
32 | return;
33 | }
34 |
35 | handlePhotoPress(data, selectedIndex);
36 | }
37 |
38 | render() {
39 | const { selectedIndex } = this.state;
40 |
41 | return (
42 |
48 | );
49 | }
50 | }
51 |
52 | Gallery.propTypes = {
53 | ...InlineGallery.propTypes,
54 | handlePhotoPress: PropTypes.func,
55 | };
56 |
57 | Gallery.defaultProps = {
58 | handlePhotoPress: undefined,
59 | };
60 |
--------------------------------------------------------------------------------
/html/components/VideoRenderer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | useComputeMaxWidthForTag,
4 | useContentWidth,
5 | } from 'react-native-render-html';
6 | import WebView from 'react-native-webview';
7 | import { findOne } from 'domutils';
8 | import PropTypes from 'prop-types';
9 |
10 | function findSource(tnode) {
11 | if (tnode.attributes.src) {
12 | return tnode.attributes.src;
13 | }
14 |
15 | const sourceElms = findOne(
16 | elm => elm.tagName === 'source',
17 | tnode.domNode.children,
18 | );
19 |
20 | return sourceElms ? sourceElms.attribs.src : '';
21 | }
22 |
23 | /**
24 | *
25 | * Generate video html with only selected attributes and parameters.
26 | * @returns HTML video element
27 | */
28 | function generateVideoHtml(tnode) {
29 | const url = findSource(tnode);
30 |
31 | const containsTimeParameter = url.match(/(#|\?)t=[^&$]*/g, '');
32 | // If video has no time parameter, we want to add time parameter at 0.1, just to be able to
33 | // display the image, first frame of the video. Video tags that don't have autoplay attribute
34 | // are not showing first frame without this solution.
35 | // Otherwise, if user has defined time parameter, keep it as is.
36 | const resolvedUrl = `${url}${containsTimeParameter ? '' : '#t=0.1'}`;
37 |
38 | return ``;
39 | }
40 |
41 | export const VideoRenderer = ({ tnode, style }) => {
42 | const computeMaxWidth = useComputeMaxWidthForTag('video');
43 | const width = computeMaxWidth(useContentWidth());
44 |
45 | // Using uri as a Webview source is not an option, because we don't have as much control of video.
46 | // E.g. if video from uri has autoplay=true, we can't prevent it from auto-playing when Webview renders.
47 | // Instead, create video HTML element with only picked attributes and parameters.
48 | const html = generateVideoHtml(tnode);
49 |
50 | return (
51 |
58 | );
59 | };
60 |
61 | VideoRenderer.propTypes = {
62 | tnode: PropTypes.object.isRequired,
63 | style: PropTypes.object,
64 | };
65 |
66 | VideoRenderer.defaultProps = {
67 | style: {},
68 | };
69 |
70 | export default VideoRenderer;
71 |
--------------------------------------------------------------------------------
/html/elements/Block.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import {
4 | combineMappers,
5 | ElementPropTypes,
6 | isBlockElement,
7 | mapElementProps,
8 | } from '../Html';
9 | import { Inline } from './Inline';
10 |
11 | /**
12 | * Should be used for the block HTML elements.
13 | * The Block behaves the same as the HTML block element,
14 | * it moves the content into a new line.
15 | * @param props {Object} Element attributes
16 | * @returns {Component}
17 | * @constructor
18 | */
19 | export function Block(props) {
20 | const { childElements } = props;
21 | const lastChild = _.last(childElements);
22 |
23 | // Wrapper is a block element that contains another block elements as the last leaf child.
24 | // In this case, the wrapper element doesn't need the bottom margin because
25 | // the last leaf child will add the margin.
26 | const styleName =
27 | lastChild && isBlockElement(lastChild) ? 'wrapper' : undefined;
28 |
29 | return ;
30 | }
31 |
32 | Block.propTypes = {
33 | ...ElementPropTypes,
34 | };
35 |
36 | export default combineMappers(mapElementProps)(Block);
37 |
--------------------------------------------------------------------------------
/html/elements/Br.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from '../../components/Text';
3 |
4 | function Br() {
5 | return {'\n'};
6 | }
7 |
8 | export default Br;
9 |
--------------------------------------------------------------------------------
/html/elements/Img.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import Image from '../components/Image';
4 | import { combineMappers, ElementPropTypes, mapElementProps } from '../Html';
5 |
6 | export function isImg(element) {
7 | return _.get(element, 'tag') === 'img';
8 | }
9 |
10 | function Img({ src, style, lightbox }) {
11 | const source = { uri: src };
12 | const imgStyle = {
13 | ...style,
14 | resizeMode: 'contain',
15 | };
16 |
17 | return ;
18 | }
19 |
20 | Img.propTypes = {
21 | ...Image.propTypes,
22 | ...ElementPropTypes,
23 | };
24 |
25 | export default combineMappers(mapElementProps)(Img);
26 |
--------------------------------------------------------------------------------
/html/elements/Text.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from 'react-native';
3 | import { AllHtmlEntities as Entities } from 'html-entities';
4 | import _ from 'lodash';
5 | import PropTypes from 'prop-types';
6 | import { combineMappers, ElementPropTypes, mapElementProps } from '../Html';
7 |
8 | const html = new Entities();
9 |
10 | function isWhiteSpaceString(element) {
11 | return _.isString(element) && element.trim().length === 0;
12 | }
13 |
14 | function isWhiteSpaceWrappedWithText(element) {
15 | return (
16 | _.size(element.childElements) === 1 &&
17 | isWhiteSpaceString(element.childElements[0])
18 | );
19 | }
20 |
21 | function isWhiteSpace(element) {
22 | return isWhiteSpaceString(element) || isWhiteSpaceWrappedWithText(element);
23 | }
24 |
25 | export function isText(element) {
26 | const elementTag = _.get(element, 'tag');
27 | return _.isString(element) || elementTag === 'text';
28 | }
29 |
30 | export function removeWhiteSpace(childElements) {
31 | return childElements.filter(child => !isWhiteSpace(child));
32 | }
33 |
34 | export function decodeHtmlEntities(childElements) {
35 | return _.map(childElements, element =>
36 | _.isString(element) ? html.decode(element) : element,
37 | );
38 | }
39 |
40 | export function TextElement(props) {
41 | // Remove empty white space lines used just to move element in new line.
42 | // Use "p" or "br" to add new line.
43 | const textualChildElements = decodeHtmlEntities(
44 | removeWhiteSpace(props.childElements),
45 | );
46 |
47 | if (textualChildElements.length === 0) {
48 | // Even if there is no children to render, the Text must be rendered
49 | // because otherwise RN may render a View to wrap a "null" which may lead to
50 | // a case where a View is in the Text.
51 | return ;
52 | }
53 |
54 | // Must be the RN Text so that style inheritance chain
55 | // doesn't break with additional layer.
56 | return {textualChildElements};
57 | }
58 |
59 | TextElement.propTypes = {
60 | ...ElementPropTypes,
61 | style: PropTypes.object,
62 | };
63 |
64 | TextElement.defaultProps = {
65 | style: {},
66 | };
67 |
68 | export default combineMappers(mapElementProps)(TextElement);
69 |
--------------------------------------------------------------------------------
/html/elements/Video.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Video as UIVideo } from '../../components/Video';
4 | import { View } from '../../components/View';
5 | import Image from '../components/Image';
6 | import { combineMappers, mapElementProps } from '../Html';
7 |
8 | /**
9 | * Transform a video to RN component with props.
10 | */
11 | function Video({ src, thumbnailUrl, style }) {
12 | // TODO - Find out if there is a better way to keep the video aspect ratio then with Image
13 | // A thumbnail is used to scale the video properly (because it holds the video aspect ratio)
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | Video.propTypes = {
24 | src: PropTypes.string,
25 | style: PropTypes.object,
26 | thumbnailUrl: PropTypes.string,
27 | };
28 |
29 | Video.defaultProps = {
30 | src: undefined,
31 | style: {
32 | container: {
33 | alignSelf: 'stretch',
34 | justifyContent: 'center',
35 | alignItems: 'center',
36 | },
37 | },
38 | thumbnailUrl: undefined,
39 | };
40 |
41 | export default combineMappers(mapElementProps)(Video);
42 |
--------------------------------------------------------------------------------
/html/elements/Virtual.js:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { combineMappers, mapElementProps, renderChildren } from '../Html';
3 |
4 | export default combineMappers(mapElementProps, renderChildren)(View);
5 |
--------------------------------------------------------------------------------
/html/elements/list/Li.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ElementPropTypes } from '../../Html';
3 | import { Inline } from '../Inline';
4 |
5 | /**
6 | * By default Li doesn't take props from parsed Li element.
7 | * @param childElements {Array}
8 | * @param renderElement {Function}
9 | * @param prefix {Component}
10 | * @param style {Object}
11 | * @returns {Component}
12 | */
13 | function Li({ element, renderElement, style }) {
14 | const {
15 | childElements,
16 | attributes: { key },
17 | } = element;
18 |
19 | return (
20 |
27 | );
28 | }
29 |
30 | Li.propTypes = {
31 | ...ElementPropTypes,
32 | };
33 |
34 | export default Li;
35 |
--------------------------------------------------------------------------------
/html/elements/list/Ol.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View } from '../../../components/View';
4 | import { combineMappers, ElementPropTypes, mapElementProps } from '../../Html';
5 | import pickLiChildElements from './helpers/pickLiChildElements';
6 | import renderItems from './helpers/renderItems';
7 |
8 | function createNumberElement(_element, index) {
9 | return {
10 | tag: 'number',
11 | attributes: {
12 | index,
13 | },
14 | };
15 | }
16 |
17 | export function Ol({ style, childElements, renderElement }) {
18 | const liItems = pickLiChildElements(childElements);
19 | return (
20 |
21 | {renderItems(liItems, renderElement, createNumberElement)}
22 |
23 | );
24 | }
25 |
26 | Ol.propTypes = {
27 | ...ElementPropTypes,
28 | style: PropTypes.object,
29 | };
30 |
31 | Ol.defaultProps = {
32 | style: {},
33 | };
34 |
35 | export default combineMappers(mapElementProps)(Ol);
36 |
--------------------------------------------------------------------------------
/html/elements/list/Ul.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View } from '../../../components/View';
4 | import { combineMappers, ElementPropTypes, mapElementProps } from '../../Html';
5 | import pickLiChildElements from './helpers/pickLiChildElements';
6 | import renderItems from './helpers/renderItems';
7 |
8 | function createBulletElement() {
9 | return { tag: 'bullet' };
10 | }
11 |
12 | export function Ul({ style, childElements, renderElement }) {
13 | // TODO (Braco) - handle list-style-type from inlineStyle prop
14 | const liItems = pickLiChildElements(childElements);
15 | return (
16 |
17 | {renderItems(liItems, renderElement, createBulletElement)}
18 |
19 | );
20 | }
21 |
22 | Ul.propTypes = {
23 | ...ElementPropTypes,
24 | style: PropTypes.object,
25 | };
26 |
27 | Ul.defaultProps = {
28 | style: {},
29 | };
30 |
31 | export default combineMappers(mapElementProps)(Ul);
32 |
--------------------------------------------------------------------------------
/html/elements/list/helpers/pickLiChildElements.js:
--------------------------------------------------------------------------------
1 | export default childElements => childElements.filter(item => item.tag === 'li');
2 |
--------------------------------------------------------------------------------
/html/elements/list/helpers/renderItems.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 |
4 | export default function renderItems(
5 | childElements,
6 | renderElement,
7 | createPrefixElement,
8 | ) {
9 | const renderedComponents = _.reduce(
10 | childElements,
11 | (items, element, index) => {
12 | const { childElements: itemChildElements } = element;
13 |
14 | const prefix = createPrefixElement
15 | ? createPrefixElement(element, index)
16 | : null;
17 | const childElements = prefix
18 | ? [prefix, ...itemChildElements]
19 | : itemChildElements;
20 |
21 | const elem = {
22 | ...element,
23 | childElements,
24 | };
25 |
26 | items.push(renderElement(elem));
27 | return items;
28 | },
29 | [],
30 | );
31 |
32 | return React.Children.toArray(renderedComponents);
33 | }
34 |
--------------------------------------------------------------------------------
/html/elements/list/index.js:
--------------------------------------------------------------------------------
1 | import Bullet from './prefix/Bullet';
2 | import Number from './prefix/Number';
3 | import Li from './Li';
4 | import Ol from './Ol';
5 | import Ul from './Ul';
6 |
7 | export { Bullet, Li, Number, Ol, Ul };
8 |
--------------------------------------------------------------------------------
/html/elements/list/prefix/Bullet.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Text } from '../../../../components/Text';
4 |
5 | export default function BulletPrefix({ style }) {
6 | return • ;
7 | }
8 |
9 | BulletPrefix.propTypes = {
10 | style: PropTypes.object,
11 | };
12 |
13 | BulletPrefix.defaultProps = {
14 | style: undefined,
15 | };
16 |
--------------------------------------------------------------------------------
/html/elements/list/prefix/Number.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Text } from '../../../../components/Text';
4 | import { ElementPropTypes } from '../../../Html';
5 |
6 | export default function NumberPrefix({ element, style }) {
7 | const { index } = element.attributes;
8 |
9 | return {index}. ;
10 | }
11 |
12 | NumberPrefix.propTypes = {
13 | element: PropTypes.shape({ ...ElementPropTypes }),
14 | style: PropTypes.object,
15 | };
16 |
17 | NumberPrefix.defaultProps = {
18 | element: undefined,
19 | style: undefined,
20 | };
21 |
--------------------------------------------------------------------------------
/html/services/Dimensions.js:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 |
3 | export function resolveMaxWidth(style) {
4 | // parentContainerPadding - padding between screen & SimpleHtml. Parent container
5 | // can have it's own padding. If it does, we have to include it in calculation,
6 | // otherwise maxWidth will be more than max & images will go over the edge
7 | const parentContainerPadding = style.outerPadding || 0;
8 | const paddingValue =
9 | style.container.paddingLeft ||
10 | 0 + style.container.paddingRight ||
11 | 0 + parentContainerPadding;
12 |
13 | return Dimensions.get('window').width - paddingValue;
14 | }
15 |
16 | export function resolveDimensions(objectToResize, style) {
17 | const { width, height } = objectToResize;
18 |
19 | if (!width || !height) {
20 | return { width, height };
21 | }
22 |
23 | const maxWidth = resolveMaxWidth(style);
24 | const objectToResizeRatio = height / width;
25 | const resolvedWidth = width > maxWidth ? maxWidth : width;
26 | const resolvedHeight = Math.round(resolvedWidth * objectToResizeRatio);
27 |
28 | return { width: resolvedWidth, height: resolvedHeight };
29 | }
30 |
--------------------------------------------------------------------------------
/html/services/DomVisitors.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | // Shoutem RTE component wraps any video attachment (hosted inside iframe or video) with
4 | // element. That element contains style that breaks the SimpleHtml UI - enormous blank
5 | // space below attachment.
6 | // This function removes height and padding-bottom styles from figure to fix this UI issue.
7 | export const removeShoutemRteVideoAttachmentWrapper = element => {
8 | if (
9 | (element.name === 'figure' || element.name === 'video') &&
10 | element.children.length > 0 &&
11 | // can have multiple generated children too - type:text. Probably caused by empty space between
12 | // figure and iframe elements in generated HTML.
13 | _.some(
14 | element.children,
15 | ({ name }) => name === 'iframe' || name === 'video',
16 | )
17 | ) {
18 | // Remove height and padding-bottom styles because they break the UI
19 | element.attribs.style = element.attribs.style
20 | ? element.attribs.style
21 | .replace(/height:[^;]+;?/, '')
22 | .replace(/padding-bottom:[^;]+;?/, '')
23 | : '';
24 | }
25 |
26 | return element;
27 | };
28 |
29 | /**
30 | * Collection of node (HTML element) alteration callbacks. Define any html element
31 | * manipulations inside.
32 | * @param {*} element - HTML element to check against and manipulate if necessary
33 | * @returns Manipulated HTML element
34 | */
35 | export const onElement = element => {
36 | return removeShoutemRteVideoAttachmentWrapper(element);
37 | };
38 |
--------------------------------------------------------------------------------
/html/services/ElementRegistry.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | /**
4 | * Contains RN element class (description) for corresponding element tag.
5 | * @type {{ elementTag: { display } }} - elementTag: ElementClass
6 | */
7 | const ElementRegistry = {};
8 |
9 | /**
10 | * HTML elements have different display settings that affect React Native composition.
11 | * Use INLINE display for Text components that are stacked horizontally.
12 | * Use BLOCK display for any components that are stacked vertically.
13 | */
14 | export const Display = {
15 | INLINE: 1,
16 | BLOCK: 2,
17 | };
18 |
19 | export function registerElement(elementTag, elementClass) {
20 | ElementRegistry[elementTag] = elementClass;
21 | }
22 |
23 | export function getElement(element) {
24 | const { tag } = element;
25 | return ElementRegistry[tag];
26 | }
27 |
28 | export function getElementProperty(element, property) {
29 | return _.get(getElement(element), property);
30 | }
31 |
32 | export function getElementDisplay(element) {
33 | const elementClass = getElement(element);
34 | if (!elementClass) {
35 | // Element is not registered.
36 | return null;
37 | }
38 |
39 | const { display } = elementClass;
40 | if (!display) {
41 | // eslint-disable-next-line no-console
42 | console.warn(
43 | `Element ${element.tag} doesn't have defined "display" attribute.`,
44 | );
45 | return null;
46 | }
47 |
48 | return _.isFunction(display) ? display(element) : display;
49 | }
50 |
--------------------------------------------------------------------------------
/html/services/HTMLElementModels.js:
--------------------------------------------------------------------------------
1 | import { HTMLContentModel, HTMLElementModel } from 'react-native-render-html';
2 |
3 | export const videoModel = HTMLElementModel.fromCustomModel({
4 | contentModel: HTMLContentModel.block,
5 | tagName: 'video',
6 | isOpaque: true,
7 | });
8 |
--------------------------------------------------------------------------------
/html/services/getEmptyObjectKeys.js:
--------------------------------------------------------------------------------
1 | export default function getEmptyObjectKeys(obj) {
2 | return Object.keys(obj).reduce((arr, key) => {
3 | const val = String(obj[key]).trim();
4 |
5 | if (!val) {
6 | arr.push(key);
7 | }
8 |
9 | return arr;
10 | }, []);
11 | }
12 |
--------------------------------------------------------------------------------
/html/services/isValidVideoFormat.js:
--------------------------------------------------------------------------------
1 | export default function isValidVideoFormat(url) {
2 | if (url.includes('.3gp') || url.includes('.ogv') || url.includes('.webm')) {
3 | return false;
4 | }
5 |
6 | return true;
7 | }
8 |
--------------------------------------------------------------------------------
/init.js:
--------------------------------------------------------------------------------
1 | import { Theme } from '@shoutem/theme';
2 | import { defaultResolver as variableResolver } from './services';
3 | import getThemeStyle, { defaultThemeVariables } from './theme';
4 |
5 | function setDefaultThemeStyle() {
6 | variableResolver.setVariables(defaultThemeVariables)
7 |
8 | const theme = getThemeStyle();
9 |
10 | Theme.setDefaultThemeStyle(theme);
11 | }
12 |
13 | export { setDefaultThemeStyle };
14 |
--------------------------------------------------------------------------------
/scripts/add-native-deps.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 |
3 | const ROOT_PACKAGE_JSON_PATH = '../../../package.json';
4 | const UI_PACKAGE_JSON_PATH = './package.json';
5 |
6 | function insertNativeDependencies() {
7 | try {
8 | const rootPackageJson = fs.readJsonSync(ROOT_PACKAGE_JSON_PATH);
9 | const uiPackageJson = fs.readJsonSync(UI_PACKAGE_JSON_PATH);
10 |
11 | const uiNativeDependencies = uiPackageJson.nativeDependencies;
12 | const uiDependencies = uiPackageJson.dependencies;
13 | const nativeDeps = uiNativeDependencies.reduce(
14 | (o, dep) => ({ ...o, [dep]: uiDependencies[dep] }),
15 | {},
16 | );
17 |
18 | const newPackageJson = {
19 | ...rootPackageJson,
20 | dependencies: {
21 | ...rootPackageJson.dependencies,
22 | ...nativeDeps,
23 | },
24 | };
25 |
26 | fs.writeJsonSync(ROOT_PACKAGE_JSON_PATH, newPackageJson, { spaces: 2 });
27 | // eslint-disable-next-line no-console
28 | console.log(
29 | '[@shoutem/ui] - native dependencies added to root package.json',
30 | );
31 | } catch (e) {
32 | // eslint-disable-next-line no-console
33 | console.log(
34 | `[@shoutem/ui] - error adding dependencies to root package.json\n${e.message}`,
35 | );
36 | }
37 | }
38 |
39 | // eslint-disable-next-line no-console
40 | console.log('[@shoutem/ui] - adding native dependencies to root package.json');
41 |
42 | insertNativeDependencies();
43 |
--------------------------------------------------------------------------------
/services/index.js:
--------------------------------------------------------------------------------
1 | export { isAndroid, isIos, isWeb } from './platform';
2 | export { unavailableInWeb } from './unavailableInWeb';
3 | export {
4 | createScopedResolver,
5 | defaultResolver,
6 | resolveVariable,
7 | ThemeVariableResolver,
8 | } from './variableResolver';
9 |
--------------------------------------------------------------------------------
/services/platform.js:
--------------------------------------------------------------------------------
1 | import { Platform } from 'react-native';
2 |
3 | export const isAndroid = Platform.OS === 'android';
4 | export const isIos = Platform.OS === 'ios';
5 | export const isWeb = Platform.OS === 'web';
6 |
--------------------------------------------------------------------------------
/services/unavailableInWeb.js:
--------------------------------------------------------------------------------
1 | import { Platform } from 'react-native';
2 | import { Toast } from '@shoutem/ui';
3 | import _ from 'lodash';
4 |
5 | const isWeb = Platform.OS === 'web';
6 |
7 | export const unavailableInWeb = callback => {
8 | if (!isWeb) {
9 | return callback;
10 | }
11 |
12 | // If callback is undefined, show toast immediatelly.
13 | if (_.isUndefined(callback)) {
14 | Toast.showInfo({
15 | title: 'Feature currently unavailable',
16 | message:
17 | 'This feature is currently unavailable in Web Preview. For better preview experience download Disclose app.',
18 | });
19 | }
20 |
21 | // Otherwise, return callback to be executed on user action.
22 |
23 | return () =>
24 | Toast.showInfo({
25 | title: 'Feature currently unavailable',
26 | message:
27 | 'This feature is currently unavailable in Web Preview. For better preview experience download Disclose app.',
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/web/style/Alert.scss:
--------------------------------------------------------------------------------
1 | .alert-container {
2 | border-bottom: 1px solid rgba(0,0,0,0.1);
3 | font-size: 15px;
4 | }
5 |
6 | .alert-popup {
7 | border-radius: 18px;
8 | padding-bottom: 0px;
9 | overflow: hidden;
10 | }
11 |
12 | .alert-title {
13 | color: black;
14 | font-size: 20px;
15 | margin-top: 10px;
16 | margin-bottom: -10px;
17 | }
18 |
19 | .alert-actions {
20 | border-top: 1px solid #DEDEDE;
21 | margin: 20px 0px 0px 0px;
22 | }
23 |
24 | .alert-confirm {
25 | color: cornflowerblue;
26 | background-color: transparent;
27 | margin: 0;
28 | height: 100%;
29 | border-radius: 0;
30 | padding: 15px;
31 | }
32 |
33 | .alert-cancel {
34 | color: cornflowerblue;
35 | background-color: transparent;
36 | margin: 0;
37 | height: 100%;
38 | border-radius: 0;
39 | padding: 15px;
40 | }
41 |
42 | .alert-deny {
43 | color: red;
44 | background-color: transparent;
45 | margin: 0;
46 | height: 100%;
47 | border-radius: 0;
48 | padding: 15px;
49 | }
50 |
51 | .full-width {
52 | width: 100%;
53 | }
54 |
55 | .half-width {
56 | width: 50%;
57 | }
58 |
59 | .button-height {
60 | height: 50px;
61 | }
62 |
63 | .border-right {
64 | border-right: 1px solid #DEDEDE;
65 | }
66 |
67 | .border-bottom {
68 | border-bottom: 1px solid #DEDEDE;
69 | }
70 |
--------------------------------------------------------------------------------