@shoutem/ui 6.2.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
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 <Image source={source} key={id} style={resolvedStyle} />;
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;
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import YoutubePlayer from 'react-native-youtube-iframe';
3
+ import iframe from '@native-html/iframe-plugin';
4
+ import PropTypes from 'prop-types';
5
+ import { Text } from '../../components/Text';
6
+ import { View } from '../../components/View';
7
+ import isValidVideoFormat from '../services/isValidVideoFormat';
8
+
9
+ const IframeRenderer = props => {
10
+ const { tnode, shoutemStyle, unsupportedVideoFormatMessage } = props;
11
+ const url = tnode?.domNode?.attribs?.src;
12
+
13
+ if (url && !isValidVideoFormat(url)) {
14
+ const message =
15
+ unsupportedVideoFormatMessage || 'Unsupported video format.';
16
+
17
+ return (
18
+ <View
19
+ style={shoutemStyle.fallback}
20
+ styleName="vertical h-center v-center"
21
+ >
22
+ <Text>{message}</Text>
23
+ </View>
24
+ );
25
+ }
26
+
27
+ if (url && (url.includes('youtube') || url.includes('youtu.be'))) {
28
+ const youtubeIdRegEx = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
29
+ const regExMatch = url.match(youtubeIdRegEx);
30
+ const youtubeId =
31
+ regExMatch && regExMatch[7].length === 11 ? regExMatch[7] : false;
32
+
33
+ return (
34
+ <YoutubePlayer
35
+ webViewProps={{
36
+ renderToHardwareTextureAndroid: true,
37
+ }}
38
+ height={shoutemStyle.video.height}
39
+ videoId={youtubeId}
40
+ />
41
+ );
42
+ }
43
+
44
+ return iframe(props);
45
+ };
46
+
47
+ IframeRenderer.propTypes = {
48
+ tnode: PropTypes.shape({
49
+ children: PropTypes.any,
50
+ }).isRequired,
51
+ shoutemStyle: PropTypes.any,
52
+ unsupportedVideoFormatMessage: PropTypes.string,
53
+ };
54
+
55
+ IframeRenderer.defaultProps = {
56
+ shoutemStyle: undefined,
57
+ unsupportedVideoFormatMessage: undefined,
58
+ };
59
+
60
+ export default IframeRenderer;
@@ -1,57 +1,22 @@
1
1
  import React, { PureComponent } from 'react';
2
- import { Dimensions, Linking } from 'react-native';
3
- import Html from 'react-native-render-html';
4
- import { iframe } from 'react-native-render-html/src/HTMLRenderers';
5
- import {
6
- cssObjectToString,
7
- cssStringToObject,
8
- } from 'react-native-render-html/src/HTMLStyles';
9
- import {
10
- alterNode as tableAlterNode,
2
+ import { Linking } from 'react-native';
3
+ import Html, { defaultSystemFonts } from 'react-native-render-html';
4
+ import WebView from 'react-native-webview';
5
+ import { iframeModel } from '@native-html/iframe-plugin';
6
+ import table, {
11
7
  cssRulesFromSpecs,
12
8
  defaultTableStylesSpecs,
13
9
  IGNORED_TAGS,
14
- makeTableRenderer,
15
- } from 'react-native-render-html-table-bridge';
16
- import WebView from 'react-native-webview';
17
- import YoutubePlayer from 'react-native-youtube-iframe';
10
+ tableModel,
11
+ } from '@native-html/table-plugin';
18
12
  import autoBindReact from 'auto-bind/react';
19
13
  import _ from 'lodash';
20
14
  import PropTypes from 'prop-types';
21
15
  import { connectStyle } from '@shoutem/theme';
22
- import { Text } from '../../components/Text';
23
16
  import { View } from '../../components/View';
24
- import getEmptyObjectKeys from '../services/getEmptyObjectKeys';
25
- import isValidVideoFormat from '../services/isValidVideoFormat';
26
- import Image from './Image';
27
-
28
- function resolveMaxWidth(style) {
29
- // parentContainerPadding - padding between screen & SimpleHtml. Parent container
30
- // can have it's own padding. If it does, we have to include it in calculation,
31
- // otherwise maxWidth will be more than max & images will go over the edge
32
- const parentContainerPadding = style.outerPadding || 0;
33
- const paddingValue =
34
- style.container.paddingLeft ||
35
- 0 + style.container.paddingRight ||
36
- 0 + parentContainerPadding;
37
-
38
- return Dimensions.get('window').width - paddingValue;
39
- }
40
-
41
- function resolveDimensions(objectToResize, style) {
42
- const { width, height } = objectToResize;
43
-
44
- if (!width || !height) {
45
- return { width, height };
46
- }
47
-
48
- const maxWidth = resolveMaxWidth(style);
49
- const objectToResizeRatio = height / width;
50
- const resolvedWidth = width > maxWidth ? maxWidth : width;
51
- const resolvedHeight = Math.round(resolvedWidth * objectToResizeRatio);
52
-
53
- return { width: resolvedWidth, height: resolvedHeight };
54
- }
17
+ import { resolveMaxWidth } from '../services/Dimensions';
18
+ import AttachmentRenderer from './AttachmentRenderer';
19
+ import IframeRenderer from './IframeRenderer';
55
20
 
56
21
  class SimpleHtml extends PureComponent {
57
22
  constructor(props) {
@@ -60,7 +25,7 @@ class SimpleHtml extends PureComponent {
60
25
  autoBindReact(this);
61
26
  }
62
27
 
63
- onLinkPress(evt, href) {
28
+ onLinkPress(_evt, href) {
64
29
  const { customHandleLinkPress } = this.props;
65
30
 
66
31
  return customHandleLinkPress
@@ -68,190 +33,15 @@ class SimpleHtml extends PureComponent {
68
33
  : Linking.openURL(href);
69
34
  }
70
35
 
71
- /**
72
- * Removes empty (therefore invalid) style attribute properties
73
- * Scales down objects with specified width and height if too large
74
- * Removes padding and height from suneditor for 'figure' tag that it nests
75
- * video iframe tags in when video format is unsupported
76
- */
77
- alterNode(node) {
78
- const { customAlterNode, style } = this.props;
79
-
80
- const styleAttrib = _.get(node, 'attribs.style', '').trim();
81
-
82
- if (customAlterNode && _.isFunction(customAlterNode)) {
83
- const resolvedNode = customAlterNode(
84
- node,
85
- cssObjectToString,
86
- cssStringToObject,
87
- );
88
-
89
- if (resolvedNode) {
90
- return resolvedNode;
91
- }
92
- }
93
-
94
- if (node.name === 'table') {
95
- return tableAlterNode(node);
96
- }
97
-
98
- if (node.name === 'figure') {
99
- const firstChild = _.head(node.children);
100
-
101
- if (firstChild.name === 'iframe') {
102
- const nodeStyle = cssStringToObject(styleAttrib);
103
- const source = _.get(firstChild, 'attribs.src', '');
104
-
105
- const resolvedNodeStyle = isValidVideoFormat(source)
106
- ? _.omit(nodeStyle, ['height', 'padding-bottom'])
107
- : nodeStyle;
108
-
109
- node.attribs.style = cssObjectToString(resolvedNodeStyle);
110
-
111
- return node;
112
- }
113
- }
114
-
115
- const nodeWidth = node.attribs?.width;
116
-
117
- if (!styleAttrib && !nodeWidth) {
118
- return false;
119
- }
120
-
121
- const nodeHeight = node.attribus?.height;
122
- const nodeDimensions = { width: nodeWidth, height: nodeHeight };
123
- const { width, height } = resolveDimensions(nodeDimensions, style);
124
-
125
- const nodeStyle = cssStringToObject(styleAttrib);
126
- const invalidKeys = getEmptyObjectKeys(nodeStyle);
127
-
128
- if (invalidKeys.length || nodeWidth) {
129
- const styleFiltered = _.omit(style, invalidKeys);
130
- node.attribs.style = cssObjectToString(styleFiltered);
131
- node.attribs.width = width;
132
- node.attribs.height = height;
133
-
134
- return node;
135
- }
136
-
137
- return false;
138
- }
139
-
140
- /**
141
- * Removes child break node in case when it's positioned at the end
142
- * of parent div node and div has content in it.
143
- * Example:
144
- * <div>first line<br></div>
145
- * <div>second line<br></div>
146
- *
147
- * If br is left as is, <Html> will show empty line under div, not just
148
- * break into new line
149
- */
150
- alterChildren(node) {
151
- const { children, name } = node;
152
-
153
- if ((name === 'div' || name === 'p') && children && children.length > 1) {
154
- const brNodes = _.filter(children, { name: 'br' });
155
- const lastBrNode = _.last(brNodes);
156
-
157
- if (lastBrNode && lastBrNode.next === null) {
158
- const lastBrNodeIndex = _.indexOf(children, lastBrNode);
159
- return children.splice(0, lastBrNodeIndex);
160
- }
161
- }
162
-
163
- return children;
164
- }
165
-
166
- renderUnorderedListPrefix() {
167
- const { style } = this.props;
168
-
169
- return <Text style={style.prefix}>• </Text>;
170
- }
171
-
172
- renderOrderedListPrefix(
173
- htmlAttribs,
174
- children,
175
- convertedCSSStyles,
176
- passProps,
177
- ) {
178
- const { style } = this.props;
179
-
180
- return <Text style={style.prefix}>{passProps.index + 1}. </Text>;
181
- }
182
-
183
- /**
184
- * Custom rendered method for handling <attachment> tags
185
- * Currently only supports rendering image attachments.
186
- */
187
- renderAttachments({ id, type }) {
188
- const { attachments, style } = this.props;
189
-
190
- if (!attachments) {
191
- return null;
192
- }
193
-
194
- if (type === 'image') {
195
- const image = _.find(attachments, { id });
196
-
197
- if (image && image.src) {
198
- const source = { uri: image.src };
199
- const imageSize = { width: image.width, height: image.height };
200
- const { height, width } = resolveDimensions(imageSize, style);
201
- const resolvedStyle = { alignSelf: 'center', height, width };
202
-
203
- return <Image source={source} key={id} style={resolvedStyle} />;
204
- }
205
- }
206
-
207
- return null;
208
- }
209
-
210
- renderIframe(htmlAttribs, children, convertedCSSStyles, passProps) {
211
- const { style } = this.props;
212
-
213
- const url = htmlAttribs?.src;
214
-
215
- if (url && !isValidVideoFormat(url)) {
216
- const { unsupportedVideoFormatMessage } = this.props;
217
-
218
- const message =
219
- unsupportedVideoFormatMessage || 'Unsupported video format.';
220
-
221
- return (
222
- <View
223
- style={style.fallback}
224
- styleName="vertical h-center v-center"
225
- key={passProps.key}
226
- >
227
- <Text>{message}</Text>
228
- </View>
229
- );
230
- }
231
-
232
- if (url && (url.includes('youtube') || url.includes('youtu.be'))) {
233
- const youtubeIdRegEx = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
234
- const regExMatch = url.match(youtubeIdRegEx);
235
- const youtubeId =
236
- regExMatch && regExMatch[7].length === 11 ? regExMatch[7] : false;
237
-
238
- return (
239
- <YoutubePlayer
240
- webViewProps={{
241
- renderToHardwareTextureAndroid: true,
242
- }}
243
- height={style.video.height}
244
- key={passProps.key}
245
- videoId={youtubeId}
246
- />
247
- );
248
- }
249
-
250
- return iframe(htmlAttribs, children, convertedCSSStyles, passProps);
251
- }
252
-
253
36
  render() {
254
- const { style, body, customTagStyles, ...otherProps } = this.props;
37
+ const {
38
+ style,
39
+ body,
40
+ customTagStyles,
41
+ unsupportedVideoFormatMessage,
42
+ attachments,
43
+ ...otherProps
44
+ } = this.props;
255
45
 
256
46
  const maxWidth = resolveMaxWidth(style);
257
47
 
@@ -260,11 +50,6 @@ class SimpleHtml extends PureComponent {
260
50
  ...customTagStyles,
261
51
  };
262
52
 
263
- const listPrefixRenderers = {
264
- ul: this.renderUnorderedListPrefix,
265
- ol: this.renderOrderedListPrefix,
266
- };
267
-
268
53
  const tableStyle = _.get(style, 'table', {});
269
54
  const tableCssStyle = _.get(style, 'tableCss', '');
270
55
  const cssStyle = cssRulesFromSpecs({
@@ -274,23 +59,48 @@ class SimpleHtml extends PureComponent {
274
59
  const cssRules = `${cssStyle}${tableCssStyle}`;
275
60
 
276
61
  const customRenderers = {
277
- iframe: this.renderIframe,
278
- table: makeTableRenderer({ WebViewComponent: WebView, cssRules }),
279
- attachment: this.renderAttachments,
62
+ iframe: props => (
63
+ <IframeRenderer
64
+ {...props}
65
+ shoutemStyle={style}
66
+ unsupportedVideoFormatMessage={unsupportedVideoFormatMessage}
67
+ />
68
+ ),
69
+ table,
70
+ attachment: props => (
71
+ <AttachmentRenderer
72
+ {...props}
73
+ attachments={attachments}
74
+ style={style}
75
+ />
76
+ ),
280
77
  };
281
78
 
282
79
  const htmlProps = {
283
- html: body,
284
- imagesMaxWidth: maxWidth,
285
- staticContentMaxWidth: maxWidth,
286
- tagsStyles: tagStyles,
287
- baseFontStyle: style.baseFont,
288
- ignoredStyles: ['font-family', 'letter-spacing', 'transform'],
289
- onLinkPress: this.onLinkPress,
290
- alterNode: this.alterNode,
291
- alterChildren: this.alterChildren,
292
- listsPrefixesRenderers: listPrefixRenderers,
80
+ source: { html: body },
81
+ computeEmbeddedMaxWidth: () => maxWidth,
82
+ contentWidth: maxWidth,
83
+ tagsStyles: _.omitBy(tagStyles, tagStyle => !tagStyle),
84
+ systemFonts: [...defaultSystemFonts, style.baseFont.fontFamily],
85
+ baseStyle: style.baseFont,
86
+ ignoredStyles: ['fontFamily', 'letterSpacing', 'transform'],
293
87
  renderers: customRenderers,
88
+ renderersProps: {
89
+ table: {
90
+ cssRules,
91
+ },
92
+ a: { onPress: this.onLinkPress },
93
+ iframe: {
94
+ webViewProps: {
95
+ renderToHardwareTextureAndroid: true,
96
+ },
97
+ },
98
+ },
99
+ customHTMLElementModels: {
100
+ table: tableModel,
101
+ iframe: iframeModel,
102
+ },
103
+ WebView,
294
104
  ignoredTags: IGNORED_TAGS,
295
105
  };
296
106
 
@@ -0,0 +1,29 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoutem/ui",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "Styleable set of components for React Native applications",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
@@ -16,6 +16,8 @@
16
16
  "@shoutem/animation": "~0.15.0",
17
17
  "@shoutem/eslint-config-react": "~1.0.6",
18
18
  "@shoutem/theme": "~0.13.0",
19
+ "@native-html/iframe-plugin": "2.6.1",
20
+ "@native-html/table-plugin": "5.3.1",
19
21
  "auto-bind": "4.0.0",
20
22
  "babel-plugin-transform-decorators-legacy": "1.3.5",
21
23
  "buffer": "5.6.0",
@@ -33,8 +35,7 @@
33
35
  "react-native-lightbox-v2": "0.9.0",
34
36
  "react-native-linear-gradient": "2.6.2",
35
37
  "react-native-modal": "13.0.1",
36
- "react-native-render-html": "4.2.5",
37
- "react-native-render-html-table-bridge": "0.6.0",
38
+ "react-native-render-html": "6.3.4",
38
39
  "react-native-svg": "12.3.0",
39
40
  "react-native-svg-transformer": "1.0.0",
40
41
  "react-native-toast-message": "2.1.5",
@@ -66,7 +67,7 @@
66
67
  "path": "0.12.7",
67
68
  "prettier": "1.19.1",
68
69
  "react": "17.0.2",
69
- "react-dom": "16.9.0",
70
+ "react-dom": "17.0.2",
70
71
  "react-native": "0.68.2",
71
72
  "react-test-renderer": "17.0.2"
72
73
  },
@@ -134,4 +135,4 @@
134
135
  "!/node_modules/@shoutem"
135
136
  ]
136
137
  }
137
- }
138
+ }