import ngModule from '../../module'; import Component from '../../lib/component'; import {buildFilter} from 'vn-loopback/util/filter'; import focus from '../../lib/focus'; import './style.scss'; /** * An input specialized to perform searches, it allows to use a panel * for advanced searches when the panel property is defined. * When model and exprBuilder properties are used, the model is updated * automatically with an and-filter exprexion in which each operand is built * by calling the exprBuilder function for each non-null parameter. * * @property {Object} filter A key-value object with filter parameters * @property {SearchPanel} panel The panel used for advanced searches * @property {Function} onSearch Function to call when search is submited * @property {CrudModel} model The model used for searching * @property {Function} exprBuilder If defined, is used to build each non-null param expresion */ export default class Searchbar extends Component { constructor($element, $) { super($element, $); this.searchState = '.'; this.autoState = true; this.deregisterCallback = this.$transitions.onSuccess( {}, transition => this.onStateChange(transition)); } $postLink() { if (this.autoState) { if (!this.baseState) { let stateParts = this.$state.current.name.split('.'); this.baseState = stateParts[0]; } this.searchState = `${this.baseState}.index`; } this.fetchStateFilter(this.autoLoad); } $onDestroy() { this.deregisterCallback(); } get filter() { return this._filter; } set filter(value) { this._filter = value; this.toBar(value); } get shownFilter() { return this.filter != null ? this.filter : this.suggestedFilter; } get searchString() { return this._searchString; } set searchString(value) { this._searchString = value; if (value == null) this.params = []; } onStateChange(transition) { let ignoreHandler = !this.element.parentNode || transition == this.transition; if (ignoreHandler) return; this.fetchStateFilter(); } fetchStateFilter(autoLoad) { let filter = null; if (this.$state.is(this.searchState)) { if (this.$params.q) { try { filter = JSON.parse(this.$params.q); } catch (e) { console.error(e); } } if (!filter && autoLoad) filter = {}; } this.doSearch(filter, 'state'); } openPanel(event) { if (event.defaultPrevented) return; this.$.popover.show(this.element); this.$panelScope = this.$.$new(); this.panelEl = this.$compile(`<${this.panel}/>`)(this.$panelScope)[0]; let panel = this.panelEl.$ctrl; if (this.shownFilter) panel.filter = JSON.parse(JSON.stringify(this.shownFilter)); panel.onSubmit = filter => this.onPanelSubmit(filter.$filter); this.$.popover.content.appendChild(this.panelEl); } onPopoverClose() { this.$panelScope.$destroy(); this.$panelScope = null; this.panelEl.remove(); this.panelEl = null; } onPanelSubmit(filter) { this.$.popover.hide(); filter = compact(filter); filter = filter != null ? filter : {}; this.doSearch(filter, 'panel'); } onSubmit() { this.doSearch(this.fromBar(), 'bar'); } removeParam(index) { this.params.splice(index, 1); this.doSearch(this.fromBar(), 'bar'); } fromBar() { let filter = {}; if (this.searchString) filter.search = this.searchString; if (this.params) { for (let param of this.params) filter[param.key] = param.value; } return filter; } toBar(filter) { this.params = []; this.searchString = filter && filter.search; if (!filter) return; let keys = Object.keys(filter); keys.forEach(key => { if (key == 'search') return; let value = filter[key]; let chip; if (typeof value == 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) value = new Date(value); switch (typeof value) { case 'boolean': chip = `${value ? '' : 'not '}${key}`; break; case 'number': case 'string': chip = `${key}: ${value}`; break; default: if (value instanceof Date) { let format = 'yyyy-MM-dd'; if (value.getHours() || value.getMinutes()) format += ' HH:mm'; chip = `${key}: ${this.$filter('date')(value, format)}`; } else chip = key; } this.params.push({chip, key, value}); }); } doSearch(filter, source) { if (filter === this.filter) return; let promise = this.onSearch({$params: filter}); promise = promise || this.$q.resolve(); promise.then(data => this.onFilter(filter, source, data)); } onFilter(filter, source, data) { let state; let params; let opts; if (filter) { let isOneResult = this.autoState && source != 'state' && !angular.equals(filter, {}) && data && data.length == 1; if (isOneResult) { let baseDepth = this.baseState.split('.').length; let stateParts = this.$state.current.name .split('.') .slice(baseDepth); let subState = stateParts[0]; switch (subState) { case 'card': subState += `.${stateParts[1]}`; if (stateParts.length >= 3) subState += '.index'; break; default: subState = 'card.summary'; } if (this.stateParams) params = this.stateParams({$row: data[0]}); state = `${this.baseState}.${subState}`; filter = null; } else { state = this.searchState; if (filter) params = {q: JSON.stringify(filter)}; if (this.$state.is(state)) opts = {location: 'replace'}; } } this.filter = filter; if (!filter && this.model) this.model.clear(); if (source != 'state') this.transition = this.$state.go(state, params, opts).transition; if (source != 'bar') focus(this.element.querySelector('vn-textfield input')); } // Default search handlers stateParams(params) { return {id: params.$row.id}; } onSearch(args) { if (!this.model) return; let filter = args.$params; if (filter === null) { this.model.clear(); return; } let where = null; let params = null; if (this.exprBuilder) { where = buildFilter(filter, (param, value) => this.exprBuilder({param, value})); } else { params = Object.assign({}, filter); if (this.fetchParams) params = this.fetchParams({$params: params}); } return this.model.applyFilter(where ? {where} : null, params) .then(() => this.model.data); } } ngModule.vnComponent('vnSearchbar', { controller: Searchbar, template: require('./searchbar.html'), bindings: { filter: '<?', suggestedFilter: '<?', panel: '@', info: '@?', onSearch: '&?', baseState: '@?', autoState: '<?', stateParams: '&?', model: '<?', exprBuilder: '&?', fetchParams: '&?' } }); class AutoSearch { constructor(vnSlotService) { this.vnSlotService = vnSlotService; } $postLink() { let searchbar = this.vnSlotService.getContent('topbar'); if (searchbar && searchbar.$ctrl instanceof Searchbar) this.model = searchbar.$ctrl.model; } } AutoSearch.$inject = ['vnSlotService']; ngModule.vnComponent('vnAutoSearch', { controller: AutoSearch, bindings: { model: '=?' } }); /** * Removes null, undefined, empty objects and empty arrays from an object. * It also applies to nested objects/arrays. * * @param {*} obj The value to format * @return {*} The formatted value */ function compact(obj) { if (obj == null) return undefined; else if (Array.isArray(obj)) { for (let i = obj.length - 1; i >= 0; i--) { if (compact(obj[i]) === undefined) obj.splice(i, 1); } if (obj.length == 0) return undefined; } else if (typeof obj == 'object' && obj.constructor == Object) { let keys = Object.keys(obj); for (let key of keys) { if (key.charAt(0) == '$' || compact(obj[key]) === undefined) delete obj[key]; } if (Object.keys(obj).length == 0) return undefined; } return obj; }