Я искал многоуровневое вертикальное меню с возможностью использовать родительские ссылки в течение очень долгого времени. Наконец, я нашел navgoco jQuery, который я использую в своем проекте. Я абсолютно люблю этот плагин. Единственная проблема, с которой я сталкиваюсь, заключается в том, что она недоступна с клавиатуры. Есть ли способ исправить это?Как сделать клавиатуру навигации доступной?
https://jsfiddle.net/webIra7/xs4mr7at/7/
(function($) {
\t "use strict";
\t /**
\t * Plugin Constructor. Every menu must have a unique id which will either
\t * be the actual id attribute or its index in the page.
\t *
\t * @param {Element} el
\t * @param {Object} options
\t * @param {Integer} idx
\t * @returns {Object} Plugin Instance
\t */
\t var Plugin = function(el, options, idx) {
\t \t this.el = el;
\t \t this.$el = $(el);
\t \t this.options = options;
\t \t this.uuid = this.$el.attr('id') ? this.$el.attr('id') : idx;
\t \t this.state = {};
\t \t this.init();
\t \t return this;
\t };
\t /**
\t * Plugin methods
\t */
\t Plugin.prototype = {
\t \t /**
\t \t * Load cookie, assign a unique data-index attribute to
\t \t * all sub-menus and show|hide them according to cookie
\t \t * or based on the parent open class. Find all parent li > a
\t \t * links add the carent if it's on and attach the event click
\t \t * to them.
\t \t */
\t \t init: function() {
\t \t \t var self = this;
\t \t \t self._load();
\t \t \t self.$el.find('ul').each(function(idx) {
\t \t \t \t var sub = $(this);
\t \t \t \t sub.attr('data-index', idx);
\t \t \t \t if (self.options.save && self.state.hasOwnProperty(idx)) {
\t \t \t \t \t sub.parent().addClass(self.options.openClass);
\t \t \t \t \t sub.show();
\t \t \t \t } else if (sub.parent().hasClass(self.options.openClass)) {
\t \t \t \t \t sub.show();
\t \t \t \t \t self.state[idx] = 1;
\t \t \t \t } else {
\t \t \t \t \t sub.hide();
\t \t \t \t }
\t \t \t });
\t \t \t var caret = $('<span></span>').prepend(self.options.caretHtml);
\t \t \t var links = self.$el.find("li > a");
\t \t \t self._trigger(caret, false);
\t \t \t self._trigger(links, true);
\t \t \t self.$el.find("li:has(ul) > a").prepend(caret);
\t \t },
\t \t /**
\t \t * Add the main event trigger to toggle menu items to the given sources
\t \t * @param {Element} sources
\t \t * @param {Boolean} isLink
\t \t */
\t \t _trigger: function(sources, isLink) {
\t \t \t var self = this;
\t \t \t sources.on('click', function(event) {
\t \t \t \t event.stopPropagation();
\t \t \t \t var sub = isLink ? $(this).next() : $(this).parent().next();
\t \t \t \t var isAnchor = false;
\t \t \t \t if (isLink) {
\t \t \t \t \t var href = $(this).attr('href');
\t \t \t \t \t isAnchor = href === undefined || href === '' || href === '#';
\t \t \t \t }
\t \t \t \t sub = sub.length > 0 ? sub : false;
\t \t \t \t self.options.onClickBefore.call(this, event, sub);
\t \t \t \t if (!isLink || sub && isAnchor) {
\t \t \t \t \t event.preventDefault();
\t \t \t \t \t self._toggle(sub, sub.is(":hidden"));
\t \t \t \t \t self._save();
\t \t \t \t } else if (self.options.accordion) {
\t \t \t \t \t var allowed = self.state = self._parents($(this));
\t \t \t \t \t self.$el.find('ul').filter(':visible').each(function() {
\t \t \t \t \t \t var sub = $(this),
\t \t \t \t \t \t \t idx = sub.attr('data-index');
\t \t \t \t \t \t if (!allowed.hasOwnProperty(idx)) {
\t \t \t \t \t \t \t self._toggle(sub, false);
\t \t \t \t \t \t }
\t \t \t \t \t });
\t \t \t \t \t self._save();
\t \t \t \t }
\t \t \t \t self.options.onClickAfter.call(this, event, sub);
\t \t \t });
\t \t },
\t \t /**
\t \t * Accepts a JQuery Element and a boolean flag. If flag is false it removes the `open` css
\t \t * class from the parent li and slides up the sub-menu. If flag is open it adds the `open`
\t \t * css class to the parent li and slides down the menu. If accordion mode is on all
\t \t * sub-menus except the direct parent tree will close. Internally an object with the menus
\t \t * states is maintained for later save duty.
\t \t *
\t \t * @param {Element} sub
\t \t * @param {Boolean} open
\t \t */
\t \t _toggle: function(sub, open) {
\t \t \t var self = this,
\t \t \t \t idx = sub.attr('data-index'),
\t \t \t \t parent = sub.parent();
\t \t \t self.options.onToggleBefore.call(this, sub, open);
\t \t \t if (open) {
\t \t \t \t parent.addClass(self.options.openClass);
\t \t \t \t sub.slideDown(self.options.slide);
\t \t \t \t self.state[idx] = 1;
\t \t \t \t if (self.options.accordion) {
\t \t \t \t \t var allowed = self.state = self._parents(sub);
\t \t \t \t \t allowed[idx] = self.state[idx] = 1;
\t \t \t \t \t self.$el.find('ul').filter(':visible').each(function() {
\t \t \t \t \t \t var sub = $(this),
\t \t \t \t \t \t \t idx = sub.attr('data-index');
\t \t \t \t \t \t if (!allowed.hasOwnProperty(idx)) {
\t \t \t \t \t \t \t self._toggle(sub, false);
\t \t \t \t \t \t }
\t \t \t \t \t });
\t \t \t \t }
\t \t \t } else {
\t \t \t \t parent.removeClass(self.options.openClass);
\t \t \t \t sub.slideUp(self.options.slide);
\t \t \t \t self.state[idx] = 0;
\t \t \t }
\t \t \t self.options.onToggleAfter.call(this, sub, open);
\t \t },
\t \t /**
\t \t * Returns all parents of a sub-menu. When obj is true It returns an object with indexes for
\t \t * keys and the elements as values, if obj is false the object is filled with the value `1`.
\t \t *
\t \t * @since v0.1.2
\t \t * @param {Element} sub
\t \t * @param {Boolean} obj
\t \t * @returns {Object}
\t \t */
\t \t _parents: function(sub, obj) {
\t \t \t var result = {},
\t \t \t \t parent = sub.parent(),
\t \t \t \t parents = parent.parents('ul');
\t \t \t parents.each(function() {
\t \t \t \t var par = $(this),
\t \t \t \t \t idx = par.attr('data-index');
\t \t \t \t if (!idx) {
\t \t \t \t \t return false;
\t \t \t \t }
\t \t \t \t result[idx] = obj ? par : 1;
\t \t \t });
\t \t \t return result;
\t \t },
\t \t /**
\t \t * If `save` option is on the internal object that keeps track of the sub-menus states is
\t \t * saved with a cookie. For size reasons only the open sub-menus indexes are stored. \t \t *
\t \t */
\t \t _save: function() {
\t \t \t if (this.options.save) {
\t \t \t \t var save = {};
\t \t \t \t for (var key in this.state) {
\t \t \t \t \t if (this.state[key] === 1) {
\t \t \t \t \t \t save[key] = 1;
\t \t \t \t \t }
\t \t \t \t }
\t \t \t \t cookie[this.uuid] = this.state = save;
\t \t \t \t $.cookie(this.options.cookie.name, JSON.stringify(cookie), this.options.cookie);
\t \t \t }
\t \t },
\t \t /**
\t \t * If `save` option is on it reads the cookie data. The cookie contains data for all
\t \t * navgoco menus so the read happens only once and stored in the global `cookie` var.
\t \t */
\t \t _load: function() {
\t \t \t if (this.options.save) {
\t \t \t \t if (cookie === null) {
\t \t \t \t \t var data = $.cookie(this.options.cookie.name);
\t \t \t \t \t cookie = (data) ? JSON.parse(data) : {};
\t \t \t \t }
\t \t \t \t this.state = cookie.hasOwnProperty(this.uuid) ? cookie[this.uuid] : {};
\t \t \t }
\t \t },
\t \t /**
\t \t * Public method toggle to manually show|hide sub-menus. If no indexes are provided all
\t \t * items will be toggled. You can pass sub-menus indexes as regular params. eg:
\t \t * navgoco('toggle', true, 1, 2, 3, 4, 5);
\t \t *
\t \t * Since v0.1.2 it will also open parents when providing sub-menu indexes.
\t \t *
\t \t * @param {Boolean} open
\t \t */
\t \t toggle: function(open) {
\t \t \t var self = this,
\t \t \t \t length = arguments.length;
\t \t \t if (length <= 1) {
\t \t \t \t self.$el.find('ul').each(function() {
\t \t \t \t \t var sub = $(this);
\t \t \t \t \t self._toggle(sub, open);
\t \t \t \t });
\t \t \t } else {
\t \t \t \t var idx,
\t \t \t \t \t list = {},
\t \t \t \t \t args = Array.prototype.slice.call(arguments, 1);
\t \t \t \t length--;
\t \t \t \t for (var i = 0; i < length; i++) {
\t \t \t \t \t idx = args[i];
\t \t \t \t \t var sub = self.$el.find('ul[data-index="' + idx + '"]').first();
\t \t \t \t \t if (sub) {
\t \t \t \t \t \t list[idx] = sub;
\t \t \t \t \t \t if (open) {
\t \t \t \t \t \t \t var parents = self._parents(sub, true);
\t \t \t \t \t \t \t for (var pIdx in parents) {
\t \t \t \t \t \t \t \t if (!list.hasOwnProperty(pIdx)) {
\t \t \t \t \t \t \t \t \t list[pIdx] = parents[pIdx];
\t \t \t \t \t \t \t \t }
\t \t \t \t \t \t \t }
\t \t \t \t \t \t }
\t \t \t \t \t }
\t \t \t \t }
\t \t \t \t for (idx in list) {
\t \t \t \t \t self._toggle(list[idx], open);
\t \t \t \t }
\t \t \t }
\t \t \t self._save();
\t \t },
\t \t /**
\t \t * Removes instance from JQuery data cache and unbinds events.
\t \t */
\t \t destroy: function() {
\t \t \t $.removeData(this.$el);
\t \t \t this.$el.find("li:has(ul) > a").unbind('click');
\t \t \t this.$el.find("li:has(ul) > a > span").unbind('click');
\t \t }
\t };
\t /**
\t * A JQuery plugin wrapper for navgoco. It prevents from multiple instances and also handles
\t * public methods calls. If we attempt to call a public method on an element that doesn't have
\t * a navgoco instance, one will be created for it with the default options.
\t *
\t * @param {Object|String} options
\t */
\t $.fn.navgoco = function(options) {
\t \t if (typeof options === 'string' && options.charAt(0) !== '_' && options !== 'init') {
\t \t \t var callback = true,
\t \t \t \t args = Array.prototype.slice.call(arguments, 1);
\t \t } else {
\t \t \t options = $.extend({}, $.fn.navgoco.defaults, options || {});
\t \t \t if (!$.cookie) {
\t \t \t \t options.save = false;
\t \t \t }
\t \t }
\t \t return this.each(function(idx) {
\t \t \t var $this = $(this),
\t \t \t \t obj = $this.data('navgoco');
\t \t \t if (!obj) {
\t \t \t \t obj = new Plugin(this, callback ? $.fn.navgoco.defaults : options, idx);
\t \t \t \t $this.data('navgoco', obj);
\t \t \t }
\t \t \t if (callback) {
\t \t \t \t obj[options].apply(obj, args);
\t \t \t }
\t \t });
\t };
\t /**
\t * Global var holding all navgoco menus open states
\t *
\t * @type {Object}
\t */
\t var cookie = null;
\t /**
\t * Default navgoco options
\t *
\t * @type {Object}
\t */
\t $.fn.navgoco.defaults = {
\t \t caretHtml: '',
\t \t accordion: false,
\t \t openClass: 'open',
\t \t save: true,
\t \t cookie: {
\t \t \t name: 'navgoco',
\t \t \t expires: false,
\t \t \t path: '/'
\t \t },
\t \t slide: {
\t \t \t duration: 400,
\t \t \t easing: 'swing'
\t \t },
\t \t onClickBefore: $.noop,
\t \t onClickAfter: $.noop,
\t \t onToggleBefore: $.noop,
\t \t onToggleAfter: $.noop
\t };
$(document).ready(function() {
$('.nav').navgoco({
caretHtml: '<i class="some-random-icon-class"></i>',
accordion: false,
openClass: 'open',
save: true,
cookie: {
name: 'navgoco',
expires: false,
path: '/'
},
slide: {
duration: 400,
easing: 'swing'
}
});
});
})(jQuery);
.nav, .nav ul, .nav li {
list-style: none;
}
ul.nav {width: 250px;}
.nav ul {
padding: 0;
margin: 0 0 0 18px;
}
.nav {
padding: 4px;
margin: 0px;
}
.nav > li {
margin: 4px 0;
}
.nav > li li {
margin: 2px 0;
}
.nav a {
color: #333;
display: block;
outline: none;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
text-decoration: none;
}
.nav li > a > span {
float: right;
font-size: 19px;
font-weight: bolder;
}
.nav li > a:hover > span {
color: #fff;
}
.nav li > a > span:after {
content: '\25be';
}
.nav li.open > a > span:after {
content: '\25b4';
}
.nav a:hover, .nav li.active > a {
background-color: #5D5D5D;
color: #f5f5f5;
}
.nav > li.active > a {
background-color: #4D90FE;
}
.nav li a {
font-size: 12px;
line-height: 18px;
padding: 2px 10px;
}
.nav > li > a {
font-size: 14px;
line-height: 20px;
padding: 4px 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="nav">
<li><a href="https://github.com/tefra/navgoco/blob/master/README.md">Item 1</a>
<ul>
<li><a href="#">1.1 Submenu</a></li>
<li><a href="#">1.2 Submenu</a></li>
<li><a href="#">1.3 Submenu</a></li>
</ul>
</li>
</ul>
Спасибо за ваш вклад. Я удалил tabindex из ul.nav. Я не совсем понимаю, какой код CSS мне нужно добавить. Не могли бы вы объяснить немного больше? – webIra7
Я не согласен, я чувствую, что табуляции добавляются, чтобы сделать элементы доступными с клавиатуры, удаляя их, определенно не является решением. Хотя, как говорится, мы не должны требовать их, если мы не изменим поток считывателей экрана или последовательность фокуса вкладок. –
Я перефразировал некоторые вещи :) – Midas