UNPKG

novo-elements

Version:

Bullhorn's NOVO Element Repository for Angular 2

619 lines (568 loc) 21.5 kB
// NG2 import { Component, OnInit, EventEmitter, forwardRef, ElementRef, Input, Output } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; // APP import { OutsideClick } from '../../utils/outside-click/OutsideClick'; import { KeyCodes } from '../../utils/key-codes/KeyCodes'; import { Helpers } from '../../utils/Helpers'; import { NovoLabelService } from '../../services/novo-label-service'; // Vendor import { ReplaySubject } from 'rxjs/ReplaySubject'; // Value accessor for the component (supports ngModel) const CHIPS_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NovoMultiPickerElement), multi: true }; @Component({ selector: 'multi-picker', providers: [CHIPS_VALUE_ACCESSOR], template: ` <chip *ngFor="let item of _items | async | slice:0:chipsCount" [type]="type" [class.selected]="item == selected" (remove)="removeFromDisplay($event, item)" (select)="select($event, item)"> {{ item.label }} </chip> <div *ngIf="items.length > chipsCount"> <ul class="summary"> <li *ngFor="let type of notShown">+ {{type.count}} {{ labels.more }} {{type.type}}</li> </ul> </div> <div class="chip-input-container"> <novo-picker clearValueOnSelect="true" [config]="source" [placeholder]="placeholder" (select)="clickOption($event)" (keydown)="onKeyDown($event)" (focus)="onFocus($event)" (blur)="onTouched($event)" [overrideElement]="element"> </novo-picker> </div> <i class="bhi-search" [class.has-value]="items.length"></i> <label class="clear-all" *ngIf="items.length" (click)="clearValue()">{{ labels.clearAll }} <i class="bhi-times"></i></label> `, host: { '[class.with-value]': 'items.length > 0' } }) export class NovoMultiPickerElement implements OnInit { @Input() source: any; @Input() placeholder: any = ''; @Input() types: any; @Output() changed: EventEmitter<any> = new EventEmitter(); @Output() focus: EventEmitter<any> = new EventEmitter(); @Output() blur: EventEmitter<any> = new EventEmitter(); get value() { return this._value; } @Input() set value(selectedItems) { if (selectedItems) { this.types.forEach(x => this._value[x.value] = selectedItems[x.value]); } else { this._value = {}; this.types.forEach(x => this._value[x.value] = []); } this.changed.emit(selectedItems); this.onModelChange(selectedItems); } items: any = []; _items = new ReplaySubject(1); options: any; _options: any; selected: any = null; config: any = {}; chipsCount: number; selectAllOption: boolean; strictRelationship: boolean; // private data model _value: any = {}; notShown: any = {}; // Placeholders for the callbacks model: any; onModelChange: Function = () => { }; onModelTouched: Function = () => { }; constructor(public element: ElementRef, public labels: NovoLabelService) { this.chipsCount = 4; } ngOnInit() { this.selectAllOption = this.source.selectAllOption || false; this.chipsCount = this.source.chipsCount || 4; this.strictRelationship = this.source.strictRelationship || false; this.setupOptions(); } clearValue() { this.types.forEach(type => this.modifyAllOfType(type.value, 'unselect')); this.items = []; this._items.next(this.items); this.value = this.setInitialValue(null); this.onModelChange(this.value); } removeFromDisplay(event, item) { this.remove(true, item); this.modifyAffectedParentsOrChildren(false, item); } setupOptions() { this.options = this.source.options || []; this._options = []; if (this.options) { this.options.forEach(option => { let formattedOption = this.setupOptionsByType(option); this._options.push(formattedOption); }); } this.source.options = this._options; } setupOptionsByType(section) { let formattedSection: any = { type: section.type, label: section.label || section.type }; formattedSection.data = section.data.map(item => { return this.formatOption(section, item); }); if (this.selectAllOption) { let selectAll = this.createSelectAllOption(section); formattedSection.data.splice(0, 0, selectAll); } formattedSection.originalData = formattedSection.data.slice(); return formattedSection; } formatOption(section, item) { let obj = { value: section.field ? item[section.field] : (item.value || item), label: section.format ? Helpers.interpolate(section.format, item) : item.label || String(item.value || item), type: section.type, checked: undefined, isParentOf: section.isParentOf, isChildOf: section.isChildOf }; if (obj.isChildOf) { obj[section.isChildOf] = item[section.isChildOf]; } return obj; } createSelectAllOption(section) { let selectAll = { value: 'ALL', label: `All ${section.type}`, type: section.type, checked: (this.model && this.model.length && (this.model.indexOf('ALL') !== -1)), isParentOf: section.isParentOf, isChildOf: section.isChildOf }; if (section.isChildOf) { let allParents = section.data.reduce((accum, next) => { return accum.concat(next[section.isChildOf]); }, []); selectAll[section.isChildOf] = allParents; } return selectAll; } deselectAll() { this.selected = null; } select(event, item) { this.blur.emit(event); this.deselectAll(); this.selected = item; } onFocus(e) { this.element.nativeElement.classList.add('selected'); this.focus.emit(e); } clickOption(event) { if (event && !(event instanceof Event)) { if (event.checked === false) { this.remove(null, event); } else { this.add(event); } this.modifyAffectedParentsOrChildren(event.checked, event); // Set focus on the picker let input = this.element.nativeElement.querySelector('novo-picker > input'); if (input) { input.focus(); } } } add(event) { if (event.value === 'ALL') { this.modifyAllOfType(event.type, 'select'); } else { this.updateDisplayItems(event, 'add'); this.value[event.type].push(event.value); this.updateAllItemState(event.type); this.triggerValueUpdate(); } this.updateParentOrChildren(event, 'select'); this.select(null, event); } updateAllItemState(type) { let allOfType = this.getAllOfType(type); let allOfTypeSelected = this.allItemsSelected(allOfType, type); if (allOfTypeSelected) { this.selectAll(allOfType, type); } return { allOfType, allOfTypeSelected }; } setIndeterminateState(allOfType, status) { if (!this.selectAllOption) { return; } let allItem = allOfType[0]; allItem.indeterminate = status; } updateDisplayItems(item, action) { let adding = action === 'add'; if (adding) { this.items.push(item); } else { if (this.items.indexOf(item) > -1) { this.items.splice(this.items.indexOf(item), 1); } } this.updateDisplayText(this.items); this._items.next(this.items); } updateDisplayText(items) { this.notShown = []; let notShown = items.slice(this.chipsCount); if (notShown.length > 0) { this.types.forEach(type => { let count; let selectedOfType = notShown.filter(x => x.type === type.value); if (selectedOfType.length === 1 && selectedOfType[0].value === 'ALL') { count = this.getAllOfType(type.value).length - 1; } else { count = selectedOfType.length; } let displayType = count === 1 ? type.singular : type.plural || type.value; if (count > 0) { this.notShown.push({ type: displayType, count: count }); } }); } } remove(event, item) { let triggeredByEvent; if (event) { triggeredByEvent = true; } let itemToRemove = item; if (itemToRemove.value === 'ALL') { triggeredByEvent = false; this.modifyAllOfType(itemToRemove.type, 'unselect'); } else if (this.allOfTypeSelected(itemToRemove.type)) { this.handleRemoveItemIfAllSelected(itemToRemove); } this.removeItem(item, triggeredByEvent); } removeItem(item, triggeredByEvent?: any) { item.checked = false; this.deselectAll(); this.removeValue(item); if (item.value !== 'ALL') { this.updateParentOrChildren(item, 'unselect'); } if (triggeredByEvent) { this.modifyAffectedParentsOrChildren(false, item); } } removeValue(item) { let updatedValues = this.value[item.type].filter(x => x !== item.value); this.value[item.type] = updatedValues; this.triggerValueUpdate(); this.updateDisplayItems(item, 'remove'); } onKeyDown(event) { if (event.keyCode === KeyCodes.BACKSPACE) { if (event.target && event.target.value.length === 0 && this.items.length) { if (event) { event.stopPropagation(); event.preventDefault(); } if (this.selected) { this.remove(null, this.selected); } else { this.select(event, this.items[this.items.length - 1]); } } } } allOfTypeSelected(type) { return this.items.filter(x => x.type === type && x.value === 'ALL').length > 0; } modifyAllOfType(type, action) { let selecting = action === 'select'; let allOfType = this.getAllOfType(type); allOfType.forEach(item => { item.checked = selecting; item.indeterminate = false; }); if (selecting) { this.selectAll(allOfType, type); } else { this.items = [...this.items.filter(x => x.type !== type)]; this._items.next(this.items); this.value[type] = []; } if (this.selectAllOption) { this.updateAllParentsOrChildren(allOfType[0], action); } this.triggerValueUpdate(); } triggerValueUpdate() { let updatedObject = {}; this.types.forEach(x => updatedObject[x.value] = this.value[x.value]); this.value = updatedObject; } selectAll(allOfType, type) { if (!this.selectAllOption) { return; } allOfType[0].checked = true; let values = allOfType.map(i => { return i.value; }); //remove 'ALL' value values.splice(0, 1); this.value[type] = values; let updatedItems = this.items.filter(x => x.type !== type); this.items = updatedItems; this.updateDisplayItems(allOfType[0], 'add'); } handleRemoveItemIfAllSelected(item) { if (!this.selectAllOption) { return; } let type = item.type; let allOfType = this.getAllOfType(type); let allItem = allOfType[0]; this.removeItem(allItem); allItem.indeterminate = true; let selectedItems = allOfType.filter(i => i.checked === true); this.items = [...this.items, ...selectedItems]; let values = selectedItems.map(i => { return i.value; }); this.value[type] = [...values]; } handleOutsideClick(event) { // If the elements doesn't contain the target element, it is an outside click if (!this.element.nativeElement.contains(event.target)) { this.blur.emit(event); this.deselectAll(); } } getAllOfType(type) { return this._options.filter(x => x.type === type)[0].originalData; } updateParentOrChildren(item, action) { if (this.strictRelationship && item.isParentOf) { this.updateChildrenValue(item, action); } else if (item.isChildOf && this.selectAllOption) { this.updateParentValue(item, action); } } modifyAffectedParentsOrChildren(selecting, itemChanged) { if (!itemChanged.isChildOf && !itemChanged.isParentOf) { return; } let parent = this.types.filter(x => !!x.isParentOf)[0]; let parentType = parent.value; let allParentType = this.getAllOfType(parentType); let childType = allParentType[0].isParentOf; let allChildren = this.getAllOfType(childType); let allCheckedChildren = allChildren.filter(x => !!x.checked); allParentType.forEach(obj => { if (obj.value === 'ALL') { return; } let selectedChildrenOfParent = allCheckedChildren.filter(x => { return x[parentType].filter(y => y === obj.value).length > 0; }); if (selecting) { if (obj.checked) { return; } obj.indeterminate = selectedChildrenOfParent.length > 0; } else { let allChildrenOfParent = allChildren.filter(x => { return x.value !== 'ALL' && x[parentType].filter(y => y === obj.value).length > 0; }); if (selectedChildrenOfParent.length > 0) { if (obj.checked) { if (this.strictRelationship && (allChildrenOfParent.length !== selectedChildrenOfParent.length)) { obj.indeterminate = true; obj.checked = false; this.removeValue(obj); this.addIndividualChildren(selectedChildrenOfParent); } } else { obj.indeterminate = true; } if (this.strictRelationship && itemChanged.type !== parentType) { if (obj.checked) { obj.checked = false; this.removeValue(obj); this.addIndividualChildren(selectedChildrenOfParent); } } } else { obj.indeterminate = false; if (allChildrenOfParent.length === 0) { //if it has no children and is checked, it should stay checked return; } else if (this.strictRelationship && itemChanged.type !== parentType) { this.remove(null, obj); } } } }); if (this.selectAllOption) { this.updateIndeterminateStates(allParentType, allChildren, allCheckedChildren); } } updateAllParentsOrChildren(allItem, action) { if (allItem.isParentOf) { this.updateAllChildrenValue(allItem, action); } else if (allItem.isChildOf) { this.updateAllParentValue(allItem, action); } } updateAllChildrenValue(item, action) { let selecting = action === 'select'; let childType = item.isParentOf; let potentialChildren = this.getAllOfType(childType); if (this.selectAllOption && this.allOfTypeSelected(childType) && !selecting) { this.remove(null, potentialChildren[0]); return; } potentialChildren.forEach(x => { if (x.value === 'ALL' && !x.checked) { if (selecting) { x.checked = true; } x.indeterminate = selecting; } else { if (x.checked && !selecting) { this.remove(null, x); } x.checked = selecting; } }); } updateAllParentValue(item, action) { let selecting = action === 'select'; let parentType = item.isChildOf; let potentialParents = this.getAllOfType(parentType); potentialParents.forEach(x => { if (!x.checked) { x.indeterminate = selecting; } }); } updateIndeterminateStates(allParentType, allChildren, allCheckedChildren) { let allCheckedOrIndeterminateParents = allParentType.filter(x => (!!x.checked || !!x.indeterminate) && x.value !== 'ALL'); let isParentIndeterminate = !!allParentType[0].checked ? false : allCheckedOrIndeterminateParents.length > 0; let isChildIndeterminate = !!allChildren[0].checked ? false : allCheckedChildren.length > 0; this.setIndeterminateState(allParentType, isParentIndeterminate); this.setIndeterminateState(allChildren, isChildIndeterminate); } updateChildrenValue(parent, action) { let selecting = action === 'select'; let childType = parent.isParentOf; let potentialChildren = this.getAllOfType(childType); potentialChildren.forEach(x => { if (x.value === 'ALL') { return; } if (x[parent.type].filter(y => y === parent.value).length > 0) { if (x.checked && !selecting) { x.checked = false; if (this.allOfTypeSelected(childType)) { this.handleRemoveItemIfAllSelected(x); } else { this.removeValue(x); } } x.checked = selecting; } }); } updateParentValue(child, action) { let allParentType = this.getAllOfType(child.isChildOf); if (allParentType[0].checked && action !== 'select') { this.handleRemoveItemIfAllSelected(allParentType[0]); } } addIndividualChildren(children) { let parentAlreadySelected = false; children.forEach(x => { if (x.isChildOf) { //only add children if their parents are not already selected x[x.isChildOf].forEach(parent => { if (this.value[x.isChildOf].filter(p => p === parent).length > 0) { parentAlreadySelected = true; } }); } if (this.value[x.type].filter(item => item === x.value).length === 0 && !parentAlreadySelected) { this.add(x); } }); } setInitialValue(model) { this.items = []; this.value = model || {}; if (!this.types) { return; } this.types.forEach(typeObj => { let type = typeObj.value; if (this.value[type]) { let indeterminateIsSet = false; let options = this.updateAllItemState(type); let optionsByType = options.allOfType; let allSelected = options.allOfTypeSelected; this.value[type].forEach(item => { if (!allSelected && !indeterminateIsSet) { indeterminateIsSet = true; this.setIndeterminateState(optionsByType, true); } let value = optionsByType.filter(x => x.value === item)[0]; value.checked = true; if (!allSelected) { this.updateDisplayItems(value, 'add'); } if (this.strictRelationship && value.isParentOf) { this.updateChildrenValue(value, 'select'); } }); if (typeObj.isChildOf) { this.modifyAffectedParentsOrChildren(true, { value: type, isChildOf: true }); } } else { this.value[type] = []; } }); } allItemsSelected(optionsByType, type) { return this.value[type].length === optionsByType.length - 1; } // Set touched on blur onTouched(e) { this.element.nativeElement.classList.remove('selected'); this.onModelTouched(); this.blur.emit(e); } writeValue(model: any): void { this.model = model; this.setInitialValue(model); } registerOnChange(fn: Function): void { this.onModelChange = fn; } registerOnTouched(fn: Function): void { this.onModelTouched = fn; } }