PageRenderTime 62ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/webapp/js/explorer.backbone.js

https://github.com/sibbr/explorador
JavaScript | 982 lines | 818 code | 88 blank | 76 comment | 75 complexity | fc5811c71692433c06402aeb9470916b MD5 | raw file
  1. /****************************
  2. Copyright (c) 2014 Canadensys
  3. Explorer backbone
  4. ****************************/
  5. /*global EXPLORER, $, window, _, Backbone, google, alert*/
  6. EXPLORER.backbone = (function(){
  7. 'use strict';
  8. var filterViewTemplate = _.template($('#filter_template_current').html()),
  9. FilterKey = Backbone.Model.extend({
  10. defaults: {
  11. searchableFieldId: -1,
  12. searchableFieldName : undefined,
  13. searchableFieldText : undefined
  14. }
  15. }),
  16. FilterItem = Backbone.Model.extend({
  17. defaults: {
  18. searchableFieldId: -1,
  19. searchableFieldName: undefined,
  20. searchableFieldText: undefined,
  21. groupId : -1,
  22. op: undefined,
  23. opText : undefined,
  24. valueList: [],
  25. valueJSON : undefined,
  26. valueText : undefined
  27. }
  28. }),
  29. SearchResult = Backbone.Model.extend({
  30. defaults: {
  31. numberOfRecord: 0
  32. }
  33. }),
  34. TextSearch = Backbone.Model.extend({
  35. defaults: {
  36. currentText: undefined,
  37. validate:false
  38. }
  39. }),
  40. FilterList = Backbone.Collection.extend({
  41. model: FilterItem
  42. }),
  43. filterList = new FilterList(),
  44. currFilterKey = new FilterKey(),
  45. currSearchResult = new SearchResult(),
  46. currTextSearch = new TextSearch(),
  47. availableSearchFields,
  48. initialFilterParamMap,
  49. DEFAULT_SEARCHABLE_FIELD_ID = 16; //scientificName
  50. function setNumberOfResult(numberOfRecord){
  51. currSearchResult.set("numberOfRecord",numberOfRecord);
  52. }
  53. function setAvailableSearchFields(_availableSearchFields){
  54. availableSearchFields = _availableSearchFields;
  55. }
  56. function getInitialFilterParamMap(){
  57. return initialFilterParamMap;
  58. }
  59. function getOperatorText(operator){
  60. var varName = 'operator.'+operator.toLowerCase();
  61. return EXPLORER.i18n.getLanguageResource(varName);
  62. }
  63. function getAvailableFieldText(fieldName){
  64. var varName = 'filter.'+fieldName.toLowerCase();
  65. return EXPLORER.i18n.getLanguageResource(varName);
  66. }
  67. //Check if a searchableField is a Boolean searchableField
  68. //This is defined by the content of 'type' in the filter
  69. function isBooleanSearchableField(searchableField){
  70. return (!_.isUndefined(searchableField.type) && searchableField.type.indexOf("Boolean") !== -1);
  71. }
  72. //Check if a searchableField is a Geospatial searchableField
  73. //For now, this is achieved using the searchableField.searchableFieldTypeEnum value.
  74. //Eventually, we should be able to use the list of searchableFieldId per category (e.g. CLASSIFICATION,LOCATION ...) which you be more reliable.
  75. function isGeospatialSearchableField(searchableField){
  76. return _.contains(['WITHIN_RADIUS_GEO','INSIDE_ENVELOPE_GEO','INSIDE_POLYGON_GEO'], searchableField.searchableFieldTypeEnum);
  77. }
  78. //Generate the value text to be displayed based on searchableField and value list.
  79. //If the searchableField is a Boolean field, value will be translated.
  80. function generateValueText(searchableField,valueList){
  81. var parsedValueText = [], varName;
  82. $.each(valueList, function() {
  83. if(isBooleanSearchableField(searchableField)){
  84. varName = 'filter.value.'+this.toString().toLowerCase();
  85. parsedValueText.push(EXPLORER.i18n.getLanguageResource(varName));
  86. }
  87. else if(isGeospatialSearchableField(searchableField)){
  88. return "";
  89. }
  90. else {
  91. parsedValueText.push(this);
  92. }
  93. });
  94. return parsedValueText.join();
  95. }
  96. //Bind an action on the current filters list
  97. function bindToFilterList(action,callback,context){
  98. filterList.bind(action, callback, context);
  99. }
  100. //Add a new active filter based on provided filter properties.
  101. //Mandatory properties are: (searchableFieldId or searchableFieldName) and valueList
  102. //A copy of the newly created FilterItem will be returned.
  103. function addActiveFilter(filterProps){
  104. filterProps = filterProps || {};
  105. var searchableFieldId = filterProps.searchableFieldId,
  106. searchableFieldName = filterProps.searchableFieldName,
  107. valueList = filterProps.valueList,
  108. searchableField, operator, valueJSON, newFilter;
  109. //find the searchableField by id or name
  110. if(searchableFieldId){
  111. searchableField = availableSearchFields[searchableFieldId.toString()];
  112. }
  113. else if(searchableFieldName){
  114. searchableField = _.find(availableSearchFields,
  115. function(sf){ return sf.searchableFieldName === searchableFieldName; });
  116. }
  117. if(!searchableField){
  118. //log to console for debug?
  119. return;
  120. }
  121. //if no operator is specified, we can accomodate that if the searchableField contains only 1 operator
  122. if(!filterProps.op && searchableField.supportedOperator.length !== 1){
  123. //log to console for debug?
  124. return;
  125. }
  126. operator = filterProps.op || searchableField.supportedOperator[0];
  127. valueJSON = JSON.stringify(valueList);
  128. newFilter = new FilterItem({
  129. searchableFieldId : searchableField.searchableFieldId,
  130. searchableFieldName : searchableField.searchableFieldName,
  131. searchableFieldText : getAvailableFieldText(searchableField.searchableFieldName),
  132. op : operator,
  133. opText : getOperatorText(operator),
  134. valueList : valueList,
  135. valueJSON : valueJSON,
  136. valueText : generateValueText(searchableField,valueList)
  137. });
  138. filterList.add(newFilter);
  139. //return a copy, maybe we should only do it if the caller asked for it?
  140. return _.extend({},newFilter);
  141. }
  142. //Update a FilterItem that is currently in the list
  143. //Only the 'valueList' array field can be updated for now
  144. function updateActiveFilter(filterItem,props){
  145. var _filterItem = filterList.get(filterItem.cid);
  146. if(_filterItem){
  147. var valueList = props.valueList || [],
  148. searchableField = availableSearchFields[_filterItem.get('searchableFieldId').toString()],
  149. newValues = {
  150. valueList : valueList,
  151. valueJSON : JSON.stringify(valueList),
  152. valueText : generateValueText(searchableField,valueList)};
  153. _filterItem.set(newValues);
  154. }
  155. }
  156. //initialize active filters from a list of json properties
  157. function initActiveFilters(json, callback, scope){
  158. var lastSearchableFieldId, key;
  159. for (key in json) {
  160. if (json.hasOwnProperty(key)) {
  161. addActiveFilter(json[key]);
  162. lastSearchableFieldId = json[key].searchableFieldId.toString();
  163. }
  164. }
  165. //keep the initial state, the other solution could be to receive this from the server
  166. initialFilterParamMap = $('form',$('#search')).serializeArray();
  167. if(!lastSearchableFieldId){
  168. lastSearchableFieldId = DEFAULT_SEARCHABLE_FIELD_ID;
  169. }
  170. currFilterKey.set({searchableFieldId:lastSearchableFieldId,
  171. searchableFieldName:availableSearchFields[lastSearchableFieldId].searchableFieldName,
  172. searchableFieldText:getAvailableFieldText(availableSearchFields[lastSearchableFieldId].searchableFieldName)});
  173. if(typeof callback === "function") {
  174. callback.call(scope || this, filterList, this);
  175. }
  176. }
  177. function removeFilter(json) {
  178. var filter = filterList.findWhere(json);
  179. if(filter) {
  180. filterList.remove(filter);
  181. }
  182. }
  183. //Get a filter based on properties
  184. //If more than on filter can be found with those properties only the first one is returned
  185. function getActiveFilter(props) {
  186. //searchableFieldId can be used as int or String
  187. //ensure we search with an int
  188. if (props.searchableFieldId){
  189. props.searchableFieldId = parseInt(props.searchableFieldId, 10);
  190. }
  191. return filterList.findWhere(props);
  192. }
  193. //View that supports the text entry
  194. var TextEntryView = Backbone.View.extend({
  195. textValueTemplate : _.template($('#filter_template_text_input').html()),
  196. initialize : function() {
  197. this.setElement(this.textValueTemplate());
  198. },
  199. render : function() {
  200. return this;
  201. },
  202. events : {
  203. "keyup input" : "onTextTyped"
  204. },
  205. onTextTyped : function(e) {
  206. var value;
  207. if(e){
  208. value = $(e.currentTarget).val();
  209. currTextSearch.set({currentText:value,validate:(e.keyCode === 13)});
  210. if(e.keyCode === 40) {
  211. $('#value_suggestions tr').keynavigator({
  212. cycle : false,
  213. useCache : false,
  214. activateOn : 'focus',
  215. keys : {
  216. enter : function(element) {
  217. $(element).trigger('click');
  218. }
  219. }
  220. }).first().focus();
  221. }
  222. }
  223. }
  224. });
  225. //View that supports text suggestion
  226. var TextValueSuggestionView = Backbone.View.extend({
  227. suggestionValueTemplate : _.template($('#filter_template_suggestions').html()),
  228. cacheKeys : [],
  229. cacheMap : {},
  230. initialize : function() {
  231. currTextSearch.bind('change:currentText', this.onTextChanged, this);
  232. },
  233. destroy : function(){
  234. currTextSearch.unbind('change:currentText',this.onTextChanged);
  235. this.remove();
  236. },
  237. render : function() {
  238. this.setElement(this.suggestionValueTemplate());
  239. this.onTextChanged(currTextSearch);
  240. return this;
  241. },
  242. events : {
  243. "click #value_suggestions tr" : "createNewSuggestionFilter"
  244. },
  245. onTextChanged : function(textSearchModel) {
  246. var mapParam = {},
  247. cacheKey = currFilterKey.get('searchableFieldId'),
  248. value = textSearchModel.get('currentText'),
  249. self = this;
  250. mapParam.fieldId = currFilterKey.get('searchableFieldId');
  251. if(value){
  252. mapParam.curr = value;
  253. cacheKey += value;
  254. }
  255. if(this.cacheMap[cacheKey]) {
  256. this.replaceTableCellContent(this.cacheMap[cacheKey]);
  257. }
  258. else{
  259. $.get(EXPLORER.settings.baseUrl + EXPLORER.settings.wsPath + 'livesearch',mapParam)
  260. .success(function(json){
  261. self.replaceTableCellContent(json);
  262. if(self.cacheKeys.length === 10){
  263. delete self.cacheMap[self.cacheKeys.shift()];
  264. }
  265. self.cacheKeys.push(cacheKey);
  266. self.cacheMap[cacheKey] = json;
  267. })
  268. .error(function(jqXHR, textStatus, errorThrown){
  269. alert(textStatus);
  270. });
  271. }
  272. },
  273. replaceTableCellContent : function(json){
  274. var rows = json.rows,
  275. lastRow=0,
  276. row,
  277. self = this;
  278. $.each(rows, function(key, val) {
  279. row = $('#value_suggestions tr:nth-child('+(key+1)+')',self.$el);
  280. row.find('td:nth-child(1)').html(val.value);
  281. row.find('td:nth-child(2)').html(val.occurrence_count);
  282. row.removeClass('hidden');
  283. row.attr('id',val.id);
  284. lastRow = key+1; //to match tr index (who starts at 1)
  285. });
  286. //move to next row
  287. lastRow += 1;
  288. //clear remaining rows
  289. while(lastRow<=10){
  290. row = $('#value_suggestions tr:nth-child('+lastRow+')',self.$el);
  291. row.find('td:nth-child(1)').html(' ');
  292. row.find('td:nth-child(2)').html(' ');
  293. row.addClass('hidden');
  294. row.removeAttr('id');
  295. lastRow += 1;
  296. }
  297. },
  298. createNewSuggestionFilter : function(e) {
  299. var value = [$(e.currentTarget).find('td:nth-child(1)').text()],
  300. valueJSON = JSON.stringify(value);
  301. //ignore duplicate filter (ignore case)
  302. if(filterList.find(function(currFilter){
  303. return (currFilter.get('searchableFieldId') === parseInt(currFilterKey.get('searchableFieldId'), 10) &&
  304. currFilter.get('valueJSON').toLowerCase() === valueJSON.toLowerCase());
  305. })){
  306. return;
  307. }
  308. addActiveFilter({
  309. searchableFieldId:currFilterKey.get('searchableFieldId'),
  310. op:'EQ',
  311. valueList : value
  312. });
  313. }
  314. });
  315. //View that supports creation of a filter with the LIKE operator
  316. var PartialTextValueView = Backbone.View.extend({
  317. partialTextTemplate : _.template($('#filter_template_partial_match').html()),
  318. initialize : function() {
  319. currTextSearch.bind('change:currentText', this.onTextChanged, this);
  320. currTextSearch.bind('change:validate', this.onValidate, this);
  321. //find the like operator
  322. this.likeOp = _.find(availableSearchFields[currFilterKey.get('searchableFieldId')].supportedOperator,
  323. function(str){ return str.toLowerCase().search("^[s|e|c]like$") !== -1; });
  324. },
  325. destroy : function(){
  326. currTextSearch.unbind('change:currentText', this.onTextChanged);
  327. currTextSearch.unbind('change:validate', this.onValidate);
  328. this.remove();
  329. },
  330. render : function() {
  331. this.setElement(this.partialTextTemplate({opText : getOperatorText(this.likeOp)}));
  332. return this;
  333. },
  334. events : {
  335. //bound to the root element
  336. "click button" : "createNewLikeFilter"
  337. },
  338. onTextChanged : function(textSearchModel) {
  339. $("#partial_match_value",this.$el).text(textSearchModel.get('currentText'));
  340. },
  341. onValidate : function(textSearchModel) {
  342. if(textSearchModel.get('validate')){
  343. this.createNewLikeFilter();
  344. }
  345. },
  346. createNewLikeFilter : function() {
  347. var value = [currTextSearch.get('currentText')],
  348. valueJSON = JSON.stringify(value);
  349. //skip empty filter
  350. if(!value || value.length === 0){
  351. return;
  352. }
  353. //ignore duplicate filter (ignore case)
  354. if(filterList.find(function(currFilter){
  355. return (currFilter.get('searchableFieldId') === parseInt(currFilterKey.get('searchableFieldId'), 10) &&
  356. currFilter.get('valueJSON').toLowerCase() === valueJSON.toLowerCase());
  357. })){
  358. return;
  359. }
  360. addActiveFilter({
  361. searchableFieldId:currFilterKey.get('searchableFieldId'),
  362. op:this.likeOp,
  363. valueList : value
  364. });
  365. }
  366. });
  367. var SelectionValueView = Backbone.View.extend({
  368. textSelectionTemplate : _.template($('#filter_template_select').html()),
  369. initialize: function() {
  370. return;
  371. },
  372. destroy : function(){
  373. this.remove();
  374. },
  375. render : function() {
  376. this.setElement(this.textSelectionTemplate());
  377. this.loadContent(currFilterKey.get('searchableFieldId'));
  378. return this;
  379. },
  380. events : {
  381. "click button" : "createNewFilter"
  382. },
  383. loadContent : function(fieldId) {
  384. var $select, options = "";
  385. //could also be loaded with a model fetch
  386. $.get(EXPLORER.settings.baseUrl + EXPLORER.settings.wsPath + 'getpossiblevalues',{fieldId:fieldId})
  387. .success(function(json){
  388. $select = $("#value_select",this.$el);
  389. $.each(json, function() {
  390. options += '<option value="'+this.value+'">'+this.value+'</option>';
  391. });
  392. $select.append(options);
  393. })
  394. .error(function(jqXHR, textStatus, errorThrown){
  395. alert(textStatus);
  396. });
  397. },
  398. createNewFilter : function() {
  399. var value = [$("#value_select",this.$el).val()],
  400. valueJSON = JSON.stringify(value);
  401. //skip empty filter
  402. if(!value || value.length === 0){
  403. return;
  404. }
  405. //ignore if a current filter exists
  406. if(getActiveFilter({searchableFieldId: currFilterKey.get('searchableFieldId'),valueJSON:valueJSON})){
  407. return;
  408. }
  409. addActiveFilter({
  410. searchableFieldId:currFilterKey.get('searchableFieldId'),
  411. valueList : value
  412. });
  413. }
  414. });
  415. //View that supports creation of a filter with yes/no options
  416. var BooleanValueView = Backbone.View.extend({
  417. booleanValueTemplate : _.template($('#filter_template_boolean_value').html()),
  418. initialize: function() {
  419. return;
  420. },
  421. destroy : function(){
  422. this.remove();
  423. },
  424. render : function() {
  425. this.setElement(this.booleanValueTemplate({fieldText : currFilterKey.get('searchableFieldText')}));
  426. return this;
  427. },
  428. events : {
  429. //bound to the root element
  430. "click button" : "createNewLikeFilter"
  431. },
  432. createNewLikeFilter : function() {
  433. var value = [($('input[name=boolGroup]:checked',this.$el).val())];
  434. //skip empty filter
  435. if(value.length === 0){
  436. return;
  437. }
  438. //ignore duplicate filter, boolean filter must not be already included
  439. if(getActiveFilter({searchableFieldId: currFilterKey.get('searchableFieldId')})){
  440. return;
  441. }
  442. addActiveFilter({
  443. searchableFieldId:currFilterKey.get('searchableFieldId'),
  444. valueList : value
  445. });
  446. }
  447. });
  448. //Responsible to render the proper view based on the options of the searchable field.
  449. var TextValueView = Backbone.View.extend({
  450. textValueTemplate : _.template($('#filter_template_single').html()),
  451. initialize : function() {
  452. this.setElement(this.textValueTemplate());
  453. var searchableField = availableSearchFields[currFilterKey.get('searchableFieldId')];
  454. this.supportSuggestion = searchableField.supportSuggestion;
  455. this.supportPartialMatch = searchableField.supportPartialMatch;
  456. this.supportSelectionList = searchableField.supportSelectionList;
  457. this.isBooleanFilter = isBooleanSearchableField(searchableField);
  458. this.textValueSuggestionView = undefined;
  459. this.partialTextValueView = undefined;
  460. this.selectionValueView = undefined;
  461. this.booleanValueView = undefined;
  462. },
  463. render : function() {
  464. currTextSearch.set('currentText','');
  465. if(this.supportPartialMatch){
  466. this.partialTextValueView = new PartialTextValueView();
  467. this.$el.append(this.partialTextValueView.render().el);
  468. }
  469. if(this.supportPartialMatch || this.supportSuggestion){
  470. this.$el.append(new TextEntryView().render().el);
  471. }
  472. if(this.supportSuggestion){
  473. this.textValueSuggestionView = new TextValueSuggestionView();
  474. this.$el.append(this.textValueSuggestionView.render().el);
  475. }
  476. if(this.supportSelectionList){
  477. this.selectionValueView = new SelectionValueView();
  478. this.$el.append(this.selectionValueView.render().el);
  479. }
  480. if(this.isBooleanFilter){
  481. this.booleanValueView = new BooleanValueView();
  482. this.$el.append(this.booleanValueView.render().el);
  483. }
  484. return this;
  485. },
  486. destroy : function() {
  487. if(this.textValueSuggestionView){
  488. this.textValueSuggestionView.destroy();
  489. this.textValueSuggestionView = undefined;
  490. }
  491. if(this.partialTextValueView){
  492. this.partialTextValueView.destroy();
  493. this.partialTextValueView = undefined;
  494. }
  495. if(this.selectionValueView){
  496. this.selectionValueView.destroy();
  497. this.selectionValueView = undefined;
  498. }
  499. if(this.booleanValueView){
  500. this.booleanValueView.destroy();
  501. this.booleanValueView = undefined;
  502. }
  503. this.remove();
  504. }
  505. });
  506. //View to add a date or date interval filter
  507. var DateIntervalValueView = Backbone.View.extend({
  508. dateIntervalTemplate : _.template($('#filter_template_date').html()),
  509. initialize: function() {
  510. return;
  511. },
  512. destroy: function() {
  513. return;
  514. },
  515. render : function() {
  516. this.setElement(this.dateIntervalTemplate());
  517. //by default, this is hidden
  518. $("#date_end",this.el).hide();
  519. return this;
  520. },
  521. events : {
  522. "click button" : "createNewFilter",
  523. "focus input[type=text]" : "onFocus",
  524. "blur input[type=text]" : "onBlur",
  525. "change input[type=checkbox]" : "onSearchIntervalChanged"
  526. },
  527. onBlur : function(e){
  528. var $el = $(e.currentTarget);
  529. //accept empty field
  530. if($el.val() && !EXPLORER.utils.isValidDateElement($el)){
  531. $el.addClass('error');
  532. }
  533. else{
  534. $el.removeClass('error');
  535. }
  536. },
  537. onSearchIntervalChanged : function(){
  538. $("#date_end",this.$el).toggle();
  539. $(".label_single",this.$el).toggleClass("hidden");
  540. $(".label_range",this.$el).toggleClass("hidden");
  541. },
  542. createNewFilter : function() {
  543. var syear = $.trim($("#date_start_y",this.$el).val()),
  544. smonth = $.trim($("#date_start_m",this.$el).val()),
  545. sday = $.trim($("#date_start_d",this.$el).val()),
  546. isInterval = $("#interval",this.$el).is(':checked'),
  547. eyear = $.trim($("#date_end_y",this.$el).val()),
  548. emonth = $.trim($("#date_end_m",this.$el).val()),
  549. eday = $.trim($("#date_end_d",this.$el).val()),
  550. valueJSON, searchValue;
  551. if(!EXPLORER.utils.isValidPartialDate(syear,smonth,sday) || (isInterval && !EXPLORER.utils.isValidPartialDate(eyear,emonth,eday))){
  552. alert(EXPLORER.i18n.getLanguageResource('control.invalid.date'));
  553. return;
  554. }
  555. if(isInterval && !EXPLORER.utils.isValidDateInterval(syear,smonth,sday,eyear,emonth,eday)){
  556. alert(EXPLORER.i18n.getLanguageResource('control.invalid.dateinterval'));
  557. return;
  558. }
  559. searchValue = [syear+'-'+EXPLORER.utils.dateElementZeroPad(smonth)+'-'+EXPLORER.utils.dateElementZeroPad(sday)];
  560. if(isInterval){
  561. searchValue.push(eyear+'-'+EXPLORER.utils.dateElementZeroPad(emonth)+'-'+EXPLORER.utils.dateElementZeroPad(eday));
  562. }
  563. valueJSON = JSON.stringify(searchValue);
  564. //ignore duplicate filter
  565. if(getActiveFilter({searchableFieldId: currFilterKey.get('searchableFieldId'),valueJSON:valueJSON})){
  566. return;
  567. }
  568. addActiveFilter({
  569. searchableFieldId:currFilterKey.get('searchableFieldId'),
  570. op: isInterval?'BETWEEN':'EQ',
  571. valueList : searchValue
  572. });
  573. }
  574. });
  575. //View to add a min/max filter. It can accept exact or interval.
  576. //e.g. altitude filter
  577. var MinMaxValueView = Backbone.View.extend({
  578. minMaxValueTemplate : _.template($('#filter_template_minmax').html()),
  579. initialize: function() {
  580. return;
  581. },
  582. destroy: function() {
  583. return;
  584. },
  585. render : function() {
  586. this.setElement(this.minMaxValueTemplate());
  587. //by default, this is hidden
  588. $("#interval_max",this.el).hide();
  589. return this;
  590. },
  591. events : {
  592. "click button" : "createNewFilter",
  593. "focus input[type=text]" : "onFocus",
  594. "blur input[type=text]" : "onBlur",
  595. "change input[type=checkbox]" : "onSearchIntervalChanged"
  596. },
  597. onFocus: function() {
  598. return;
  599. },
  600. onBlur : function(e){
  601. var $el = $(e.currentTarget),
  602. value = $.trim($el.val());
  603. //accept empty field
  604. if(!EXPLORER.utils.isValidNumber(value)){
  605. $el.addClass('error');
  606. }
  607. else{
  608. $el.removeClass('error');
  609. }
  610. },
  611. onSearchIntervalChanged : function(){
  612. $("#interval_max",this.$el).toggle();
  613. $(".label_single",this.$el).toggleClass("hidden");
  614. $(".label_range",this.$el).toggleClass("hidden");
  615. },
  616. createNewFilter : function() {
  617. var minValue = $.trim($("#value_min",this.$el).val()),
  618. isInterval = $("#interval",this.$el).is(':checked'),
  619. maxValue = $.trim($("#value_max",this.$el).val()),
  620. searchValue, valueJSON;
  621. if(!EXPLORER.utils.isValidNumber(minValue)){
  622. alert(EXPLORER.i18n.getLanguageResource('control.invalid.number'));
  623. return;
  624. }
  625. if(isInterval && !EXPLORER.utils.isValidNumber(maxValue)){
  626. alert(EXPLORER.i18n.getLanguageResource('control.invalid.numberinterval'));
  627. return;
  628. }
  629. searchValue = [minValue];
  630. if(isInterval){
  631. searchValue.push(maxValue);
  632. }
  633. valueJSON = JSON.stringify(searchValue);
  634. //ignore duplicate filter
  635. if(getActiveFilter({searchableFieldId: currFilterKey.get('searchableFieldId'),valueJSON:valueJSON})){
  636. return;
  637. }
  638. addActiveFilter({
  639. searchableFieldId:currFilterKey.get('searchableFieldId'),
  640. op: isInterval?'BETWEEN':'EQ',
  641. valueList : searchValue
  642. });
  643. }
  644. });
  645. //View of a group of active filters of the same type.
  646. var FilterGroupView = Backbone.View.extend({
  647. tagName: 'li', // name of tag to be created
  648. className: 'filter round',
  649. initialize: function() {
  650. return;
  651. },
  652. render : function() {
  653. $(this.el).html(this.model.get('searchableFieldText') + '<ul></ul>');
  654. return this;
  655. }
  656. });
  657. //View of a single active filter
  658. var FilterView = Backbone.View.extend({
  659. tagName: 'li', // name of tag to be created
  660. events: {
  661. 'click span.delete': 'remove'
  662. },
  663. initialize : function() {
  664. this.model.bind('remove', this.unrender, this);
  665. this.model.bind('change', this.render, this);
  666. },
  667. render : function() {
  668. //compute from template
  669. $(this.el).html(filterViewTemplate(this.model.toJSON()));
  670. return this;
  671. },
  672. unrender: function(){
  673. $(this.el).remove();
  674. },
  675. remove: function(){
  676. //destoy will also remove events bound to the model
  677. this.model.destroy();
  678. }
  679. });
  680. var CurrentFiltersView = Backbone.View.extend({
  681. initialize : function() {
  682. filterList.bind('add', this.addFilter, this);
  683. filterList.bind('remove', this.removeFilter, this);
  684. this.emptyFilterHtml = $('#filter_empty');
  685. this.filterCounter = 0;
  686. this.nbOfFilter = 0;
  687. //keep track of the grouping component
  688. this.filterGroupView = {};
  689. },
  690. addFilter : function(filter) {
  691. //remove empty filter element
  692. if(this.nbOfFilter === 0){
  693. $('#filter_current').find(':first-child').remove();
  694. }
  695. //group exists ?
  696. if(!this.filterGroupView[filter.get('searchableFieldId')]) {
  697. var view = new FilterGroupView({
  698. model : filter
  699. });
  700. this.filterGroupView[filter.get('searchableFieldId')] = view.render().el;
  701. $("#filter_current").append(this.filterGroupView[filter.get('searchableFieldId')]);
  702. }
  703. this.filterCounter = this.filterCounter+1;
  704. this.nbOfFilter = this.nbOfFilter+1;
  705. //Set the groupId because the counter is here
  706. filter.set('groupId', this.filterCounter);
  707. var filterView = new FilterView({
  708. model : filter
  709. });
  710. $('ul', this.filterGroupView[filter.get('searchableFieldId')]).append(filterView.render().el);
  711. },
  712. removeFilter : function(filter) {
  713. //check if we just removed the last element
  714. if(filterList.where({searchableFieldId: filter.get('searchableFieldId')}).length === 0){
  715. $(this.filterGroupView[filter.get('searchableFieldId')]).remove();
  716. delete this.filterGroupView[filter.get('searchableFieldId')];
  717. }
  718. this.nbOfFilter = this.nbOfFilter-1;
  719. if(this.nbOfFilter===0){
  720. $("#filter_current").append(this.emptyFilterHtml);
  721. }
  722. },
  723. render : function() {
  724. return this;
  725. }
  726. });
  727. //Allows to select the field like country, taxonRank
  728. var FilterFieldSelectionView = Backbone.View.extend({
  729. $key_select : undefined,
  730. initialize : function() {
  731. //el could be set through the caller
  732. this.setElement(this.el);
  733. this.$key_select = $('#key_select',this.$el); //cache this component
  734. //this.$key_select.val('1');
  735. currFilterKey.bind('change:searchableFieldId', this.onFilterKeyChanged, this);
  736. },
  737. events : {
  738. "change #key_select" : "onFilterFieldChanged"
  739. },
  740. onFilterKeyChanged : function(model){
  741. //make sure this event is not from us (onFilterFieldChanged)
  742. //this should be called on pageload to set the current filter properly
  743. if(this.$key_select.val() !== model.get('searchableFieldId')){
  744. this.$key_select.val(model.get('searchableFieldId'));
  745. }
  746. },
  747. onFilterFieldChanged : function() {
  748. var searchableFieldId = this.$key_select.val();
  749. currFilterKey.set({searchableFieldId:searchableFieldId,
  750. searchableFieldName:availableSearchFields[searchableFieldId].searchableFieldName,
  751. searchableFieldText:getAvailableFieldText(availableSearchFields[searchableFieldId].searchableFieldName)});
  752. },
  753. render : function() {
  754. return this;
  755. }
  756. });
  757. //Allows to select the specific value based on the field (previously selected)
  758. var FilterSelectionView = Backbone.View.extend({
  759. lastComponent: undefined,
  760. initialize : function() {
  761. this.setElement('#filter_content');
  762. currFilterKey.bind('change:searchableFieldId', this.onFilterKeyChanged, this);
  763. },
  764. events : {
  765. },
  766. onFilterKeyChanged : function(filterKey) {
  767. var searchableFieldTypeEnum = availableSearchFields[filterKey.get('searchableFieldId')].searchableFieldTypeEnum;
  768. //This is not necessary but it makes it clear that we create new element each time
  769. if(this.lastComponent){
  770. this.lastComponent.destroy();
  771. }
  772. if(searchableFieldTypeEnum === 'SINGLE_VALUE'){
  773. this.lastComponent = new TextValueView();
  774. }
  775. else if(searchableFieldTypeEnum === 'START_END_DATE'){
  776. this.lastComponent = new DateIntervalValueView();
  777. }
  778. else if(searchableFieldTypeEnum === 'MIN_MAX_NUMBER'){
  779. this.lastComponent = new MinMaxValueView();
  780. }
  781. else{
  782. this.lastComponent = new TextValueView();
  783. }
  784. this.$el.html(this.lastComponent.render().el);
  785. },
  786. render : function() {
  787. return this;
  788. }
  789. });
  790. var DownloadEmailView = Backbone.View.extend({
  791. downloadEmailTemplate : _.template($('#download_template_email').html()),
  792. events: {
  793. 'click button' : 'onAskForDownload'
  794. },
  795. initialize : function() {
  796. //el could be set through the caller
  797. this.setElement(this.el);
  798. },
  799. render : function() {
  800. this.$el.html(this.downloadEmailTemplate());
  801. this.$requestElement = $('#request',this.$el);
  802. this.$statusElement = $('#status',this.$el);
  803. return this;
  804. },
  805. onAskForDownload : function(){
  806. var self = this, email = $.trim($('#email',this.$el).val());
  807. if(!EXPLORER.utils.isValidEmail(email)){
  808. alert(EXPLORER.i18n.getLanguageResource('control.download.email.error'));
  809. return;
  810. }
  811. this.paramMap = [];
  812. _.extend(this.paramMap,initialFilterParamMap);
  813. this.paramMap.push({name:'e',value:email});
  814. this.$requestElement.hide();
  815. $.get(EXPLORER.settings.baseUrl + EXPLORER.settings.wsPath + 'downloadresult',this.paramMap)
  816. .success(function(json){
  817. if(json.status !== 'deferred'){
  818. self.$statusElement.html(json.error);
  819. }
  820. self.$statusElement.show();
  821. })
  822. .error(function(jqXHR, textStatus, errorThrown){
  823. self.showError();
  824. });
  825. }
  826. });
  827. var DownloadView = Backbone.View.extend({
  828. initialize : function() {
  829. currSearchResult.bind('change:numberOfRecord', this.onNumberOfRecordChanged, this);
  830. },
  831. render : function() {
  832. var currDownloadView = new DownloadEmailView({ el: $("#download_content") });
  833. currDownloadView.render();
  834. return this;
  835. },
  836. onNumberOfRecordChanged : function() {
  837. this.render();
  838. }
  839. });
  840. var DisplayMapView = Backbone.View.extend({
  841. displayMapTemplate : _.template($('#display_template_map').html()),
  842. events: {
  843. },
  844. initialize : function() {
  845. //el could be set through the caller
  846. this.setElement(this.el);
  847. },
  848. render : function() {
  849. this.$el.html(this.displayMapTemplate());
  850. }
  851. });
  852. var DisplayTableView = Backbone.View.extend({
  853. displayTableTemplate : _.template($('#display_template_table').html()),
  854. initialize : function() {
  855. //el could be set through the caller
  856. this.setElement(this.el);
  857. },
  858. render : function() {
  859. this.$el.html(this.displayTableTemplate());
  860. }
  861. });
  862. var DisplayView = Backbone.View.extend({
  863. initialize : function() {
  864. this.render();
  865. },
  866. render : function() {
  867. var currDisplayView,
  868. $form = $('form',$('#search')),
  869. currView = $('input[name=view]',$form).val();
  870. currDisplayView = (currView === 'table') ? new DisplayTableView({ el: $("#display") }) : new DisplayMapView({ el: $("#display") });
  871. currDisplayView.render();
  872. return this;
  873. }
  874. });
  875. function init() {
  876. new CurrentFiltersView();
  877. new FilterSelectionView();
  878. new FilterFieldSelectionView({ el: $('#filter_select') });
  879. new DownloadView();
  880. new DisplayView();
  881. }
  882. //Public methods
  883. return {
  884. init: init,
  885. setNumberOfResult : setNumberOfResult,
  886. setAvailableSearchFields : setAvailableSearchFields,
  887. initActiveFilters : initActiveFilters,
  888. addActiveFilter : addActiveFilter,
  889. updateActiveFilter : updateActiveFilter,
  890. bindToFilterList : bindToFilterList,
  891. removeFilter : removeFilter,
  892. getInitialFilterParamMap : getInitialFilterParamMap
  893. };
  894. }());