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 * @property {String} baseState The base state for searchs */ export default class Searchbar extends Component { constructor($element, $) { super($element, $); this.searchState = '.'; this.placeholder = 'Search'; this.autoState = true; this.separateIndex = true; this.entityState = 'card.summary'; this.isIndex = false; 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`; } else this.searchState = this.baseState; let description = this.$state.get(this.baseState).description; this.placeholder = this.$t('Search for', { module: this.$t(description).toLowerCase() }); } 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 = this.filter ? this.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 = {}; } let stateParts = this.$state.current.name.split('.'); this.isIndex = stateParts[1] == 'index'; 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) { const field = this.params[index].key; this.filterSanitizer(field); this.params.splice(index, 1); this.toRemove = field; this.doSearch(this.fromBar(), 'removeBar'); } 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' || key == 'tableQ' || key == 'tableOrder') 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 && !this.isIndex) return; let promise = this.onSearch({$params: filter}); promise = promise || this.$q.resolve(); promise.then(data => this.onFilter(filter, source, data)); this.toBar(filter); } 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 = this.entityState; } if (this.stateParams) params = this.stateParams({$row: data[0]}); state = `${this.baseState}.${subState}`; filter = null; } else { state = this.searchState; if (filter) { if (this.tableQ) filter.tableQ = this.tableQ; params = {q: JSON.stringify(filter)}; } if (this.$state.is(state)) opts = {location: 'replace'}; } } this.filter = filter; if (source == 'removeBar') { delete params[this.toRemove]; delete this.model.userParams[this.toRemove]; this.model.refresh(); } if (!filter && this.model) this.model.clear(); if (source != 'state') this.transition = this.$state.go(state, params, opts).transition; if (source != 'bar' && (source != 'state' || this.$state.is(this.baseState))) 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; } if (Object.keys(filter).length === 0) { this.filterSanitizer('search'); if (this.model.userParams) delete this.model.userParams['search']; } let where = null; let params = {}; 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}); } this.tableQ = null; const hasParams = this.$params.q && JSON.parse(this.$params.q).tableQ; if (hasParams) { const stateFilter = JSON.parse(this.$params.q); for (let param in stateFilter) { if (param != 'tableQ' && param != 'orderQ') this.filterSanitizer(param); } for (let param in this.suggestedFilter) { this.filterSanitizer(param); delete stateFilter[param]; } this.tableQ = stateFilter.tableQ; for (let param in stateFilter.tableQ) params[param] = stateFilter.tableQ[param]; const newParams = Object.assign(stateFilter, params); return this.model.applyParams(newParams) .then(() => this.model.data); } return this.model.applyFilter(where ? {where} : null, params) .then(() => this.model.data); } filterSanitizer(field) { if (!field) return; const userFilter = this.model.userFilter; const userParams = this.model.userParams; const where = userFilter && userFilter.where; if (this.model.userParams) delete this.model.userParams[field]; if (this.exprBuilder) { const param = this.exprBuilder({ param: field, value: null }); if (param) [field] = Object.keys(param); } if (!where) return; const whereKeys = Object.keys(where); for (let key of whereKeys) { removeProp(where, field, key); if (Object.keys(where).length == 0) delete userFilter.where; } function removeProp(obj, targetProp, prop) { if (prop == targetProp) delete obj[prop]; if (prop === 'and' || prop === 'or' && obj[prop]) { const arrayCopy = obj[prop].slice(); for (let param of arrayCopy) { const [key] = Object.keys(param); const index = obj[prop].findIndex(param => { return Object.keys(param)[0] == key; }); if (key == targetProp) obj[prop].splice(index, 1); if (param[key] instanceof Array) removeProp(param, field, key); if (Object.keys(param).length == 0) obj[prop].splice(index, 1); } if (obj[prop].length == 0) delete obj[prop]; } } return {userFilter, userParams}; } } ngModule.vnComponent('vnSearchbar', { controller: Searchbar, template: require('./searchbar.html'), bindings: { filter: '= 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; }