@millieofzo/ion-selectable
Version:
Ionic selectable with extended features
1,349 lines • 203 kB
JavaScript
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @angular-eslint/no-output-on-prefix */
import { NgFor, NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, IterableDiffers, Optional, Output, Renderer2, TemplateRef, ViewEncapsulation, forwardRef, } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { IonItem, ModalController, Platform } from '@ionic/angular';
import { IonHeader } from '@ionic/angular/standalone';
import { IonSelectableModalComponent } from './ion-selectable-modal.component';
import { IonSelectableValueTemplateDirective, IonSelectableItemTemplateDirective, IonSelectableItemEndTemplateDirective, IonSelectableTitleTemplateDirective, IonSelectablePlaceholderTemplateDirective, IonSelectableMessageTemplateDirective, IonSelectableGroupTemplateDirective, IonSelectableGroupEndTemplateDirective, IonSelectableCloseButtonTemplateDirective, IonSelectableSearchFailTemplateDirective, IonSelectableAddItemTemplateDirective, IonSelectableFooterTemplateDirective, IonSelectableHeaderTemplateDirective, IonSelectableItemIconTemplateDirective, IonSelectableIconTemplateDirective, } from '../directives';
import * as i0 from "@angular/core";
import * as i1 from "@ionic/angular";
export class IonSelectableComponent {
get _isMultipleCssClass() {
return this.isMultiple;
}
get _hasValueCssClass() {
return this.hasValue();
}
get _hasPlaceholderCssClass() {
return this._hasPlaceholder;
}
get _hasIonLabelCssClass() {
return this._hasIonLabel;
}
get _hasDefaultIonLabelCssClass() {
return this._ionLabelPosition === 'default';
}
get _hasFixedIonLabelCssClass() {
return this._ionLabelPosition === 'fixed';
}
get _hasStackedIonLabelCssClass() {
return this._ionLabelPosition === 'stacked';
}
get _hasFloatingIonLabelCssClass() {
return this._ionLabelPosition === 'floating';
}
get _hasInfiniteScroll() {
return this.isEnabled &&
this._modalComponent &&
this._modalComponent._infiniteScroll
? true
: false;
}
get _shouldStoreItemValue() {
return this.shouldStoreItemValue && this._hasObjects;
}
/**
* Text of [Ionic Label](https://ionicframework.com/docs/api/label).
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#label).
*
* @readonly
* @default null
* @memberof IonSelectableComponent
*/
get label() {
return this._label;
}
/**
* Text that the user has typed in Searchbar.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#searchtext).
*
* @readonly
* @default ''
* @memberof IonSelectableComponent
*/
get searchText() {
return this._searchText;
}
set searchText(searchText) {
this._searchText = searchText;
this._setHasSearchText();
}
/**
* Determines whether search is running.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#issearching).
*
* @default false
* @readonly
* @memberof IonSelectableComponent
*/
get isSearching() {
return this._isSearching;
}
/**
* Determines whether user has typed anything in Searchbar.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#hassearchtext).
*
* @default false
* @readonly
* @memberof IonSelectableComponent
*/
get hasSearchText() {
return this._hasSearchText;
}
get value() {
return this._value;
}
set value(value) {
this._value = value;
// Set value items.
this._valueItems.splice(0, this._valueItems.length);
if (this.isMultiple) {
if (value && value.length) {
Array.prototype.push.apply(this._valueItems, value);
}
}
else {
if (!this._isNullOrWhiteSpace(value)) {
this._valueItems.push(value);
}
}
this._setIonItemHasValue();
this._setHasPlaceholder();
}
/**
* Determines whether the component is enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#isenabled).
*
* @default true
* @memberof IonSelectableComponent
*/
get isEnabled() {
return this._isEnabled;
}
set isEnabled(isEnabled) {
this._isEnabled = !!isEnabled;
this.enableIonItem(this._isEnabled);
}
/**
* Determines whether Modal should be closed when backdrop is clicked.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#shouldbackdropclose).
*
* @default true
* @memberof IonSelectableComponent
*/
get shouldBackdropClose() {
return this._shouldBackdropClose;
}
set shouldBackdropClose(shouldBackdropClose) {
this._shouldBackdropClose = !!shouldBackdropClose;
}
/**
* Determines whether Modal is opened.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#isopened).
*
* @default false
* @readonly
* @memberof IonSelectableComponent
*/
get isOpened() {
return this._isOpened;
}
/**
* Determines whether Confirm button is visible for single selection.
* By default Confirm button is visible only for multiple selection.
* **Note**: It is always true for multiple selection and cannot be changed.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#hasconfirmbutton).
*
* @default true
* @memberof IonSelectableComponent
*/
get hasConfirmButton() {
return this._hasConfirmButton;
}
set hasConfirmButton(hasConfirmButton) {
this._hasConfirmButton = !!hasConfirmButton;
this._countFooterButtons();
}
/**
* Determines whether `onSearch` event is enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#isonsearchenabled).
*
* @default true
* @memberof IonSelectableComponent
*/
get isOnSearchEnabled() {
return this._isOnSearchEnabled;
}
set isOnSearchEnabled(isOnSearchEnabled) {
this._isOnSearchEnabled = !!isOnSearchEnabled;
}
/**
* Determines whether to show Clear button.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#canclear).
*
* @default false
* @memberof IonSelectableComponent
*/
get canClear() {
return this._canClear;
}
set canClear(canClear) {
this._canClear = !!canClear;
this._countFooterButtons();
}
/**
* Determines whether multiple items can be selected.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#ismultiple).
*
* @default false
* @memberof IonSelectableComponent
*/
get isMultiple() {
return this._isMultiple;
}
set isMultiple(isMultiple) {
this._isMultiple = !!isMultiple;
this._countFooterButtons();
}
/**
* A list of items that are selected and awaiting confirmation by user, when he has clicked Confirm button.
* After the user has clicked Confirm button items to confirm are cleared.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#itemstoconfirm).
*
* @default []
* @readonly
* @memberof IonSelectableComponent
*/
get itemsToConfirm() {
return this._itemsToConfirm;
}
/**
* Determines whether to allow adding items.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#canadditem).
*
* @default false
* @memberof IonSelectableComponent
*/
get canAddItem() {
return this._canAddItem;
}
set canAddItem(canAddItem) {
this._canAddItem = !!canAddItem;
this._countFooterButtons();
}
constructor(_modalController, _platform, ionItem, _iterableDiffers, _element, _renderer) {
this._modalController = _modalController;
this._platform = _platform;
this.ionItem = ionItem;
this._iterableDiffers = _iterableDiffers;
this._element = _element;
this._renderer = _renderer;
this._cssClass = true;
this._isOnSearchEnabled = true;
this._isEnabled = true;
this._shouldBackdropClose = true;
this._isOpened = false;
this._value = null;
this._canClear = false;
this._hasConfirmButton = false;
this._isMultiple = false;
this._canAddItem = false;
this.onItemsChange = new EventEmitter();
this._hasIonLabel = false;
this._ionLabelPosition = null;
this._label = '';
this._valueItems = [];
this._searchText = '';
this._hasSearchText = false;
this._groups = [];
this._itemsToConfirm = [];
this._selectedItems = [];
this._filteredGroups = [];
this._isAddItemTemplateVisible = false;
this._isFooterVisible = true;
this._itemToAdd = null;
this._footerButtonsCount = 0;
this._hasFilteredItems = false;
/**
* A list of items.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#items).
*
* @default []
* @memberof IonSelectableComponent
*/
this.items = [];
this.itemsChange = new EventEmitter();
/**
* Modal CSS class.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#modalcssclass).
*
* @default null
* @memberof IonSelectableComponent
*/
this.modalCssClass = '';
/**
* Determines whether Confirm button is enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#isconfirmbuttonenabled).
*
* @default true
* @memberof IonSelectableComponent
*/
this.isConfirmButtonEnabled = true;
/**
* Item property to use as a unique identifier, e.g, `'id'`.
* **Note**: `items` should be an object array.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#itemvaluefield).
*
* @default null
* @memberof IonSelectableComponent
*/
this.itemValueField = '';
/**
* Item property to display, e.g, `'name'`.
* **Note**: `items` should be an object array.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#itemtextfield).
*
* @default false
* @memberof IonSelectableComponent
*/
this.itemTextField = '';
/**
*
* Group property to use as a unique identifier to group items, e.g. `'country.id'`.
* **Note**: `items` should be an object array.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#groupvaluefield).
*
* @default null
* @memberof IonSelectableComponent
*/
this.groupValueField = '';
/**
* Group property to display, e.g. `'country.name'`.
* **Note**: `items` should be an object array.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#grouptextfield).
*
* @default null
* @memberof IonSelectableComponent
*/
this.groupTextField = '';
/**
* Determines whether to show Searchbar.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#cansearch).
*
* @default false
* @memberof IonSelectableComponent
*/
this.canSearch = false;
/**
* Determines whether Ionic [InfiniteScroll](https://ionicframework.com/docs/api/components/infinite-scroll/InfiniteScroll/) is enabled.
* **Note**: Infinite scroll cannot be used together with virtual scroll.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#hasinfinitescroll).
*
* @default false
* @memberof IonSelectableComponent
*/
this.hasInfiniteScroll = false;
/**
* Determines whether Ionic [VirtualScroll](https://ionicframework.com/docs/api/components/virtual-scroll/VirtualScroll/) is enabled.
* **Note**: Virtual scroll cannot be used together with infinite scroll.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#hasvirtualscroll).
*
* @default false
* @memberof IonSelectableComponent
*/
this.hasVirtualScroll = false;
/**
* See Ionic VirtualScroll [approxItemHeight](https://ionicframework.com/docs/api/components/virtual-scroll/VirtualScroll/).
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#virtualscrollapproxitemheight).
*
* @default '40px'
* @memberof IonSelectableComponent
*/
this.virtualScrollApproxItemHeight = '40px';
/**
* A placeholder for Searchbar.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#searchplaceholder).
*
* @default 'Search'
* @memberof IonSelectableComponent
*/
this.searchPlaceholder = 'Search';
/**
* A placeholder.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#placeholder).
*
* @default null
* @memberof IonSelectableComponent
*/
this.placeholder = '';
/**
* Text to display when no items have been found during search.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#searchfailtext).
*
* @default 'No items found.'
* @memberof IonSelectableComponent
*/
this.searchFailText = 'No items found.';
/**
* Clear button text.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#clearbuttontext).
*
* @default 'Clear'
* @memberof IonSelectableComponent
*/
this.clearButtonText = 'Clear';
/**
* Add button text.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#addbuttontext).
*
* @default 'Add'
* @memberof IonSelectableComponent
*/
this.addButtonText = 'Add';
/**
* Confirm button text.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#confirmbuttontext).
*
* @default 'OK'
* @memberof IonSelectableComponent
*/
this.confirmButtonText = 'OK';
/**
* Close button text.
* The field is only applicable to **iOS** platform, on **Android** only Cross icon is displayed.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#closebuttontext).
*
* @default 'Cancel'
* @memberof IonSelectableComponent
*/
this.closeButtonText = 'Cancel';
/**
* Determines whether Searchbar should receive focus when Modal is opened.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#shouldfocussearchbar).
*
* @default false
* @memberof IonSelectableComponent
*/
this.shouldFocusSearchbar = false;
/**
* Header color. [Ionic colors](https://ionicframework.com/docs/theming/advanced#colors) are supported.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#headercolor).
*
* @default null
* @memberof IonSelectableComponent
*/
this.headerColor = '';
/**
* Group color. [Ionic colors](https://ionicframework.com/docs/theming/advanced#colors) are supported.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#groupcolor).
*
* @default null
* @memberof IonSelectableComponent
*/
this.groupColor = '';
/**
* Close button slot. [Ionic slots](https://ionicframework.com/docs/api/buttons) are supported.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#closebuttonslot).
*
* @default 'start'
* @memberof IonSelectableComponent
*/
this.closeButtonSlot = 'start';
/**
* Item icon slot. [Ionic slots](https://ionicframework.com/docs/api/item) are supported.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#itemiconslot).
*
* @default 'start'
* @memberof IonSelectableComponent
*/
this.itemIconSlot = 'start';
/**
* Fires when item/s has been selected and Modal closed.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onchange).
*
* @memberof IonSelectableComponent
*/
this.onChange = new EventEmitter();
/**
* Fires when the user is typing in Searchbar.
* **Note**: `canSearch` and `isOnSearchEnabled` has to be enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onsearch).
*
* @memberof IonSelectableComponent
*/
this.onSearch = new EventEmitter();
/**
* Fires when no items have been found.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onsearchfail).
*
* @memberof IonSelectableComponent
*/
this.onSearchFail = new EventEmitter();
/**
* Fires when some items have been found.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onsearchsuccess).
*
* @memberof IonSelectableComponent
*/
this.onSearchSuccess = new EventEmitter();
/**
* Fires when the user has scrolled to the end of the list.
* **Note**: `hasInfiniteScroll` has to be enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#oninfinitescroll).
*
* @memberof IonSelectableComponent
*/
this.onInfiniteScroll = new EventEmitter();
/**
* Fires when Modal has been opened.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onopen).
*
* @memberof IonSelectableComponent
*/
this.onOpen = new EventEmitter();
/**
* Fires when Modal has been closed.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onclose).
*
* @memberof IonSelectableComponent
*/
this.onClose = new EventEmitter();
/**
* Fires when an item has been selected or unselected.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onselect).
*
* @memberof IonSelectableComponent
*/
this.onSelect = new EventEmitter();
/**
* Fires when Clear button has been clicked.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onclear).
*
* @memberof IonSelectableComponent
*/
this.onClear = new EventEmitter();
/**
* How long, in milliseconds, to wait to filter items or to trigger `onSearch` event after each keystroke.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#searchdebounce).
*
* @default 250
* @memberof IonSelectableComponent
*/
this.searchDebounce = 250;
/**
* A list of items to disable.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#disableditems).
*
* @default []
* @memberof IonSelectableComponent
*/
this.disabledItems = [];
/**
* Determines whether item value only should be stored in `ngModel`, not the entire item.
* **Note**: Item value is defined by `itemValueField`.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#shouldstoreitemvalue).
*
* @default false
* @memberof IonSelectableComponent
*/
this.shouldStoreItemValue = false;
/**
* Determines whether to allow editing items.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#cansaveitem).
*
* @default false
* @memberof IonSelectableComponent
*/
this.canSaveItem = false;
/**
* Determines whether to allow deleting items.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#candeleteitem).
*
* @default false
* @memberof IonSelectableComponent
*/
this.canDeleteItem = false;
/**
* Fires when Edit item button has been clicked.
* When the button has been clicked `IonSelectableAddItemTemplate` will be shown. Use the template to create a form to edit item.
* **Note**: `canSaveItem` has to be enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onsaveitem).
*
* @memberof IonSelectableComponent
*/
this.onSaveItem = new EventEmitter();
/**
* Fires when Delete item button has been clicked.
* **Note**: `canDeleteItem` has to be enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#ondeleteitem).
*
* @memberof IonSelectableComponent
*/
this.onDeleteItem = new EventEmitter();
/**
* Fires when Add item button has been clicked.
* When the button has been clicked `IonSelectableAddItemTemplate` will be shown. Use the template to create a form to add item.
* **Note**: `canAddItem` has to be enabled.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#onadditem).
*
* @memberof IonSelectableComponent
*/
this.onAddItem = new EventEmitter();
/**
* See Ionic VirtualScroll [headerFn](https://ionicframework.com/docs/api/components/virtual-scroll/VirtualScroll/).
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#virtualscrollheaderfn).
*
* @memberof IonSelectableComponent
*/
this.virtualScrollHeaderFn = () => {
return null;
};
this.propagateOnChange = (_) => { };
this.propagateOnTouched = () => { };
if (!this.items?.length) {
this.items = [];
}
this._itemsDiffer = this._iterableDiffers.find(this.items).create();
}
initFocus() { }
enableIonItem(isEnabled) {
if (!this.ionItem) {
return;
}
this.ionItem.disabled = !isEnabled;
}
_isNullOrWhiteSpace(value) {
if (value === null || value === undefined) {
return true;
}
// Convert value to string in case if it's not.
return value.toString().replace(/\s/g, '').length < 1;
}
_setHasSearchText() {
this._hasSearchText = !this._isNullOrWhiteSpace(this._searchText);
}
_hasOnSearch() {
return this.isOnSearchEnabled && this.onSearch.observers.length > 0;
}
_hasOnSaveItem() {
return this.canSaveItem && this.onSaveItem.observers.length > 0;
}
_hasOnAddItem() {
return this.canAddItem && this.onAddItem.observers.length > 0;
}
_hasOnDeleteItem() {
return this.canDeleteItem && this.onDeleteItem.observers.length > 0;
}
_emitValueChange() {
this.propagateOnChange(this.value);
this.onChange.emit({
component: this,
value: this.value,
});
}
_emitSearch() {
if (!this.canSearch) {
return;
}
this.onSearch.emit({
component: this,
text: this._searchText,
});
}
_emitOnSelect(item, isSelected) {
this.onSelect.emit({
component: this,
item: item,
isSelected: isSelected,
});
}
_emitOnClear(items) {
this.onClear.emit({
component: this,
items: items,
});
}
_emitOnSearchSuccessOrFail(isSuccess) {
const eventData = {
component: this,
text: this._searchText,
};
if (isSuccess) {
this.onSearchSuccess.emit(eventData);
}
else {
this.onSearchFail.emit(eventData);
}
}
_formatItem(item) {
if (this._isNullOrWhiteSpace(item)) {
return '';
}
return this.itemTextField ? item[this.itemTextField] : item.toString();
}
_formatValueItem(item) {
if (this._shouldStoreItemValue) {
// Get item text from the list as we store it's value only.
const selectedItem = this.items.find((_item) => {
return _item[this.itemValueField] === item;
});
return this._formatItem(selectedItem);
}
else {
return this._formatItem(item);
}
}
_getItemValue(item) {
if (!this._hasObjects) {
return item;
}
return item[this.itemValueField];
}
_getStoredItemValue(item) {
if (!this._hasObjects) {
return item;
}
return this._shouldStoreItemValue ? item : item[this.itemValueField];
}
_onSearchbarClear() {
// Ionic Searchbar doesn't clear bind with ngModel value.
// Do it ourselves.
this._searchText = '';
}
_filterItems() {
this._setHasSearchText();
if (this._hasOnSearch()) {
// Delegate filtering to the event.
this._emitSearch();
}
else {
// Default filtering.
let groups = [];
if (!this._searchText?.trim()) {
groups = this._groups;
}
else {
const filterText = this._searchText.trim().toLowerCase();
this._groups.forEach((group) => {
const items = group.items.filter((item) => {
const itemText = (this.itemTextField ? item[this.itemTextField] : item)
.toString()
.toLowerCase();
return itemText.indexOf(filterText) !== -1;
});
if (items.length) {
groups.push({
value: group.value,
text: group.text,
items: items,
});
}
});
// No items found.
if (!groups.length) {
groups.push({
items: [],
});
}
}
this._filteredGroups = groups;
this._hasFilteredItems = !this._areGroupsEmpty(groups);
this._emitOnSearchSuccessOrFail(this._hasFilteredItems);
}
}
_isItemDisabled(item) {
if (!this.disabledItems) {
return false;
}
return this.disabledItems.some((_item) => {
return this._getItemValue(_item) === this._getItemValue(item);
});
}
_isItemSelected(item) {
return (this._selectedItems.find((selectedItem) => {
return (this._getItemValue(item) === this._getStoredItemValue(selectedItem));
}) !== undefined);
}
_addSelectedItem(item) {
if (this._shouldStoreItemValue) {
this._selectedItems.push(this._getItemValue(item));
}
else {
this._selectedItems.push(item);
}
}
_deleteSelectedItem(item) {
let itemToDeleteIndex = -1;
this._selectedItems.forEach((selectedItem, itemIndex) => {
if (this._getItemValue(item) === this._getStoredItemValue(selectedItem)) {
itemToDeleteIndex = itemIndex;
}
});
this._selectedItems.splice(itemToDeleteIndex, 1);
}
_click() {
if (!this.isEnabled) {
return;
}
this._label = this._getLabelText();
this.open().then(() => {
this.onOpen.emit({
component: this,
});
});
}
_saveItem(event, item) {
event.stopPropagation();
this._itemToAdd = item;
if (this._hasOnSaveItem()) {
this.onSaveItem.emit({
component: this,
item: this._itemToAdd,
});
}
else {
this.showAddItemTemplate();
}
}
_deleteItemClick(event, item) {
event.stopPropagation();
this._itemToAdd = item;
if (this._hasOnDeleteItem()) {
// Delegate logic to event.
this.onDeleteItem.emit({
component: this,
item: this._itemToAdd,
});
}
else {
this.deleteItem(this._itemToAdd);
}
}
_addItemClick() {
if (this._hasOnAddItem()) {
this.onAddItem.emit({
component: this,
});
}
else {
this.showAddItemTemplate();
}
}
_positionAddItemTemplate() {
// Wait for the template to render.
setTimeout(() => {
const footer = this._modalComponent._element.nativeElement.querySelector('.ionic-selectable-add-item-template ion-footer');
this._addItemTemplateFooterHeight = footer
? `calc(100% - ${footer.offsetHeight}px)`
: '100%';
}, 100);
}
_close() {
this.close().then(() => {
this.onClose.emit({
component: this,
});
});
if (!this._hasOnSearch()) {
this._searchText = '';
this._setHasSearchText();
}
}
_clear() {
const selectedItems = this._selectedItems;
this.clear();
this._emitValueChange();
this._emitOnClear(selectedItems);
this.close().then(() => {
this.onClose.emit({
component: this,
});
});
}
_getMoreItems() {
this.onInfiniteScroll.emit({
component: this,
text: this._searchText,
});
}
_setItemsToConfirm(items) {
// Return a copy of original array, so it couldn't be changed from outside.
this._itemsToConfirm = [].concat(items);
}
_doSelect(selectedItem) {
this.value = selectedItem;
this._emitValueChange();
}
_select(item) {
const isItemSelected = this._isItemSelected(item);
if (this.isMultiple) {
if (isItemSelected) {
this._deleteSelectedItem(item);
}
else {
this._addSelectedItem(item);
}
this._setItemsToConfirm(this._selectedItems);
// Emit onSelect event after setting items to confirm so they could be used
// inside the event.
this._emitOnSelect(item, !isItemSelected);
}
else {
if (this.hasConfirmButton || this.footerTemplate) {
// Don't close Modal and keep track on items to confirm.
// When footer template is used it's up to developer to close Modal.
this._selectedItems = [];
if (isItemSelected) {
this._deleteSelectedItem(item);
}
else {
this._addSelectedItem(item);
}
this._setItemsToConfirm(this._selectedItems);
// Emit onSelect event after setting items to confirm so they could be used
// inside the event.
this._emitOnSelect(item, !isItemSelected);
}
else {
if (!isItemSelected) {
this._selectedItems = [];
this._addSelectedItem(item);
// Emit onSelect before onChange.
this._emitOnSelect(item, true);
if (this._shouldStoreItemValue) {
this._doSelect(this._getItemValue(item));
}
else {
this._doSelect(item);
}
}
this._close();
}
}
}
_confirm() {
this.confirm();
this._close();
}
_getLabelText() {
return this._ionLabelElement ? this._ionLabelElement.textContent : null;
}
_areGroupsEmpty(groups) {
return (groups.length === 0 ||
groups.every((group) => {
return !group.items?.length;
}));
}
_countFooterButtons() {
let footerButtonsCount = 0;
if (this.canClear) {
footerButtonsCount++;
}
if (this.isMultiple || this._hasConfirmButton) {
footerButtonsCount++;
}
if (this.canAddItem) {
footerButtonsCount++;
}
this._footerButtonsCount = footerButtonsCount;
}
_setItems(items) {
// It's important to have an empty starting group with empty items (groups[0].items),
// because we bind to it when using VirtualScroll.
// See https://github.com/eakoriakin/ionic-selectable/issues/70.
let groups = [
{
items: items || [],
},
];
if (items?.length) {
if (this._hasGroups) {
groups = [];
items.forEach((item) => {
const groupValue = this._getPropertyValue(item, this.groupValueField), group = groups.find((_group) => _group.value === groupValue);
if (group) {
group.items.push(item);
}
else {
groups.push({
value: groupValue,
text: this._getPropertyValue(item, this.groupTextField),
items: [item],
});
}
});
}
}
this._groups = groups;
this._filteredGroups = this._groups;
this._hasFilteredItems = !this._areGroupsEmpty(this._filteredGroups);
}
_getPropertyValue(object, property) {
if (!property) {
return null;
}
return property.split('.').reduce((_object, _property) => {
return _object ? _object[_property] : null;
}, object);
}
_setIonItemHasFocus(hasFocus) {
if (!this.ionItem) {
return;
}
// Apply focus CSS class for proper stylying of ion-item/ion-label.
this._setIonItemCssClass('item-has-focus', hasFocus);
}
_setIonItemHasValue() {
if (!this.ionItem) {
return;
}
// Apply value CSS class for proper stylying of ion-item/ion-label.
this._setIonItemCssClass('item-has-value', this.hasValue());
}
_setHasPlaceholder() {
this._hasPlaceholder =
!this.hasValue() &&
(!this._isNullOrWhiteSpace(this.placeholder) || this.placeholderTemplate)
? true
: false;
}
_setIonItemCssClass(cssClass, shouldAdd) {
if (!this._ionItemElement) {
return;
}
// Change to Renderer2
if (shouldAdd) {
this._renderer.addClass(this._ionItemElement, cssClass);
}
else {
this._renderer.removeClass(this._ionItemElement, cssClass);
}
}
_toggleAddItemTemplate(isVisible) {
// It should be possible to show/hide the template regardless
// canAddItem or canSaveItem parameters, so we could implement some
// custom behavior. E.g. adding item when search fails using onSearchFail event.
if (!this.addItemTemplate) {
return;
}
// To make SaveItemTemplate visible we just position it over list using CSS.
// We don't hide list with *ngIf or [hidden] to prevent its scroll position.
this._isAddItemTemplateVisible = isVisible;
this._isFooterVisible = !isVisible;
}
/* ControlValueAccessor */
writeValue(value) {
this.value = value;
}
registerOnChange(method) {
this.propagateOnChange = method;
}
registerOnTouched(method) {
this.propagateOnTouched = method;
}
setDisabledState(isDisabled) {
this.isEnabled = !isDisabled;
}
/* .ControlValueAccessor */
ngOnInit() {
this._isIos = this._platform.is('ios');
this._isMD = !this._isIos;
this._hasObjects = !this._isNullOrWhiteSpace(this.itemValueField);
// Grouping is supported for objects only.
// Ionic VirtualScroll has it's own implementation of grouping.
this._hasGroups = Boolean(this._hasObjects && this.groupValueField && !this.hasVirtualScroll);
if (this.ionItem) {
this._ionItemElement = this._element.nativeElement.closest('ion-item');
this._setIonItemCssClass('item-interactive', true);
this._setIonItemCssClass('item-ionic-selectable', true);
if (this._ionItemElement) {
this._ionLabelElement = this._ionItemElement.querySelector('ion-label');
if (this._ionLabelElement) {
this._hasIonLabel = true;
this._ionLabelPosition =
this._ionLabelElement.getAttribute('position') || 'default';
}
}
}
this.enableIonItem(this.isEnabled);
}
ngDoCheck() {
const itemsChanges = this._itemsDiffer.diff(this.items);
if (itemsChanges) {
this._setItems(this.items);
this.onItemsChange.emit({
component: this,
});
}
}
/**
* Adds item.
* **Note**: If you want an item to be added to the original array as well use two-way data binding syntax on `[(items)]` field.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#additem).
*
* @param item Item to add.
* @returns Promise that resolves when item has been added.
* @memberof IonSelectableComponent
*/
addItem(item) {
const self = this;
// Adding item triggers onItemsChange.
// Return a promise that resolves when onItemsChange finishes.
// We need a promise or user could do something after item has been added,
// e.g. use search() method to find the added item.
this.items.unshift(item);
// Close any running subscription.
if (this._addItemObservable) {
this._addItemObservable.unsubscribe();
}
return new Promise(function (resolve, reject) {
// Complete callback isn't fired for some reason,
// so unsubscribe in both success and fail cases.
self._addItemObservable = self.onItemsChange.asObservable().subscribe(() => {
self._addItemObservable.unsubscribe();
resolve('');
}, () => {
self._addItemObservable.unsubscribe();
reject();
});
});
}
/**
* Deletes item.
* **Note**: If you want an item to be deleted from the original array as well use two-way data binding syntax on `[(items)]` field.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#deleteitem).
*
* @param item Item to delete.
* @returns Promise that resolves when item has been deleted.
* @memberof IonSelectableComponent
*/
deleteItem(item) {
const self = this;
let hasValueChanged = false;
// Remove deleted item from selected items.
if (this._selectedItems) {
this._selectedItems = this._selectedItems.filter((_item) => {
return this._getItemValue(item) !== this._getStoredItemValue(_item);
});
}
// Remove deleted item from value.
if (this.value) {
if (this.isMultiple) {
const values = this.value.filter((value) => {
return value.id !== item.id;
});
if (values.length !== this.value.length) {
this.value = values;
hasValueChanged = true;
}
}
else {
if (item === this.value) {
this.value = null;
hasValueChanged = true;
}
}
}
if (hasValueChanged) {
this._emitValueChange();
}
// Remove deleted item from list.
const items = this.items.filter((_item) => {
return _item.id !== item.id;
});
// Refresh items on parent component.
this.itemsChange.emit(items);
// Refresh list.
this._setItems(items);
this.onItemsChange.emit({
component: this,
});
// Close any running subscription.
if (this._deleteItemObservable) {
this._deleteItemObservable.unsubscribe();
}
return new Promise(function (resolve, reject) {
// Complete callback isn't fired for some reason,
// so unsubscribe in both success and fail cases.
self._deleteItemObservable = self.onItemsChange.asObservable().subscribe(() => {
self._deleteItemObservable.unsubscribe();
resolve('');
}, () => {
self._deleteItemObservable.unsubscribe();
reject();
});
});
}
/**
* Determines whether any item has been selected.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#hasvalue).
*
* @returns A boolean determining whether any item has been selected.
* @memberof IonSelectableComponent
*/
hasValue() {
if (this.isMultiple) {
return this._valueItems.length !== 0;
}
else {
return (this._valueItems.length !== 0 &&
!this._isNullOrWhiteSpace(this._valueItems[0]));
}
}
/**
* Opens Modal.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#open).
*
* @returns Promise that resolves when Modal has been opened.
* @memberof IonSelectableComponent
*/
open() {
const self = this;
return new Promise(function (resolve, reject) {
if (!self._isEnabled || self._isOpened) {
reject('IonSelectable is disabled or already opened.');
return;
}
self._filterItems();
self._isOpened = true;
const modalOptions = {
component: IonSelectableModalComponent,
componentProps: { selectComponent: self },
backdropDismiss: self._shouldBackdropClose,
};
if (self.modalCssClass) {
modalOptions.cssClass = self.modalCssClass;
}
if (self.modalEnterAnimation) {
modalOptions.enterAnimation = self.modalEnterAnimation;
}
if (self.modalLeaveAnimation) {
modalOptions.leaveAnimation = self.modalLeaveAnimation;
}
self._modalController.create(modalOptions).then((modal) => {
self._modal = modal;
modal.present().then(() => {
// Set focus after Modal has opened to avoid flickering of focus highlighting
// before Modal opening.
self._setIonItemHasFocus(true);
resolve();
});
modal.onWillDismiss().then(() => {
self._setIonItemHasFocus(false);
});
modal.onDidDismiss().then((event) => {
self._isOpened = false;
self._itemsToConfirm = [];
// Closed by clicking on backdrop outside modal.
if (event.role === 'backdrop') {
self.onClose.emit({
component: self,
});
}
});
});
});
}
/**
* Closes Modal.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#close).
*
* @returns Promise that resolves when Modal has been closed.
* @memberof IonSelectableComponent
*/
close() {
const self = this;
return new Promise(function (resolve, reject) {
if (!self._isEnabled || !self._isOpened) {
reject('IonSelectable is disabled or already closed.');
return;
}
self.propagateOnTouched();
self._isOpened = false;
self._itemToAdd = null;
self._modal.dismiss().then(() => {
self._setIonItemHasFocus(false);
self.hideAddItemTemplate();
resolve();
});
});
}
/**
* Clears value.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#clear).
*
* @memberof IonSelectableComponent
*/
clear() {
this.value = this.isMultiple ? [] : null;
this._itemsToConfirm = [];
this.propagateOnChange(this.value);
}
/**
* Confirms selected items by updating value.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#confirm).
*
* @memberof IonSelectableComponent
*/
confirm() {
if (this.isMultiple) {
this._doSelect(this._selectedItems);
}
else if (this.hasConfirmButton || this.footerTemplate) {
this._doSelect(this._selectedItems[0] || null);
}
}
/**
* Selects or deselects all or specific items.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#toggleitems).
*
* @param isSelect Determines whether to select or deselect items.
* @param [items] Items to toggle. If items are not set all items will be toggled.
* @memberof IonSelectableComponent
*/
toggleItems(isSelect, items) {
if (isSelect) {
const hasItems = items?.length;
let itemsToToggle = this._groups.reduce((allItems, group) => {
return allItems.concat(group.items);
}, []);
// Don't allow to select all items in single mode.
if (!this.isMultiple && !hasItems) {
itemsToToggle = [];
}
// Toggle specific items.
if (hasItems) {
itemsToToggle = itemsToToggle.filter((itemToToggle) => {
return (items.find((item) => {
return (this._getItemValue(itemToToggle) === this._getItemValue(item));
}) !== undefined);
});
// Take the first item for single mode.
if (!this.isMultiple) {
itemsToToggle.splice(0, 1);
}
}
itemsToToggle.forEach((item) => {
this._addSelectedItem(item);
});
}
else {
this._selectedItems = [];
}
this._setItemsToConfirm(this._selectedItems);
}
/**
* Scrolls to the top of Modal content.
* See more on [GitHub](https://github.com/eakoriakin/ionic-selectable/wiki/Documentation#scrolltotop).
*
* @returns Promise that resolves when scroll has been completed.
* @memberof IonSelectableComponent
*/
scrollToTop() {
const self = this;
return new Promise(function (resolve, reject) {
if (!self._isOpened) {
reject('IonSelectable content cannot be scrolled.');