UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

257 lines (234 loc) • 9.7 kB
import { IRelatedQuestionAnswerResponse } from '../../rest/QuestionAnswerResponse'; import { uniqueId } from 'underscore'; import { AccessibleButton } from '../../utils/AccessibleButton'; import { SVGIcons } from '../../utils/SVGIcons'; import { attachShadow } from '../../misc/AttachShadowPolyfill'; import { $$, Dom } from '../../utils/Dom'; import { l } from '../../strings/Strings'; import { IQueryResult } from '../../rest/QueryResult'; import { analyticsActionCauseList, IAnalyticsSmartSnippetSuggestionMeta, IAnalyticsSmartSnippetSuggestionOpenSnippetInlineLinkMeta, IAnalyticsSmartSnippetSuggestionOpenSourceMeta } from '../Analytics/AnalyticsActionListMeta'; import { ResultLink } from '../ResultLink/ResultLink'; import { IComponentBindings } from '../Base/ComponentBindings'; import { Utils } from '../../utils/Utils'; import { IFieldOption } from '../Base/IComponentOptions'; import { getSanitizedAnswerSnippet, transformSnippetLinks } from './SmartSnippetCommon'; const QUESTION_CLASSNAME = `coveo-smart-snippet-suggestions-question`; const QUESTION_TITLE_CLASSNAME = `${QUESTION_CLASSNAME}-title`; const QUESTION_TITLE_LABEL_CLASSNAME = `${QUESTION_TITLE_CLASSNAME}-label`; const QUESTION_TITLE_CHECKBOX_CLASSNAME = `${QUESTION_TITLE_CLASSNAME}-checkbox`; const QUESTION_SNIPPET_CLASSNAME = `${QUESTION_CLASSNAME}-snippet`; const QUESTION_SNIPPET_CONTAINER_CLASSNAME = `${QUESTION_SNIPPET_CLASSNAME}-container`; const QUESTION_SNIPPET_HIDDEN_CLASSNAME = `${QUESTION_SNIPPET_CLASSNAME}-hidden`; const SHADOW_CLASSNAME = `${QUESTION_SNIPPET_CLASSNAME}-content`; const RAW_CONTENT_CLASSNAME = `${SHADOW_CLASSNAME}-raw`; const SOURCE_CLASSNAME = `${QUESTION_CLASSNAME}-source`; const SOURCE_TITLE_CLASSNAME = `${SOURCE_CLASSNAME}-title`; const SOURCE_URL_CLASSNAME = `${SOURCE_CLASSNAME}-url`; export const SmartSnippetCollapsibleSuggestionClassNames = { QUESTION_CLASSNAME, QUESTION_TITLE_CLASSNAME, QUESTION_TITLE_LABEL_CLASSNAME, QUESTION_TITLE_CHECKBOX_CLASSNAME, QUESTION_SNIPPET_CLASSNAME, QUESTION_SNIPPET_CONTAINER_CLASSNAME, QUESTION_SNIPPET_HIDDEN_CLASSNAME, SHADOW_CLASSNAME, RAW_CONTENT_CLASSNAME, SOURCE_CLASSNAME, SOURCE_TITLE_CLASSNAME, SOURCE_URL_CLASSNAME }; export class SmartSnippetCollapsibleSuggestion { private readonly labelId = uniqueId(QUESTION_TITLE_LABEL_CLASSNAME); private readonly snippetId = uniqueId(QUESTION_SNIPPET_CLASSNAME); private readonly checkboxId = uniqueId(QUESTION_TITLE_CHECKBOX_CLASSNAME); private contentLoaded: Promise<void>; private snippetAndSourceContainer: Dom; private collapsibleContainer: Dom; private checkbox: Dom; private expanded = false; constructor( private readonly options: { readonly questionAnswer: IRelatedQuestionAnswerResponse; readonly bindings: IComponentBindings; readonly innerCSS: string; readonly searchUid: string; readonly titleField: IFieldOption; readonly hrefTemplate?: string; readonly alwaysOpenInNewWindow?: boolean; readonly source?: IQueryResult; readonly useIFrame?: boolean; } ) {} public get loading() { return this.contentLoaded; } private get analyticsSuggestionMeta(): IAnalyticsSmartSnippetSuggestionMeta { const { documentId, question, answerSnippet } = this.options.questionAnswer; return { searchQueryUid: this.options.searchUid, documentId, question, answerSnippet }; } public build() { const collapsibleContainer = this.buildCollapsibleContainer(this.options.questionAnswer, this.buildStyle(this.options.innerCSS)); const title = this.buildTitle(this.options.questionAnswer.question); this.updateExpanded(); return $$( 'li', { className: QUESTION_CLASSNAME, ariaLabelledby: this.labelId }, title, collapsibleContainer ).el as HTMLLIElement; } private buildStyle(innerCSS: string) { const styleTag = document.createElement('style'); styleTag.innerHTML = innerCSS; return styleTag; } private buildTitle(question: string) { const checkbox = this.buildCheckbox(question); const label = $$('span', { className: QUESTION_TITLE_LABEL_CLASSNAME, id: this.labelId }); label.text(question); const title = $$('span', { className: QUESTION_TITLE_CLASSNAME }, label, checkbox); title.on('click', () => this.toggle()); return title; } private buildCheckbox(question: string) { this.checkbox = $$('div', { role: 'button', tabindex: 0, ariaControls: this.snippetId, className: QUESTION_TITLE_CHECKBOX_CLASSNAME, id: this.checkboxId }); new AccessibleButton() .withElement(this.checkbox) .withLabel(l('ExpandQuestionAnswer', question)) .withEnterKeyboardAction(() => this.toggle()) .build(); return this.checkbox; } private buildCollapsibleContainer(questionAnswer: IRelatedQuestionAnswerResponse, style: HTMLStyleElement) { const shadowContainer = $$('div', { className: SHADOW_CLASSNAME }); this.snippetAndSourceContainer = $$('div', { className: QUESTION_SNIPPET_CONTAINER_CLASSNAME }, shadowContainer); this.collapsibleContainer = $$('div', { className: QUESTION_SNIPPET_CLASSNAME, id: this.snippetId }, this.snippetAndSourceContainer); this.contentLoaded = attachShadow(shadowContainer.el, { mode: 'open', title: l('AnswerSpecificSnippet', questionAnswer.question), useIFrame: this.options.useIFrame }).then(shadowRoot => { shadowRoot.appendChild(this.buildAnswerSnippetContent(questionAnswer, style).el); }); if (this.options.source) { this.snippetAndSourceContainer.append(this.buildSourceUrl().el); this.snippetAndSourceContainer.append(this.buildSourceTitle().el); } return this.collapsibleContainer; } private buildAnswerSnippetContent(questionAnswer: IRelatedQuestionAnswerResponse, style: HTMLStyleElement) { const snippet = $$('div', { className: RAW_CONTENT_CLASSNAME }, getSanitizedAnswerSnippet(questionAnswer)); transformSnippetLinks(snippet.el, this.options.alwaysOpenInNewWindow, link => this.sendOpenSnippetLinkAnalytics(link)); const container = $$('div', {}, snippet); container.append(style); return container; } private buildSourceTitle() { const link = this.buildLink(SOURCE_TITLE_CLASSNAME); link.text(Utils.getFieldValue(this.options.source!, <string>this.options.titleField)); return link; } private buildSourceUrl() { const link = this.buildLink(SOURCE_URL_CLASSNAME); link.text((link.el as HTMLAnchorElement).href); return link; } private buildLink(className: string) { const element = $$('a', { className: `CoveoResultLink ${className}` }); new ResultLink( element.el, { hrefTemplate: this.options.hrefTemplate, logAnalytics: href => this.sendOpenSourceAnalytics(element.el, href), alwaysOpenInNewWindow: this.options.alwaysOpenInNewWindow }, { ...this.options.bindings, resultElement: this.collapsibleContainer.el }, this.options.source ); return element; } private toggle() { this.expanded = !this.expanded; this.updateExpanded(); if (this.expanded) { this.sendExpandAnalytics(); } else { this.sendCollapseAnalytics(); } } private updateIFrameExpanded() { const iframe = this.snippetAndSourceContainer.find('iframe'); if (!iframe) { return; } this.expanded ? iframe.removeAttribute('tabindex') : iframe.setAttribute('tabindex', '-1'); } private updateExpanded() { this.checkbox.setAttribute('aria-expanded', this.expanded.toString()); this.checkbox.setHtml(this.expanded ? SVGIcons.icons.arrowUp : SVGIcons.icons.arrowDown); this.collapsibleContainer.setAttribute('aria-hidden', (!this.expanded).toString()); this.collapsibleContainer.toggleClass(QUESTION_SNIPPET_HIDDEN_CLASSNAME, !this.expanded); this.collapsibleContainer.el.style.visibility = this.expanded ? 'inherit' : 'hidden'; this.collapsibleContainer.el.style.height = this.expanded ? `${this.snippetAndSourceContainer.el.clientHeight}px` : '0px'; this.updateIFrameExpanded(); } private sendExpandAnalytics() { return this.options.bindings.usageAnalytics.logCustomEvent<IAnalyticsSmartSnippetSuggestionMeta>( analyticsActionCauseList.expandSmartSnippetSuggestion, this.analyticsSuggestionMeta, this.checkbox.el ); } private sendCollapseAnalytics() { return this.options.bindings.usageAnalytics.logCustomEvent<IAnalyticsSmartSnippetSuggestionMeta>( analyticsActionCauseList.collapseSmartSnippetSuggestion, this.analyticsSuggestionMeta, this.checkbox.el ); } private sendOpenSourceAnalytics(element: HTMLElement, href: string) { return this.options.bindings.usageAnalytics.logClickEvent<IAnalyticsSmartSnippetSuggestionOpenSourceMeta>( analyticsActionCauseList.openSmartSnippetSuggestionSource, { ...this.analyticsSuggestionMeta, documentTitle: this.options.source.title, author: Utils.getFieldValue(this.options.source, 'author'), documentURL: href }, this.options.source, element ); } private sendOpenSnippetLinkAnalytics(link: HTMLAnchorElement) { return this.options.bindings.usageAnalytics.logClickEvent<IAnalyticsSmartSnippetSuggestionOpenSnippetInlineLinkMeta>( analyticsActionCauseList.openSmartSnippetSuggestionInlineLink, { ...this.analyticsSuggestionMeta, linkText: link.innerText, linkURL: link.href }, this.options.source, link ); } }