sferik/rails_admin

View on GitHub
src/rails_admin/filtering-select.js

Summary

Maintainability
C
1 day
Test Coverage
import jQuery from "jquery";
import "jquery-ui/ui/widget.js";
import "jquery-ui/ui/widgets/autocomplete.js";
import I18n from "./i18n";

(function ($) {
  "use strict";

  $.widget("ra.filteringSelect", $.ra.abstractSelect, {
    options: {
      minLength: 0,
      searchDelay: 200,
      remote_source: null,
      source: null,
      xhr: false,
    },

    button: null,
    input: null,
    select: null,
    filtering_select: null,

    _create: function () {
      this.filtering_select = this.element.siblings(
        '[data-input-for="' + this.element.attr("id") + '"]'
      );

      // When using the browser back and forward buttons, it is possible that
      // the autocomplete field will be cached which causes duplicate fields
      // to be generated.
      if (this.filtering_select.length > 0) {
        this.input = this.filtering_select.children("input");
        this.button = this.filtering_select.children(".input-group-btn");
      } else {
        this.element.hide();
        this.filtering_select = this._inputGroup(this.element.attr("id"));
        this.input = this._inputField();
        this.button = this._buttonField();
      }
      this.clearOption = $('<span style="color: #888"></span>').append(
        '<i class="fas fa-times"></i> ' +
          $("<span></span>").text(I18n.t("clear")).html()
      );
      this.noObjectsPlaceholder = $('<option disabled="disabled" />').text(
        I18n.t("no_objects")
      );

      this._setOptionsSource();
      this._initAutocomplete();
      this._initKeyEvent();
      this._overloadRenderItem();
      this._autocompleteDropdownEvent(this.button);

      return this.filtering_select
        .append(this.input)
        .append(this.button)
        .insertAfter(this.element);
    },

    _getResultSet: function (request, data, xhr) {
      var matcher = new RegExp(
        $.ui.autocomplete.escapeRegex(request.term),
        "i"
      );

      var spannedContent = function (content) {
        return $("<span>").text(content).html();
      };

      var highlighter = function (label, word) {
        if (word.length) {
          return $.map(label.split(word), function (el) {
            return spannedContent(el);
          }).join($("<strong>").text(word)[0].outerHTML);
        } else {
          return spannedContent(label);
        }
      };

      var matches = $.map(data, function (el) {
        var id = el.id || el.value;
        var value = el.label || el.id;
        // match regexp only for local requests, remote ones are already
        // filtered, and label may not contain filtered term.
        if (id && (xhr || matcher.test(el.label))) {
          return {
            html: highlighter(value, request.term),
            value: value,
            id: id,
          };
        }
      });

      if (request.term.length === 0 && !this.input.attr("required")) {
        return [{ html: this.clearOption, value: null, id: null }].concat(
          matches
        );
      } else if (matches.length === 0) {
        return [{ html: this.noObjectsPlaceholder, value: null, id: null }];
      } else {
        return matches;
      }
    },

    _getSourceFunction: function (source) {
      var self = this;
      var requestIndex = 0;

      if ($.isArray(source)) {
        return function (request, response) {
          response(self._getResultSet(request, source, false));
        };
      } else if (typeof source === "string") {
        return function (request, response) {
          if (this.xhr) {
            this.xhr.abort();
          }

          this.xhr = $.ajax({
            url: source,
            data: self.options.createQuery(request.term),
            dataType: "json",
            autocompleteRequest: ++requestIndex,
            success: function (data, status) {
              if (this.autocompleteRequest === requestIndex) {
                response(self._getResultSet(request, data, true));
              }
            },
            error: function () {
              if (this.autocompleteRequest === requestIndex) {
                response([]);
              }
            },
          });
        };
      } else {
        return source;
      }
    },

    _setOptionsSource: function () {
      if (this.options.xhr) {
        this.options.source = this.options.remote_source;
      } else {
        this.options.source = this.element
          .children("option")
          .map(function () {
            return { label: $(this).text(), value: this.value };
          })
          .toArray();
      }
    },

    _buttonField: function () {
      return $(
        '<span class="input-group-btn">' +
          '<label class="btn btn-info dropdown-toggle" title="Show All Items" role="button">' +
          "</label>" +
          "</span>"
      );
    },

    _autocompleteDropdownEvent: function (element) {
      var self = this;

      return element.click(function () {
        // close if already visible
        if (self.input.autocomplete("widget").is(":visible")) {
          self.input.autocomplete("close");
          return;
        }

        // pass empty string as value to search for, displaying all results
        self.input.autocomplete("search", "");
        self.input.focus();
      });
    },

    _inputField: function () {
      var input;
      var selected = this.element.children(":selected");
      var value = selected.val() ? selected.text() : "";

      input = $('<input type="text">')
        .val(value)
        .addClass("form-control ra-filtering-select-input")
        .attr("style", this.element.attr("style"))
        .show();

      if (this.element.attr("placeholder")) {
        input.attr("placeholder", this.element.attr("placeholder"));
      }

      if (this.element.attr("required")) {
        input.attr("required", this.element.attr("required"));
        this.element.attr("required", false);
      }

      return input;
    },

    _inputGroup: function (inputFor) {
      return $("<div>")
        .addClass("input-group filtering-select")
        .attr("data-input-for", inputFor);
    },

    _initAutocomplete: function () {
      var self = this;

      return this.input.autocomplete({
        delay: this.options.searchDelay,
        minLength: this.options.minLength,
        source: this._getSourceFunction(this.options.source),
        select: function (event, ui) {
          var option = self.element.find(
            `option[value="${CSS.escape(ui.item.id)}"]`
          );
          self.element.find("option[selected]").attr("selected", false);
          if (option.length > 0) {
            option.attr("selected", "selected");
          } else {
            option = $("<option>")
              .attr("value", ui.item.id)
              .attr("selected", true)
              .text(ui.item.value);
            self.element.append(option);
          }
          self.element.trigger("change", ui.item.id);
          self._trigger("selected", event, {
            item: option,
          });
          $(self.element.parents(".controls")[0])
            .find(".update")
            .removeClass("disabled");
        },
        change: function (event, ui) {
          if (ui.item) {
            return;
          }

          var matcher = new RegExp(
            "^" + $.ui.autocomplete.escapeRegex($(this).val()) + "$",
            "i"
          );
          var valid = false;

          self.element.children("option").each(function () {
            if ($(this).text().match(matcher)) {
              valid = true;
              return false;
            }
          });

          if (valid || $(this).val() !== "") {
            return;
          }

          // remove invalid value, as it didn't match anything
          $(this).val(null);
          self.element.html(
            $('<option value="" selected="selected"></option>')
          );
          self.input.data("ui-autocomplete").term = "";
          $(self.element.parents(".controls")[0])
            .find(".update")
            .addClass("disabled");
          return false;
        },
      });
    },

    _initKeyEvent: function () {
      var self = this;

      return this.input.keyup(function () {
        if ($(this).val().length) {
          return;
        }

        /* Clear select options and trigger change if selected item is deleted */
        return self.element
          .html($('<option value="" selected="selected"></option>'))
          .trigger("change");
      });
    },

    _overloadRenderItem: function () {
      this.input.data("ui-autocomplete")._renderItem = function (ul, item) {
        return $("<li></li>")
          .data("ui-autocomplete-item", item)
          .append($("<a></a>").html(item.html || item.id))
          .appendTo(ul);
      };
    },

    destroy: function () {
      this.input.remove();
      this.button.remove();
      this.element.show();
      this.filtering_select.remove();
      $.Widget.prototype.destroy.call(this);
    },
  });
})(jQuery);