coveo-search-ui
Version:
Coveo JavaScript Search Framework
201 lines (181 loc) • 7.76 kB
text/typescript
import 'styling/_SmartSnippetSuggestions';
import { Component } from '../Base/Component';
import { exportGlobally } from '../../GlobalExports';
import { IComponentBindings } from '../Base/ComponentBindings';
import { QueryEvents, IQuerySuccessEventArgs } from '../../events/QueryEvents';
import { IRelatedQuestionAnswerResponse, IQuestionAnswerResponse } from '../../rest/QuestionAnswerResponse';
import { $$, Dom } from '../../utils/Dom';
import { uniqueId, isEqual, find } from 'underscore';
import { SmartSnippetCollapsibleSuggestion, SmartSnippetCollapsibleSuggestionClassNames } from './SmartSnippetCollapsibleSuggestion';
import { l } from '../../strings/Strings';
import { Initialization } from '../Base/Initialization';
import { Utils } from '../../utils/Utils';
import { getDefaultSnippetStyle } from './SmartSnippetCommon';
import { ComponentOptions } from '../Base/ComponentOptions';
import { IFieldOption } from '../Base/IComponentOptions';
const BASE_CLASSNAME = 'coveo-smart-snippet-suggestions';
const HAS_QUESTIONS_CLASSNAME = `${BASE_CLASSNAME}-has-questions`;
const QUESTIONS_LIST_CLASSNAME = `${BASE_CLASSNAME}-questions`;
const QUESTIONS_LIST_TITLE_CLASSNAME = `${QUESTIONS_LIST_CLASSNAME}-title`;
export const SmartSnippetSuggestionsClassNames = {
HAS_QUESTIONS_CLASSNAME,
QUESTIONS_LIST_CLASSNAME,
QUESTIONS_LIST_TITLE_CLASSNAME
};
export interface ISmartSnippetSuggestionsOptions {
titleField: IFieldOption;
hrefTemplate?: string;
alwaysOpenInNewWindow?: boolean;
useIFrame?: boolean;
}
/**
* The SmartSnippetSuggestions component displays additional queries for which a Coveo Smart Snippets model can provide relevant excerpts.
*/
export class SmartSnippetSuggestions extends Component {
static ID = 'SmartSnippetSuggestions';
static doExport = () => {
exportGlobally({
SmartSnippetSuggestions
});
};
/**
* The options for the SmartSnippetSuggestions
* @componentOptions
*/
static options: ISmartSnippetSuggestionsOptions = {
/**
* The field to display for the title.
*/
titleField: ComponentOptions.buildFieldOption({ defaultValue: '@title' }),
/**
* Specifies a template literal from which to generate the title and URI's `href` attribute value (see
* [Template literals](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals)).
*
* This option overrides the [`field`]{@link SmartSnippet.options.uriField} option value.
*
* The template literal can reference any number of fields from the parent result. It can also reference global
* scope properties.
*
* **Examples:**
*
* - The following markup generates an `href` value such as `http://uri.com?id=itemTitle`:
*
* ```html
* <a class='CoveoSmartSnippet' data-href-template='${clickUri}?id=${raw.title}'></a>
* ```
*
* - The following markup generates an `href` value such as `localhost/fooBar`:
*
* ```html
* <a class='CoveoSmartSnippet' data-href-template='${window.location.hostname}/{Foo.Bar}'></a>
* ```
*
* Default value is `undefined`.
*/
hrefTemplate: ComponentOptions.buildStringOption(),
/**
* Specifies whether the component should open its links in a new window instead of opening them in the current
* context.
*
* Default value is `false`.
*/
alwaysOpenInNewWindow: ComponentOptions.buildBooleanOption({ defaultValue: false }),
/**
* Specify if the SmartSnippetSuggestion snippet should be displayed inside an iframe or not.
*
* Use this option in specific cases where your environment has limitations around iframe usage.
*
* **Examples:**
*
* ```html
* <div class='CoveoSmartSnippetSuggestions' data-use-i-frame='true'></div>
* ```
*
* Default value is `true`.
*/
useIFrame: ComponentOptions.buildBooleanOption({ defaultValue: true })
};
private readonly titleId = uniqueId(QUESTIONS_LIST_TITLE_CLASSNAME);
private contentLoaded: Promise<SmartSnippetCollapsibleSuggestion[]>;
private title: Dom;
private questionAnswers: Dom;
private renderedQuestionAnswer: IQuestionAnswerResponse;
private searchUid: string;
constructor(public element: HTMLElement, public options?: ISmartSnippetSuggestionsOptions, bindings?: IComponentBindings) {
super(element, SmartSnippetSuggestions.ID, bindings);
this.options = ComponentOptions.initComponentOptions(element, SmartSnippetSuggestions, options);
this.bind.onRootElement(QueryEvents.deferredQuerySuccess, (data: IQuerySuccessEventArgs) => this.handleQuerySuccess(data));
}
public get loading() {
return this.contentLoaded;
}
/**
* @warning This method only works for the demo. In practice, the source of the answer will not always be part of the results.
*/
private getCorrespondingResult(questionAnswer: IRelatedQuestionAnswerResponse) {
return find(
this.queryController.getLastResults().results,
result => result.raw[questionAnswer.documentId.contentIdKey] === questionAnswer.documentId.contentIdValue
);
}
private handleQuerySuccess(data: IQuerySuccessEventArgs) {
const questionAnswer = data.results.questionAnswer;
const hasQuestions = !!(questionAnswer && questionAnswer.relatedQuestions.length);
$$(this.element).toggleClass(HAS_QUESTIONS_CLASSNAME, hasQuestions);
if (hasQuestions) {
this.searchUid = data.results.searchUid;
if (this.renderedQuestionAnswer && isEqual(questionAnswer, this.renderedQuestionAnswer)) {
return;
}
this.detachContent();
this.element.appendChild((this.title = this.buildTitle()).el);
this.element.appendChild((this.questionAnswers = this.buildQuestionAnswers(questionAnswer.relatedQuestions)).el);
} else {
this.detachContent();
}
this.renderedQuestionAnswer = questionAnswer;
}
private detachContent() {
this.title && this.title.detach();
this.questionAnswers && this.questionAnswers.detach();
this.title = this.questionAnswers = null;
}
private buildTitle() {
return $$('span', { className: QUESTIONS_LIST_TITLE_CLASSNAME, id: this.titleId }, l('SuggestedQuestions'));
}
private buildQuestionAnswers(questionAnswers: IRelatedQuestionAnswerResponse[]) {
const innerCSS = this.getInnerCSS();
const answers = questionAnswers.map(
questionAnswer =>
new SmartSnippetCollapsibleSuggestion({
questionAnswer,
bindings: this.getBindings(),
innerCSS: Utils.isNullOrUndefined(innerCSS)
? getDefaultSnippetStyle(SmartSnippetCollapsibleSuggestionClassNames.RAW_CONTENT_CLASSNAME)
: innerCSS,
searchUid: this.searchUid,
titleField: this.options.titleField,
hrefTemplate: this.options.hrefTemplate,
alwaysOpenInNewWindow: this.options.alwaysOpenInNewWindow,
source: this.getCorrespondingResult(questionAnswer),
useIFrame: this.options.useIFrame
})
);
const container = $$(
'ul',
{ className: QUESTIONS_LIST_CLASSNAME, ariaLabelledby: this.titleId },
...answers.map(answer => answer.build())
);
this.contentLoaded = Promise.all(answers.map(answer => answer.loading.then(() => answer)));
return container;
}
private getInnerCSS() {
const styles = $$(this.element)
.children()
.filter(element => element instanceof HTMLScriptElement && element.type.toLowerCase() === 'text/css')
.map(element => element.innerHTML);
return styles.length ? styles.join('\n') : null;
}
}
Initialization.registerAutoCreateComponent(SmartSnippetSuggestions);
SmartSnippetSuggestions.doExport();