SidebarSearch.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /**
  2. * --------------------------------------------
  3. * AdminLTE SidebarSearch.js
  4. * License MIT
  5. * --------------------------------------------
  6. */
  7. import $, { trim } from 'jquery'
  8. /**
  9. * Constants
  10. * ====================================================
  11. */
  12. const NAME = 'SidebarSearch'
  13. const DATA_KEY = 'lte.sidebar-search'
  14. const JQUERY_NO_CONFLICT = $.fn[NAME]
  15. const CLASS_NAME_OPEN = 'sidebar-search-open'
  16. const CLASS_NAME_ICON_SEARCH = 'fa-search'
  17. const CLASS_NAME_ICON_CLOSE = 'fa-times'
  18. const CLASS_NAME_HEADER = 'nav-header'
  19. const CLASS_NAME_SEARCH_RESULTS = 'sidebar-search-results'
  20. const CLASS_NAME_LIST_GROUP = 'list-group'
  21. const SELECTOR_DATA_WIDGET = '[data-widget="sidebar-search"]'
  22. const SELECTOR_SIDEBAR = '.main-sidebar .nav-sidebar'
  23. const SELECTOR_NAV_LINK = '.nav-link'
  24. const SELECTOR_NAV_TREEVIEW = '.nav-treeview'
  25. const SELECTOR_SEARCH_INPUT = `${SELECTOR_DATA_WIDGET} .form-control`
  26. const SELECTOR_SEARCH_BUTTON = `${SELECTOR_DATA_WIDGET} .btn`
  27. const SELECTOR_SEARCH_ICON = `${SELECTOR_SEARCH_BUTTON} i`
  28. const SELECTOR_SEARCH_LIST_GROUP = `.${CLASS_NAME_LIST_GROUP}`
  29. const SELECTOR_SEARCH_RESULTS = `.${CLASS_NAME_SEARCH_RESULTS}`
  30. const SELECTOR_SEARCH_RESULTS_GROUP = `${SELECTOR_SEARCH_RESULTS} .${CLASS_NAME_LIST_GROUP}`
  31. const Default = {
  32. arrowSign: '->',
  33. minLength: 3,
  34. maxResults: 7,
  35. highlightName: true,
  36. highlightPath: false,
  37. highlightClass: 'text-light',
  38. notFoundText: 'No element found!'
  39. }
  40. const SearchItems = []
  41. /**
  42. * Class Definition
  43. * ====================================================
  44. */
  45. class SidebarSearch {
  46. constructor(_element, _options) {
  47. this.element = _element
  48. this.options = $.extend({}, Default, _options)
  49. this.items = []
  50. }
  51. // Public
  52. _init() {
  53. if ($(SELECTOR_DATA_WIDGET).length === 0) {
  54. return
  55. }
  56. if ($(SELECTOR_DATA_WIDGET).next(SELECTOR_SEARCH_RESULTS).length === 0) {
  57. $(SELECTOR_DATA_WIDGET).after(
  58. $('<div />', { class: CLASS_NAME_SEARCH_RESULTS })
  59. )
  60. }
  61. if ($(SELECTOR_SEARCH_RESULTS).children(SELECTOR_SEARCH_LIST_GROUP).length === 0) {
  62. $(SELECTOR_SEARCH_RESULTS).append(
  63. $('<div />', { class: CLASS_NAME_LIST_GROUP })
  64. )
  65. }
  66. this._addNotFound()
  67. $(SELECTOR_SIDEBAR).children().each((i, child) => {
  68. this._parseItem(child)
  69. })
  70. }
  71. search() {
  72. const searchValue = $(SELECTOR_SEARCH_INPUT).val().toLowerCase()
  73. if (searchValue.length < this.options.minLength) {
  74. $(SELECTOR_SEARCH_RESULTS_GROUP).empty()
  75. this._addNotFound()
  76. this.close()
  77. return
  78. }
  79. const searchResults = SearchItems.filter(item => (item.name).toLowerCase().includes(searchValue))
  80. const endResults = $(searchResults.slice(0, this.options.maxResults))
  81. $(SELECTOR_SEARCH_RESULTS_GROUP).empty()
  82. if (endResults.length === 0) {
  83. this._addNotFound()
  84. } else {
  85. endResults.each((i, result) => {
  86. $(SELECTOR_SEARCH_RESULTS_GROUP).append(this._renderItem(escape(result.name), encodeURI(result.link), result.path))
  87. })
  88. }
  89. this.open()
  90. }
  91. open() {
  92. $(SELECTOR_DATA_WIDGET).parent().addClass(CLASS_NAME_OPEN)
  93. $(SELECTOR_SEARCH_ICON).removeClass(CLASS_NAME_ICON_SEARCH).addClass(CLASS_NAME_ICON_CLOSE)
  94. }
  95. close() {
  96. $(SELECTOR_DATA_WIDGET).parent().removeClass(CLASS_NAME_OPEN)
  97. $(SELECTOR_SEARCH_ICON).removeClass(CLASS_NAME_ICON_CLOSE).addClass(CLASS_NAME_ICON_SEARCH)
  98. }
  99. toggle() {
  100. if ($(SELECTOR_DATA_WIDGET).parent().hasClass(CLASS_NAME_OPEN)) {
  101. this.close()
  102. } else {
  103. this.open()
  104. }
  105. }
  106. // Private
  107. _parseItem(item, path = []) {
  108. if ($(item).hasClass(CLASS_NAME_HEADER)) {
  109. return
  110. }
  111. const itemObject = {}
  112. const navLink = $(item).clone().find(`> ${SELECTOR_NAV_LINK}`)
  113. const navTreeview = $(item).clone().find(`> ${SELECTOR_NAV_TREEVIEW}`)
  114. const link = navLink.attr('href')
  115. const name = navLink.find('p').children().remove().end().text()
  116. itemObject.name = this._trimText(name)
  117. itemObject.link = link
  118. itemObject.path = path
  119. if (navTreeview.length === 0) {
  120. SearchItems.push(itemObject)
  121. } else {
  122. const newPath = itemObject.path.concat([itemObject.name])
  123. navTreeview.children().each((i, child) => {
  124. this._parseItem(child, newPath)
  125. })
  126. }
  127. }
  128. _trimText(text) {
  129. return trim(text.replace(/(\r\n|\n|\r)/gm, ' '))
  130. }
  131. _renderItem(name, link, path) {
  132. path = path.join(` ${this.options.arrowSign} `)
  133. name = unescape(name)
  134. link = decodeURI(link)
  135. if (this.options.highlightName || this.options.highlightPath) {
  136. const searchValue = $(SELECTOR_SEARCH_INPUT).val().toLowerCase()
  137. const regExp = new RegExp(searchValue, 'gi')
  138. if (this.options.highlightName) {
  139. name = name.replace(
  140. regExp,
  141. str => {
  142. return `<strong class="${this.options.highlightClass}">${str}</strong>`
  143. }
  144. )
  145. }
  146. if (this.options.highlightPath) {
  147. path = path.replace(
  148. regExp,
  149. str => {
  150. return `<strong class="${this.options.highlightClass}">${str}</strong>`
  151. }
  152. )
  153. }
  154. }
  155. const groupItemElement = $('<a/>', {
  156. href: decodeURIComponent(link),
  157. class: 'list-group-item'
  158. })
  159. const searchTitleElement = $('<div/>', {
  160. class: 'search-title'
  161. }).html(name)
  162. const searchPathElement = $('<div/>', {
  163. class: 'search-path'
  164. }).html(path)
  165. groupItemElement.append(searchTitleElement).append(searchPathElement)
  166. return groupItemElement
  167. }
  168. _addNotFound() {
  169. $(SELECTOR_SEARCH_RESULTS_GROUP).append(this._renderItem(this.options.notFoundText, '#', []))
  170. }
  171. // Static
  172. static _jQueryInterface(config) {
  173. return this.each(function () {
  174. let data = $(this).data(DATA_KEY)
  175. const _config = $.extend({}, Default, typeof config === 'object' ? config : $(this).data())
  176. if (!data) {
  177. data = new SidebarSearch($(this), _config)
  178. $(this).data(DATA_KEY, data)
  179. data._init()
  180. } else if (typeof config === 'string') {
  181. if (typeof data[config] === 'undefined') {
  182. throw new TypeError(`No method named "${config}"`)
  183. }
  184. data[config]()
  185. } else if (typeof config === 'undefined') {
  186. data._init()
  187. }
  188. })
  189. }
  190. }
  191. /**
  192. * Data API
  193. * ====================================================
  194. */
  195. $(document).on('click', SELECTOR_SEARCH_BUTTON, event => {
  196. event.preventDefault()
  197. SidebarSearch._jQueryInterface.call($(SELECTOR_DATA_WIDGET), 'toggle')
  198. })
  199. $(document).on('keyup', SELECTOR_SEARCH_INPUT, event => {
  200. if (event.keyCode == 38) {
  201. event.preventDefault()
  202. $(SELECTOR_SEARCH_RESULTS_GROUP).children().last().focus()
  203. return
  204. }
  205. if (event.keyCode == 40) {
  206. event.preventDefault()
  207. $(SELECTOR_SEARCH_RESULTS_GROUP).children().first().focus()
  208. return
  209. }
  210. setTimeout(() => {
  211. SidebarSearch._jQueryInterface.call($(SELECTOR_DATA_WIDGET), 'search')
  212. }, 100)
  213. })
  214. $(document).on('keydown', SELECTOR_SEARCH_RESULTS_GROUP, event => {
  215. const $focused = $(':focus')
  216. if (event.keyCode == 38) {
  217. event.preventDefault()
  218. if ($focused.is(':first-child')) {
  219. $focused.siblings().last().focus()
  220. } else {
  221. $focused.prev().focus()
  222. }
  223. }
  224. if (event.keyCode == 40) {
  225. event.preventDefault()
  226. if ($focused.is(':last-child')) {
  227. $focused.siblings().first().focus()
  228. } else {
  229. $focused.next().focus()
  230. }
  231. }
  232. })
  233. $(window).on('load', () => {
  234. SidebarSearch._jQueryInterface.call($(SELECTOR_DATA_WIDGET), 'init')
  235. })
  236. /**
  237. * jQuery API
  238. * ====================================================
  239. */
  240. $.fn[NAME] = SidebarSearch._jQueryInterface
  241. $.fn[NAME].Constructor = SidebarSearch
  242. $.fn[NAME].noConflict = function () {
  243. $.fn[NAME] = JQUERY_NO_CONFLICT
  244. return SidebarSearch._jQueryInterface
  245. }
  246. export default SidebarSearch