bootstrap-table-filter-control.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. /**
  2. * @author: Dennis Hernández
  3. * @version: v3.0.1
  4. */
  5. import * as UtilsFilterControl from './utils.js'
  6. const Utils = $.fn.bootstrapTable.utils
  7. $.extend($.fn.bootstrapTable.defaults, {
  8. filterControl: false,
  9. filterControlVisible: true,
  10. filterControlMultipleSearch: false,
  11. filterControlMultipleSearchDelimiter: ',',
  12. // eslint-disable-next-line no-unused-vars
  13. onColumnSearch (field, text) {
  14. return false
  15. },
  16. onCreatedControls () {
  17. return false
  18. },
  19. alignmentSelectControlOptions: undefined,
  20. filterTemplate: {
  21. input (that, column, placeholder, value) {
  22. return Utils.sprintf(
  23. '<input type="search" class="%s bootstrap-table-filter-control-%s search-input" style="width: 100%;" placeholder="%s" value="%s">',
  24. UtilsFilterControl.getInputClass(that),
  25. column.field,
  26. 'undefined' === typeof placeholder ? '' : placeholder,
  27. 'undefined' === typeof value ? '' : value
  28. )
  29. },
  30. select (that, column) {
  31. return Utils.sprintf(
  32. '<select class="%s bootstrap-table-filter-control-%s %s" %s style="width: 100%;" dir="%s"></select>',
  33. UtilsFilterControl.getInputClass(that, true),
  34. column.field,
  35. '',
  36. '',
  37. UtilsFilterControl.getDirectionOfSelectOptions(
  38. that.options.alignmentSelectControlOptions
  39. )
  40. )
  41. },
  42. datepicker (that, column, value) {
  43. return Utils.sprintf(
  44. '<input type="date" class="%s date-filter-control bootstrap-table-filter-control-%s" style="width: 100%;" value="%s">',
  45. UtilsFilterControl.getInputClass(that),
  46. column.field,
  47. 'undefined' === typeof value ? '' : value
  48. )
  49. }
  50. },
  51. searchOnEnterKey: false,
  52. showFilterControlSwitch: false,
  53. sortSelectOptions: false,
  54. // internal variables
  55. _valuesFilterControl: [],
  56. _initialized: false,
  57. _isRendering: false,
  58. _usingMultipleSelect: false
  59. })
  60. $.extend($.fn.bootstrapTable.columnDefaults, {
  61. filterControl: undefined, // input, select, datepicker
  62. filterControlMultipleSelect: false,
  63. filterControlMultipleSelectOptions: {},
  64. filterDataCollector: undefined,
  65. filterData: undefined,
  66. filterDatepickerOptions: {},
  67. filterStrictSearch: false,
  68. filterStartsWithSearch: false,
  69. filterControlPlaceholder: '',
  70. filterDefault: '',
  71. filterOrderBy: 'asc', // asc || desc
  72. filterCustomSearch: undefined
  73. })
  74. $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
  75. 'column-search.bs.table': 'onColumnSearch',
  76. 'created-controls.bs.table': 'onCreatedControls'
  77. })
  78. $.extend($.fn.bootstrapTable.defaults.icons, {
  79. filterControlSwitchHide: {
  80. bootstrap3: 'glyphicon-zoom-out icon-zoom-out',
  81. bootstrap5: 'bi-zoom-out',
  82. materialize: 'zoom_out'
  83. }[$.fn.bootstrapTable.theme] || 'fa-search-minus',
  84. filterControlSwitchShow: {
  85. bootstrap3: 'glyphicon-zoom-in icon-zoom-in',
  86. bootstrap5: 'bi-zoom-in',
  87. materialize: 'zoom_in'
  88. }[$.fn.bootstrapTable.theme] || 'fa-search-plus'
  89. })
  90. $.extend($.fn.bootstrapTable.locales, {
  91. formatFilterControlSwitch () {
  92. return 'Hide/Show controls'
  93. },
  94. formatFilterControlSwitchHide () {
  95. return 'Hide controls'
  96. },
  97. formatFilterControlSwitchShow () {
  98. return 'Show controls'
  99. }
  100. })
  101. $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales)
  102. $.extend($.fn.bootstrapTable.defaults, {
  103. formatClearSearch () {
  104. return 'Clear filters'
  105. }
  106. })
  107. $.fn.bootstrapTable.methods.push('triggerSearch')
  108. $.fn.bootstrapTable.methods.push('clearFilterControl')
  109. $.fn.bootstrapTable.methods.push('toggleFilterControl')
  110. $.BootstrapTable = class extends $.BootstrapTable {
  111. init () {
  112. // Make sure that the filterControl option is set
  113. if (this.options.filterControl) {
  114. // Make sure that the internal variables are set correctly
  115. this._valuesFilterControl = []
  116. this._initialized = false
  117. this._usingMultipleSelect = false
  118. this._isRendering = false
  119. this.$el
  120. .on('reset-view.bs.table', Utils.debounce(() => {
  121. UtilsFilterControl.initFilterSelectControls(this)
  122. UtilsFilterControl.setValues(this)
  123. }, 3))
  124. .on('toggle.bs.table', Utils.debounce((_, cardView) => {
  125. this._initialized = false
  126. if (!cardView) {
  127. UtilsFilterControl.initFilterSelectControls(this)
  128. UtilsFilterControl.setValues(this)
  129. this._initialized = true
  130. }
  131. }, 1))
  132. .on('post-header.bs.table', Utils.debounce(() => {
  133. UtilsFilterControl.initFilterSelectControls(this)
  134. UtilsFilterControl.setValues(this)
  135. }, 3))
  136. .on('column-switch.bs.table', Utils.debounce(() => {
  137. UtilsFilterControl.setValues(this)
  138. if (this.options.height) {
  139. this.fitHeader()
  140. }
  141. }, 1))
  142. .on('post-body.bs.table', Utils.debounce(() => {
  143. if (this.options.height && !this.options.filterControlContainer && this.options.filterControlVisible) {
  144. UtilsFilterControl.fixHeaderCSS(this)
  145. }
  146. this.$tableLoading.css('top', this.$header.outerHeight() + 1)
  147. }, 1))
  148. .on('all.bs.table', () => {
  149. UtilsFilterControl.syncHeaders(this)
  150. })
  151. }
  152. super.init()
  153. }
  154. initBody () {
  155. super.initBody()
  156. if (!this.options.filterControl) {
  157. return
  158. }
  159. setTimeout(() => {
  160. UtilsFilterControl.initFilterSelectControls(this)
  161. UtilsFilterControl.setValues(this)
  162. }, 3)
  163. }
  164. load (data) {
  165. super.load(data)
  166. if (!this.options.filterControl) {
  167. return
  168. }
  169. UtilsFilterControl.createControls(this, UtilsFilterControl.getControlContainer(this))
  170. UtilsFilterControl.setValues(this)
  171. }
  172. initHeader () {
  173. super.initHeader()
  174. if (!this.options.filterControl) {
  175. return
  176. }
  177. UtilsFilterControl.createControls(this, UtilsFilterControl.getControlContainer(this))
  178. this._initialized = true
  179. }
  180. initSearch () {
  181. const that = this
  182. const filterPartial = $.isEmptyObject(that.filterColumnsPartial) ? null : that.filterColumnsPartial
  183. super.initSearch()
  184. if (this.options.sidePagination === 'server' || filterPartial === null) {
  185. return
  186. }
  187. // Check partial column filter
  188. that.data = filterPartial ?
  189. that.data.filter((item, i) => {
  190. const itemIsExpected = []
  191. const keys1 = Object.keys(item)
  192. const keys2 = Object.keys(filterPartial)
  193. const keys = keys1.concat(keys2.filter(item => !keys1.includes(item)))
  194. keys.forEach(key => {
  195. const thisColumn = that.columns[that.fieldsColumnsIndex[key]]
  196. const rawFilterValue = (filterPartial[key] || '')
  197. let filterValue = rawFilterValue.toLowerCase()
  198. let value = Utils.unescapeHTML(Utils.getItemField(item, key, false))
  199. let tmpItemIsExpected
  200. if (this.options.searchAccentNeutralise) {
  201. filterValue = Utils.normalizeAccent(filterValue)
  202. }
  203. let filterValues = [filterValue]
  204. if (
  205. this.options.filterControlMultipleSearch
  206. ) {
  207. filterValues = filterValue.split(this.options.filterControlMultipleSearchDelimiter)
  208. }
  209. filterValues.forEach(filterValue => {
  210. if (tmpItemIsExpected === true) {
  211. return
  212. }
  213. filterValue = filterValue.trim()
  214. if (filterValue === '') {
  215. tmpItemIsExpected = true
  216. } else {
  217. // Fix #142: search use formatted data
  218. if (thisColumn) {
  219. if (thisColumn.searchFormatter || thisColumn._forceFormatter) {
  220. value = $.fn.bootstrapTable.utils.calculateObjectValue(
  221. thisColumn,
  222. that.header.formatters[$.inArray(key, that.header.fields)],
  223. [value, item, i],
  224. value
  225. )
  226. }
  227. }
  228. if ($.inArray(key, that.header.fields) !== -1) {
  229. if (value === undefined || value === null) {
  230. tmpItemIsExpected = false
  231. } else if (typeof value === 'object' && thisColumn.filterCustomSearch) {
  232. itemIsExpected.push(that.isValueExpected(rawFilterValue, value, thisColumn, key))
  233. } else if (typeof value === 'object' && Array.isArray(value)) {
  234. value.forEach(objectValue => {
  235. if (tmpItemIsExpected) {
  236. return
  237. }
  238. tmpItemIsExpected = that.isValueExpected(filterValue, objectValue, thisColumn, key)
  239. })
  240. } else if (typeof value === 'object' && !Array.isArray(value)) {
  241. Object.values(value).forEach(objectValue => {
  242. if (tmpItemIsExpected) {
  243. return
  244. }
  245. tmpItemIsExpected = that.isValueExpected(filterValue, objectValue, thisColumn, key)
  246. })
  247. } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
  248. tmpItemIsExpected = that.isValueExpected(filterValue, value, thisColumn, key)
  249. }
  250. }
  251. }
  252. })
  253. itemIsExpected.push(tmpItemIsExpected)
  254. })
  255. return !itemIsExpected.includes(false)
  256. }) :
  257. that.data
  258. that.unsortedData = [...that.data]
  259. }
  260. isValueExpected (searchValue, value, column, key) {
  261. let tmpItemIsExpected = false
  262. if (
  263. column.filterStrictSearch ||
  264. (column.filterControl === 'select' && column.passed.filterStrictSearch !== false)
  265. ) {
  266. tmpItemIsExpected = value.toString().toLowerCase() === searchValue.toString().toLowerCase()
  267. } else if (column.filterStartsWithSearch) {
  268. tmpItemIsExpected = (`${value}`).toLowerCase().indexOf(searchValue) === 0
  269. } else if (column.filterControl === 'datepicker') {
  270. tmpItemIsExpected = new Date(value).getTime() === new Date(searchValue).getTime()
  271. } else if (this.options.regexSearch) {
  272. tmpItemIsExpected = Utils.regexCompare(value, searchValue)
  273. } else {
  274. tmpItemIsExpected = (`${value}`).toLowerCase().includes(searchValue)
  275. }
  276. const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
  277. const matches = largerSmallerEqualsRegex.exec(searchValue)
  278. if (matches) {
  279. const operator = matches[1] || `${matches[5]}l`
  280. const comparisonValue = matches[2] || matches[3]
  281. const int = parseInt(value, 10)
  282. const comparisonInt = parseInt(comparisonValue, 10)
  283. switch (operator) {
  284. case '>':
  285. case '<l':
  286. tmpItemIsExpected = int > comparisonInt
  287. break
  288. case '<':
  289. case '>l':
  290. tmpItemIsExpected = int < comparisonInt
  291. break
  292. case '<=':
  293. case '=<':
  294. case '>=l':
  295. case '=>l':
  296. tmpItemIsExpected = int <= comparisonInt
  297. break
  298. case '>=':
  299. case '=>':
  300. case '<=l':
  301. case '=<l':
  302. tmpItemIsExpected = int >= comparisonInt
  303. break
  304. default:
  305. break
  306. }
  307. }
  308. if (column.filterCustomSearch) {
  309. const customSearchResult = Utils.calculateObjectValue(column, column.filterCustomSearch, [searchValue, value, key, this.options.data], true)
  310. if (customSearchResult !== null) {
  311. tmpItemIsExpected = customSearchResult
  312. }
  313. }
  314. return tmpItemIsExpected
  315. }
  316. initColumnSearch (filterColumnsDefaults) {
  317. UtilsFilterControl.cacheValues(this)
  318. if (filterColumnsDefaults) {
  319. this.filterColumnsPartial = filterColumnsDefaults
  320. this.updatePagination()
  321. // eslint-disable-next-line guard-for-in
  322. for (const filter in filterColumnsDefaults) {
  323. this.trigger('column-search', filter, filterColumnsDefaults[filter])
  324. }
  325. }
  326. }
  327. initToolbar () {
  328. this.showToolbar = this.showToolbar || this.options.showFilterControlSwitch
  329. this.showSearchClearButton = this.options.filterControl && this.options.showSearchClearButton
  330. if (this.options.showFilterControlSwitch) {
  331. this.buttons = Object.assign(this.buttons, {
  332. filterControlSwitch: {
  333. text: this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow(),
  334. icon: this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow,
  335. event: this.toggleFilterControl,
  336. attributes: {
  337. 'aria-label': this.options.formatFilterControlSwitch(),
  338. title: this.options.formatFilterControlSwitch()
  339. }
  340. }
  341. })
  342. }
  343. super.initToolbar()
  344. }
  345. resetSearch (text) {
  346. if (this.options.filterControl && this.options.showSearchClearButton) {
  347. this.clearFilterControl()
  348. }
  349. super.resetSearch(text)
  350. }
  351. clearFilterControl () {
  352. if (!this.options.filterControl) {
  353. return
  354. }
  355. const that = this
  356. const table = this.$el.closest('table')
  357. const cookies = UtilsFilterControl.collectBootstrapTableFilterCookies()
  358. const controls = UtilsFilterControl.getSearchControls(that)
  359. // const search = Utils.getSearchInput(this)
  360. let hasValues = false
  361. let timeoutId = 0
  362. // Clear cache values
  363. $.each(that._valuesFilterControl, (i, item) => {
  364. hasValues = hasValues ? true : item.value !== ''
  365. item.value = ''
  366. })
  367. // Clear controls in UI
  368. $.each(controls, (i, item) => {
  369. item.value = ''
  370. })
  371. // Cache controls again
  372. UtilsFilterControl.setValues(that)
  373. // clear cookies once the filters are clean
  374. clearTimeout(timeoutId)
  375. timeoutId = setTimeout(() => {
  376. if (cookies && cookies.length > 0) {
  377. $.each(cookies, (i, item) => {
  378. if (that.deleteCookie !== undefined) {
  379. that.deleteCookie(item)
  380. }
  381. })
  382. }
  383. }, that.options.searchTimeOut)
  384. // If there is not any value in the controls exit this method
  385. if (!hasValues) {
  386. return
  387. }
  388. // Clear each type of filter if it exists.
  389. // Requires the body to reload each time a type of filter is found because we never know
  390. // which ones are going to be present.
  391. if (controls.length > 0) {
  392. this.filterColumnsPartial = {}
  393. controls.eq(0).trigger(this.tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 })
  394. /* controls.each(function () {
  395. $(this).trigger(this.tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 })
  396. })*/
  397. } else {
  398. return
  399. }
  400. /* if (search.length > 0) {
  401. that.resetSearch('fc')
  402. }*/
  403. // use the default sort order if it exists. do nothing if it does not
  404. if (that.options.sortName !== table.data('sortName') || that.options.sortOrder !== table.data('sortOrder')) {
  405. const sorter = this.$header.find(Utils.sprintf('[data-field="%s"]', $(controls[0]).closest('table').data('sortName')))
  406. if (sorter.length > 0) {
  407. that.onSort({ type: 'keypress', currentTarget: sorter })
  408. $(sorter).find('.sortable').trigger('click')
  409. }
  410. }
  411. }
  412. // EVENTS
  413. onColumnSearch ({ currentTarget, keyCode }) {
  414. if (UtilsFilterControl.isKeyAllowed(keyCode)) {
  415. return
  416. }
  417. UtilsFilterControl.cacheValues(this)
  418. // Cookie extension support
  419. if (!this.options.cookie) {
  420. this.options.pageNumber = 1
  421. } else {
  422. // Force call the initServer method in Cookie extension
  423. this._filterControlValuesLoaded = true
  424. }
  425. if ($.isEmptyObject(this.filterColumnsPartial)) {
  426. this.filterColumnsPartial = {}
  427. }
  428. // If searchOnEnterKey is set to true, then we need to iterate over all controls and grab their values.
  429. const controls = this.options.searchOnEnterKey ? UtilsFilterControl.getSearchControls(this).toArray() : [currentTarget]
  430. controls.forEach(element => {
  431. const $element = $(element)
  432. const elementValue = $element.val()
  433. const text = elementValue ? elementValue.trim() : ''
  434. const $field = $element.closest('[data-field]').data('field')
  435. this.trigger('column-search', $field, text)
  436. if (text) {
  437. this.filterColumnsPartial[$field] = text
  438. } else {
  439. delete this.filterColumnsPartial[$field]
  440. }
  441. })
  442. this.onSearch({ currentTarget }, false)
  443. }
  444. toggleFilterControl () {
  445. this.options.filterControlVisible = !this.options.filterControlVisible
  446. // Controls in original header or container.
  447. const $filterControls = UtilsFilterControl.getControlContainer(this).find('.filter-control, .no-filter-control')
  448. if (this.options.filterControlVisible) {
  449. $filterControls.show()
  450. } else {
  451. $filterControls.hide()
  452. this.clearFilterControl()
  453. }
  454. // Controls in fixed header
  455. if (this.options.height) {
  456. const $fixedControls = $('.fixed-table-header table thead').find('.filter-control, .no-filter-control')
  457. $fixedControls.toggle(this.options.filterControlVisible)
  458. UtilsFilterControl.fixHeaderCSS(this)
  459. }
  460. const icon = this.options.showButtonIcons ? this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow : ''
  461. const text = this.options.showButtonText ? this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow() : ''
  462. this.$toolbar.find('>.columns').find('.filter-control-switch')
  463. .html(`${Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, icon) } ${ text}`)
  464. }
  465. triggerSearch () {
  466. const searchControls = UtilsFilterControl.getSearchControls(this)
  467. searchControls.each(function () {
  468. const $element = $(this)
  469. if ($element.is('select')) {
  470. $element.trigger('change')
  471. } else {
  472. $element.trigger('keyup')
  473. }
  474. })
  475. }
  476. _toggleColumn (index, checked, needUpdate) {
  477. this._initialized = false
  478. super._toggleColumn(index, checked, needUpdate)
  479. UtilsFilterControl.syncHeaders(this)
  480. }
  481. }