aurelia-bootstrap-select
Version:
An Aurelia Custom Element for the 3rd party addon [Bootstrap-Select]
494 lines (442 loc) • 17 kB
JavaScript
import {inject, bindable, bindingMode, DOM} from 'aurelia-framework';
import {UtilService} from './util-service';
import $ from 'jquery';
import 'bootstrap-select';
import {globalExtraOptions, globalPickerOptions} from './picker-global-options';
import {BindingEngine} from 'aurelia-binding';
//import 'bootstrap-select/dist/css/bootstrap-select.min.css';
export class AbpSelectCustomElement {
collection = [];
element;
selectedItem;
selectedValue;
class;
dataMappingStructure;
disabled = false;
emptyOnNull = false;
hasOptgroup = false;
multiple = false;
objectKey = 'id';
pickerOptions;
placeholder;
required = false;
selected;
// events (from the View)
onChanged;
onHide;
onHidden;
onLoaded;
onRendered;
onRefreshed;
onShow;
onShown;
// variables
_originalSelectedIndexes;
_originalSelectedObjects;
pickerRef;
bindingEngine;
collectionSubscription;
constructor(elm, utilService, bindingEngine) {
this.elm = elm;
this.util = utilService;
this.bindingEngine = bindingEngine;
// ensure the element exposes a "focus" method for Aurelia-Validation
elm.focus = () => this.input.focus();
}
attached() {
// reference to the DOM element
this.domElm = $(this.pickerRef);
// expose events & methods
let events = this.applyExposeEvents();
let methods = this.exposeMethods();
// finally create the bootstrap-select with all options
let pickerOptions = Object.assign({}, globalPickerOptions, this.pickerOptions || {});
this.domElm.selectpicker(pickerOptions);
// expose the element object to the outside
// this will be useful for calling events/methods/options from the outside
this.element = {
events: events,
options: pickerOptions,
methods: methods,
dataMappingStructure: this.dataMappingStructure
};
let observer = this.bindingEngine.expressionObserver(this, 'collection');
this.collectionSubscription = observer.subscribe((newCollection, oldCollection) => this.collectionChangedObserver(newCollection, oldCollection));
this.watchOnLoadedToRenderPreSelection();
this.watchOnChangedToUpdateValueAndItemObjects();
}
/**
* Keep original value(s) that could be passed by the user ViewModel.
* If nothing was passed, it will default to first option of select
*/
bind() {
if (this.elm.hasAttribute('multiple')) {
this.multiple = true;
if (this.elm.getAttribute('multiple') === false) {
this.multiple = false;
}
}
let originalSelectedObjects = this.selectedItem || this.elm.getAttribute('selectedItem');
let originalSelectedIndexes = this.selectedValue || this.elm.getAttribute('selectedValue');
// make a deep clone copy to avoid object pointer issues
this._originalSelectedObjects = originalSelectedObjects ? JSON.parse(JSON.stringify(originalSelectedObjects)) : null;
this._originalSelectedIndexes = originalSelectedIndexes ? JSON.parse(JSON.stringify(originalSelectedIndexes)) : null;
}
/**
* Apply/expose selectpicker events
* Each event has 2 ways of triggering an event (from the View as an attribute or from the ViewModel has a function call)
*/
applyExposeEvents() {
let events = {};
this.domElm.on('show.bs.select', (e) => {
if (typeof this.onShow === 'function') {
this.onShow(e);
}
if (typeof events.onShow === 'function') {
events.onShow(e);
}
});
this.domElm.on('shown.bs.select', (e) => {
if (typeof this.onShown === 'function') {
this.onShown(e);
}
if (typeof events.onShown === 'function') {
events.onShown(e);
}
});
this.domElm.on('hide.bs.select', (e) => {
if (typeof this.onHide === 'function') {
this.onHide(e);
}
if (typeof events.onHide === 'function') {
events.onHide(e);
}
});
this.domElm.on('hidden.bs.select', (e) => {
if (typeof this.onHidden === 'function') {
this.onHidden(e);
}
if (typeof events.onHidden === 'function') {
events.onHidden(e);
}
});
this.domElm.on('loaded.bs.select', (e) => {
if (typeof this.onLoaded === 'function') {
this.onLoaded(e);
}
if (typeof events.onLoaded === 'function') {
events.onLoaded(e);
}
});
this.domElm.on('rendered.bs.select', (e) => {
if (typeof this.onRendered === 'function') {
this.onRendered(e);
}
if (typeof events.onRendered === 'function') {
events.onRendered(e);
}
});
this.domElm.on('refreshed.bs.select', (e) => {
if (typeof this.onRefreshed === 'function') {
this.onRefreshed(e);
}
if (typeof events.onRefreshed === 'function') {
events.onRefreshed(e);
}
});
this.domElm.on('changed.bs.select', (e, clickedIndex, newValue, oldValue) => {
if (typeof this.onChanged === 'function') {
this.onChanged(e);
}
if (typeof events.onChanged === 'function') {
events.onChanged(e);
}
});
return events;
}
/**
* forward "blur" events to the custom element
* As described in Aurelia-Validation
* https://www.danyow.net/aurelia-validation-alpha/
*/
blur() {
const event = DOM.createCustomEvent('blur');
this.elm.dispatchEvent(event);
}
/**
* Expose selectpicker methods
*/
exposeMethods() {
let methods = {
deselectAll: () => this.domElm.selectpicker('deselectAll'),
destroy: () => this.domElm.selectpicker('destroy'),
disableOptgroupByIndex: (index, isDisable = true) => {
if (this.domElm.find('optgroup')[index]) {
const optgroup = this.domElm.find('optgroup').eq(index);
const label = optgroup.prop('label');
optgroup.prop('disabled', isDisable);
this.collection.forEach((item) => {
if (item.group === label) {
item.disabled = isDisable;
}
});
this.domElm.selectpicker('refresh');
}
},
disableOptgroupByLabel: (label, isDisable = true) => {
this.domElm.find(`optgroup[label=${label}]`).prop('disabled', isDisable);
this.collection.forEach((item) => {
if (item.group === label) {
item.disabled = isDisable;
}
});
this.domElm.selectpicker('refresh');
},
mobile: () => this.domElm.selectpicker('mobile'),
refresh: () => this.domElm.selectpicker('refresh'),
render: () => this.domElm.selectpicker('render'),
val: (value) => this.domElm.selectpicker('val', value),
selectAll: () => this.domElm.selectpicker('selectAll'),
setStyle: (style, isAddingTheClass = true) => {
if (style.includes('btn')) {
let action = isAddingTheClass ? 'add' : 'remove';
this.domElm.selectpicker('setStyle', style, action);
} else {
this.domElm.addClass(style).selectpicker('setStyle');
}
}
};
return methods;
}
collectionChangedObserver(newCollection, oldCollection) {
setTimeout(() => {
this.domElm.selectpicker('refresh');
const currentValue = this.selectedValue || this.selectedItem;
const selection = this.findItems(this.collection, currentValue, this.objectKey);
if (this.isEmptySelection(selection)) {
this.renderPreSelection();
}
});
}
detached() {
if (this.domElm && this.domElm.selectpicker) {
this.domElm.selectpicker('destroy');
}
this.collectionSubscription.dispose();
}
/**
* Get the grouped collection by the mapping 'groupLabel' property
* @return {array} groupedCollection
*/
getGroupedCollection() {
// group the array by the mapping group property
// ex input: [{ id: 12, option: 'Steam', group: 'Breads' }, { id: 4, option: 'Mayonnaise', group: 'Condiments' }]
// output: { Breads: [{ id: 12, option: 'Steam', group: 'Breads' }], Condiments: [{ id: 4, option: 'Mayonnaise', group: 'Condiments' }]}
let groupingPropName = this.getMappingProperty('groupLabel');
let collectionGroupedAsObject = this.collection.reduce((groups, y) => {
let key = y[groupingPropName];
(groups[key] = groups[key] || []).push(y);
return groups;
}, {});
// then recreate an array with previously found subgroup
// output: [ [{ id: 12, option: 'Steam', group: 'Breads' }], [{ id: 4, option: 'Mayonnaise', group: 'Condiments' }] ]
return Object.keys(collectionGroupedAsObject).map(k => collectionGroupedAsObject[k]);
}
/**
* Get data structure mapping property
* @param {string} type
* @return {string} mappingPropertyName
*/
getMappingProperty(type) {
let dataMappingStructure = this.getMergedMappingStructure();
return dataMappingStructure[type];
}
/**
* From an input array, find the mapping property of the first element
* Example: getMappingPropertyValueFromGroup(option, 'groupLabel') => 'groupLabel' or the user custom group
* @param {array} input array
* @param {string} search property name
* @return {any} found item (stringo/object)
*/
getMappingPropertyValueFromGroup(inputArray, searchPropName) {
let propertyName = this.getMappingProperty(searchPropName);
return (Array.isArray(inputArray) && inputArray[0] && inputArray[0].hasOwnProperty(propertyName)) ? inputArray[0][propertyName] : '';
}
/**
* From an input array, find the mapping property value
* Example: getMappingPropertyValue(option, 'divider') => 'divider' or the user custom divider
* @param {array} input array
* @param {string} search property name
* @return {any} found item (stringo/object)
*/
getMappingPropertyValue(inputArray, searchPropName) {
let propertyName = this.getMappingProperty(searchPropName);
return inputArray.hasOwnProperty(propertyName) ? inputArray[propertyName] : '';
}
/**
* Get merged data structure mapping
* @return {array} mergedDataMappingStructure
*/
getMergedMappingStructure() {
let dataMappingStructure = Object.assign({}, globalExtraOptions.mappingDataStructure, this.dataMappingStructure || {});
return dataMappingStructure;
}
/**
* Find item(s) in the collection.
* @param {array} collection
* @param {any} newValue
* @param {string} objectKey
* @return {object} found selection, the output is an object with structure of selection = { indexes: any | any[], items: any | any[] };
*/
findItems(collection, newValue, objectKey) {
let searchingItems = [];
let selection = {
index: this.multiple ? [] : undefined,
item: this.multiple ? [] : undefined
};
if (newValue === null || newValue === undefined) {
return selection;
}
if (!Array.isArray(newValue)) {
searchingItems.push(newValue);
} else {
searchingItems = newValue;
}
for (let searchItem of searchingItems) {
let searchFilter = this.util.isObject(searchItem) ? searchItem[objectKey] : searchItem;
let foundItem = collection.find(item => {
// for comparison, we're using == mostly because indexes are passed as string because of html
const itemInput = this.util.isObject(item) ? item[objectKey] : item;
return !item.disabled && itemInput.toString() === searchFilter.toString();
});
if (foundItem) {
const foundItemIndex = this.util.isObject(foundItem) ? foundItem[objectKey] : foundItem;
if (this.multiple) {
selection.index.push(foundItemIndex);
selection.item.push(foundItem);
} else {
selection.index = foundItemIndex;
selection.item = foundItem;
break;
}
}
}
return selection;
}
/**
* From the selection object provided, we want to know if the selection is empty
* The structure is:: selection = { indexes: [], items: [] };
* @param {obejct} selection object
*/
isEmptySelection(selection) {
if (!selection) {
return true;
}
if (this.multiple) {
return selection.item.length === 0;
}
return selection.item ? false : true;
}
/**
* Select the item in the UI element from a selection object passed
* The structure is:: selection = { indexes: [], items: [] };
* @param {object} selection object
*/
renderSelection(selection) {
if (this.domElm) {
if (this.isEmptySelection(selection)) {
this.domElm.selectpicker('val', null);
} else if (!this.isEmptySelection(selection)) {
this.domElm.selectpicker('val', selection.index);
}
}
}
renderPreSelection() {
let newValue = this._originalSelectedIndexes || this._originalSelectedObjects;
let selection = this.findItems(this.collection, newValue, this.objectKey);
if (this.isEmptySelection(selection) && !this.util.parseBool(this.emptyOnNull)) {
this.selectedValue = selection.index = (this.util.isObject(this.collection[0]) ? this.collection[0][this.objectKey] : this.collection[0]);
this.selectedItem = selection.item = this.collection[0];
} else {
this.selectedValue = selection.index;
this.selectedItem = selection.item;
}
this.renderSelection(selection);
}
/**
* On selected item (ids) changed, we will update the selectedValue(s) unless user chose emptyOnNull on first pass.
* We will also render the selection with new value(s)
* @param {any} newValue
* @param {any} oldValue
*/
selectedItemChanged(newValue, oldValue) {
if (!this.util.isEqual(newValue, oldValue)) {
let value = (newValue !== null && newValue !== undefined) ? newValue : this._originalSelectedIndexes;
let selection = this.findItems(this.collection, value, this.objectKey);
// get selected indexes (ids), unless user chose to emptyOnNull on first pass
if (this.isEmptySelection(selection) && !this.util.parseBool(this.emptyOnNull) && !this.multiple) {
// value could be an object, if so we will use the objectKey (object.id by default)
this.selectedValue = (this.util.isObject(this.collection[0]) ? this.collection[0][this.objectKey] : this.collection[0]);
} else if (!this.isEmptySelection(selection)) {
this.selectedValue = selection.index;
}
this.renderSelection(selection);
}
}
/**
* On selected value (string/object) changed, we will update the selectedItem(s) unless user chose emptyOnNull on first pass.
* We will also render the selection with new value(s)
* @param {any} newValue
* @param {any} oldValue
*/
selectedValueChanged(newValue, oldValue) {
if (!this.util.isEqual(newValue, oldValue)) {
let value = (newValue !== null && newValue !== undefined) ? newValue : this._originalSelectedObjects;
let selection = this.findItems(this.collection, value, this.objectKey);
this.selectedItem = selection.item;
}
}
/**
* onLoaded trigger, we will also check if user pre-selected any option(s).
* Pre-selection of options can be done by index and/or by item object.
* If provided, we will make them part of the select index/item and render selection on UI as well
*/
watchOnLoadedToRenderPreSelection() {
this.domElm.on('loaded.bs.select', (e) => {
this.renderPreSelection();
});
}
/**
* onChanged trigger, we will update our index(es) and item object(s)
*/
watchOnChangedToUpdateValueAndItemObjects() {
this.domElm.on('changed.bs.select', (e, clickedIndex, newValue, oldValue) => {
if (clickedIndex !== undefined) {
const val = this.domElm.selectpicker('val');
let selection = this.findItems(this.collection, val, this.objectKey);
this.selectedValue = selection.index;
}
});
}
} // class > end
export class OptionalBindingBehavior {
bind(binding, scope, interceptor) {
binding.originalupdateTarget = binding.updateTarget;
binding.originalTargetProperty = binding.targetProperty;
binding.updateTarget = val => {
if (val === undefined || val === null || val === '') {
binding.targetProperty = null;
} else {
binding.targetProperty = binding.originalTargetProperty;
}
binding.originalupdateTarget(val);
};
}
unbind(binding, scope) {
binding.updateTarget = binding.originalupdateTarget;
binding.originalupdateTarget = null;
binding.targetProperty = binding.originalTargetProperty;
binding.originalTargetProperty = null;
}
}