mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-07 08:57:54 +08:00
fix($compile): use the correct namespace for transcluded svg elements
Via transclusion, svg elements can occur outside an `<svg>` container in an Angular template but are put into an `<svg>` container through compilation and linking. E.g. Given that `svg-container` is a transcluding directive with the following template: ``` <svg ng-transclude></svg> ``` The following markup creates a `<circle>` inside of an `<svg>` element during runtime: ``` <svg-container> <circle></circle> </svg-container> ``` However, this produces non working `<circle>` elements, as svg elements need to be created inside of an `<svg>` element. This change detects for most cases the correct namespace of transcluded content and recreates that content in the correct `<svg>` container when needed during compilation. For special cases it adds an addition argument to `$transclude` that allows to specify the future parent node of elements that will be cloned and attached using the `cloneAttachFn`. Related to #8494 Closes #8716
This commit is contained in:
@@ -185,9 +185,18 @@
|
||||
* * `$scope` - Current scope associated with the element
|
||||
* * `$element` - Current element
|
||||
* * `$attrs` - Current attributes object for the element
|
||||
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope.
|
||||
* The scope can be overridden by an optional first argument.
|
||||
* `function([scope], cloneLinkingFn)`.
|
||||
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
|
||||
* `function([scope], cloneLinkingFn, futureParentElement)`.
|
||||
* * `scope`: optional argument to override the scope.
|
||||
* * `cloneLinkingFn`: optional argument to create clones of the original translcuded content.
|
||||
* * `futureParentElement`:
|
||||
* * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
|
||||
* * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
|
||||
* * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
|
||||
* and when the `cloneLinkinFn` is passed,
|
||||
* as those elements need to created and cloned in a special way when they are defined outside their
|
||||
* usual containers (e.g. like `<svg>`).
|
||||
* * See also the `directive.templateNamespace` property.
|
||||
*
|
||||
*
|
||||
* #### `require`
|
||||
@@ -265,6 +274,10 @@
|
||||
* one. See the {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive
|
||||
* Directives Guide} for an example.
|
||||
*
|
||||
* There very few scenarios were element replacement is required for the application function,
|
||||
* the main one being reusable custom components that are used within SVG contexts
|
||||
* (because SVG doesn't work with custom elements in the DOM tree).
|
||||
*
|
||||
* #### `transclude`
|
||||
* compile the content of the element and make it available to the directive.
|
||||
* Typically used with {@link ng.directive:ngTransclude
|
||||
@@ -359,10 +372,9 @@
|
||||
* the directives to use the controllers as a communication channel.
|
||||
*
|
||||
* * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
|
||||
* The scope can be overridden by an optional first argument. This is the same as the `$transclude`
|
||||
* parameter of directive controllers.
|
||||
* `function([scope], cloneLinkingFn)`.
|
||||
*
|
||||
* This is the same as the `$transclude`
|
||||
* parameter of directive controllers, see there for details.
|
||||
* `function([scope], cloneLinkingFn, futureParentElement)`.
|
||||
*
|
||||
* #### Pre-linking function
|
||||
*
|
||||
@@ -879,8 +891,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
compileNodes($compileNodes, transcludeFn, $compileNodes,
|
||||
maxPriority, ignoreDirective, previousCompileContext);
|
||||
safeAddClass($compileNodes, 'ng-scope');
|
||||
return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn){
|
||||
var namespace = null;
|
||||
return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){
|
||||
assertArg(scope, 'scope');
|
||||
if (!namespace) {
|
||||
namespace = detectNamespaceForChildElements(futureParentElement);
|
||||
if (namespace !== 'html') {
|
||||
$compileNodes = jqLite(
|
||||
wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
|
||||
// and sometimes changes the structure of the DOM.
|
||||
var $linkNode = cloneConnectFn
|
||||
@@ -901,6 +923,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
};
|
||||
}
|
||||
|
||||
function detectNamespaceForChildElements(parentElement) {
|
||||
// TODO: Make this detect MathML as well...
|
||||
var node = parentElement && parentElement[0];
|
||||
if (!node) {
|
||||
return 'html';
|
||||
} else {
|
||||
return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg': 'html';
|
||||
}
|
||||
}
|
||||
|
||||
function safeAddClass($element, className) {
|
||||
try {
|
||||
$element.addClass(className);
|
||||
@@ -1024,7 +1056,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
|
||||
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) {
|
||||
|
||||
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) {
|
||||
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement) {
|
||||
var scopeCreated = false;
|
||||
|
||||
if (!transcludedScope) {
|
||||
@@ -1033,7 +1065,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
scopeCreated = true;
|
||||
}
|
||||
|
||||
var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn);
|
||||
var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement);
|
||||
if (scopeCreated && !elementTransclusion) {
|
||||
clone.on('$destroy', function() { transcludedScope.$destroy(); });
|
||||
}
|
||||
@@ -1645,11 +1677,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
|
||||
// This is the function that is injected as `$transclude`.
|
||||
function controllersBoundTransclude(scope, cloneAttachFn) {
|
||||
// Note: all arguments are optional!
|
||||
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) {
|
||||
var transcludeControllers;
|
||||
|
||||
// no scope passed
|
||||
if (!cloneAttachFn) {
|
||||
// No scope passed in:
|
||||
if (!isScope(scope)) {
|
||||
futureParentElement = cloneAttachFn;
|
||||
cloneAttachFn = scope;
|
||||
scope = undefined;
|
||||
}
|
||||
@@ -1657,8 +1691,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
if (hasElementTranscludeDirective) {
|
||||
transcludeControllers = elementControllers;
|
||||
}
|
||||
|
||||
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers);
|
||||
if (!futureParentElement) {
|
||||
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
|
||||
}
|
||||
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ describe('$compile', function() {
|
||||
return !isUnknownElement(d.firstChild);
|
||||
}
|
||||
|
||||
// IE9-11 do not support foreignObject in svg...
|
||||
function supportsForeignObject() {
|
||||
var d = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
|
||||
return !!d.toString().match(/SVGForeignObject/);
|
||||
}
|
||||
|
||||
var element, directive, $compile, $rootScope;
|
||||
|
||||
beforeEach(module(provideLog, function($provide, $compileProvider){
|
||||
@@ -80,6 +86,45 @@ describe('$compile', function() {
|
||||
terminal: true
|
||||
}));
|
||||
|
||||
directive('svgContainer', function() {
|
||||
return {
|
||||
template: '<svg width="400" height="400" ng-transclude></svg>',
|
||||
replace: true,
|
||||
transclude: true
|
||||
};
|
||||
});
|
||||
|
||||
directive('svgCustomTranscludeContainer', function() {
|
||||
return {
|
||||
template: '<svg width="400" height="400"></svg>',
|
||||
transclude: true,
|
||||
link: function(scope, element, attr, ctrls, $transclude) {
|
||||
var futureParent = element.children().eq(0);
|
||||
$transclude(function(clone) {
|
||||
futureParent.append(clone);
|
||||
}, futureParent);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
directive('svgCircle', function(){
|
||||
return {
|
||||
template: '<circle cx="2" cy="2" r="1"></circle>',
|
||||
templateNamespace: 'svg',
|
||||
replace: true
|
||||
};
|
||||
});
|
||||
|
||||
directive('myForeignObject', function(){
|
||||
return {
|
||||
template: '<foreignObject width="100" height="100" ng-transclude></foreignObject>',
|
||||
templateNamespace: 'svg',
|
||||
replace: true,
|
||||
transclude: true
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
return function(_$compile_, _$rootScope_) {
|
||||
$rootScope = _$rootScope_;
|
||||
$compile = _$compile_;
|
||||
@@ -154,6 +199,105 @@ describe('$compile', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('svg namespace transcludes', function() {
|
||||
// this method assumes some sort of sized SVG element is being inspected.
|
||||
function assertIsValidSvgCircle(elem) {
|
||||
expect(isUnknownElement(elem)).toBe(false);
|
||||
expect(isSVGElement(elem)).toBe(true);
|
||||
var box = elem.getBoundingClientRect();
|
||||
expect(box.width === 0 && box.height === 0).toBe(false);
|
||||
}
|
||||
|
||||
it('should handle transcluded svg elements', inject(function($compile){
|
||||
element = jqLite('<div><svg-container>' +
|
||||
'<circle cx="4" cy="4" r="2"></circle>' +
|
||||
'</svg-container></div>');
|
||||
$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', inject(function(){
|
||||
element = jqLite('<div><svg width="300" height="300">' +
|
||||
'<svg-circle></svg-circle>' +
|
||||
'</svg></div>');
|
||||
$compile(element.contents())($rootScope);
|
||||
document.body.appendChild(element[0]);
|
||||
|
||||
var circle = element.find('circle');
|
||||
assertIsValidSvgCircle(circle[0]);
|
||||
}));
|
||||
|
||||
it('should handle transcluded custom svg elements', inject(function(){
|
||||
element = jqLite('<div><svg-container>' +
|
||||
'<svg-circle></svg-circle>' +
|
||||
'</svg-container></div>');
|
||||
$compile(element.contents())($rootScope);
|
||||
document.body.appendChild(element[0]);
|
||||
|
||||
var circle = element.find('circle');
|
||||
assertIsValidSvgCircle(circle[0]);
|
||||
}));
|
||||
|
||||
if (supportsForeignObject()) {
|
||||
it('should handle foreignObject', inject(function(){
|
||||
element = jqLite('<div><svg-container>' +
|
||||
'<foreignObject width="100" height="100"><div class="test" style="position:absolute;width:20px;height:20px">test</div></foreignObject>' +
|
||||
'</svg-container></div>');
|
||||
$compile(element.contents())($rootScope);
|
||||
document.body.appendChild(element[0]);
|
||||
|
||||
var testElem = element.find('div');
|
||||
expect(isHTMLElement(testElem[0])).toBe(true);
|
||||
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', inject(function(){
|
||||
element = jqLite('<div><svg-container>' +
|
||||
'<my-foreign-object><div class="test" style="width:20px;height:20px">test</div></my-foreign-object>' +
|
||||
'</svg-container></div>');
|
||||
$compile(element.contents())($rootScope);
|
||||
document.body.appendChild(element[0]);
|
||||
|
||||
var testElem = element.find('div');
|
||||
expect(isHTMLElement(testElem[0])).toBe(true);
|
||||
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', inject(function(){
|
||||
element = jqLite('<div><svg-container>' +
|
||||
'<my-foreign-object><svg-container><svg-circle></svg-circle></svg-container></my-foreign-object>' +
|
||||
'</svg-container></div>');
|
||||
$compile(element.contents())($rootScope);
|
||||
document.body.appendChild(element[0]);
|
||||
|
||||
var circle = element.find('circle');
|
||||
assertIsValidSvgCircle(circle[0]);
|
||||
}));
|
||||
}
|
||||
|
||||
it('should handle directives with templates that manually add the transclude further down', inject(function() {
|
||||
element = jqLite('<div><svg-custom-transclude-container>' +
|
||||
'<circle cx="2" cy="2" r="1"></circle></svg-custom-transclude-container>' +
|
||||
'</div>');
|
||||
$compile(element.contents())($rootScope);
|
||||
document.body.appendChild(element[0]);
|
||||
|
||||
var circle = element.find('circle');
|
||||
assertIsValidSvgCircle(circle[0]);
|
||||
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('compile phase', function() {
|
||||
|
||||
it('should attach scope to the document node when it is compiled explicitly', inject(function($document){
|
||||
|
||||
@@ -351,4 +351,23 @@ describe('ngIf animations', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with svg elements when the svg container is transcluded', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('svgContainer', function() {
|
||||
return {
|
||||
template: '<svg ng-transclude></svg>',
|
||||
replace: true,
|
||||
transclude: true
|
||||
};
|
||||
});
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
element = $compile('<svg-container><circle ng-if="flag"></circle></svg-container>')($rootScope);
|
||||
$rootScope.flag = true;
|
||||
$rootScope.$apply();
|
||||
|
||||
var circle = element.find('circle');
|
||||
expect(circle[0].toString()).toMatch(/SVG/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1463,4 +1463,24 @@ describe('ngRepeat animations', function() {
|
||||
})
|
||||
);
|
||||
|
||||
it('should work with svg elements when the svg container is transcluded', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('svgContainer', function() {
|
||||
return {
|
||||
template: '<svg ng-transclude></svg>',
|
||||
replace: true,
|
||||
transclude: true
|
||||
};
|
||||
});
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
element = $compile('<svg-container><circle ng-repeat="r in rows"></circle></svg-container>')($rootScope);
|
||||
$rootScope.rows = [1];
|
||||
$rootScope.$apply();
|
||||
|
||||
var circle = element.find('circle');
|
||||
expect(circle[0].toString()).toMatch(/SVG/);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -433,4 +433,25 @@ describe('ngSwitch animations', function() {
|
||||
expect(destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with svg elements when the svg container is transcluded', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('svgContainer', function() {
|
||||
return {
|
||||
template: '<svg ng-transclude></svg>',
|
||||
replace: true,
|
||||
transclude: true
|
||||
};
|
||||
});
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
element = $compile('<svg-container ng-switch="inc"><circle ng-switch-when="one"></circle>' +
|
||||
'</svg-container>')($rootScope);
|
||||
$rootScope.inc = 'one';
|
||||
$rootScope.$apply();
|
||||
|
||||
var circle = element.find('circle');
|
||||
expect(circle[0].toString()).toMatch(/SVG/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user