diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json
index 292da376b..71066d6ec 100644
--- a/.bundlewatch.config.json
+++ b/.bundlewatch.config.json
@@ -50,11 +50,11 @@
},
{
"path": "./dist/js/adminlte.js",
- "maxSize": "10 kB"
+ "maxSize": "11.6 kB"
},
{
"path": "./dist/js/adminlte.min.js",
- "maxSize": "6.5 kB"
+ "maxSize": "7.5 kB"
}
]
}
diff --git a/build/js/AdminLTE.js b/build/js/AdminLTE.js
index cb004e543..fd709a432 100644
--- a/build/js/AdminLTE.js
+++ b/build/js/AdminLTE.js
@@ -6,6 +6,7 @@ import Dropdown from './Dropdown'
import ExpandableTable from './ExpandableTable'
import Layout from './Layout'
import PushMenu from './PushMenu'
+import SidebarSearch from './SidebarSearch'
import Toasts from './Toasts'
import TodoList from './TodoList'
import Treeview from './Treeview'
@@ -19,6 +20,7 @@ export {
ExpandableTable,
Layout,
PushMenu,
+ SidebarSearch,
Toasts,
TodoList,
Treeview
diff --git a/build/js/SidebarSearch.js b/build/js/SidebarSearch.js
new file mode 100644
index 000000000..092c631d8
--- /dev/null
+++ b/build/js/SidebarSearch.js
@@ -0,0 +1,253 @@
+/**
+ * --------------------------------------------
+ * AdminLTE SidebarSearch.js
+ * License MIT
+ * --------------------------------------------
+ */
+
+import $, { trim } from 'jquery'
+
+/**
+ * Constants
+ * ====================================================
+ */
+
+const NAME = 'SidebarSearch'
+const DATA_KEY = 'lte.sidebar-search'
+const JQUERY_NO_CONFLICT = $.fn[NAME]
+
+const CLASS_NAME_OPEN = 'sidebar-search-open'
+const CLASS_NAME_ICON_SEARCH = 'fa-search'
+const CLASS_NAME_ICON_CLOSE = 'fa-times'
+const CLASS_NAME_HEADER = 'nav-header'
+const CLASS_NAME_SEARCH_RESULTS = 'sidebar-search-results'
+const CLASS_NAME_LIST_GROUP = 'list-group'
+
+const SELECTOR_DATA_WIDGET = '[data-widget="sidebar-search"]'
+const SELECTOR_SIDEBAR = '.main-sidebar .nav-sidebar'
+const SELECTOR_NAV_LINK = '.nav-link'
+const SELECTOR_NAV_TREEVIEW = '.nav-treeview'
+const SELECTOR_SEARCH_INPUT = `${SELECTOR_DATA_WIDGET} .form-control`
+const SELECTOR_SEARCH_BUTTON = `${SELECTOR_DATA_WIDGET} .btn`
+const SELECTOR_SEARCH_ICON = `${SELECTOR_SEARCH_BUTTON} i`
+const SELECTOR_SEARCH_LIST_GROUP = `.${CLASS_NAME_LIST_GROUP}`
+const SELECTOR_SEARCH_RESULTS = `.${CLASS_NAME_SEARCH_RESULTS}`
+const SELECTOR_SEARCH_RESULTS_GROUP = `${SELECTOR_SEARCH_RESULTS} .${CLASS_NAME_LIST_GROUP}`
+
+const Default = {
+ arrowSign: '->',
+ minLength: 3,
+ maxResults: 7,
+ highlightName: true,
+ highlightPath: false,
+ highlightClass: 'text-light',
+ notFoundText: 'No element found!'
+}
+
+const SearchItems = []
+
+/**
+ * Class Definition
+ * ====================================================
+ */
+
+class SidebarSearch {
+ constructor(_element, _options) {
+ this.element = _element
+ this.options = $.extend({}, Default, _options)
+ this.items = []
+ }
+
+ // Public
+
+ init() {
+ if ($(SELECTOR_DATA_WIDGET).next(SELECTOR_SEARCH_RESULTS).length == 0) {
+ $(SELECTOR_DATA_WIDGET).after(
+ $('
', { class: CLASS_NAME_SEARCH_RESULTS })
+ )
+ }
+
+ if ($(SELECTOR_SEARCH_RESULTS).children(SELECTOR_SEARCH_LIST_GROUP).length == 0) {
+ $(SELECTOR_SEARCH_RESULTS).append(
+ $('', { class: CLASS_NAME_LIST_GROUP })
+ )
+ }
+
+ this._addNotFound()
+
+ $(SELECTOR_SIDEBAR).children().each((i, child) => {
+ this._parseItem(child)
+ })
+ }
+
+ search() {
+ const searchValue = $(SELECTOR_SEARCH_INPUT).val().toLowerCase()
+ if (searchValue.length < this.options.minLength) {
+ $(SELECTOR_SEARCH_RESULTS_GROUP).empty()
+ this._addNotFound()
+ this.close()
+ return
+ }
+
+ const searchResults = SearchItems.filter(item => (item.name).toLowerCase().includes(searchValue))
+ const endResults = $(searchResults.slice(0, this.options.maxResults))
+ $(SELECTOR_SEARCH_RESULTS_GROUP).empty()
+
+ if (endResults.length === 0) {
+ this._addNotFound()
+ } else {
+ endResults.each((i, result) => {
+ $(SELECTOR_SEARCH_RESULTS_GROUP).append(this._renderItem(result.name, result.link, result.path))
+ })
+ }
+
+ this.open()
+ }
+
+ open() {
+ $(SELECTOR_DATA_WIDGET).parent().addClass(CLASS_NAME_OPEN)
+ $(SELECTOR_SEARCH_ICON).removeClass(CLASS_NAME_ICON_SEARCH).addClass(CLASS_NAME_ICON_CLOSE)
+ }
+
+ close() {
+ $(SELECTOR_DATA_WIDGET).parent().removeClass(CLASS_NAME_OPEN)
+ $(SELECTOR_SEARCH_ICON).removeClass(CLASS_NAME_ICON_CLOSE).addClass(CLASS_NAME_ICON_SEARCH)
+ }
+
+ toggle() {
+ if ($(SELECTOR_DATA_WIDGET).parent().hasClass(CLASS_NAME_OPEN)) {
+ this.close()
+ } else {
+ this.open()
+ }
+ }
+
+ // Private
+
+ _parseItem(item, path = []) {
+ if ($(item).hasClass(CLASS_NAME_HEADER)) {
+ return
+ }
+
+ const itemObject = {}
+ const navLink = $(item).clone().find(`> ${SELECTOR_NAV_LINK}`)
+ const navTreeview = $(item).clone().find(`> ${SELECTOR_NAV_TREEVIEW}`)
+
+ const link = navLink.attr('href')
+ const name = navLink.find('p').children().remove().end().text()
+
+ itemObject.name = this._trimText(name)
+ itemObject.link = link
+ itemObject.path = path
+
+ if (navTreeview.length === 0) {
+ SearchItems.push(itemObject)
+ } else {
+ const newPath = itemObject.path.concat([itemObject.name])
+ navTreeview.children().each((i, child) => {
+ this._parseItem(child, newPath)
+ })
+ }
+ }
+
+ _trimText(text) {
+ return trim(text.replace(/(\r\n|\n|\r)/gm, ' '))
+ }
+
+ _renderItem(name, link, path) {
+ path = path.join(` ${this.options.arrowSign} `)
+
+ if (this.options.highlightName || this.options.highlightPath) {
+ const searchValue = $(SELECTOR_SEARCH_INPUT).val().toLowerCase()
+ const regExp = new RegExp(searchValue, 'gi')
+
+ if (this.options.highlightName) {
+ name = name.replace(
+ regExp,
+ str => {
+ return `${str}`
+ }
+ )
+ }
+
+ if (this.options.highlightPath) {
+ path = path.replace(
+ regExp,
+ str => {
+ return `${str}`
+ }
+ )
+ }
+ }
+
+ return `
+
+ ${name}
+
+
+ ${path}
+
+ `
+ }
+
+ _addNotFound() {
+ $(SELECTOR_SEARCH_RESULTS_GROUP).append(this._renderItem(this.options.notFoundText, '#', []))
+ }
+
+ // Static
+
+ static _jQueryInterface(config) {
+ let data = $(this).data(DATA_KEY)
+
+ if (!data) {
+ data = $(this).data()
+ }
+
+ const _options = $.extend({}, Default, typeof config === 'object' ? config : data)
+ const plugin = new SidebarSearch($(this), _options)
+
+ $(this).data(DATA_KEY, typeof config === 'object' ? config : data)
+
+ if (typeof config === 'string' && config.match(/init|toggle|close|open|search/)) {
+ plugin[config]()
+ } else {
+ plugin.init()
+ }
+ }
+}
+
+/**
+ * Data API
+ * ====================================================
+ */
+$(document).on('click', SELECTOR_SEARCH_BUTTON, event => {
+ event.preventDefault()
+
+ SidebarSearch._jQueryInterface.call($(SELECTOR_DATA_WIDGET), 'toggle')
+})
+
+$(document).on('keyup', SELECTOR_SEARCH_INPUT, () => {
+ let timer = 0
+ clearTimeout(timer)
+ timer = setTimeout(() => {
+ SidebarSearch._jQueryInterface.call($(SELECTOR_DATA_WIDGET), 'search')
+ }, 100)
+})
+
+$(window).on('load', () => {
+ SidebarSearch._jQueryInterface.call($(SELECTOR_DATA_WIDGET), 'init')
+})
+
+/**
+ * jQuery API
+ * ====================================================
+ */
+
+$.fn[NAME] = SidebarSearch._jQueryInterface
+$.fn[NAME].Constructor = SidebarSearch
+$.fn[NAME].noConflict = function () {
+ $.fn[NAME] = JQUERY_NO_CONFLICT
+ return SidebarSearch._jQueryInterface
+}
+
+export default SidebarSearch
diff --git a/build/scss/_main-sidebar.scss b/build/scss/_main-sidebar.scss
index e8818adfb..fff1609ad 100644
--- a/build/scss/_main-sidebar.scss
+++ b/build/scss/_main-sidebar.scss
@@ -902,6 +902,24 @@
.btn-sidebar:focus {
background-color: lighten($sidebar-dark-bg, 10%);
}
+
+ .list-group-item {
+ background-color: lighten($sidebar-dark-bg, 7.5%);
+ border-color: lighten($sidebar-dark-bg, 15%);
+ color: $sidebar-dark-color;
+
+ &:hover {
+ background-color: lighten($sidebar-dark-bg, 10%);
+ }
+
+ &:focus {
+ background-color: lighten($sidebar-dark-bg, 12.5%);
+ }
+
+ .search-path {
+ color: $gray-500;
+ }
+ }
}
[class*="sidebar-light"] {
@@ -924,6 +942,22 @@
.btn-sidebar:focus {
background-color: darken($sidebar-light-bg, 10%);
}
+
+ .list-group-item {
+ border-color: darken($sidebar-light-bg, 15%);
+
+ &:hover {
+ background-color: darken($sidebar-light-bg, 7.5%);
+ }
+
+ &:focus {
+ background-color: darken($sidebar-light-bg, 10%);
+ }
+
+ .search-path {
+ color: $gray-600;
+ }
+ }
}
// Sidebar inline input-group fix
@@ -946,3 +980,52 @@
position: relative;
}
}
+
+// Sidebar Search
+.sidebar-collapse {
+ .form-control-sidebar,
+ .form-control-sidebar ~ .input-group-append,
+ .sidebar-search-results {
+ display: none;
+ }
+}
+
+.sidebar-search-results {
+ position: relative;
+ display: none;
+ width: 100%;
+
+ .sidebar-search-open & {
+ display: inline-block;
+ }
+
+ .search-title {
+ margin-bottom: -.1rem;
+ }
+
+ .list-group {
+ position: absolute;
+ width: 100%;
+ z-index: $zindex-main-sidebar + 1;
+
+ .list-group-item {
+ padding: $input-padding-y $input-padding-x;
+ }
+
+ > .list-group-item:first-child {
+ border-top: 0;
+ @include border-top-radius(0);
+ }
+ }
+}
+
+.sidebar-search-results .search-path {
+ font-size: $small-font-size;
+}
+
+.sidebar-search-open {
+ .btn,
+ .form-control {
+ @include border-bottom-radius(0);
+ }
+}
diff --git a/docs/_config.yml b/docs/_config.yml
index 4069a3129..8b4618753 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -65,6 +65,8 @@ navigation:
url: javascript/todo-list.html
- title: Toasts
url: javascript/toasts.html
+ - title: Sidebar Search
+ url: javascript/sidebar-search.html
- title: Browser Support
url: browser-support.html
icon: fab fa-chrome
diff --git a/docs/javascript/sidebar-search.md b/docs/javascript/sidebar-search.md
new file mode 100644
index 000000000..8eb20a31c
--- /dev/null
+++ b/docs/javascript/sidebar-search.md
@@ -0,0 +1,71 @@
+---
+layout: page
+title: Sidebar Search Plugin
+---
+
+The sidebar search plugin provides the functionality to search menu items from the sidebar menu entries.
+
+##### Usage
+
+This plugin can be activated as a jQuery plugin or using the data API.
+
+###### Data API
+{: .text-bold }
+
+Activate the plugin by adding the following data-attribue `data-widget="sidebar-search"` to a input-group inside the sidebar. You can use the HTML Markup below for a quick start.
+
+
+###### jQuery
+{: .text-bold }
+
+The jQuery API provides more customizable options that allows the developer to pre-process the request before rendering and post-process it after rendering.
+
+```js
+("[data-widget="sidebar-search"]").SidebarSearch(options)
+```
+
+##### HTML Markup
+Place this HTML Markup after `div.user-panel`.
+```html
+
+```
+
+##### Options
+{: .mt-4}
+
+|---
+| Name | Type | Default | Description
+|-|-|-|-
+| arrowSign | String | '->' | Arrow Sign between the menu item path.
+| minLength | Number | 3 | Min search query length.
+| maxResults | Number | 7 | Max search results to display.
+| highlightName | Boolean | TRUE | Whether to highlight menu item name.
+| highlightPath | Boolean | FALSE | Whether to highlight menu item path.
+| highlightClass | String | 'text-light' | Hightlight class.
+| notFoundText | String | 'No element found! | Response text if no menu item found.
+{: .table .table-bordered .bg-light}
+
+
+##### Methods
+{: .mt-4}
+
+|---
+| Method | Description
+|-|-
+|init | Init's the SidebarSearch Plugin and registers all visible menu items.
+|toggle | Toggles the search dropdown list.
+|close | Closes the search dropdown list.
+|open | Opens the search dropdown list.
+|search | Triggers a search.
+{: .table .table-bordered .bg-light}
+
+Example: `$('[data-widget="sidebar-search"]').SidebarSearch('toggle')`
diff --git a/index.html b/index.html
index df14556df..f6aa23d03 100644
--- a/index.html
+++ b/index.html
@@ -172,6 +172,18 @@
+
+
+