|
8 | 8 |
|
9 | 9 | function classDirective(name, selector) {
|
10 | 10 | name = 'ngClass' + name;
|
| 11 | + var indexWatchExpression; |
11 | 12 |
|
12 |
| - return [function() { |
| 13 | + return ['$parse', function($parse) { |
13 | 14 | return {
|
14 | 15 | restrict: 'AC',
|
15 | 16 | link: function(scope, element, attr) {
|
16 |
| - var oldVal; |
| 17 | + var expression = attr[name].trim(); |
| 18 | + var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':'); |
| 19 | + |
| 20 | + var watchInterceptor = isOneTime ? toFlatValue : toClassString; |
| 21 | + var watchExpression = $parse(expression, watchInterceptor); |
| 22 | + var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction; |
| 23 | + |
| 24 | + var classCounts = element.data('$classCounts'); |
| 25 | + var oldModulo = true; |
| 26 | + var oldClassString; |
| 27 | + |
| 28 | + if (!classCounts) { |
| 29 | + // Use createMap() to prevent class assumptions involving property |
| 30 | + // names in Object.prototype |
| 31 | + classCounts = createMap(); |
| 32 | + element.data('$classCounts', classCounts); |
| 33 | + } |
17 | 34 |
|
18 | 35 | if (name !== 'ngClass') {
|
19 |
| - scope.$watch('$index', function($index, old$index) { |
20 |
| - /* eslint-disable no-bitwise */ |
21 |
| - var mod = $index & 1; |
22 |
| - if (mod !== (old$index & 1)) { |
23 |
| - var classes = arrayClasses(oldVal); |
24 |
| - if (mod === selector) { |
25 |
| - addClasses(classes); |
26 |
| - } else { |
27 |
| - removeClasses(classes); |
28 |
| - } |
29 |
| - } |
30 |
| - /* eslint-enable */ |
31 |
| - }); |
| 36 | + if (!indexWatchExpression) { |
| 37 | + indexWatchExpression = $parse('$index', function moduloTwo($index) { |
| 38 | + // eslint-disable-next-line no-bitwise |
| 39 | + return $index & 1; |
| 40 | + }); |
| 41 | + } |
| 42 | + |
| 43 | + scope.$watch(indexWatchExpression, ngClassIndexWatchAction); |
32 | 44 | }
|
33 | 45 |
|
34 |
| - scope.$watch(attr[name], ngClassWatchAction, true); |
| 46 | + scope.$watch(watchExpression, watchAction, isOneTime); |
35 | 47 |
|
36 |
| - function addClasses(classes) { |
37 |
| - var newClasses = digestClassCounts(classes, 1); |
38 |
| - attr.$addClass(newClasses); |
| 48 | + function addClasses(classString) { |
| 49 | + classString = digestClassCounts(split(classString), 1); |
| 50 | + attr.$addClass(classString); |
39 | 51 | }
|
40 | 52 |
|
41 |
| - function removeClasses(classes) { |
42 |
| - var newClasses = digestClassCounts(classes, -1); |
43 |
| - attr.$removeClass(newClasses); |
| 53 | + function removeClasses(classString) { |
| 54 | + classString = digestClassCounts(split(classString), -1); |
| 55 | + attr.$removeClass(classString); |
44 | 56 | }
|
45 | 57 |
|
46 |
| - function digestClassCounts(classes, count) { |
47 |
| - // Use createMap() to prevent class assumptions involving property |
48 |
| - // names in Object.prototype |
49 |
| - var classCounts = element.data('$classCounts') || createMap(); |
| 58 | + function updateClasses(oldClassString, newClassString) { |
| 59 | + var oldClassArray = split(oldClassString); |
| 60 | + var newClassArray = split(newClassString); |
| 61 | + |
| 62 | + var toRemoveArray = arrayDifference(oldClassArray, newClassArray); |
| 63 | + var toAddArray = arrayDifference(newClassArray, oldClassArray); |
| 64 | + |
| 65 | + var toRemoveString = digestClassCounts(toRemoveArray, -1); |
| 66 | + var toAddString = digestClassCounts(toAddArray, 1); |
| 67 | + |
| 68 | + attr.$addClass(toAddString); |
| 69 | + attr.$removeClass(toRemoveString); |
| 70 | + } |
| 71 | + |
| 72 | + function digestClassCounts(classArray, count) { |
50 | 73 | var classesToUpdate = [];
|
51 |
| - forEach(classes, function(className) { |
| 74 | + |
| 75 | + forEach(classArray, function(className) { |
52 | 76 | if (count > 0 || classCounts[className]) {
|
53 | 77 | classCounts[className] = (classCounts[className] || 0) + count;
|
54 | 78 | if (classCounts[className] === +(count > 0)) {
|
55 | 79 | classesToUpdate.push(className);
|
56 | 80 | }
|
57 | 81 | }
|
58 | 82 | });
|
59 |
| - element.data('$classCounts', classCounts); |
| 83 | + |
60 | 84 | return classesToUpdate.join(' ');
|
61 | 85 | }
|
62 | 86 |
|
63 |
| - function updateClasses(oldClasses, newClasses) { |
64 |
| - var toAdd = arrayDifference(newClasses, oldClasses); |
65 |
| - var toRemove = arrayDifference(oldClasses, newClasses); |
66 |
| - toAdd = digestClassCounts(toAdd, 1); |
67 |
| - toRemove = digestClassCounts(toRemove, -1); |
| 87 | + function ngClassIndexWatchAction(newModulo) { |
| 88 | + // This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it |
| 89 | + // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the |
| 90 | + // `ngClass[OneTime]WatchAction()` will update the classes. |
| 91 | + if (newModulo === selector) { |
| 92 | + addClasses(oldClassString); |
| 93 | + } else { |
| 94 | + removeClasses(oldClassString); |
| 95 | + } |
68 | 96 |
|
69 |
| - attr.$addClass(toAdd); |
70 |
| - attr.$removeClass(toRemove); |
| 97 | + oldModulo = newModulo; |
71 | 98 | }
|
72 | 99 |
|
73 |
| - function ngClassWatchAction(newVal) { |
74 |
| - // eslint-disable-next-line no-bitwise |
75 |
| - if (selector === true || (scope.$index & 1) === selector) { |
76 |
| - var newClasses = arrayClasses(newVal || []); |
77 |
| - if (!oldVal) { |
78 |
| - addClasses(newClasses); |
79 |
| - } else if (!equals(newVal,oldVal)) { |
80 |
| - var oldClasses = arrayClasses(oldVal); |
81 |
| - updateClasses(oldClasses, newClasses); |
82 |
| - } |
| 100 | + function ngClassOneTimeWatchAction(newClassValue) { |
| 101 | + var newClassString = toClassString(newClassValue); |
| 102 | + |
| 103 | + if (newClassString !== oldClassString) { |
| 104 | + ngClassWatchAction(newClassString); |
83 | 105 | }
|
84 |
| - if (isArray(newVal)) { |
85 |
| - oldVal = newVal.map(function(v) { return shallowCopy(v); }); |
86 |
| - } else { |
87 |
| - oldVal = shallowCopy(newVal); |
| 106 | + } |
| 107 | + |
| 108 | + function ngClassWatchAction(newClassString) { |
| 109 | + if (oldModulo === selector) { |
| 110 | + updateClasses(oldClassString, newClassString); |
88 | 111 | }
|
| 112 | + |
| 113 | + oldClassString = newClassString; |
89 | 114 | }
|
90 | 115 | }
|
91 | 116 | };
|
| 117 | + }]; |
92 | 118 |
|
93 |
| - function arrayDifference(tokens1, tokens2) { |
94 |
| - var values = []; |
| 119 | + // Helpers |
| 120 | + function arrayDifference(tokens1, tokens2) { |
| 121 | + if (!tokens1 || !tokens1.length) return []; |
| 122 | + if (!tokens2 || !tokens2.length) return tokens1; |
95 | 123 |
|
96 |
| - outer: |
97 |
| - for (var i = 0; i < tokens1.length; i++) { |
98 |
| - var token = tokens1[i]; |
99 |
| - for (var j = 0; j < tokens2.length; j++) { |
100 |
| - if (token === tokens2[j]) continue outer; |
101 |
| - } |
102 |
| - values.push(token); |
| 124 | + var values = []; |
| 125 | + |
| 126 | + outer: |
| 127 | + for (var i = 0; i < tokens1.length; i++) { |
| 128 | + var token = tokens1[i]; |
| 129 | + for (var j = 0; j < tokens2.length; j++) { |
| 130 | + if (token === tokens2[j]) continue outer; |
103 | 131 | }
|
104 |
| - return values; |
| 132 | + values.push(token); |
105 | 133 | }
|
106 | 134 |
|
107 |
| - function arrayClasses(classVal) { |
108 |
| - var classes = []; |
109 |
| - if (isArray(classVal)) { |
110 |
| - forEach(classVal, function(v) { |
111 |
| - classes = classes.concat(arrayClasses(v)); |
112 |
| - }); |
113 |
| - return classes; |
114 |
| - } else if (isString(classVal)) { |
115 |
| - return classVal.split(' '); |
116 |
| - } else if (isObject(classVal)) { |
117 |
| - forEach(classVal, function(v, k) { |
118 |
| - if (v) { |
119 |
| - classes = classes.concat(k.split(' ')); |
120 |
| - } |
121 |
| - }); |
122 |
| - return classes; |
| 135 | + return values; |
| 136 | + } |
| 137 | + |
| 138 | + function split(classString) { |
| 139 | + return classString && classString.split(' '); |
| 140 | + } |
| 141 | + |
| 142 | + function toClassString(classValue) { |
| 143 | + var classString = classValue; |
| 144 | + |
| 145 | + if (isArray(classValue)) { |
| 146 | + classString = classValue.map(toClassString).join(' '); |
| 147 | + } else if (isObject(classValue)) { |
| 148 | + classString = Object.keys(classValue). |
| 149 | + filter(function(key) { return classValue[key]; }). |
| 150 | + join(' '); |
| 151 | + } |
| 152 | + |
| 153 | + return classString; |
| 154 | + } |
| 155 | + |
| 156 | + function toFlatValue(classValue) { |
| 157 | + var flatValue = classValue; |
| 158 | + |
| 159 | + if (isArray(classValue)) { |
| 160 | + flatValue = classValue.map(toFlatValue); |
| 161 | + } else if (isObject(classValue)) { |
| 162 | + var hasUndefined = false; |
| 163 | + |
| 164 | + flatValue = Object.keys(classValue).filter(function(key) { |
| 165 | + var value = classValue[key]; |
| 166 | + |
| 167 | + if (!hasUndefined && isUndefined(value)) { |
| 168 | + hasUndefined = true; |
| 169 | + } |
| 170 | + |
| 171 | + return value; |
| 172 | + }); |
| 173 | + |
| 174 | + if (hasUndefined) { |
| 175 | + // Prevent the `oneTimeLiteralWatchInterceptor` from unregistering |
| 176 | + // the watcher, by including at least one `undefined` value. |
| 177 | + flatValue.push(undefined); |
123 | 178 | }
|
124 |
| - return classVal; |
125 | 179 | }
|
126 |
| - }]; |
| 180 | + |
| 181 | + return flatValue; |
| 182 | + } |
127 | 183 | }
|
128 | 184 |
|
129 | 185 | /**
|
|
0 commit comments