jQuery custom radio and checkbox

Как обычно все началось из-за отсутствия или плохого поиска, или не полной реализации того что мне было необходимо.
А необходимы мне были кастомные radio и checkbox'ы которые я смог бы применять в своей повседневной работе при верстке. При этом они должны были бы работать в IE6+ и всех основных браузерах.
Также элементы должны были реагировать по клику на лейбл. И еще одно их могло быть на 1 странице сколько угодно с разными стилями(да иногда случаются такие мего дизайны).
Поэтому я решил взять все лучшее, что встречал в реализациях чекбоксов и радиобатонов на JS. И написать свой плагин jQuery который удовлетворял бы мои потребности.

Вид элементов реализовывался с помощью спрайта с 4 состояниями:
— неактивный вид;
— неактивный мышь нажата;
— активный;
— активный мышь нажата.
image

Т.к. придется работать со спрайтом то необходимо знать смешения по вертикали картинок в нем. Для этого необходимо передавать в плагин высоту элемента, а в спрайте расположить их так чтоб они занимали одинаковое по высоте пространство.
Также необходимо чтоб при вызове плагина передавался класс, через который я смогу стилизовать элемент.
После первоначальной реализации я понял, что необходимы еще пару условий, без которых плагин будет работать некорректно. Это возможность смены состояний элементов(актинвный/неактивный) и стилизация динамически созданных элементов.

Приведу для начала весь код плагина:

(function ($) {
    $.CustomData = {
        elements:$()
    };

    $.fn.extend({

        Custom:function (options) {

            var elements = this;
            $.CustomData.elements = $.CustomData.elements.add(elements);

            /*Дефолтные значения параметров*/
            var defaults = {
                customStyleClass:"checkbox",
                customHeight:"16"
            };

            /*Заменяем дефолтные опции на переданные если таковые есть*/
            options = $.extend(defaults, options);

            /*Вид при нажатии на активный и неактивный элементы*/
            var pushed = function () {
                var element = $(this).children('input');
                if (element.is(':checked')) {
                    /*смещения в спрайте*/
                    $(this).css('backgroundPosition', "0px -" + options.customHeight * 3 + "px");
                } else {
                    $(this).css('backgroundPosition', "0px -" + options.customHeight + "px");
                }
            };

            /*Отмечаем нажатый елемент все остальные сбрасываем, если они в групе(radio)*/
            var check = function () {
                var element = $(this).children('input');
                if (element.is(':checked') && element.attr('type') === 'checkbox') {/*Отмеченный чекбокс*/
                    $(this).css('backgroundPosition', '0px 0px');
                    $(this).children('input').attr('checked', false).change();
                    /*Меняем атрибут на неотмеченный и вызываем событие смены состояния элемента*/
                } else {
                    if (element.attr('type') === 'checkbox') {/*Неотмеченный чекбокс*/
                        $(this).css('backgroundPosition', "0px -" + options.customHeight * 2 + "px");
                    } else {
                        /*Радиобатоны*/
                        $(this).css('backgroundPosition', "0px -" + options.customHeight * 2 + "px");
                        $('input[name=' + element.attr('name') + ']').not(element).parent().css('backgroundPosition', '0px 0px');
                    }

                    $(this).children('input').attr('checked', 'checked').change();
                }

            };

            /*Обновление картинки при клике по лейблу и загрузке документа*/
            var update = function () {
                $.CustomData.elements.each(function () { /*Проходим по всем елементам и проверяем их состояние*/
                    if ($(this).is(':checked')) {
                        $(this).parent().css('backgroundPosition', "0px -" + $(this).attr('data-height') * 2 + "px");
                    } else {
                        $(this).parent().css('backgroundPosition', "0px 0px");
                    }
                });
            };

            /*Обновление при изменении состояния disabled/enabled */
            var refresh = function () {
                if (!$(this).prop('disabled')) {
                    $(this).parent().mousedown(pushed);
                    $(this).parent().mouseup(check);
                    $(this).parent().removeClass('disabled');
                } else {
                    $(this).parent().addClass('disabled');
                    $(this).parent().unbind('mousedown', pushed);
                    $(this).parent().unbind('mouseup', check);
                }
            };

            return this.each(function () {
                if ($(this).attr('data-init') != '1') {
                    $(this).attr('data-init', '1');
                    $(this).attr('data-height', options.customHeight);
                    /*Оборачиваем в <span></span>*/
                    $(this).wrap('<span/>');
                    /*Приписываем класс оформления переданный в параметрах*/
                    var span = $(this).parent().addClass(options.customStyleClass);
                
                    if ($(this).is(':checked') === true) { /*Задаем картинку еси элемент отмечен*/
                        span.css('backgroundPosition', "0px -" + (options.customHeight * 2) + "px");
                    }

                    /*Бинд на изменение состояния элемента и кастомное событие для обновления после программного изменения состояния кнопки*/
                    $(this).bind('change', update);
                    $(this).bind('custom.refresh', refresh);

                    if (!$(this).prop('disabled')) {
                        /*Бинд функций на span*/
                        span.mousedown(pushed);
                        span.mouseup(check);
                    } else {
                        /*Добавление класса если элемент неактивен*/
                        span.addClass('disabled');
                    }
                }
            });
        }
    });

})(jQuery);

Начал я с реализации структуры плагина, а вид она имеет такой:

(function ($) {
    $.fn.extend({

        Custom:function (options) {

		/*Дефолтные значения параметров*/
		var defaults = {
			customStyleClass:"checkbox",
			customHeight:"16"
		};

		/*Заменяем дефолтные опции на переданные если таковые есть*/
		options = $.extend(defaults, options);

            };

            return this.each(function () {
                
            });
        }
    });

Можно встретить такое описание выше напечатанного — «используется для создания типичного дополнения jQuery".

Начнём с конца:

return this.each(function () {
	if ($(this).attr('data-init') != '1') {
		$(this).attr('data-init', '1');
		$(this).attr('data-height', options.customHeight);
		/*Оборачиваем в <span></span>*/
		$(this).wrap('<span/>');
		/*Приписываем класс оформления переданный в параметрах*/
		var span = $(this).parent().addClass(options.customStyleClass);
	
		if ($(this).is(':checked') === true) { /*Задаем картинку еси элемент отмечен*/
			span.css('backgroundPosition', "0px -" + (options.customHeight * 2) + "px");
		}

		/*Бинд на изменение состояния элемента и кастомное событие для обновления после программного изменения состояния кнопки*/
		$(this).bind('change', update);
		$(this).bind('custom.refresh', refresh);

		if (!$(this).prop('disabled')) {
			/*Бинд функций на span*/
			span.mousedown(pushed);
			span.mouseup(check);
		} else {
			/*Добавление класса если элемент неактивен*/
			span.addClass('disabled');
		}
	}
});

В принципе в комментариях к коду все описано, могу добавить лишь
$(this).attr('data-init', '1');

использую для того чтоб знать стилизован уже элемент или нет(конечно можно и через родителя узнать но я решил протестировать такой вот метод).

$(this).attr('data-height', options.customHeight);

Здесь сохраняю высоту элемента т.к. элементов у меня с различными спрайтами может быть сколько угодно.


$(this).bind('custom.refresh', refresh);

Тоже интересный метод, привязывается на кастомное событие «custom.refresh» вызов функции refresh.
Например изменилось состояние элемента на неактивное, тогда необходимо изменить вид элементы и снять все события с него.
Например:

$('#radio3').removeAttr('disabled').trigger('custom.refresh');

элемент стал неактивен и произошло события «custom.refresh» благодаря которому выполнилась функция refresh.

Далее идут функции которые описаны в этой части:
Custom:function (options) {...};

Функция pushed — ставит смещение в спрайте для активного и неактивного вида, она довольно простая:

var pushed = function () {
	var element = $(this).children('input');
	if (element.is(':checked')) {
		/*смещения в спрайте*/
		$(this).css('backgroundPosition', "0px -" + options.customHeight * 3 + "px");
	} else {
		$(this).css('backgroundPosition', "0px -" + options.customHeight + "px");
	}
};

Фукция check:

/*Отмечаем нажатый элемент все остальные сбрасываем, если они в группе (radio)*/
var check = function () {
	var element = $(this).children('input');
	if (element.is(':checked') && element.attr('type') === 'checkbox') {/*Отмеченный чекбокс*/
		$(this).css('backgroundPosition', '0px 0px');
		$(this).children('input').attr('checked', false).change();
		/*Меняем атрибут на неотмеченный и вызываем событие смены состояния элемента*/
	} else {
		if (element.attr('type') === 'checkbox') {/*Неотмеченный чекбокс*/
			$(this).css('backgroundPosition', "0px -" + options.customHeight * 2 + "px");
		} else {
			/*Радиобатоны*/
			$(this).css('backgroundPosition', "0px -" + options.customHeight * 2 + "px");
			$('input[name=' + element.attr('name') + ']').not(element).parent().css('backgroundPosition', '0px 0px');
		}

		$(this).children('input').attr('checked', 'checked').change();
	}
};

Также все довольно ясно из комментариев в коде добавлю лишь об этом
$(this).children('input').attr('checked', false).change();

.attr('checked', false).change() — необходимо чтоб событие change() файрилось и атрибут checked был изменен. Пришлось поискать это на stackoverflow.

Функция Update:


/*Обновление картинки при клике по лейблу*/
var update = function () {
	$.CustomData.elements.each(function () { /*Проходим по всем елементам и проверяем их состояние*/
		if ($(this).is(':checked')) {
			$(this).parent().css('backgroundPosition', "0px -" + $(this).attr('data-height') * 2 + "px");
		} else {
			$(this).parent().css('backgroundPosition', "0px 0px");
		}
	});
};

Тут не вышло обойтись без переменной, куда можно было б сохранить все элементы, на которых вызывается плагин($.CustomData.elements). Описывается она выше $.fn.extend({...})

$.CustomData = {
    elements:$()
}; 


и при вызове плагина в нее помещается набор элементов

var elements = this;
$.CustomData.elements = $.CustomData.elements.add(elements);


И последняя функция, которая необходима при изменении состояния элемента refresh:

/*Обновление при изменении состояния disabled/enabled */
var refresh = function () {
	if (!$(this).prop('disabled')) {
		$(this).parent().mousedown(pushed);
		$(this).parent().mouseup(check);
		$(this).parent().removeClass('disabled');
	} else {
		$(this).parent().addClass('disabled');
		$(this).parent().unbind('mousedown', pushed);
		$(this).parent().unbind('mouseup', check);
	}
};


Довольно простая в понимании, просто снимаем обработчики или добавляем вновь и дописываем/убираем класс «disabled», через который можно задать вид элемента в неактивном состоянии(обычно прозрачность меняют).

Пример класса с описанием вида радиобатона:
.radio {
    display:block;
	height: 25px;
	width: 19px;
    overflow:hidden;
	background: url("radio.png") no-repeat 0 0 transparent;
	position:relative;
}

.radio.disabled{
	opacity:0.5;
	filter:Alpha(opacity="50");
}

.radio input{
	position:absolute;
	right:-400px;
	top:0px;
}


Вызов плагина:

$("input[type='radio']").Custom({
	customStyleClass:'radio',
	customHeight:'25'
});


Надеюсь это поможет таким как я в написании своих первых плагинов на jQuery.

Это все о чем я хотел поведать в связи со своими плохими поисками нужного мне плагина. Благодарю хорошего человека Андрея за помощь в познании jQuery и написании плагина а также разработчика вот этого плагина http://ryanfait.com/resources/custom-checkboxes-and-radio-buttons/ за то что он был на js и не до конца допилен, дав мне возможность для написания своего с необходимыми мне фишками. Буду рад коментам и критике для улучшения работы плангина.

Плагин тут https://github.com/n0r8/Custom-radio-checkbox
Share post

Comments 20

    –2
    Взял на вооружение. Спасибо! :)
      0
      В chromium 18 в демо вижу оригинальные чекбоксы и радио справа от текста. Из них (оригинальных) кликаются только радио.
        0
        Оригинальные стоят только для того чтоб показать что все работает. Кликать по ним нет необходимости. Можно добавить overflow:hidden; в стиль для радио и чекбокс оберток.
        0
        bind -> live
        А стандартный костыль с прозрачным элементом и подложкой в виде картинки нельзя было здесь применить?
          +3
          Вы извините, но это кошмар.
          if (!$(this).prop('disabled')) {
          	$(this).parent().mousedown(pushed);
          	$(this).parent().mouseup(check);
          	$(this).parent().removeClass('disabled');
          } else {
          	$(this).parent().addClass('disabled');
          	$(this).parent().unbind('mousedown', pushed);
          	$(this).parent().unbind('mouseup', check);
          }


          Переписать бы код.
            +1
            типа так $(this).parent().mousedown(pushed).parent().mouseup(check).parent().removeClass('disabled');?
              0
              ну тоесть без парент:)
                +2
                $(this).parent() выносим в переменную.
                  0
                  if (!$(this).prop('disabled')) {
                    $(this).parent().mousedown(pushed).mouseup(check).removeClass('disabled');
                  } else {
                    $(this).parent().addClass('disabled').unbind('mousedown', pushed).unbind('mouseup', check);
                  }

                  Думаю так лучше будет т.к. вызов $(this).parent() 1 раз происходит в любом случае.
                    +2
                    Все $(this) вынести в $this не помешает, зачем вызывать jq каждый раз. Цепочки само собой.
                      0
                      пасиба постараюсь исправить
              0
              Получать каждый раз необходимые элементы заново типичное порождение jQuery :) Просто пользователи jQuery разработчиками не являются, отсюда и такой код.
                +1
                Немного больше понимания самого js и такого уже не бывает. Все же учатся на чем-то.
                  +1
                  Для многих JS это jQuery, о чем речь вообще )
              +3
              Очень жаль что люди досих пор пишут плагины, а не юишные виджеты. Вроде разница в коде между виджетами и плагинами не большая, но уже намекает на то что человек таки прочел мануал. Из плюсов виджетов стандартное API из коробки, темизация(если автор не поленился реализовать ее поддержку)

              PS Прошу прощения за офтоп.
                0
                Мне вот что интересно, почему все оборачивают в span, а не в label?
                • UFO just landed and posted this here
                  0
                  странно, что
                  .attr('checked', false)
                  .attr('checked', 'checked')
                  .is(':checked')
                  

                  но
                  .prop('disabled')
                  


                  может так:?
                  .prop('checked', false)
                  .prop('checked', true)
                  .prop('checked')
                  
                    0
                    $('input[name=' + element.attr('name') + ']')
                    

                    name у элементов может быть и посложнее qname, в т.ч. содержать скобки
                      0
                      спасибо

                  Only users with full accounts can post comments. Log in, please.