UNPKG

@cantoo/rn-svg

Version:
311 lines (299 loc) 8.72 kB
import React from 'react'; import { StyleSheet } from 'react-native'; import { unstable_createElement as uce } from 'react-native-web'; import useElementLayout from 'react-native-web/dist/modules/useElementLayout'; import usePlatformMethods from 'react-native-web/dist/modules/usePlatformMethods'; import useResponderEvents from 'react-native-web/dist/modules/useResponderEvents'; import useMergeRefs from 'react-native-web/dist/modules/useMergeRefs'; import * as forwardedProps from 'react-native-web/dist/modules/forwardedProps'; import pick from 'react-native-web/dist/modules/pick'; const forwardPropsList = { ...forwardedProps.defaultProps, ...forwardedProps.accessibilityProps, ...forwardedProps.clickProps, ...forwardedProps.focusProps, ...forwardedProps.keyboardProps, ...forwardedProps.mouseProps, ...forwardedProps.touchProps, ...forwardedProps.styleProps }; const pickProps = props => pick(props, forwardPropsList); // rgba values inside range 0 to 1 inclusive // rgbaArray = [r, g, b, a] // argb values inside range 0x00 to 0xff inclusive // int32ARGBColor = 0xaarrggbb /* ColumnMajorTransformMatrix [a, b, c, d, tx, ty] This matrix can be visualized as: ╔═ ═╗ ║ a c tx ║ ║ b d ty ║ ║ 0 0 1 ║ ╚═ ═╝ */ const SvgXml = /*#__PURE__*/React.forwardRef(({ xml, ...props }, forwardedRef) => { const { innerSVG, svgAttributes } = React.useMemo(() => { const { attributes, innerHTML } = parseSVG(xml || ''); return { innerSVG: innerHTML, svgAttributes: kebabToCamel(attributes) }; }, [xml]); const svgRef = React.useRef(null); React.useLayoutEffect(() => { if (!svgRef.current) { return; } svgRef.current.innerHTML = innerSVG; }, [innerSVG]); const { height, width, viewBox, preserveAspectRatio, title, opacity, fill, fillOpacity, fillRule, transform, stroke, strokeWidth, strokeOpacity, strokeDasharray, strokeDashoffset, strokeLinecap, strokeLinejoin, strokeMiterlimit, clipRule, clipPath, vectorEffect, id, markerStart, markerMid, markerEnd, mask, originX, originY, translate, scale, rotation, skewX, skewY, pointerEvents = 'none', style, // props that should be applyed to the View container ...containerProps } = props; const svgTransform = React.useMemo(() => { const transformArray = []; if (originX != null || originY != null) { transformArray.push(`translate(${originX || 0}, ${originY || 0})`); } if (translate != null) { transformArray.push(`translate(${translate})`); } if (scale != null) { transformArray.push(`scale(${scale})`); } // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block) if (rotation != null) { transformArray.push(`rotate(${rotation})`); } if (skewX != null) { transformArray.push(`skewX(${skewX})`); } if (skewY != null) { transformArray.push(`skewY(${skewY})`); } if (originX != null || originY != null) { transformArray.push(`translate(${-(originX || 0)}, ${-(originY || 0)})`); } if (transform) { transformArray.push(transform); } return transformArray.length ? transformArray.join(' ') : undefined; }, [originX, originY, rotation, scale, skewX, skewY, transform, translate]); const containerStyle = React.useMemo(() => { const [,, widthBox, heightBox] = (viewBox || '').split(' '); const { width: styleWidth, height: styleHeight, ...otherStyle } = StyleSheet.flatten(style) || {}; return [styleSheet.view$raw, removeUndefined({ ...otherStyle, display: 'inline-flex', width: parseDimension(width ?? styleWidth ?? svgAttributes.width ?? widthBox), height: parseDimension(height ?? styleHeight ?? svgAttributes.height ?? heightBox) })]; }, [svgAttributes.height, svgAttributes.width, height, style, viewBox, width]); // these props should override the xml props const overrideProps = React.useMemo(() => ({ ...removeUndefined(svgAttributes), ...removeUndefined({ style: styleSheet.svg, transform: svgTransform, viewBox, preserveAspectRatio, title, opacity, fill, fillOpacity, fillRule, stroke, strokeWidth, strokeOpacity, strokeDasharray, strokeDashoffset, strokeLinecap, strokeLinejoin, strokeMiterlimit, clipRule, clipPath, vectorEffect, pointerEvents, id, markerStart, markerMid, markerEnd, mask }), width: undefined, height: undefined }), [svgAttributes, svgTransform, viewBox, preserveAspectRatio, title, opacity, fill, fillOpacity, fillRule, stroke, strokeWidth, strokeOpacity, strokeDasharray, strokeDashoffset, strokeLinecap, strokeLinejoin, strokeMiterlimit, clipRule, clipPath, vectorEffect, pointerEvents, id, markerStart, markerMid, markerEnd, mask]); const Svg = uce('svg', { ref: svgRef, ...overrideProps }); const { onLayout, onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderEnd, onResponderGrant, onResponderMove, onResponderReject, onResponderRelease, onResponderStart, onResponderTerminate, onResponderTerminationRequest, onStartShouldSetResponder, onStartShouldSetResponderCapture, ...finalProps } = containerProps; const finalContainerProps = pickProps({ ...finalProps, children: Svg, style: containerStyle }); const hostRef = React.useRef(null); useElementLayout(hostRef, onLayout); const responderConfig = React.useMemo(() => ({ onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderEnd, onResponderGrant, onResponderMove, onResponderReject, onResponderRelease, onResponderStart, onResponderTerminate, onResponderTerminationRequest, onStartShouldSetResponder, onStartShouldSetResponderCapture }), [onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderEnd, onResponderGrant, onResponderMove, onResponderReject, onResponderRelease, onResponderStart, onResponderTerminate, onResponderTerminationRequest, onStartShouldSetResponder, onStartShouldSetResponderCapture]); useResponderEvents(hostRef, responderConfig); const platformMethodsRef = usePlatformMethods(finalContainerProps); const setRef = useMergeRefs(hostRef, platformMethodsRef, forwardedRef); finalContainerProps.ref = setRef; return uce('span', finalContainerProps); }); SvgXml.displayName = 'Svg'; export default SvgXml; /** polyfill for Node < 12 */ function matchAll(str) { return re => { const matches = []; let groups; while (groups = re.exec(str)) { matches.push(groups); } return matches; }; } function parseSVG(svg) { const contentMatch = svg.match(/<svg(.*)<\/svg>/ims); const content = contentMatch ? contentMatch[1] : ''; const [, attrs, innerHTML] = content.match(/(.*?)>(.*)/ims) || ['', '', '']; const attributes = [...matchAll(attrs)(/([a-z0-9-]+)(=['"](.*?)['"])?/gims)].map(([, key,, value]) => ({ [key]: value })); return { attributes, innerHTML }; } const styleSheet = StyleSheet.create({ svg: { flexGrow: 1, flexShrink: 1 }, view$raw: { alignItems: 'stretch', backgroundColor: 'transparent', // Hack because those are web props unknown from react-native ['border']: '0 solid black', ['boxSizing']: 'border-box', display: 'inline-flex', flexBasis: 'auto', flexDirection: 'column', color: 'inherit', flexShrink: 0, margin: 0, minHeight: 0, minWidth: 0, padding: 0, position: 'relative', zIndex: 0 } }); function kebabToCamel(attrs) { const camelObj = {}; attrs.forEach(attr => { const key = Object.keys(attr)[0]; camelObj[key.replace(/-./g, x => x.toUpperCase()[1])] = attr[key]; }); return camelObj; } function removeUndefined(obj) { const finalObj = {}; Object.keys(obj).forEach(key => { const value = obj[key]; if (value !== undefined) { finalObj[key] = value; } }); return finalObj; } /** Ensure that the dimension is valid for React Native */ function parseDimension(dimension) { if (typeof dimension === 'number') { return dimension; } else if (!dimension) { return dimension; } else { return dimension.trim().match(/^\d+(\.\d+)?$/) ? Number.parseFloat(dimension) : dimension; } } //# sourceMappingURL=SvgXml.web.js.map