function compile()

in modules/closure/select/select.js [187:683]


  function compile(tElement, tAttrs) {
    var isMultiple = $mdUtil.parseAttributeBoolean(tAttrs.multiple);
    tElement.addClass('md-auto-horizontal-margin');

    // add the select value that will hold our placeholder or selected option value
    var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
    valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
    valueEl.addClass('md-select-value');
    if (!valueEl[0].hasAttribute('id')) {
      valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
    }

    // There's got to be an md-content inside. If there's not one, let's add it.
    var mdContentEl = tElement.find('md-content');
    if (!mdContentEl.length) {
      tElement.append(angular.element('<md-content>').append(tElement.contents()));
      mdContentEl = tElement.find('md-content');
    }
    mdContentEl.attr('role', 'listbox');
    mdContentEl.attr('tabindex', '-1');

    if (isMultiple) {
      mdContentEl.attr('aria-multiselectable', 'true');
    } else {
      mdContentEl.attr('aria-multiselectable', 'false');
    }

    // Add progress spinner for md-options-loading
    if (tAttrs.mdOnOpen) {

      // Show progress indicator while loading async
      // Use ng-hide for `display:none` so the indicator does not interfere with the options list
      tElement
        .find('md-content')
        .prepend(angular.element(
          '<div>' +
          ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false"' +
          ' md-diameter="25px"></md-progress-circular>' +
          '</div>'
        ));

      // Hide list [of item options] while loading async
      tElement
        .find('md-option')
        .attr('ng-show', '$$loadingAsyncDone');
    }

    if (tAttrs.name) {
      var autofillClone = angular.element('<select class="md-visually-hidden"></select>');
      autofillClone.attr({
        'name': tAttrs.name,
        'aria-hidden': 'true',
        'tabindex': '-1'
      });
      var opts = tElement.find('md-option');
      angular.forEach(opts, function(el) {
        var newEl = angular.element('<option>' + el.innerHTML + '</option>');
        if (el.hasAttribute('ng-value')) {
          newEl.attr('ng-value', el.getAttribute('ng-value'));
        }
        else if (el.hasAttribute('value')) {
          newEl.attr('value', el.getAttribute('value'));
        }
        autofillClone.append(newEl);
      });

      // Adds an extra option that will hold the selected value for the
      // cases where the select is a part of a non-AngularJS form. This can be done with a ng-model,
      // however if the `md-option` is being `ng-repeat`-ed, AngularJS seems to insert a similar
      // `option` node, but with a value of `? string: <value> ?` which would then get submitted.
      // This also goes around having to prepend a dot to the name attribute.
      autofillClone.append(
        '<option ng-value="' + tAttrs.ngModel + '" selected></option>'
      );

      tElement.parent().append(autofillClone);
    }

    // Use everything that's left inside element.contents() as the contents of the menu
    var multipleContent = isMultiple ? 'multiple' : '';
    var ngModelOptions = tAttrs.ngModelOptions ? $mdUtil.supplant('ng-model-options="{0}"', [tAttrs.ngModelOptions]) : '';
    var selectTemplate = '' +
      '<div class="md-select-menu-container" aria-hidden="true" role="presentation">' +
      '  <md-select-menu role="presentation" {0} {1}>{2}</md-select-menu>' +
      '</div>';

    selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, ngModelOptions,  tElement.html()]);
    tElement.empty().append(valueEl);
    tElement.append(selectTemplate);

    if (!tAttrs.tabindex) {
      tAttrs.$set('tabindex', 0);
    }

    return function postLink(scope, element, attrs, ctrls) {
      var untouched = true;
      var isDisabled;

      var containerCtrl = ctrls[0];
      var mdSelectCtrl = ctrls[1];
      var ngModelCtrl = ctrls[2];
      var formCtrl = ctrls[3];
      // grab a reference to the select menu value label
      var selectValueElement = element.find('md-select-value');
      var isReadonly = angular.isDefined(attrs.readonly);
      var disableAsterisk = $mdUtil.parseAttributeBoolean(attrs.mdNoAsterisk);
      var stopMdMultipleWatch;
      var userDefinedLabelledby = angular.isDefined(attrs.ariaLabelledby);
      var listboxContentElement = element.find('md-content');
      var initialPlaceholder = element.attr('placeholder');

      if (disableAsterisk) {
        element.addClass('md-no-asterisk');
      }

      if (containerCtrl) {
        var isErrorGetter = containerCtrl.isErrorGetter || function() {
          return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
        };

        if (containerCtrl.input) {
          // We ignore inputs that are in the md-select-header.
          // One case where this might be useful would be adding as searchbox.
          if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
            throw new Error("<md-input-container> can only have *one* child <input>, <textarea>, or <select> element!");
          }
        }

        containerCtrl.input = element;
        if (!containerCtrl.label) {
          $mdAria.expect(element, 'aria-label', initialPlaceholder);
          var selectLabel = element.attr('aria-label');
          if (!selectLabel) {
            selectLabel = initialPlaceholder;
          }
          listboxContentElement.attr('aria-label', selectLabel);
        } else {
          containerCtrl.label.attr('aria-hidden', 'true');
          listboxContentElement.attr('aria-label', containerCtrl.label.text());
          containerCtrl.setHasPlaceholder(!!initialPlaceholder);
        }

        var stopInvalidWatch = scope.$watch(isErrorGetter, containerCtrl.setInvalid);
      }

      var selectContainer, selectScope, selectMenuCtrl;

      selectContainer = findSelectContainer();
      $mdTheming(element);

      var originalRender = ngModelCtrl.$render;
      ngModelCtrl.$render = function() {
        originalRender();
        syncSelectValueText();
        inputCheckValue();
      };

      var stopPlaceholderObserver = attrs.$observe('placeholder', ngModelCtrl.$render);

      var stopRequiredObserver = attrs.$observe('required', function (value) {
        if (containerCtrl && containerCtrl.label) {
          // Toggle the md-required class on the input containers label, because the input container
          // is automatically applying the asterisk indicator on the label.
          containerCtrl.label.toggleClass('md-required', value && !disableAsterisk);
        }
        element.removeAttr('aria-required');
        if (value) {
          listboxContentElement.attr('aria-required', 'true');
        } else {
          listboxContentElement.removeAttr('aria-required');
        }
      });

      /**
       * Set the contents of the md-select-value element. This element's contents are announced by
       * screen readers and used for displaying the value of the select in both single and multiple
       * selection modes.
       * @param {string=} text A sanitized and trusted HTML string or a pure text string from user
       *  input.
       */
      mdSelectCtrl.setSelectValueText = function(text) {
        var useDefaultText = text === undefined || text === '';
        // Whether the select label has been given via user content rather than the internal
        // template of <md-option>
        var isSelectLabelFromUser = false;

        mdSelectCtrl.setIsPlaceholder(!text);

        if (attrs.mdSelectedText && attrs.mdSelectedHtml) {
          throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
        }

        if (attrs.mdSelectedText || attrs.mdSelectedHtml) {
          text = $parse(attrs.mdSelectedText || attrs.mdSelectedHtml)(scope);
          isSelectLabelFromUser = true;
        } else if (useDefaultText) {
          // Use placeholder attribute, otherwise fallback to the md-input-container label
          var tmpPlaceholder = attrs.placeholder ||
              (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');

          text = tmpPlaceholder || '';
          isSelectLabelFromUser = true;
        }

        var target = selectValueElement.children().eq(0);

        if (attrs.mdSelectedHtml) {
          // Using getTrustedHtml will run the content through $sanitize if it is not already
          // explicitly trusted. If the ngSanitize module is not loaded, this will
          // *correctly* throw an sce error.
          target.html($sce.getTrustedHtml(text));
        } else if (isSelectLabelFromUser) {
          target.text(text);
        } else {
          // If we've reached this point, the text is not user-provided.
          target.html(text);
        }

        if (useDefaultText) {
          // Avoid screen readers double announcing the label name when no value has been selected
          selectValueElement.attr('aria-hidden', 'true');
          if (!userDefinedLabelledby) {
            element.removeAttr('aria-labelledby');
          }
        } else {
          selectValueElement.removeAttr('aria-hidden');
          if (!userDefinedLabelledby) {
            element.attr('aria-labelledby', element[0].id + ' ' + selectValueElement[0].id);
          }
        }
      };

      /**
       * @param {boolean} isPlaceholder true to mark the md-select-value element and
       *  input container, if one exists, with classes for styling when a placeholder is present.
       *  false to remove those classes.
       */
      mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
          if (isPlaceholder) {
            selectValueElement.addClass('md-select-placeholder');
            // Don't hide the floating label if the md-select has a placeholder.
            if (containerCtrl && containerCtrl.label && !element.attr('placeholder')) {
              containerCtrl.label.addClass('md-placeholder');
            }
          } else {
            selectValueElement.removeClass('md-select-placeholder');
            if (containerCtrl && containerCtrl.label && !element.attr('placeholder')) {
              containerCtrl.label.removeClass('md-placeholder');
            }
          }
      };

      if (!isReadonly) {
        var handleBlur = function(event) {
          // Attach before ngModel's blur listener to stop propagation of blur event
          // and prevent setting $touched.
          if (untouched) {
            untouched = false;
            if (selectScope._mdSelectIsOpen) {
              event.stopImmediatePropagation();
            }
          }

          containerCtrl && containerCtrl.setFocused(false);
          inputCheckValue();
        };
        var handleFocus = function() {
          // Always focus the container (if we have one) so floating labels and other styles are
          // applied properly
          containerCtrl && containerCtrl.setFocused(true);
        };

        element.on('focus', handleFocus);
        element.on('blur', handleBlur);
      }

      mdSelectCtrl.triggerClose = function() {
        $parse(attrs.mdOnClose)(scope);
      };

      scope.$$postDigest(function() {
        initAriaLabel();
        syncSelectValueText();
      });

      function initAriaLabel() {
        var labelText = element.attr('aria-label') || element.attr('placeholder');
        if (!labelText && containerCtrl && containerCtrl.label) {
          labelText = containerCtrl.label.text();
        }
        $mdAria.expect(element, 'aria-label', labelText);
      }

      var stopSelectedLabelsWatcher = scope.$watch(function() {
        return selectMenuCtrl.getSelectedLabels();
      }, syncSelectValueText);

      function syncSelectValueText() {
        selectMenuCtrl = selectMenuCtrl ||
          selectContainer.find('md-select-menu').controller('mdSelectMenu');
        mdSelectCtrl.setSelectValueText(selectMenuCtrl.getSelectedLabels());
      }

      // TODO add tests for mdMultiple
      // TODO add docs for mdMultiple
      var stopMdMultipleObserver = attrs.$observe('mdMultiple', function(val) {
        if (stopMdMultipleWatch) {
          stopMdMultipleWatch();
        }
        var parser = $parse(val);
        stopMdMultipleWatch = scope.$watch(function() {
          return parser(scope);
        }, function(multiple, prevVal) {
          var selectMenu = selectContainer.find('md-select-menu');
          // assume compiler did a good job
          if (multiple === undefined && prevVal === undefined) {
            return;
          }
          if (multiple) {
            var setMultipleAttrs = {'multiple': 'multiple'};
            element.attr(setMultipleAttrs);
            selectMenu.attr(setMultipleAttrs);
          } else {
            element.removeAttr('multiple');
            selectMenu.removeAttr('multiple');
          }
          element.find('md-content').attr('aria-multiselectable', multiple ? 'true' : 'false');

          if (selectContainer) {
            selectMenuCtrl.setMultiple(Boolean(multiple));
            originalRender = ngModelCtrl.$render;
            ngModelCtrl.$render = function() {
              originalRender();
              syncSelectValueText();
              inputCheckValue();
            };
            ngModelCtrl.$render();
          }
        });
      });

      var stopDisabledObserver = attrs.$observe('disabled', function(disabled) {
        if (angular.isString(disabled)) {
          disabled = true;
        }
        // Prevent click event being registered twice
        if (isDisabled !== undefined && isDisabled === disabled) {
          return;
        }
        isDisabled = disabled;
        if (disabled) {
          element
            .attr({'aria-disabled': 'true'})
            .removeAttr('tabindex')
            .removeAttr('aria-expanded')
            .removeAttr('aria-haspopup')
            .off('click', openSelect)
            .off('keydown', handleKeypress);
        } else {
          element
            .attr({
              'tabindex': attrs.tabindex,
              'aria-haspopup': 'listbox'
            })
            .removeAttr('aria-disabled')
            .on('click', openSelect)
            .on('keydown', handleKeypress);
        }
      });

      if (!attrs.hasOwnProperty('disabled') && !attrs.hasOwnProperty('ngDisabled')) {
        element.attr({'aria-disabled': 'false'});
        element.on('click', openSelect);
        element.on('keydown', handleKeypress);
      }

      var ariaAttrs = {
        role: 'button',
        'aria-haspopup': 'listbox'
      };

      if (!element[0].hasAttribute('id')) {
        ariaAttrs.id = 'select_' + $mdUtil.nextUid();
      }

      var containerId = 'select_container_' + $mdUtil.nextUid();
      selectContainer.attr('id', containerId);
      var listboxContentId = 'select_listbox_' + $mdUtil.nextUid();
      selectContainer.find('md-content').attr('id', listboxContentId);
      // Only add aria-owns if element ownership is NOT represented in the DOM.
      if (!element.find('md-select-menu').length) {
        ariaAttrs['aria-owns'] = listboxContentId;
      }
      element.attr(ariaAttrs);

      scope.$on('$destroy', function() {
        stopRequiredObserver && stopRequiredObserver();
        stopDisabledObserver && stopDisabledObserver();
        stopMdMultipleWatch && stopMdMultipleWatch();
        stopMdMultipleObserver && stopMdMultipleObserver();
        stopSelectedLabelsWatcher && stopSelectedLabelsWatcher();
        stopPlaceholderObserver && stopPlaceholderObserver();
        stopInvalidWatch && stopInvalidWatch();

        element.off('focus');
        element.off('blur');

        $mdSelect
          .destroy()
          .finally(function() {
            if (containerCtrl) {
              containerCtrl.setFocused(false);
              containerCtrl.setHasValue(false);
              containerCtrl.input = null;
            }
            ngModelCtrl.$setTouched();
          });
      });

      function inputCheckValue() {
        // The select counts as having a value if one or more options are selected,
        // or if the input's validity state says it has bad input (eg: string in a number input).
        // We must do this on nextTick as the $render is sometimes invoked on nextTick.
        $mdUtil.nextTick(function () {
          containerCtrl && containerCtrl.setHasValue(
            selectMenuCtrl.getSelectedLabels().length > 0 || (element[0].validity || {}).badInput);
        });
      }

      function findSelectContainer() {
        var selectContainer = angular.element(
          element[0].querySelector('.md-select-menu-container')
        );
        selectScope = scope;
        attrs.mdContainerClass && selectContainer.addClass(attrs.mdContainerClass);
        selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
        selectMenuCtrl.init(ngModelCtrl, attrs);
        element.on('$destroy', function() {
          selectContainer.remove();
        });
        return selectContainer;
      }

      /**
       * Determine if the select menu should be opened or an option in the select menu should be
       * selected.
       * @param {KeyboardEvent} e keyboard event to handle
       */
      function handleKeypress(e) {
        if ($mdConstant.isNavigationKey(e)) {
          // prevent page scrolling on interaction
          e.preventDefault();
          openSelect(e);
        } else {
          if (shouldHandleKey(e, $mdConstant)) {
            e.preventDefault();

            var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
            if (!node || node.hasAttribute('disabled')) {
              return;
            }
            var optionCtrl = angular.element(node).controller('mdOption');
            if (!selectMenuCtrl.isMultiple) {
              angular.forEach(Object.keys(selectMenuCtrl.selected), function (key) {
                selectMenuCtrl.deselect(key);
              });
            }
            selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
            selectMenuCtrl.refreshViewValue();
          }
        }
      }

      function openSelect() {
        selectScope._mdSelectIsOpen = true;
        element.attr('aria-expanded', 'true');

        $mdSelect.show({
          scope: selectScope,
          preserveScope: true,
          skipCompile: true,
          element: selectContainer,
          target: element[0],
          selectCtrl: mdSelectCtrl,
          preserveElement: true,
          hasBackdrop: true,
          loadingAsync: attrs.mdOnOpen ? scope.$eval(attrs.mdOnOpen) || true : false
        }).finally(function() {
          selectScope._mdSelectIsOpen = false;
          element.removeAttr('aria-expanded');
          element.removeAttr('aria-activedescendant');
          ngModelCtrl.$setTouched();
        });
      }

    };
  }