diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c5a4e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Numerous always-ignore extensions +*.diff +*.err +*.orig +*.log +*.rej +*.swo +*.swp +*.vi +*~ +*.sass-cache + +# OS or Editor folders +.DS_Store +Thumbs.db +.cache +.project +.settings +.tmproj +*.esproj +nbproject +*.sublime-project +*.sublime-workspace + +# Dreamweaver added files +_notes +dwsync.xml + +# Komodo +*.komodoproject +.komodotools + +# Folders to ignore +.hg +.svn +.CVS +intermediate +.idea +cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..3da3ff3 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Wordpress Live Admin Navigation Filter + +Add a search box to the wordpress admin nav that allows you to more easily find that menu item you can't see at a glance. + +## Installation + +1. Upload the `live-admin-nav-filter` folder to your `/wp-content/plugins/` directory. +2. Activate the plugin through the `Plugins` menu in WordPress. +3. You should now see a search box below the `collapse menu` button in the admin menu. + +## Features + +* Quickly and easily find what you're looking for. +* Keyboard navigation friendly (hit `/` to focus the search field, use the `up` and `down` keys to select the menu item you're after, hit the `enter` key to go to the selected menu item) +* Customizable border and highlight colours. + +## Screenshots + +1. Example results on expanded menu item. +2. Multiple results. +3. The settings screen. \ No newline at end of file diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..32dca3a --- /dev/null +++ b/README.txt @@ -0,0 +1,36 @@ +=== Live Admin Navigation Filter === +Contributors: Remy Bach +Donate link: Through PayPal... remy[at]bach.me.uk +Tags: admin, nav, filter, live filter, navigation, navigation search, admin search +Requires at least: 3.2 +Tested up to: 3.4.1 +Stable tag: 1.0 + +Add a search box to the admin nav that allows you to more easily find that menu item you can't see at a glance. + +== Description == + +Add a search box to the admin nav that allows you to more easily find that menu item you can't see at a glance. + +== Installation == + +1. Upload the `live-admin-nav-filter` folder to your `/wp-content/plugins/` directory. +2. Activate the plugin through the `Plugins` menu in WordPress. +3. You should now see a search box below the `collapse menu` button in the admin menu. + +== Features == + +* Quickly and easily find what you're looking for. +* Keyboard navigation friendly (hit `/` to focus the search field, use the `up` and `down` keys to select the menu item you're after, hit the `enter` key to go to the selected menu item) +* Customizable border and highlight colours. + +== Screenshots == + +1. Example results on expanded menu item. +2. Multiple results. +3. The settings screen. + +== Changelog == + += 1.0 = +* Initial Release \ No newline at end of file diff --git a/css/admin.css b/css/admin.css new file mode 100644 index 0000000..1b1eb21 --- /dev/null +++ b/css/admin.css @@ -0,0 +1,71 @@ +.lnf-filter-bottom, .lnf-filter-top { + margin:3px; +} +.admin-bar.branch-3-2 .lnf-filter-top, +.admin-bar.branch-3-3 .lnf-filter-top { + position:relative; + top:28px; +} +/* Word Highlight */ +.lnf-highlight { + background:#FFED2B; + color:#000; + display:inline-block; + text-shadow:none; +} +.lnf-focus, #adminmenu .lnf-focus .wp-submenu { + border:2px solid #FFED2B; +} +#adminmenu a.lnf-focused-item, .lnf-focused-item .lnf-highlight { + text-decoration:underline; +} +#adminmenu .lnf-focus .wp-menu-arrow, +#adminmenu .lnf-focus .wp-menu-arrow div { + display:block; + z-index:1001; +} +#adminmenu .lnf-focus .wp-menu-img { + opacity:1; + + filter:alpha(opacity=100); +} +#adminmenu .lnf-focus .wp-submenu { + z-index:1000; +} +#adminmenu li .wp-submenu.lnf-sub-open, .folded #adminmenu .wp-has-current-submenu .wp-submenu.lnf-sub-open { + display:block; +} +.lnf-error { + color:#FF0000; +} + +/* Admin Settings Colours */ +.lnf-colour { + display:inline-block; + height:32px; + margin-right:10px; + width:32px; + + -webkit-box-shadow:1px 1px 2px 0px rgba(0,0,0,0.5); /* Saf3-4, iOS 4.0.2 - 4.2, Android 2.3+ */ + box-shadow:1px 1px 2px 0px rgba(0,0,0,0.5); /* Opera 10.5, IE9, FF4+, Chrome 6+, iOS 5 */ + + -webkit-transition:all 0.2s ease-out; /* Saf3.2+, Chrome */ + -moz-transition:all 0.2s ease-out; /* FF4+ */ + -o-transition:all 0.2s ease-out; /* Opera 10.5+ */ + transition:all 0.2s ease-out; +} +.lnf-colour.selected { + border:1px solid black !important; /* Needs !important to override the border in the style attribute (allows for dynamic adding of colours) */ +} +.lnf-colour:not(.selected) { + cursor:pointer; +} +.lnf-colour:not(.selected):hover { + height:35px; + margin-left:-3px; + margin-top:-3px; + width:35px; + + -webkit-box-shadow:2px 2px 4px 0px rgba(0,0,0,0.5); /* Saf3-4, iOS 4.0.2 - 4.2, Android 2.3+ */ + box-shadow:2px 2px 4px 0px rgba(0,0,0,0.5); /* Opera 10.5, IE9, FF4+, Chrome 6+, iOS 5 */ +} \ No newline at end of file diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..5f23e5e --- /dev/null +++ b/js/admin.js @@ -0,0 +1,216 @@ +// lnf = live nav filter : use this in an attempt to avoid clashing with other plugins' class names. +(function($) { + jQuery(function($) { + // Add our filter field. + var filter_field = ''; + + // Position this as specified in our settings + if (LNF_POSITION && LNF_POSITION === 'top') { + $('#adminmenu').before(filter_field); + } else { + $('#adminmenu').after(filter_field); + } + $('#adminmenu li > a').each(function(i, val) { + var searchable = $(this).html() + .replace(/^<.*?\/?>/, '') // Remove any html tags at the beginning of the element. + .match(/([\w\s-&;]+)/); // Lastly, search for the first words (\w\s) in the (sometimes there's + // additional HTML for update counts, etc.) Let's also include dashes, + // and allow html entities (-&;) + + // Don't go any further if this doesn't match + if ( !searchable || (searchable && searchable.length < 2) ) { return false; } + + // Append this as a data attribute for searching later (we do this so that we can still search while the + // element has our additional highlighting markup in it). + $.data(this, 'lnf-search', searchable[1]); + }); + + var timer = false; + $('#lnf').keyup(function(e) { + var lnf_input = this; + + // Add some latency so the menu doesn't flash when typing faster than ~60wpm... + // 60wpm / 5 (approximate average of characters per word) / 60 seconds = 0.2seconds (or 200ms) + clearTimeout(timer); + timer = setTimeout(function() { + filter.call(lnf_input, e); + }, 200); + }).change(filter); + + // If the user hits '/', focus our filter field. + $(document).keyup(function(e) { + // If the user isn't trying to legitimately type & the key that was pressed matches + if ( e.keyCode === 191 && $('input:focus, textarea:focus').length === 0 ) { // 191 == / + if ( $('#lnf:hidden').length > 0 ) { + $('#lnf:hidden').slideDown(100, function() { + $(this).focus(); + }); + } else { + $('#lnf').focus(); + } + + return false; + } + }); + + function filter(e) { + if ( e.keyCode === 13 ) { // If the user hits enter, let's go to the selected link. + location.href = $('.lnf-focused-item').attr('href'); + } else if ( e.keyCode === 40 ) { // If the user has pressed down + focus(1); + return false; + } else if ( e.keyCode === 38 ) { // If the user has pressed up + focus(-1); + return false; + } else if ( this.value && this.value.length >= 3 ) { // Make sure there's something to search for + var search = new RegExp(this.value, 'i'); + + // Make sure there is even something matching at all here + if ( search.test( $('#adminmenu').html() ) ) { + $('#adminmenu li > a').each(function(i, val) { + // If the text in this link matches, work our magic. + if ( !search.test( $(this).data('lnf-search') ) ) { + $(this).parent('li').fadeTo('fast', 0.15).addClass('lnf-hidden'); + } else { + reset(this); + unhighlight($(this).find('.lnf-highlight')); + $(this).html(val.innerHTML.replace(search, ''+val.innerHTML.match(search)[0]+'')); + + // If this is a sub menu, show it + if ( $(this).parents('.wp-submenu').length > 0 ) { + $(this).parents('.wp-submenu').addClass('lnf-sub-open'); + $(this).parents('.lnf-hidden').fadeTo('fast', 1).removeClass('lnf-hidden'); + } + } + }); + + // Focus the first result + focus(0); + + $('#lnf').removeClass('lnf-error'); + } else { + reset('.lnf-hidden'); + unhighlight( $('.lnf-highlight') ); + $('#lnf').addClass('lnf-error'); + } + } else { + if (e.keyCode === 27 && LNF_HIDDEN) { // 27 == Escape + $('#lnf').slideUp(100).blur(); + } + + reset('.lnf-hidden'); + unhighlight( $('.lnf-highlight') ); + $('#lnf').removeClass('lnf-error'); + $('.lnf-sub-open').removeClass('lnf-sub-open'); + $('.lnf-focused-item').removeClass('lnf-focused-item'); + } + } + + /** + * Reset the given element back to it's original state + * @param DOM Element what The element to reset. + */ + function reset(what) { + $(what).fadeTo('fast', 1).removeClass('lnf-hidden').parents('.lnf-focus').removeClass('lnf-focus'); + $(what).parents('.lnf-focused-item').removeClass('lnf-focused-item'); + + $('#adminmenu .wp-submenu:visible').each(function(i, val) { + if ( $(this).hasClass('lnf-sub-open') && $(this).parent('li').find('.lnf-highlight').length === 0 ) { + $(this).removeClass('lnf-sub-open'); + } + }); + } + + /** + * Remove the highlight wrapper from the given element. + * @param DOM Element what The element to remove all highlighting from. + */ + function unhighlight(what) { + // Remove the highlighting + $(what).each(function(i, val) { + $(this).replaceWith(this.innerHTML); + }); + } + + /** + * Focus the next/previous element. Has scope for selecting the first item too if none are selected yet. + * @param Number which_way Either -1 or 1 depending on whether to go to the previous or next item respectively. + */ + function focus(which_way) { + var i = 0, + highlighted = $('.lnf-highlight') + where_to = 0; + + // If there isn't a focused item, make it the first one. + if ( $('.lnf-focused-item').length === 0 ) { + which_way = 0; + } else { + // Get the index of the currently focused item. + i = $.inArray( $('.lnf-focused-item .lnf-highlight')[0], highlighted ); + } + + // Make the selection wrap around to the beginning when hitting the end and vice versa + if ( i+which_way < 0 ) { // when pressing up on the first item. + where_to = highlighted.length-1; + } else if ( i+which_way > highlighted.length-1 ) { // when pressing down on the last item. + where_to = 0; + } else { // otherwise it's safe to go to the next/previous item. + where_to = i+which_way; + } + + // Remove the currently focused item. + $('.lnf-focus, .lnf-focused-item').removeClass('lnf-focus').removeClass('lnf-focused-item'); + + // Add the highlight class to the li that's highest up in the dom. + var parent_lis = $( $('.lnf-highlight').get(where_to) ).parents('li'); + $(parent_lis[parent_lis.length-1]).addClass('lnf-focus'); + // Also indicate which is the currently selected link (so the user can see where hitting the Enter key takes them.) + $( $('.lnf-highlight').get(where_to) ).parents('a').addClass('lnf-focused-item'); + } + + /*===== Give the user predefined options for colours on the options page. =====*/ + if ( $('#border_colour').length > 0 ) { + var colours = { + blue:'80C8F0', + green:'6EAF51', + orange:'EC6D1E', + pink:'EA6C9C', + purple:'A05DA2', + yellow:'FFED2B' + }; + + // Add our suggested colour swatches before the input field. + $('#border_colour, #highlight_colour').before('
'); + $.each(colours, function(name, hex) { + $('.lnf-colour-options').each(function() { + var selected = false; + if ( $(this).next('input').val().match(hex) ) { + selected = true; + } + $(this).append(''+ + ''); + }); + }); + + // Give the colour swatches their functionality here. + $('.lnf-colour').click(function(e) { + var current = $(this).parents('.lnf-colour-options').find('.selected'); + + // Update our input field. + $(this).parents('.lnf-colour-options').next('input').val( '#'+$(this).data('hex') ); + // Clear the currently selected swatch + current.removeClass('selected').css('border', '1px solid #'+$(current).data('hex')); + // Indicate that this is the currently selected swatch. + $(this).addClass('selected'); + + e.preventDefault(); + }); + } + }); +})(jQuery); \ No newline at end of file diff --git a/plugin.php b/plugin.php new file mode 100644 index 0000000..41859bc --- /dev/null +++ b/plugin.php @@ -0,0 +1,338 @@ +checkboxes = array(); + $this->settings = array(); + $this->get_settings(); + $this->sections['position'] = __( 'Position Settings' ); + $this->sections['colour'] = __( 'Colour Settings' ); + add_action( 'admin_menu', array( &$this, 'add_pages' ) ); + add_action( 'admin_init', array( &$this, 'register_settings' ) ); + + // Initialize our settings page + if ( ! get_option( 'lnf-options' ) ) { + $this->initialize_settings(); + } + + register_activation_hook( __FILE__, array( &$this, 'activate' ) ); + register_deactivation_hook( __FILE__, array( &$this, 'deactivate' ) ); + } // end constructor + + /** + * Registers and enqueues admin-specific styles. + */ + public function register_admin_styles() { + wp_register_style( 'live-admin-nav-filter-admin-styles', plugins_url( 'live-admin-nav-filter/css/admin.css' ) ); + wp_enqueue_style( 'live-admin-nav-filter-admin-styles' ); + } // end register_admin_styles + + /** + * Registers and enqueues admin-specific JavaScript. + */ + public function register_admin_scripts() { + wp_register_script( 'live-admin-nav-filter-admin-script', plugins_url( 'live-admin-nav-filter/js/admin.js' ) ); + wp_enqueue_script( 'live-admin-nav-filter-admin-script' ); + } // end register_admin_scripts + + public function admin_css_js() { + $options = get_option('lnf-options'); + + if ( !empty( $options ) ) { + echo ''; + + echo ''; + } + } // end admin_css_js + + /** + * Add our settings menu. + */ + /* Add page(s) to the admin menu */ + public function add_pages() { + $admin_page = add_options_page( 'Live Admin Nav Filter', 'Live Admin Nav Filter', 'manage_options', 'lnf-options', array( &$this, 'display_page' ) ); + } // end add_pages + + /* HTML to display the settings page */ + public function display_page() { + echo '
+
+

' . __( 'Live Admin Nav Filter' ) . '

+
+ '; + settings_fields( 'lnf-options' ); + do_settings_sections( $_GET['page'] ); + echo '

+
+ +

' . __( 'Usage' ) . '

+ +
    +
  • + Hitting \'/\' while not in a text area or input field will let you quickly begin searching. +
  • +
  • + While results are displayed, press the up and down keys to cycle through your results. +
  • +
  • + When you\'re happy with your selection, hit the enter key to go to it. +
  • +
+ + + Lovingly crafted by
Rémy Bach.
+ Should you feel like throwing any
+ money my way, please do so below: +
+
+ + + + +
'; + } // end display_page + + /* Define all settings and their defaults */ + public function get_settings() { + // Position Settings + $this->settings['position'] = array( + 'title' => __( 'Position' ), + 'desc' => __( 'Choose whether the filter field goes at the top, or the bottom of the menu.' ), + 'std' => 'bottom', + 'type' => 'select', + 'choices' => array( 'bottom'=>'Bottom', 'top'=>'Top' ), + 'section' => 'position' + ); + $this->settings['hidden'] = array( + 'title' => __( 'Hidden' ), + 'desc' => __( 'Choose whether the filter field is hidden until a "/" is typed.' ), + 'std' => 0, + 'type' => 'select', + 'choices' => array( 0=>'No', 1=>'Yes' ), + 'section' => 'position' + ); + + // Colour Settings + $this->settings['border_colour'] = array( + 'title' => __( 'Border Colour' ), + 'desc' => __( 'Choose a colour for the border around navigation elements that contain matches.' ), + 'std' => '#FFED2B', + 'type' => 'text', + 'section' => 'colour' + ); + $this->settings['highlight_colour'] = array( + 'title' => __( 'Highlight Colour' ), + 'desc' => __( 'Choose a colour that will highlight your matched search.' ), + 'std' => '#FFED2B', + 'type' => 'text', + 'section' => 'colour' + ); + } // end get_settings + + /* Initialize settings to their default values */ + public function initialize_settings() { + $default_settings = array(); + foreach ( $this->settings as $id => $setting ) { + if ( $setting['type'] != 'heading' ) + $default_settings[$id] = $setting['std']; + } + + update_option( 'lnf-options', $default_settings ); + } // end initialize_settings + + /* Register settings via the WP Settings API */ + public function register_settings() { + register_setting( 'lnf-options', 'lnf-options', array ( &$this, 'validate_settings' ) ); + + foreach ( $this->sections as $slug => $title ) { + add_settings_section( $slug, $title, array( &$this, 'display_section' ), 'lnf-options' ); + } + + $this->get_settings(); + + foreach ( $this->settings as $id => $setting ) { + $setting['id'] = $id; + $this->create_setting( $setting ); + } + } // end register_settings + + /* Description for section */ + public function display_section() { + // code + } // end display_section + + public function create_setting( $args = array() ) { + $defaults = array( + 'id' => 'default_field', + 'title' => 'Default Field', + 'desc' => 'This is a default description.', + 'std' => '', + 'type' => 'text', + 'section' => 'general', + 'choices' => array(), + 'class' => '' + ); + + extract( wp_parse_args( $args, $defaults ) ); + + $field_args = array( + 'type' => $type, + 'id' => $id, + 'desc' => $desc, + 'std' => $std, + 'choices' => $choices, + 'label_for' => $id, + 'class' => $class + ); + + if ( $type == 'checkbox' ) { + $this->checkboxes[] = $id; + } + + add_settings_field( $id, $title, array( $this, 'display_setting' ), 'lnf-options', $section, $field_args ); + } // end create_setting + + /* HTML output for individual settings */ + public function display_setting( $args = array() ) { + extract( $args ); + + $options = get_option( 'lnf-options' ); + + if ( ! isset( $options[$id] ) && $type != 'checkbox' ) { + $options[$id] = $std; + } else if ( ! isset( $options[$id] ) ) { + $options[$id] = 0; + } + + $field_class = ''; + if ( $class != '' ) { + $field_class = ' ' . $class; + } + + switch ( $type ) { + + case 'heading': + echo '

' . $desc . '

'; + break; + + case 'checkbox': + + echo ' '; + + break; + + case 'select': + echo ''; + + if ( $desc != '' ) { + echo '
' . $desc . ''; + } + + break; + + case 'radio': + $i = 0; + foreach ( $choices as $value => $label ) { + echo ' '; + if ( $i < count( $options ) - 1 ) { + echo '
'; + } + $i++; + } + + if ( $desc != '' ) { + echo '
' . $desc . ''; + } + + break; + + case 'textarea': + echo ''; + + if ( $desc != '' ) { + echo '
' . $desc . ''; + } + + break; + + case 'password': + echo ''; + + if ( $desc != '' ) { + echo '
' . $desc . ''; + } + + break; + + case 'text': + default: + echo ''; + + if ( $desc != '' ) { + echo '
' . $desc . ''; + } + + break; + } + } // end display_setting + + /** + * Validate our input fields + */ + public function validate_settings( $input ) { + $options = get_option( 'lnf-options' ); + + foreach ( $this->checkboxes as $id ) { + if ( isset( $options[$id] ) && ! isset( $input[$id] ) ) + unset( $options[$id] ); + } + + foreach ($input as $key => $val) { + // Match hex codes only. + if ( preg_match('/colour/', $key) && !preg_match( '/^#[a-zA-Z0-9]{6}$/', $val ) ) { + return false; + } + } + + return $input; + } // end validate_settings + +} // end class + +new LiveAdminNavFilter(); \ No newline at end of file diff --git a/screenshot-1.jpg b/screenshot-1.jpg new file mode 100644 index 0000000..8579c92 Binary files /dev/null and b/screenshot-1.jpg differ diff --git a/screenshot-2.jpg b/screenshot-2.jpg new file mode 100644 index 0000000..6623740 Binary files /dev/null and b/screenshot-2.jpg differ diff --git a/screenshot-3.jpg b/screenshot-3.jpg new file mode 100644 index 0000000..9def042 Binary files /dev/null and b/screenshot-3.jpg differ