-
Notifications
You must be signed in to change notification settings - Fork 455
/
Copy pathHtml.js
273 lines (236 loc) · 7.37 KB
/
Html.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import React, { PureComponent } from 'react';
import autoBindReact from 'auto-bind/react';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { connectStyle } from '@shoutem/theme';
import { Spinner } from '../components/Spinner';
import { View } from '../components/View';
import {
Display,
getElement,
getElementDisplay,
getElementProperty,
registerElement,
} from './services/ElementRegistry';
import { parseHtml } from './services/HtmlParser';
const defaultElementSettings = {
display: Display.BLOCK,
};
class Html extends PureComponent {
/**
* Create Element class for given element tag and add it to the ElementClassMap.
* Use the settings to additionally describe a Element class.
* @param elementTag {string} HTML element tag
* @param component {Component} React Native Component
* @param settings {Object} Default settings override
* Most times a developer will only want to override one setting,
* that's why settings are merged with defaultElementSettings.
* @param settings.display {Display || Function}
* Describe component display.
* Can be a function to dynamically resolve display.
*/
static registerElement(elementTag, component, settings = {}) {
const elementSettings = _.assign({}, defaultElementSettings, settings);
registerElement(elementTag, { ...elementSettings, component });
}
static getElement(tag) {
// TODO - standardize ElementRegistry getElement
return getElement({ tag });
}
static getDerivedStateFromProps(props, state) {
const htmlTree = props.body ? parseHtml(props.body) : false;
if (htmlTree && state.htmlTree === htmlTree) {
return state;
}
return { htmlTree };
}
constructor(props, context) {
super(props, context);
autoBindReact(this);
this.state = {
htmlTree: null,
};
}
/**
* Get element style from the Html instance style.
* @param element {Element}
* @returns {Object|undefined}
*/
getElementStyle({ tag }) {
const { style } = this.props;
return _.get(style, tag, {});
}
/**
* Render HTML element as React Native component.
* This method is passed to both custom renderElement and
* element corresponding component. It is also used to render children
* and should be passed down the tree so that children can be rendered.
* If Html has style named by element tag it will be passed to rendered element.
* @param element {Element} Parsed HTML element
* @returns {Component} The element rendered as a React Native component
*/
renderElement(element) {
const { renderElement } = this.props;
const elementStyle = this.getElementStyle(element);
let renderedElement;
if (renderElement) {
renderedElement = renderElement(
element,
elementStyle,
this.renderElement,
);
}
// Custom renderElement for the specific Html implementation
// has advantage over the "global". If custom renderElement rendered
// a component that component will be used, otherwise fallback to "global".
// Render element must be undefined to fallback to default,
// null is a valid RN type to render.
if (_.isUndefined(renderedElement)) {
const ElementComponent = getElementProperty(element, 'component');
if (!ElementComponent) {
// eslint-disable-next-line no-console
console.warn('Can not find component for element: ', element.tag);
return null;
}
renderedElement = (
<ElementComponent
element={element}
style={elementStyle}
renderElement={this.renderElement}
/>
);
}
return renderedElement;
}
render() {
const { style, body } = this.props;
const { htmlTree } = this.state;
if (!body) {
return null;
}
if (!htmlTree) {
// Either still processing the Html or
// waiting for layout animations to complete
return (
<View styleName="md-gutter">
<Spinner styleName="sm-gutter" />
</View>
);
}
const htmlRootElement = htmlTree.getRootNode();
return (
<View style={style.container}>{this.renderElement(htmlRootElement)}</View>
);
}
}
Html.propTypes = {
body: PropTypes.string.isRequired,
style: PropTypes.object.isRequired,
renderElement: PropTypes.func,
};
Html.defaultProps = {
renderElement: undefined,
};
export const ElementPropTypes = {
childElements: PropTypes.array,
renderElement: PropTypes.func,
inlineStyle: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
export default connectStyle('shoutem.ui.Html')(Html);
/* Helpers */
/**
* @param element {Element}
* @returns {boolean}
*/
export const isBlockElement = function(element) {
// eslint-disable-next-line no-use-before-define
return getElementDisplay(element, 'display') === Display.BLOCK;
};
/**
* @param elements {Array}
* @returns {boolean}
*/
export const hasBlockElement = function(elements) {
return _.some(elements, isBlockElement);
};
/**
* Use to create an enhanced component that mapS
* element (description) to the wrapped component props.
* Element is default property that Html renderElement provides to the components.
* @param mapFunctions {Array}
* List of functions that map element description to the component props.
* @returns {function({element, renderElement}): Component}
* Returns HOC that will map component props with provided map functions.
*/
export const combineMappers = function(...mapFunctions) {
return WrappedComponent => props => {
// eslint-disable-next-line prefer-arrow-callback
const customizedProps = _.reduce(
mapFunctions,
function(result, mapFunction) {
return {
...result,
...mapFunction(props),
};
},
{ ...props },
);
return <WrappedComponent {...customizedProps} />;
};
};
/**
* Destruct an element description to the component props format.
* @param props {{ element, renderElement }}
* @returns {Object}
*/
export const mapElementProps = function({ element, style }) {
const { childElements, attributes, tag } = element;
return {
...attributes,
style,
childElements,
htmlInlineStyle: attributes.style,
elementTag: tag,
};
};
/**
* @param childElements {Array}
* @param renderElement {Function}
* @returns {Children}
*/
export const renderChildElements = function(childElements, renderElement) {
return React.Children.toArray(childElements.map(renderElement));
};
/**
* Render and map elements to the children prop.
* @param element {Element}
* @param renderElement {Function}
* @returns {Object} Props with children prop
*/
export const renderChildren = function({ element, renderElement }) {
const { childElements } = element;
return {
children: renderChildElements(childElements, renderElement),
};
};
/**
* Extend the original renderElement with a customizer.
* If the customizer doesn't render a element, renderElement will be used.
* It can be used to customize renderElement from certain element node.
* @param customizer {Function}
* @param renderElement {Function}
* @returns {Component}
*/
export const customizeRenderElement = function(customizer, renderElement) {
return function(element) {
const renderedElement = customizer(element);
if (renderedElement) {
return renderedElement;
}
return renderElement(element);
};
};