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 @@
+ +
+
+ +
+ +
+
+
+