diff --git a/src/ng/compile.js b/src/ng/compile.js index 175efc13211e..e1a9bb34248b 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1,5 +1,7 @@ 'use strict'; +window.dump = function() {}; + /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! * * DOM-related variables: @@ -545,6 +547,35 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // 'on' and be composed of only English letters. var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; + var svgWrap = function(template) { + var wrapper = document.createElement('div'); + wrapper.innerHTML = ''+template+''; + return wrapper.childNodes[0].childNodes; + }; + + var SVG_NAMESPACE = { + type: 'svg', + wrap: svgWrap, + clone: svgWrap, + cloneJqLite: function(elements) { + var nodes = []; + forEach(elements, function(element) { + nodes.push(svgWrap(element.outerHTML)[0]); + }); + return jqLite(nodes); + } + }; + + var HTML_NAMESPACE = { + type: 'html', + wrap: identity, + clone: jqLiteClone, + cloneJqLite: function(elements) { + return JQLitePrototype.clone.call(elements); + } + }; + + /** * @ngdoc method * @name $compileProvider#directive @@ -852,12 +883,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //================================ function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, - previousCompileContext) { + previousCompileContext, namespace) { if (!($compileNodes instanceof jqLite)) { // jquery always rewraps, whereas we need to preserve the original selector so that we can // modify it. $compileNodes = jqLite($compileNodes); } + + $compileNodes.namespace = namespace || HTML_NAMESPACE; + // 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){ @@ -867,16 +901,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, - maxPriority, ignoreDirective, previousCompileContext); + maxPriority, ignoreDirective, previousCompileContext, namespace); safeAddClass($compileNodes, 'ng-scope'); return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn){ assertArg(scope, 'scope'); // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart // and sometimes changes the structure of the DOM. + // NOTE: namespace passed into cloneJqLite on $compileNodes + var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! + ? $compileNodes.namespace.cloneJqLite($compileNodes) // IMPORTANT!!! : $compileNodes; + $linkNode.namespace = $compileNodes.namespace; + forEach(transcludeControllers, function(instance, name) { $linkNode.data('$' + name + 'Controller', instance); }); @@ -914,34 +952,51 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {Function} A composite linking function of all of the matched directives or null. */ function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, - previousCompileContext) { + previousCompileContext, namespace) { var linkFns = [], attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound; for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); + var node = nodeList[i]; + namespace = namespace || HTML_NAMESPACE; + + var nodeTagName = nodeName_(node); + if(nodeTagName === 'svg') { + namespace = SVG_NAMESPACE; + } + // else if(nodeTagName === 'foreignobject') { + // namespace = HTML_NAMESPACE; + // } + + nodeList.namespace = namespace; + // we must always refer to nodeList[i] since the nodes can be replaced underneath us. - directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, + directives = collectDirectives(node, [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); nodeLinkFn = (directives.length) - ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, - null, [], [], previousCompileContext) + ? applyDirectivesToNode(directives, node, attrs, transcludeFn, $rootElement, + null, [], [], previousCompileContext, namespace) : null; + if (nodeLinkFn) { + nodeLinkFn.namespace = nodeLinkFn.namespace || namespace; + } + if (nodeLinkFn && nodeLinkFn.scope) { safeAddClass(attrs.$$element, 'ng-scope'); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || - !(childNodes = nodeList[i].childNodes) || + !(childNodes = node.childNodes) || !childNodes.length) ? null : compileNodes(childNodes, nodeLinkFn ? ( (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) - && nodeLinkFn.transclude) : transcludeFn); + && nodeLinkFn.transclude) : transcludeFn, undefined, undefined, undefined, undefined, namespace); linkFns.push(nodeLinkFn, childLinkFn); linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; @@ -999,7 +1054,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { - var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) { + var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, namespace) { var scopeCreated = false; if (!transcludedScope) { @@ -1008,7 +1063,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn); + var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, namespace); if (scopeCreated) { clone.on('$destroy', function() { transcludedScope.$destroy(); }); } @@ -1189,7 +1244,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, - previousCompileContext) { + previousCompileContext, namespace) { previousCompileContext = previousCompileContext || {}; var terminalPriority = -Number.MAX_VALUE, @@ -1276,6 +1331,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); + $compileNode.namespace = namespace; compileNode = $compileNode[0]; replaceWith(jqCollection, sliceArgs($template), compileNode); @@ -1291,9 +1347,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nonTlbTranscludeDirective: nonTlbTranscludeDirective }); } else { - $template = jqLite(jqLiteClone(compileNode)).contents(); - $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn); + $template = $compileNode.contents(); + $template.remove(); + childTranscludeFn = (function($template) { + return function childTranscludeFnFactory() { + var namespaceArg = arguments[4]; + var childTranscludeFn = childTranscludeFnFactory.fn; + + if (!childTranscludeFn) { + childTranscludeFn = childTranscludeFnFactory.fn = compile($template, transcludeFn, undefined, undefined, undefined, namespaceArg); + } + + return childTranscludeFn.apply(undefined, arguments); + } + }($template)); } } @@ -1313,7 +1380,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(wrapTemplate(directive.type, trim(directiveValue))); + $template = jqLite(namespace.wrap(trim(directiveValue))); } compileNode = $template[0]; @@ -1325,6 +1392,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceWith(jqCollection, $compileNode, compileNode); + var compileNodeTagName = nodeName_($compileNode[0]); + if (compileNodeTagName === 'svg') { + nodeLinkFn.namespace = $compileNode.namespace = SVG_NAMESPACE; + } else if(compileNodeTagName=== 'foreignobject') { + nodeLinkFn.namespace = $compileNode.namespace = HTML_NAMESPACE; + } + + var newTemplateAttrs = {$attr: {}}; // combine directives from the original node and from the template: @@ -1604,6 +1679,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { for(i = postLinkFns.length - 1; i >= 0; i--) { try { linkFn = postLinkFns[i]; + $element.namespace = nodeLinkFn.namespace; linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); } catch (e) { @@ -1612,7 +1688,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } // This is the function that is injected as `$transclude`. - function controllersBoundTransclude(scope, cloneAttachFn) { + function controllersBoundTransclude(scope, cloneAttachFn, namespace) { var transcludeControllers; // no scope passed @@ -1625,7 +1701,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { transcludeControllers = elementControllers; } - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, namespace); } } } @@ -1815,6 +1891,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!(previousCompileContext.hasElementTranscludeDirective && origAsyncDirective.replace)) { // it was cloned therefore we have to clone as well. + // NOTE: namespace is not taken into account here. Should it be? linkNode = jqLiteClone(compileNode); } diff --git a/src/ng/directive/ngTransclude.js b/src/ng/directive/ngTransclude.js index 9f0aa58c5e0b..e8746f72fd5c 100644 --- a/src/ng/directive/ngTransclude.js +++ b/src/ng/directive/ngTransclude.js @@ -65,9 +65,9 @@ var ngTranscludeDirective = ngDirective({ startingTag($element)); } - $transclude(function(clone) { + $transclude(null, function(clone) { $element.empty(); $element.append(clone); - }); + }, $element.namespace); } }); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 83e899345607..bafddd7d41a1 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -7,6 +7,7 @@ function calcCacheSize() { return size; } + describe('$compile', function() { var element, directive, $compile, $rootScope; @@ -63,6 +64,29 @@ describe('$compile', function() { terminal: true })); + directive('svgContainer', function() { + return { + template: '', + replace: true, + transclude: true, + }; + }); + + directive('svgCircle', function(){ + return { + template: '', + replace: true, + }; + }); + + directive('myForeignObject', function(){ + return { + template: '', + replace: true, + transclude: true, + }; + }); + return function(_$compile_, _$rootScope_) { $rootScope = _$rootScope_; $compile = _$compile_; @@ -78,6 +102,87 @@ describe('$compile', function() { dealoc(element); }); + describe('svg namespace', function() { + // this method assumes some sort of sized SVG element is being inspected. + function assertIsValidSvgCircle(elem) { + var unknownElement = Object.prototype.toString.call(elem) === '[object HTMLUnknownElement]'; + expect(unknownElement).toBe(false); + var box = elem.getBoundingClientRect(); + expect(box.width === 0 && box.height === 0).toBe(false); + } + + it('should handle transcluded svg elements', inject(function($compile){ + element = jqLite('
' + + '' + + '
'); + $compile(element.contents())($rootScope); + document.body.appendChild(element[0]); + + var circle = element.find('circle'); + + assertIsValidSvgCircle(circle[0]); + })); + + it('should handle custom svg elements inside svg tag', function(){ + element = jqLite('
' + + '' + + '
'); + $compile(element.contents())($rootScope); + document.body.appendChild(element[0]); + + var circle = element.find('circle'); + assertIsValidSvgCircle(circle[0]); + }); + + it('should handle transcluded custom svg elements', function(){ + element = jqLite('
' + + '' + + '
'); + $compile(element.contents())($rootScope); + document.body.appendChild(element[0]); + + var circle = element.find('circle'); + assertIsValidSvgCircle(circle[0]); + }); + + it('should handle foreignObject', function(){ + element = jqLite('
' + + '
test
' + + '
'); + $compile(element.contents())($rootScope); + document.body.appendChild(element[0]); + + var testElem = element.find('div'); + expect(testElem[0].toString()).toBe('[object HTMLDivElement]'); + var bounds = testElem[0].getBoundingClientRect(); + expect(bounds.width === 20 && bounds.height === 20).toBe(true); + }); + + it('should handle custom svg containers that transclude to foreignObject that transclude html', function(){ + element = jqLite('
' + + '
test
' + + '
'); + $compile(element.contents())($rootScope); + document.body.appendChild(element[0]); + + var testElem = element.find('div'); + expect(testElem[0].toString()).toBe('[object HTMLDivElement]'); + var bounds = testElem[0].getBoundingClientRect(); + expect(bounds.width === 20 && bounds.height === 20).toBe(true); + }); + + // NOTE: This test may be redundant. + it('should handle custom svg containers that transclude to foreignObject that transclude to custom svg containers that transclude to custom elements', function(){ + element = jqLite('
' + + '' + + '
'); + $compile(element.contents())($rootScope); + document.body.appendChild(element[0]); + + var circle = element.find('circle'); + assertIsValidSvgCircle(circle[0]); + }); + }); describe('configuration', function() { it('should register a directive', function() { @@ -6040,4 +6145,4 @@ describe('$compile', function() { expect(element.hasClass('fire')).toBe(true); })); }); -}); +}); \ No newline at end of file