1/**
2 * Copyright (c) 2017 Google Inc. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you
5 * may not use this file except in compliance with the License. You may
6 * obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13 * implied. See the License for the specific language governing
14 * permissions and limitations under the License.
15 */
16
17(function ($) {
18  var _inequalityRegex = '(^)(<|>|<=|>=|=)?[ ]*?[0-9]+$';
19  var _inequalityHint = 'e.g. 5, >0, <=10';
20
21  function _validate(input, valueSet) {
22    var value = input.val();
23    if (valueSet.has(value) || !value) {
24      input.removeClass('invalid');
25    } else {
26      input.addClass('invalid');
27    }
28  }
29
30  function _createInput(key, config) {
31    var value = config.value;
32    var values = config.options.corpus;
33    var displayName = config.displayName;
34    var width = config.options.width || 's4';
35    var div = $('<div class="input-field col"></div>');
36    div.addClass(width);
37    var input = $('<input class="filter-input"></input>');
38    input.attr('type', config.options.type || 'text');
39    input.appendTo(div);
40    var label = $('<label></label>').text(displayName).appendTo(div);
41    if (value) {
42      input.attr('value', value);
43      label.addClass('active');
44    }
45    if (config.options.validate == 'inequality') {
46      input.addClass('validate');
47      input.attr('pattern', _inequalityRegex);
48      input.attr('placeholder', _inequalityHint);
49      label.addClass('active');
50    }
51    input.focusout(function() {
52      config.value = input.val();
53    });
54    if (values && values.length > 0) {
55      var valueSet = new Set(values);
56      input.sizedAutocomplete({
57        source: values,
58        classes: {
59          'ui-autocomplete': 'card search-bar-menu'
60        }
61      });
62      input.focusout(function() {
63        _validate(input, valueSet);
64      });
65    }
66    if (values && values.length > 0 && value) {
67      _validate(input, valueSet);
68    }
69    return div;
70  }
71
72  function _verifyCheckboxes(checkboxes, refreshObject) {
73    var oneChecked = checkboxes.presubmit || checkboxes.postsubmit;
74    if (!oneChecked) {
75      refreshObject.addClass('disabled');
76    } else {
77      refreshObject.removeClass('disabled');
78    }
79  }
80
81  function _createRunTypeBoxes(checkboxes, refreshObject) {
82    var container = $('<div class="run-type-wrapper col s12"></div>');
83    var presubmit = $('<input type="checkbox" id="presubmit"></input>');
84    presubmit.appendTo(container);
85    if (checkboxes.presubmit) {
86      presubmit.prop('checked', true);
87    }
88    container.append('<label for="presubmit">Presubmit</label>');
89    var postsubmit = $('<input type="checkbox" id="postsubmit"></input>');
90    postsubmit.appendTo(container);
91    if (checkboxes.postsubmit) {
92      postsubmit.prop('checked', true);
93    }
94    container.append('<label for="postsubmit">Postsubmit</label>');
95    presubmit.change(function() {
96      checkboxes.presubmit = presubmit.prop('checked');
97      _verifyCheckboxes(checkboxes, refreshObject);
98    });
99    postsubmit.change(function() {
100      checkboxes.postsubmit = postsubmit.prop('checked');
101      _verifyCheckboxes(checkboxes, refreshObject);
102    });
103    return container;
104  }
105
106  function _expand(
107      container, filters, checkboxes, onRefreshCallback, animate=true) {
108    var wrapper = $('<div class="search-wrapper"></div>');
109    var col = $('<div class="col s9"></div>');
110    col.appendTo(wrapper);
111    Object.keys(filters).forEach(function(key) {
112      col.append(_createInput(key, filters[key]));
113    });
114    var refreshCol = $('<div class="col s3 refresh-wrapper"></div>');
115    var refresh = $('<a class="btn-floating btn-medium red right waves-effect waves-light"></a>')
116      .append($('<i class="medium material-icons">cached</i>'))
117      .appendTo(refreshCol);
118    refresh.click(onRefreshCallback);
119    refreshCol.appendTo(wrapper);
120    if (Object.keys(checkboxes).length > 0) {
121      col.append(_createRunTypeBoxes(checkboxes, refresh));
122    }
123    if (animate) {
124      wrapper.hide().appendTo(container).slideDown({
125        duration: 200,
126        easing: "easeOutQuart",
127        queue: false
128      });
129    } else {
130      wrapper.appendTo(container);
131    }
132    container.addClass('expanded')
133  }
134
135  function _renderHeader(
136      container, label, value, filters, checkboxes, expand, onRefreshCallback) {
137    var div = $('<div class="row card search-bar"></div>');
138    var wrapper = $('<div class="header-wrapper"></div>');
139    var header = $('<h5 class="section-header"></h5>');
140    $('<b></b>').text(label).appendTo(header);
141    $('<span></span>').text(value).appendTo(header);
142    header.appendTo(wrapper);
143    var iconWrapper = $('<div class="search-icon-wrapper"></div>');
144    $('<i class="material-icons">search</i>').appendTo(iconWrapper);
145    iconWrapper.appendTo(wrapper);
146    wrapper.appendTo(div);
147    if (expand) {
148      _expand(div, filters, checkboxes, onRefreshCallback, false);
149    } else {
150      var expanded = false;
151      iconWrapper.click(function() {
152        if (expanded) return;
153        expanded = true;
154        _expand(div, filters, checkboxes, onRefreshCallback);
155      });
156    }
157    div.appendTo(container);
158  }
159
160  function _addFilter(filters, displayName, keyName, options, defaultValue) {
161    filters[keyName] = {};
162    filters[keyName].displayName = displayName;
163    filters[keyName].value = defaultValue;
164    filters[keyName].options = options;
165  }
166
167  function _getOptionString(filters, checkboxes) {
168    var args = Object.keys(filters).reduce(function(acc, key) {
169      if (filters[key].value) {
170        return acc + '&' + key + '=' + encodeURIComponent(filters[key].value);
171      }
172      return acc;
173    }, '');
174    if (checkboxes.presubmit != undefined && checkboxes.presubmit) {
175      args += '&showPresubmit='
176    }
177    if (checkboxes.postsubmit != undefined && checkboxes.postsubmit) {
178      args += '&showPostsubmit='
179    }
180    return args;
181  }
182
183  /**
184   * Create a search header element.
185   * @param label The header label.
186   * @param value The value to display next to the label.
187   * @param onRefreshCallback The function to call on refresh.
188   */
189  $.fn.createSearchHeader = function(label, value, onRefreshCallback) {
190    var self = $(this);
191    $.widget('custom.sizedAutocomplete', $.ui.autocomplete, {
192      _resizeMenu : function() {
193        this.menu.element.outerWidth($('.search-bar .filter-input').width());
194      }
195    });
196    var filters = {};
197    var checkboxes = {};
198    var expandOnRender = false;
199    var displayed = false;
200    return {
201      /**
202       * Add a filter to the display.
203       * @param displayName The input placeholder/label text.
204       * @param keyName The URL key to use for the filter options.
205       * @param options A dict of additional options (e.g. width, type).
206       * @param defaultValue A default filter value.
207       */
208      addFilter : function(displayName, keyName, options, defaultValue) {
209        if (displayed) return;
210        _addFilter(filters, displayName, keyName, options, defaultValue);
211        if (defaultValue) expandOnRender = true;
212      },
213      /**
214       * Enable run type checkboxes in the filter options.
215       *
216       * This will display two checkboxes for selecting pre-/postsubmit runs.
217       * @param showPresubmit True if presubmit runs are selected.
218       * @param showPostsubmit True if postsubmit runs are selected.
219       *
220       */
221      addRunTypeCheckboxes: function(showPresubmit, showPostsubmit) {
222        if (displayed) return;
223        checkboxes['presubmit'] = showPresubmit;
224        checkboxes['postsubmit'] = showPostsubmit;
225        if (!showPostsubmit || showPresubmit) {
226          expandOnRender = true;
227        }
228      },
229      /**
230       * Display the created search bar.
231       *
232       * This must be called after filters have been added. After displaying, no
233       * modifications to the filter options will take effect.
234       */
235      display : function() {
236        displayed = true;
237        _renderHeader(
238          self, label, value, filters, checkboxes, expandOnRender,
239          onRefreshCallback);
240      },
241      /**
242       * Get the URL arguments string for the current set of filters.
243       * @returns a URI-encoded component with the search bar keys and values.
244       */
245      args : function () {
246        return _getOptionString(filters, checkboxes);
247      }
248    }
249  }
250
251})(jQuery);
252