bootstrap-table-filter-control.js 17 KB

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