Skip to content

Commit

Permalink
JSUI-3283 Smart snippet without iframe (#1779)
Browse files Browse the repository at this point in the history
  • Loading branch information
erocheleau authored and btaillon-coveo committed Sep 29, 2021
1 parent 05606ae commit df13713
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 20 deletions.
32 changes: 21 additions & 11 deletions src/misc/AttachShadowPolyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,34 @@ import 'styling/_AttachShadowPolyfill';
export interface IShadowOptions {
title: string;
onSizeChanged?: Function;
useIFrame?: Boolean;
}

export async function attachShadow(element: HTMLElement, options: IShadowOptions & ShadowRootInit): Promise<HTMLElement> {
const iframe = $$('iframe', { className: 'coveo-shadow-iframe', scrolling: 'no', title: options.title }).el as HTMLIFrameElement;
const onLoad = new Promise(resolve => iframe.addEventListener('load', () => resolve()));
element.appendChild(iframe);
await onLoad;
let autoUpdateContainer: HTMLElement;
let contentBody: HTMLElement;
if (options.useIFrame) {
const iframe = $$('iframe', { className: 'coveo-shadow-iframe', scrolling: 'no', title: options.title }).el as HTMLIFrameElement;
const onLoad = new Promise(resolve => iframe.addEventListener('load', () => resolve()));
element.appendChild(iframe);
await onLoad;
contentBody = iframe.contentDocument.body as HTMLBodyElement;
autoUpdateContainer = iframe;
} else {
autoUpdateContainer = $$('div', { className: 'coveo-shadow-iframe', scrolling: 'no', title: options.title }).el as HTMLElement;
contentBody = autoUpdateContainer;
element.appendChild(autoUpdateContainer);
}

const iframeBody = iframe.contentDocument.body as HTMLBodyElement;
iframeBody.style.margin = '0';
const shadowRoot = $$('div', { style: 'overflow: auto;' }).el;
iframeBody.appendChild(shadowRoot);
autoUpdateHeight(iframe, shadowRoot, options.onSizeChanged);
contentBody.style.margin = '0';
const shadowRootContainer = $$('div', { style: 'overflow: auto;' }).el;
contentBody.appendChild(shadowRootContainer);
autoUpdateHeight(autoUpdateContainer, shadowRootContainer, options.onSizeChanged);
if (options.mode === 'open') {
Object.defineProperty(element, 'shadowRoot', { get: () => shadowRoot });
Object.defineProperty(element, 'shadowRoot', { get: () => shadowRootContainer });
}

return shadowRoot;
return shadowRootContainer;
}

function autoUpdateHeight(elementToResize: HTMLElement, content: HTMLElement, onUpdate?: Function) {
Expand Down
19 changes: 17 additions & 2 deletions src/ui/SmartSnippet/SmartSnippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface ISmartSnippetOptions {
maximumSnippetHeight: number;
titleField: IFieldOption;
hrefTemplate?: string;
useIFrame?: boolean;
}
/**
* The SmartSnippet component displays the excerpt of a document that would be most likely to answer a particular query.
Expand Down Expand Up @@ -134,7 +135,20 @@ export class SmartSnippet extends Component {
*
* Default value is `undefined`.
*/
hrefTemplate: ComponentOptions.buildStringOption()
hrefTemplate: ComponentOptions.buildStringOption(),

/**
* Specify if the SmartSnippet 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='CoveoSmartSnippet' data-without-frame='true'></div>
* ```
*/
useIFrame: ComponentOptions.buildBooleanOption({ defaultValue: true })
};

private lastRenderedResult: IQueryResult = null;
Expand Down Expand Up @@ -221,7 +235,8 @@ export class SmartSnippet extends Component {
this.shadowLoading = attachShadow(this.shadowContainer, {
mode: 'open',
title: l('AnswerSnippet'),
onSizeChanged: () => this.handleAnswerSizeChanged()
onSizeChanged: () => this.handleAnswerSizeChanged(),
useIFrame: this.options.useIFrame
}).then(shadow => {
shadow.appendChild(this.snippetContainer);
const style = this.buildStyle();
Expand Down
9 changes: 7 additions & 2 deletions src/ui/SmartSnippet/SmartSnippetCollapsibleSuggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export class SmartSnippetCollapsibleSuggestion {
private readonly bindings: IComponentBindings,
private readonly innerCSS: string,
private readonly searchUid: string,
private readonly source?: IQueryResult
private readonly source?: IQueryResult,
private readonly useIFrame?: boolean
) {}

public get loading() {
Expand Down Expand Up @@ -119,7 +120,11 @@ export class SmartSnippetCollapsibleSuggestion {
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', title) }).then(shadowRoot => {
this.contentLoaded = attachShadow(shadowContainer.el, {
mode: 'open',
title: l('AnswerSpecificSnippet', title),
useIFrame: this.useIFrame
}).then(shadowRoot => {
shadowRoot.appendChild(this.buildAnswerSnippetContent(innerHTML, style).el);
});
if (this.source) {
Expand Down
30 changes: 28 additions & 2 deletions src/ui/SmartSnippet/SmartSnippetSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { l } from '../../strings/Strings';
import { Initialization } from '../Base/Initialization';
import { Utils } from '../../utils/Utils';
import { getDefaultSnippetStyle } from './SmartSnippetCommon';
import { ComponentOptions } from '../Base/ComponentOptions';

const BASE_CLASSNAME = 'coveo-smart-snippet-suggestions';
const HAS_QUESTIONS_CLASSNAME = `${BASE_CLASSNAME}-has-questions`;
Expand All @@ -23,6 +24,10 @@ export const SmartSnippetSuggestionsClassNames = {
QUESTIONS_LIST_TITLE_CLASSNAME
};

export interface ISmartSnippetSuggestionsOptions {
useIFrame?: boolean;
}

/**
* The SmartSnippetSuggestions component displays additional queries for which a Coveo Smart Snippets model can provide relevant excerpts.
*/
Expand All @@ -35,15 +40,35 @@ export class SmartSnippetSuggestions extends Component {
});
};

/**
* The options for the SmartSnippetSuggestions
* @componentOptions
*/
static options: ISmartSnippetSuggestionsOptions = {
/**
* 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-without-shadow='true'></div>
* ```
*/
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?: {}, bindings?: IComponentBindings) {
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));
}

Expand Down Expand Up @@ -100,7 +125,8 @@ export class SmartSnippetSuggestions extends Component {
? getDefaultSnippetStyle(SmartSnippetCollapsibleSuggestionClassNames.RAW_CONTENT_CLASSNAME)
: innerCSS,
this.searchUid,
this.getCorrespondingResult(questionAnswer)
this.getCorrespondingResult(questionAnswer),
this.options.useIFrame
)
);
const container = $$(
Expand Down
36 changes: 33 additions & 3 deletions unitTests/ui/SmartSnippetSuggestionsTest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { IQuerySuccessEventArgs, QueryEvents } from '../../src/events/QueryEvents';
import { IQueryResult } from '../../src/rest/QueryResult';
import { IQuestionAnswerResponse, IRelatedQuestionAnswerResponse } from '../../src/rest/QuestionAnswerResponse';
import { SmartSnippetSuggestions, SmartSnippetSuggestionsClassNames } from '../../src/ui/SmartSnippet/SmartSnippetSuggestions';
import {
ISmartSnippetSuggestionsOptions,
SmartSnippetSuggestions,
SmartSnippetSuggestionsClassNames
} from '../../src/ui/SmartSnippet/SmartSnippetSuggestions';
import { advancedComponentSetup, AdvancedComponentSetupOptions, IBasicComponentSetup } from '../MockEnvironment';
import { SmartSnippetCollapsibleSuggestionClassNames } from '../../src/ui/SmartSnippet/SmartSnippetCollapsibleSuggestion';
import { $$ } from '../../src/utils/Dom';
Expand Down Expand Up @@ -117,10 +121,10 @@ export function SmartSnippetSuggestionsTest() {
let test: IBasicComponentSetup<SmartSnippetSuggestions>;
let searchUid: string;

function instantiateSmartSnippetSuggestions(styling: string) {
function instantiateSmartSnippetSuggestions(styling: string, options: Partial<ISmartSnippetSuggestionsOptions> = {}) {
test = advancedComponentSetup<SmartSnippetSuggestions>(
SmartSnippetSuggestions,
new AdvancedComponentSetupOptions($$('div', {}, ...(Utils.isNullOrUndefined(styling) ? [] : [mockStyling(styling)])).el)
new AdvancedComponentSetupOptions($$('div', {}, ...(Utils.isNullOrUndefined(styling) ? [] : [mockStyling(styling)])).el, options)
);
}

Expand Down Expand Up @@ -362,6 +366,32 @@ export function SmartSnippetSuggestionsTest() {
});
});

describe('with the useIFrame option', () => {
afterEach(() => {
test.env.root.remove();
});

it('renders a div instead of an iframe when the option is false', async done => {
const IFRAME_CLASSNAME = 'coveo-shadow-iframe';
instantiateSmartSnippetSuggestions(null, {
useIFrame: false
});
document.body.appendChild(test.env.root);
await triggerQuestionAnswerQuery(true);
expect(test.cmp.element.querySelector(`.${IFRAME_CLASSNAME}`).nodeName).toEqual('DIV');
done();
});

it('renders an iframe by default', async done => {
const IFRAME_CLASSNAME = 'coveo-shadow-iframe';
instantiateSmartSnippetSuggestions(null);
document.body.appendChild(test.env.root);
await triggerQuestionAnswerQuery(true);
expect(test.cmp.element.querySelector(`.${IFRAME_CLASSNAME}`).nodeName).toEqual('IFRAME');
done();
});
});

describe('with no styling, without sources', () => {
beforeEach(async done => {
instantiateSmartSnippetSuggestions('');
Expand Down
22 changes: 22 additions & 0 deletions unitTests/ui/SmartSnippetTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,28 @@ export function SmartSnippetTest() {
done();
});

it('creates an iframe if the option withIFrame is omitted', async done => {
const IFRAME_CLASSNAME = 'coveo-shadow-iframe';
instantiateSmartSnippet(null);
document.body.appendChild(test.env.root);
await triggerQuestionAnswerQuery(true);
expect(getFirstChild(IFRAME_CLASSNAME).nodeName).toEqual('IFRAME');
test.env.root.remove();
done();
});

it("doesn't create an iframe if the option useIFrame is false", async done => {
const IFRAME_CLASSNAME = 'coveo-shadow-iframe';
instantiateSmartSnippet(null, {
useIFrame: false
});
document.body.appendChild(test.env.root);
await triggerQuestionAnswerQuery(true);
expect(getFirstChild(IFRAME_CLASSNAME).nodeName).toEqual('DIV');
test.env.root.remove();
done();
});

describe('with styling without a source', () => {
beforeEach(async done => {
instantiateSmartSnippet(style);
Expand Down

0 comments on commit df13713

Please sign in to comment.