salix/front/core/components/searchbar/searchbar.js

464 lines
13 KiB
JavaScript
Raw Permalink Normal View History

import ngModule from '../../module';
import Component from '../../lib/component';
2018-12-27 11:54:16 +00:00
import {buildFilter} from 'vn-loopback/util/filter';
2019-11-10 10:08:44 +00:00
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
2020-03-17 13:01:25 +00:00
* @property {Function} onSearch Function to call when search is submited
2020-03-07 12:52:02 +00:00
* @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
*/
2020-03-07 12:52:02 +00:00
export default class Searchbar extends Component {
2019-11-10 10:08:44 +00:00
constructor($element, $) {
super($element, $);
this.searchState = '.';
2020-06-12 08:53:33 +00:00
this.placeholder = 'Search';
2020-03-13 19:33:12 +00:00
this.autoState = true;
this.separateIndex = true;
this.entityState = 'card.summary';
2022-11-22 09:36:40 +00:00
this.isIndex = false;
2018-12-19 07:42:07 +00:00
2019-11-10 10:08:44 +00:00
this.deregisterCallback = this.$transitions.onSuccess(
2020-03-13 19:33:12 +00:00
{}, transition => this.onStateChange(transition));
}
2018-12-19 07:42:07 +00:00
$postLink() {
2020-03-13 19:33:12 +00:00
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;
2020-03-13 19:33:12 +00:00
let description = this.$state.get(this.baseState).description;
this.placeholder = this.$t('Search for', {
module: this.$t(description).toLowerCase()
2020-06-12 08:53:33 +00:00
});
2020-03-13 19:33:12 +00:00
}
2020-03-07 12:52:02 +00:00
2020-03-13 19:33:12 +00:00
this.fetchStateFilter(this.autoLoad);
2018-12-19 07:42:07 +00:00
}
2019-11-10 10:08:44 +00:00
$onDestroy() {
this.deregisterCallback();
2018-12-19 07:42:07 +00:00
}
2019-11-10 12:10:52 +00:00
get filter() {
return this._filter;
}
set filter(value) {
this._filter = value;
this.toBar(value);
}
2019-11-10 10:08:44 +00:00
get shownFilter() {
return this.filter != null
? this.filter
: this.suggestedFilter;
}
2019-11-10 12:10:52 +00:00
get searchString() {
return this._searchString;
}
set searchString(value) {
this._searchString = value;
if (value == null) this.params = [];
}
2020-03-13 19:33:12 +00:00
onStateChange(transition) {
2020-03-18 12:54:05 +00:00
let ignoreHandler =
!this.element.parentNode
|| transition == this.transition;
2020-03-13 19:33:12 +00:00
2020-03-18 12:54:05 +00:00
if (ignoreHandler) return;
2020-03-13 19:33:12 +00:00
this.fetchStateFilter();
}
fetchStateFilter(autoLoad) {
2020-06-02 12:42:58 +00:00
let filter = this.filter ? this.filter : null;
2019-11-10 12:10:52 +00:00
2020-03-16 17:08:44 +00:00
if (this.$state.is(this.searchState)) {
2019-11-10 12:10:52 +00:00
if (this.$params.q) {
2019-11-10 10:08:44 +00:00
try {
2019-11-10 12:10:52 +00:00
filter = JSON.parse(this.$params.q);
2019-11-10 10:08:44 +00:00
} catch (e) {
console.error(e);
}
2019-11-10 12:10:52 +00:00
}
2018-12-19 07:42:07 +00:00
2020-03-13 19:33:12 +00:00
if (!filter && autoLoad)
filter = {};
2020-03-16 17:08:44 +00:00
}
2020-03-07 12:52:02 +00:00
let stateParts = this.$state.current.name.split('.');
2022-11-22 09:36:40 +00:00
this.isIndex = stateParts[1] == 'index';
2022-11-22 09:36:40 +00:00
this.doSearch(filter, 'state');
2019-08-29 06:32:46 +00:00
}
openPanel(event) {
if (event.defaultPrevented) return;
this.$.popover.show(this.element);
2019-02-15 10:03:27 +00:00
this.$panelScope = this.$.$new();
this.panelEl = this.$compile(`<${this.panel}/>`)(this.$panelScope)[0];
let panel = this.panelEl.$ctrl;
2019-08-29 06:32:46 +00:00
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() {
2019-02-15 10:03:27 +00:00
this.$panelScope.$destroy();
this.$panelScope = null;
this.panelEl.remove();
this.panelEl = null;
}
onPanelSubmit(filter) {
this.$.popover.hide();
2019-01-16 14:29:01 +00:00
filter = compact(filter);
2019-11-10 10:08:44 +00:00
filter = filter != null ? filter : {};
2020-03-13 19:33:12 +00:00
this.doSearch(filter, 'panel');
}
onSubmit() {
2020-03-13 19:33:12 +00:00
this.doSearch(this.fromBar(), 'bar');
}
2019-11-10 10:08:44 +00:00
removeParam(index) {
2022-09-27 13:17:54 +00:00
const field = this.params[index].key;
this.filterSanitizer(field);
2019-11-10 10:08:44 +00:00
this.params.splice(index, 1);
2022-09-27 13:17:54 +00:00
this.toRemove = field;
this.doSearch(this.fromBar(), 'removeBar');
2019-11-10 10:08:44 +00:00
}
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 => {
2022-09-28 09:22:29 +00:00
if (key == 'search' || key == 'tableQ' || key == 'tableOrder') return;
2019-11-10 10:08:44 +00:00
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});
});
}
2018-12-19 07:42:07 +00:00
2020-03-16 17:08:44 +00:00
doSearch(filter, source) {
2022-11-22 09:36:40 +00:00
if (filter === this.filter && !this.isIndex) return;
let promise = this.onSearch({$params: filter}, source);
2020-03-16 17:08:44 +00:00
promise = promise || this.$q.resolve();
promise.then(data => this.onFilter(filter, source, data));
2022-09-27 13:17:54 +00:00
this.toBar(filter);
2020-03-13 19:33:12 +00:00
}
2020-03-16 17:08:44 +00:00
onFilter(filter, source, data) {
let state;
let params;
let opts;
2020-03-13 19:33:12 +00:00
2020-03-16 17:08:44 +00:00
if (filter) {
2020-03-18 12:54:05 +00:00
let isOneResult = this.autoState
2020-03-16 17:08:44 +00:00
&& source != 'state'
2020-03-16 17:54:22 +00:00
&& !angular.equals(filter, {})
2020-03-16 17:08:44 +00:00
&& data
2020-09-11 12:47:23 +00:00
&& data.length == 1;
2020-03-16 17:08:44 +00:00
2020-03-18 12:54:05 +00:00
if (isOneResult) {
2020-03-16 17:08:44 +00:00
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;
2020-03-16 17:08:44 +00:00
}
2020-03-13 19:33:12 +00:00
2020-03-16 17:08:44 +00:00
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;
2020-03-16 17:08:44 +00:00
params = {q: JSON.stringify(filter)};
}
2020-03-16 17:08:44 +00:00
if (this.$state.is(state))
opts = {location: 'replace'};
}
}
this.filter = filter;
2020-03-23 09:40:09 +00:00
if (!filter && this.model)
this.model.clear();
2020-03-16 17:08:44 +00:00
if (source != 'state')
this.transition = this.$state.go(state, params, opts).transition;
if (source != 'bar' && (source != 'state' || this.$state.is(this.baseState)))
2020-03-16 17:08:44 +00:00
focus(this.element.querySelector('vn-textfield input'));
2020-03-13 19:33:12 +00:00
}
2020-03-16 17:08:44 +00:00
// Default search handlers
stateParams(params) {
return {id: params.$row.id};
}
onSearch(args, source) {
2020-03-16 17:08:44 +00:00
if (!this.model) return;
let filter = args.$params;
2020-03-13 19:33:12 +00:00
if (filter === null) {
this.model.clear();
2020-03-16 17:08:44 +00:00
return;
}
2022-09-27 13:17:54 +00:00
if (Object.keys(filter).length === 0) {
this.filterSanitizer('search');
if (this.model.userParams)
delete this.model.userParams['search'];
}
2020-03-13 19:33:12 +00:00
let where = null;
let params = {};
2020-03-13 19:33:12 +00:00
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;
2022-10-14 05:40:11 +00:00
2022-12-21 14:01:05 +00:00
const hasParams = this.$params.q && JSON.parse(this.$params.q).tableQ;
2022-10-14 05:40:11 +00:00
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];
if (source == 'removeBar') {
delete params[this.toRemove];
delete this.model.userParams[this.toRemove];
delete stateFilter[this.toRemove];
}
2022-12-21 14:01:05 +00:00
const newParams = Object.assign(stateFilter, params);
return this.model.applyParams(newParams)
2022-09-28 09:22:29 +00:00
.then(() => this.model.data);
}
2022-09-28 09:22:29 +00:00
return this.model.applyFilter(where ? {where} : null, params)
2020-03-13 19:33:12 +00:00
.then(() => this.model.data);
}
2022-09-27 13:17:54 +00:00
filterSanitizer(field) {
if (!field) return;
2022-09-27 13:17:54 +00:00
const userFilter = this.model.userFilter;
const userParams = this.model.userParams;
const where = userFilter && userFilter.where;
2022-09-28 09:22:29 +00:00
if (this.model.userParams)
delete this.model.userParams[field];
2022-09-27 13:17:54 +00:00
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};
}
}
2020-03-07 12:52:02 +00:00
ngModule.vnComponent('vnSearchbar', {
controller: Searchbar,
template: require('./searchbar.html'),
bindings: {
2020-03-07 12:52:02 +00:00
filter: '<?',
suggestedFilter: '<?',
panel: '@',
info: '@?',
2019-11-10 10:08:44 +00:00
onSearch: '&?',
2020-03-13 19:33:12 +00:00
autoState: '<?',
baseState: '@?',
entityState: '@?',
separateIndex: '<?',
2020-03-16 17:08:44 +00:00
stateParams: '&?',
model: '<?',
exprBuilder: '&?',
2020-06-12 08:53:33 +00:00
fetchParams: '&?',
placeholder: '@?'
2020-03-07 12:52:02 +00:00
}
});
class AutoSearch {
constructor(vnSlotService) {
2020-03-13 19:33:12 +00:00
this.vnSlotService = vnSlotService;
}
$postLink() {
let searchbar = this.vnSlotService.getContent('topbar');
if (searchbar && searchbar.$ctrl instanceof Searchbar)
2020-03-07 12:52:02 +00:00
this.model = searchbar.$ctrl.model;
}
}
AutoSearch.$inject = ['vnSlotService'];
ngModule.vnComponent('vnAutoSearch', {
controller: AutoSearch,
bindings: {
model: '=?'
2019-11-10 10:08:44 +00:00
}
});
2019-01-16 14:29:01 +00:00
/**
* 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) {
2019-11-10 10:08:44 +00:00
if (key.charAt(0) == '$' || compact(obj[key]) === undefined)
2019-01-16 14:29:01 +00:00
delete obj[key];
}
if (Object.keys(obj).length == 0)
return undefined;
}
return obj;
}