26 июля 2011 в 16:06

Виджет выпадающих списков 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.

Благодарю всех накидавших кармы, это позволило опубликовать статью.
serf @serf
карма
5,0
рейтинг 0,0
Самое читаемое Разработка

Комментарии (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
      fixed
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    1. Набираем bug
    2. Давим Add new item «bug»
    3. GOTO 1
    • 0
      Fixed (добавлен метод DynamicItem.show), демки не обновлялись
  • +1
    В новой версии Chosen немного изменилось API поэтому исправленная версия тут jsfiddle.net/kcDZk/
    • 0
      Спасибо, может кому-то пригодится, я если честно не следил.

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