that ng-transclude generates
element.append( angular.element('').append(element.contents()) );
element.attr('tabindex', attr.tabindex || '0');
return postLink;
}
function postLink(scope, element, attr, ctrls) {
var optionCtrl = ctrls[0];
var selectCtrl = ctrls[1];
if (angular.isDefined(attr.ngValue)) {
scope.$watch(attr.ngValue, setOptionValue);
} else if (angular.isDefined(attr.value)) {
setOptionValue(attr.value);
} else {
scope.$watch(function() { return element.text(); }, setOptionValue);
}
scope.$$postDigest(function() {
attr.$observe('selected', function(selected) {
if (!angular.isDefined(selected)) return;
if (selected) {
if (!selectCtrl.isMultiple) {
selectCtrl.deselect( Object.keys(selectCtrl.selected)[0] );
}
selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
} else {
selectCtrl.deselect(optionCtrl.hashKey);
}
selectCtrl.refreshViewValue();
selectCtrl.ngModel.$render();
});
});
$mdButtonInkRipple.attach(scope, element);
configureAria();
function setOptionValue(newValue, oldValue) {
var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
var newHashKey = selectCtrl.hashGetter(newValue, scope);
optionCtrl.hashKey = newHashKey;
optionCtrl.value = newValue;
selectCtrl.removeOption(oldHashKey, optionCtrl);
selectCtrl.addOption(newHashKey, optionCtrl);
}
scope.$on('$destroy', function() {
selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
});
function configureAria() {
var ariaAttrs = {
'role': 'option',
'aria-selected': 'false'
};
if (!element[0].hasAttribute('id')) {
ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
}
element.attr(ariaAttrs);
}
}
function OptionController($element) {
this.selected = false;
this.setSelected = function(isSelected) {
if (isSelected && !this.selected) {
$element.attr({
'selected': 'selected',
'aria-selected': 'true'
});
} else if (!isSelected && this.selected) {
$element.removeAttr('selected');
$element.attr('aria-selected', 'false');
}
this.selected = isSelected;
};
}
}
OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
function OptgroupDirective() {
return {
restrict: 'E',
compile: compile
};
function compile(el, attrs) {
var labelElement = el.find('label');
if (!labelElement.length) {
labelElement = angular.element('');
el.prepend(labelElement);
}
if (attrs.label) labelElement.text(attrs.label);
}
}
function SelectProvider($$interimElementProvider) {
selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$$rAF", "$mdUtil", "$mdTheming", "$timeout", "$window"];
return $$interimElementProvider('$mdSelect')
.setDefaults({
methods: ['target'],
options: selectDefaultOptions
});
/* ngInject */
function selectDefaultOptions($mdSelect, $mdConstant, $$rAF, $mdUtil, $mdTheming, $timeout, $window ) {
return {
parent: 'body',
onShow: onShow,
onRemove: onRemove,
hasBackdrop: true,
disableParentScroll: true,
themable: true
};
function onShow(scope, element, opts) {
if (!opts.target) {
throw new Error('$mdSelect.show() expected a target element in options.target but got ' +
'"' + opts.target + '"!');
}
angular.extend(opts, {
isRemoved: false,
target: angular.element(opts.target), //make sure it's not a naked dom node
parent: angular.element(opts.parent),
selectEl: element.find('md-select-menu'),
contentEl: element.find('md-content'),
backdrop: opts.hasBackdrop && angular.element('')
});
opts.resizeFn = function() {
$$rAF(function() {
$$rAF(function() {
animateSelect(scope, element, opts);
});
});
};
angular.element($window).on('resize', opts.resizeFn);
angular.element($window).on('orientationchange', opts.resizeFn);
configureAria();
element.removeClass('md-leave');
var optionNodes = opts.selectEl[0].getElementsByTagName('md-option');
if (opts.loadingAsync && opts.loadingAsync.then) {
opts.loadingAsync.then(function() {
scope.$$loadingAsyncDone = true;
// Give ourselves two frames for the progress loader to clear out.
$$rAF(function() {
$$rAF(function() {
// Don't go forward if the select has been removed in this time...
if (opts.isRemoved) return;
animateSelect(scope, element, opts);
});
});
});
} else if (opts.loadingAsync) {
scope.$$loadingAsyncDone = true;
}
if (opts.disableParentScroll && !$mdUtil.getClosest(opts.target, 'MD-DIALOG')) {
opts.restoreScroll = $mdUtil.disableScrollAround(opts.target);
} else {
opts.disableParentScroll = false;
}
// Only activate click listeners after a short time to stop accidental double taps/clicks
// from clicking the wrong item
$timeout(activateInteraction, 75, false);
if (opts.backdrop) {
$mdTheming.inherit(opts.backdrop, opts.parent);
opts.parent.append(opts.backdrop);
}
opts.parent.append(element);
// Give the select a frame to 'initialize' in the DOM,
// so we can read its height/width/position
$$rAF(function() {
$$rAF(function() {
if (opts.isRemoved) return;
animateSelect(scope, element, opts);
});
});
return $mdUtil.transitionEndPromise(opts.selectEl, {timeout: 350});
function configureAria() {
opts.target.attr('aria-expanded', 'true');
}
function activateInteraction() {
if (opts.isRemoved) return;
var selectCtrl = opts.selectEl.controller('mdSelectMenu') || {};
element.addClass('md-clickable');
opts.backdrop && opts.backdrop.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
opts.restoreFocus = false;
scope.$apply($mdSelect.cancel);
});
// Escape to close
opts.selectEl.on('keydown', function(ev) {
switch (ev.keyCode) {
case $mdConstant.KEY_CODE.SPACE:
case $mdConstant.KEY_CODE.ENTER:
var option = $mdUtil.getClosest(ev.target, 'md-option');
if (option) {
opts.selectEl.triggerHandler({
type: 'click',
target: option
});
ev.preventDefault();
}
break;
case $mdConstant.KEY_CODE.TAB:
case $mdConstant.KEY_CODE.ESCAPE:
ev.preventDefault();
opts.restoreFocus = true;
scope.$apply($mdSelect.cancel);
}
});
// Cycling of options, and closing on enter
opts.selectEl.on('keydown', function(ev) {
switch (ev.keyCode) {
case $mdConstant.KEY_CODE.UP_ARROW: return focusPrevOption();
case $mdConstant.KEY_CODE.DOWN_ARROW: return focusNextOption();
default:
if (ev.keyCode >= 31 && ev.keyCode <= 90) {
var optNode = opts.selectEl.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
optNode && optNode.focus();
}
}
});
function focusOption(direction) {
var optionsArray = nodesToArray(optionNodes);
var index = optionsArray.indexOf(opts.focusedNode);
if (index === -1) {
// We lost the previously focused element, reset to first option
index = 0;
} else if (direction === 'next' && index < optionsArray.length - 1) {
index++;
} else if (direction === 'prev' && index > 0) {
index--;
}
var newOption = opts.focusedNode = optionsArray[index];
newOption && newOption.focus();
}
function focusNextOption() {
focusOption('next');
}
function focusPrevOption() {
focusOption('prev');
}
opts.selectEl.on('click', checkCloseMenu);
opts.selectEl.on('keydown', function(e) {
if (e.keyCode == 32 || e.keyCode == 13) {
checkCloseMenu();
}
});
function checkCloseMenu() {
if (!selectCtrl.isMultiple) {
opts.restoreFocus = true;
scope.$evalAsync(function() {
$mdSelect.hide(selectCtrl.ngModel.$viewValue);
});
}
}
}
}
function onRemove(scope, element, opts) {
opts.isRemoved = true;
element.addClass('md-leave')
.removeClass('md-clickable');
opts.target.attr('aria-expanded', 'false');
angular.element($window).off('resize', opts.resizeFn);
angular.element($window).off('orientationchange', opts.resizefn);
opts.resizeFn = undefined;
var mdSelect = opts.selectEl.controller('mdSelect');
if (mdSelect) {
mdSelect.setLabelText(opts.selectEl.controller('mdSelectMenu').selectedLabels());
}
return $mdUtil.transitionEndPromise(element, { timeout: 350 }).then(function() {
element.removeClass('md-active');
opts.backdrop && opts.backdrop.remove();
if (element[0].parentNode === opts.parent[0]) {
opts.parent[0].removeChild(element[0]); // use browser to avoid $destroy event
}
if (opts.disableParentScroll) {
opts.restoreScroll();
}
if (opts.restoreFocus) opts.target.focus();
});
}
function animateSelect(scope, element, opts) {
var containerNode = element[0],
targetNode = opts.target[0].firstElementChild.firstElementChild, // target the first span, functioning as the label
parentNode = opts.parent[0],
selectNode = opts.selectEl[0],
contentNode = opts.contentEl[0],
parentRect = parentNode.getBoundingClientRect(),
targetRect = targetNode.getBoundingClientRect(),
shouldOpenAroundTarget = false,
bounds = {
left: parentRect.left + SELECT_EDGE_MARGIN,
top: SELECT_EDGE_MARGIN,
bottom: parentRect.height - SELECT_EDGE_MARGIN,
right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
},
spaceAvailable = {
top: targetRect.top - bounds.top,
left: targetRect.left - bounds.left,
right: bounds.right - (targetRect.left + targetRect.width),
bottom: bounds.bottom - (targetRect.top + targetRect.height)
},
maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
isScrollable = contentNode.scrollHeight > contentNode.offsetHeight,
selectedNode = selectNode.querySelector('md-option[selected]'),
optionNodes = selectNode.getElementsByTagName('md-option'),
optgroupNodes = selectNode.getElementsByTagName('md-optgroup');
var centeredNode;
// If a selected node, center around that
if (selectedNode) {
centeredNode = selectedNode;
// If there are option groups, center around the first option group
} else if (optgroupNodes.length) {
centeredNode = optgroupNodes[0];
// Otherwise, center around the first optionNode
} else if (optionNodes.length){
centeredNode = optionNodes[0];
// In case there are no options, center on whatever's in there... (eg progress indicator)
} else {
centeredNode = contentNode.firstElementChild || contentNode;
}
if (contentNode.offsetWidth > maxWidth) {
contentNode.style['max-width'] = maxWidth + 'px';
}
if (shouldOpenAroundTarget) {
contentNode.style['min-width'] = targetRect.width + 'px';
}
// Remove padding before we compute the position of the menu
if (isScrollable) {
selectNode.classList.add('md-overflow');
}
// Get the selectMenuRect *after* max-width is possibly set above
var selectMenuRect = selectNode.getBoundingClientRect();
var centeredRect = getOffsetRect(centeredNode);
if (centeredNode) {
var centeredStyle = $window.getComputedStyle(centeredNode);
centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
}
var focusedNode = centeredNode;
if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
}
if (isScrollable) {
var scrollBuffer = contentNode.offsetHeight / 2;
contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
if (spaceAvailable.top < scrollBuffer) {
contentNode.scrollTop = Math.min(
centeredRect.top,
contentNode.scrollTop + scrollBuffer - spaceAvailable.top
);
} else if (spaceAvailable.bottom < scrollBuffer) {
contentNode.scrollTop = Math.max(
centeredRect.top + centeredRect.height - selectMenuRect.height,
contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
);
}
}
var left, top, transformOrigin;
if (shouldOpenAroundTarget) {
left = targetRect.left;
top = targetRect.top + targetRect.height;
transformOrigin = '50% 0';
if (top + selectMenuRect.height > bounds.bottom) {
top = targetRect.top - selectMenuRect.height;
transformOrigin = '50% 100%';
}
} else {
left = targetRect.left + centeredRect.left - centeredRect.paddingLeft;
top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
centeredRect.top + contentNode.scrollTop);
transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
(centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
containerNode.style.minWidth = targetRect.width + centeredRect.paddingLeft +
centeredRect.paddingRight + 'px';
}
// Keep left and top within the window
var containerRect = containerNode.getBoundingClientRect();
containerNode.style.left = clamp(bounds.left, left, bounds.right - containerRect.width) + 'px';
containerNode.style.top = clamp(bounds.top, top, bounds.bottom - containerRect.height) + 'px';
selectNode.style[$mdConstant.CSS.TRANSFORM_ORIGIN] = transformOrigin;
selectNode.style[$mdConstant.CSS.TRANSFORM] = 'scale(' +
Math.min(targetRect.width / selectMenuRect.width, 1.0) + ',' +
Math.min(targetRect.height / selectMenuRect.height, 1.0) +
')';
$$rAF(function() {
element.addClass('md-active');
selectNode.style[$mdConstant.CSS.TRANSFORM] = '';
if (focusedNode) {
opts.focusedNode = focusedNode;
focusedNode.focus();
}
});
}
}
function clamp(min, n, max) {
return Math.max(min, Math.min(n, max));
}
function getOffsetRect(node) {
return node ? {
left: node.offsetLeft,
top: node.offsetTop,
width: node.offsetWidth,
height: node.offsetHeight
} : { left: 0, top: 0, width: 0, height: 0 };
}
}
SelectProvider.$inject = ["$$interimElementProvider"];
// Annoying method to copy nodes to an array, thanks to IE
function nodesToArray(nodes) {
var results = [];
for (var i = 0; i < nodes.length; ++i) {
results.push(nodes.item(i));
}
return results;
}
ng.material.components.select = angular.module("material.components.select");