bootstrap-table-filter-control.js 18 KB

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