From 2478f1d2b064d5e06ba57d01b5460fe232f4b270 Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Fri, 9 Oct 2015 16:55:31 -0500 Subject: [PATCH] fix(layouts): interpolated values, validations, test & CSS fixes * improved consistent use of `attrs.$observe( )` to watch for interpolated attribute values (for Layout attributes which support value specifiers). * add validation of Layout attribute values with fallbacks to default value approprate to each attribute type * significant code cleanup for use of `$observe( )` * removed possible classname generation with raw, uninterpolated strings * removed multiple classname generation * fixed invalid classname generation when interpolation values are invalid * remove `md-` prefix from layout class names * remove all attribute selectors from layout.scss * deprecated use of `offset` attribute; now uses `layout-offset` * add box-sizing to all flex variants * add max-width/height to flex variants * fixed max-width/height for flex 33, 34, 66, and 67 variants. * complete refactor for spec testing of Layouts Fixes #5076. Fixes #5054. Refs #5014. Fixes #4994. Fixes #4959. Fixes #4902. Fixes #2954. Fixes #5014. Closes #5090. --- docs/app/css/layout-demo.css | 3 +- docs/app/partials/layout-options.tmpl.html | 8 +- src/core/services/layout/layout.js | 653 ++++++++++++--------- src/core/services/layout/layout.scss | 97 ++- src/core/services/layout/layout.spec.js | 398 +++++++++---- 5 files changed, 719 insertions(+), 440 deletions(-) diff --git a/docs/app/css/layout-demo.css b/docs/app/css/layout-demo.css index f194f212f76..3f6e22cbf3e 100644 --- a/docs/app/css/layout-demo.css +++ b/docs/app/css/layout-demo.css @@ -42,7 +42,8 @@ demo-include { margin-top: 16px; } -.layout-demo :not(.md-layout) { +.layout-demo :not(.layout-row), +.layout-demo :not(.layout-column) { border: 1px solid #eee; padding: 8px; } diff --git a/docs/app/partials/layout-options.tmpl.html b/docs/app/partials/layout-options.tmpl.html index d2a3f6032b6..62bb3d9a2ea 100644 --- a/docs/app/partials/layout-options.tmpl.html +++ b/docs/app/partials/layout-options.tmpl.html @@ -80,8 +80,8 @@
[flex=33]
-
[flex=67]
-
[flex=67]
+
[flex=67]
+
[flex=67]
[flex=33]
@@ -98,10 +98,10 @@
-
+
I flex to one-third of the space on mobile, and two-thirds on other devices.
-
+
I flex to two-thirds of the space on mobile, and one-third on other devices.
diff --git a/src/core/services/layout/layout.js b/src/core/services/layout/layout.js index 436ebb8b693..8b36149a78f 100644 --- a/src/core/services/layout/layout.js +++ b/src/core/services/layout/layout.js @@ -1,319 +1,414 @@ -(function () { +(function() { 'use strict'; - var $mdUtil, $$mdLayout, $parse, $interpolate; + var $mdUtil, $$mdLayout, $interpolate; + var SUFFIXES = /(-gt)?-(sm|md|lg)/g; + var WHITESPACE = /\s+/g; + + var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none']; + var LAYOUT_OPTIONS = ['row', 'column']; + var ALIGNMENT_OPTIONS = [ + "start start", "start center", "start end", + "center", "center center", "center start", "center end", + "end", "end center", "end start", "end end", + "space-around", "space-around center", "space-around start", "space-around end", + "space-between", "space-between center", "space-between start", "space-between end" + ]; + + + var config = { /** + * Enable directive attribute-to-class conversions + */ + enabled: true, + + /** + * List of mediaQuery breakpoints and associated suffixes * - * The original ngMaterial Layout solution used attribute selectors and CSS. - * - * ```html - *
My Content
- * ``` - * - * ```css - * [layout] { + * [ + * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" }, + * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" } + * ] + */ + breakpoints: [] + }; + + /** + * The original ngMaterial Layout solution used attribute selectors and CSS. + * + * ```html + *
My Content
+ * ``` + * + * ```css + * [layout] { * box-sizing: border-box; * display:flex; * } - * [layout=column] { + * [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 - *
My Content
- *``` - * - * ```css - * .layout { + * ``` + * + * 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 + *
My Content
+ *``` + * + * ```css + * .layout { * box-sizing: border-box; * display:flex; * } - * .layout-column { + * .layout-column { * flex-direction : column * } - * ``` - */ - angular.module('material.core.layout', [ 'ng' ]) - - /** - * Model of flags used by the Layout directives - * Allows changes while running tests or runtime app changes - */ - .factory("$$mdLayout", function() { - return { - removeAttributes : true - - }; - }) - - // Attribute directives with optional value(s) - - .directive('layout' , attributeWithObserve('layout' ) ) - .directive('layoutSm' , attributeWithObserve('layout-sm' ) ) - .directive('layoutGtSm' , attributeWithObserve('layout-gt-sm') ) - .directive('layoutMd' , attributeWithObserve('layout-md' ) ) - .directive('layoutGtMd' , attributeWithObserve('layout-gt-md') ) - .directive('layoutLg' , attributeWithObserve('layout-lg' ) ) - .directive('layoutGtLg' , attributeWithObserve('layout-gt-lg') ) - - .directive('flex' , attributeWithObserve('flex' ) ) - .directive('flexSm' , attributeWithObserve('flex-sm' ) ) - .directive('flexGtSm' , attributeWithObserve('flex-gt-sm' ) ) - .directive('flexMd' , attributeWithObserve('flex-md' ) ) - .directive('flexGtMd' , attributeWithObserve('flex-gt-md' ) ) - .directive('flexLg' , attributeWithObserve('flex-lg' ) ) - .directive('flexGtLg' , attributeWithObserve('flex-gt-lg' ) ) - - // Attribute directives with optional value(s) but directiveName is NOT added as a class - - .directive('layoutAlign' , attributeWithObserve('layout-align') ) - .directive('layoutAlignSm' , attributeWithObserve('layout-align-sm') ) - .directive('layoutAlignGtSm' , attributeWithObserve('layout-align-gt-sm') ) - .directive('layoutAlignMd' , attributeWithObserve('layout-align-md') ) - .directive('layoutAlignGtMd' , attributeWithObserve('layout-align-gt-md') ) - .directive('layoutAlignLg' , attributeWithObserve('layout-align-lg') ) - .directive('layoutAlignGtLg' , attributeWithObserve('layout-align-gt-lg') ) - - .directive('flexOrder' , attributeWithObserve('flex-order') ) - .directive('flexOrderSm' , attributeWithObserve('flex-order-sm') ) - .directive('flexOrderGtSm' , attributeWithObserve('flex-order-gt-sm') ) - .directive('flexOrderMd' , attributeWithObserve('flex-order-md') ) - .directive('flexOrderGtMd' , attributeWithObserve('flex-order-gt-md') ) - .directive('flexOrderLg' , attributeWithObserve('flex-order-lg') ) - .directive('flexOrderGtLg' , attributeWithObserve('flex-order-gt-lg') ) - - .directive('offset' , attributeWithObserve('offset') ) - .directive('offsetSm' , attributeWithObserve('offset-sm') ) - .directive('offsetGtSm' , attributeWithObserve('offset-gt-sm') ) - .directive('offsetMd' , attributeWithObserve('offset-md') ) - .directive('offsetGtMd' , attributeWithObserve('offset-gt-md') ) - .directive('offsetLg' , attributeWithObserve('offset-lg') ) - .directive('offsetGtLg' , attributeWithObserve('offset-gt-lg') ) - - // Attribute directives with no value(s) - - .directive('layoutMargin' , attributeWithoutValue('layout-margin') ) - .directive('layoutPadding' , attributeWithoutValue('layout-padding') ) - .directive('layoutWrap' , attributeWithoutValue('layout-wrap') ) - .directive('layoutFill' , attributeWithoutValue('layout-fill') ) - - .directive('hide' , attributeWithoutValue('hide') ) - .directive('hideSm' , attributeWithoutValue('hide-sm') ) - .directive('hideGtSm' , attributeWithoutValue('hide-gt-sm') ) - .directive('hideMd' , attributeWithoutValue('hide-md') ) - .directive('hideGtMd' , attributeWithoutValue('hide-gt-md') ) - .directive('hideLg' , attributeWithoutValue('hide-lg') ) - .directive('hideGtLg' , attributeWithoutValue('hide-gt-lg') ) - .directive('show' , attributeWithoutValue('show') ) - .directive('showSm' , attributeWithoutValue('show-sm') ) - .directive('showGtSm' , attributeWithoutValue('show-gt-sm') ) - .directive('showMd' , attributeWithoutValue('show-md') ) - .directive('showGtMd' , attributeWithoutValue('show-gt-md') ) - .directive('showLg' , attributeWithoutValue('show-lg') ) - .directive('showGtLg' , attributeWithoutValue('show-gt-lg') ) - - // !! 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('offset-lt-md') ) - .directive('offsetLtLg' , warnAttrNotSupported('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') ); + * ``` + */ + angular.module('material.core.layout', ['ng']) + + /** + * Model of flags used by the Layout directives + * Allows changes while running tests or runtime app changes + */ + .factory("$$mdLayout", function() { + return { + removeAttributes: true + }; + }) + + // Attribute directives with optional value(s) + + .directive('layout', attributeWithObserve('layout')) + .directive('layoutSm', attributeWithObserve('layout-sm')) + .directive('layoutGtSm', attributeWithObserve('layout-gt-sm')) + .directive('layoutMd', attributeWithObserve('layout-md')) + .directive('layoutGtMd', attributeWithObserve('layout-gt-md')) + .directive('layoutLg', attributeWithObserve('layout-lg')) + .directive('layoutGtLg', attributeWithObserve('layout-gt-lg')) + + .directive('flex', attributeWithObserve('flex')) + .directive('flexSm', attributeWithObserve('flex-sm')) + .directive('flexGtSm', attributeWithObserve('flex-gt-sm')) + .directive('flexMd', attributeWithObserve('flex-md')) + .directive('flexGtMd', attributeWithObserve('flex-gt-md')) + .directive('flexLg', attributeWithObserve('flex-lg')) + .directive('flexGtLg', attributeWithObserve('flex-gt-lg')) + + .directive('flexOrder', attributeWithObserve('flex-order')) + .directive('flexOrderSm', attributeWithObserve('flex-order-sm')) + .directive('flexOrderGtSm', attributeWithObserve('flex-order-gt-sm')) + .directive('flexOrderMd', attributeWithObserve('flex-order-md')) + .directive('flexOrderGtMd', attributeWithObserve('flex-order-gt-md')) + .directive('flexOrderLg', attributeWithObserve('flex-order-lg')) + .directive('flexOrderGtLg', attributeWithObserve('flex-order-gt-lg')) + + .directive('offset', attributeWithObserve('layout-offset')) + .directive('offsetSm', attributeWithObserve('layout-offset-sm')) + .directive('offsetGtSm', attributeWithObserve('layout-offset-gt-sm')) + .directive('offsetMd', attributeWithObserve('layout-offset-md')) + .directive('offsetGtMd', attributeWithObserve('layout-offset-gt-md')) + .directive('offsetLg', attributeWithObserve('layout-offset-lg')) + .directive('offsetGtLg', attributeWithObserve('layout-offset-gt-lg')) + .directive('layoutOffset', attributeWithObserve('layout-offset')) + .directive('layoutOffsetSm', attributeWithObserve('layout-offset-sm')) + .directive('layoutOffsetGtSm', attributeWithObserve('layout-offset-gt-sm')) + .directive('layoutOffsetMd', attributeWithObserve('layout-offset-md')) + .directive('layoutOffsetGtMd', attributeWithObserve('layout-offset-gt-md')) + .directive('layoutOffsetLg', attributeWithObserve('layout-offset-lg')) + .directive('layoutOffsetGtLg', attributeWithObserve('layout-offset-gt-lg')) + + .directive('layoutAlign', attributeWithObserve('layout-align')) + .directive('layoutAlignSm', attributeWithObserve('layout-align-sm')) + .directive('layoutAlignGtSm', attributeWithObserve('layout-align-gt-sm')) + .directive('layoutAlignMd', attributeWithObserve('layout-align-md')) + .directive('layoutAlignGtMd', attributeWithObserve('layout-align-gt-md')) + .directive('layoutAlignLg', attributeWithObserve('layout-align-lg')) + .directive('layoutAlignGtLg', attributeWithObserve('layout-align-gt-lg')) + + // Attribute directives with no value(s) + + .directive('hide', attributeWithoutValue('hide')) + .directive('hideSm', attributeWithoutValue('hide-sm')) + .directive('hideGtSm', attributeWithoutValue('hide-gt-sm')) + .directive('hideMd', attributeWithoutValue('hide-md')) + .directive('hideGtMd', attributeWithoutValue('hide-gt-md')) + .directive('hideLg', attributeWithoutValue('hide-lg')) + .directive('hideGtLg', attributeWithoutValue('hide-gt-lg')) + .directive('show', attributeWithoutValue('show')) + .directive('showSm', attributeWithoutValue('show-sm')) + .directive('showGtSm', attributeWithoutValue('show-gt-sm')) + .directive('showMd', attributeWithoutValue('show-md')) + .directive('showGtMd', attributeWithoutValue('show-gt-md')) + .directive('showLg', attributeWithoutValue('show-lg')) + .directive('showGtLg', attributeWithoutValue('show-gt-lg')) + + // Attribute directives with no value(s) and NO breakpoints + + .directive('layoutMargin', attributeWithoutValue('layout-margin')) + .directive('layoutPadding', attributeWithoutValue('layout-padding')) + .directive('layoutWrap', attributeWithoutValue('layout-wrap')) + .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('layout-offset-lt-md')) + .directive('offsetLtLg', warnAttrNotSupported('layout-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')); + + /** + * 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', '$$mdLayout', '$interpolate', function(_$mdUtil_, _$$mdLayout_, _$interpolate_) { + $mdUtil = _$mdUtil_; + $$mdLayout = _$$mdLayout_; + $interpolate = _$interpolate_; + + 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) + ); + + linkFn = translateWithValueToCssClass; + } - /** - * 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 - */ + // Use for postLink to account for transforms after ng-transclude. + return linkFn || angular.noop; + } + }; + }]; /** - * Creates a directive registration function where a possbile dynamic attribute value will - * be observed/watched. - * @param {string} className attribute name; eg `md-layout-gt-md` with value ="row" + * Add as transformed class selector(s), then + * remove the deprecated attribute selector */ - function attributeWithObserve(className) { - - return ['$mdUtil', '$$mdLayout', '$document', '$parse', '$interpolate', function(_$mdUtil_, _$$mdLayout_, $document, _$parse_, _$interpolate_) { - $mdUtil = _$mdUtil_; - $$mdLayout = _$$mdLayout_; - $parse = _$parse_; - $interpolate = _$interpolate_; + function translateWithValueToCssClass(scope, element, attrs) { + var updateFn = updateClassWithValue(element, className, attrs); + var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); - return { - restrict : 'A', - compile: function(element, attr) { - // Use for postLink to account for transforms after ng-transclude. + updateFn(getNormalizedAttrValue(className, attrs, "")); + scope.$on("$destroy", function() { unwatch() }); - if ( !injectLayoutSpecifier(element, attr) ) { - attributeValueToClass(null, element, attr); - return attributeValueToClass; - } + if ($$mdLayout.removeAttributes) element.removeAttr(className); + } + } + + /** + * 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 ['$$mdLayout', '$interpolate', function(_$$mdLayout_, _$interpolate_) { + $$mdLayout = _$$mdLayout_; + $interpolate = _$interpolate_; + + 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); - return angular.noop; - } - }; - }]; - - /** - * To avoid large sets of CSS rules - * for layout-gt-md-row, layout-sm-column, etc... - * - * Instead create either a md-layout-row or md-layout-column - * class that acts as a generic specifier. - * - */ - function injectLayoutSpecifier(element, attrs) { - var injected = false; - var breakpoints = ['','-sm','-gt-sm','-md','-gt-md','-lg','-gt-lg']; - angular.forEach(breakpoints, function(it){ - if ( className === "layout"+it ) { - - var updateClassFn = updateClassWithValue(element,"md-layout"+it, attrs); - var normalizedAttr = attrs.$normalize(className); - var attrValue = attrs[normalizedAttr] ? attrs[normalizedAttr].replace(/\s+/g, "-") : "row"; - var addImmediate = attrValue ? !needsInterpolation(attrValue) : false; - var watchValue = needsInterpolation(attrValue); - - - // Add special layout class: either '.md-layout-row' or '.md-layout-column' - if ( addImmediate ) element.addClass( $mdUtil.supplant('md-layout{0}-{1}',[it,attrValue]) ); - if ( watchValue ) attrs.$observe( normalizedAttr, updateClassFn ); - if ( $$mdLayout.removeAttributes ) element.removeAttr(className); - - injected = true; + // Use for postLink to account for transforms after ng-transclude. + linkFn = translateToCssClass; } - }); - - return injected; - } - - /** - * Add as transformed class selector(s), then - * remove the deprecated attribute selector - */ - function attributeValueToClass(scope, element, attrs) { - var updateClassFn = updateClassWithValue(element,className, attrs); - var normalizedAttr = attrs.$normalize(className); - var attrValue = attrs[normalizedAttr] ? attrs[normalizedAttr].replace(/\s+/g, "-") : null; - var addImmediate = attrValue ? !needsInterpolation(attrValue) : false; - var watchValue = needsInterpolation(attrValue); - - // Add transformed class selector(s) - - if ( addImmediate ) element.addClass(className + "-" + attrValue); - if ( watchValue ) attrs.$observe( normalizedAttr, updateClassFn ); - if ( !addImmediate && !watchValue ) element.addClass(className); - - if ( $$mdLayout.removeAttributes ) element.removeAttr(className); - } - - } - /** - * See if the original value has interpolation symbols: - * e.g. flex-gt-md="{{triggerPoint}}" - */ - function needsInterpolation(value) { - return (value ||"").indexOf($interpolate.startSymbol()) > -1; - } + return linkFn || angular.noop; + } + }; + }]; /** - * 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. - * + * Add as transformed class selector, then + * remove the deprecated attribute selector */ - function updateClassWithValue(element, className, attr) { - var lastClass; - - return function updateClassWithValue(newValue) { - var value = String(newValue || "").replace(/\s+/g, "-"); + function translateToCssClass(scope, element) { + element.addClass(className); + if ($$mdLayout.removeAttributes) { + // After link-phase, remove deprecated layout attribute selector + element.removeAttr(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) ) { element.removeClass(lastClass); - lastClass = !value ? className : className + "-" + value; + 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-` variant."); + return angular.noop; + }]; + } + + /** + * 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; - // Conditionally remove the attribute selector in case the browser attempts to - // read it and suffers a performance downgrade (IE). - - if ( $$mdLayout.removeAttributes ) element.removeAttr(className); - }; - } - - /** - * Creates a registration function with for ngMaterial Layout attribute directive. - * This is a `simple` transpose of attribute usage to class usage - */ - function attributeWithoutValue(className) { - return ['$$mdLayout', '$document', function(_$$mdLayout_, $document) { - $$mdLayout = _$$mdLayout_; - return { - restrict : 'A', - compile: function(element, attrs) { + case 'flex' : + if (!findIn(value, FLEX_OPTIONS)) { + if (isNaN(value)) { + value = ''; + } + } + break; - attributeToClass(null, element); + case 'layout-offset' : + case 'flex-order' : + if (!value || isNaN(+value)) { + value = '0'; + } + break; - // Use for postLink to account for transforms after ng-transclude. - return attributeToClass; + case 'layout-align' : + if (!findIn(value, ALIGNMENT_OPTIONS, "-")) { + value = ALIGNMENT_OPTIONS[0]; // 'start-start'; } - }; - }]; - - /** - * Add as transformed class selector, then - * remove the deprecated attribute selector - */ - function attributeToClass(scope, element) { - element.addClass(className); - - if ( $$mdLayout.removeAttributes ) { - // After link-phase, remove deprecated layout attribute selector - element.removeAttr(className); - } + break; + + case 'layout-padding' : + case 'layout-margin' : + case 'layout-fill' : + case 'layout-wrap' : + case 'layout-no-wrap' : + value = ''; + break; } - } - - /** - * 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-` variant."); - return angular.noop; - }]; + 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)) { + element.attr(className, fallback); + 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; + } })(); diff --git a/src/core/services/layout/layout.scss b/src/core/services/layout/layout.scss index 5d8f6f4bee2..037c612234a 100644 --- a/src/core/services/layout/layout.scss +++ b/src/core/services/layout/layout.scss @@ -11,7 +11,7 @@ */ @-moz-document url-prefix() { - .layout-fill, [layout-fill] { + .layout-fill { margin: 0; width: 100%; min-height: 100%; @@ -31,7 +31,7 @@ // } // - .flex-order, [flex-order] { + .flex-order { order : 0; } } @@ -44,11 +44,7 @@ @if $s != '' { $suffix : '#{$s}-#{$i}'; } @else { $suffix : '#{$i}'; } - $order : $order + '.flex-order-#{$suffix}, '; - } - @each $s in $sizes { - @if ( $s != '' ) { $order : $order + '[flex-order-#{$s}="#{$i}"], '; } - @else { $order : $order + '[flex-order="#{$i}"], '; } + $order : $order + '.flex-order-#{$suffix}'; } // .flex-order-0, [order="0"] { @@ -91,11 +87,7 @@ @if $s != '' { $suffix : '#{$s}-#{$i * 5}'; } @else { $suffix : '#{$i * 5}'; } - $offsets : $offsets + '.offset-#{$suffix}, '; - } - @each $s in $sizes { - @if ( $s != '' ) { $offsets : $offsets + '[offset-#{$s}="#{$i * 5}"], '; } - @else { $offsets : $offsets + '[offset="#{$i * 5}"], '; } + $offsets : $offsets + '.offset-#{$suffix}'; } #{$offsets} { @@ -111,12 +103,7 @@ @if $s != '' { $suffix : '#{$s}-#{$i}'; } @else { $suffix : '#{$i}'; } - $offsets : $offsets + '.offset-#{$suffix}, '; - } - - // add attribute selectors - @each $s in $sizes { - $offsets : $offsets + '[offset-#{$s}="#{$i}"], '; + $offsets : $offsets + '.offset-#{$suffix}'; } } @@ -136,12 +123,7 @@ @if $s != '' { $suffix : '#{$s}-#{$i}'; } @else { $suffix : '#{$i}'; } - $offsets : $offsets + '.offset-#{$suffix}, '; - } - - // add attribute selectors - @each $s in $sizes { - $offsets : $offsets + '[offset-#{$s}="#{$i}"], '; + $offsets : $offsets + '.offset-#{$suffix}'; } } @@ -158,7 +140,7 @@ @if $name == null { $name : ''; } @if $name != '' { $name : '-#{$name}'; } - .md-layout#{$name}, .md-layout#{$name}-column, .md-layout#{$name}-row, { + .layout#{$name}, .layout#{$name}-column, .layout#{$name}-row, { box-sizing: border-box; display: -webkit-box; display: -webkit-flex; @@ -166,8 +148,8 @@ display: -ms-flexbox; display: flex; } - .md-layout#{$name}-column { flex-direction: column; } - .md-layout#{$name}-row { flex-direction: row; } + .layout#{$name}-column { flex-direction: column; } + .layout#{$name}-row { flex-direction: row; } } @mixin flex-properties-for-name($name: null) { $flexName: 'flex'; @@ -178,36 +160,51 @@ $name : ''; } - .#{$flexName} { - box-sizing: border-box; - } - - .#{$flexName} { flex: 1; } // === 1 1 0% - .#{$flexName}-grow { flex: 1 1 100%; } - .#{$flexName}-initial { flex: 0 1 auto; } - .#{$flexName}-auto { flex: 1 1 auto; } - .#{$flexName}-none { flex: 0 0 auto; } + .#{$flexName} { flex: 1; box-sizing: border-box; } // === flex: 1 1 0%; + .#{$flexName}-grow { flex: 1 1 100%; box-sizing: border-box; } + .#{$flexName}-initial { flex: 0 1 auto; box-sizing: border-box; } + .#{$flexName}-auto { flex: 1 1 auto; box-sizing: border-box; } + .#{$flexName}-none { flex: 0 0 auto; box-sizing: border-box; } // (1-20) * 5 = 0-100% @for $i from 0 through 20 { $value : #{$i * 5 + '%'}; - .#{$flexName}-#{$i * 5} { box-sizing: border-box; flex: 0 0 #{$value}; } + .#{$flexName}-#{$i * 5} { + flex: 0 0 #{$value}; + max-width: #{$value}; + max-height: 100%; + box-sizing: border-box; + } - .md-layout-row > .#{$flexName}-#{$i * 5}, - .md-layout#{$name}-row > .#{$flexName}-#{$i * 5} { flex: 0 0 #{$value}; max-width: #{$value}; max-height: 100%; } + .layout-row > .#{$flexName}-#{$i * 5}, + .layout#{$name}-row > .#{$flexName}-#{$i * 5} { + flex: 0 0 #{$value}; + max-width: #{$value}; + max-height: 100%; + box-sizing: border-box; + } - .md-layout-column > .#{$flexName}-#{$i * 5}, - .md-layout#{$name}-column > .#{$flexName}-#{$i * 5} { flex: 0 0 #{$value}; max-width: 100%; max-height: #{$value}; } + .layout-column > .#{$flexName}-#{$i * 5}, + .layout#{$name}-column > .#{$flexName}-#{$i * 5} { + flex: 0 0 #{$value}; + max-width: 100%; + max-height: #{$value}; + box-sizing: border-box; + } } - .md-layout-row, .md-layout#{$name}-row { - > .#{$flexName}-33 , > .#{$flexName}-34 { flex: 0 0 33%; max-width: 33%; max-height: 100%; } - > .#{$flexName}-66 , > .#{$flexName}-67 { flex: 0 0 67%; max-width: 67%; max-height: 100%; } + .layout-row, .layout#{$name}-row { + > .#{$flexName}-33 , > .#{$flexName}-33 { flex: 0 0 33%; max-width: 33%; max-height: 100%; box-sizing: border-box; } + > .#{$flexName}-34 , > .#{$flexName}-34 { flex: 0 0 34%; max-width: 34%; max-height: 100%; box-sizing: border-box; } + > .#{$flexName}-66 , > .#{$flexName}-66 { flex: 0 0 66%; max-width: 66%; max-height: 100%; box-sizing: border-box; } + > .#{$flexName}-67 , > .#{$flexName}-67 { flex: 0 0 67%; max-width: 67%; max-height: 100%; box-sizing: border-box; } } - .md-layout-column, .md-layout#{$name}-column { - > .#{$flexName}-33 , > .#{$flexName}-34 { flex: 0 0 33%; max-width: 100%; max-height: 33%; } - > .#{$flexName}-66 , > .#{$flexName}-67 { flex: 0 0 67%; max-width: 100%; max-height: 67%; } + .layout-column, .layout#{$name}-column { + > .#{$flexName}-33 , > .#{$flexName}-33 { flex: 0 0 33%; max-width: 100%; max-height: 33%; box-sizing: border-box; } + > .#{$flexName}-34 , > .#{$flexName}-34 { flex: 0 0 34%; max-width: 100%; max-height: 34%; box-sizing: border-box; } + > .#{$flexName}-66 , > .#{$flexName}-66 { flex: 0 0 66%; max-width: 100%; max-height: 66%; box-sizing: border-box; } + > .#{$flexName}-67 , > .#{$flexName}-67 { flex: 0 0 67%; max-width: 100%; max-height: 67%; box-sizing: border-box; } } } @@ -338,15 +335,15 @@ margin: $layout-gutter-width / 1; } - .layout-wrap, [layout-wrap] { + .layout-wrap { flex-wrap: wrap; } - .layout-nowrap, [layout-nowrap] { + .layout-nowrap { flex-wrap: nowrap; } - .layout-fill, [layout-fill] { + .layout-fill { margin: 0; width: 100%; min-height: 100%; diff --git a/src/core/services/layout/layout.spec.js b/src/core/services/layout/layout.spec.js index 5cfdbf136f7..105172eb48a 100644 --- a/src/core/services/layout/layout.spec.js +++ b/src/core/services/layout/layout.spec.js @@ -1,125 +1,94 @@ describe('layout directives', function() { + var suffixes = ['sm', 'gt-sm', 'md', 'gt-md', 'lg', 'gt-lg'], + $mdUtil, $compile, pageScope; + beforeEach(module('material.core', 'material.core.layout')); - describe('translated to layout classes', function() { + beforeEach(inject(function(_$compile_, _$rootScope_, _$mdUtil_) { + $mdUtil = _$mdUtil_; + $compile = _$compile_; + pageScope = _$rootScope_.$new(); + })); - var suffixes = ['sm', 'gt-sm', 'md', 'gt-md', 'lg', 'gt-lg']; - var directionValues = ['row', 'column']; - var flexOrderValues = [-9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - var flexValues = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 33, 34, 66, 67]; - var offsetValues = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 33, 34, 66, 67]; - var alignmentValues = [ - "center", "center center", "center start", "center end", - "end", "end center", "end start", "end end", - "space-around", "space-around center", "space-around start", "space-around end", - "space-between", "space-between center", "space-between start", "space-between end", - "start center", "start start", "start end"]; - var mappings = [ - { attribute: 'flex', suffixes: suffixes, values: flexValues, testStandAlone: true}, - { attribute: 'flex-order', suffixes: suffixes, values: flexOrderValues }, - { attribute: 'offset', suffixes: suffixes, values: offsetValues }, - { attribute: 'hide', suffixes: suffixes, testStandAlone: true }, - { attribute: 'show', suffixes: suffixes, testStandAlone: true }, - { attribute: 'layout-align', suffixes: suffixes, values: alignmentValues }, - { attribute: 'layout-padding', testStandAlone: true }, - { attribute: 'layout-margin', testStandAlone: true }, - { attribute: 'layout-wrap', testStandAlone: true }, - { attribute: 'layout-fill', testStandAlone: true } - ]; + describe('using [layout] attributes', function() { - // Run all the tests; iterating the mappings... - testWithSuffix('layout', suffixes, directionValues); + it("should support attribute without value '
'", function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("layout")).toBeFalsy(); + expect(element.hasClass("layout-row")).toBeTruthy(); + }); - for (var i = 0; i < mappings.length; i++) { - var map = mappings[i]; + it('should ignore invalid values', function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("layout-row")).toBeTruthy(); + expect(element.hasClass('layout-humpty')).toBeFalsy(); + }); - if (map.testStandAlone) testSimpleDirective(map.attribute); - if (map.values) testWithSuffixAndValue(map.attribute, map.values, undefined ); - if (map.suffixes) testWithSuffix(map.attribute, map.suffixes, map.values, map.testStandAlone ); - } + it('should support interpolated values layout-gt-sm="{{direction}}"', function() { + var element = $compile('
Layout
')(pageScope); + pageScope.$apply('direction = "row"'); + expect(element.hasClass('layout-gt-sm-row')).toBeTruthy(); - /** Test a simple layout directive to validate that the layout class is added. */ - function testSimpleDirective(attribute, expectedClass) { - // default fallback is attribute as class... - expectedClass = expectedClass || attribute; + pageScope.$apply('direction = undefined'); + expect(element.hasClass('layout-gt-sm-row')).toBeTruthy(); - it('should fail if the class ' + expectedClass + ' was not added for attribute ' + attribute, inject(function($compile, $rootScope) { - var element = $compile('
Layout
')($rootScope.$new()); - expect(element.hasClass(expectedClass)).toBe(true); - })); - } + pageScope.$apply('direction = "column"'); + expect(element.hasClass('layout-gt-sm-column')).toBeTruthy(); + }); - /** Test directives with 'sm', 'gt-sm', 'md', 'gt-md', 'lg', and 'gt-lg' suffixes */ - function testWithSuffixAndValue(attribute, values, suffix) { - for (var j = 0; j < values.length; j++) { - var value = values[j].toString(); - var attr = suffix ? attribute + '-' + suffix : attribute; + /** + * For all breakpoints, + * - Test percentage values + * - Test valid non-numerics + * + * NOTE: include the '' suffix: layout='' === layout-row + */ + var directionValues = ['row', 'column']; - var attrWithValue = buildAttributeWithValue(attr, value); - var expectedClass = buildExpectedClass(attr, value); + angular.forEach(directionValues, function(direction) { + angular.forEach([''].concat(suffixes), function(suffix) { + var className = suffix ? 'layout-' + suffix : 'layout'; + testWithValue(className, direction); + }); + }); - // Run each test. - testSimpleDirective(attrWithValue, expectedClass); - } + }); - /** - * Build string of expected classes that should be added to the DOM element. - * - * Convert directive with value to classes - * - * @param {string} attrClass Full attribute name; eg 'layout-gt-lg' - * @param {string} attrValue HTML directive; eg "column" - * - * @returns {string} Class name(s) to be added; e.g., `layout-gt-lg-column`. - */ - function buildExpectedClass(attrClass, attrValue) { - - // Layout attributes have special md-layout prefix class names - angular.forEach([''].concat(suffixes), function(it){ - var layout = (it ? "layout-" : "layout") + it; - if (attrClass == layout) attrClass = "md-" + attrClass; - }); - - return attrClass + "-" + attrValue.replace(/\s+/g, "-"); - } + describe('using [flex] attributes', function() { + var allowedValues = [ + 'grow', 'initial', 'auto', 'none', + 0, 5, 10, 15, 20, 25, + 30, 33, 34, 35, 40, 45, + 50, 55, 60, 65, 66, 67, + 70, 75, 80, 85, 90, 95, 100 + ]; - /** - * Build full string of expected directive with its value - * Note: The expected class always starts with the - * attribute name, add the suffix if any. - * - * @param {string} attrClass Full attribute name; eg 'layout-gt-lg' - * @param {string} attrValue HTML directive; eg "column" - * - * @returns {string} Attribute with value, e.g., `layout-gt-lg="column"` - */ - function buildAttributeWithValue(attrClass, attrValue) { - return attrClass + '="' + attrValue + '"'; - } - } + it('should support attribute without value "
"', function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("flex")).toBeTruthy(); + expect(element.hasClass("flex-flex")).toBeFalsy(); + }); - /** - * Test directive as simple with media suffix and with associated values. - * E.g., layout-gt-md="row" - */ - function testWithSuffix(attribute, suffixes, values, testStandAlone) { - for (var j = 0; j < suffixes.length; j++) { - var suffix = suffixes[j]; - var attr = attribute + '-' + suffix; + it('should ignore invalid values non-numericals like flex="flex"', function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("flex")).toBeTruthy(); + expect(element.hasClass('flex-flex')).toBeFalsy(); + }); - if (testStandAlone) testSimpleDirective(attr); - if (values) testWithSuffixAndValue(attribute, values, suffix); - } - } - }); + it('should support interpolated values flex-gt-sm="{{columnSize}}"', function() { + var scope = pageScope, + element = $compile('
Layout
')(scope); - describe('layout attribute with dynamic values', function() { + scope.$apply('columnSize = 33'); + expect(element.hasClass('flex-gt-sm-33')).toBeTruthy(); - it('should observe the attribute value and update the layout class(es)', inject(function($rootScope, $compile) { - var scope = $rootScope.$new(); - scope.size = undefined; + scope.$apply('columnSize = undefined'); + expect(element.hasClass('flex-gt-sm')).toBeTruthy(); + }); + it('should observe the attribute value and update the layout class(es)', inject(function($rootScope, $compile) { + var scope = pageScope; var element = angular.element($compile('
')(scope)); expect(element.hasClass('flex-gt-md')).toBe(true); @@ -132,13 +101,230 @@ describe('layout directives', function() { expect(element.hasClass('flex-gt-md-32')).toBe(true); scope.$apply(function() { + // This should be rejected/ignored and the fallback "" value used scope.size = "fishCheeks"; }); - expect(element.hasClass('flex-gt-md-32')).toBe(false); - expect(element.hasClass('flex-gt-md-fishCheeks')).toBe(true); - + expect(element.hasClass('flex-gt-md')).toBe(true); + expect(element.hasClass('flex-gt-md-fishCheeks')).toBe(false); })); - }) + testAllSuffixesWithValues("flex", allowedValues); + }); + + describe('using [flex-order] attributes', function() { + var flexOrderValues = [ + -9, -8, -7, -6, -5, -4, -3, -2, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + ]; + + it('should support attribute without value "
"', function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("flex-order-0")).toBeTruthy(); + expect(element.hasClass("flex-order")).toBeFalsy(); + }); + + it('should ignore invalid values non-numericals like flex-order="humpty"', function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("flex-order-0")).toBeTruthy(); + expect(element.hasClass('flex-order-humpty')).toBeFalsy(); + }); + + it('should support interpolated values flex-order-gt-sm="{{index}}"', function() { + var scope = pageScope, + element = $compile('
Layout
')(scope); + + scope.$apply('index = 3'); + expect(element.hasClass('flex-order-gt-sm-3')).toBeTruthy(); + }); + + testAllSuffixesWithValues("flex-order", flexOrderValues); + }); + + describe('using [layout-offset] attributes', function() { + var offsetValues = [ + 5, 10, 15, 20, 25, + 30, 35, 40, 45, 50, + 55, 60, 65, 70, 75, + 80, 85, 90, 95, + 33, 34, 66, 67 + ]; + + it('should support attribute without value "
"', function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("layout-offset-0")).toBeTruthy(); + expect(element.hasClass("layout-offset")).toBeFalsy(); + }); + + it('should ignore invalid values non-numericals like layout-offset="humpty"', function() { + var element = $compile('
Layout
')(pageScope); + expect(element.hasClass("layout-offset-0")).toBeTruthy(); + expect(element.hasClass('layout-offset-humpty')).toBeFalsy(); + }); + + it('should support interpolated values layout-offset-gt-sm="{{padding}}"', function() { + var scope = pageScope, + element = $compile('
Layout
')(scope); + + scope.$apply('padding = 15'); + expect(element.hasClass('layout-offset-gt-sm-15')).toBeTruthy(); + }); + + testAllSuffixesWithValues("layout-offset", offsetValues); + }); + + describe('using [layout-align] attributes', function() { + var attrName = "layout-align"; + var alignmentValues = [ + "center", "center center", "center start", "center end", + "end", "end center", "end start", "end end", + "space-around", "space-around center", "space-around start", "space-around end", + "space-between", "space-between center", "space-between start", "space-between end", + "start center", "start start", "start end" + ]; + + it('should support attribute without value "
"', function() { + var markup = $mdUtil.supplant('
Layout
', [attrName]); + var element = $compile(markup)(pageScope); + + expect(element.hasClass(attrName + "-start-start")).toBeTruthy(); + expect(element.hasClass(attrName)).toBeFalsy(); + }); + + it('should ignore invalid values non-numericals like layout-align="humpty"', function() { + var markup = $mdUtil.supplant('
Layout
', [attrName]); + var element = $compile(markup)(pageScope); + + expect(element.hasClass(attrName + "-start-start")).toBeTruthy(); + expect(element.hasClass(attrName + '-humpty')).toBeFalsy(); + }); + + it('should support interpolated values layout-align-gt-sm="{{alignItems}}"', function() { + var scope = pageScope, + markup = $mdUtil.supplant('
Layout
', [attrName]), + element = $compile(markup)(scope); + + scope.$apply('alignItems = "center center"'); + expect(element.hasClass(attrName + '-gt-sm-center-center')).toBeTruthy(); + }); + + testAllSuffixesWithValues(attrName, alignmentValues); + }); + + describe('using [layout-] padding, fill, margin, wrap, and nowrap attributes', function() { + var allowedAttrsNoValues = [ + "layout-padding", + "layout-margin", + "layout-fill", + "layout-wrap", + "layout-no-wrap" + ]; + + angular.forEach(allowedAttrsNoValues, function(name) { + testNoValueAllowed(name); + }) + }); + + describe('using [hide] attributes', function() { + var attrName = "hide", + breakpoints = [''].concat(suffixes); + + angular.forEach(breakpoints, function(suffix) { + var className = suffix ? attrName + "-" + suffix : attrName; + testNoValueAllowed(className); + }); + + }); + + describe('using [show] attributes', function() { + var attrName = "show", + breakpoints = [''].concat(suffixes); + + angular.forEach(breakpoints, function(suffix) { + var className = suffix ? attrName + "-" + suffix : attrName; + testNoValueAllowed(className); + }); + + }); + + // ***************************************************************** + // Internal Test methods for the angular.forEach( ) loops + // ***************************************************************** + + /** + * For the specified attrName (e.g. flex) test all breakpoints + * with all allowed values. + */ + function testAllSuffixesWithValues(attrName, allowedValues) { + var breakpoints = [''].concat(suffixes); + + angular.forEach(breakpoints, function(suffix) { + angular.forEach(allowedValues, function(value) { + var className = suffix ? attrName + "-" + suffix : attrName; + testWithValue(className, value, attrName); + }); + }); + } + + /** + * Test other Layout directives (e.g. flex, flex-order, layout-offset) + */ + function testWithValue(className, value, raw) { + var title = 'should allow valid values `' + className + '=' + value + '`'; + + it(title, function() { + + var expected = $mdUtil.supplant('{0}-{1}', [className, value ? String(value).replace(/\s+/g, "-") : value]); + var markup = $mdUtil.supplant('
Layout
', [className, value]); + + var element = $compile(markup)(pageScope); + expect(element.hasClass(expected)).toBeTruthy(); + + if (raw) { + // Is the raw value also present? + expect(element.hasClass(raw)).toBeFalsy(); + } + + }); + } + + /** + * Layout directives do NOT support values nor breakpoint usages: + * + * - layout-margin, + * - layout-padding, + * - layout-fill, + * - layout-wrap, + * - layout-nowrap + * + */ + function testNoValueAllowed(attrName) { + + it('should support attribute without value "
"', function() { + var markup = $mdUtil.supplant('
Layout
', [attrName]); + var element = $compile(markup)(pageScope); + + expect(element.hasClass(attrName)).toBeTruthy(); + }); + + it('should ignore invalid values non-numericals like ' + attrName + '="humpty"', function() { + var markup = $mdUtil.supplant('
Layout
', [attrName]); + var element = $compile(markup)(pageScope); + + expect(element.hasClass(attrName)).toBeTruthy(); + expect(element.hasClass(attrName + '-humpty')).toBeFalsy(); + }); + + it('should ignore interpolated values ' + attrName + '="{{someVal}}"', function() { + var markup = $mdUtil.supplant('
Layout
', [attrName]), + element = $compile(markup)(pageScope); + + pageScope.$apply('someVal = "30"'); + + expect(element.hasClass(attrName)).toBeTruthy(); + expect(element.hasClass($mdUtil.supplant("{0}-30", [attrName]))).toBeFalsy(); + + }); + } + });