"use strict";

/**
 * A widget to select related objects using search and autocomplete.
 *
 * This is particularly useful for models where there can be a ton of rows in
 * the database. The built-in admin widgets provide a pretty poor
 * experience--either populating the list with the entire contents of the
 * table, which is super slow, or just listing PKs, which isn't usable.
 */
Djblets.RelatedObjectSelectorView = Backbone.View.extend({
  className: 'related-object-selector',
  /**
   * The search placeholder text.
   *
   * Subclasses should override this.
   */
  searchPlaceholderText: '',
  /**
   * The element template.
   *
   * Subclasses may override this to change rendering.
   */
  template: _.template(`<select placeholder="<%- searchPlaceholderText %>"
        class="related-object-options"></select>
<% if (multivalued) { %>
<ul class="related-object-selected"></ul>
<% } %>`),
  /**
   * Initialize the view.
   *
   * Args:
   *     options (object):
   *         Options for the view.
   *
   * Option Args:
   *     $input (jQuery):
   *         The ``<input>`` element which should be populated with the list
   *         of selected item PKs.
   *
   *     initialOptions (Array of object):
   *         The initially selected options.
   *
   *     multivalued (boolean):
   *         Whether or not the widget should allow selecting multiple
   *         values.
   *
   *     selectizeOptions (object):
   *          Additional options to pass in to $.selectize.
   */
  initialize(options) {
    this.options = options;
    this._$input = options.$input;
    this._selectizeOptions = options.selectizeOptions;
    this._selectedIDs = new Map();
    _.bindAll(this, 'renderOption');
  },
  /**
   * Render the view.
   *
   * Returns:
   *     RB.RelatedObjectSelectorView:
   *     This object, for chaining.
   */
  render() {
    const self = this;
    this.$el.html(this.template({
      searchPlaceholderText: this.searchPlaceholderText,
      multivalued: this.options.multivalued
    }));
    this._$selected = this.$('.related-object-selected');
    const renderItem = this.options.multivalued ? () => '' : this.renderOption;
    const selectizeOptions = _.defaults(this._selectizeOptions, {
      copyClassesToDropdown: true,
      dropdownParent: 'body',
      preload: 'focus',
      render: {
        item: renderItem,
        option: this.renderOption
      },
      load(query, callback) {
        self.loadOptions(query, data => callback(data.filter(item => !self._selectedIDs.has(item.id))));
      },
      onChange(selected) {
        if (selected) {
          self._onItemSelected(this.options[selected], true);
          if (self.options.multivalued) {
            this.removeOption(selected);
          }
        }
        if (self.options.multivalued) {
          this.clear();
        }
      }
    });
    if (!this.options.multivalued && this.options.initialOptions.length) {
      const item = this.options.initialOptions[0];
      selectizeOptions.options = this.options.initialOptions;
      selectizeOptions.items = [item[selectizeOptions.valueField]];
    }
    this.$('select').selectize(selectizeOptions);
    if (this.options.multivalued) {
      this.options.initialOptions.forEach(item => this._onItemSelected(item, false));
    }
    this._$input.after(this.$el);
    return this;
  },
  /**
   * Update the "official" ``<input>`` element.
   *
   * This copies the list of selected item IDs into the form field which will
   * be submitted.
   */
  _updateInput() {
    this._$input.val(Array.from(this._selectedIDs.keys()).join(','));
  },
  /**
   * Callback for when an item is selected.
   *
   * Args:
   *     item (object):
   *         The newly-selected item.
   *
   *     addToInput (boolean):
   *         Whether the ID of the item should be added to the ``<input>``
   *         field.
   *
   *         This will be ``false`` when populating the visible list from the
   *         value of the form field when the page is initially loaded, and
   *         ``true`` when adding items interactively.
   */
  _onItemSelected(item, addToInput) {
    if (this.options.multivalued) {
      const $li = $('<li>').html(this.renderOption(item));
      const $items = this._$selected.children();
      const text = $li.text();
      $('<span class="remove-item fa fa-close">').click(() => this._onItemRemoved($li, item)).appendTo($li);
      let attached = false;
      for (let i = 0; i < $items.length; i++) {
        const $item = $items.eq(i);
        if ($item.text().localeCompare(text) > 0) {
          $item.before($li);
          attached = true;
          break;
        }
      }
      if (!attached) {
        $li.appendTo(this._$selected);
      }
      this._selectedIDs.set(item.id, item);
      if (addToInput) {
        this._updateInput();
      }
    } else {
      this._selectedIDs = new Map([[item.id, item]]);
      this._updateInput();
    }
  },
  /**
   * Callback for when an item is removed from the list.
   *
   * Args:
   *     $li (jQuery):
   *         The element representing the item in the selected list.
   *
   *     item (object):
   *         The item being removed.
   */
  _onItemRemoved($li, item) {
    $li.remove();
    this._selectedIDs.delete(item.id);
    this._updateInput();
  },
  /**
   * Render an option in the drop-down menu.
   *
   * This should be overridden in order to render type-specific data.
   *
   * Args:
   *     item (object):
   *         The item to render.
   *
   * Returns:
   *     string:
   *     HTML to insert into the drop-down menu.
   */
  renderOption( /* item */
  ) {
    return '';
  },
  /**
   * Load options from the server.
   *
   * This should be overridden in order to make necessary API requests.
   *
   * Args:
   *     query (string):
   *         The string typed in by the user.
   *
   *     callback (function):
   *         A callback to be called once data has been loaded. This should
   *         be passed an array of objects, each representing an option in
   *         the drop-down.
   */
  loadOptions(query, callback) {
    callback();
  }
});

//# sourceMappingURL=relatedObjectSelectorView.js.map