bootstrap-table-filter-control.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  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, 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. initialized: false
  51. })
  52. $.extend($.fn.bootstrapTable.columnDefaults, {
  53. filterControl: undefined, // input, select, datepicker
  54. filterDataCollector: undefined,
  55. filterData: undefined,
  56. filterDatepickerOptions: {},
  57. filterStrictSearch: false,
  58. filterStartsWithSearch: false,
  59. filterControlPlaceholder: '',
  60. filterDefault: '',
  61. filterOrderBy: 'asc' // asc || desc
  62. })
  63. $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
  64. 'column-search.bs.table': 'onColumnSearch',
  65. 'created-controls.bs.table': 'onCreatedControls'
  66. })
  67. $.extend($.fn.bootstrapTable.defaults.icons, {
  68. clear: {
  69. bootstrap3: 'glyphicon-trash icon-clear'
  70. }[$.fn.bootstrapTable.theme] || 'fa-trash',
  71. filterControlSwitchHide: {
  72. bootstrap3: 'glyphicon-zoom-out icon-zoom-out',
  73. materialize: 'zoom_out'
  74. }[$.fn.bootstrapTable.theme] || 'fa-search-minus',
  75. filterControlSwitchShow: {
  76. bootstrap3: 'glyphicon-zoom-in icon-zoom-in',
  77. materialize: 'zoom_in'
  78. }[$.fn.bootstrapTable.theme] || 'fa-search-plus'
  79. })
  80. $.extend($.fn.bootstrapTable.locales, {
  81. formatFilterControlSwitch () {
  82. return 'Hide/Show controls'
  83. },
  84. formatFilterControlSwitchHide () {
  85. return 'Hide controls'
  86. },
  87. formatFilterControlSwitchShow () {
  88. return 'Show controls'
  89. }
  90. })
  91. $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales)
  92. $.extend($.fn.bootstrapTable.defaults, {
  93. formatClearSearch () {
  94. return 'Clear filters'
  95. }
  96. })
  97. $.fn.bootstrapTable.methods.push('triggerSearch')
  98. $.fn.bootstrapTable.methods.push('clearFilterControl')
  99. $.fn.bootstrapTable.methods.push('toggleFilterControl')
  100. $.BootstrapTable = class extends $.BootstrapTable {
  101. init () {
  102. // Make sure that the filterControl option is set
  103. if (this.options.filterControl) {
  104. // Make sure that the internal variables are set correctly
  105. this.options.valuesFilterControl = []
  106. this.options.initialized = false
  107. this.$el
  108. .on('reset-view.bs.table', () => {
  109. // Create controls on $tableHeader if the height is set
  110. if (!this.options.height) {
  111. return
  112. }
  113. // Avoid recreate the controls
  114. const $controlContainer = UtilsFilterControl.getControlContainer(this)
  115. if (
  116. ($controlContainer.find('select').length > 0 || $controlContainer.find('input:not([type="checkbox"]):not([type="radio"])').length > 0) &&
  117. !this.options.filterControlContainer
  118. ) {
  119. return
  120. }
  121. UtilsFilterControl.createControls(this, $controlContainer)
  122. })
  123. .on('post-body.bs.table', () => {
  124. if (this.options.height && !this.options.filterControlContainer) {
  125. UtilsFilterControl.fixHeaderCSS(this)
  126. }
  127. this.$tableLoading.css('top', this.$header.outerHeight() + 1)
  128. })
  129. }
  130. super.init()
  131. }
  132. initBody () {
  133. super.initBody()
  134. }
  135. initHeader () {
  136. super.initHeader()
  137. if (!this.options.filterControl) {
  138. return
  139. }
  140. UtilsFilterControl.createControls(this, UtilsFilterControl.getControlContainer(this))
  141. }
  142. fitHeader () {
  143. if (this.$el.is(':hidden')) {
  144. this.timeoutId_ = setTimeout(() => this.fitHeader(), 100)
  145. return
  146. }
  147. const fixedBody = this.$tableBody.get(0)
  148. const scrollWidth = fixedBody.scrollWidth > fixedBody.clientWidth &&
  149. fixedBody.scrollHeight > fixedBody.clientHeight + this.$header.outerHeight() ?
  150. Utils.getScrollBarWidth() : 0
  151. this.$el.css('margin-top', -this.$header.outerHeight())
  152. const focused = $(':focus')
  153. if (focused.length > 0) {
  154. const $th = focused.parents('th')
  155. if ($th.length > 0) {
  156. const dataField = $th.attr('data-field')
  157. if (dataField !== undefined) {
  158. const $headerTh = this.$header.find(`[data-field='${dataField}']`)
  159. if ($headerTh.length > 0) {
  160. $headerTh.find(':input').addClass('focus-temp')
  161. }
  162. }
  163. }
  164. }
  165. if (this.options.height && this.options.filterControl && this.options.initialized) {
  166. this.$header_ = $('.fixed-table-header table thead').clone(true, true)
  167. } else {
  168. this.$header_ = this.$header.clone(true, true)
  169. this.options.initialized = true
  170. }
  171. this.$selectAll_ = this.$header_.find('[name="btSelectAll"]')
  172. this.$tableHeader
  173. .css('margin-right', scrollWidth)
  174. .find('table').css('width', this.$el.outerWidth())
  175. .html('').attr('class', this.$el.attr('class'))
  176. .append(this.$header_)
  177. this.$tableLoading.css('width', this.$el.outerWidth())
  178. const focusedTemp = $('.focus-temp:visible:eq(0)')
  179. if (focusedTemp.length > 0) {
  180. focusedTemp.focus()
  181. this.$header.find('.focus-temp').removeClass('focus-temp')
  182. }
  183. // fix bug: $.data() is not working as expected after $.append()
  184. this.$header.find('th[data-field]').each((i, el) => {
  185. this.$header_.find(Utils.sprintf('th[data-field="%s"]', $(el).data('field'))).data($(el).data())
  186. })
  187. const visibleFields = this.getVisibleFields()
  188. const $ths = this.$header_.find('th')
  189. let $tr = this.$body.find('>tr:not(.no-records-found,.virtual-scroll-top)').eq(0)
  190. while ($tr.length && $tr.find('>td[colspan]:not([colspan="1"])').length) {
  191. $tr = $tr.next()
  192. }
  193. const trLength = $tr.find('> *').length
  194. $tr.find('> *').each((i, el) => {
  195. const $this = $(el)
  196. if (Utils.hasDetailViewIcon(this.options)) {
  197. if (
  198. i === 0 && this.options.detailViewAlign !== 'right' ||
  199. i === trLength - 1 && this.options.detailViewAlign === 'right'
  200. ) {
  201. const $thDetail = $ths.filter('.detail')
  202. const zoomWidth = $thDetail.innerWidth() - $thDetail.find('.fht-cell').width()
  203. $thDetail.find('.fht-cell').width($this.innerWidth() - zoomWidth)
  204. return
  205. }
  206. }
  207. const index = i - Utils.getDetailViewIndexOffset(this.options)
  208. let $th = this.$header_.find(Utils.sprintf('th[data-field="%s"]', visibleFields[index]))
  209. if ($th.length > 1) {
  210. $th = $($ths[$this[0].cellIndex])
  211. }
  212. const zoomWidth = $th.innerWidth() - $th.find('.fht-cell').width()
  213. $th.find('.fht-cell').width($this.innerWidth() - zoomWidth)
  214. })
  215. this.horizontalScroll()
  216. this.trigger('post-header')
  217. }
  218. initSearch () {
  219. const that = this
  220. const fp = $.isEmptyObject(that.filterColumnsPartial) ? null : that.filterColumnsPartial
  221. super.initSearch()
  222. if (this.options.sidePagination === 'server' || fp === null) {
  223. return
  224. }
  225. // Check partial column filter
  226. that.data = fp ?
  227. that.data.filter((item, i) => {
  228. const itemIsExpected = []
  229. const keys1 = Object.keys(item)
  230. const keys2 = Object.keys(fp)
  231. const keys = keys1.concat(keys2.filter(item => !keys1.includes(item)))
  232. keys.forEach(key => {
  233. const thisColumn = that.columns[that.fieldsColumnsIndex[key]]
  234. const fval = (fp[key] || '').toLowerCase()
  235. let value = Utils.unescapeHTML(Utils.getItemField(item, key, false))
  236. let tmpItemIsExpected
  237. if (fval === '') {
  238. tmpItemIsExpected = true
  239. } else {
  240. // Fix #142: search use formatted data
  241. if (thisColumn && thisColumn.searchFormatter) {
  242. value = $.fn.bootstrapTable.utils.calculateObjectValue(
  243. that.header,
  244. that.header.formatters[$.inArray(key, that.header.fields)],
  245. [value, item, i],
  246. value
  247. )
  248. }
  249. if ($.inArray(key, that.header.fields) !== -1) {
  250. if (value === undefined || value === null) {
  251. tmpItemIsExpected = false
  252. } else if (typeof value === 'object') {
  253. value.forEach(objectValue => {
  254. if (tmpItemIsExpected) {
  255. return
  256. }
  257. if (this.options.searchAccentNeutralise) {
  258. objectValue = Utils.normalizeAccent(objectValue)
  259. }
  260. tmpItemIsExpected = that.isValueExpected(fval, objectValue, thisColumn, key)
  261. })
  262. } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
  263. if (this.options.searchAccentNeutralise) {
  264. value = Utils.normalizeAccent(value)
  265. }
  266. tmpItemIsExpected = that.isValueExpected(fval, value, thisColumn, key)
  267. }
  268. }
  269. }
  270. itemIsExpected.push(tmpItemIsExpected)
  271. })
  272. return !itemIsExpected.includes(false)
  273. }) :
  274. that.data
  275. that.unsortedData = [...that.data]
  276. }
  277. isValueExpected (searchValue, value, column, key) {
  278. let tmpItemIsExpected = false
  279. if (column.filterStrictSearch) {
  280. tmpItemIsExpected = value.toString().toLowerCase() === searchValue.toString().toLowerCase()
  281. } else if (column.filterStartsWithSearch) {
  282. tmpItemIsExpected = (`${value}`).toLowerCase().indexOf(searchValue) === 0
  283. } else {
  284. tmpItemIsExpected = (`${value}`).toLowerCase().includes(searchValue)
  285. }
  286. const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
  287. const matches = largerSmallerEqualsRegex.exec(searchValue)
  288. if (matches) {
  289. const operator = matches[1] || `${matches[5]}l`
  290. const comparisonValue = matches[2] || matches[3]
  291. const int = parseInt(value, 10)
  292. const comparisonInt = parseInt(comparisonValue, 10)
  293. switch (operator) {
  294. case '>':
  295. case '<l':
  296. tmpItemIsExpected = int > comparisonInt
  297. break
  298. case '<':
  299. case '>l':
  300. tmpItemIsExpected = int < comparisonInt
  301. break
  302. case '<=':
  303. case '=<':
  304. case '>=l':
  305. case '=>l':
  306. tmpItemIsExpected = int <= comparisonInt
  307. break
  308. case '>=':
  309. case '=>':
  310. case '<=l':
  311. case '=<l':
  312. tmpItemIsExpected = int >= comparisonInt
  313. break
  314. default:
  315. break
  316. }
  317. }
  318. if (column.filterCustomSearch) {
  319. const customSearchResult = Utils.calculateObjectValue(this, column.filterCustomSearch, [searchValue, value, key, this.options.data], true)
  320. if (customSearchResult !== null) {
  321. tmpItemIsExpected = customSearchResult
  322. }
  323. }
  324. return tmpItemIsExpected
  325. }
  326. initColumnSearch (filterColumnsDefaults) {
  327. if (filterColumnsDefaults) {
  328. this.filterColumnsPartial = filterColumnsDefaults
  329. this.updatePagination()
  330. // eslint-disable-next-line guard-for-in
  331. for (const filter in filterColumnsDefaults) {
  332. this.trigger('column-search', filter, filterColumnsDefaults[filter])
  333. }
  334. }
  335. }
  336. initToolbar () {
  337. this.showToolbar = this.showToolbar || this.options.showFilterControlSwitch
  338. this.showSearchClearButton = this.options.filterControl && this.options.showSearchClearButton
  339. if (this.options.showFilterControlSwitch) {
  340. this.buttons = Object.assign(this.buttons, {
  341. filterControlSwitch: {
  342. text: this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow(),
  343. icon: this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow,
  344. event: this.toggleFilterControl,
  345. attributes: {
  346. 'aria-label': this.options.formatFilterControlSwitch(),
  347. title: this.options.formatFilterControlSwitch()
  348. }
  349. }
  350. })
  351. }
  352. super.initToolbar()
  353. }
  354. resetSearch (text) {
  355. if (this.options.filterControl && this.options.showSearchClearButton) {
  356. this.clearFilterControl()
  357. }
  358. super.resetSearch(text)
  359. }
  360. clearFilterControl () {
  361. if (!this.options.filterControl) {
  362. return
  363. }
  364. const that = this
  365. const cookies = UtilsFilterControl.collectBootstrapCookies()
  366. const table = this.$el.closest('table')
  367. const controls = UtilsFilterControl.getSearchControls(that)
  368. const search = Utils.getSearchInput(this)
  369. let hasValues = false
  370. let timeoutId = 0
  371. $.each(that.options.valuesFilterControl, (i, item) => {
  372. hasValues = hasValues ? true : item.value !== ''
  373. item.value = ''
  374. })
  375. $.each(that.options.filterControls, (i, item) => {
  376. item.text = ''
  377. })
  378. UtilsFilterControl.setValues(that)
  379. // clear cookies once the filters are clean
  380. clearTimeout(timeoutId)
  381. timeoutId = setTimeout(() => {
  382. if (cookies && cookies.length > 0) {
  383. $.each(cookies, (i, item) => {
  384. if (that.deleteCookie !== undefined) {
  385. that.deleteCookie(item)
  386. }
  387. })
  388. }
  389. }, that.options.searchTimeOut)
  390. // If there is not any value in the controls exit this method
  391. if (!hasValues) {
  392. return
  393. }
  394. // Clear each type of filter if it exists.
  395. // Requires the body to reload each time a type of filter is found because we never know
  396. // which ones are going to be present.
  397. if (controls.length > 0) {
  398. this.filterColumnsPartial = {}
  399. $(controls[0]).trigger(
  400. controls[0].tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 }
  401. )
  402. } else {
  403. return
  404. }
  405. if (search.length > 0) {
  406. that.resetSearch()
  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. const $currentTarget = $(currentTarget)
  423. UtilsFilterControl.copyValues(this)
  424. const text = $currentTarget.val().trim()
  425. const $field = $currentTarget.closest('[data-field]').data('field')
  426. this.trigger('column-search', $field, text)
  427. if ($.isEmptyObject(this.filterColumnsPartial)) {
  428. this.filterColumnsPartial = {}
  429. }
  430. if (text) {
  431. this.filterColumnsPartial[$field] = text
  432. } else {
  433. delete this.filterColumnsPartial[$field]
  434. }
  435. this.options.pageNumber = 1
  436. this.onSearch({ currentTarget }, false)
  437. }
  438. toggleFilterControl () {
  439. this.options.filterControlVisible = !this.options.filterControlVisible
  440. // Controls in original header or container.
  441. const $filterControls = UtilsFilterControl.getControlContainer(this).find('.filter-control, .no-filter-control')
  442. if (this.options.filterControlVisible) {
  443. $filterControls.show()
  444. } else {
  445. $filterControls.hide()
  446. this.clearFilterControl()
  447. }
  448. // Controls in fixed header
  449. if (this.options.height) {
  450. const $fixedControls = $('.fixed-table-header table thead').find('.filter-control, .no-filter-control')
  451. if (this.options.filterControlVisible) {
  452. $fixedControls.show()
  453. } else {
  454. $fixedControls.hide()
  455. }
  456. UtilsFilterControl.fixHeaderCSS(this, '49px')
  457. }
  458. const icon = this.options.showButtonIcons ? this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow : ''
  459. const text = this.options.showButtonText ? this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow() : ''
  460. this.$toolbar.find('>.columns').find('.filter-control-switch')
  461. .html(`${Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, icon) } ${ text}`)
  462. }
  463. triggerSearch () {
  464. const searchControls = UtilsFilterControl.getSearchControls(this)
  465. searchControls.each(function () {
  466. const $element = $(this)
  467. if ($element.is('select')) {
  468. $element.trigger('change')
  469. } else {
  470. $element.trigger('keyup')
  471. }
  472. })
  473. }
  474. }