Как стать автором
Обновить

Виджет выпадающих списков Chosen: реализуем динамическое добавление позиций

Время на прочтение 8 мин
Количество просмотров 18K
По мотивам топика Chosen: сделай выпадающие списки более дружественными.

Довольно симпатичный виджет, иногда даже может быть полезен (допустим когда есть определенные требования к дизайну). Но в данный момент виджет не позволят добавлять позиции динамически, что возмутило товарища alexsrdk, да и меня тоже :) Сейчас попробуем это дело исправить.

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

Вариант с jQuery версией


Патчим код виджета

В jQuery версии я не нашел способа добраться до основного класса виджета «Chosen», поэтому пришлось немного патчить основной исходник виджета (понимаю что это плохой подход).

Вариантов добраться до класса Chosen можно придумать много, мне в голову приходит как минимуму 3:
  1. Изменить тело кастом функции jQuery.fn.chosen подобным образом
    $.fn.extend({
      chosen: function(data, options) {
        var createdInstances = [];
        $(this).each(function(input_field) {
          if (!($(this)).hasClass("chzn-done")) {
            createdInstances.push(new Chosen(this, data, options));
          }
        });
        return createdInstances;
      }
    });


    * This source code was highlighted with Source Code Highlighter.

  2. Можно сохранять созданный экземпляр в хранилище элемента
    $.fn.extend({
      chosen: function(data, options) {
        return $(this).each(function(input_field) {
          if (!($(this)).hasClass("chzn-done")) {
            return $(this).data('chosenInstance', new Chosen(this, data, options));
          }
        });
      }
    });


    * This source code was highlighted with Source Code Highlighter.

    Получить объект Chosen можно подобным образом
    var createdChosenInstance = $('#bears_multiple').chosen().data('chosenInstance');

    * This source code was highlighted with Source Code Highlighter.

  3. Можно сделать отдельную функцию на получения класса
    $.fn.extend({
      chosenClass: function() {
        return Chosen;
      }
    });


    * This source code was highlighted with Source Code Highlighter.


  4. Библиотекой jQuery пользуюсь очень редко (в основном работаю с YUI), с API под рукой, поэтому вероятно эти варианты не оптимальны.

    В последующем коде используется третий вариант доступа к классу виджета. Модификация класса Chosen будет происходить на уровне prototype (методы будут общими, то есть изменения коснуться всех вновь создаваемых экземпляров класса). В принципе можно расширять уже созданные объекты (получая созданные объекты по вариантам 1 или 2), но если изменения должны коснуться всех виджетов, лучше работать с прототипом.

    Основной код расширения функционала jQuery версии виджета

    (function($, ChosenClass) {
      var dynamicItemInstance;

      function DynamicItem(chosenInstance) {
        $((this.chosen = chosenInstance).search_results).parent().prepend(
            this.elContainer = $(document.createElement('div')));
        this.elContainer.addClass('chzn-results-additemcontainer');
        this.elContainer.append(this.elButton = $(document.createElement('button')));
        this.elButton.click($.proxy(this.addNewItem, this));
      }
      DynamicItem.prototype = {
        constructor: DynamicItem,
        show: function() {
          var data = this.chosen.results_data,
            text = this.text,
            isNotSelected = true;

          if (this.chosen.choices) {
            (this.chosen.search_choices.find("li.search-choice").each(function(el) {
              var itemIdx = this.id.substr(this.id.lastIndexOf("_") + 1),
                item = data[itemIdx];

              if (item.value === text) {
                isNotSelected = !isNotSelected;
                return false;
              }
            }));
          }

          this.elContainer[isNotSelected ? 'show' : 'hide']();
        },
        update: function(terms) {
          if ((this.text = terms).length) {
            this.elButton.text('Add new item "' + this.text + '"');
            this.show();
          } else {
            this.elContainer.hide();
          }
        },
        addNewItem: function(terms) {
          this.chosen.form_field.options.add(new Option(this.text, this.text));
          this.chosen.form_field_jq.trigger('liszt:updated');
          this.chosen.result_highlight = this.chosen.search_results.children().last();
          return this.chosen.result_select();
        }
      };

      $.extend(ChosenClass.prototype, {
        no_results: (function(fnSuper) {
          return function(terms) {
            (dynamicItemInstance || (dynamicItemInstance = new DynamicItem(this))).update(terms);
            return fnSuper.call(this, terms);
          };
        })(ChosenClass.prototype.no_results),
        results_hide: (function(fnSuper) {
          return function() {
            dynamicItemInstance && dynamicItemInstance.elContainer.hide();
            return fnSuper.call(this);
          };
        })(ChosenClass.prototype.results_hide),
        winnow_results_set_highlight: (function(fnSuper) {
          return function() {
            dynamicItemInstance && dynamicItemInstance.elContainer.hide();
            return fnSuper.apply(this, arguments);
          };
        })(ChosenClass.prototype.winnow_results_set_highlight)
      });
    })(jQuery, jQuery.fn.chosenClass());

    * This source code was highlighted with Source Code Highlighter.

    Немного проанализировав исходники виджета, прикинул как можно внедрить динамическое добавление позиций. Сhosen методы пееропределены с использованием замыканий, функционал добавления позиции в отдельном классе. Конечно при необходимости кнопку можно заменить на ссылку, добавить CSS стилей (маркерующий класс на блок кнопки установлен) и так далее.

    Демо

    Вариант с Prototype версией


    В Prototype версии виджета класс Chosen доступен глобально (window.Chosen) поэтому патчить ничего не пришлось.
    (function(Chosen) {
      var dynamicItemInstance;

      function DynamicItem(chosenInstance) {
        (this.chosen = chosenInstance).search_results.up().insert({
          top: this.elContainer = $(document.createElement('div'))
        });
        this.elContainer.addClassName('chzn-results-additemcontainer');
        this.elContainer.insert(this.elButton = $(document.createElement('button')));
        Event.observe(this.elButton, 'click', this.addNewItem.bind(this));
      }
      DynamicItem.prototype = {
        constructor: DynamicItem,
        show: function() {
          var data = this.chosen.results_data,
            text = this.text,
            isNotSelected = true;

          if (this.chosen.choices) {
            (this.chosen.search_choices.select("li.search-choice").each(function(el) {
              var itemIdx = el.id.substr(el.id.lastIndexOf("_") + 1),
                item = data[itemIdx];

              if (item.value === text) {
                isNotSelected = !isNotSelected;
                return false;
              }
            }));
          }

          this.elContainer[isNotSelected ? 'show' : 'hide']();
        },
        update: function(terms) {
          if ((this.text = terms).length) {
            this.elButton.update('Add new item "' + this.text + '"');
            this.show();
          } else {
            this.elContainer.hide();
          }
        },
        addNewItem: function(terms) {
          this.chosen.form_field.options.add(new Option(this.text, this.text));
          Event.fire(this.chosen.form_field, "liszt:updated");
          this.chosen.result_highlight = this.chosen.search_results.childElements().pop();
          return this.chosen.result_select();
        }
      };

      Chosen.prototype.no_results = (function(fnSuper) {
        return function(terms) {
          (dynamicItemInstance || (dynamicItemInstance = new DynamicItem(this))).update(terms);
          return fnSuper.call(this, terms);
        };
      })(Chosen.prototype.no_results);
      Chosen.prototype.results_hide = (function(fnSuper) {
        return function() {
          dynamicItemInstance && dynamicItemInstance.elContainer.hide();
          return fnSuper.call(this);
        };
      })(Chosen.prototype.results_hide);
      Chosen.prototype.winnow_results_set_highlight = (function(fnSuper) {
        return function() {
          dynamicItemInstance && dynamicItemInstance.elContainer.hide();
          return fnSuper.apply(this, arguments);
        };
      })(Chosen.prototype.winnow_results_set_highlight);
    })(window.Chosen);

    new Chosen($('bears_multiple'));

    * This source code was highlighted with Source Code Highlighter.

    Демо

    Замечания


    На месте автора виджета, я бы реализовывал общее ядро (вместо отдельных версий для Prototype/jQuery/Mootools). Из библиотек использовал бы только самые необходимые методы, через обертку (интерфейс). Цель этого — иметь одну (общую) версию основного кода виджета. Сейчас, при теперешнем подходе форков под разные фреймворки, не вижу большого смысла комитить что-то на github.

    Благодарю всех накидавших кармы, это позволило опубликовать статью.
Теги:
Хабы:
+65
Комментарии 16
Комментарии Комментарии 16

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн