diff options
| author | Matias Niemelä | 2013-11-18 22:13:28 -0500 | 
|---|---|---|
| committer | Matias Niemelä | 2013-11-21 20:47:44 -0500 | 
| commit | 0cd7e8f22721f62b62440bb059ae764ebbe7b42a (patch) | |
| tree | 6eeb56d123d2bc4c2417b80f1f64079eb2b97059 /src/ng/compile.js | |
| parent | ba1b47f85b771f8221db58a46b58429375b0ee6e (diff) | |
| download | angular.js-0cd7e8f22721f62b62440bb059ae764ebbe7b42a.tar.bz2 | |
fix($compile): ensure CSS classes are added and removed only when necessary
When $compile interpolates a CSS class attribute expression it will
do so by comparing the CSS class value already present on the element.
This may lead to unexpected results when dealing with ngClass values being
added and removed therefore it is best that both compile and ngClass delegate
addClass/removeClass operations to the same block of code.
Diffstat (limited to 'src/ng/compile.js')
| -rw-r--r-- | src/ng/compile.js | 132 | 
1 files changed, 85 insertions, 47 deletions
| diff --git a/src/ng/compile.js b/src/ng/compile.js index ce3d0514..d977f173 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -673,6 +673,24 @@ function $CompileProvider($provide) {        },        /** +       * @ngdoc function +       * @name ng.$compile.directive.Attributes#$updateClass +       * @methodOf ng.$compile.directive.Attributes +       * @function +       * +       * @description +       * Adds and removes the appropriate CSS class values to the element based on the difference +       * between the new and old CSS class values (specified as newClasses and oldClasses). +       * +       * @param {string} newClasses The current CSS className value +       * @param {string} oldClasses The former CSS className value +       */ +      $updateClass : function(newClasses, oldClasses) { +        this.$removeClass(tokenDifference(oldClasses, newClasses)); +        this.$addClass(tokenDifference(newClasses, oldClasses)); +      }, + +      /**         * Set a normalized attribute on the element in a way such that all directives         * can share the attribute. This function properly handles boolean attributes.         * @param {string} key Normalized key. (ie ngAttribute) @@ -682,59 +700,53 @@ function $CompileProvider($provide) {         * @param {string=} attrName Optional none normalized name. Defaults to key.         */        $set: function(key, value, writeAttr, attrName) { -        //special case for class attribute addition + removal -        //so that class changes can tap into the animation -        //hooks provided by the $animate service -        if(key == 'class') { -          value = value || ''; -          var current = this.$$element.attr('class') || ''; -          this.$removeClass(tokenDifference(current, value)); -          this.$addClass(tokenDifference(value, current)); -        } else { -          var booleanKey = getBooleanAttrName(this.$$element[0], key), -              normalizedVal, -              nodeName; +        // TODO: decide whether or not to throw an error if "class" +        //is set through this function since it may cause $updateClass to +        //become unstable. -          if (booleanKey) { -            this.$$element.prop(key, value); -            attrName = booleanKey; -          } +        var booleanKey = getBooleanAttrName(this.$$element[0], key), +            normalizedVal, +            nodeName; -          this[key] = value; +        if (booleanKey) { +          this.$$element.prop(key, value); +          attrName = booleanKey; +        } -          // translate normalized key to actual key -          if (attrName) { -            this.$attr[key] = attrName; -          } else { -            attrName = this.$attr[key]; -            if (!attrName) { -              this.$attr[key] = attrName = snake_case(key, '-'); -            } +        this[key] = value; + +        // translate normalized key to actual key +        if (attrName) { +          this.$attr[key] = attrName; +        } else { +          attrName = this.$attr[key]; +          if (!attrName) { +            this.$attr[key] = attrName = snake_case(key, '-');            } +        } -          nodeName = nodeName_(this.$$element); - -          // sanitize a[href] and img[src] values -          if ((nodeName === 'A' && key === 'href') || -              (nodeName === 'IMG' && key === 'src')) { -            // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. -            if (!msie || msie >= 8 ) { -              normalizedVal = urlResolve(value).href; -              if (normalizedVal !== '') { -                if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || -                    (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { -                  this[key] = value = 'unsafe:' + normalizedVal; -                } +        nodeName = nodeName_(this.$$element); + +        // sanitize a[href] and img[src] values +        if ((nodeName === 'A' && key === 'href') || +            (nodeName === 'IMG' && key === 'src')) { +          // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. +          if (!msie || msie >= 8 ) { +            normalizedVal = urlResolve(value).href; +            if (normalizedVal !== '') { +              if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || +                  (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { +                this[key] = value = 'unsafe:' + normalizedVal;                }              }            } +        } -          if (writeAttr !== false) { -            if (value === null || value === undefined) { -              this.$$element.removeAttr(attrName); -            } else { -              this.$$element.attr(attrName, value); -            } +        if (writeAttr !== false) { +          if (value === null || value === undefined) { +            this.$$element.removeAttr(attrName); +          } else { +            this.$$element.attr(attrName, value);            }          } @@ -1816,9 +1828,19 @@ function $CompileProvider($provide) {                  attr[name] = interpolateFn(scope);                  ($$observers[name] || ($$observers[name] = [])).$$inter = true;                  (attr.$$observers && attr.$$observers[name].$$scope || scope). -                    $watch(interpolateFn, function interpolateFnWatchAction(value) { -                      attr.$set(name, value); -                    }); +                  $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { +                    //special case for class attribute addition + removal +                    //so that class changes can tap into the animation +                    //hooks provided by the $animate service. Be sure to +                    //skip animations when the first digest occurs (when +                    //both the new and the old values are the same) since +                    //the CSS classes are the non-interpolated values +                    if(name === 'class' && newValue != oldValue) { +                      attr.$updateClass(newValue, oldValue); +                    } else { +                      attr.$set(name, newValue); +                    } +                  });                }              };            } @@ -1958,3 +1980,19 @@ function directiveLinkingFn(    /* Element */ rootElement,    /* function(Function) */ boundTranscludeFn  ){} + +function tokenDifference(str1, str2) { +  var values = '', +      tokens1 = str1.split(/\s+/), +      tokens2 = str2.split(/\s+/); + +  outer: +  for(var i = 0; i < tokens1.length; i++) { +    var token = tokens1[i]; +    for(var j = 0; j < tokens2.length; j++) { +      if(token == tokens2[j]) continue outer; +    } +    values += (values.length > 0 ? ' ' : '') + token; +  } +  return values; +} | 
