Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

ngClass[Odd/Even] overhaul (2) #15246

Closed
wants to merge 9 commits into from
215 changes: 132 additions & 83 deletions src/ng/directive/ngClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,129 +8,178 @@

function classDirective(name, selector) {
name = 'ngClass' + name;
return ['$animate', function($animate) {
var indexWatchExpression;

return ['$parse', function($parse) {
return {
restrict: 'AC',
link: function(scope, element, attr) {
var oldVal;
var expression = attr[name].trim();
var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':');

scope.$watch(attr[name], ngClassWatchAction, true);
var watchInterceptor = isOneTime ? toFlatValue : toClassString;
var watchExpression = $parse(expression, watchInterceptor);
var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction;

attr.$observe('class', function(value) {
ngClassWatchAction(scope.$eval(attr[name]));
});
var classCounts = element.data('$classCounts');
var oldModulo = true;
var oldClassString;

if (!classCounts) {
// Use createMap() to prevent class assumptions involving property
// names in Object.prototype
classCounts = createMap();
element.data('$classCounts', classCounts);
}

if (name !== 'ngClass') {
scope.$watch('$index', function($index, old$index) {
/* eslint-disable no-bitwise */
var mod = $index & 1;
if (mod !== (old$index & 1)) {
var classes = arrayClasses(scope.$eval(attr[name]));
if (mod === selector) {
addClasses(classes);
} else {
removeClasses(classes);
}
}
/* eslint-enable */
});
if (!indexWatchExpression) {
indexWatchExpression = $parse('$index', function moduloTwo($index) {
// eslint-disable-next-line no-bitwise
return $index & 1;
});
}

scope.$watch(indexWatchExpression, ngClassIndexWatchAction);
}

function addClasses(classes) {
var newClasses = digestClassCounts(classes, 1);
attr.$addClass(newClasses);
scope.$watch(watchExpression, watchAction, isOneTime);

function addClasses(classString) {
classString = digestClassCounts(split(classString), 1);
attr.$addClass(classString);
}

function removeClasses(classes) {
var newClasses = digestClassCounts(classes, -1);
attr.$removeClass(newClasses);
function removeClasses(classString) {
classString = digestClassCounts(split(classString), -1);
attr.$removeClass(classString);
}

function digestClassCounts(classes, count) {
// Use createMap() to prevent class assumptions involving property
// names in Object.prototype
var classCounts = element.data('$classCounts') || createMap();
function updateClasses(oldClassString, newClassString) {
var oldClassArray = split(oldClassString);
var newClassArray = split(newClassString);

var toRemoveArray = arrayDifference(oldClassArray, newClassArray);
var toAddArray = arrayDifference(newClassArray, oldClassArray);

var toRemoveString = digestClassCounts(toRemoveArray, -1);
var toAddString = digestClassCounts(toAddArray, 1);

attr.$addClass(toAddString);
attr.$removeClass(toRemoveString);
}

function digestClassCounts(classArray, count) {
var classesToUpdate = [];
forEach(classes, function(className) {

forEach(classArray, function(className) {
if (count > 0 || classCounts[className]) {
classCounts[className] = (classCounts[className] || 0) + count;
if (classCounts[className] === +(count > 0)) {
classesToUpdate.push(className);
}
}
});
element.data('$classCounts', classCounts);

return classesToUpdate.join(' ');
}

function updateClasses(oldClasses, newClasses) {
var toAdd = arrayDifference(newClasses, oldClasses);
var toRemove = arrayDifference(oldClasses, newClasses);
toAdd = digestClassCounts(toAdd, 1);
toRemove = digestClassCounts(toRemove, -1);
if (toAdd && toAdd.length) {
$animate.addClass(element, toAdd);
}
if (toRemove && toRemove.length) {
$animate.removeClass(element, toRemove);
function ngClassIndexWatchAction(newModulo) {
// This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it
// adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the
// `ngClass[OneTime]WatchAction()` will update the classes.
if (newModulo === selector) {
addClasses(oldClassString);
} else {
removeClasses(oldClassString);
}

oldModulo = newModulo;
}

function ngClassWatchAction(newVal) {
// eslint-disable-next-line no-bitwise
if (selector === true || (scope.$index & 1) === selector) {
var newClasses = arrayClasses(newVal || []);
if (!oldVal) {
addClasses(newClasses);
} else if (!equals(newVal,oldVal)) {
var oldClasses = arrayClasses(oldVal);
updateClasses(oldClasses, newClasses);
}
function ngClassOneTimeWatchAction(newClassValue) {
var newClassString = toClassString(newClassValue);

if (newClassString !== oldClassString) {
ngClassWatchAction(newClassString);
}
if (isArray(newVal)) {
oldVal = newVal.map(function(v) { return shallowCopy(v); });
} else {
oldVal = shallowCopy(newVal);
}

function ngClassWatchAction(newClassString) {
if (oldModulo === selector) {
updateClasses(oldClassString, newClassString);
}

oldClassString = newClassString;
}
}
};
}];

function arrayDifference(tokens1, tokens2) {
var values = [];
// Helpers
function arrayDifference(tokens1, tokens2) {
if (!tokens1 || !tokens1.length) return [];
if (!tokens2 || !tokens2.length) return tokens1;

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.push(token);
var values = [];

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;
}
return values;
values.push(token);
}

function arrayClasses(classVal) {
var classes = [];
if (isArray(classVal)) {
forEach(classVal, function(v) {
classes = classes.concat(arrayClasses(v));
});
return classes;
} else if (isString(classVal)) {
return classVal.split(' ');
} else if (isObject(classVal)) {
forEach(classVal, function(v, k) {
if (v) {
classes = classes.concat(k.split(' '));
}
});
return classes;
return values;
}

function split(classString) {
return classString && classString.split(' ');
}

function toClassString(classValue) {
var classString = classValue;

if (isArray(classValue)) {
classString = classValue.map(toClassString).join(' ');
} else if (isObject(classValue)) {
classString = Object.keys(classValue).
filter(function(key) { return classValue[key]; }).
join(' ');
}

return classString;
}

function toFlatValue(classValue) {
var flatValue = classValue;

if (isArray(classValue)) {
flatValue = classValue.map(toFlatValue);
} else if (isObject(classValue)) {
var hasUndefined = false;

flatValue = Object.keys(classValue).filter(function(key) {
var value = classValue[key];

if (!hasUndefined && isUndefined(value)) {
hasUndefined = true;
}

return value;
});

if (hasUndefined) {
// Prevent the `oneTimeLiteralWatchInterceptor` from unregistering
// the watcher, by including at least one `undefined` value.
flatValue.push(undefined);
}
return classVal;
}
}];

return flatValue;
}
}

/**
Expand Down
Loading