import * as React from 'react' ; import ITemplateRendererProps from './ITemplateRendererProps' ; import ITemplateRendererState from './ITemplateRendererState' ; import './TemplateRenderer.scss' ; import { isEqual } from "@microsoft/sp-lodash-subset" ; import * as DOMPurify from 'dompurify' ; import { DomPurifyHelper } from '../../helpers/DomPurifyHelper' ; import { TestConstants } from '../../common/Constants' ; import { IDataResultsTemplateContext } from '../../models/common/ITemplateContext' ; const rootCssClassName = "pnp-modern-search" ; const TEMPLATE_ID_PREFIX = "pnp-template_" ; export class TemplateRenderer extends React . Component < ITemplateRendererProps , ITemplateRendererState > { private _domPurify : any ; constructor ( props ) { super ( props ) ; this . state = { processedTemplate : null } ; this . _domPurify = DOMPurify . default ; this . _domPurify . setConfig ( { ADD_TAGS : [ 'style' ] , ADD_ATTR : [ 'target' , 'loading' ] , ALLOW_DATA_ATTR : true , ALLOWED_URI_REGEXP : /^(?:(?:(?:f|ht)tps?|mailto|file|tel|callto|cid|xmpp|xxx|ms-\w+):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i , WHOLE_DOCUMENT : true , } ) ; this . _domPurify . addHook ( 'uponSanitizeElement' , DomPurifyHelper . allowCustomComponentsHook ) ; this . _domPurify . addHook ( 'uponSanitizeAttribute' , DomPurifyHelper . allowCustomAttributesHook ) ; } public render ( ) { if ( this . state . processedTemplate ) { return < div className = { rootCssClassName } dangerouslySetInnerHTML = { { __html : this . state . processedTemplate } } > < /div>; } else { return null ; } } public async componentDidMount ( ) { await this . updateTemplate ( this . props ) ; } public async componentDidUpdate ( prevProps ) { if ( ! isEqual ( prevProps . templateContent , this . props . templateContent ) || ! isEqual ( ( prevProps . templateContext as IDataResultsTemplateContext ) . data , ( this . props . templateContext as IDataResultsTemplateContext ) . data ) || ! isEqual ( prevProps . templateContext . filters , this . props . templateContext . filters ) || ! isEqual ( prevProps . templateContext . properties , this . props . templateContext . properties ) || ! isEqual ( prevProps . templateContext . theme , this . props . templateContext . theme ) || ! isEqual ( ( prevProps . templateContext as IDataResultsTemplateContext ) . selectedKeys , ( this . props . templateContext as IDataResultsTemplateContext ) . selectedKeys ) ) { await this . updateTemplate ( this . props ) ; } } private async updateTemplate ( props ) { let templateContent = props . templateContent ; let template = await this . props . templateService . processTemplate ( props . templateContext , templateContent ) ; if ( template ) { template = template ? this . _domPurify . sanitize ( `${ template }` ) : template ; const templateAsHtml = new DOMParser ( ) . parseFromString ( template , "text/html" ) ; const styleElements = templateAsHtml . getElementsByTagName ( "style" ) ; let prefixedStyles = [ ] ; let i , j , k = 0 ; if ( styleElements . length > 0 ) { const elementPrefixId = `${ TEMPLATE_ID_PREFIX }${ this . props . instanceId }` ; for ( i = 0 ; i < styleElements . length ; i ++ ) { const style = styleElements . item ( i ) ; const sheet = style . sheet ; if ( ( sheet as CSSStyleSheet ) . cssRules ) { const cssRules = ( sheet as CSSStyleSheet ) . cssRules ; for ( j = 0 ; j < cssRules . length ; j ++ ) { const cssRule = cssRules . item ( j ) ; if ( ( cssRule as CSSMediaRule ) . media ) { const cssMediaRule = cssRule as CSSMediaRule ; let cssPrefixedMediaRules = '' ; for ( k = 0 ; k < cssMediaRule . cssRules . length ; k ++ ) { const cssRuleMedia = cssMediaRule . cssRules . item ( k ) ; cssPrefixedMediaRules += `#${ elementPrefixId } ${ cssRuleMedia . cssText }` ; } prefixedStyles . push ( `@media ${ cssMediaRule . conditionText } { ${ cssPrefixedMediaRules } }` ) ; } else { if ( cssRule . cssText . indexOf ( TestConstants . SearchResultsErrorMessage ) !== - 1 ) { prefixedStyles . push ( `${ cssRule . cssText }` ) ; } else { prefixedStyles . push ( `#${ elementPrefixId } ${ cssRule . cssText }` ) ; } } } } style . remove ( ) ; } } template = `<style>${ prefixedStyles . join ( ' ' ) }</style><div id="${ TEMPLATE_ID_PREFIX }${ this . props . instanceId }">${ templateAsHtml . body . innerHTML }</div>` ; } this . setState ( { processedTemplate : template } ) ; } }