diff --git a/src/jqLite.js b/src/jqLite.js index cf9d1fa168e8..4959a2ed36b9 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -165,7 +165,8 @@ function JQLite(element) { div.innerHTML = '
 
' + element; // IE insanity to make NoScope elements work! div.removeChild(div.firstChild); // remove the superfluous div JQLiteAddNodes(this, div.childNodes); - this.remove(); // detach the elements from the temporary DOM div. + var fragment = jqLite(document.createDocumentFragment()); + fragment.append(this); // detach the elements from the temporary DOM div. } else { JQLiteAddNodes(this, element); } @@ -456,24 +457,26 @@ forEach({ } }, - text: extend((msie < 9) - ? function(element, value) { - if (element.nodeType == 1 /** Element */) { - if (isUndefined(value)) - return element.innerText; - element.innerText = value; - } else { - if (isUndefined(value)) - return element.nodeValue; - element.nodeValue = value; - } + text: (function() { + var NODE_TYPE_TEXT_PROPERTY = []; + if (msie < 9) { + NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ + } else { + NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ + } + getText.$dv = ''; + return getText; + + function getText(element, value) { + var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType] + if (isUndefined(value)) { + return textProp ? element[textProp] : ''; } - : function(element, value) { - if (isUndefined(value)) { - return element.textContent; - } - element.textContent = value; - }, {$dv:''}), + element[textProp] = value; + } + })(), val: function(element, value) { if (isUndefined(value)) { @@ -518,8 +521,14 @@ forEach({ return this; } else { // we are a read, so read the first child. - if (this.length) - return fn(this[0], arg1, arg2); + var value = fn.$dv; + // Only if we have $dv do we iterate over all, otherwise it is just the first element. + var jj = value == undefined ? Math.min(this.length, 1) : this.length; + for (var j = 0; j < jj; j++) { + var nodeValue = fn(this[j], arg1, arg2); + value = value ? value + nodeValue : nodeValue; + } + return value; } } else { // we are a write, so apply to all children @@ -529,7 +538,6 @@ forEach({ // return self for chaining return this; } - return fn.$dv; }; }); diff --git a/src/ng/animator.js b/src/ng/animator.js index 2965717bdc3d..2b399813264b 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -395,11 +395,16 @@ var $AnimatorProvider = function() { } function insert(element, parent, after) { - if (after) { - after.after(element); - } else { - parent.append(element); - } + var afterNode = after && after[after.length - 1]; + var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; + var afterNextSibling = afterNode && afterNode.nextSibling; + forEach(element, function(node) { + if (afterNextSibling) { + parentNode.insertBefore(node, afterNextSibling); + } else { + parentNode.appendChild(node); + } + }); } function remove(element) { diff --git a/src/ng/compile.js b/src/ng/compile.js index be22482b033b..2dddf82dcbff 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -358,11 +358,12 @@ function $CompileProvider($provide) { // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it. $compileNodes = jqLite($compileNodes); } + var tempParent = document.createDocumentFragment(); // We can not compile top level text elements since text nodes can be merged and we will // not be able to attach scope data to them, so we will wrap them in forEach($compileNodes, function(node, index){ if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = jqLite(node).wrap('').parent()[0]; + $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; } }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority); @@ -420,7 +421,7 @@ function $CompileProvider($provide) { attrs = new Attributes(); // we must always refer to nodeList[i] since the nodes can be replaced underneath us. - directives = collectDirectives(nodeList[i], [], attrs, maxPriority); + directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined); nodeLinkFn = (directives.length) ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement) @@ -509,6 +510,10 @@ function $CompileProvider($provide) { // iterate over the attributes for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { + var attrStartName; + var attrEndName; + var index; + attr = nAttrs[j]; if (attr.specified) { name = attr.name; @@ -517,6 +522,11 @@ function $CompileProvider($provide) { if (NG_ATTR_BINDING.test(ngAttrName)) { name = ngAttrName.substr(6).toLowerCase(); } + if ((index = ngAttrName.lastIndexOf('Start')) != -1 && index == ngAttrName.length - 5) { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } nName = directiveNormalize(name.toLowerCase()); attrsMap[nName] = name; attrs[nName] = value = trim((msie && name == 'href') @@ -526,7 +536,7 @@ function $CompileProvider($provide) { attrs[nName] = true; // presence means true } addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority); + addDirective(directives, nName, 'A', maxPriority, attrStartName, attrEndName); } } @@ -565,6 +575,47 @@ function $CompileProvider($provide) { return directives; } + /** + * Given a node with an directive-start it collects all of the siblings until it find directive-end. + * @param node + * @param attrStart + * @param attrEnd + * @returns {*} + */ + function groupScan(node, attrStart, attrEnd) { + var nodes = []; + var depth = 0; + if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { + var startNode = node; + do { + if (!node) { + throw ngError(51, "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); + } + if (node.hasAttribute(attrStart)) depth++; + if (node.hasAttribute(attrEnd)) depth--; + nodes.push(node); + node = node.nextSibling; + } while (depth > 0); + } else { + nodes.push(node); + } + return jqLite(nodes); + } + + /** + * Wrapper for linking function which converts normal linking function into a grouped + * linking function. + * @param linkFn + * @param attrStart + * @param attrEnd + * @returns {Function} + */ + function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { + return function(scope, element, attrs, controllers) { + element = groupScan(element[0], attrStart, attrEnd); + return linkFn(scope, element, attrs, controllers); + } + } /** * Once the directives have been collected, their compile functions are executed. This method @@ -601,6 +652,13 @@ function $CompileProvider($provide) { // executes all directives on the current element for(var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; + var attrStart = directive.$$start; + var attrEnd = directive.$$end; + + // collect multiblock sections + if (attrStart) { + $compileNode = groupScan(compileNode, attrStart, attrEnd) + } $template = undefined; if (terminalPriority > directive.priority) { @@ -631,11 +689,11 @@ function $CompileProvider($provide) { transcludeDirective = directive; terminalPriority = directive.priority; if (directiveValue == 'element') { - $template = jqLite(compileNode); + $template = groupScan(compileNode, attrStart, attrEnd) $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite($template[0]), compileNode); + replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority); } else { $template = jqLite(JQLiteClone(compileNode)).contents(); @@ -699,9 +757,9 @@ function $CompileProvider($provide) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); if (isFunction(linkFn)) { - addLinkFns(null, linkFn); + addLinkFns(null, linkFn, attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post); + addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -723,12 +781,14 @@ function $CompileProvider($provide) { //////////////////// - function addLinkFns(pre, post) { + function addLinkFns(pre, post, attrStart, attrEnd) { if (pre) { + if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; preLinkFns.push(pre); } if (post) { + if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; postLinkFns.push(post); } @@ -907,8 +967,8 @@ function $CompileProvider($provide) { * * `M`: comment * @returns true if directive was added. */ - function addDirective(tDirectives, name, location, maxPriority) { - var match = false; + function addDirective(tDirectives, name, location, maxPriority, startAttrName, endAttrName) { + var match = null; if (hasDirectives.hasOwnProperty(name)) { for(var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i directive.priority) && directive.restrict.indexOf(location) != -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + } tDirectives.push(directive); - match = true; + match = directive; } } catch(e) { $exceptionHandler(e); } } @@ -1120,30 +1183,50 @@ function $CompileProvider($provide) { * * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes * in the root of the tree. - * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell, + * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell, * but replace its DOM node reference. * @param {Node} newNode The new DOM node. */ - function replaceWith($rootElement, $element, newNode) { - var oldNode = $element[0], - parent = oldNode.parentNode, + function replaceWith($rootElement, elementsToRemove, newNode) { + var firstElementToRemove = elementsToRemove[0], + removeCount = elementsToRemove.length, + parent = firstElementToRemove.parentNode, i, ii; if ($rootElement) { for(i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == oldNode) { - $rootElement[i] = newNode; + if ($rootElement[i] == firstElementToRemove) { + $rootElement[i++] = newNode; + for (var j = i, j2 = j + removeCount - 1, + jj = $rootElement.length; + j < jj; j++, j2++) { + if (j2 < jj) { + $rootElement[j] = $rootElement[j2]; + } else { + delete $rootElement[j]; + } + } + $rootElement.length -= removeCount - 1; break; } } } if (parent) { - parent.replaceChild(newNode, oldNode); + parent.replaceChild(newNode, firstElementToRemove); + } + var fragment = document.createDocumentFragment(); + fragment.appendChild(firstElementToRemove); + newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { + var element = elementsToRemove[k]; + jqLite(element).remove(); // must do this way to clean up expando + fragment.appendChild(element); + delete elementsToRemove[k]; } - newNode[jqLite.expando] = oldNode[jqLite.expando]; - $element[0] = newNode; + elementsToRemove[0] = newNode; + elementsToRemove.length = 1 } }]; } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 34d32f59adb2..6c2da071956d 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -258,7 +258,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (lastBlockMap.hasOwnProperty(key)) { block = lastBlockMap[key]; animate.leave(block.element); - block.element[0][NG_REMOVED] = true; + forEach(block.element, function(element) { element[NG_REMOVED] = true}); block.scope.$destroy(); } } diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 1ebe6ad48943..70c18d35600f 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -56,6 +56,9 @@ describe('jqLite', function() { it('should allow construction with html', function() { var nodes = jqLite('
1
2'); + expect(nodes[0].parentNode).toBeDefined(); + expect(nodes[0].parentNode.nodeType).toBe(11); /** Document Fragment **/; + expect(nodes[0].parentNode).toBe(nodes[1].parentNode); expect(nodes.length).toEqual(2); expect(nodes[0].innerHTML).toEqual('1'); expect(nodes[1].innerHTML).toEqual('2'); @@ -644,12 +647,13 @@ describe('jqLite', function() { it('should read/write value', function() { - var element = jqLite('
abc
'); - expect(element.length).toEqual(1); - expect(element[0].innerHTML).toEqual('abc'); + var element = jqLite('
ab
c'); + expect(element.length).toEqual(2); + expect(element[0].innerHTML).toEqual('ab'); + expect(element[1].innerHTML).toEqual('c'); expect(element.text()).toEqual('abc'); expect(element.text('xyz') == element).toBeTruthy(); - expect(element.text()).toEqual('xyz'); + expect(element.text()).toEqual('xyzxyz'); }); }); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index bf3d0b777c50..95b2ab72d93f 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2718,4 +2718,129 @@ describe('$compile', function() { expect(element.attr('test4')).toBe('Misko'); })); }); + + + describe('multi-element directive', function() { + it('should group on link function', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); + var spans = element.find('span'); + expect(spans.eq(0).css('display')).toBe('none'); + expect(spans.eq(1).css('display')).toBe('none'); + })); + + + it('should group on compile function', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '{{i}}A' + + '{{i}}B;' + + '
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('1A1B;2A2B;'); + })); + + + it('should group on $root compile function', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '{{i}}A' + + '{{i}}B;' + + '
')($rootScope); + $rootScope.$digest(); + element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. + expect(element.text()).toEqual('1A1B;2A2B;'); + })); + + + it('should group on nested groups', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '
{{i}}A
' + + '' + + '' + + '
{{i}}B;
' + + '
')($rootScope); + $rootScope.$digest(); + element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. + expect(element.text()).toEqual('1A..1B;2A..2B;'); + })); + + + it('should group on nested groups', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '
{{i}}(
' + + '{{j}}-' + + '{{j}}' + + '
){{i}};
' + + '
')($rootScope); + $rootScope.$digest(); + element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. + expect(element.text()).toEqual('1(2-23-3)1;2(2-23-3)2;'); + })); + + + it('should throw error if unterminated', function () { + module(function($compileProvider) { + $compileProvider.directive('foo', function() { + return { + }; + }); + }); + inject(function($compile, $rootScope) { + expect(function() { + element = $compile( + '
' + + '' + + '
'); + }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found."); + }); + }); + + + it('should throw error if unterminated', function () { + module(function($compileProvider) { + $compileProvider.directive('foo', function() { + return { + }; + }); + }); + inject(function($compile, $rootScope) { + expect(function() { + element = $compile( + '
' + + '' + + '
'); + }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found."); + }); + }); + + + it('should support data- and x- prefix', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '' + + '' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); + var spans = element.find('span'); + expect(spans.eq(0).css('display')).toBe('none'); + expect(spans.eq(1).css('display')).toBe('none'); + expect(spans.eq(2).css('display')).toBe('none'); + expect(spans.eq(3).css('display')).toBe('none'); + })); + }); });