/** @module Survey-Taking Question Client */ import PluginClient, { MessageData, PluginClientHandler } from '@qualtrics/plugin-client'; import { HostHandler, PluginHandler, QuestionDef, QuestionResponse, PluginStateDataDef } from '../../survey-taking-shared/src/index'; import { Context } from '../../survey-taking-shared/src/types/models'; import { document } from '../../survey-taking-shared/src/window' import { FetchResponse, InvokePredefinedRequest, PredefinedRequestHandler, UserProvidedPredefinedRequest } from '../../survey-question-shared/src/predefined-request'; const defaultPostMessageTimeoutMs = 10000; // 10 seconds export type ClientOptions = { handlers: PluginHandler, stateData?: PluginStateData, } export default class SurveyTakingQuestionClient implements PredefinedRequestHandler { private _pluginClient: PluginClient = ((null as any) as PluginClient); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get pluginClient(): PluginClient { return this._pluginClient } private _question: QuestionType = ((null as any) as QuestionType); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get question(): QuestionType { return this._question; } private _response: ResponseType = ((null as any) as ResponseType); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get response(): ResponseType { return this._response; } private _stateData: PluginStateData = ((null as any) as PluginStateData); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get stateData(): PluginStateData { return this._stateData; } private _language: string = ((null as any) as string); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get language(): string { return this._language } private _surveyId: string = ((null as any) as string); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get surveyId(): string { return this._surveyId } private _surveyVersionId: string = ((null as any) as string); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get surveyVersionId(): string { return this._surveyVersionId } private _sessionToken: string = ((null as any) as string); // Force TS to believe that the this will never be null, because the initialize function will guarantee it public get sessionToken(): string { return this._sessionToken } static async initialize(config: ClientOptions): Promise> { const clientInstance = new SurveyTakingQuestionClient(); const handlers = clientInstance.getPluginClientHandler(config); clientInstance._pluginClient = await PluginClient.initialize(handlers); const context = clientInstance.pluginClient.getContext() as Context; await clientInstance.updatePluginStateDataObject(config.stateData); clientInstance._question = context.questionDef as QuestionType; clientInstance._response = context.response; clientInstance._language = context.language; clientInstance._surveyId = context.surveyId; clientInstance._surveyVersionId = context.surveyVersionId; clientInstance._sessionToken = context.sessionToken; const baseCssUrl = context.questionDef.questionTypePluginProperties.BaseCssUrl const cssLink = document.createElement('link'); cssLink.rel = 'stylesheet'; cssLink.href = baseCssUrl; // Wait for the baseCssUrl to load, then return clientInstance after baseCssUrl is fully loaded, or exceptions thrown during loading baseCssUrl return new Promise(resolve => { cssLink.onload = () => { resolve(clientInstance); clientInstance.loadPlugin(); } cssLink.onerror = () => { resolve(clientInstance); clientInstance.loadPlugin(); } document.head.appendChild(cssLink); }); } // The constructor must always be private! Use the initialize function, so that the necessary async initialization steps can happen. private constructor() {} // Returns a set of handlers which the Survey Taking host can call to interact with the Survey Taking Question plugin private getPluginClientHandler(config: ClientOptions): Record, PluginClientHandler> { return { onHostHasChangedResponse: async (data: any, pluginClient) => { this._response = data.response as ResponseType; config.handlers.onHostHasChangedResponse?.(this._response); }, onHostHasChangedPluginStateData: async (data: any, pluginClient) => { this._stateData = data.stateData as PluginStateData; config.handlers.onHostHasChangedPluginStateData?.(this._stateData); }, }; } /** * Notifies the survey-taking host application that the plugin iframe has initialized * and that the question can be shown in place of the loading spinner */ private loadPlugin(): void { this.postMessage('onPluginHasLoaded', {}) } /** * Retrieves localized strings from the Plugin's translation files, specifically * from the translation that matches the User's language. Valid keys are defined by top-level * properties of the `strings` object in the translation file. * * @param key The key for the text template to use * @param options.placeHolders (optional) - An object with values for any named placeholders in the template text * @param options.defaultVal (optional) - A default value to return if the key or translation doesn't exist * @returns the translation associated with the key */ public getText(key: string, options?: { placeHolders?: Record, defaultVal?: string }): string { const { placeHolders, defaultVal } = options || {}; return this.pluginClient.getText(key, placeHolders, defaultVal); } /** * Returns the language code of the logged in user given by the host on init. * @returns the language code of the logged user given by the host on init. */ getLanguage() { return this.pluginClient.getLanguage(); } private async postMessage

(name: keyof HostHandler, data: MessageData, timeout: number = defaultPostMessageTimeoutMs): Promise

{ const messageData = await this.pluginClient.postMessage(name, data, timeout); return messageData as P; } /** * Notifies the survey-taking host application of updated response data. Plugin * response data will be preserved by the survey-taking host through any navigation * event * @param response the updated response */ updateResponse(response?: ResponseType): void { this.postMessage('onPluginHasChangedResponse', { response }) } /** * Notifies the survey-taking host application of updated plugin state data. Plugin * state data will be dismissed by the survey-taking host on any navigation event. * @param stateData the updated state data object */ async updatePluginStateDataObject(stateData?: PluginStateData): Promise { return this.postMessage('onPluginHasChangedStateData', { stateData }) } /** * Notifies the survey-taking host application of an update to the plugin state data. * A single field of the stateData object is updated and then the updated object is * passed to the survey-taking host application. Plugin state data will be dismissed * by the survey-taking host on any navigation event. * @param stateData the updated state data object * @param key key of the field to be updated * @param value new value for the field specified by the key */ updatePluginStateDataField(stateData: PluginStateDataDef, key: string, value: any): void { stateData[key] = value; this.postMessage('onPluginHasChangedStateData', { stateData }) } /** * Notifies the survey-taking host application of an update to the plugin height * @param height the height in pixels */ setContainerHeight(height: number): void { this.postMessage('setContainerHeight', height) } /** * Invokes a predefined request configured for this question type. * @see {@link InvokePredefinedRequest} */ public async invokePredefinedRequest(request: UserProvidedPredefinedRequest, timeout?: number): Promise { const response = await this.postMessage('invokePredefinedRequestProxy', request, timeout); return response as ReturnType; } }