angular-material-npfixed
Version:
The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M
528 lines (445 loc) • 17.3 kB
JavaScript
(function() {
'use strict';
var $mdUtil, $interpolate, $log;
var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g;
var WHITESPACE = /\s+/g;
var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ];
var LAYOUT_OPTIONS = ['row', 'column'];
var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ];
var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ];
var config = {
/**
* Enable directive attribute-to-class conversions
* Developers can use `<body md-layout-css />` to quickly
* disable the Layout directives and prohibit the injection of Layout classNames
*/
enabled: true,
/**
* List of mediaQuery breakpoints and associated suffixes
*
* [
* { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" },
* { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" }
* ]
*/
breakpoints: []
};
registerLayoutAPI( angular.module('material.core.layout', ['ng']) );
/**
* registerLayoutAPI()
*
* The original ngMaterial Layout solution used attribute selectors and CSS.
*
* ```html
* <div layout="column"> My Content </div>
* ```
*
* ```css
* [layout] {
* box-sizing: border-box;
* display:flex;
* }
* [layout=column] {
* flex-direction : column
* }
* ```
*
* Use of attribute selectors creates significant performance impacts in some
* browsers... mainly IE.
*
* This module registers directives that allow the same layout attributes to be
* interpreted and converted to class selectors. The directive will add equivalent classes to each element that
* contains a Layout directive.
*
* ```html
* <div layout="column" class="layout layout-column"> My Content </div>
*```
*
* ```css
* .layout {
* box-sizing: border-box;
* display:flex;
* }
* .layout-column {
* flex-direction : column
* }
* ```
*/
function registerLayoutAPI(module){
var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
// NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA
var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ];
var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ];
var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ];
// Build directive registration functions for the standard Layout API... for all breakpoints.
angular.forEach(BREAKPOINTS, function(mqb) {
// Attribute directives with expected, observable value(s)
angular.forEach( API_WITH_VALUES, function(name){
var fullName = mqb ? name + "-" + mqb : name;
module.directive( directiveNormalize(fullName), attributeWithObserve(fullName));
});
// Attribute directives with no expected value(s)
angular.forEach( API_NO_VALUES, function(name){
var fullName = mqb ? name + "-" + mqb : name;
module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName));
});
});
// Register other, special directive functions for the Layout features:
module
.provider('$$mdLayout' , function() {
// Publish internal service for Layouts
return {
$get : angular.noop,
validateAttributeValue : validateAttributeValue,
validateAttributeUsage : validateAttributeUsage,
/**
* Easy way to disable/enable the Layout API.
* When disabled, this stops all attribute-to-classname generations
*/
disableLayouts : function(isDisabled) {
config.enabled = (isDisabled !== true);
}
};
})
.directive('mdLayoutCss' , disableLayoutDirective )
.directive('ngCloak' , buildCloakInterceptor('ng-cloak'))
.directive('layoutWrap' , attributeWithoutValue('layout-wrap'))
.directive('layoutNowrap' , attributeWithoutValue('layout-nowrap'))
.directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap'))
.directive('layoutFill' , attributeWithoutValue('layout-fill'))
// !! Deprecated attributes: use the `-lt` (aka less-than) notations
.directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true))
.directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true))
.directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true))
.directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true))
.directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md'))
.directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg'))
.directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md'))
.directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg'))
.directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md'))
.directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg'))
.directive('hideLtMd' , warnAttrNotSupported('hide-lt-md'))
.directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg'))
.directive('showLtMd' , warnAttrNotSupported('show-lt-md'))
.directive('showLtLg' , warnAttrNotSupported('show-lt-lg'))
// Determine if
.config( detectDisabledLayouts );
/**
* Converts snake_case to camelCase.
* Also there is special case for Moz prefix starting with upper case letter.
* @param name Name to normalize
*/
function directiveNormalize(name) {
return name
.replace(PREFIX_REGEXP, '')
.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter;
});
}
}
/**
* Detect if any of the HTML tags has a [md-layouts-disabled] attribute;
* If yes, then immediately disable all layout API features
*
* Note: this attribute should be specified on either the HTML or BODY tags
*/
/**
* @ngInject
*/
function detectDisabledLayouts() {
var isDisabled = !!document.querySelector('[md-layouts-disabled]');
config.enabled = !isDisabled;
}
/**
* Special directive that will disable ALL Layout conversions of layout
* attribute(s) to classname(s).
*
* <link rel="stylesheet" href="angular-material.min.css">
* <link rel="stylesheet" href="angular-material.layout.css">
*
* <body md-layout-css>
* ...
* </body>
*
* Note: Using md-layout-css directive requires the developer to load the Material
* Layout Attribute stylesheet (which only uses attribute selectors):
*
* `angular-material.layout.css`
*
* Another option is to use the LayoutProvider to configure and disable the attribute
* conversions; this would obviate the use of the `md-layout-css` directive
*
*/
function disableLayoutDirective() {
// Return a 1x-only, first-match attribute directive
config.enabled = false;
return {
restrict : 'A',
priority : '900'
};
}
/**
* Tail-hook ngCloak to delay the uncloaking while Layout transformers
* finish processing. Eliminates flicker with Material.Layouts
*/
function buildCloakInterceptor(className) {
return [ '$timeout', function($timeout){
return {
restrict : 'A',
priority : -10, // run after normal ng-cloak
compile : function( element ) {
if (!config.enabled) return angular.noop;
// Re-add the cloak
element.addClass(className);
return function( scope, element ) {
// Wait while layout injectors configure, then uncloak
// NOTE: $rAF does not delay enough... and this is a 1x-only event,
// $timeout is acceptable.
$timeout( function(){
element.removeClass(className);
}, 10, false);
};
}
};
}];
}
// *********************************************************************************
//
// These functions create registration functions for ngMaterial Layout attribute directives
// This provides easy translation to switch ngMaterial attribute selectors to
// CLASS selectors and directives; which has huge performance implications
// for IE Browsers
//
// *********************************************************************************
/**
* Creates a directive registration function where a possible dynamic attribute
* value will be observed/watched.
* @param {string} className attribute name; eg `layout-gt-md` with value ="row"
*/
function attributeWithObserve(className) {
return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
$mdUtil = _$mdUtil_;
$interpolate = _$interpolate_;
$log = _$log_;
return {
restrict: 'A',
compile: function(element, attr) {
var linkFn;
if (config.enabled) {
// immediately replace static (non-interpolated) invalid values...
validateAttributeUsage(className, attr, element, $log);
validateAttributeValue( className,
getNormalizedAttrValue(className, attr, ""),
buildUpdateFn(element, className, attr)
);
linkFn = translateWithValueToCssClass;
}
// Use for postLink to account for transforms after ng-transclude.
return linkFn || angular.noop;
}
};
}];
/**
* Add as transformed class selector(s), then
* remove the deprecated attribute selector
*/
function translateWithValueToCssClass(scope, element, attrs) {
var updateFn = updateClassWithValue(element, className, attrs);
var unwatch = attrs.$observe(attrs.$normalize(className), updateFn);
updateFn(getNormalizedAttrValue(className, attrs, ""));
scope.$on("$destroy", function() { unwatch(); });
}
}
/**
* Creates a registration function for ngMaterial Layout attribute directive.
* This is a `simple` transpose of attribute usage to class usage; where we ignore
* any attribute value
*/
function attributeWithoutValue(className) {
return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
$mdUtil = _$mdUtil_;
$interpolate = _$interpolate_;
$log = _$log_;
return {
restrict: 'A',
compile: function(element, attr) {
var linkFn;
if (config.enabled) {
// immediately replace static (non-interpolated) invalid values...
validateAttributeValue( className,
getNormalizedAttrValue(className, attr, ""),
buildUpdateFn(element, className, attr)
);
translateToCssClass(null, element);
// Use for postLink to account for transforms after ng-transclude.
linkFn = translateToCssClass;
}
return linkFn || angular.noop;
}
};
}];
/**
* Add as transformed class selector, then
* remove the deprecated attribute selector
*/
function translateToCssClass(scope, element) {
element.addClass(className);
}
}
/**
* After link-phase, do NOT remove deprecated layout attribute selector.
* Instead watch the attribute so interpolated data-bindings to layout
* selectors will continue to be supported.
*
* $observe() the className and update with new class (after removing the last one)
*
* e.g. `layout="{{layoutDemo.direction}}"` will update...
*
* NOTE: The value must match one of the specified styles in the CSS.
* For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since
* only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined.
*
*/
function updateClassWithValue(element, className) {
var lastClass;
return function updateClassFn(newValue) {
var value = validateAttributeValue(className, newValue || "");
if ( angular.isDefined(value) ) {
if (lastClass) element.removeClass(lastClass);
lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-");
element.addClass(lastClass);
}
};
}
/**
* Provide console warning that this layout attribute has been deprecated
*
*/
function warnAttrNotSupported(className) {
var parts = className.split("-");
return ["$log", function($log) {
$log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-<xxx>` variant.");
return angular.noop;
}];
}
/**
* Centralize warnings for known flexbox issues (especially IE-related issues)
*/
function validateAttributeUsage(className, attr, element, $log){
var message, usage, url;
var nodeName = element[0].nodeName.toLowerCase();
switch(className.replace(SUFFIXES,"")) {
case "flex":
if ((nodeName == "md-button") || (nodeName == "fieldset")){
// @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
// Use <div flex> wrapper inside (preferred) or outside
usage = "<" + nodeName + " " + className + "></" + nodeName + ">";
url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers";
message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details.";
$log.warn( $mdUtil.supplant(message, [usage, url]) );
}
}
}
/**
* For the Layout attribute value, validate or replace with default
* fallback value
*/
function validateAttributeValue(className, value, updateFn) {
var origValue = value;
if (!needsInterpolation(value)) {
switch (className.replace(SUFFIXES,"")) {
case 'layout' :
if ( !findIn(value, LAYOUT_OPTIONS) ) {
value = LAYOUT_OPTIONS[0]; // 'row';
}
break;
case 'flex' :
if (!findIn(value, FLEX_OPTIONS)) {
if (isNaN(value)) {
value = '';
}
}
break;
case 'flex-offset' :
case 'flex-order' :
if (!value || isNaN(+value)) {
value = '0';
}
break;
case 'layout-align' :
var axis = extractAlignAxis(value);
value = $mdUtil.supplant("{main}-{cross}",axis);
break;
case 'layout-padding' :
case 'layout-margin' :
case 'layout-fill' :
case 'layout-wrap' :
case 'layout-nowrap' :
case 'layout-nowrap' :
value = '';
break;
}
if (value != origValue) {
(updateFn || angular.noop)(value);
}
}
return value;
}
/**
* Replace current attribute value with fallback value
*/
function buildUpdateFn(element, className, attrs) {
return function updateAttrValue(fallback) {
if (!needsInterpolation(fallback)) {
// Do not modify the element's attribute value; so
// uses '<ui-layout layout="/api/sidebar.html" />' will not
// be affected. Just update the attrs value.
attrs[attrs.$normalize(className)] = fallback;
}
};
}
/**
* See if the original value has interpolation symbols:
* e.g. flex-gt-md="{{triggerPoint}}"
*/
function needsInterpolation(value) {
return (value || "").indexOf($interpolate.startSymbol()) > -1;
}
function getNormalizedAttrValue(className, attrs, defaultVal) {
var normalizedAttr = attrs.$normalize(className);
return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null;
}
function findIn(item, list, replaceWith) {
item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item;
var found = false;
if (item) {
list.forEach(function(it) {
it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it;
found = found || (it === item);
});
}
return found;
}
function extractAlignAxis(attrValue) {
var axis = {
main : "start",
cross: "stretch"
}, values;
attrValue = (attrValue || "");
if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) {
// For missing main-axis values
attrValue = "none" + attrValue;
}
values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-");
if ( values.length && (values[0] === "space") ) {
// for main-axis values of "space-around" or "space-between"
values = [ values[0]+"-"+values[1],values[2] ];
}
if ( values.length > 0 ) axis.main = values[0] || axis.main;
if ( values.length > 1 ) axis.cross = values[1] || axis.cross;
if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start";
if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch";
return axis;
}
})();