filter.component.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. /*
  2. @author valentin.carruesco
  3. * data-hide-filters: Si renseigné, masque la recherche,
  4. * data-default: permet de renseigner des filtres par défaut à la recherche (au format attendu -> voir fonction php filer_set()),
  5. * data-label: Libellé affiché dans la partie gauche de la recherche simple,
  6. * data-slug : Si spécifié, la recherche devient enregistrable pour une réutilisation ultérieure,
  7. * data-only-advanced : Si l'attribut est présent, cache la recherche simple et ouvre par defaut la recherche avancée
  8. * data-autosearch (default: true) : si définit a false, ne lancera pas la fonction data-function automatiquement en fin de chargement du composant
  9. nb : La touche shift enfoncée lors de l'indentation d'un filtre forcera la création d'un nouveau groupe au lieu de chercher une fusion de groupe.
  10. nb 2: ce composant fonctionne avec les templates de type de filtres situés dans la page footer, exemple de type de filtre
  11. <div class="filter-value-block" data-value-type="dictionnary" data-value-selector=".filter-value:last-child">
  12. <select class="form-control filter-operator border-0 text-primary">
  13. <option value="=">Égal</option>
  14. <option value="!=">Différent</option>
  15. <option value="IS NULL" data-values="0">Non Renseigné</option>
  16. <option value="IS NOT NULL" data-values="0">Renseigné</option>
  17. </select>
  18. <select data-template="dictionnary" data-slug="{{slug}}" data-depth="{{depth}}" data-filter-type-value="{{filterTypeValue}}" class="form-control filter-value" data-disable-label></select>
  19. </div>
  20. data-value-type="" : permet de définir le data-type si la donnée est un composant
  21. data-value-selector : selecteur permetant de choisir le(s) iputs représeant la valeur réele du filtre (le selecteur est scopé sur la ligne du filtre)
  22. data-values="0" : sur l'operateur, cela permet de multiplier ou anihiler le champs de valeurs (0: supression, 1(default) un champs de valeur , n : n champs d valeur )
  23. */
  24. var FilterBox = function(input,options){
  25. var input = $(input);
  26. var object = input.data('component-object');
  27. this.input = input;
  28. //Premiere création du composant
  29. if(object == null){
  30. object = this;
  31. object.shift = false;
  32. //permet le forcing de création d'un nouveau groupe lors de l'appuis sur maj
  33. $(document).keydown(function(e) {
  34. if((e.which | e.keyCode) != 16) return;
  35. object.shift = true;
  36. }).keyup(function(e) {
  37. if((e.which | e.keyCode) != 16) return;
  38. object.shift = false;
  39. });
  40. object.data = options;
  41. object.slug = input.attr('data-slug');
  42. if(object.slug != null) {
  43. $('.btn-search-save',object.box).removeClass('hidden');
  44. $('.dropdown-divider',object.box).removeClass('hidden');
  45. $('.btn-search-reset',object.box).removeClass('hidden');
  46. }
  47. input.addClass('hidden');
  48. object.box = $('.advanced-search-box.hidden').clone().removeClass('hidden');
  49. input.after(object.box);
  50. object.columns = [];
  51. object.tpl = $('.criterias .condition.hidden',object.box).get(0).outerHTML;
  52. input.data('component-object',object);
  53. object.columns.push({label : ' - Choix filtre - ',value:''});
  54. $('option',input).each(function(i,element){
  55. object.columns.push({
  56. label : $(element).text(),
  57. type : $(element).attr('data-filter-type'),
  58. value : $(element).val()
  59. });
  60. });
  61. if(object.columns.length<=1){
  62. $('.options', object.box).addClass('hidden');
  63. $('.simple-search .input-group-append').removeClass('hidden');
  64. }
  65. object.box.on('change','.filter-column',function(){
  66. var condition = $(this).closest('.condition');
  67. object.filter_refresh(condition,true);
  68. }).on('change','.filter-operator',function(){
  69. var condition = $(this).closest('.condition');
  70. object.filter_refresh(condition);
  71. }).on('click','.filter-option .btn-add',function(){
  72. var button = $(this);
  73. var condition = button.closest('.condition');
  74. object.filter_add(condition);
  75. }).on('keyup click','.filter-keyword',function(){
  76. // RESET KEYWORD
  77. var posx = (this.offsetWidth + this.offsetLeft - 20);
  78. var props = $(this).val()!='' ? {'left':posx,'opacity':'1'} : {'left':posx+30,'opacity':'0'}
  79. $('#search-clear').css(props)
  80. }).on('mouseover','li.condition',function(event){
  81. event.stopPropagation();
  82. event.preventDefault();
  83. $(this).addClass('hover');
  84. }).on('mouseout','li.condition',function(event){
  85. event.stopPropagation();
  86. event.preventDefault();
  87. $(this).removeClass('hover');
  88. //Indentation d'un groupe
  89. }).on('click',' li.condition .btn-indent',function(){
  90. var line = $(this).closest('li');
  91. //si un groupe existe AVANT la condition, on déplace celle ci en FIN de ce groupe
  92. if(!object.shift && line.prev('.condition').find('>ul.group').length!=0){
  93. group = line.prev('.condition').find('>ul.group')
  94. group.append(line.detach());
  95. //si un groupe existe APRES la condition, on déplace celle ci en DEBUT de ce groupe
  96. }else if(!object.shift && line.next('.condition').find('>ul.group').length!=0){
  97. group = line.next('.condition').find('>ul.group');
  98. group.prepend(line.detach());
  99. //si aucun groupe a proximité, on créé un nouveau groupe
  100. }else{
  101. //tweak js car jquery ne capte pas bien les selected sur les clones
  102. line.find(':selected').attr('selected','selected');
  103. var newline = line.clone();
  104. var newGroup = $('<ul class="group"></ul>');
  105. newline.prepend(newGroup);
  106. newline.find('.filter-column,.filter-operator,.filter-value').remove();
  107. line.after(newline);
  108. newGroup.append(line.detach());
  109. group = newGroup;
  110. }
  111. $(group).sortable({
  112. axis : 'y',
  113. handle: ".btn-move",
  114. });
  115. }).on('click',' li.condition .btn-unindent',function(){
  116. var line = $(this).closest('li.condition');
  117. var parent = line.closest('ul.group');
  118. if(parent.hasClass('criterias')) return;
  119. //en fonction de la position de l'item dans son group on définit si on le déplace apres ou avant le groupe d'ou on le sort.
  120. var middleGroup = Math.trunc($("li.condition",parent).length / 2);
  121. var index = $("li.condition",parent).index(line) +1 ;
  122. var parentLine = parent.parent();
  123. var current = line.detach();
  124. if(index <= middleGroup){
  125. parentLine.before(current);
  126. }else{
  127. parentLine.after(current);
  128. }
  129. //Supression auto du groupe si plus aucun condition à l'interieur
  130. if(parent.find('.condition').length==0) parent.parent().remove();
  131. }).on('click',' li .btn-delete',function(){
  132. var line = $(this).closest('li.condition');
  133. object.filter_remove(line);
  134. }).on('click','.btn-search',function(){
  135. object.search();
  136. }).on('keypress','.filter-keyword, .filter-value input',function(e){
  137. if((e.wich | e.keyCode) != 13) return;
  138. object.search();
  139. }).on('click','.btn-search-save',function(){
  140. object.filter_save();
  141. }).on('click','.btn-search-reset',function(){
  142. if(!confirm('Êtes-vous sûr de supprimer tous vos filtres définitivement ?')) return;
  143. $.urlParam('filters','');
  144. $('.condition:visible',object.box).each(function(i,element){
  145. object.filter_remove($(element));
  146. });
  147. object.filter_save();
  148. $('.condition',object.box).removeClass('error');
  149. }).on('click','.btn-search-clean',function(){
  150. $.urlParam('filters','');
  151. $('.condition:visible',object.box).each(function(i,element){
  152. object.filter_remove($(element));
  153. });
  154. $('.condition',object.box).removeClass('error');
  155. });
  156. //ajout de la premiere condition
  157. object.filter_add();
  158. $('.advanced-button-search',object.box).click(function(){
  159. object.box.toggleClass('advanced');
  160. });
  161. //Preset des filtres depuis l'url si défini
  162. var filters = $.urlParam('filters');
  163. if(filters && filters!=''){
  164. filters = JSON.parse(atob(filters));
  165. } else if (input.attr('data-default') && input.attr('data-default').length){
  166. //Preset des filtres depuis l'attribut data-default
  167. filters = JSON.parse(input.attr('data-default'));
  168. //Récupération du type de filtre pour la colonne ciblée
  169. $.each(filters.a, function(i, filter){
  170. if(filter.c==null || !filter.c.length || (filter.t!=null && filter.t.length)) return;
  171. var option = $('option[value="'+filter.c+'"]', object.input);
  172. if(!option.length){
  173. delete filters.a[i];
  174. return;
  175. }
  176. filters.a[i]['t'] = option.attr('data-filter-type');
  177. filters.a[i]['o'] = filter.o.toLowerCase();
  178. });
  179. }
  180. if(filters && filters!='') {
  181. filters.keyword = filters.k;
  182. filters.advanced = filters.a;
  183. delete filters.a;
  184. delete filters.k;
  185. filters.advanced = object.filter_rename_keys(filters.advanced,{j:'join',o:'operator',v:'value',t:'type',c:'column',g:'group'});
  186. if(filters.keyword && filters.keyword!='') $('.filter-keyword',object.box).val(filters.keyword);
  187. if(filters.advanced.length>0){
  188. $(object.box).addClass('advanced');
  189. object.filters(filters.advanced);
  190. object.box.find('.condition:visible:eq(0)').remove();
  191. }
  192. object.load_callback();
  193. }else{
  194. if(object.slug!=null && object.slug!=''){
  195. $.action({
  196. action : 'filter_load',
  197. slug : object.slug,
  198. },function(response){
  199. if(!response.filters.advanced || response.filters.advanced.length == 0){
  200. object.load_callback();
  201. return;
  202. }
  203. var filters = response.filters.advanced;
  204. object.box.addClass('advanced');
  205. object.filter_recursive_set(null,filters);
  206. object.box.find('.condition:visible:eq(0)').remove();
  207. object.search();
  208. });
  209. }else{
  210. object.load_callback();
  211. }
  212. }
  213. $('#search-clear',object.box).click(function(){
  214. $(".filter-keyword",object.box).val('').focus();
  215. $(this).attr('style', '');
  216. object.load_callback();
  217. });
  218. }else{
  219. //Rafraichissement et/ou récuperation du composant
  220. //on reload les données de l'input aux cas ou elles aient changées
  221. object.data = input.data();
  222. }
  223. if(object.data && object.data.onlyAdvanced!=null){
  224. $('.simple-search,.advanced-button-search',object.box).addClass('hidden');
  225. object.box.addClass('advanced');
  226. }
  227. if(object.data && object.data.label != null && object.data.label.length) $('.simple-search .data-search-label',object.box).html(object.data.label);
  228. if(object.data && object.data.hideFilters != null) setTimeout(function(){object.box.addClass('hidden');}, 0);
  229. }
  230. FilterBox.prototype.load_callback = function(){
  231. if(this.data && this.data.function && (!this.data.hasOwnProperty('autosearch') || this.data.autosearch==true))
  232. window[this.data.function]();
  233. };
  234. //Enregistrement des filtres
  235. FilterBox.prototype.filter_save = function(){
  236. var object = this;
  237. var data = object.filters();
  238. $.action({
  239. action : 'filter_save',
  240. slug : object.slug,
  241. filters : data
  242. },function(response){
  243. if(response.message != null)
  244. $.message('info',response.message);
  245. });
  246. };
  247. //Rafraichissement des données et de la structure d'un filtre
  248. FilterBox.prototype.filter_refresh = function(condition,refreshOperator,data){
  249. var object = this;
  250. var column = $('.filter-column select',condition);
  251. var optionColumn = $('option:selected',column);
  252. if(data && data.column)
  253. column.val(data.column);
  254. var type = optionColumn.attr('data-filter-type');
  255. var dataAttributes = object.input.find('option[value="'+column.val()+'"]').data();
  256. var operator = $('>.filter-operator',condition);
  257. var operatorSelect = $('select',operator);
  258. if(column.val()==""){
  259. $('.filter-operator,.filter-value',condition).html('');
  260. return;
  261. }
  262. if(!type) return;
  263. if(operator.length==0 || refreshOperator){
  264. var operatorSelect = $($('.filter-value-block[data-value-type="'+type+'"] > .filter-operator').get(0).outerHTML);
  265. //On normalise en lowercase les opérateurs
  266. $('option', operatorSelect).val(function(){
  267. return this.value.toLowerCase();
  268. });
  269. condition.find('> .filter-operator').html(operatorSelect);
  270. }
  271. if(data && data.operator) operatorSelect.val(data.operator);
  272. if(data && data.join) $('>.filter-join',condition).val(data.join);
  273. condition.find('.filter-value').html('');
  274. var value = $('.filter-value-block[data-value-type="'+type+'"] .filter-value').get(0).outerHTML;
  275. value = value.replace('data-template','data-type');
  276. var repeat = $('option:selected',operator).attr('data-values');
  277. repeat = repeat==null || repeat == '' ? 1 : repeat;
  278. for(i=0;i<repeat;i++){
  279. var valueInput = $(Mustache.render(value,dataAttributes));
  280. condition.find('span.filter-value').append(valueInput);
  281. //Si le type de champ est une liste on la remplit avec le datasource
  282. if(dataAttributes.filterSource){
  283. var source = dataAttributes.filterSource;
  284. var options = '<option value=""> - </option>';
  285. for (var k in source)
  286. options += '<option value="'+k+'">'+source[k]+'</option>';
  287. valueInput.append(options);
  288. }
  289. if(data && data.value!=null){
  290. valueInput
  291. .val(data.value[i])
  292. .attr('data-value',data.value[i])
  293. .trigger('change');
  294. }
  295. }
  296. init_components(condition);
  297. }
  298. //lance la recherche (via le bouton)
  299. FilterBox.prototype.search = function(){
  300. var object = this;
  301. var filters = object.filters();
  302. if(filters.advanced.length>0 || filters.keyword!=''){
  303. filters.k = filters.keyword;
  304. filters.a = filters.advanced;
  305. delete filters.advanced;
  306. delete filters.keyword;
  307. filters.a = object.filter_rename_keys(filters.a,{join:'j',operator:'o',value:'v',type:'t',column:'c',group:'g'});
  308. filters = JSON.stringify(filters);
  309. if(!this.data.hasOwnProperty('urlsearch') || this.data.urlsearch==true)
  310. $.urlParam('filters',btoa(filters));
  311. }else{
  312. $.urlParam('filters','');
  313. }
  314. window[object.data.function]();
  315. if(object.data.callback) window[object.data.callback]();
  316. }
  317. //Renommage des clés des filtres (permet de compresser la chaine base64 encodée en url)
  318. FilterBox.prototype.filter_rename_keys = function(filters,mapping){
  319. object = this;
  320. var newFilters = [];
  321. for(var k in filters){
  322. newFilters[k] = {};
  323. var keys = Object.keys(filters[k]);
  324. for(var i in keys){
  325. var key = keys[i];
  326. if(mapping[key] === null) continue;
  327. newFilters[k][mapping[key]] = filters[k][key];
  328. }
  329. if(newFilters[k].g) newFilters[k].g = object.filter_rename_keys(newFilters[k].g,mapping);
  330. if(newFilters[k].group) newFilters[k].group = object.filter_rename_keys(newFilters[k].group,mapping);
  331. }
  332. return newFilters;
  333. }
  334. //définition ou récuperation d'un tableau de filtres
  335. FilterBox.prototype.filters = function(values,showErrors){
  336. var object = this;
  337. if(values){
  338. object.filter_recursive_set($('.criterias',object.box),values);
  339. return;
  340. }
  341. filters = object.filter_recursive_get($('.criterias',object.box));
  342. return {
  343. keyword : $('.filter-keyword',object.box).val(),
  344. advanced : filters
  345. };
  346. }
  347. //Définition des filtres dpeuis un objet values de manière récursive
  348. FilterBox.prototype.filter_recursive_set = function(parent,values){
  349. var object = this;
  350. var filters = [];
  351. for(var key in values){
  352. condition = object.filter_add(null,values[key],parent);
  353. if(values[key].group){
  354. var newGroup = $('<ul class="group"></ul>');
  355. condition.find('.filter-column').after(newGroup);
  356. condition.find('.filter-column').remove();
  357. condition.find('.filter-operator').remove();
  358. condition.find('.filter-value').remove();
  359. if(values[key].join && values[key].join!='') condition.find('>.filter-join').val(values[key].join);
  360. condition.prepend(newGroup);
  361. object.filter_recursive_set(newGroup,values[key].group);
  362. }
  363. }
  364. return filters;
  365. }
  366. //Récuperation des diltres définis sur l'ui dans un object et d emanière récursive
  367. FilterBox.prototype.filter_recursive_get = function(parent){
  368. var object = this;
  369. var filters = [];
  370. $(parent).find('> .condition:visible').each(function(i,element){
  371. var filter = {};
  372. var element = $(element);
  373. if(element.find('> .group').length>0){
  374. filter.join = element.find('> .filter-join:visible').val();
  375. filter.group = object.filter_recursive_get(element.find('> .group'));
  376. }else{
  377. filter.type = element.find('> .filter-column select option:selected').attr('data-filter-type');
  378. var typeData = $('.filter-value-block[data-value-type="'+filter.type+'"]').data();
  379. filter.column = element.find('> .filter-column select').val();
  380. if(filter.column == null || filter.column == ''){
  381. object.filter_remove(element);
  382. return;
  383. } else {
  384. filter.operator = element.find('> .filter-operator select').val();
  385. filter.value = [];
  386. if(typeData.valueSelector){
  387. var valueElements = element.find('> .filter-value '+typeData.valueSelector);
  388. }else{
  389. var valueElements = element.find('> .filter-value > .filter-value');
  390. }
  391. valueElements.each(function(u,input){
  392. filter.value.push($(input).val());
  393. });
  394. filter.join = element.find('> .filter-join:visible').val();
  395. }
  396. }
  397. if(!filter.group || filter.group.length) filters.push(filter);
  398. });
  399. return filters;
  400. }
  401. //Ajout d'un filtre visuel (vide ou replis avec l'obj data) dans un parent (optionnel) ou après un element (optionnel)
  402. //si pas de parent ou d'élements définis, le filtre s'ajoute au premier niveau.
  403. FilterBox.prototype.filter_add = function(element,data,parent){
  404. var object = this;
  405. if(!data) data = {};
  406. data.columns = object.columns;
  407. var condition = $(Mustache.render(object.tpl,data));
  408. condition.removeClass('hidden');
  409. if(element){
  410. element.after(condition);
  411. }else if(parent){
  412. parent.append(condition);
  413. }else{
  414. $('.criterias',object.box).append(condition);
  415. }
  416. if(data) object.filter_refresh(condition,true,data);
  417. $(condition.parent()).sortable({
  418. axis : 'y',
  419. handle: ".btn-move",
  420. });
  421. return condition;
  422. }
  423. //Supression d'un filtre et de ses parents vides.
  424. FilterBox.prototype.filter_remove = function(line){
  425. var object = this;
  426. var group = line.closest('ul.group');
  427. //Si le filter est le dernier de la recherche, on reset ses champs sans le supprimer
  428. if($('.criterias .condition:visible:has(>.filter-column)',object.box).length==1){
  429. line.find('.filter-column select').prop('selectedIndex',0);
  430. line.find('.filter-operator,.filter-value').html('');
  431. line.find('.btn-unindent').trigger('click');
  432. return;
  433. }
  434. //sinon on le supprime complétement
  435. if($('.criterias > .condition:visible',object.box).length>1 || line.siblings('.condition:visible').length>0) line.remove();
  436. //suppression ascendante récursive des groupes vides après suppression de la ligne
  437. while(1){
  438. if(!group || group.length == 0 || group.find('.condition').length!=0) break;
  439. oldgroup = group.parent().parent();
  440. group.parent().remove();
  441. group = oldgroup;
  442. }
  443. }