angular-instantsearch
Version:
Lightning-fast search for Angular apps, by Algolia.
1,574 lines (1,525 loc) • 96.5 kB
JavaScript
import { Input, EventEmitter, isDevMode, VERSION as VERSION$1, Component, Inject, PLATFORM_ID, Output, SkipSelf, forwardRef, Optional, NgModule, ChangeDetectionStrategy, ContentChild, TemplateRef, ViewChild, KeyValueDiffers, NgZone } from '@angular/core';
import { isPlatformBrowser, CommonModule, DOCUMENT } from '@angular/common';
import { connectBreadcrumb, connectClearRefinements, connectCurrentRefinements, connectHierarchicalMenu, connectHitsPerPage, connectHitsWithInsights, connectInfiniteHitsWithInsights, connectMenu, connectNumericMenu, connectPagination, connectRange, connectRefinementList, connectSearchBox, connectSortBy, connectRatingMenu, connectStats, connectToggleRefinement, connectConfigure, EXPERIMENTAL_connectConfigureRelatedItems, connectQueryRules, connectVoiceSearch } from 'instantsearch.js/es/connectors';
import * as algoliasearchProxy from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js/es';
import indexWidget from 'instantsearch.js/es/widgets/index/index';
import { highlight, reverseHighlight, snippet, reverseSnippet } from 'instantsearch.js/es/helpers';
import { getPropertyByPath as getPropertyByPath$1 } from 'instantsearch.js/es/lib/utils';
import * as noUiSlider from 'nouislider';
import * as encodeProxy from 'querystring-es3/encode';
function bem(widgetName) {
const cx = function (element, subElement) {
let cssClass = `ais-${widgetName}`;
if (element) {
cssClass += `-${element}`;
}
if (subElement) {
cssClass += `--${subElement}`;
}
return cssClass;
};
return cx;
}
function parseNumberInput(input) {
return typeof input === 'string' ? parseInt(input, 10) : input;
}
function noop(...args) { }
function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
// See https://github.com/algolia/instantsearch.js/blob/9296022fecadfbf82f15e837c215a1356eac4bc5/src/lib/utils/range.ts
function range({ start = 0, end, step = 1, }) {
// We can't divide by 0 so we re-assign the step to 1 if it happens.
const limitStep = step === 0 ? 1 : step;
// In some cases the array to create has a decimal length.
// We therefore need to round the value.
// Example:
// { start: 1, end: 5000, step: 500 }
// => Array length = (5000 - 1) / 500 = 9.998
const arrayLength = Math.round((end - start) / limitStep);
return [...Array(arrayLength)].map((_, current) => start + current * limitStep);
}
// See https://github.com/algolia/react-instantsearch/blob/86dfe8674d566124af55a8f044051d0062786c1a/packages/react-instantsearch-core/src/core/utils.ts#L138-L142
function getPropertyByPath(object, path) {
return path
.replace(/\[(\d+)]/g, '.$1')
.split('.')
.reduce((current, key) => (current ? current[key] : undefined), object);
}
class TypedBaseWidget {
constructor(widgetName) {
this.updateState = (state, isFirstRendering) => {
if (isFirstRendering) {
Promise.resolve().then(() => {
this.state = state;
});
}
else {
this.state = state;
}
};
this.cx = bem(widgetName);
}
get parent() {
if (this.parentIndex) {
return this.parentIndex;
}
return this.instantSearchInstance;
}
createWidget(connector, options, additionalWidgetProperties = {}) {
this.widget = Object.assign(Object.assign({}, connector(this.updateState, noop)(options)), additionalWidgetProperties);
}
ngOnInit() {
this.parent.addWidgets([this.widget]);
}
ngOnDestroy() {
if (isPlatformBrowser(this.instantSearchInstance.platformId)) {
this.parent.removeWidgets([this.widget]);
}
}
/**
* Helper to generate class names for an item
* @param item element to generate a class name for
*/
getItemClass(item) {
const className = this.cx('item');
if (item.isRefined) {
return `${className} ${this.cx('item', 'selected')}`;
}
return className;
}
}
TypedBaseWidget.propDecorators = {
autoHideContainer: [{ type: Input }]
};
const VERSION = '4.4.3';
// this is needed for different webpack/typescript configurations
const algoliasearch$1 = algoliasearchProxy.default || algoliasearchProxy;
class NgAisInstantSearch {
constructor(platformId) {
this.platformId = platformId;
this.instanceName = 'default';
this.change = new EventEmitter();
this.onRender = () => {
this.change.emit({
results: this.instantSearchInstance.helper.lastResults,
state: this.instantSearchInstance.helper.state,
});
};
}
ngOnInit() {
if (isDevMode()) {
console.warn(`We are deprecating Angular InstantSearch.
For more information and alternative solutions, go to https://alg.li/angular-deprecation.`);
}
if (typeof this.config.searchClient.addAlgoliaAgent === 'function') {
this.config.searchClient.addAlgoliaAgent(`angular (${VERSION$1.full})`);
this.config.searchClient.addAlgoliaAgent(`angular-instantsearch (${VERSION})`);
}
this.instantSearchInstance = instantsearch(this.config);
this.instantSearchInstance.on('render', this.onRender);
}
ngAfterViewInit() {
this.instantSearchInstance.start();
}
ngOnDestroy() {
if (this.instantSearchInstance) {
this.instantSearchInstance.removeListener('render', this.onRender);
this.instantSearchInstance.dispose();
}
}
addWidgets(widgets) {
this.instantSearchInstance.addWidgets(widgets);
}
removeWidgets(widgets) {
this.instantSearchInstance.removeWidgets(widgets);
}
refresh() {
this.instantSearchInstance.refresh();
}
}
NgAisInstantSearch.decorators = [
{ type: Component, args: [{
selector: 'ais-instantsearch',
template: '<ng-content></ng-content>'
},] }
];
NgAisInstantSearch.ctorParameters = () => [
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }
];
NgAisInstantSearch.propDecorators = {
config: [{ type: Input }],
instanceName: [{ type: Input }],
change: [{ type: Output }]
};
class NgAisIndex {
constructor(
// public API does not include SkipSelf, but the index widget should accept parents, avoiding itself.
parentIndex, instantSearchInstance) {
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
}
get parent() {
if (this.parentIndex) {
return this.parentIndex;
}
return this.instantSearchInstance;
}
createWidget() {
this.widget = Object.assign(Object.assign({}, indexWidget({
indexName: this.indexName,
indexId: this.indexId,
})), { $$widgetType: 'ais.index' });
}
addWidgets(widgets) {
this.widget.addWidgets(widgets);
}
removeWidgets(widgets) {
this.widget.removeWidgets(widgets);
}
ngOnInit() {
this.createWidget();
this.parent.addWidgets([this.widget]);
}
ngOnDestroy() {
if (isPlatformBrowser(this.instantSearchInstance.platformId)) {
this.parent.removeWidgets([this.widget]);
}
}
}
NgAisIndex.decorators = [
{ type: Component, args: [{
selector: 'ais-index',
template: `<ng-content></ng-content>`
},] }
];
NgAisIndex.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: SkipSelf }, { type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisIndex.propDecorators = {
indexName: [{ type: Input }],
indexId: [{ type: Input }]
};
class NgAisBreadcrumb extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('Breadcrumb');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
this.state = {
createURL: () => '#',
items: [],
refine: noop,
canRefine: false,
};
}
get isHidden() {
return this.state.items.length === 0 && this.autoHideContainer;
}
get items() {
return this.state.items.map((item, idx) => (Object.assign(Object.assign({}, item), { separator: idx !== 0, isLast: idx === this.state.items.length - 1 })));
}
ngOnInit() {
this.createWidget(connectBreadcrumb, {
attributes: this.attributes,
rootPath: this.rootPath,
separator: this.separator,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.breadcrumb',
});
super.ngOnInit();
}
handleClick(event, item) {
event.preventDefault();
event.stopPropagation();
if (item.value) {
this.state.refine(item.value);
}
}
}
NgAisBreadcrumb.decorators = [
{ type: Component, args: [{
selector: 'ais-breadcrumb',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<ul [class]="cx('list')">
<li
*ngFor="let item of items"
[ngClass]="[cx('item'), item.isLast ? cx('item', 'selected') : '']"
(click)="handleClick($event, item)"
>
<span
*ngIf="item.separator"
[class]="cx('separator')"
aria-hidden="true"
>
>
</span>
<a
[class]="cx('link')"
href="{{state.createURL(item.value)}}"
*ngIf="!item.isLast"
(click)="handleClick($event, item)"
>
{{item.label}}
</a>
<span *ngIf="item.isLast">
{{item.label}}
</span>
</li>
</ul>
</div>
`
},] }
];
NgAisBreadcrumb.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisBreadcrumb.propDecorators = {
attributes: [{ type: Input }],
rootPath: [{ type: Input }],
separator: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisBreadcrumbModule {
}
NgAisBreadcrumbModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisBreadcrumb],
entryComponents: [NgAisBreadcrumb],
exports: [NgAisBreadcrumb],
imports: [CommonModule],
},] }
];
class NgAisClearRefinements extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('ClearRefinements');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
// rendering options
this.resetLabel = 'Clear refinements';
this.state = {
hasRefinements: false,
canRefine: false,
refine: noop,
createURL: () => '#',
};
}
get isHidden() {
return !this.state.hasRefinements && this.autoHideContainer;
}
ngOnInit() {
this.createWidget(connectClearRefinements, {
includedAttributes: this.includedAttributes,
excludedAttributes: this.excludedAttributes,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.clearRefinements',
});
super.ngOnInit();
}
handleClick(event) {
event.preventDefault();
if (this.state.hasRefinements) {
this.state.refine();
}
}
}
NgAisClearRefinements.decorators = [
{ type: Component, args: [{
selector: 'ais-clear-refinements',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<button
[class]="cx('button') + (!state.hasRefinements ? (' ' + cx('button', 'disabled')) : '')"
(click)="handleClick($event)"
[disabled]="!state.hasRefinements"
>
{{resetLabel}}
</button>
</div>
`
},] }
];
NgAisClearRefinements.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisClearRefinements.propDecorators = {
resetLabel: [{ type: Input }],
includedAttributes: [{ type: Input }],
excludedAttributes: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisClearRefinementsModule {
}
NgAisClearRefinementsModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisClearRefinements],
entryComponents: [NgAisClearRefinements],
exports: [NgAisClearRefinements],
imports: [CommonModule],
},] }
];
class NgAisCurrentRefinements extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('CurrentRefinements');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
this.state = {
createURL: () => '#',
refine: noop,
items: [],
canRefine: false,
};
}
get isHidden() {
return this.state.items.length === 0 && this.autoHideContainer;
}
ngOnInit() {
this.createWidget(connectCurrentRefinements, {
includedAttributes: this.includedAttributes,
excludedAttributes: this.excludedAttributes,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.currentRefinements',
});
super.ngOnInit();
}
handleClick(event, refinement) {
event.preventDefault();
this.state.refine(refinement);
}
}
NgAisCurrentRefinements.decorators = [
{ type: Component, args: [{
selector: 'ais-current-refinements',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<ul
[class]="cx('list')"
*ngFor="let item of state.items"
>
<li [class]="cx('item')">
<span [class]="cx('label')">{{item.label | titlecase}}:</span>
<span
[class]="cx('category')"
*ngFor="let refinement of item.refinements"
>
<span [class]="cx('categoryLabel')">{{refinement.label}}</span>
<button [class]="cx('delete')" (click)="handleClick($event, refinement)">✕</button>
</span>
</li>
</ul>
</div>
`
},] }
];
NgAisCurrentRefinements.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisCurrentRefinements.propDecorators = {
includedAttributes: [{ type: Input }],
excludedAttributes: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisCurrentRefinementsModule {
}
NgAisCurrentRefinementsModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisCurrentRefinements],
entryComponents: [NgAisCurrentRefinements],
exports: [NgAisCurrentRefinements],
imports: [CommonModule],
},] }
];
class NgAisHierarchicalMenu extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('HierarchicalMenu');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
this.state = {
createURL: () => '#',
items: [],
refine: noop,
canRefine: false,
isShowingMore: false,
toggleShowMore: noop,
canToggleShowMore: false,
sendEvent: noop,
};
}
get isHidden() {
return this.state.items.length === 0 && this.autoHideContainer;
}
ngOnInit() {
this.createWidget(connectHierarchicalMenu, {
limit: parseNumberInput(this.limit),
attributes: this.attributes,
rootPath: this.rootPath,
separator: this.separator,
showParentLevel: this.showParentLevel,
sortBy: this.sortBy,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.hierarchicalMenu',
});
super.ngOnInit();
}
}
NgAisHierarchicalMenu.decorators = [
{ type: Component, args: [{
selector: 'ais-hierarchical-menu',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<ul [class]="cx('list') + ' ' + cx('list', 'lvl0')">
<ais-hierarchical-menu-item
*ngFor="let item of state.items"
[item]="item"
[createURL]="state.createURL"
[refine]="state.refine"
>
</ais-hierarchical-menu-item>
</ul>
</div>
`
},] }
];
NgAisHierarchicalMenu.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisHierarchicalMenu.propDecorators = {
attributes: [{ type: Input }],
separator: [{ type: Input }],
rootPath: [{ type: Input }],
showParentLevel: [{ type: Input }],
limit: [{ type: Input }],
sortBy: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisHierarchicalMenuItem {
constructor() {
this.lvl = 1;
this.cx = bem('HierarchicalMenu');
}
getItemClass(item) {
let className = this.cx('item');
if (item.isRefined) {
className = `${className} ${this.cx('item', 'selected')}`;
}
if (this.isArray(item.data) && item.data.length > 0) {
className = `${className} ${this.cx('item', 'parent')}`;
}
return className;
}
getListClass() {
return `${this.cx('list')} ${this.cx('list', 'child')} ${this.cx('list', `lvl${this.lvl}`)}`;
}
isArray(potentialArray) {
return Array.isArray(potentialArray);
}
handleClick(event, item) {
event.preventDefault();
event.stopPropagation();
this.refine(item.value);
}
}
NgAisHierarchicalMenuItem.decorators = [
{ type: Component, args: [{
selector: 'ais-hierarchical-menu-item',
template: `
<li
[class]="getItemClass(item)"
(click)="handleClick($event, item)"
>
<a
[class]="cx('link')"
href="{{createURL(item.value)}}"
(click)="handleClick($event, item)"
>
<span [class]="cx('label')">{{item.label}}</span>
<span [class]="cx('count')">{{item.count}}</span>
</a>
<ul
[class]="getListClass()"
*ngIf="item.isRefined && isArray(item.data) && item.data.length > 0"
>
<ais-hierarchical-menu-item
*ngFor="let child of item.data"
[item]="child"
[createURL]="createURL"
[refine]="refine"
[lvl]="lvl + 1"
>
</ais-hierarchical-menu-item>
</ul>
</li>
`
},] }
];
NgAisHierarchicalMenuItem.propDecorators = {
lvl: [{ type: Input }],
refine: [{ type: Input }],
createURL: [{ type: Input }],
item: [{ type: Input }]
};
class NgAisHierarchicalMenuModule {
}
NgAisHierarchicalMenuModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisHierarchicalMenu, NgAisHierarchicalMenuItem],
entryComponents: [NgAisHierarchicalMenu],
exports: [NgAisHierarchicalMenu],
imports: [CommonModule],
},] }
];
class NgAisHitsPerPage extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('HitsPerPage');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
this.state = {
items: [],
refine: noop,
hasNoResults: true,
canRefine: false,
};
}
get isHidden() {
return this.state.items.length === 0 && this.autoHideContainer;
}
ngOnInit() {
this.createWidget(connectHitsPerPage, {
items: this.items,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.hitsPerPage',
});
super.ngOnInit();
}
}
NgAisHitsPerPage.decorators = [
{ type: Component, args: [{
selector: 'ais-hits-per-page',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<select
[class]="cx('select')"
(change)="state.refine($event.target.value)"
>
<option
[class]="cx('option')"
*ngFor="let item of state.items"
[value]="item.value"
[selected]="item.isRefined"
>
{{item.label}}
</option>
</select>
</div>
`
},] }
];
NgAisHitsPerPage.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisHitsPerPage.propDecorators = {
items: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisHitsPerPageModule {
}
NgAisHitsPerPageModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisHitsPerPage],
entryComponents: [NgAisHitsPerPage],
exports: [NgAisHitsPerPage],
imports: [CommonModule],
},] }
];
class NgAisHighlight {
constructor() {
this.tagName = 'mark';
}
get content() {
const highlightAttributeResult = getPropertyByPath$1(this.hit._highlightResult, this.attribute);
const fallback = getPropertyByPath$1(this.hit, this.attribute);
// @MAJOR drop this custom fallback once it is implemented directly in instantsearch.js v5
if (!highlightAttributeResult && fallback) {
return fallback;
}
return highlight({
attribute: this.attribute,
highlightedTagName: this.tagName,
hit: this.hit,
});
}
}
NgAisHighlight.decorators = [
{ type: Component, args: [{
selector: 'ais-highlight',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<span class="ais-Highlight" [innerHtml]="content"></span>`
},] }
];
NgAisHighlight.propDecorators = {
attribute: [{ type: Input }],
hit: [{ type: Input }],
tagName: [{ type: Input }]
};
class NgAisHighlightModule {
}
NgAisHighlightModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisHighlight],
entryComponents: [NgAisHighlight],
exports: [NgAisHighlight],
imports: [CommonModule],
},] }
];
class NgAisHits extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('Hits');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
this.state = {
hits: [],
results: undefined,
bindEvent: undefined,
sendEvent: undefined,
};
this.updateState = (state, isFirstRendering) => {
if (isFirstRendering)
return;
this.state = state;
};
}
ngOnInit() {
this.createWidget(connectHitsWithInsights, {
escapeHTML: this.escapeHTML,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.hits',
});
super.ngOnInit();
}
}
NgAisHits.decorators = [
{ type: Component, args: [{
selector: 'ais-hits',
template: `
<div [class]="cx()">
<ng-container *ngTemplateOutlet="template; context: state"></ng-container>
<!-- default rendering if no template specified -->
<div *ngIf="!template">
<ul [class]="cx('list')">
<li
[class]="cx('item')"
*ngFor="let hit of state.hits"
>
<ais-highlight attribute="name" [hit]="hit">
</ais-highlight>
</li>
</ul>
</div>
</div>
`
},] }
];
NgAisHits.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisHits.propDecorators = {
template: [{ type: ContentChild, args: [TemplateRef, { static: false },] }],
escapeHTML: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisHitsModule {
}
NgAisHitsModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisHits],
entryComponents: [NgAisHits],
exports: [NgAisHits],
imports: [CommonModule, NgAisHighlightModule],
},] }
];
class NgAisIndexModule {
static forRoot() {
return {
ngModule: NgAisIndexModule,
providers: [],
};
}
}
NgAisIndexModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisIndex],
entryComponents: [NgAisIndex],
exports: [NgAisIndex],
imports: [CommonModule],
},] }
];
class NgAisInfiniteHits extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('InfiniteHits');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
this.showPrevious = false;
this.showPreviousLabel = 'Show previous results';
this.showMoreLabel = 'Show more results';
this.state = {
hits: [],
results: undefined,
currentPageHits: [],
isFirstPage: false,
isLastPage: false,
showMore: noop,
showPrevious: noop,
sendEvent: noop,
bindEvent: () => '',
};
this.updateState = (state, isFirstRendering) => {
if (isFirstRendering)
return;
this.state = state;
};
}
ngOnInit() {
this.createWidget(connectInfiniteHitsWithInsights, {
escapeHTML: this.escapeHTML,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.infiniteHits',
});
super.ngOnInit();
}
showMoreHandler(event) {
event.preventDefault();
this.state.showMore();
}
showPreviousHandler(event) {
event.preventDefault();
this.state.showPrevious();
}
}
NgAisInfiniteHits.decorators = [
{ type: Component, args: [{
selector: 'ais-infinite-hits',
template: `
<div [class]="cx()">
<ng-container *ngTemplateOutlet="template; context: state"></ng-container>
<!-- default rendering if no template specified -->
<button
[ngClass]="[cx('loadPrevious'), this.state.isFirstPage ? cx('loadPrevious', 'disabled') : '']"
(click)="showPreviousHandler($event)"
[disabled]="state.isFirstPage"
*ngIf="showPrevious && !template"
>
{{showPreviousLabel}}
</button>
<div *ngIf="!template">
<ul [class]="cx('list')">
<li
[class]="cx('item')"
*ngFor="let hit of state.hits"
>
<ais-highlight attribute="name" [hit]="hit">
</ais-highlight>
</li>
</ul>
</div>
<button
[ngClass]="[cx('loadMore'), this.state.isLastPage ? cx('loadMore', 'disabled') : '']"
(click)="showMoreHandler($event)"
[disabled]="state.isLastPage"
*ngIf="!template"
>
{{showMoreLabel}}
</button>
</div>
`
},] }
];
NgAisInfiniteHits.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisInfiniteHits.propDecorators = {
template: [{ type: ContentChild, args: [TemplateRef, { static: false },] }],
escapeHTML: [{ type: Input }],
showPrevious: [{ type: Input }],
showPreviousLabel: [{ type: Input }],
showMoreLabel: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisInfiniteHitsModule {
}
NgAisInfiniteHitsModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisInfiniteHits],
entryComponents: [NgAisInfiniteHits],
exports: [NgAisInfiniteHits],
imports: [CommonModule, NgAisHighlightModule],
},] }
];
class NgAisInstantSearchModule {
static forRoot() {
return {
ngModule: NgAisInstantSearchModule,
providers: [],
};
}
}
NgAisInstantSearchModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisInstantSearch],
entryComponents: [NgAisInstantSearch],
exports: [NgAisInstantSearch],
imports: [CommonModule],
},] }
];
class NgAisMenu extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('Menu');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
// rendering options
this.showMoreLabel = 'Show more';
this.showLessLabel = 'Show less';
this.state = {
items: [],
refine: noop,
createURL: () => '#',
canRefine: false,
isShowingMore: false,
canToggleShowMore: false,
toggleShowMore: noop,
sendEvent: noop,
};
}
get isHidden() {
return this.state.items.length === 0 && this.autoHideContainer;
}
get showMoreClass() {
let className = this.cx('showMore');
if (!this.state.canToggleShowMore) {
className = `${className} ${this.cx('showMore', 'disabled')}`;
}
return className;
}
ngOnInit() {
this.createWidget(connectMenu, {
attribute: this.attribute,
showMore: this.showMore,
limit: this.limit,
showMoreLimit: this.showMoreLimit,
sortBy: this.sortBy,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.menu',
});
super.ngOnInit();
}
handleClick(event, value) {
event.preventDefault();
event.stopPropagation();
this.state.refine(value);
}
}
NgAisMenu.decorators = [
{ type: Component, args: [{
selector: 'ais-menu',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<ul [class]="cx('list')">
<li
[class]="getItemClass(item)"
*ngFor="let item of state.items"
(click)="handleClick($event, item.value)"
>
<a
href="{{state.createURL(item.value)}}"
[class]="cx('link')"
(click)="handleClick($event, item.value)"
>
<span [class]="cx('label')">{{item.label}}</span>
<span [class]="cx('count')">{{item.count}}</span>
</a>
</li>
</ul>
<button
*ngIf="showMore"
(click)="state.toggleShowMore()"
[class]="showMoreClass"
[disabled]="!state.canToggleShowMore"
>
{{state.isShowingMore ? showLessLabel : showMoreLabel}}
</button>
</div>
`
},] }
];
NgAisMenu.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisMenu.propDecorators = {
showMoreLabel: [{ type: Input }],
showLessLabel: [{ type: Input }],
attribute: [{ type: Input }],
showMore: [{ type: Input }],
limit: [{ type: Input }],
showMoreLimit: [{ type: Input }],
sortBy: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisMenuModule {
}
NgAisMenuModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisMenu],
entryComponents: [NgAisMenu],
exports: [NgAisMenu],
imports: [CommonModule],
},] }
];
class NgAisNumericMenu extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('NumericMenu');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
this.state = {
items: [],
refine: noop,
createURL: () => '#',
hasNoResults: true,
sendEvent: noop,
canRefine: false,
};
}
get isHidden() {
return this.state.items.length === 0 && this.autoHideContainer;
}
ngOnInit() {
this.createWidget(connectNumericMenu, {
attribute: this.attribute,
items: this.items,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.numericMenu',
});
super.ngOnInit();
}
refine(event, item) {
event.preventDefault();
event.stopPropagation();
this.state.refine(item.value);
}
}
NgAisNumericMenu.decorators = [
{ type: Component, args: [{
selector: 'ais-numeric-menu',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<ul [class]="cx('list')">
<li
[class]="getItemClass(item)"
*ngFor="let item of state.items"
>
<label [class]="cx('label')">
<input
[class]="cx('radio')"
type="radio"
name="NumericMenu"
[checked]="item.isRefined"
(change)="refine($event, item)"
/>
<span [class]="cx('labelText')">{{item.label}}</span>
</label>
</li>
</ul>
</div>
`
},] }
];
NgAisNumericMenu.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisNumericMenu.propDecorators = {
attribute: [{ type: Input }],
items: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisNumericMenuModule {
}
NgAisNumericMenuModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisNumericMenu],
entryComponents: [NgAisNumericMenu],
exports: [NgAisNumericMenu],
imports: [CommonModule],
},] }
];
class NgAisPagination extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('Pagination');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
// rendering options
this.showFirst = true;
this.showLast = true;
this.showPrevious = true;
this.showNext = true;
// TODO: check if this works, padding and totalPages are most likely strings when passed to the template
this.state = {
createURL: () => '#',
currentRefinement: 0,
nbHits: 0,
nbPages: 0,
refine: noop,
pages: [],
canRefine: false,
isFirstPage: false,
isLastPage: false,
};
}
ngOnInit() {
this.createWidget(connectPagination, {
padding: parseNumberInput(this.padding),
totalPages: parseNumberInput(this.totalPages),
}, {
$$widgetType: 'ais.pagination',
});
super.ngOnInit();
}
refine(event, page) {
event.stopPropagation();
event.preventDefault();
if (page < 0 ||
page === this.state.currentRefinement ||
page >= this.state.nbPages) {
return;
}
this.state.refine(page);
}
}
NgAisPagination.decorators = [
{ type: Component, args: [{
selector: 'ais-pagination',
template: `
<div [ngClass]="[cx(), state.nbPages <= 1 ? cx('', 'noRefinement') : '']">
<ul [class]="cx('list')">
<li
*ngIf="showFirst"
(click)="refine($event, 0)"
[class]="
cx('item') +
' ' +
cx('item', 'firstPage') +
(state.currentRefinement === 0 ? ' ' + cx('item', 'disabled') : '')
"
>
<a
[href]="state.createURL(0)"
[class]="cx('link')"
>
‹‹
</a>
</li>
<li
*ngIf="showPrevious"
(click)="refine($event, state.currentRefinement - 1)"
[class]="
cx('item') +
' ' +
cx('item', 'previousPage') +
(state.currentRefinement === 0 ? ' ' + cx('item', 'disabled') : '')
"
>
<a
[href]="state.createURL(state.currentRefinement - 1)"
[class]="cx('link')"
>
‹
</a>
</li>
<li
[class]="
cx('item') +
' ' +
cx('item', 'page') +
(state.currentRefinement === page ? ' ' + cx('item', 'selected') : '')
"
*ngFor="let page of state.pages"
(click)="refine($event, page)"
>
<a
[class]="cx('link')"
[href]="state.createURL(page)"
>
{{page + 1}}
</a>
</li>
<li
*ngIf="showNext"
(click)="refine($event, state.currentRefinement + 1)"
[class]="
cx('item') +
' ' +
cx('item', 'nextPage') +
(state.currentRefinement + 1 === state.nbPages ? ' ' + cx('item', 'disabled') : '')
"
>
<a
[href]="state.createURL(state.currentRefinement + 1)"
[class]="cx('link')"
>
›
</a>
</li>
<li
*ngIf="showLast"
(click)="refine($event, state.nbPages - 1)"
[class]="
cx('item') +
' ' +
cx('item', 'lastPage') +
(state.currentRefinement + 1 === state.nbPages ? ' ' + cx('item', 'disabled') : '')
"
>
<a
[href]="state.createURL(state.nbPages - 1)"
[class]="cx('link')"
>
››
</a>
</li>
</ul>
</div>
`
},] }
];
NgAisPagination.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisPagination.propDecorators = {
showFirst: [{ type: Input }],
showLast: [{ type: Input }],
showPrevious: [{ type: Input }],
showNext: [{ type: Input }],
padding: [{ type: Input }],
totalPages: [{ type: Input }]
};
class NgAisPaginationModule {
}
NgAisPaginationModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisPagination],
entryComponents: [NgAisPagination],
exports: [NgAisPagination],
imports: [CommonModule],
},] }
];
class NgAisRangeSlider extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('RangeSlider');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
// rendering options
this.pips = true;
this.tooltips = true;
this.state = {
canRefine: false,
format: {
from: () => '',
to: () => '',
},
range: { min: 0, max: 1 },
refine: noop,
start: [0, 1],
sendEvent: noop,
};
this.updateState = (state, isFirstRendering) => {
if (isFirstRendering) {
// create slider
const config = {
animate: false,
behaviour: 'snap',
connect: true,
range: { min: 0, max: 1 },
start: [0, 1],
step: this.step,
tooltips: this.tooltips && [
{ to: this.formatTooltip },
{ to: this.formatTooltip },
],
};
// tslint:disable-next-line: no-boolean-literal-compare (pips is @Input, so could be not a boolean)
if (this.pips === true || typeof this.pips === 'undefined') {
Object.assign(config, {
pips: {
density: 3,
mode: 'positions',
stepped: true,
values: [0, 50, 100],
},
});
}
else if (this.pips !== undefined) {
Object.assign(config, { pips: this.pips });
}
this.slider = noUiSlider.create(this.sliderContainer.nativeElement, config);
// register listen events
this.sliderContainer.nativeElement.noUiSlider.on('change', this.handleChange);
}
// update component inner state
this.state = state;
// update the slider state
const { range: { min, max }, start, } = state;
const disabled = min === max;
const range = disabled ? { min, max: max + 0.0001 } : { min, max };
// TODO: test this as we're nolonger passing disable
// it seems the API has changed: slider.setAttribute('disabled', true) / slider.removeAttribute('disabled');
// see: https://refreshless.com/nouislider/more/#section-disable
this.slider.updateOptions({ range, start });
};
this.handleChange = (values) => {
this.state.refine(values);
};
this.formatTooltip = (value) => {
return value.toFixed(parseNumberInput(this.precision));
};
}
get step() {
// compute step from the precision value
const precision = parseNumberInput(this.precision) || 2;
return 1 / Math.pow(10, precision);
}
ngOnInit() {
this.createWidget(connectRange, {
attribute: this.attribute,
max: parseNumberInput(this.max),
min: parseNumberInput(this.min),
precision: parseNumberInput(this.precision),
}, {
$$widgetType: 'ais.rangeSlider',
});
super.ngOnInit();
}
}
NgAisRangeSlider.decorators = [
{ type: Component, args: [{
selector: 'ais-range-slider',
template: `
<div [class]="cx()">
<div [class]="cx('body')">
<div #sliderContainer></div>
</div>
</div>
`
},] }
];
NgAisRangeSlider.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisRangeSlider.propDecorators = {
sliderContainer: [{ type: ViewChild, args: ['sliderContainer', { static: false },] }],
pips: [{ type: Input }],
tooltips: [{ type: Input }],
attribute: [{ type: Input }],
min: [{ type: Input }],
max: [{ type: Input }],
precision: [{ type: Input }]
};
class NgAisRangeSliderModule {
}
NgAisRangeSliderModule.decorators = [
{ type: NgModule, args: [{
declarations: [NgAisRangeSlider],
entryComponents: [NgAisRangeSlider],
exports: [NgAisRangeSlider],
imports: [CommonModule],
},] }
];
class NgAisRefinementList extends TypedBaseWidget {
constructor(parentIndex, instantSearchInstance) {
super('RefinementList');
this.parentIndex = parentIndex;
this.instantSearchInstance = instantSearchInstance;
// rendering options
this.showMoreLabel = 'Show more';
this.showLessLabel = 'Show less';
this.searchPlaceholder = 'Search here...';
this.state = {
canRefine: false,
canToggleShowMore: false,
createURL: () => '',
isShowingMore: false,
items: [],
refine: noop,
toggleShowMore: noop,
searchForItems: noop,
isFromSearch: false,
hasExhaustiveItems: false,
sendEvent: noop,
};
}
get isHidden() {
return this.state.items.length === 0 && this.autoHideContainer;
}
ngOnInit() {
this.createWidget(connectRefinementList, {
showMore: this.showMore,
limit: parseNumberInput(this.limit),
showMoreLimit: parseNumberInput(this.showMoreLimit),
attribute: this.attribute,
operator: this.operator,
sortBy: this.sortBy,
escapeFacetValues: true,
transformItems: this.transformItems,
}, {
$$widgetType: 'ais.refinementList',
});
super.ngOnInit();
}
refine(event, item) {
event.preventDefault();
event.stopPropagation();
if (this.state.canRefine) {
// update UI directly, it will update the checkbox state
item.isRefined = !item.isRefined;
// refine through Algolia API
this.state.refine(item.value);
}
}
}
NgAisRefinementList.decorators = [
{ type: Component, args: [{
selector: 'ais-refinement-list',
template: `
<div
[class]="cx()"
*ngIf="!isHidden"
>
<div
*ngIf="searchable"
[class]="cx('searchBox')"
>
<ais-facets-search
[search]="state.searchForItems"
[searchPlaceholder]="searchPlaceholder"
>
</ais-facets-search>
</div>
<ul [class]="cx('list')">
<li
[class]="getItemClass(item)"
*ngFor="let item of state.items"
(click)="refine($event, item)"
>
<label [class]="cx('label')">
<input
[class]="cx('checkbox')"
type="checkbox"
value="{{item.value}}"
[checked]="item.isRefined"
/>
<span [class]="cx('labelText')">
<ais-highlight attribute="highlighted" [hit]="item"></ais-highlight>
</span>
<span [class]="cx('count')">{{item.count}}</span>
</label>
</li>
</ul>
<button
[class]="cx('showMore')"
*ngIf="showMore"
(click)="state.toggleShowMore()"
[disabled]="!state.canToggleShowMore"
>
{{state.isShowingMore ? showLessLabel : showMoreLabel}}
</button>
</div>
`
},] }
];
NgAisRefinementList.ctorParameters = () => [
{ type: NgAisIndex, decorators: [{ type: Inject, args: [forwardRef(() => NgAisIndex),] }, { type: Optional }] },
{ type: NgAisInstantSearch, decorators: [{ type: Inject, args: [forwardRef(() => NgAisInstantSearch),] }] }
];
NgAisRefinementList.propDecorators = {
showMoreLabel: [{ type: Input }],
showLessLabel: [{ type: Input }],
searchable: [{ type: Input }],
searchPlaceholder: [{ type: Input }],
attribute: [{ type: Input }],
operator: [{ type: Input }],
limit: [{ type: Input }],
showMore: [{ type: Input }],
showMoreLimit: [{ type: Input }],
sortBy: [{ type: Input }],
transformItems: [{ type: Input }]
};
class NgAisFacetsSearch {
constructor() {
this.cx = bem('SearchBox');
this.searchQuery = '';
}
handleChange(value) {
this.searchQuery = value;
this.search(value);
}
handleSubmit(event) {
event.preventDefault();
this.search(this.searchQuery);
}
}
NgAisFacetsSearch.decorators = [
{ type: Component, args: [{
selector: 'ais-facets-search',
template: `
<div [class]="cx()">
<form
[class]="cx('form')"
(submit)="handleSubmit($event)"
novalidate
>
<input
[class]="cx('input')"
autocapitalize="off"
autocorrect="off"
placeholder="{{searchPlaceholder}}"
role="textbox"
spellcheck="false"