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

    По мотивам топика 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.


    Библиотекой 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.

    Благодарю всех накидавших кармы, это позволило опубликовать статью.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 16
    • +2
      Супер, как раз собираюсь подключить к одному проекту. Наверное, воспользуюсь Вашим улучшением.

      Толи фича, толи баг — нет возможности добавить, к примеру, «Hello», если в списке есть: «Hello world». Т.е. покуда фильтр что-то находит, кнопка «Добавить» не отображается. Это может быть неудобным в ряде специфических случаев.
      • +1
        Можно добавить «Hello world» в список выбранных, после чего можно будет добавить «Hello» новым элементом так как «Hello world» уже не будет находится при поиске :)

        Конечно можно многое сделать оп другому, но это усложнить код, и потребует дополнительного времени, которого у меня не было в тот момент.

        Глубокое тестирование тоже не проводилось если что :)
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Согласен, было бы удобно добавлять позиции по Enter.
        • 0
          Cgfcибо, вовремя! Подключили к проекту, и наткунлись на пробелмы, связанные с модификацией данных внутри списка.
          • 0
            Что за проблемы, как проявляются
          • 0
            А теперь надо сделать форк проекта или отправить патч автору :)
            • 0
              Спасибо.
              А можно ли сделать так, чтобы Chosen обрабатывал список <a> в заданном <div>?
              • +1
                Можно сделать что угодно, но нужно понимать что делаешь. Chosen не случайно в качестве исходных данных использует контрол формы (select), это по сути просто обертка на select, и при сабмите через select долны передаваться отмеченные позиции.
              • +1
                Баг:
                1) набираем «тест»
                2) стираем весь «тест»
                3) профит баг — получаем изначальный список результатов и кнопку 'Add new item «т»'.
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  1. Набираем bug
                  2. Давим Add new item «bug»
                  3. GOTO 1
                  • 0
                    Fixed (добавлен метод DynamicItem.show), демки не обновлялись
                  • +1
                    В новой версии Chosen немного изменилось API поэтому исправленная версия тут jsfiddle.net/kcDZk/
                    • 0
                      Спасибо, может кому-то пригодится, я если честно не следил.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое