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