import { ITemplateService } from "./ITemplateService" ; import { ServiceKey , ServiceScope , Text } from "@microsoft/sp-core-library" ; import { SPHttpClient , SPHttpClientResponse } from '@microsoft/sp-http' ; import * as Handlebars from 'handlebars' ; import { uniqBy , uniq , isEmpty , trimEnd , get } from "@microsoft/sp-lodash-subset" ; import * as strings from 'CommonStrings' ; import { DateHelper } from "../../helpers/DateHelper" ; import { PageContext } from "@microsoft/sp-page-context" ; import { IComponentDefinition } from "@pnp/modern-search-extensibility" ; import groupBy from 'handlebars-group-by' ; import { IComponentFieldsConfiguration } from "../../models/common/IComponentFieldsConfiguration" ; import { initializeFileTypeIcons } from '@uifabric/file-type-icons' ; import { GlobalSettings } from 'office-ui-fabric-react' ; import { IDataResultType , ResultTypeOperator } from "../../models/common/IDataResultType" ; import { IDataResultsTemplateContext } from "../../models/common/ITemplateContext" ; import { UrlHelper } from "../../helpers/UrlHelper" ; import { ObjectHelper } from "../../helpers/ObjectHelper" ; import { Constants } from "../../common/Constants" ; import * as handlebarsHelpers from 'handlebars-helpers' ; import { ServiceScopeHelper } from "../../helpers/ServiceScopeHelper" ; const TemplateService_ServiceKey = 'PnPModernSearchTemplateService' ; /**
 * The CSS identifer to load the template markup from a layout html file
 */ const Template_Content_Id = 'data-content' ; /**
 * The CSS identifer to load the placeholder markup from a layout html file
 */ const Template_PlaceHolder_Id = 'placeholder-content' ; export class TemplateService implements ITemplateService { private spHttpClient : SPHttpClient ; private pageContext : PageContext ; private dateHelper : DateHelper ; private serviceScope : ServiceScope ; /**
     * The moment.js library reference
     */ private moment : any ; private timeZoneBias = { WebBias : 0 , UserBias : 0 , WebDST : 0 , UserDST : 0 } ; private dayLightSavings = true ; /**
     * A no conflict instance of Handlebars
     */ private _handlebars : typeof Handlebars ; get Handlebars ( ) : typeof Handlebars { return this . _handlebars ; } public static ServiceKey : ServiceKey < ITemplateService > = ServiceKey . create ( TemplateService_ServiceKey , TemplateService ) ; public constructor ( serviceScope ) { this . serviceScope = serviceScope ; serviceScope . whenFinished ( async ( ) => { this . pageContext = serviceScope . consume < PageContext > ( PageContext . serviceKey ) ; this . spHttpClient = serviceScope . consume < SPHttpClient > ( SPHttpClient . serviceKey ) ; this . dateHelper = serviceScope . consume < DateHelper > ( DateHelper . ServiceKey ) ; this . dayLightSavings = this . dateHelper . isDST ( ) ; this . timeZoneBias = { WebBias : this . pageContext . legacyPageContext . webTimeZoneData . Bias ? this . pageContext . legacyPageContext . webTimeZoneData . Bias : 0 , WebDST : this . pageContext . legacyPageContext . webTimeZoneData . DaylightBias ? this . pageContext . legacyPageContext . webTimeZoneData . DaylightBias : 0 , UserBias : null , UserDST : null } ; if ( this . pageContext . legacyPageContext . userTimeZoneData ) { this . timeZoneBias . UserBias = this . pageContext . legacyPageContext . userTimeZoneData . Bias ? this . pageContext . legacyPageContext . userTimeZoneData . Bias : 0 ; this . timeZoneBias . UserDST = this . pageContext . legacyPageContext . userTimeZoneData . DaylightBias ? this . pageContext . legacyPageContext . userTimeZoneData . DaylightBias : 0 ; } if ( ! this . moment ) { this . dateHelper . moment ( ) . then ( moment => { this . moment = moment ; } ) ; } this . _handlebars = Handlebars . create ( ) ; const helpers = handlebarsHelpers ( ) ; Object . keys ( helpers ) . forEach ( helperName => { this . _handlebars . registerHelper ( helperName , helpers [ helperName ] ) ; } ) ; this . registerCustomHelpers ( ) ; if ( ! GlobalSettings . getValue ( 'fileTypeIconsInitialized' ) ) { initializeFileTypeIcons ( ) ; GlobalSettings . setValue ( 'fileTypeIconsInitialized' , true ) ; } } ) ; } param templateContent public getTemplateMarkup ( templateContent ) { const domParser = new DOMParser ( ) ; const htmlContent = domParser . parseFromString ( templateContent , 'text/html' ) ; let templates = htmlContent . getElementById ( Template_Content_Id ) ; if ( templates && templates . innerHTML ) { return templates . innerHTML . replace ( /\&gt;/g , '>' ) ; } else { return templateContent ; } } param templateContent public getPlaceholderMarkup ( templateContent ) { const domParser = new DOMParser ( ) ; const htmlContent = domParser . parseFromString ( templateContent , 'text/html' ) ; const placeHolders = htmlContent . getElementById ( Template_PlaceHolder_Id ) ; if ( placeHolders && placeHolders . innerHTML ) { return placeHolders . innerHTML . replace ( /\&gt;/g , '>' ) ; } else { return null ; } } param fileUrl public async getFileContent ( fileUrl ) { const response = await this . spHttpClient . get ( fileUrl , SPHttpClient . configurations . v1 , { headers : { 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { return await response . text ( ) ; } else { throw response . statusText ; } } param filePath public async ensureFileResolves ( fileUrl ) { const response = await this . spHttpClient . get ( fileUrl , SPHttpClient . configurations . v1 , { headers : { 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { if ( response . url . indexOf ( 'AccessDenied.aspx' ) > - 1 ) { throw 'Access Denied' ; } return ; } else { throw response . statusText ; } } param filePath public isValidTemplateFile ( filePath ) { let path = filePath . toLowerCase ( ) . trim ( ) ; let pathExtension = path . substring ( path . lastIndexOf ( '.' ) ) ; return ( pathExtension == '.htm' || pathExtension == '.html' ) ; } returns public async processTemplate ( templateContext , templateContent ) { let template = this . Handlebars . compile ( templateContent ) ; let result = template ( templateContext ) ; return result ; } /**
     * Registers web components on the current page to be able to use them in the Handlebars template
     */ public async registerWebComponents ( webComponents , instanceId ) { return new Promise < void > ( ( resolve ) => { const availableServiceKeys = { TemplateService : TemplateService . ServiceKey } ; this . serviceScope . whenFinished ( ( ) => { webComponents . forEach ( wc => { const component = window . customElements . get ( wc . componentName ) ; if ( ! component ) { wc . componentClass . prototype . _webPartServiceScopes = new Map < string , ServiceScope > ( ) ; wc . componentClass . prototype . _webPartServiceScopes . set ( instanceId , this . serviceScope ) ; wc . componentClass . prototype . _webPartServiceKeys = new Map < string , { [ key ] : ServiceKey < any > ; } > ( ) ; wc . componentClass . prototype . _webPartServiceKeys . set ( instanceId , availableServiceKeys ) ; wc . componentClass . prototype . _serviceScope = ServiceScopeHelper . getRootServiceScope ( this . serviceScope ) ; wc . componentClass . prototype . moment = this . moment ; window . customElements . define ( wc . componentName , wc . componentClass ) ; } else { if ( component . prototype . _webPartServiceScopes && ! component . prototype . _webPartServiceScopes . get ( instanceId ) ) { component . prototype . _webPartServiceScopes . set ( instanceId , this . serviceScope ) ; } if ( component . prototype . _webPartServiceScopes && ! component . prototype . _webPartServiceKeys . get ( instanceId ) ) { component . prototype . _webPartServiceKeys . set ( instanceId , availableServiceKeys ) ; } } } ) ; resolve ( ) ; } ) ; } ) ; } param fieldsConfigurationAsString param itemAsString param themeVariant public processFieldsConfiguration < T > ( fieldsConfiguration , item , context ? ) { let processedProps = { } ; fieldsConfiguration . forEach ( configuration => { let processedValue = ObjectHelper . byPath ( item , configuration . value ) ; if ( configuration . useHandlebarsExpr && configuration . value ) { try { let templateContext = context ? context : { } ; const tempTemplateContent = `{{#with item as |item|}}${ configuration . value }{{/with}}` ; let template = this . Handlebars . compile ( tempTemplateContent , { noEscape : true } ) ; processedValue = template ( { item : item } , { data : { root : { slots : templateContext . slots , theme : templateContext . theme , context : templateContext . context , instanceId : templateContext . instanceId } } } ) ; processedValue = processedValue ? processedValue . trim ( ) : null ; } catch ( error ) { processedValue = `###Error: ${ error . message }###` ; } } processedProps [ configuration . field ] = processedValue ; } ) ; return processedProps as T ; } param resultTypes public async registerResultTypes ( resultTypes ) { this . Handlebars . unregisterPartial ( "resultTypes" ) ; if ( resultTypes . length > 0 ) { let content = await this . _buildCondition ( resultTypes , resultTypes [ 0 ] , 0 ) ; let template = this . Handlebars . compile ( content ) ; this . Handlebars . registerPartial ( 'resultTypes' , template ) ; } else { this . Handlebars . registerPartial ( 'resultTypes' , '{{> @partial-block }}' ) ; } return ; } param resultTypes param currentResultType param currentIdx private async _buildCondition ( resultTypes , currentResultType , currentIdx ) { let conditionBlockContent ; let templateContent = currentResultType . inlineTemplateContent ; if ( currentResultType . externalTemplateUrl ) { templateContent = await this . getFileContent ( currentResultType . externalTemplateUrl ) ; } if ( currentResultType . value ) { let handlebarsToken = currentResultType . value . match ( /^\{\{(.*)\}\}$/ ) ; let operator = currentResultType . operator ; let param1 = currentResultType . property ; let param2 = handlebarsToken ? handlebarsToken [ 1 ] : `"${ currentResultType . value }"` ; if ( currentResultType . operator === ResultTypeOperator . StartsWith ) { param1 = `"${ currentResultType . value }"` ; param2 = `${ currentResultType . property }` ; } if ( currentResultType . operator === ResultTypeOperator . NotNull ) { param2 = null ; } const baseCondition = `{{#${ operator } ${ param1 } ${ param2 || "" }}} 
                                        ${ templateContent }` ; if ( currentIdx === resultTypes . length - 1 ) { conditionBlockContent = "{{> @partial-block }}" ; } else { conditionBlockContent = await this . _buildCondition ( resultTypes , resultTypes [ currentIdx + 1 ] , currentIdx + 1 ) ; } return `${ baseCondition }   
                    {{else}} 
                        ${ conditionBlockContent }
                    {{/${ operator }}}` ; } else { return '' ; } } /**
     * Registers custom Handlebars helpers in the global context
     */ private registerCustomHelpers ( ) { this . Handlebars . registerHelper ( "getGraphPreviewUrl" , ( url , context ? ) => { return new this . Handlebars . SafeString ( UrlHelper . getGraphPreviewUrl ( url ) ) ; } ) ; this . Handlebars . registerHelper ( "getCountMessage" , ( totalRows , inputQuery ? ) => { const countResultMessage = inputQuery ? Text . format ( strings . HandlebarsHelpers . CountMessageLong , totalRows , inputQuery ) : Text . format ( strings . HandlebarsHelpers . CountMessageShort , totalRows ) ; return new this . Handlebars . SafeString ( countResultMessage ) ; } ) ; this . Handlebars . registerHelper ( "getSummary" , ( hitHighlightedSummary ) => { if ( ! isEmpty ( hitHighlightedSummary ) ) { return new this . Handlebars . SafeString ( hitHighlightedSummary . replace ( /<c0\>/g , "<strong>" ) . replace ( /<\/c0\>/g , "</strong>" ) . replace ( /<ddd\/>/g , "&#8230;" ) . replace ( /\w+-\w+-\w+-\w+-\w+/g , "" ) ) ; } } ) ; this . Handlebars . registerHelper ( "getTagName" , ( tag ) => { if ( ! isEmpty ( tag ) ) { return new Handlebars . SafeString ( tag . split ( "|" ) . pop ( ) ) ; } } ) ; this . Handlebars . registerHelper ( "getDate" , ( ( date , format , timeHandling ? , isZ ? ) => { try { if ( isZ && ! date . toUpperCase ( ) . endsWith ( "Z" ) ) { if ( date . indexOf ( ' ' ) !== - 1 ) { date += " " ; } date += "Z" ; } let itemDate = new Date ( date ) ; if ( itemDate . toISOString ( ) !== new Date ( null ) . toISOString ( ) ) { if ( typeof timeHandling === "number" ) { if ( timeHandling === 1 ) { date = trimEnd ( date , "Z" ) ; } else if ( timeHandling === 2 ) { let idx = date . indexOf ( 'T' ) ; date = date . substr ( 0 , idx ) + "T00:00:00" ; } else if ( timeHandling === 3 ) { date = this . dateHelper . addMinutes ( this . dayLightSavings , itemDate , - this . timeZoneBias . WebBias , - this . timeZoneBias . WebDST ) . toISOString ( ) ; date = trimEnd ( date , "Z" ) ; } else if ( timeHandling === 4 && this . timeZoneBias . UserBias ) { date = this . dateHelper . addMinutes ( this . dayLightSavings , itemDate , - this . timeZoneBias . UserBias , - this . timeZoneBias . UserDST ) . toISOString ( ) ; date = trimEnd ( date , "Z" ) ; } } return this . moment ( new Date ( date ) ) . format ( format ) ; } } catch ( error ) { return date ; } } ) . bind ( this ) ) ; this . Handlebars . registerHelper ( "getUrlField" , ( urlField , value ) => { if ( ! isEmpty ( urlField ) ) { let separatorPos = urlField . indexOf ( "," ) ; if ( separatorPos === - 1 ) { return urlField ; } if ( value === "URL" ) { return urlField . substr ( 0 , separatorPos ) ; } return urlField . substr ( separatorPos + 1 ) . trim ( ) ; } return new this . Handlebars . SafeString ( urlField ) ; } ) ; this . Handlebars . registerHelper ( "getUniqueCount" , ( array , property ) => { if ( ! Array . isArray ( array ) ) return 0 ; if ( array . length === 0 ) return 0 ; let result ; if ( property ) { result = uniqBy ( array , property ) ; } else { result = uniq ( array ) ; } return result . length ; } ) ; this . Handlebars . registerHelper ( "getUnique" , ( array , property ) => { if ( ! Array . isArray ( array ) ) return 0 ; if ( array . length === 0 ) return 0 ; let result ; if ( property ) { result = uniqBy ( array , property ) ; } else { result = uniq ( array ) ; } return result ; } ) ; this . Handlebars . unregisterHelper ( 'times' ) ; this . Handlebars . registerHelper ( 'times' , ( n , block ) => { var accum = '' ; for ( var i = 0 ; i < n ; ++ i ) accum += block . fn ( i ) ; return accum ; } ) ; this . Handlebars . registerHelper ( "regex" , ( regx , str ) => { let rx = new RegExp ( regx ) ; let i = rx . exec ( str ) ; if ( ! ! ! i || i . length === 0 ) return "-" ; let ret = i [ 0 ] ; return ret ; } ) ; this . Handlebars . registerHelper ( groupBy ( this . Handlebars ) ) ; this . Handlebars . registerHelper ( "slot" , ( item , propertyPath ) => { if ( propertyPath && ! isEmpty ( propertyPath ) ) { const value = ObjectHelper . byPath ( item , propertyPath ) ; return value ; } } ) ; this . Handlebars . registerHelper ( "getUserEmail" , ( expr ) => { if ( ! isEmpty ( expr ) ) { const matches = expr . match ( /([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})/gi ) ; if ( matches ) { return matches [ 0 ] ; } else { return expr ; } } } ) ; this . Handlebars . registerHelper ( "getPageContext" , ( name ) => { if ( ! name ) return "" ; let value = get ( this . pageContext , name ) ; if ( value ) return value ; return "" ; } ) ; this . Handlebars . registerHelper ( "getAttachments" , ( value , options ) => { let out = "" ; if ( ! isEmpty ( value ) ) { let splitArr = value . split ( /\n+/ ) ; if ( splitArr && splitArr . length > 0 ) { let index = 0 ; for ( let i of splitArr ) { let pos = i . lastIndexOf ( "/" ) ; if ( pos !== - 1 ) { let fileName = i . substring ( pos + 1 ) ; let objLine = { url : i , fileName : fileName , index : index } ; out += options . fn ( objLine ) ; index ++ ; } } } } return out ; } ) ; this . Handlebars . registerHelper ( "JSONparse" , ( str , options ) => { return JSON . parse ( str ) ; } ) ; this . Handlebars . registerHelper ( "isItemSelected" , ( selectedKeys , itemIndex , options ) => { if ( Array . isArray ( selectedKeys ) && selectedKeys . length > 0 ) { return selectedKeys . indexOf ( `${ options . data . root . paging . currentPageNumber }${ itemIndex }` ) !== - 1 ; } } ) ; } }