UNPKG

@millieofzo/ion-selectable

Version:
1,349 lines 203 kB
/* 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.');