text: define([
      'jquery',
      './options',
      './utils',
      './keys'
  ], function ($, Options, Utils, KEYS) {
      var Select2 = function ($element, options) {
          if ($element.data('select2') != null) {
              $element.data('select2').destroy();
          }
  
          this.$element = $element;
  
          this.id = this._generateId($element);
  
          options = options || {};
  
          this.options = new Options(options, $element);
  
          Select2.__super__.constructor.call(this);
  
          // Set up the tabindex
  
          var tabindex = $element.attr('tabindex') || 0;
          $element.data('old-tabindex', tabindex);
          $element.attr('tabindex', '-1');
  
          // Set up containers and adapters
  
          var DataAdapter = this.options.get('dataAdapter');
          this.dataAdapter = new DataAdapter($element, this.options);
  
          var $container = this.render();
  
          this._placeContainer($container);
  
          var SelectionAdapter = this.options.get('selectionAdapter');
          this.selection = new SelectionAdapter($element, this.options);
          this.$selection = this.selection.render();
  
          this.selection.position(this.$selection, $container);
  
          var DropdownAdapter = this.options.get('dropdownAdapter');
          this.dropdown = new DropdownAdapter($element, this.options);
          this.$dropdown = this.dropdown.render();
  
          this.dropdown.position(this.$dropdown, $container);
  
          var ResultsAdapter = this.options.get('resultsAdapter');
          this.results = new ResultsAdapter($element, this.options, this.dataAdapter);
          this.$results = this.results.render();
  
          this.results.position(this.$results, this.$dropdown);
  
          // Bind events
  
          var self = this;
  
          // Bind the container to all of the adapters
          this._bindAdapters();
  
          // Register any DOM event handlers
          this._registerDomEvents();
  
          // Register any internal event handlers
          this._registerDataEvents();
          this._registerSelectionEvents();
          this._registerDropdownEvents();
          this._registerResultsEvents();
          this._registerEvents();
  
          // Set the initial state
          this.dataAdapter.current(function (initialData) {
              self.trigger('selection:update', {
                  data: initialData
              });
          });
  
          // Hide the original select
          $element.addClass('select2-hidden-accessible');
          $element.attr('aria-hidden', 'true');
  
          // Synchronize any monitored attributes
          this._syncAttributes();
  
          $element.data('select2', this);
      };
  
      Utils.Extend(Select2, Utils.Observable);
  
      Select2.prototype._generateId = function ($element) {
          var id = '';
  
          if ($element.attr('id') != null) {
              id = $element.attr('id');
          } else if ($element.attr('name') != null) {
              id = $element.attr('name') + '-' + Utils.generateChars(2);
          } else {
              id = Utils.generateChars(4);
          }
  
          id = id.replace(/(:|\.|\[|\]|,)/g, '');
          id = 'select2-' + id;
  
          return id;
      };
  
      Select2.prototype._placeContainer = function ($container) {
          $container.insertAfter(this.$element);
  
          var width = this._resolveWidth(this.$element, this.options.get('width'));
  
          if (width != null) {
              $container.css('width', width);
          }
      };
  
      Select2.prototype._resolveWidth = function ($element, method) {
          var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;
  
          if (method == 'resolve') {
              var styleWidth = this._resolveWidth($element, 'style');
  
              if (styleWidth != null) {
                  return styleWidth;
              }
  
              return this._resolveWidth($element, 'element');
          }
  
          if (method == 'element') {
              var elementWidth = $element.outerWidth(false);
  
              if (elementWidth <= 0) {
                  return 'auto';
              }
  
              return elementWidth + 'px';
          }
  
          if (method == 'style') {
              var style = $element.attr('style');
  
              if (typeof (style) !== 'string') {
                  return null;
              }
  
              var attrs = style.split(';');
  
              for (var i = 0, l = attrs.length; i < l; i = i + 1) {
                  var attr = attrs[i].replace(/\s/g, '');
                  var matches = attr.match(WIDTH);
  
                  if (matches !== null && matches.length >= 1) {
                      return matches[1];
                  }
              }
  
              return null;
          }
  
          return method;
      };
  
      Select2.prototype._bindAdapters = function () {
          this.dataAdapter.bind(this, this.$container);
          this.selection.bind(this, this.$container);
  
          this.dropdown.bind(this, this.$container);
          this.results.bind(this, this.$container);
      };
  
      Select2.prototype._registerDomEvents = function () {
          var self = this;
  
          this.$element.on('change.select2', function () {
              self.dataAdapter.current(function (data) {
                  self.trigger('selection:update', {
                      data: data
                  });
              });
          });
  
          this.$element.on('focus.select2', function (evt) {
              self.trigger('focus', evt);
          });
  
          this._syncA = Utils.bind(this._syncAttributes, this);
          this._syncS = Utils.bind(this._syncSubtree, this);
  
          if (this.$element[0].attachEvent) {
              this.$element[0].attachEvent('onpropertychange', this._syncA);
          }
  
          var observer = window.MutationObserver ||
              window.WebKitMutationObserver ||
              window.MozMutationObserver
          ;
  
          if (observer != null) {
              this._observer = new observer(function (mutations) {
                  $.each(mutations, self._syncA);
                  $.each(mutations, self._syncS);
              });
              this._observer.observe(this.$element[0], {
                  attributes: true,
                  childList: true,
                  subtree: false
              });
          } else if (this.$element[0].addEventListener) {
              this.$element[0].addEventListener(
                  'DOMAttrModified',
                  self._syncA,
                  false
              );
              this.$element[0].addEventListener(
                  'DOMNodeInserted',
                  self._syncS,
                  false
              );
              this.$element[0].addEventListener(
                  'DOMNodeRemoved',
                  self._syncS,
                  false
              );
          }
      };
  
      Select2.prototype._registerDataEvents = function () {
          var self = this;
  
          this.dataAdapter.on('*', function (name, params) {
              self.trigger(name, params);
          });
      };
  
      Select2.prototype._registerSelectionEvents = function () {
          var self = this;
          var nonRelayEvents = ['toggle', 'focus'];
  
          this.selection.on('toggle', function () {
              self.toggleDropdown();
          });
  
          this.selection.on('focus', function (params) {
              self.focus(params);
          });
  
          this.selection.on('*', function (name, params) {
              if ($.inArray(name, nonRelayEvents) !== -1) {
                  return;
              }
  
              self.trigger(name, params);
          });
      };
  
      Select2.prototype._registerDropdownEvents = function () {
          var self = this;
  
          this.dropdown.on('*', function (name, params) {
              self.trigger(name, params);
          });
      };
  
      Select2.prototype._registerResultsEvents = function () {
          var self = this;
  
          this.results.on('*', function (name, params) {
              self.trigger(name, params);
          });
      };
  
      Select2.prototype._registerEvents = function () {
          var self = this;
  
          this.on('open', function () {
              self.$container.addClass('select2-container--open');
          });
  
          this.on('close', function () {
              self.$container.removeClass('select2-container--open');
          });
  
          this.on('enable', function () {
              self.$container.removeClass('select2-container--disabled');
          });
  
          this.on('disable', function () {
              self.$container.addClass('select2-container--disabled');
          });
  
          this.on('blur', function () {
              self.$container.removeClass('select2-container--focus');
          });
  
          this.on('query', function (params) {
              if (!self.isOpen()) {
                  self.trigger('open', {});
              }
  
              this.dataAdapter.query(params, function (data) {
                  self.trigger('results:all', {
                      data: data,
                      query: params
                  });
              });
          });
  
          this.on('query:append', function (params) {
              this.dataAdapter.query(params, function (data) {
                  self.trigger('results:append', {
                      data: data,
                      query: params
                  });
              });
          });
  
          this.on('keypress', function (evt) {
              var key = evt.which;
  
              if (self.isOpen()) {
                  if (key === KEYS.ESC || key === KEYS.TAB ||
                      (key === KEYS.UP && evt.altKey)) {
                      self.close();
  
                      evt.preventDefault();
                  } else if (key === KEYS.ENTER) {
                      self.trigger('results:select', {});
  
                      evt.preventDefault();
                  } else if ((key === KEYS.SPACE && evt.ctrlKey)) {
                      self.trigger('results:toggle', {});
  
                      evt.preventDefault();
                  } else if (key === KEYS.UP) {
                      self.trigger('results:previous', {});
  
                      evt.preventDefault();
                  } else if (key === KEYS.DOWN) {
                      self.trigger('results:next', {});
  
                      evt.preventDefault();
                  }
              } else {
                  if (key === KEYS.ENTER || key === KEYS.SPACE ||
                      (key === KEYS.DOWN && evt.altKey)) {
                      self.open();
  
                      evt.preventDefault();
                  }
              }
          });
      };
  
      Select2.prototype._syncAttributes = function () {
          this.options.set('disabled', this.$element.prop('disabled'));
  
          if (this.options.get('disabled')) {
              if (this.isOpen()) {
                  this.close();
              }
  
              this.trigger('disable', {});
          } else {
              this.trigger('enable', {});
          }
      };
  
      Select2.prototype._syncSubtree = function (evt, mutations) {
          var changed = false;
          var self = this;
  
          // Ignore any mutation events raised for elements that aren't options or
          // optgroups. This handles the case when the select element is destroyed
          if (
              evt && evt.target && (
                  evt.target.nodeName !== 'OPTION' && evt.target.nodeName !== 'OPTGROUP'
              )
          ) {
              return;
          }
  
          if (!mutations) {
              // If mutation events aren't supported, then we can only assume that the
              // change affected the selections
              changed = true;
          } else if (mutations.addedNodes && mutations.addedNodes.length > 0) {
              for (var n = 0; n < mutations.addedNodes.length; n++) {
                  var node = mutations.addedNodes[n];
  
                  if (node.selected) {
                      changed = true;
                  }
              }
          } else if (mutations.removedNodes && mutations.removedNodes.length > 0) {
              changed = true;
          }
  
          // Only re-pull the data if we think there is a change
          if (changed) {
              this.dataAdapter.current(function (currentData) {
                  self.trigger('selection:update', {
                      data: currentData
                  });
              });
          }
      };
  
      /**
       * Override the trigger method to automatically trigger pre-events when
       * there are events that can be prevented.
       */
      Select2.prototype.trigger = function (name, args) {
          var actualTrigger = Select2.__super__.trigger;
          var preTriggerMap = {
              'open': 'opening',
              'close': 'closing',
              'select': 'selecting',
              'unselect': 'unselecting'
          };
  
          if (args === undefined) {
              args = {};
          }
  
          if (name in preTriggerMap) {
              var preTriggerName = preTriggerMap[name];
              var preTriggerArgs = {
                  prevented: false,
                  name: name,
                  args: args
              };
  
              actualTrigger.call(this, preTriggerName, preTriggerArgs);
  
              if (preTriggerArgs.prevented) {
                  args.prevented = true;
  
                  return;
              }
          }
  
          actualTrigger.call(this, name, args);
      };
  
      Select2.prototype.toggleDropdown = function () {
          if (this.options.get('disabled')) {
              return;
          }
  
          if (this.isOpen()) {
              this.close();
          } else {
              this.open();
          }
      };
  
      Select2.prototype.open = function () {
          if (this.isOpen()) {
              return;
          }
  
          this.trigger('query', {});
      };
  
      Select2.prototype.close = function () {
          if (!this.isOpen()) {
              return;
          }
  
          this.trigger('close', {});
      };
  
      Select2.prototype.isOpen = function () {
          return this.$container.hasClass('select2-container--open');
      };
  
      Select2.prototype.hasFocus = function () {
          return this.$container.hasClass('select2-container--focus');
      };
  
      Select2.prototype.focus = function (data) {
          // No need to re-trigger focus events if we are already focused
          if (this.hasFocus()) {
              return;
          }
  
          this.$container.addClass('select2-container--focus');
          this.trigger('focus', {});
      };
  
      Select2.prototype.enable = function (args) {
          if (this.options.get('debug') && window.console && console.warn) {
              console.warn(
                  'Select2: The `select2("enable")` method has been deprecated and will' +
                  ' be removed in later Select2 versions. Use $element.prop("disabled")' +
                  ' instead.'
              );
          }
  
          if (args == null || args.length === 0) {
              args = [true];
          }
  
          var disabled = !args[0];
  
          this.$element.prop('disabled', disabled);
      };
  
      Select2.prototype.data = function () {
          if (this.options.get('debug') &&
              arguments.length > 0 && window.console && console.warn) {
              console.warn(
                  'Select2: Data can no longer be set using `select2("data")`. You ' +
                  'should consider setting the value instead using `$element.val()`.'
              );
          }
  
          var data = [];
  
          this.dataAdapter.current(function (currentData) {
              data = currentData;
          });
  
          return data;
      };
  
      Select2.prototype.val = function (args) {
          if (this.options.get('debug') && window.console && console.warn) {
              console.warn(
                  'Select2: The `select2("val")` method has been deprecated and will be' +
                  ' removed in later Select2 versions. Use $element.val() instead.'
              );
          }
  
          if (args == null || args.length === 0) {
              return this.$element.val();
          }
  
          var newVal = args[0];
  
          if ($.isArray(newVal)) {
              newVal = $.map(newVal, function (obj) {
                  return obj.toString();
              });
          }
  
          this.$element.val(newVal).trigger('change');
      };
  
      Select2.prototype.destroy = function () {
          this.$container.remove();
  
          if (this.$element[0].detachEvent) {
              this.$element[0].detachEvent('onpropertychange', this._syncA);
          }
  
          if (this._observer != null) {
              this._observer.disconnect();
              this._observer = null;
          } else if (this.$element[0].removeEventListener) {
              this.$element[0]
                  .removeEventListener('DOMAttrModified', this._syncA, false);
              this.$element[0]
                  .removeEventListener('DOMNodeInserted', this._syncS, false);
              this.$element[0]
                  .removeEventListener('DOMNodeRemoved', this._syncS, false);
          }
  
          this._syncA = null;
          this._syncS = null;
  
          this.$element.off('.select2');
          this.$element.attr('tabindex', this.$element.data('old-tabindex'));
  
          this.$element.removeClass('select2-hidden-accessible');
          this.$element.attr('aria-hidden', 'false');
          this.$element.removeData('select2');
  
          this.dataAdapter.destroy();
          this.selection.destroy();
          this.dropdown.destroy();
          this.results.destroy();
  
          this.dataAdapter = null;
          this.selection = null;
          this.dropdown = null;
          this.results = null;
      };
  
      Select2.prototype.render = function () {
          var $container = $(
              '<span class="select2 select2-container">' +
              '<span class="selection"></span>' +
              '<span class="dropdown-wrapper" aria-hidden="true"></span>' +
              '</span>'
          );
  
          $container.attr('dir', this.options.get('dir'));
  
          this.$container = $container;
  
          this.$container.addClass('select2-container--' + this.options.get('theme'));
  
          $container.data('element', this.$element);
  
          return $container;
      };
  
      return Select2;
  });
  