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 = '';
+ 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