bootstrap-table-filter-control.js 15 KB

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