bootstrap-table-filter-control.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  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. _usingMultipleSelect: false
  56. })
  57. $.extend($.fn.bootstrapTable.columnDefaults, {
  58. filterControl: undefined, // input, select, datepicker
  59. filterControlMultipleSelect: false,
  60. filterMultipleSelectOptions: {},
  61. filterDataCollector: undefined,
  62. filterData: undefined,
  63. filterDatepickerOptions: {},
  64. filterStrictSearch: false,
  65. filterStartsWithSearch: false,
  66. filterControlPlaceholder: '',
  67. filterDefault: '',
  68. filterOrderBy: 'asc' // asc || desc
  69. })
  70. $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
  71. 'column-search.bs.table': 'onColumnSearch',
  72. 'created-controls.bs.table': 'onCreatedControls'
  73. })
  74. $.extend($.fn.bootstrapTable.defaults.icons, {
  75. filterControlSwitchHide: {
  76. bootstrap3: 'glyphicon-zoom-out icon-zoom-out',
  77. bootstrap5: 'bi-zoom-out',
  78. materialize: 'zoom_out'
  79. }[$.fn.bootstrapTable.theme] || 'fa-search-minus',
  80. filterControlSwitchShow: {
  81. bootstrap3: 'glyphicon-zoom-in icon-zoom-in',
  82. bootstrap5: 'bi-zoom-in',
  83. materialize: 'zoom_in'
  84. }[$.fn.bootstrapTable.theme] || 'fa-search-plus'
  85. })
  86. $.extend($.fn.bootstrapTable.locales, {
  87. formatFilterControlSwitch () {
  88. return 'Hide/Show controls'
  89. },
  90. formatFilterControlSwitchHide () {
  91. return 'Hide controls'
  92. },
  93. formatFilterControlSwitchShow () {
  94. return 'Show controls'
  95. }
  96. })
  97. $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales)
  98. $.extend($.fn.bootstrapTable.defaults, {
  99. formatClearSearch () {
  100. return 'Clear filters'
  101. }
  102. })
  103. $.fn.bootstrapTable.methods.push('triggerSearch')
  104. $.fn.bootstrapTable.methods.push('clearFilterControl')
  105. $.fn.bootstrapTable.methods.push('toggleFilterControl')
  106. $.BootstrapTable = class extends $.BootstrapTable {
  107. init () {
  108. // Make sure that the filterControl option is set
  109. if (this.options.filterControl) {
  110. // Make sure that the internal variables are set correctly
  111. this._valuesFilterControl = []
  112. this._initialized = false
  113. this._usingMultipleSelect = false
  114. this.$el
  115. .on('reset-view.bs.table', () => {
  116. setTimeout(() => {
  117. UtilsFilterControl.initFilterSelectControls(this)
  118. UtilsFilterControl.setValues(this)
  119. }, 2)
  120. })
  121. .on('toggle.bs.table', (_, cardView) => {
  122. this._initialized = false
  123. if (!cardView) {
  124. UtilsFilterControl.initFilterSelectControls(this)
  125. UtilsFilterControl.setValues(this)
  126. this._initialized = true
  127. }
  128. })
  129. .on('post-header.bs.table', () => {
  130. setTimeout(() => {
  131. UtilsFilterControl.initFilterSelectControls(this)
  132. UtilsFilterControl.setValues(this)
  133. setTimeout(() => {
  134. const container = UtilsFilterControl.getControlContainer(this)
  135. const multipleSelects = container.find('.fc-multipleselect')
  136. if (multipleSelects.length > 0 && $.fn.multipleSelect) {
  137. multipleSelects.multipleSelect('destroy').multipleSelect(this.options.filterMultipleSelectOptions)
  138. }
  139. }, 2)
  140. }, 2)
  141. })
  142. .on('column-switch.bs.table', () => {
  143. UtilsFilterControl.setValues(this)
  144. if (this.options.height) {
  145. this.fitHeader()
  146. }
  147. })
  148. .on('post-body.bs.table', () => {
  149. if (this.options.height && !this.options.filterControlContainer && this.options.filterControlVisible) {
  150. UtilsFilterControl.fixHeaderCSS(this)
  151. }
  152. this.$tableLoading.css('top', this.$header.outerHeight() + 1)
  153. })
  154. .on('all.bs.table', () => {
  155. UtilsFilterControl.syncHeaders(this)
  156. })
  157. }
  158. super.init()
  159. }
  160. initBody () {
  161. super.initBody()
  162. if (this.options.filterControl) {
  163. setTimeout(() => {
  164. UtilsFilterControl.initFilterSelectControls(this)
  165. UtilsFilterControl.setValues(this)
  166. }, 3)
  167. }
  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 fp = $.isEmptyObject(that.filterColumnsPartial) ? null : that.filterColumnsPartial
  180. super.initSearch()
  181. if (this.options.sidePagination === 'server' || fp === null) {
  182. return
  183. }
  184. // Check partial column filter
  185. that.data = fp ?
  186. that.data.filter((item, i) => {
  187. const itemIsExpected = []
  188. const keys1 = Object.keys(item)
  189. const keys2 = Object.keys(fp)
  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 fval = (fp[key] || '').toLowerCase()
  194. let value = Utils.unescapeHTML(Utils.getItemField(item, key, false))
  195. let tmpItemIsExpected
  196. if (fval === '') {
  197. tmpItemIsExpected = true
  198. } else {
  199. // Fix #142: search use formatted data
  200. if (thisColumn && thisColumn.searchFormatter) {
  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(fval, 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(fval, 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 {
  245. tmpItemIsExpected = (`${value}`).toLowerCase().includes(searchValue)
  246. }
  247. const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
  248. const matches = largerSmallerEqualsRegex.exec(searchValue)
  249. if (matches) {
  250. const operator = matches[1] || `${matches[5]}l`
  251. const comparisonValue = matches[2] || matches[3]
  252. const int = parseInt(value, 10)
  253. const comparisonInt = parseInt(comparisonValue, 10)
  254. switch (operator) {
  255. case '>':
  256. case '<l':
  257. tmpItemIsExpected = int > comparisonInt
  258. break
  259. case '<':
  260. case '>l':
  261. tmpItemIsExpected = int < comparisonInt
  262. break
  263. case '<=':
  264. case '=<':
  265. case '>=l':
  266. case '=>l':
  267. tmpItemIsExpected = int <= comparisonInt
  268. break
  269. case '>=':
  270. case '=>':
  271. case '<=l':
  272. case '=<l':
  273. tmpItemIsExpected = int >= comparisonInt
  274. break
  275. default:
  276. break
  277. }
  278. }
  279. if (column.filterCustomSearch) {
  280. const customSearchResult = Utils.calculateObjectValue(this, column.filterCustomSearch, [searchValue, value, key, this.options.data], true)
  281. if (customSearchResult !== null) {
  282. tmpItemIsExpected = customSearchResult
  283. }
  284. }
  285. return tmpItemIsExpected
  286. }
  287. initColumnSearch (filterColumnsDefaults) {
  288. UtilsFilterControl.cacheValues(this)
  289. if (filterColumnsDefaults) {
  290. this.filterColumnsPartial = filterColumnsDefaults
  291. this.updatePagination()
  292. // eslint-disable-next-line guard-for-in
  293. for (const filter in filterColumnsDefaults) {
  294. this.trigger('column-search', filter, filterColumnsDefaults[filter])
  295. }
  296. }
  297. }
  298. initToolbar () {
  299. this.showToolbar = this.showToolbar || this.options.showFilterControlSwitch
  300. this.showSearchClearButton = this.options.filterControl && this.options.showSearchClearButton
  301. if (this.options.showFilterControlSwitch) {
  302. this.buttons = Object.assign(this.buttons, {
  303. filterControlSwitch: {
  304. text: this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow(),
  305. icon: this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow,
  306. event: this.toggleFilterControl,
  307. attributes: {
  308. 'aria-label': this.options.formatFilterControlSwitch(),
  309. title: this.options.formatFilterControlSwitch()
  310. }
  311. }
  312. })
  313. }
  314. super.initToolbar()
  315. }
  316. resetSearch (text) {
  317. if (this.options.filterControl && this.options.showSearchClearButton) {
  318. this.clearFilterControl()
  319. }
  320. super.resetSearch(text)
  321. }
  322. clearFilterControl () {
  323. if (!this.options.filterControl) {
  324. return
  325. }
  326. const that = this
  327. const table = this.$el.closest('table')
  328. const controls = UtilsFilterControl.getSearchControls(that)
  329. // const search = Utils.getSearchInput(this)
  330. let hasValues = false
  331. let timeoutId = 0
  332. // Clear cache values
  333. $.each(that._valuesFilterControl, (i, item) => {
  334. hasValues = hasValues ? true : item.value !== ''
  335. item.value = ''
  336. })
  337. // Clear controls in UI
  338. $.each(controls, (i, item) => {
  339. item.value = ''
  340. })
  341. // Cache controls again
  342. UtilsFilterControl.setValues(that)
  343. // clear cookies once the filters are clean. Should clear only Filtercontrol cookie
  344. clearTimeout(timeoutId)
  345. timeoutId = setTimeout(() => {
  346. if (that.options.cookie) {
  347. that.deleteCookie('filterControl')
  348. }
  349. }, that.options.searchTimeOut)
  350. // If there is not any value in the controls exit this method
  351. if (!hasValues) {
  352. return
  353. }
  354. // Clear each type of filter if it exists.
  355. // Requires the body to reload each time a type of filter is found because we never know
  356. // which ones are going to be present.
  357. if (controls.length > 0) {
  358. this.filterColumnsPartial = {}
  359. controls.eq(0).trigger(this.tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 })
  360. /* controls.each(function () {
  361. $(this).trigger(this.tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 })
  362. })*/
  363. } else {
  364. return
  365. }
  366. /* if (search.length > 0) {
  367. that.resetSearch('fc')
  368. }*/
  369. // use the default sort order if it exists. do nothing if it does not
  370. if (that.options.sortName !== table.data('sortName') || that.options.sortOrder !== table.data('sortOrder')) {
  371. const sorter = this.$header.find(Utils.sprintf('[data-field="%s"]', $(controls[0]).closest('table').data('sortName')))
  372. if (sorter.length > 0) {
  373. that.onSort({ type: 'keypress', currentTarget: sorter })
  374. $(sorter).find('.sortable').trigger('click')
  375. }
  376. }
  377. }
  378. // EVENTS
  379. onColumnSearch ({ currentTarget, keyCode }) {
  380. if (UtilsFilterControl.isKeyAllowed(keyCode)) {
  381. return
  382. }
  383. UtilsFilterControl.cacheValues(this)
  384. // Cookie extension support
  385. if (!this.options.cookie) {
  386. this.options.pageNumber = 1
  387. } else {
  388. // Force call the initServer method in Cookie extension
  389. this._filterControlValuesLoaded = true
  390. }
  391. if ($.isEmptyObject(this.filterColumnsPartial)) {
  392. this.filterColumnsPartial = {}
  393. }
  394. const $currentTarget = $(currentTarget)
  395. const currentTargetValue = $currentTarget.val()
  396. if (Array.isArray(currentTargetValue)) {
  397. const filterByArray = []
  398. const $field = $currentTarget.closest('[data-field]').data('field')
  399. for (let i = 0; i < currentTargetValue.length; i++) {
  400. const text = currentTargetValue[i] ? currentTargetValue[i].trim() : ''
  401. this.trigger('column-search', $field, text)
  402. if (text) {
  403. filterByArray.push(text)
  404. }
  405. }
  406. if (filterByArray.length > 0) {
  407. const filterObj = {}
  408. filterObj[$field] = filterByArray
  409. this.filterBy(filterObj)
  410. } else {
  411. this.filterBy({})
  412. }
  413. } else {
  414. const text = currentTargetValue ? currentTargetValue.trim() : ''
  415. const $field = $currentTarget.closest('[data-field]').data('field')
  416. this.trigger('column-search', $field, text)
  417. if (text) {
  418. this.filterColumnsPartial[$field] = text
  419. } else {
  420. delete this.filterColumnsPartial[$field]
  421. }
  422. }
  423. this.onSearch({ currentTarget }, false)
  424. }
  425. toggleFilterControl () {
  426. this.options.filterControlVisible = !this.options.filterControlVisible
  427. // Controls in original header or container.
  428. const $filterControls = UtilsFilterControl.getControlContainer(this).find('.filter-control, .no-filter-control')
  429. if (this.options.filterControlVisible) {
  430. $filterControls.show()
  431. } else {
  432. $filterControls.hide()
  433. this.clearFilterControl()
  434. }
  435. // Controls in fixed header
  436. if (this.options.height) {
  437. const $fixedControls = $('.fixed-table-header table thead').find('.filter-control, .no-filter-control')
  438. if (this.options.filterControlVisible) {
  439. $fixedControls.show()
  440. UtilsFilterControl.fixHeaderCSS(this)
  441. } else {
  442. $fixedControls.hide()
  443. UtilsFilterControl.fixHeaderCSS(this, '49px')
  444. }
  445. }
  446. const icon = this.options.showButtonIcons ? this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow : ''
  447. const text = this.options.showButtonText ? this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow() : ''
  448. this.$toolbar.find('>.columns').find('.filter-control-switch')
  449. .html(`${Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, icon) } ${ text}`)
  450. }
  451. triggerSearch () {
  452. const searchControls = UtilsFilterControl.getSearchControls(this)
  453. searchControls.each(function () {
  454. const $element = $(this)
  455. if ($element.is('select')) {
  456. $element.trigger('change')
  457. } else {
  458. $element.trigger('keyup')
  459. }
  460. })
  461. }
  462. _toggleColumn (index, checked, needUpdate) {
  463. this._initialized = false
  464. super._toggleColumn(index, checked, needUpdate)
  465. UtilsFilterControl.syncHeaders(this)
  466. }
  467. }