'),
position: $mdPanel.newPanelPosition().absolute().center()
};
});
it('should wrap the content element in the proper HTML and assign ' +
'the wrapper to the panel reference', function() {
openPanel(config);
expect(panelRef.panelEl.parent())
.toHaveClass(INNER_WRAPPER_CLASS);
expect(panelRef.panelEl.parent().parent())
.toHaveClass(PANEL_WRAPPER_CLASS);
expect(panelRef.innerWrapper[0]).toBe(config.contentElement.parent()[0]);
expect(panelRef.panelContainer[0]).toBe(config.contentElement.parent().parent()[0]);
});
it('should add the proper class to the panel element and assign ' +
'it to the panel reference', function() {
openPanel(config);
expect(panelRef.panelEl).toHaveClass(PANEL_EL_CLASS);
expect(panelRef.panelEl[0]).toBe(config.contentElement[0]);
});
it('should restore the inline styles and classes of the element on close',
function() {
var element = config.contentElement;
element.addClass('my-only-class');
element.css({ top: '42px', left: '1337px' });
openPanel(config);
closePanel();
expect(element.attr('class')).toBe('my-only-class');
expect(element.css('top')).toBe('42px');
expect(element.css('left')).toBe('1337px');
});
it('should clear out any panel-specific inline styles from the element',
function() {
config.contentElement.css('color', 'red');
var initialStyles = config.contentElement.attr('style');
openPanel(config);
closePanel();
expect(config.contentElement.attr('style')).toBe(initialStyles);
});
it('should clean up the panel via the cleanup function from the compiler',
function() {
openPanel(config);
spyOn(panelRef, '_compilerCleanup');
closePanel();
expect(panelRef._compilerCleanup).toHaveBeenCalled();
});
});
/**
* Attached an element to document.body. Keeps track of attached elements
* so that they can be removed in an afterEach.
* @param el
*/
function attachToBody(el) {
var element = angular.element(el);
angular.element(document.body).append(element);
attachedElements.push(element);
}
/**
* Returns the angular element associated with a CSS selector or element.
* @param el {string|!angular.JQLite|!Element}
* @returns {!angular.JQLite}
*/
function getElement(el) {
var queryResult = angular.isString(el) ? document.querySelector(el) : el;
return angular.element(queryResult);
}
function clickPanelContainer(container) {
if (!panelRef) {
return;
}
container = container || panelRef.panelContainer;
container.triggerHandler({
type: 'mousedown',
target: container[0]
});
container.triggerHandler({
type: 'mouseup',
target: container[0]
});
flushPanel();
}
function pressEscape() {
if (!panelRef) {
return;
}
var container = panelRef.panelContainer;
container.triggerHandler({
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.ESCAPE
});
flushPanel();
}
/**
* Opens the panel. If a config value is passed, creates a new panelRef
* using $mdPanel.open(config); Otherwise, called open on the panelRef,
* assuming one has already been created.
* @param {!Object=} opt_config
*/
function openPanel(preset, opt_config) {
// TODO(ErinCoughlan): Investigate why panelRef.open() doesn't return
// panelRef.
var openPromise;
if (panelRef) {
openPromise = panelRef.open();
} else {
openPromise = $mdPanel.open(preset, opt_config);
}
openPromise.then(function(createdPanelRef) {
panelRef = createdPanelRef;
return panelRef;
});
flushPanel();
}
/**
* Closes the panel using an already created panelRef.
*/
function closePanel() {
panelRef && panelRef.close();
flushPanel();
}
function showPanel() {
panelRef && panelRef.show();
flushPanel();
}
function hidePanel() {
panelRef && panelRef.hide();
flushPanel();
}
function flushPanel() {
$rootScope.$apply();
$material.flushOutstandingAnimations();
}
function getNumberOfGroups() {
return Object.keys($mdPanel._groups).length;
}
function getGroupPanels(groupName) {
return $mdPanel._groups[groupName].panels;
}
function getGroupOpenPanels(groupName) {
return $mdPanel._groups[groupName].openPanels;
}
function getGroupMaxOpen(groupName) {
return $mdPanel._groups[groupName].maxOpen;
}
});
================================================
FILE: src/components/progressCircular/demoBasicUsage/index.html
================================================
Determinate
For operations where the percentage of the operation completed can be determined, use a determinate indicator. They
give users a quick sense of how long an operation will take.
Indeterminate
For operations where the user is asked to wait a moment while something finishes up, and it's not necessary to
expose what's happening behind the scenes and how long it will take, use an indeterminate indicator.
Theming
Your current theme colors can be used to easily colorize your progress indicator with `md-warn` or `md-accent`
colors.
Dark theme
This is an example of the <md-progress-circular> component, with a dark theme.
Progress Circular Indicators:
Off
On
================================================
FILE: src/components/progressCircular/demoBasicUsage/script.js
================================================
angular
.module('progressCircularDemo1', ['ngMaterial'], function($mdThemingProvider) {
$mdThemingProvider.theme('docs-dark', 'default')
.primaryPalette('yellow')
.dark();
})
.controller('AppCtrl', ['$interval',
function($interval) {
var self = this;
self.activated = true;
self.determinateValue = 30;
// Iterate every 100ms, non-stop and increment
// the Determinate loader.
$interval(function() {
self.determinateValue += 1;
if (self.determinateValue > 100) {
self.determinateValue = 30;
}
}, 100);
}
]);
================================================
FILE: src/components/progressCircular/demoBasicUsage/style.css
================================================
body {
padding: 20px;
}
h4 {
margin: 10px 0;
}
md-progress-circular {
margin-bottom:20px;
}
#loaders > md-switch {
margin:0;
margin-left: 10px;
margin-top: -10px;
}
#loaders > h5 {
margin-top: 0;
}
#loaders > p {
margin-right: 20px;
margin-bottom: 24px;
}
p.small {
font-size: 0.8em;
margin-top: -18px;
}
hr {
width: 100%;
margin-top: 20px;
border-color: rgba(221, 221, 177, 0.1);
}
p.small > code {
font-size: 0.8em;
}
.visible {
border-color: rgba(221, 221, 177, 0);
}
================================================
FILE: src/components/progressCircular/js/progressCircularDirective.js
================================================
/**
* @ngdoc directive
* @name mdProgressCircular
* @module material.components.progressCircular
* @restrict E
*
* @description
* The circular progress directive is used to make loading content in your app as delightful and
* painless as possible by minimizing the amount of visual change a user sees before they can view
* and interact with content.
*
* For operations where the percentage of the operation completed can be determined, use a
* determinate indicator. They give users a quick sense of how long an operation will take.
*
* For operations where the user is asked to wait a moment while something finishes up, and it’s
* not necessary to expose what's happening behind the scenes and how long it will take, use an
* indeterminate indicator.
*
* @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**.
*
* Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then **'indeterminate'**
* will be auto-applied as the mode.
*
* Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute.
* If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead.
* @param {number=} value In determinate mode, this number represents the percentage of the
* circular progress. Default: 0
* @param {number=} md-diameter This specifies the diameter of the circular progress. The value
* should be a pixel-size value (eg '100'). If this attribute is
* not present then a default value of '50px' is assumed.
*
* @param {boolean=} ng-disabled Determines whether to disable the progress element.
*
* @usage
*
*
*
*
*
*
*
*
*
*/
angular
.module('material.components.progressCircular')
.directive('mdProgressCircular', MdProgressCircularDirective);
/* @ngInject */
function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
$mdUtil, $interval, $log) {
// Note that this shouldn't use use $$rAF, because it can cause an infinite loop
// in any tests that call $animate.flush.
var rAF = $window.requestAnimationFrame ||
$window.webkitRequestAnimationFrame ||
angular.noop;
var cAF = $window.cancelAnimationFrame ||
$window.webkitCancelAnimationFrame ||
$window.webkitCancelRequestAnimationFrame ||
angular.noop;
var MODE_DETERMINATE = 'determinate';
var MODE_INDETERMINATE = 'indeterminate';
var DISABLED_CLASS = '_md-progress-circular-disabled';
var INDETERMINATE_CLASS = 'md-mode-indeterminate';
return {
restrict: 'E',
scope: {
value: '@',
mdDiameter: '@',
mdMode: '@'
},
template:
'
' +
' ' +
' ',
compile: function(element, attrs) {
element.attr({
'aria-valuemin': 0,
'aria-valuemax': 100,
'role': 'progressbar'
});
if (angular.isUndefined(attrs.mdMode)) {
var mode = attrs.hasOwnProperty('value') ? MODE_DETERMINATE : MODE_INDETERMINATE;
attrs.$set('mdMode', mode);
} else {
attrs.$set('mdMode', attrs.mdMode.trim());
}
return MdProgressCircularLink;
}
};
function MdProgressCircularLink(scope, element, attrs) {
var node = element[0];
var svg = angular.element(node.querySelector('svg'));
var path = angular.element(node.querySelector('path'));
var startIndeterminate = $mdProgressCircular.startIndeterminate;
var endIndeterminate = $mdProgressCircular.endIndeterminate;
var iterationCount = 0;
var lastAnimationId = 0;
var lastDrawFrame;
var interval;
$mdTheming(element);
element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
// If the mode is indeterminate, it doesn't need to
// wait for the next digest. It can start right away.
if (scope.mdMode === MODE_INDETERMINATE){
startIndeterminateAnimation();
}
scope.$on('$destroy', function(){
cleanupIndeterminateAnimation();
if (lastDrawFrame) {
cAF(lastDrawFrame);
}
});
scope.$watchGroup(['value', 'mdMode', function() {
var isDisabled = node.disabled;
// Sometimes the browser doesn't return a boolean, in
// which case we should check whether the attribute is
// present.
if (isDisabled === true || isDisabled === false){
return isDisabled;
}
return angular.isDefined(element.attr('disabled'));
}], function(newValues, oldValues) {
var mode = newValues[1];
var isDisabled = newValues[2];
var wasDisabled = oldValues[2];
var diameter = 0;
var strokeWidth = 0;
if (isDisabled !== wasDisabled) {
element.toggleClass(DISABLED_CLASS, !!isDisabled);
}
if (isDisabled) {
cleanupIndeterminateAnimation();
} else {
if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
mode = MODE_INDETERMINATE;
attrs.$set('mdMode', mode);
}
if (mode === MODE_INDETERMINATE) {
if (oldValues[1] === MODE_DETERMINATE) {
diameter = getSize(scope.mdDiameter);
strokeWidth = getStroke(diameter);
path.attr('d', getSvgArc(diameter, strokeWidth, true));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 75));
}
startIndeterminateAnimation();
} else {
var newValue = clamp(newValues[0]);
var oldValue = clamp(oldValues[0]);
cleanupIndeterminateAnimation();
if (oldValues[1] === MODE_INDETERMINATE) {
diameter = getSize(scope.mdDiameter);
strokeWidth = getStroke(diameter);
path.attr('d', getSvgArc(diameter, strokeWidth, false));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 100));
}
element.attr('aria-valuenow', newValue);
renderCircle(oldValue, newValue);
}
}
});
// This is in a separate watch in order to avoid layout, unless
// the value has actually changed.
scope.$watch('mdDiameter', function(newValue) {
var diameter = getSize(newValue);
var strokeWidth = getStroke(diameter);
var value = clamp(scope.value);
var transformOrigin = (diameter / 2) + 'px';
var dimensions = {
width: diameter + 'px',
height: diameter + 'px'
};
// The viewBox has to be applied via setAttribute, because it is
// case-sensitive. If jQuery is included in the page, `.attr` lowercases
// all attribute names.
svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
// Usually viewBox sets the dimensions for the SVG, however that doesn't
// seem to be the case on IE10.
// Important! The transform origin has to be set from here and it has to
// be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
// IE and Edge, because they don't account for the stroke width when
// rotating. Also "center" doesn't help in this case, it has to be a
// precise value.
svg
.css(dimensions)
.css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
element.css(dimensions);
path.attr('stroke-width', strokeWidth);
path.attr('stroke-linecap', 'square');
if (scope.mdMode == MODE_INDETERMINATE) {
path.attr('d', getSvgArc(diameter, strokeWidth, true));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 75));
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, 1, 75));
} else {
path.attr('d', getSvgArc(diameter, strokeWidth, false));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 100));
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, 0, 100));
renderCircle(value, value);
}
});
function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) {
var id = ++lastAnimationId;
var startTime = $mdUtil.now();
var changeInValue = animateTo - animateFrom;
var diameter = getSize(scope.mdDiameter);
var strokeWidth = getStroke(diameter);
var ease = easing || $mdProgressCircular.easeFn;
var animationDuration = duration || $mdProgressCircular.duration;
var rotation = -90 * (iterationCount || 0);
var dashLimit = maxValue || 100;
// No need to animate it if the values are the same
if (animateTo === animateFrom) {
renderFrame(animateTo);
} else {
lastDrawFrame = rAF(function animation() {
var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration));
// Do not allow overlapping animations
if (id === lastAnimationId && currentTime < animationDuration) {
lastDrawFrame = rAF(animation);
}
});
}
function renderFrame(value) {
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, value, dashLimit));
path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')');
}
}
function animateIndeterminate() {
renderCircle(
startIndeterminate,
endIndeterminate,
$mdProgressCircular.easeFnIndeterminate,
$mdProgressCircular.durationIndeterminate,
iterationCount,
75
);
// The %4 technically isn't necessary, but it keeps the rotation
// under 360, instead of becoming a crazy large number.
iterationCount = ++iterationCount % 4;
}
function startIndeterminateAnimation() {
if (!interval) {
// Note that this interval isn't supposed to trigger a digest.
interval = $interval(
animateIndeterminate,
$mdProgressCircular.durationIndeterminate,
0,
false
);
animateIndeterminate();
element
.addClass(INDETERMINATE_CLASS)
.removeAttr('aria-valuenow');
}
}
function cleanupIndeterminateAnimation() {
if (interval) {
$interval.cancel(interval);
interval = null;
element.removeClass(INDETERMINATE_CLASS);
}
}
}
/**
* Returns SVG path data for progress circle
* Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
* @param {boolean} indeterminate Use if progress circle will be used for indeterminate
*
* @returns {string} String representation of an SVG arc.
*/
function getSvgArc(diameter, strokeWidth, indeterminate) {
var radius = diameter / 2;
var offset = strokeWidth / 2;
var start = radius + ',' + offset; // ie: (25, 2.5) or 12 o'clock
var end = offset + ',' + radius; // ie: (2.5, 25) or 9 o'clock
var arcRadius = radius - offset;
return 'M' + start
+ 'A' + arcRadius + ',' + arcRadius + ' 0 1 1 ' + end // 75% circle
+ (indeterminate ? '' : 'A' + arcRadius + ',' + arcRadius + ' 0 0 1 ' + start); // loop to start
}
/**
* Return stroke length for progress circle
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
* @param {number} value Percentage of circle (between 0 and 100)
* @param {number} maxArcLength Maximum length of arc as a percentage of circle (between 0 and 100)
*
* @returns {number} Stroke length for progress circle
*/
function getDashOffset(diameter, strokeWidth, value, maxArcLength) {
return getSpinnerCircumference(diameter, strokeWidth) * ((maxArcLength - value) / 100);
}
/**
* Limits a value between 0 and 100.
*/
function clamp(value) {
return $window.Math.max(0, $window.Math.min(value || 0, 100));
}
/**
* Determines the size of a progress circle, based on the provided
* value in the following formats: `X`, `Ypx`, `Z%`.
*/
function getSize(value) {
var defaultValue = $mdProgressCircular.progressSize;
if (value) {
var parsed = parseFloat(value);
if (value.lastIndexOf('%') === value.length - 1) {
parsed = (parsed / 100) * defaultValue;
}
return parsed;
}
return defaultValue;
}
/**
* Determines the circle's stroke width, based on
* the provided diameter.
*/
function getStroke(diameter) {
return $mdProgressCircular.strokeWidth / 100 * diameter;
}
/**
* Return length of the dash
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
* @param {number} value Percentage of circle (between 0 and 100)
*
* @returns {number} Length of the dash
*/
function getDashLength(diameter, strokeWidth, value) {
return getSpinnerCircumference(diameter, strokeWidth) * (value / 100);
}
/**
* Return circumference of the spinner
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
*
* @returns {number} Circumference of the spinner
*/
function getSpinnerCircumference(diameter, strokeWidth) {
return ((diameter - strokeWidth) * $window.Math.PI);
}
}
================================================
FILE: src/components/progressCircular/js/progressCircularProvider.js
================================================
/**
* @ngdoc service
* @name $mdProgressCircular
* @module material.components.progressCircular
*
* @description
* Allows the user to specify the default options for the `progressCircular` directive.
*
* @property {number} progressSize Diameter of the progress circle in pixels.
* @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
* @property {number} duration Length of the circle animation in milliseconds.
* @property {function} easeFn Default easing animation function.
* @property {object} easingPresets Collection of pre-defined easing functions.
*
* @property {number} durationIndeterminate Duration of the indeterminate animation.
* @property {number} startIndeterminate Indeterminate animation start point.
* @property {number} endIndeterminate Indeterminate animation end point.
* @property {function} easeFnIndeterminate Easing function to be used when animating
* between the indeterminate values.
*
* @property {(function(object): object)} configure Used to modify the default options.
*
* @usage
*
* myAppModule.config(function($mdProgressCircularProvider) {
*
* // Example of changing the default progress options.
* $mdProgressCircularProvider.configure({
* progressSize: 100,
* strokeWidth: 20,
* duration: 800
* });
* });
*
*
*/
angular
.module('material.components.progressCircular')
.provider("$mdProgressCircular", MdProgressCircularProvider);
function MdProgressCircularProvider() {
var progressConfig = {
progressSize: 50,
strokeWidth: 10,
duration: 100,
easeFn: linearEase,
durationIndeterminate: 1333,
startIndeterminate: 1,
endIndeterminate: 149,
easeFnIndeterminate: materialEase,
easingPresets: {
linearEase: linearEase,
materialEase: materialEase
}
};
return {
configure: function(options) {
progressConfig = angular.extend(progressConfig, options || {});
return progressConfig;
},
$get: function() { return progressConfig; }
};
function linearEase(t, b, c, d) {
return c * t / d + b;
}
function materialEase(t, b, c, d) {
// via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
// with settings of [0, 0, 1, 1]
var ts = (t /= d) * t;
var tc = ts * t;
return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
}
}
================================================
FILE: src/components/progressCircular/progress-circular-theme.scss
================================================
md-progress-circular.md-THEME_NAME-theme {
path {
stroke: '{{primary-color}}';
}
&.md-warn {
path {
stroke: '{{warn-color}}';
}
}
&.md-accent {
path {
stroke: '{{accent-color}}';
}
}
}
================================================
FILE: src/components/progressCircular/progress-circular.js
================================================
/**
* @ngdoc module
* @name material.components.progressCircular
* @description Module for a circular progressbar
*/
angular.module('material.components.progressCircular', ['material.core']);
================================================
FILE: src/components/progressCircular/progress-circular.scss
================================================
$progress-circular-indeterminate-duration: 1568.63ms !default;
@keyframes indeterminate-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Used to avoid unnecessary layout
md-progress-circular {
position: relative;
display: block;
@include rtl(transform, scale(1, 1), scale(-1, 1));
&._md-progress-circular-disabled {
visibility: hidden;
}
&.md-mode-indeterminate svg {
animation: indeterminate-rotate $progress-circular-indeterminate-duration linear infinite;
}
svg {
position: absolute;
overflow: visible;
top: 0;
left: 0;
}
}
================================================
FILE: src/components/progressCircular/progress-circular.spec.js
================================================
describe('mdProgressCircular', function() {
var $compile, $rootScope, config, element;
beforeEach(module('material.components.progressCircular'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$mdProgressCircular_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
config = _$mdProgressCircular_;
}));
afterEach(function() {
if (element) {
element.remove();
}
});
it('should auto-set the md-mode to "indeterminate" if not specified', function() {
var progress = buildIndicator('
');
$rootScope.$apply(function() {
$rootScope.progress = 50;
$rootScope.mode = "";
});
expect(progress.attr('md-mode')).toEqual('indeterminate');
});
it('should auto-set the md-mode to "indeterminate" if specified not as "indeterminate" or "determinate"', function() {
var progress = buildIndicator('
');
$rootScope.$apply(function() {
$rootScope.progress = 50;
$rootScope.mode = "";
});
expect(progress.attr('md-mode')).toEqual('indeterminate');
});
it('should trim the md-mode value', function() {
var progress = buildIndicator('
');
$rootScope.$apply(function() {
$rootScope.progress = 50;
});
expect(progress.attr('md-mode')).toEqual('indeterminate');
});
it('should auto-set the md-mode to "determinate" if not specified but has value', function() {
var progress = buildIndicator('
');
$rootScope.$apply(function() {
$rootScope.progress = 50;
$rootScope.mode = "";
});
expect(progress.attr('md-mode')).toEqual('determinate');
});
it('should update aria-valuenow', function() {
var progress = buildIndicator('
');
$rootScope.$apply(function() {
$rootScope.progress = 50;
});
expect(progress.attr('aria-valuenow')).toEqual('50');
});
it('should not set aria-valuenow in indeterminate mode', function() {
var progress = buildIndicator('
');
expect(progress.attr('aria-valuenow')).toBeUndefined();
});
it('should set the size using percentage values',function() {
var progress = buildIndicator('
');
var expectedSize = config.progressSize / 2 + 'px';
expect(progress.css('width')).toBe(expectedSize);
expect(progress.css('height')).toBe(expectedSize);
});
it('should set the size using pixel values', function() {
var progress = buildIndicator('
');
expect(progress.css('width')).toBe('37px');
expect(progress.css('height')).toBe('37px');
});
it('should scale the stroke width as a percentage of the diameter', function() {
var ratio = config.strokeWidth;
var diameter = 25;
var path = buildIndicator(
'
'
).find('path').eq(0);
expect(parseFloat(path.attr('stroke-width'))).toBe(diameter / ratio);
});
it('should hide the element if is disabled', function() {
var element = buildIndicator(
'
'
);
expect(element.hasClass('_md-progress-circular-disabled')).toBe(true);
});
it('should set the transform origin in all dimensions', function() {
var svg = buildIndicator('
').find('svg').eq(0);
expect(svg.css('transform-origin')).toBe('21px 21px 21px');
});
it('should adjust the element size when the diameter changes', function() {
$rootScope.diameter = 30;
var element = buildIndicator(
'
'
);
var path = element.find('path');
var initialPathDimensions = path.attr('d');
expect(element.css('width')).toBe('30px');
expect(element.css('height')).toBe('30px');
expect(initialPathDimensions).toBeTruthy();
$rootScope.$apply('diameter = 60');
expect(element.css('width')).toBe('60px');
expect(element.css('height')).toBe('60px');
expect(path.attr('d')).not.toBe(initialPathDimensions);
});
/**
* Build a progressCircular
*/
function buildIndicator(template) {
element = $compile(template)($rootScope);
$rootScope.$digest();
return element;
}
});
describe('mdProgressCircularProvider', function() {
beforeEach(function() {
module('material.components.progressCircular', function($mdProgressCircularProvider) {
$mdProgressCircularProvider.configure({
progressSize: 1337,
strokeWidth: 42
});
});
});
it('should allow for the default options to be configured', inject(function($mdProgressCircular) {
expect($mdProgressCircular.progressSize).toBe(1337);
expect($mdProgressCircular.strokeWidth).toBe(42);
}));
});
================================================
FILE: src/components/progressLinear/demoBasicUsage/index.html
================================================
Determinate
For operations where the percentage of the operation completed can be determined, use a determinate
indicator.
They give users a quick sense of how long an operation will take.
Indeterminate
For operations where the user is asked to wait a moment while something finishes up, and it's not
necessary to expose what's happening behind the scenes and how long it will take, use an
indeterminate indicator:
Buffer
For operations where the user wants to indicate some activity or loading from the server,
use the buffer indicator:
Query
For situations where the user wants to indicate pre-loading (until the loading can actually be made),
use the query indicator:
Loading application libraries...
Query and Buffer progress linear indicators:
Off
On
================================================
FILE: src/components/progressLinear/demoBasicUsage/script.js
================================================
angular.module('progressLinearDemo1', ['ngMaterial'])
.config(function($mdThemingProvider) {
})
.controller('AppCtrl', ['$scope', '$interval', function($scope, $interval) {
var self = this, j= 0, counter = 0;
self.mode = 'query';
self.activated = true;
self.determinateValue = 30;
self.determinateValue2 = 30;
self.showList = [];
/**
* Turn off or on the 5 themed loaders
*/
self.toggleActivation = function() {
if (!self.activated) self.showList = [];
if (self.activated) {
j = counter = 0;
self.determinateValue = 30;
self.determinateValue2 = 30;
}
};
$interval(function() {
self.determinateValue += 1;
self.determinateValue2 += 1.5;
if (self.determinateValue > 100) self.determinateValue = 30;
if (self.determinateValue2 > 100) self.determinateValue2 = 30;
// Incrementally start animation the five (5) Indeterminate,
// themed progress circular bars
if ((j < 2) && !self.showList[j] && self.activated) {
self.showList[j] = true;
}
if (counter++ % 4 === 0) j++;
// Show the indicator in the "Used within Containers" after 200ms delay
if (j == 2) self.contained = "indeterminate";
}, 100, 0, true);
$interval(function() {
self.mode = (self.mode == 'query' ? 'determinate' : 'query');
}, 7200, 0, true);
}]);
================================================
FILE: src/components/progressLinear/demoBasicUsage/style.css
================================================
body {
padding: 20px;
}
h4 {
margin: 10px 0;
}
md-progress-linear {
padding-top: 10px;
margin-bottom: 20px;
}
#loaders > md-switch {
margin: 0;
margin-left: 10px;
margin-top: -10px;
}
#loaders > h5 {
margin-top: 0;
}
#loaders > p {
margin-right: 20px;
margin-bottom: 24px;
}
p.small {
font-size: 0.8em;
margin-top: -18px;
}
hr {
width: 100%;
margin-top: 20px;
border-color: rgba(221, 221, 177, 0.1);
}
p.small > code {
font-size: 0.8em;
}
.visible {
opacity: 0;
border: 2px solid white !important;
}
.container {
display: block;
position: relative;
width: 100%;
border: 2px solid rgb(170,209,249);
transition: opacity 0.1s linear;
border-top: 0px;
}
.bottom-block {
display: block;
position: relative;
background-color: rgba(255, 235, 169, 0.25);
height: 85px;
width: 100%;
}
.bottom-block > span {
display: inline-block;
margin-top:10px;
padding:25px;
font-size: 0.9em;
}
================================================
FILE: src/components/progressLinear/progress-linear-theme.scss
================================================
md-progress-linear.md-THEME_NAME-theme {
.md-container {
background-color: '{{primary-100}}';
}
.md-bar {
background-color: '{{primary-color}}';
}
&.md-warn {
.md-container {
background-color: '{{warn-100}}';
}
.md-bar {
background-color: '{{warn-color}}';
}
}
&.md-accent {
.md-container {
background-color: '{{accent-100}}';
}
.md-bar {
background-color: '{{accent-color}}';
}
}
&[md-mode=buffer] {
&.md-primary {
.md-bar1 {
background-color: '{{primary-100}}';
}
.md-dashed:before {
background: radial-gradient('{{primary-100}}' 0%, '{{primary-100}}' 16%, transparent 42%);
}
}
&.md-warn {
.md-bar1 {
background-color: '{{warn-100}}';
}
.md-dashed:before {
background: radial-gradient('{{warn-100}}' 0%, '{{warn-100}}' 16%, transparent 42%);
}
}
&.md-accent {
.md-bar1 {
background-color: '{{accent-100}}';
}
.md-dashed:before {
background: radial-gradient('{{accent-100}}' 0%, '{{accent-100}}' 16%, transparent 42%);
}
}
}
}
================================================
FILE: src/components/progressLinear/progress-linear.js
================================================
/**
* @ngdoc module
* @name material.components.progressLinear
* @description Linear Progress module!
*/
angular.module('material.components.progressLinear', [
'material.core'
])
.directive('mdProgressLinear', MdProgressLinearDirective);
/**
* @ngdoc directive
* @name mdProgressLinear
* @module material.components.progressLinear
* @restrict E
*
* @description
* The linear progress directive is used to make loading content
* in your app as delightful and painless as possible by minimizing
* the amount of visual change a user sees before they can view
* and interact with content.
*
* Each operation should only be represented by one activity indicator
* For example: one refresh operation should not display both a
* refresh bar and an activity circle.
*
* For operations where the percentage of the operation completed
* can be determined, use a determinate indicator. They give users
* a quick sense of how long an operation will take.
*
* For operations where the user is asked to wait a moment while
* something finishes up, and it’s not necessary to expose what's
* happening behind the scenes and how long it will take, use an
* indeterminate indicator.
*
* @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query.
*
* Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate`
* will be auto-applied as the mode.
*
* Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however,
* then `md-mode="determinate"` would be auto-injected instead.
* @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0
* @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0
* @param {boolean=} ng-disabled Determines whether to disable the progress element.
*
* @usage
*
*
*
*
*
*
*
*
*
*
*
*/
function MdProgressLinearDirective($mdTheming, $mdUtil, $log) {
var MODE_DETERMINATE = "determinate";
var MODE_INDETERMINATE = "indeterminate";
var MODE_BUFFER = "buffer";
var MODE_QUERY = "query";
var DISABLED_CLASS = "_md-progress-linear-disabled";
return {
restrict: 'E',
template: '
',
compile: compile
};
function compile(tElement, tAttrs, transclude) {
tElement.attr('aria-valuemin', 0);
tElement.attr('aria-valuemax', 100);
tElement.attr('role', 'progressbar');
return postLink;
}
function postLink(scope, element, attr) {
$mdTheming(element);
var lastMode;
var isDisabled = attr.hasOwnProperty('disabled');
var toVendorCSS = $mdUtil.dom.animator.toCss;
var bar1 = angular.element(element[0].querySelector('.md-bar1'));
var bar2 = angular.element(element[0].querySelector('.md-bar2'));
var container = angular.element(element[0].querySelector('.md-container'));
element
.attr('md-mode', mode())
.toggleClass(DISABLED_CLASS, isDisabled);
validateMode();
watchAttributes();
/**
* Watch the value, md-buffer-value, and md-mode attributes
*/
function watchAttributes() {
attr.$observe('value', function(value) {
var percentValue = clamp(value);
element.attr('aria-valuenow', percentValue);
if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue);
});
attr.$observe('mdBufferValue', function(value) {
animateIndicator(bar1, clamp(value));
});
attr.$observe('disabled', function(value) {
if (value === true || value === false) {
isDisabled = !!value;
} else {
isDisabled = angular.isDefined(value);
}
element.toggleClass(DISABLED_CLASS, isDisabled);
container.toggleClass(lastMode, !isDisabled);
});
attr.$observe('mdMode', function(mode) {
if (lastMode) container.removeClass(lastMode);
switch (mode) {
case MODE_QUERY:
case MODE_BUFFER:
case MODE_DETERMINATE:
case MODE_INDETERMINATE:
container.addClass(lastMode = "md-mode-" + mode);
break;
default:
container.addClass(lastMode = "md-mode-" + MODE_INDETERMINATE);
break;
}
});
}
/**
* Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified
*/
function validateMode() {
if (angular.isUndefined(attr.mdMode)) {
var hasValue = angular.isDefined(attr.value);
var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element";
element.attr("md-mode", mode);
attr.mdMode = mode;
}
}
/**
* Is the md-mode a valid option?
*/
function mode() {
var value = (attr.mdMode || "").trim();
if (value) {
switch (value) {
case MODE_DETERMINATE:
case MODE_INDETERMINATE:
case MODE_BUFFER:
case MODE_QUERY:
break;
default:
value = MODE_INDETERMINATE;
break;
}
}
return value;
}
/**
* Manually set CSS to animate the Determinate indicator based on the specified
* percentage value (0-100).
*/
function animateIndicator(target, value) {
if (isDisabled || !mode()) return;
var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [(value-100)/2, value/100]);
var styles = toVendorCSS({ transform : to });
angular.element(target).css(styles);
}
}
/**
* Clamps the value to be between 0 and 100.
* @param {number} value The value to clamp.
* @returns {number}
*/
function clamp(value) {
return Math.max(0, Math.min(value || 0, 100));
}
}
================================================
FILE: src/components/progressLinear/progress-linear.scss
================================================
$progress-linear-bar-height: 5px !default;
md-progress-linear {
display: block;
position: relative;
width: 100%;
height: $progress-linear-bar-height;
padding-top: 0 !important;
margin-bottom: 0 !important;
@include rtl(transform, scale(1, 1), scale(-1, 1));
&._md-progress-linear-disabled {
visibility: hidden;
}
.md-container {
display:block;
position: relative;
overflow: hidden;
width:100%;
height: $progress-linear-bar-height;
transform: translate(0, 0) scale(1, 1);
.md-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
height: $progress-linear-bar-height;
}
.md-dashed:before {
content: "";
display: none;
position: absolute;
margin-top: 0;
height: $progress-linear-bar-height;
width: 100%;
background-color: transparent;
background-size: 10px 10px !important;
background-position: 0px -23px;
}
.md-bar1, .md-bar2 {
// Just set the transition information here.
// Note: the actual transform values are calculated in JS
transition: transform 0.2s linear;
}
// ************************************************************
// Animations for modes: Determinate, InDeterminate, and Query
// ************************************************************
&.md-mode-query {
.md-bar1 {
display: none;
}
.md-bar2 {
transition: all 0.2s linear;
animation: query .8s infinite cubic-bezier(0.390, 0.575, 0.565, 1.000);
}
}
&.md-mode-determinate {
.md-bar1 {
display: none;
}
}
&.md-mode-indeterminate {
.md-bar1 {
animation: md-progress-linear-indeterminate-scale-1 4s infinite,
md-progress-linear-indeterminate-1 4s infinite;
}
.md-bar2 {
animation: md-progress-linear-indeterminate-scale-2 4s infinite,
md-progress-linear-indeterminate-2 4s infinite;
}
}
&.ng-hide
._md-progress-linear-disabled & {
animation: none;
.md-bar1 {
animation-name: none;
}
.md-bar2 {
animation-name: none;
}
}
}
// Special animations for the `buffer` mode
.md-container.md-mode-buffer {
background-color: transparent !important;
transition: all 0.2s linear;
.md-dashed:before {
display: block;
animation: buffer 3s infinite linear;
}
}
}
@keyframes query {
0% {
opacity: 1;
transform: translateX(35%) scale(.3, 1);
}
100% {
opacity: 0;
transform: translateX(-50%) scale(0, 1);
}
}
@keyframes buffer {
0% {
opacity: 1;
background-position: 0px -23px;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
background-position: -200px -23px;
}
}
@keyframes md-progress-linear-indeterminate-scale-1 {
0% {
transform: scaleX(0.1);
animation-timing-function: linear;
}
36.6% {
transform: scaleX(0.1);
animation-timing-function: cubic-bezier(0.334731432, 0.124819821, 0.785843996, 1);
}
69.15% {
transform: scaleX(0.83);
animation-timing-function: cubic-bezier(0.225732004, 0, 0.233648906, 1.3709798);
}
100% {
transform: scaleX(0.1);
}
}
@keyframes md-progress-linear-indeterminate-1 {
0% {
left: math.div(-378.6 * 100%, 360);
animation-timing-function: linear;
}
20% {
left: math.div(-378.6 * 100%, 360);
animation-timing-function: cubic-bezier(0.5, 0, 0.701732, 0.495818703);
}
69.15% {
left: math.div(77.4 * 100%, 360);
animation-timing-function: cubic-bezier(0.302435, 0.38135197, 0.55, 0.956352125);
}
100% {
left: math.div(343.6 * 100%, 360);
}
}
@keyframes md-progress-linear-indeterminate-scale-2 {
0% {
transform: scaleX(0.1);
animation-timing-function: cubic-bezier(0.205028172, 0.057050836, 0.57660995, 0.453970841);
}
19.15% {
transform: scaleX(0.57);
animation-timing-function: cubic-bezier(0.152312994, 0.196431957, 0.648373778, 1.00431535);
}
44.15% {
transform: scaleX(0.91);
animation-timing-function: cubic-bezier(0.25775882, -0.003163357, 0.211761916, 1.38178961);
}
100% {
transform: scaleX(0.1);
}
}
@keyframes md-progress-linear-indeterminate-2 {
0% {
left: math.div(-197.6 * 100%, 360);
animation-timing-function: cubic-bezier(0.15, 0, 0.5150584, 0.409684966);
}
25% {
left: math.div(-62.1 * 100%, 360);
animation-timing-function: cubic-bezier(0.3103299, 0.284057684, 0.8, 0.733718979);
}
48.35% {
left: math.div(106.2 * 100%, 360);
animation-timing-function: cubic-bezier(0.4, 0.627034903, 0.6, 0.902025796);
}
100% {
left: math.div(422.6 * 100%, 360);
}
}
================================================
FILE: src/components/progressLinear/progress-linear.spec.js
================================================
describe('mdProgressLinear', function() {
var element, $rootScope, $compile, $mdConstant;
function makeElement(attrs) {
element = $compile(
'
' +
' ' +
'
'
)($rootScope);
$rootScope.$digest();
return element;
}
beforeEach(function() {
module('material.components.progressLinear');
inject(function(_$compile_, _$rootScope_, _$mdConstant_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$mdConstant = _$mdConstant_;
});
});
afterEach(function() {
element && element.remove();
});
it('should auto-set the md-mode to "indeterminate" if not specified', function() {
var element = makeElement();
var progress = element.find('md-progress-linear');
expect(progress.attr('md-mode')).toEqual('indeterminate');
});
it('should auto-set the md-mode to "indeterminate" if specified a not valid mode', function() {
var element = makeElement('md-mode="test"');
var progress = element.find('md-progress-linear');
expect(progress.attr('md-mode')).toEqual('indeterminate');
});
it('should trim the md-mode value', function() {
var element = makeElement('md-mode=" indeterminate"');
var progress = element.find('md-progress-linear');
expect(progress.attr('md-mode')).toEqual('indeterminate');
});
it('should auto-set the md-mode to "determinate" if not specified but has value', function() {
var element = makeElement('value="{{progress}}"');
var progress = element.find('md-progress-linear');
$rootScope.$apply('progress = 50');
expect(progress.attr('md-mode')).toEqual('determinate');
});
it('should set not transform if mode is undefined', function() {
var element = makeElement('value="{{progress}}" md-mode="{{mode}}"');
var bar2 = element[0].querySelector('.md-bar2');
$rootScope.$apply(function() {
$rootScope.progress = 50;
$rootScope.mode = '';
});
expect(bar2.style[$mdConstant.CSS.TRANSFORM]).toEqual('');
});
it('should set transform based on value', function() {
var element = makeElement('value="{{progress}}" md-mode="determinate"');
var bar2 = element[0].querySelector('.md-bar2');
$rootScope.$apply('progress = 50');
expect(bar2.style[$mdConstant.CSS.TRANSFORM]).toEqual('translateX(-25%) scale(0.5, 1)');
});
it('should update aria-valuenow', function() {
var element = makeElement('value="{{progress}}"');
var progress = element.find('md-progress-linear');
$rootScope.$apply('progress = 50');
expect(progress.eq(0).attr('aria-valuenow')).toEqual('50');
});
it('should set transform based on buffer value', function() {
var element = makeElement('value="{{progress}}" md-buffer-value="{{progress2}}" md-mode="buffer"');
var bar1 = element[0].querySelector('.md-bar1');
$rootScope.$apply(function() {
$rootScope.progress = 50;
$rootScope.progress2 = 75;
});
expect(bar1.style[$mdConstant.CSS.TRANSFORM]).toEqual('translateX(-12.5%) scale(0.75, 1)');
});
it('should not set transform in query mode', function() {
var element = makeElement('md-mode="query" value="{{progress}}"');
var bar2 = element[0].querySelector('.md-bar2');
$rootScope.$apply('progress = 80');
expect(bar2.style[$mdConstant.CSS.TRANSFORM]).toBeFalsy();
});
describe('disabled mode', function() {
it('should hide the element', function() {
var element = makeElement('disabled');
var progress = element.find('md-progress-linear').eq(0);
expect(progress.hasClass('_md-progress-linear-disabled')).toBe(true);
});
it('should toggle the mode on the container', function() {
var element = makeElement('md-mode="query" ng-disabled="isDisabled"');
var container = angular.element(element[0].querySelector('.md-container'));
var modeClass = 'md-mode-query';
expect(container.hasClass(modeClass)).toBe(true);
$rootScope.$apply('isDisabled = true');
expect(container.hasClass(modeClass)).toBe(false);
$rootScope.$apply('isDisabled = false');
expect(container.hasClass(modeClass)).toBe(true);
});
});
});
================================================
FILE: src/components/radioButton/demoBasicUsage/index.html
================================================
================================================
FILE: src/components/radioButton/demoBasicUsage/script.js
================================================
angular
.module('radioDemo1', ['ngMaterial'])
.controller('AppCtrl', function($scope) {
$scope.data = {
group1 : 'Banana',
group2 : '2',
group3 : 'avatar-1'
};
$scope.avatarData = [{
id: "avatars:svg-1",
title: 'avatar 1',
value: 'avatar-1'
},{
id: "avatars:svg-2",
title: 'avatar 2',
value: 'avatar-2'
},{
id: "avatars:svg-3",
title: 'avatar 3',
value: 'avatar-3'
}];
$scope.radioData = [
{ label: '1', value: 1 },
{ label: '2', value: 2 },
{ label: '3', value: '3', isDisabled: true },
{ label: '4', value: '4' }
];
$scope.submit = function() {
alert('submit');
};
$scope.addItem = function() {
var r = Math.ceil(Math.random() * 1000);
$scope.radioData.push({ label: r, value: r });
};
$scope.removeItem = function() {
$scope.radioData.pop();
};
})
.config(function($mdIconProvider) {
$mdIconProvider.iconSet("avatars", 'icons/avatar-icons.svg',128);
});
================================================
FILE: src/components/radioButton/demoBasicUsage/style.css
================================================
body {
padding: 20px;
}
hr {
margin-left: -20px;
opacity: 0.3;
}
md-radio-group {
width: 150px;
}
p:last-child {
padding-bottom: 50px;
}
form {
padding: 0 20px;
}
.radioValue {
margin-left: 5px;
color: #0f9d58;
font-weight: bold;
}
md-icon {
margin: 0 20px 20px;
width: 128px;
height: 128px;
}
.ipsum {
color: saddlebrown;
font-size: 0.9em;
}
.demo-description {
margin-bottom: 0;
}
================================================
FILE: src/components/radioButton/demoMultiColumn/index.html
================================================
Contact List
{{person.title}}
{{person.fullName}}
Selected User: {{ctrl.selectedUser()}}
================================================
FILE: src/components/radioButton/demoMultiColumn/script.js
================================================
angular
.module('radioMultiColumnDemo', ['ngMaterial'])
.controller('ContactController', function($scope, $filter) {
var self = this;
self.contacts = [{
'id': 1,
'fullName': 'María Guadalupe',
'lastName': 'Guadalupe',
'title': "CEO, Found"
}, {
'id': 2,
'fullName': 'Gabriel García Márquez',
'lastName': 'Márquez',
'title': "VP Sales & Marketing"
}, {
'id': 3,
'fullName': 'Miguel de Cervantes',
'lastName': 'Cervantes',
'title': "Manager, Operations"
}, {
'id': 4,
'fullName': 'Pacorro de Castel',
'lastName': 'Castel',
'title': "Security"
}];
self.selectedId = 2;
self.selectedUser = function() {
return $filter('filter')(self.contacts, { id: self.selectedId })[0].lastName;
};
});
================================================
FILE: src/components/radioButton/demoMultiColumn/style.css
================================================
h2 {
margin: 16px;
}
.demo-checked {
background-color: #ECFAFB;
border-radius: 2px;
}
.demo-row {
border-bottom: 1px dashed #ddd;
}
.demo-row:last-child {
border-bottom: 0px dashed #ddd;
}
label {
font-weight: bolder;
}
.demo-title {
flex: 0 1 200px;
}
html[dir="rtl"] .demo-bidi {
padding-right: 20px;
padding-left: 0;
}
================================================
FILE: src/components/radioButton/radio-button-theme.scss
================================================
md-radio-button.md-THEME_NAME-theme {
.md-off {
border-color: '{{foreground-2}}';
}
.md-on {
background-color: '{{accent-color-0.87}}';
}
&.md-checked .md-off {
border-color: '{{accent-color-0.87}}';
}
&.md-checked .md-ink-ripple {
color: '{{accent-color-0.87}}';
}
.md-container .md-ripple {
color: '{{accent-A700}}';
}
}
md-radio-group.md-THEME_NAME-theme,
md-radio-button.md-THEME_NAME-theme {
&:not([disabled]) {
.md-primary,
&.md-primary {
.md-on {
background-color: '{{primary-color-0.87}}';
}
.md-checked,
&.md-checked {
.md-off {
border-color: '{{primary-color-0.87}}';
}
}
.md-checked,
&.md-checked {
.md-ink-ripple {
color: '{{primary-color-0.87}}';
}
}
.md-container .md-ripple {
color: '{{primary-600}}';
}
}
.md-warn,
&.md-warn {
.md-on {
background-color: '{{warn-color-0.87}}';
}
.md-checked,
&.md-checked {
.md-off {
border-color: '{{warn-color-0.87}}';
}
}
.md-checked,
&.md-checked {
.md-ink-ripple {
color: '{{warn-color-0.87}}';
}
}
.md-container .md-ripple {
color: '{{warn-600}}';
}
}
}
&[disabled] {
color: '{{foreground-3}}';
.md-container .md-off {
border-color: '{{foreground-3}}';
}
.md-container .md-on {
border-color: '{{foreground-3}}';
}
}
}
md-radio-group.md-THEME_NAME-theme {
.md-checked .md-ink-ripple {
color: '{{accent-color-0.26}}';
}
&.md-primary .md-checked:not([disabled]) .md-ink-ripple, .md-checked:not([disabled]).md-primary .md-ink-ripple {
color: '{{primary-color-0.26}}';
}
}
md-radio-group.md-THEME_NAME-theme.md-focused.ng-empty>md-radio-button:first-child {
.md-container:before {
background-color: '{{foreground-3-0.26}}';
}
}
md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) {
.md-checked .md-container:before {
background-color: '{{accent-color-0.26}}';
}
&.md-primary .md-checked .md-container:before,
.md-checked.md-primary .md-container:before {
background-color: '{{primary-color-0.26}}';
}
&.md-warn .md-checked .md-container:before,
.md-checked.md-warn .md-container:before {
background-color: '{{warn-color-0.26}}';
}
}
================================================
FILE: src/components/radioButton/radio-button.js
================================================
/**
* @ngdoc module
* @name material.components.radioButton
* @description radioButton module!
*/
angular.module('material.components.radioButton', [
'material.core'
])
.directive('mdRadioGroup', mdRadioGroupDirective)
.directive('mdRadioButton', mdRadioButtonDirective);
/**
* @type {Readonly<{NEXT: number, CURRENT: number, PREVIOUS: number}>}
*/
var incrementSelection = Object.freeze({PREVIOUS: -1, CURRENT: 0, NEXT: 1});
/**
* @ngdoc directive
* @module material.components.radioButton
* @name mdRadioGroup
*
* @restrict E
*
* @description
* The `
` directive identifies a grouping
* container for the 1..n grouped radio buttons; specified using nested
* `` elements.
*
* The radio button uses the accent color by default. The primary color palette may be used with
* the `md-primary` class.
*
* Note: `` and `` handle `tabindex` differently
* than the native ` ` controls. Whereas the native controls
* force the user to tab through all the radio buttons, ``
* is focusable and by default the ``s are not.
*
* @param {string} ng-model Assignable angular expression to data-bind to.
* @param {string=} ng-change AngularJS expression to be executed when input changes due to user
* interaction.
* @param {boolean=} md-no-ink If present, disables ink ripple effects.
*
* @usage
*
*
*
* {{ item.label }}
*
*
*
*/
function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
RadioGroupController.prototype = createRadioGroupControllerProto();
return {
restrict: 'E',
controller: ['$element', RadioGroupController],
require: ['mdRadioGroup', '?ngModel'],
link: { pre: linkRadioGroup }
};
function linkRadioGroup(scope, element, attr, controllers) {
// private md component indicator for styling
element.addClass('_md');
$mdTheming(element);
var radioGroupController = controllers[0];
var ngModelCtrl = controllers[1] || $mdUtil.fakeNgModel();
radioGroupController.init(ngModelCtrl);
scope.mouseActive = false;
element
.attr({
'role': 'radiogroup',
'tabIndex': element.attr('tabindex') || '0'
})
.on('keydown', keydownListener)
.on('mousedown', function() {
scope.mouseActive = true;
$timeout(function() {
scope.mouseActive = false;
}, 100);
})
.on('focus', function() {
if (scope.mouseActive === false) {
radioGroupController.$element.addClass('md-focused');
}
})
.on('blur', function() {
radioGroupController.$element.removeClass('md-focused');
});
// Initially set the first radio button as the aria-activedescendant. This will be overridden
// if a 'checked' radio button gets rendered. We need to wait for the nextTick here so that the
// radio buttons have their id values assigned.
$mdUtil.nextTick(function () {
var radioButtons = getRadioButtons(radioGroupController.$element);
if (radioButtons.count() &&
!radioGroupController.$element[0].hasAttribute('aria-activedescendant')) {
radioGroupController.setActiveDescendant(radioButtons.first().id);
}
});
/**
* Apply the md-focused class if it isn't already applied.
*/
function setFocus() {
if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
}
/**
* @param {KeyboardEvent} keyboardEvent
*/
function keydownListener(keyboardEvent) {
var keyCode = keyboardEvent.which || keyboardEvent.keyCode;
// Only listen to events that we originated ourselves
// so that we don't trigger on things like arrow keys in inputs.
if (keyCode !== $mdConstant.KEY_CODE.ENTER &&
keyboardEvent.currentTarget !== keyboardEvent.target) {
return;
}
switch (keyCode) {
case $mdConstant.KEY_CODE.LEFT_ARROW:
case $mdConstant.KEY_CODE.UP_ARROW:
keyboardEvent.preventDefault();
radioGroupController.selectPrevious();
setFocus();
break;
case $mdConstant.KEY_CODE.RIGHT_ARROW:
case $mdConstant.KEY_CODE.DOWN_ARROW:
keyboardEvent.preventDefault();
radioGroupController.selectNext();
setFocus();
break;
case $mdConstant.KEY_CODE.SPACE:
keyboardEvent.preventDefault();
radioGroupController.selectCurrent();
break;
case $mdConstant.KEY_CODE.ENTER:
var form = angular.element($mdUtil.getClosest(element[0], 'form'));
if (form.length > 0) {
form.triggerHandler('submit');
}
break;
}
}
}
/**
* @param {JQLite} $element
* @constructor
*/
function RadioGroupController($element) {
this._radioButtonRenderFns = [];
this.$element = $element;
}
function createRadioGroupControllerProto() {
return {
init: function(ngModelCtrl) {
this._ngModelCtrl = ngModelCtrl;
this._ngModelCtrl.$render = angular.bind(this, this.render);
},
add: function(rbRender) {
this._radioButtonRenderFns.push(rbRender);
},
remove: function(rbRender) {
var index = this._radioButtonRenderFns.indexOf(rbRender);
if (index !== -1) {
this._radioButtonRenderFns.splice(index, 1);
}
},
render: function() {
this._radioButtonRenderFns.forEach(function(rbRender) {
rbRender();
});
},
setViewValue: function(value, eventType) {
this._ngModelCtrl.$setViewValue(value, eventType);
// update the other radio buttons as well
this.render();
},
getViewValue: function() {
return this._ngModelCtrl.$viewValue;
},
selectCurrent: function() {
return changeSelectedButton(this.$element, incrementSelection.CURRENT);
},
selectNext: function() {
return changeSelectedButton(this.$element, incrementSelection.NEXT);
},
selectPrevious: function() {
return changeSelectedButton(this.$element, incrementSelection.PREVIOUS);
},
setActiveDescendant: function (radioId) {
this.$element.attr('aria-activedescendant', radioId);
},
isDisabled: function() {
return this.$element[0].hasAttribute('disabled');
}
};
}
/**
* Coerce all child radio buttons into an array, then wrap them in an iterator.
* @param parent {!JQLite}
* @return {{add: add, next: (function()), last: (function(): any|null), previous: (function()), count: (function(): number), hasNext: (function(*=): Array.length|*|number|boolean), inRange: (function(*): boolean), remove: remove, contains: (function(*=): *|boolean), itemAt: (function(*=): any|null), findBy: (function(*, *): *[]), hasPrevious: (function(*=): Array.length|*|number|boolean), items: (function(): *[]), indexOf: (function(*=): number), first: (function(): any|null)}}
*/
function getRadioButtons(parent) {
return $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
}
/**
* Change the radio group's selected button by a given increment.
* If no button is selected, select the first button.
* @param {JQLite} parent the md-radio-group
* @param {incrementSelection} increment enum that determines whether the next or
* previous button is clicked. For current, only the first button is selected, otherwise the
* current selection is maintained (by doing nothing).
*/
function changeSelectedButton(parent, increment) {
var buttons = getRadioButtons(parent);
var target;
if (buttons.count()) {
var validate = function (button) {
// If disabled, then NOT valid
return !angular.element(button).attr("disabled");
};
var selected = parent[0].querySelector('md-radio-button.md-checked');
if (!selected) {
target = buttons.first();
} else if (increment === incrementSelection.PREVIOUS ||
increment === incrementSelection.NEXT) {
target = buttons[
increment === incrementSelection.PREVIOUS ? 'previous' : 'next'
](selected, validate);
}
if (target) {
// Activate radioButton's click listener (triggerHandler won't create a real click event)
angular.element(target).triggerHandler('click');
}
}
}
}
/**
* @ngdoc directive
* @module material.components.radioButton
* @name mdRadioButton
*
* @restrict E
*
* @description
* The ``directive is the child directive required to be used within `` elements.
*
* While similar to the ` ` directive,
* the `` directive provides ink effects, ARIA support, and
* supports use within named radio groups.
*
* One of `value` or `ng-value` must be set so that the `md-radio-group`'s model is set properly when the
* `md-radio-button` is selected.
*
* @param {string} value The value to which the model should be set when selected.
* @param {string} ng-value AngularJS expression which sets the value to which the model should
* be set when selected.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} aria-label Adds label to radio button for accessibility.
* Defaults to radio button's text. If no text content is available, a warning will be logged.
*
* @usage
*
*
*
* Label 1
*
*
*
* Green
*
*
*
*
*/
function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
var CHECKED_CSS = 'md-checked';
return {
restrict: 'E',
require: '^mdRadioGroup',
transclude: true,
template: '' +
'
',
link: link
};
function link(scope, element, attr, radioGroupController) {
var lastChecked;
$mdTheming(element);
configureAria(element);
element.addClass('md-auto-horizontal-margin');
// ngAria overwrites the aria-checked inside a $watch for ngValue.
// We should defer the initialization until all the watches have fired.
// This can also be fixed by removing the `lastChecked` check, but that'll
// cause more DOM manipulation on each digest.
if (attr.ngValue) {
$mdUtil.nextTick(initialize, false);
} else {
initialize();
}
/**
* Initializes the component.
*/
function initialize() {
if (!radioGroupController) {
throw 'RadioButton: No RadioGroupController could be found.';
}
radioGroupController.add(render);
attr.$observe('value', render);
element
.on('click', listener)
.on('$destroy', function() {
radioGroupController.remove(render);
});
}
/**
* On click functionality.
*/
function listener(ev) {
if (element[0].hasAttribute('disabled') || radioGroupController.isDisabled()) return;
scope.$apply(function() {
radioGroupController.setViewValue(attr.value, ev && ev.type);
});
}
/**
* Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent).
* Update the `aria-activedescendant` attribute.
*/
function render() {
var checked = radioGroupController.getViewValue() == attr.value;
if (checked === lastChecked) return;
if (element[0] && element[0].parentNode &&
element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') {
// If the radioButton is inside a div, then add class so highlighting will work.
element.parent().toggleClass(CHECKED_CSS, checked);
}
if (checked) {
radioGroupController.setActiveDescendant(element.attr('id'));
}
lastChecked = checked;
element
.attr('aria-checked', checked)
.toggleClass(CHECKED_CSS, checked);
}
/**
* Inject ARIA-specific attributes appropriate for each radio button
*/
function configureAria(element) {
element.attr({
id: attr.id || 'radio_' + $mdUtil.nextUid(),
role: 'radio',
'aria-checked': 'false'
});
$mdAria.expectWithText(element, 'aria-label');
}
}
}
================================================
FILE: src/components/radioButton/radio-button.scss
================================================
$radio-width: 20px !default;
$radio-height: $radio-width !default;
$radio-text-margin: 10px !default;
$radio-top-left: 12px !default;
$radio-margin: 16px !default;
@mixin md-radio-button-disabled {
cursor: default;
.md-container {
cursor: default;
}
}
md-radio-button {
box-sizing: border-box;
display: block;
margin-bottom: $radio-margin;
white-space: nowrap;
cursor: pointer;
position: relative;
// When the radio-button is disabled.
&[disabled] {
@include md-radio-button-disabled();
}
.md-container {
position: absolute;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
display: inline-block;
width: $radio-width;
height: $radio-width;
cursor: pointer;
@include rtl(left, 0, auto);
@include rtl(right, auto, 0);
.md-ripple-container {
position: absolute;
display: block;
width: auto;
height: auto;
left: -15px;
top: -15px;
right: -15px;
bottom: -15px;
}
&:before {
box-sizing: border-box;
background-color: transparent;
border-radius: 50%;
content: '';
position: absolute;
display: block;
height: auto;
left: 0;
top: 0;
right: 0;
bottom: 0;
transition: all 0.5s;
width: auto;
}
}
&.md-align-top-left > div.md-container {
top: $radio-top-left;
}
.md-off {
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
width: $radio-width;
height: $radio-width;
border-style: solid;
border-width: 2px;
border-radius: 50%;
transition: border-color ease 0.28s;
}
.md-on {
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
width: $radio-width;
height: $radio-width;
border-radius: 50%;
transition: transform ease 0.28s;
transform: scale(0);
}
&.md-checked .md-on {
transform: scale(0.50);
}
.md-label {
box-sizing: border-box;
position: relative;
display: inline-block;
@include rtl(margin-left, $radio-text-margin + $radio-width, 0);
@include rtl(margin-right, 0, $radio-text-margin + $radio-width);
vertical-align: middle;
white-space: normal;
pointer-events: none;
width: auto;
}
}
md-radio-group {
&:focus {
outline: none;
}
&.md-focused.ng-not-empty {
.md-checked .md-container:before {
left: -8px;
top: -8px;
right: -8px;
bottom: -8px;
}
}
&.md-focused.ng-empty>md-radio-button:first-child {
.md-container:before {
left: -8px;
top: -8px;
right: -8px;
bottom: -8px;
}
}
&[disabled] md-radio-button {
@include md-radio-button-disabled();
}
}
@include when-layout-row(md-radio-button) {
margin-bottom: 0;
}
.md-inline-form {
md-radio-group {
margin: $input-container-vertical-margin 0 $input-container-vertical-margin + 1px;
md-radio-button {
display: inline-block;
height: 30px;
padding: 2px 10px 2px 6px;
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0;
.md-label {
top: 4px;
}
.md-container {
margin-top: 2px;
}
}
}
}
@media screen and (-ms-high-contrast: active) {
md-radio-button.md-default-theme .md-on {
background-color: #fff;
}
}
================================================
FILE: src/components/radioButton/radio-button.spec.js
================================================
describe('mdRadioButton component', function() {
var CHECKED_CSS = 'md-checked';
beforeEach(module('material.components.radioButton', 'ngAria'));
describe('md-radio-group', function() {
it('should have `._md` class indicator',inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
expect(element.hasClass('_md')).toBe(true);
}));
it('should correctly apply the checked class', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
$rootScope.$apply('color = "green"');
var radioButtons = element.find('md-radio-button');
expect(radioButtons.eq(0).hasClass(CHECKED_CSS)).toEqual(false);
expect(radioButtons.eq(1).hasClass(CHECKED_CSS)).toEqual(true);
}));
it('should support mixed values', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
$rootScope.$apply('value = 1');
var radioButtons = element.find('md-radio-button');
expect(radioButtons.eq(0).hasClass(CHECKED_CSS)).toEqual(true);
}));
it('should set the role attribute', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
var radioButton = element.find('md-radio-button').eq(0);
expect(element.eq(0).attr('role')).toEqual('radiogroup');
expect(radioButton.attr('role')).toEqual('radio');
}));
it('should apply aria state attributes', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
$rootScope.$apply('color = "green"');
var radioButtons = element.find('md-radio-button');
expect(radioButtons.eq(0).attr('aria-checked')).toEqual('false');
expect(radioButtons.eq(1).attr('aria-checked')).toEqual('true');
expect(element.attr('aria-activedescendant')).toEqual(radioButtons.eq(1).attr('id'));
expect(element.attr('aria-activedescendant')).not.toEqual(radioButtons.eq(0).attr('id'));
}));
it('should warn developers if no label is specified', inject(function($compile, $rootScope, $log) {
spyOn($log, "warn");
$compile(
'' +
' ' +
' ' +
' ')
($rootScope);
expect($log.warn).toHaveBeenCalled();
}));
it('should create an aria label from provided text', inject(function($compile, $rootScope) {
var element = $compile(
'' +
'Blue ' +
'Green ' +
' ')
($rootScope);
var radioButtons = element.find('md-radio-button');
expect(radioButtons.eq(0).attr('aria-label')).toEqual('Blue');
}));
it('should disable all child radio buttons', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ')
($rootScope);
var radioButton = element.find('md-radio-button');
$rootScope.$apply('isDisabled = true');
$rootScope.$apply('color = null');
radioButton.triggerHandler('click');
expect($rootScope.color).toBe(null);
$rootScope.$apply('isDisabled = false');
radioButton.triggerHandler('click');
expect($rootScope.color).toBe('white');
}));
it('should preserve tabindex', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
expect(element.attr('tabindex')).toEqual('3');
}));
it('should be operable via arrow keys', inject(function($compile, $rootScope, $mdConstant) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
$rootScope.$apply('color = "blue"');
element.triggerHandler({
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.RIGHT_ARROW,
currentTarget: element[0],
target: element[0]
});
expect($rootScope.color).toEqual('green');
}));
it('should not set focus state on mousedown', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
$rootScope.$apply();
element.triggerHandler('mousedown');
expect(element).not.toHaveClass('md-focused');
}));
it('should apply focus class on focus and remove on blur', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
$rootScope.$apply();
element.triggerHandler('focus');
expect(element[0]).toHaveClass('md-focused');
element.triggerHandler('blur');
expect(element[0]).not.toHaveClass('md-focused');
}));
it('should apply focus class on keyboard interaction', inject(function($compile, $rootScope, $mdConstant) {
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
$rootScope.$apply();
element.triggerHandler('mousedown');
element.triggerHandler({
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.DOWN_ARROW,
currentTarget: element[0],
target: element[0]
});
expect(element[0]).toHaveClass('md-focused');
}));
it('should apply aria-checked properly when using ng-value', inject(function($compile, $rootScope, $timeout) {
$rootScope.color = 'blue';
var element = $compile(
'' +
' ' +
' ' +
' ' +
' ')
($rootScope);
$timeout.flush();
var checkedItems = element[0].querySelectorAll('[aria-checked="true"]');
var uncheckedItems = element[0].querySelectorAll('[aria-checked="false"]');
expect(checkedItems.length).toBe(1);
expect(uncheckedItems.length).toBe(2);
expect(checkedItems[0].getAttribute('value')).toBe($rootScope.color);
}));
});
describe('md-radio-button', function() {
it('should be static with no model', inject(function($compile, $rootScope) {
var element;
expect(function() {
element = $compile(
'' +
'' +
' ')
($rootScope);
}).not.toThrow();
var radioButtons = element.find('md-radio-button');
// Fire off the render function with no ngModel, make sure nothing
// goes unexpectedly.
expect(function() {
radioButtons.eq(0).triggerHandler('click');
}).not.toThrow();
}));
it('should update the model', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ' +
' ' +
' ')
($rootScope);
var radioButtons = element.find('md-radio-button');
$rootScope.$apply("color = 'white'");
expect(radioButtons.eq(0).hasClass(CHECKED_CSS)).toBe(true);
expect(radioButtons.eq(1).hasClass(CHECKED_CSS)).toBe(false);
expect(radioButtons.eq(2).hasClass(CHECKED_CSS)).toBe(false);
$rootScope.$apply("color = 'red'");
expect(radioButtons.eq(0).hasClass(CHECKED_CSS)).toBe(false);
expect(radioButtons.eq(1).hasClass(CHECKED_CSS)).toBe(true);
expect(radioButtons.eq(2).hasClass(CHECKED_CSS)).toBe(false);
radioButtons.eq(2).triggerHandler('click');
expect($rootScope.color).toBe('blue');
}));
it('should trigger a submit action', inject(function($compile, $rootScope, $mdConstant) {
$rootScope.testValue = false;
var element = $compile(
'' +
'' +
'
')
($rootScope);
var radioGroupElement = element.find('md-radio-group');
expect($rootScope.testValue).toBeFalsy();
radioGroupElement.triggerHandler({
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.ENTER
});
expect($rootScope.testValue).toBe(true);
}));
it('should correctly disable the button', inject(function($compile, $rootScope) {
var element = $compile(
'' +
' ' +
' ')
($rootScope);
var radioButton = element.find('md-radio-button');
$rootScope.$apply('isDisabled = true');
$rootScope.$apply('color = null');
radioButton.triggerHandler('click');
expect($rootScope.color).toBe(null);
$rootScope.$apply('isDisabled = false');
radioButton.triggerHandler('click');
expect($rootScope.color).toBe('white');
}));
it('should skip disabled on arrow key', inject(function($compile, $rootScope, $mdConstant) {
var element = $compile(
'' +
' ' +
' ' +
' ' +
' '
)($rootScope);
$rootScope.$apply('isDisabled = true');
$rootScope.$apply('color = "red"');
expect($rootScope.color).toBe("red");
rightArrow();
expect($rootScope.color).toEqual('blue');
rightArrow();
expect($rootScope.color).toEqual('red');
rightArrow();
expect($rootScope.color).toEqual('blue');
$rootScope.$apply('isDisabled = false');
rightArrow();
rightArrow();
expect($rootScope.color).toEqual('white');
rightArrow();
expect($rootScope.color).toEqual('blue');
function rightArrow() {
element.triggerHandler({
type: 'keydown',
target: element[0],
currentTarget: element[0],
keyCode: $mdConstant.KEY_CODE.RIGHT_ARROW
});
}
}));
it('should allow interpolation as a value', inject(function($compile, $rootScope) {
$rootScope.some = 11;
var element = $compile(
'' +
' ' +
' ' +
' ')
($rootScope);
var radioButtons = element.find('md-radio-button');
$rootScope.$apply(function() {
$rootScope.value = 'blue';
$rootScope.some = 'blue';
$rootScope.other = 'red';
});
expect(radioButtons.eq(0).hasClass(CHECKED_CSS)).toBe(true);
expect(radioButtons.eq(1).hasClass(CHECKED_CSS)).toBe(false);
radioButtons.eq(1).triggerHandler('click');
expect($rootScope.value).toBe('red');
$rootScope.$apply("other = 'non-red'");
expect(radioButtons.eq(0).hasClass(CHECKED_CSS)).toBe(false);
expect(radioButtons.eq(1).hasClass(CHECKED_CSS)).toBe(false);
}));
});
});
================================================
FILE: src/components/select/demoBasicUsage/index.html
================================================
================================================
FILE: src/components/select/demoBasicUsage/script.js
================================================
(function () {
'use strict';
angular
.module('selectDemoBasic', ['ngMaterial'])
.controller('AppCtrl', function() {
var ctrl = this;
ctrl.userState = '';
ctrl.states = ('AL AK AZ AR CA CO CT DE FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS ' +
'MO MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI ' +
'WY').split(' ').map(function (state) { return { abbrev: state }; });
});
})();
================================================
FILE: src/components/select/demoBasicUsage/style.css
================================================
================================================
FILE: src/components/select/demoOptionGroups/index.html
================================================
Pick your pizza below
Size
{{size.name}}
{{size.name}}
Toppings
{{topping.name}}
{{topping.name}}
You ordered a {{size.toLowerCase()}} pizza with
{{printSelectedToppings()}}.
================================================
FILE: src/components/select/demoOptionGroups/script.js
================================================
angular
.module('selectDemoOptGroups', ['ngMaterial'])
.controller('SelectOptGroupController', function($scope) {
$scope.sizes = [
{ surcharge: 'none', name: "small (12-inch)" },
{ surcharge: 'none', name: "medium (14-inch)" },
{ surcharge: 'extra', name: "large (16-inch)" },
{ surcharge: 'extra', name: "giant (42-inch)" }
];
$scope.toppings = [
{ category: 'meat', name: 'Pepperoni' },
{ category: 'meat', name: 'Sausage' },
{ category: 'meat', name: 'Ground Beef' },
{ category: 'meat', name: 'Bacon' },
{ category: 'veg', name: 'Mushrooms' },
{ category: 'veg', name: 'Onion' },
{ category: 'veg', name: 'Green Pepper' },
{ category: 'veg', name: 'Green Olives' }
];
$scope.selectedToppings = [];
$scope.printSelectedToppings = function printSelectedToppings() {
var numberOfToppings = this.selectedToppings.length;
// If there is more than one topping, we add an 'and'
// to be grammatically correct. If there are 3+ toppings,
// we also add an oxford comma.
if (numberOfToppings > 1) {
var needsOxfordComma = numberOfToppings > 2;
var lastToppingConjunction = (needsOxfordComma ? ',' : '') + ' and ';
var lastTopping = lastToppingConjunction +
this.selectedToppings[this.selectedToppings.length - 1];
return this.selectedToppings.slice(0, -1).join(', ') + lastTopping;
}
return this.selectedToppings.join('');
};
});
================================================
FILE: src/components/select/demoOptionsWithAsyncSearch/index.html
================================================
Select can call an arbitrary function on show. If this function returns a promise, it will display a loading indicator while it is being resolved:
{{user.name}}
You have assigned the task to: {{ user ? user.name : 'No one yet' }}
================================================
FILE: src/components/select/demoOptionsWithAsyncSearch/script.js
================================================
angular.module('selectDemoOptionsAsync', ['ngMaterial'])
.controller('SelectAsyncController', function($timeout, $scope) {
$scope.user = null;
$scope.users = null;
$scope.loadUsers = function() {
// Use timeout to simulate a 650ms request.
return $timeout(function() {
$scope.users = $scope.users || [
{ id: 1, name: 'Scooby Doo' },
{ id: 2, name: 'Shaggy Rodgers' },
{ id: 3, name: 'Fred Jones' },
{ id: 4, name: 'Daphne Blake' },
{ id: 5, name: 'Velma Dinkley' }
];
}, 650);
};
});
================================================
FILE: src/components/select/demoSelectHeader/index.html
================================================
Pick a vegetable below
Vegetables
{{vegetable}}
================================================
FILE: src/components/select/demoSelectHeader/script.js
================================================
(function () {
'use strict';
angular.module('selectDemoSelectHeader', ['ngMaterial'])
.controller('SelectHeaderController', function ($scope, $element) {
$scope.vegetables = ['Corn', 'Onions', 'Kale', 'Arugula', 'Peas', 'Zucchini'];
$scope.searchTerm = '';
$scope.clearSearchTerm = function () {
$scope.searchTerm = '';
};
// The md-select directive eats keydown events for some quick select
// logic. Since we have a search input here, we don't need that logic.
$element.find('input').on('keydown', function (ev) {
ev.stopPropagation();
});
});
})();
================================================
FILE: src/components/select/demoSelectHeader/style.css
================================================
/* Please note: All these selectors are only applied to children of elements with the 'selectdemoSelectHeader' class */
.demo-header-searchbox {
border: none;
outline: none;
height: 100%;
width: 100%;
padding: 0;
}
.demo-select-header {
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1), 0 0 0 0 rgba(0, 0, 0,
0.14), 0 0 0 0 rgba(0, 0, 0, 0.12);
padding-left: 16px;
height: 48px;
cursor: pointer;
position: relative;
display: flex;
width: auto;
}
md-content._md {
max-height: 240px;
}
md-input-container {
min-width: 112px;
}
================================================
FILE: src/components/select/demoSelectedText/index.html
================================================
Pick an item below
Items
Item {{item}}
================================================
FILE: src/components/select/demoSelectedText/script.js
================================================
angular
.module('selectDemoSelectedText', ['ngMaterial'])
.controller('SelectedTextController', function ($scope) {
$scope.items = [1, 2, 3, 4, 5, 6, 7];
$scope.selectedItem = undefined;
$scope.getSelectedText = function () {
if ($scope.selectedItem !== undefined) {
return "You have selected: Item " + $scope.selectedItem;
} else {
return "Please select an item";
}
};
});
================================================
FILE: src/components/select/demoTrackBy/index.html
================================================
Without trackBy
Items
{{ item.name }}
Initial model
{{ ::ctrl.selectedItem | json }}
Current model
{{ ctrl.selectedItem | json }}
Model has changed
With trackBy
Items
{{ item.name }}
Initial model
{{ ::ctrl.selectedItem | json }}
Current model
{{ ctrl.selectedItem | json }}
Model has changed
================================================
FILE: src/components/select/demoTrackBy/script.js
================================================
(function() {
'use strict';
angular
.module('selectDemoTrackBy', ['ngMaterial', 'ngMessages'])
.controller('AppCtrl', function() {
this.selectedItem = {
id: '5a61e00',
name: 'Bob',
randomAddedProperty: 123
};
this.items = [
{
id: '5a61e00',
name: 'Bob',
},
{
id: '5a61e01',
name: 'Max',
},
{
id: '5a61e02',
name: 'Alice',
},
];
});
})();
================================================
FILE: src/components/select/demoTrackBy/style.css
================================================
code {
display: block;
padding: 8px;
}
================================================
FILE: src/components/select/demoValidations/index.html
================================================
================================================
FILE: src/components/select/demoValidations/script.js
================================================
angular.module('selectDemoValidation', ['ngMaterial', 'ngMessages'])
.controller('AppCtrl', function($scope) {
$scope.clearValue = function() {
$scope.quest = undefined;
$scope.favoriteColor = undefined;
$scope.myForm.$setPristine();
};
$scope.save = function() {
if ($scope.myForm.$valid) {
$scope.myForm.$setSubmitted();
alert('Form was valid.');
} else {
alert('Form was invalid!');
}
};
});
================================================
FILE: src/components/select/select-theme.scss
================================================
md-input-container {
// The asterisk of the select should always use the warn color.
md-select.md-THEME_NAME-theme .md-select-value {
span:first-child:after {
color: '{{warn-A700}}'
}
}
// When the select is blurred and not invalid then the asterisk should use the foreground color.
&:not(.md-input-focused):not(.md-input-invalid) {
md-select.md-THEME_NAME-theme .md-select-value {
span:first-child:after {
color: '{{foreground-3}}';
}
}
}
&.md-input-focused:not(.md-input-has-value) {
md-select.md-THEME_NAME-theme .md-select-value {
color: '{{primary-color}}';
&.md-select-placeholder {
color: '{{primary-color}}';
}
}
}
&.md-input-invalid {
md-select.md-THEME_NAME-theme .md-select-value {
color: '{{warn-A700}}' !important;
border-bottom-color: '{{warn-A700}}' !important;
}
md-select.md-THEME_NAME-theme.md-no-underline .md-select-value {
border-bottom-color: transparent !important;
}
}
&:not(.md-input-invalid) {
&.md-input-focused {
&.md-accent {
.md-select-value {
border-color: '{{accent-color}}';
span {
color: '{{accent-color}}';
}
}
}
&.md-warn {
.md-select-value {
border-color: '{{warn-A700}}';
span {
color: '{{warn-A700}}';
}
}
}
}
}
}
md-select.md-THEME_NAME-theme {
&[disabled] .md-select-value {
border-bottom-color: transparent;
background-image: linear-gradient(to right, '{{foreground-3}}' 0%, '{{foreground-3}}' 33%, transparent 0%);
background-image: -ms-linear-gradient(left, transparent 0%, '{{foreground-3}}' 100%);
}
.md-select-value {
border-bottom-color: '{{foreground-4}}';
&.md-select-placeholder {
color: '{{foreground-3}}';
}
span:first-child:after {
color: '{{warn-A700}}'
}
}
&.md-no-underline .md-select-value {
border-bottom-color: transparent !important;
}
&.ng-invalid.ng-touched {
.md-select-value {
color: '{{warn-A700}}' !important;
border-bottom-color: '{{warn-A700}}' !important;
}
&.md-no-underline .md-select-value {
border-bottom-color: transparent !important;
}
}
&:not([disabled]):focus {
.md-select-value {
border-bottom-color: '{{primary-color}}';
color: '{{ foreground-1 }}';
&.md-select-placeholder {
color: '{{ foreground-1 }}';
}
}
&.md-no-underline .md-select-value {
border-bottom-color: transparent !important;
}
&.md-accent .md-select-value {
border-bottom-color: '{{accent-color}}';
}
&.md-warn .md-select-value {
border-bottom-color: '{{warn-color}}';
}
}
&[disabled] {
.md-select-value {
color: '{{foreground-3}}';
&.md-select-placeholder {
color: '{{foreground-3}}';
}
}
.md-select-icon {
color: '{{foreground-3}}';
}
}
.md-select-icon {
color: '{{foreground-2}}';
}
}
md-select-menu.md-THEME_NAME-theme {
md-content {
background-color: '{{background-hue-1}}';
md-optgroup {
color: '{{foreground-2}}';
}
md-option {
color: '{{foreground-1}}';
&[disabled] {
.md-text {
color: '{{foreground-3}}';
}
}
&:not([disabled]) {
&:hover {
background-color: '{{background-500-0.10}}'
}
&:focus,
&.md-focused {
background-color: '{{background-500-0.18}}'
}
}
&[selected] {
color: '{{primary-500}}';
&:focus,
&.md-focused {
color: '{{primary-600}}';
}
&.md-accent {
color: '{{accent-color}}';
&:focus,
&.md-focused {
color: '{{accent-A700}}';
}
}
}
}
}
}
.md-checkbox-enabled.md-THEME_NAME-theme {
@include checkbox-primary('[selected]');
md-option .md-text {
color: '{{foreground-1}}';
}
}
================================================
FILE: src/components/select/select.js
================================================
/**
* @ngdoc module
* @name material.components.select
*/
/***************************************************
### TODO ###
- [ ] Abstract placement logic in $mdSelect service to $mdMenu service
***************************************************/
var SELECT_EDGE_MARGIN = 8;
var selectNextId = 0;
var CHECKBOX_SELECTION_INDICATOR;
angular.module('material.components.select', [
'material.core',
'material.components.backdrop'
])
.directive('mdSelect', SelectDirective)
.directive('mdSelectMenu', SelectMenuDirective)
.directive('mdOption', OptionDirective)
.directive('mdOptgroup', OptgroupDirective)
.directive('mdSelectHeader', SelectHeaderDirective)
.provider('$mdSelect', SelectProvider);
/**
* @ngdoc directive
* @name mdSelect
* @restrict E
* @module material.components.select
*
* @description Displays a select box, bound to an `ng-model`. Selectable options are defined using
* the md-option element directive. Options can be grouped
* using the md-optgroup element directive.
*
* When the select is required and uses a floating label, then the label will automatically contain
* an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute.
*
* By default, the select will display with an underline to match other form elements. This can be
* disabled by applying the `md-no-underline` CSS class.
*
* @param {expression} ng-model Assignable angular expression to data-bind to.
* @param {expression=} ng-change Expression to be executed when the model value changes.
* @param {boolean=} multiple When present, allows for more than one option to be selected.
* The model is an array with the selected choices. **Note:** This attribute is only evaluated
* once; it is not watched.
* @param {expression=} md-on-close Expression to be evaluated when the select is closed.
* @param {expression=} md-on-open Expression to be evaluated when opening the select.
* Will hide the select options and show a spinner until the evaluated promise resolves.
* @param {expression=} md-selected-text Expression to be evaluated that will return a string
* to be displayed as a placeholder in the select input box when it is closed. The value
* will be treated as *text* (not html).
* @param {expression=} md-selected-html Expression to be evaluated that will return a string
* to be displayed as a placeholder in the select input box when it is closed. The value
* will be treated as *html*. The value must either be explicitly marked as trustedHtml or
* the ngSanitize module must be loaded.
* @param {string=} placeholder Placeholder hint text.
* @param {boolean=} md-no-asterisk When set to true, an asterisk will not be appended to the
* floating label. **Note:** This attribute is only evaluated once; it is not watched.
* @param {string=} aria-label Optional label for accessibility. Only necessary if no explicit label
* is present.
* @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container`
* element (for custom styling).
* @param {string=} md-select-only-option If specified, a `` will automatically select
* it's first option, if it only has one.
*
* @usage
* With a placeholder (label and aria-label are added dynamically)
*
*
*
* {{ opt }}
*
*
*
*
* With an explicit label
*
*
* State
*
* {{ opt }}
*
*
*
*
* Using the `md-select-header` element directive
*
* When a developer needs to put more than just a text label in the `md-select-menu`, they should
* use one or more `md-select-header`s. These elements can contain custom HTML which can be styled
* as desired. Use cases for this element include a sticky search bar and custom option group
* labels.
*
*
*
*
*
* Neighborhoods -
*
* {{ opt }}
*
*
*
*
* ## Selects and object equality
* When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles
* equality. Consider the following example:
*
* angular.controller('MyCtrl', function($scope) {
* $scope.users = [
* { id: 1, name: 'Bob' },
* { id: 2, name: 'Alice' },
* { id: 3, name: 'Steve' }
* ];
* $scope.selectedUser = { id: 1, name: 'Bob' };
* });
*
*
*
*
* {{ user.name }}
*
*
*
*
* At first one might expect that the select should be populated with "Bob" as the selected user.
* However, this is not true. To determine whether something is selected,
* `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`;
*
* Javascript's `==` operator does not check for deep equality (ie. that all properties
* on the object are the same), but instead whether the objects are *the same object in memory*.
* In this case, we have two instances of identical objects, but they exist in memory as unique
* entities. Because of this, the select will have no value populated for a selected user.
*
* To get around this, `ngModelController` provides a `track by` option that allows us to specify a
* different expression which will be used for the equality operator. As such, we can update our
* `html` to make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the
* `md-select` element. This converts our equality expression to be
* `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));`
* which results in Bob being selected as desired.
*
* **Note:** We do not support AngularJS's `track by` syntax. For instance
* `ng-options="user in users track by user.id"` will not work with `md-select`.
*
* Working HTML:
*
*
*
* {{ user.name }}
*
*
*
*/
function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce) {
return {
restrict: 'E',
require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
compile: compile,
controller: function() {
} // empty placeholder controller to be initialized in link
};
/**
* @param {JQLite} tElement
* @param {IAttributes} tAttrs
* @return {postLink}
*/
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(' ');
valueEl.append(' ');
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('').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(
'' +
' ' +
'
'
));
// Hide list [of item options] while loading async
tElement
.find('md-option')
.attr('ng-show', '$$loadingAsyncDone');
}
if (tAttrs.name) {
var autofillClone = angular.element(' ');
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('' + el.innerHTML + ' ');
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: ?` which would then get submitted.
// This also goes around having to prepend a dot to the name attribute.
autofillClone.append(
' '
);
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 = '' +
'';
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(" can only have *one* child ,