bootstrap-table-filter-control.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. /**
  2. * @author: Dennis Hernández
  3. * @webSite: http://djhvscf.github.io/Blog
  4. * @version: v2.3.0
  5. */
  6. import * as UtilsFilterControl from './utils.js'
  7. const Utils = $.fn.bootstrapTable.utils
  8. $.extend($.fn.bootstrapTable.defaults, {
  9. filterControl: false,
  10. filterControlVisible: true,
  11. onColumnSearch (field, text) {
  12. return false
  13. },
  14. onCreatedControls () {
  15. return false
  16. },
  17. alignmentSelectControlOptions: undefined,
  18. filterTemplate: {
  19. input (that, field, placeholder, value) {
  20. return Utils.sprintf(
  21. '<input type="search" class="form-control bootstrap-table-filter-control-%s search-input" style="width: 100%;" placeholder="%s" value="%s">',
  22. field,
  23. 'undefined' === typeof placeholder ? '' : placeholder,
  24. 'undefined' === typeof value ? '' : value
  25. )
  26. },
  27. select ({options}, field) {
  28. return Utils.sprintf(
  29. '<select class="form-control bootstrap-table-filter-control-%s" style="width: 100%;" dir="%s"></select>',
  30. field,
  31. UtilsFilterControl.getDirectionOfSelectOptions(
  32. options.alignmentSelectControlOptions
  33. )
  34. )
  35. },
  36. datepicker (that, field, value) {
  37. return Utils.sprintf(
  38. '<input type="text" class="form-control date-filter-control bootstrap-table-filter-control-%s" style="width: 100%;" value="%s">',
  39. field,
  40. 'undefined' === typeof value ? '' : value
  41. )
  42. }
  43. },
  44. disableControlWhenSearch: false,
  45. searchOnEnterKey: false,
  46. showFilterControlSwitch: false,
  47. // internal variables
  48. valuesFilterControl: []
  49. })
  50. $.extend($.fn.bootstrapTable.columnDefaults, {
  51. filterControl: undefined, // input, select, datepicker
  52. filterDataCollector: undefined,
  53. filterData: undefined,
  54. filterDatepickerOptions: undefined,
  55. filterStrictSearch: false,
  56. filterStartsWithSearch: false,
  57. filterControlPlaceholder: '',
  58. filterDefault: '',
  59. filterOrderBy: 'asc' // asc || desc
  60. })
  61. $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
  62. 'column-search.bs.table': 'onColumnSearch',
  63. 'created-controls.bs.table': 'onCreatedControls'
  64. })
  65. $.extend($.fn.bootstrapTable.defaults.icons, {
  66. clear: {
  67. bootstrap3: 'glyphicon-trash icon-clear'
  68. }[$.fn.bootstrapTable.theme] || 'fa-trash',
  69. filterControlSwitchHide: {
  70. bootstrap3: 'glyphicon-zoom-out icon-zoom-out',
  71. materialize: 'zoom_out'
  72. }[$.fn.bootstrapTable.theme] || 'fa-search-minus',
  73. filterControlSwitchShow: {
  74. bootstrap3: 'glyphicon-zoom-in icon-zoom-in',
  75. materialize: 'zoom_in'
  76. }[$.fn.bootstrapTable.theme] || 'fa-search-plus'
  77. })
  78. $.extend($.fn.bootstrapTable.locales, {
  79. formatFilterControlSwitch () {
  80. return 'Hide/Show controls'
  81. },
  82. formatFilterControlSwitchHide () {
  83. return 'Hide controls'
  84. },
  85. formatFilterControlSwitchShow () {
  86. return 'Show controls'
  87. }
  88. })
  89. $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales)
  90. $.extend($.fn.bootstrapTable.defaults, {
  91. formatClearSearch () {
  92. return 'Clear filters'
  93. }
  94. })
  95. $.fn.bootstrapTable.methods.push('triggerSearch')
  96. $.fn.bootstrapTable.methods.push('clearFilterControl')
  97. $.fn.bootstrapTable.methods.push('toggleFilterControl')
  98. $.BootstrapTable = class extends $.BootstrapTable {
  99. init () {
  100. // Make sure that the filterControl option is set
  101. if (this.options.filterControl) {
  102. // Make sure that the internal variables are set correctly
  103. this.options.valuesFilterControl = []
  104. this.$el
  105. .on('reset-view.bs.table', () => {
  106. // Create controls on $tableHeader if the height is set
  107. if (!this.options.height) {
  108. return
  109. }
  110. // Avoid recreate the controls
  111. const $controlContainer = UtilsFilterControl.getControlContainer(this)
  112. if ($controlContainer.find('select').length > 0 || $controlContainer.find('input:not([type="checkbox"]):not([type="radio"])').length > 0) {
  113. return
  114. }
  115. UtilsFilterControl.createControls(this, $controlContainer)
  116. })
  117. .on('post-header.bs.table', () => {
  118. UtilsFilterControl.setValues(this)
  119. })
  120. .on('post-body.bs.table', () => {
  121. if (this.options.height && !this.options.filterControlContainer) {
  122. UtilsFilterControl.fixHeaderCSS(this)
  123. }
  124. this.$tableLoading.css('top', this.$header.outerHeight() + 1)
  125. })
  126. .on('column-switch.bs.table', () => {
  127. UtilsFilterControl.setValues(this)
  128. })
  129. .on('load-success.bs.table', () => {
  130. this.enableControls(true)
  131. })
  132. .on('load-error.bs.table', () => {
  133. this.enableControls(true)
  134. })
  135. }
  136. super.init()
  137. }
  138. initHeader () {
  139. super.initHeader()
  140. if (!this.options.filterControl || this.options.height) {
  141. return
  142. }
  143. UtilsFilterControl.createControls(this, UtilsFilterControl.getControlContainer(this))
  144. }
  145. initBody () {
  146. super.initBody()
  147. UtilsFilterControl.syncControls(this)
  148. UtilsFilterControl.initFilterSelectControls(this)
  149. }
  150. initSearch () {
  151. const that = this
  152. const fp = $.isEmptyObject(that.filterColumnsPartial) ? null : that.filterColumnsPartial
  153. super.initSearch()
  154. if (this.options.sidePagination === 'server' || fp === null) {
  155. return
  156. }
  157. // Check partial column filter
  158. that.data = fp
  159. ? that.data.filter((item, i) => {
  160. const itemIsExpected = []
  161. const keys1 = Object.keys(item)
  162. const keys2 = Object.keys(fp)
  163. const keys = keys1.concat(keys2.filter(item => !keys1.includes(item)))
  164. keys.forEach(key => {
  165. const thisColumn = that.columns[that.fieldsColumnsIndex[key]]
  166. const fval = (fp[key] || '').toLowerCase()
  167. let value = Utils.getItemField(item, key, false)
  168. let tmpItemIsExpected
  169. if (fval === '') {
  170. tmpItemIsExpected = true
  171. } else {
  172. // Fix #142: search use formatted data
  173. if (thisColumn && thisColumn.searchFormatter) {
  174. value = $.fn.bootstrapTable.utils.calculateObjectValue(
  175. that.header,
  176. that.header.formatters[$.inArray(key, that.header.fields)],
  177. [value, item, i],
  178. value
  179. )
  180. }
  181. if ($.inArray(key, that.header.fields) !== -1) {
  182. if (value === undefined || value === null) {
  183. tmpItemIsExpected = false
  184. } else if (typeof value === 'object') {
  185. value.forEach((objectValue) => {
  186. if (tmpItemIsExpected) {
  187. return
  188. }
  189. if (this.options.searchAccentNeutralise) {
  190. objectValue = Utils.normalizeAccent(objectValue)
  191. }
  192. tmpItemIsExpected = that.isValueExpected(fval, objectValue, thisColumn, key)
  193. })
  194. } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
  195. if (this.options.searchAccentNeutralise) {
  196. value = Utils.normalizeAccent(value)
  197. }
  198. tmpItemIsExpected = that.isValueExpected(fval, value, thisColumn, key)
  199. }
  200. }
  201. }
  202. itemIsExpected.push(tmpItemIsExpected)
  203. })
  204. return !itemIsExpected.includes(false)
  205. })
  206. : that.data
  207. that.unsortedData = [...that.data]
  208. }
  209. isValueExpected (searchValue, value, column, key) {
  210. let tmpItemIsExpected = false
  211. if (column.filterStrictSearch) {
  212. tmpItemIsExpected = value.toString().toLowerCase() === searchValue.toString().toLowerCase()
  213. } else if (column.filterStartsWithSearch) {
  214. tmpItemIsExpected = (`${value}`).toLowerCase().indexOf(searchValue) === 0
  215. } else {
  216. tmpItemIsExpected = (`${value}`).toLowerCase().includes(searchValue)
  217. }
  218. const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
  219. const matches = largerSmallerEqualsRegex.exec(searchValue)
  220. if (matches) {
  221. const operator = matches[1] || `${matches[5]}l`
  222. const comparisonValue = matches[2] || matches[3]
  223. const int = parseInt(value, 10)
  224. const comparisonInt = parseInt(comparisonValue, 10)
  225. switch (operator) {
  226. case '>':
  227. case '<l':
  228. tmpItemIsExpected = int > comparisonInt
  229. break
  230. case '<':
  231. case '>l':
  232. tmpItemIsExpected = int < comparisonInt
  233. break
  234. case '<=':
  235. case '=<':
  236. case '>=l':
  237. case '=>l':
  238. tmpItemIsExpected = int <= comparisonInt
  239. break
  240. case '>=':
  241. case '=>':
  242. case '<=l':
  243. case '=<l':
  244. tmpItemIsExpected = int >= comparisonInt
  245. break
  246. default:
  247. break
  248. }
  249. }
  250. if (column.filterCustomSearch) {
  251. const customSearchResult = Utils.calculateObjectValue(this, column.filterCustomSearch, [searchValue, value, key, this.options.data], true)
  252. if (customSearchResult !== null) {
  253. tmpItemIsExpected = customSearchResult
  254. }
  255. }
  256. return tmpItemIsExpected
  257. }
  258. initColumnSearch (filterColumnsDefaults) {
  259. UtilsFilterControl.copyValues(this)
  260. if (filterColumnsDefaults) {
  261. this.filterColumnsPartial = filterColumnsDefaults
  262. this.updatePagination()
  263. for (const filter in filterColumnsDefaults) {
  264. this.trigger('column-search', filter, filterColumnsDefaults[filter])
  265. }
  266. }
  267. }
  268. onColumnSearch ({currentTarget, keyCode}) {
  269. if ($.inArray(keyCode, [37, 38, 39, 40]) > -1) {
  270. return
  271. }
  272. UtilsFilterControl.copyValues(this)
  273. const text = $.trim($(currentTarget).val())
  274. const $field = $(currentTarget).closest('[data-field]').data('field')
  275. this.trigger('column-search', $field, text)
  276. if ($.isEmptyObject(this.filterColumnsPartial)) {
  277. this.filterColumnsPartial = {}
  278. }
  279. if (text) {
  280. this.filterColumnsPartial[$field] = text
  281. } else {
  282. delete this.filterColumnsPartial[$field]
  283. }
  284. this.options.pageNumber = 1
  285. this.enableControls(false)
  286. this.onSearch({currentTarget}, false)
  287. }
  288. initToolbar () {
  289. this.showToolbar = this.showToolbar || this.options.showFilterControlSwitch
  290. this.showSearchClearButton = this.options.filterControl && this.options.showSearchClearButton
  291. super.initToolbar()
  292. if (this.options.showFilterControlSwitch) {
  293. const $btnGroup = this.$toolbar.find('>.columns')
  294. let $btnFilterControlSwitch = $btnGroup.find('.filter-control-switch')
  295. if (!$btnFilterControlSwitch.length) {
  296. $btnFilterControlSwitch = $(`
  297. <button class="filter-control-switch ${this.constants.buttonsClass}"
  298. type="button" title="${this.options.formatFilterControlSwitch()}">
  299. ${this.options.showButtonIcons ? Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow) : ''}
  300. ${this.options.showButtonText ? this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow() : ''}
  301. </button>
  302. `).appendTo($btnGroup)
  303. $btnFilterControlSwitch.on('click', $.proxy(this.toggleFilterControl, this))
  304. }
  305. }
  306. }
  307. resetSearch (text) {
  308. if (this.options.filterControl && this.options.showSearchClearButton) {
  309. this.clearFilterControl()
  310. }
  311. super.resetSearch(text)
  312. }
  313. clearFilterControl () {
  314. if (this.options.filterControl) {
  315. const that = this
  316. const cookies = UtilsFilterControl.collectBootstrapCookies()
  317. const table = this.$el.closest('table')
  318. const controls = UtilsFilterControl.getSearchControls(that)
  319. const search = that.$toolbar.find('.search input')
  320. let hasValues = false
  321. let timeoutId = 0
  322. $.each(that.options.valuesFilterControl, (i, item) => {
  323. hasValues = hasValues ? true : item.value !== ''
  324. item.value = ''
  325. })
  326. $.each(that.options.filterControls, (i, item) => {
  327. item.text = ''
  328. })
  329. UtilsFilterControl.setValues(that)
  330. // clear cookies once the filters are clean
  331. clearTimeout(timeoutId)
  332. timeoutId = setTimeout(() => {
  333. if (cookies && cookies.length > 0) {
  334. $.each(cookies, (i, item) => {
  335. if (that.deleteCookie !== undefined) {
  336. that.deleteCookie(item)
  337. }
  338. })
  339. }
  340. }, that.options.searchTimeOut)
  341. // If there is not any value in the controls exit this method
  342. if (!hasValues) {
  343. return
  344. }
  345. // Clear each type of filter if it exists.
  346. // Requires the body to reload each time a type of filter is found because we never know
  347. // which ones are going to be present.
  348. if (controls.length > 0) {
  349. this.filterColumnsPartial = {}
  350. $(controls[0]).trigger(
  351. controls[0].tagName === 'INPUT' ? 'keyup' : 'change', {keyCode: 13}
  352. )
  353. } else {
  354. return
  355. }
  356. if (search.length > 0) {
  357. that.resetSearch()
  358. }
  359. // use the default sort order if it exists. do nothing if it does not
  360. if (that.options.sortName !== table.data('sortName') || that.options.sortOrder !== table.data('sortOrder')) {
  361. const sorter = this.$header.find(Utils.sprintf('[data-field="%s"]', $(controls[0]).closest('table').data('sortName')))
  362. if (sorter.length > 0) {
  363. that.onSort({type: 'keypress', currentTarget: sorter})
  364. $(sorter).find('.sortable').trigger('click')
  365. }
  366. }
  367. }
  368. }
  369. triggerSearch () {
  370. const searchControls = UtilsFilterControl.getSearchControls(this)
  371. searchControls.each(function () {
  372. const el = $(this)
  373. if (el.is('select')) {
  374. el.change()
  375. } else {
  376. el.keyup()
  377. }
  378. })
  379. }
  380. enableControls (enable) {
  381. if (this.options.disableControlWhenSearch && this.options.sidePagination === 'server') {
  382. const searchControls = UtilsFilterControl.getSearchControls(this)
  383. if (!enable) {
  384. searchControls.prop('disabled', 'disabled')
  385. } else {
  386. searchControls.removeProp('disabled')
  387. }
  388. }
  389. }
  390. toggleFilterControl () {
  391. this.options.filterControlVisible = !this.options.filterControlVisible
  392. const $filterControls = UtilsFilterControl.getControlContainer(this).find('.filter-control, .no-filter-control')
  393. if (this.options.filterControlVisible) {
  394. $filterControls.show()
  395. } else {
  396. $filterControls.hide()
  397. this.clearFilterControl()
  398. }
  399. const icon = this.options.showButtonIcons ? this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow : ''
  400. const text = this.options.showButtonText ? this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow() : ''
  401. this.$toolbar.find('>.columns').find('.filter-control-switch')
  402. .html(Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, icon) + ' ' + text)
  403. }
  404. }