UNPKG

angular-instantsearch

Version:

Lightning-fast search for Angular apps, by Algolia.

1,574 lines (1,525 loc) 96.5 kB
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"