From ac52bfd80fbabb53d376fcf7bf851670b195c79e Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 27 Oct 2015 13:16:27 -0700 Subject: [PATCH] fix(render): create svg elements with the right namespace Temporary fix for #4506 Closes #4949 --- .../src/core/dom/abstract_html_adapter.dart | 8 ++ .../src/core/dom/browser_adapter.dart | 9 ++ .../angular2/src/core/dom/browser_adapter.ts | 4 + modules/angular2/src/core/dom/dom_adapter.ts | 2 + .../angular2/src/core/dom/parse5_adapter.ts | 2 + .../src/core/render/dom/dom_renderer.ts | 104 +++++++++++++++++- .../test/core/linker/integration_spec.ts | 23 ++++ 7 files changed, 147 insertions(+), 5 deletions(-) diff --git a/modules/angular2/src/core/dom/abstract_html_adapter.dart b/modules/angular2/src/core/dom/abstract_html_adapter.dart index 58440d39a3..05b2cd2f1e 100644 --- a/modules/angular2/src/core/dom/abstract_html_adapter.dart +++ b/modules/angular2/src/core/dom/abstract_html_adapter.dart @@ -215,6 +215,10 @@ abstract class AbstractHtml5LibAdapter implements DomAdapter { return new Element.tag(tagName); } + createElementNS(ns, tagName, [doc]) { + throw 'not implemented'; + } + createTextNode(String text, [doc]) => new Text(text); createScriptTag(String attrName, String attrValue, [doc]) { @@ -297,6 +301,10 @@ abstract class AbstractHtml5LibAdapter implements DomAdapter { element.attributes[name] = value; } + setAttributeNS(element, String ns, String name, String value) { + throw 'not implemented'; + } + removeAttribute(element, String attribute) { element.attributes.remove(attribute); } diff --git a/modules/angular2/src/core/dom/browser_adapter.dart b/modules/angular2/src/core/dom/browser_adapter.dart index 66fab085c6..78a3924d45 100644 --- a/modules/angular2/src/core/dom/browser_adapter.dart +++ b/modules/angular2/src/core/dom/browser_adapter.dart @@ -287,6 +287,11 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter { return doc.createElement(tagName); } + Element createElementNS(String ns, String tagName, [HtmlDocument doc = null]) { + if (doc == null) doc = document; + return doc.createElementNS(ns, tagName); + } + Text createTextNode(String text, [HtmlDocument doc = null]) { return new Text(text); } @@ -354,6 +359,10 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter { element.setAttribute(name, value); } + void setAttributeNS(Element element, String ns, String name, String value) { + element.setAttributeNS(ns, name, value); + } + void removeAttribute(Element element, String name) { //there is no removeAttribute method as of now in Dart: //https://code.google.com/p/dart/issues/detail?id=19934 diff --git a/modules/angular2/src/core/dom/browser_adapter.ts b/modules/angular2/src/core/dom/browser_adapter.ts index f893d306e0..cb7a6a808f 100644 --- a/modules/angular2/src/core/dom/browser_adapter.ts +++ b/modules/angular2/src/core/dom/browser_adapter.ts @@ -182,6 +182,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { return t; } createElement(tagName, doc = document): HTMLElement { return doc.createElement(tagName); } + createElementNS(ns, tagName, doc = document): Element { return doc.createElementNS(ns, tagName); } createTextNode(text: string, doc = document): Text { return doc.createTextNode(text); } createScriptTag(attrName: string, attrValue: string, doc = document): HTMLScriptElement { var el = doc.createElement('SCRIPT'); @@ -225,6 +226,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { hasAttribute(element, attribute: string): boolean { return element.hasAttribute(attribute); } getAttribute(element, attribute: string): string { return element.getAttribute(attribute); } setAttribute(element, name: string, value: string) { element.setAttribute(name, value); } + setAttributeNS(ns: string, element, name: string, value: string) { + element.setAttributeNS(ns, name, value); + } removeAttribute(element, attribute: string) { element.removeAttribute(attribute); } templateAwareRoot(el): any { return this.isTemplateElement(el) ? this.content(el) : el; } createHtmlDocument(): HTMLDocument { diff --git a/modules/angular2/src/core/dom/dom_adapter.ts b/modules/angular2/src/core/dom/dom_adapter.ts index f1b016082c..2ac78dfaf1 100644 --- a/modules/angular2/src/core/dom/dom_adapter.ts +++ b/modules/angular2/src/core/dom/dom_adapter.ts @@ -71,6 +71,7 @@ export abstract class DomAdapter { abstract createComment(text: string): any; abstract createTemplate(html): HTMLElement; abstract createElement(tagName, doc?): HTMLElement; + abstract createElementNS(ns: string, tagName: string, doc?): Element; abstract createTextNode(text: string, doc?): Text; abstract createScriptTag(attrName: string, attrValue: string, doc?): HTMLElement; abstract createStyleElement(css: string, doc?): HTMLStyleElement; @@ -93,6 +94,7 @@ export abstract class DomAdapter { abstract hasAttribute(element, attribute: string): boolean; abstract getAttribute(element, attribute: string): string; abstract setAttribute(element, name: string, value: string); + abstract setAttributeNS(element, ns: string, name: string, value: string); abstract removeAttribute(element, attribute: string); abstract templateAwareRoot(el); abstract createHtmlDocument(): HTMLDocument; diff --git a/modules/angular2/src/core/dom/parse5_adapter.ts b/modules/angular2/src/core/dom/parse5_adapter.ts index a9b299b30b..d05cf7ce00 100644 --- a/modules/angular2/src/core/dom/parse5_adapter.ts +++ b/modules/angular2/src/core/dom/parse5_adapter.ts @@ -276,6 +276,7 @@ export class Parse5DomAdapter extends DomAdapter { createElement(tagName): HTMLElement { return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []); } + createElementNS(ns, tagName): HTMLElement { throw 'not implemented'; } createTextNode(text: string): Text { var t = this.createComment(text); t.type = 'text'; @@ -435,6 +436,7 @@ export class Parse5DomAdapter extends DomAdapter { } } } + setAttributeNS(element, ns: string, attribute: string, value: string) { throw 'not implemented'; } removeAttribute(element, attribute: string) { if (attribute) { StringMapWrapper.delete(element.attribs, attribute); diff --git a/modules/angular2/src/core/render/dom/dom_renderer.ts b/modules/angular2/src/core/render/dom/dom_renderer.ts index d65442aef1..5b59c64f80 100644 --- a/modules/angular2/src/core/render/dom/dom_renderer.ts +++ b/modules/angular2/src/core/render/dom/dom_renderer.ts @@ -32,6 +32,92 @@ import {createRenderView, NodeFactory} from '../view_factory'; import {DefaultRenderView, DefaultRenderFragmentRef, DefaultProtoViewRef} from '../view'; import {camelCaseToDashCase} from './util'; +// TODO(tbosch): solve SVG properly once https://github.com/angular/angular/issues/4417 is done +const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'; +const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; +const SVG_ELEMENT_NAMES = CONST_EXPR({ + 'altGlyph': true, + 'altGlyphDef': true, + 'altGlyphItem': true, + 'animate': true, + 'animateColor': true, + 'animateMotion': true, + 'animateTransform': true, + 'circle': true, + 'clipPath': true, + 'color-profile': true, + 'cursor': true, + 'defs': true, + 'desc': true, + 'ellipse': true, + 'feBlend': true, + 'feColorMatrix': true, + 'feComponentTransfer': true, + 'feComposite': true, + 'feConvolveMatrix': true, + 'feDiffuseLighting': true, + 'feDisplacementMap': true, + 'feDistantLight': true, + 'feFlood': true, + 'feFuncA': true, + 'feFuncB': true, + 'feFuncG': true, + 'feFuncR': true, + 'feGaussianBlur': true, + 'feImage': true, + 'feMerge': true, + 'feMergeNode': true, + 'feMorphology': true, + 'feOffset': true, + 'fePointLight': true, + 'feSpecularLighting': true, + 'feSpotLight': true, + 'feTile': true, + 'feTurbulence': true, + 'filter': true, + 'font': true, + 'font-face': true, + 'font-face-format': true, + 'font-face-name': true, + 'font-face-src': true, + 'font-face-uri': true, + 'foreignObject': true, + 'g': true, + 'glyph': true, + 'glyphRef': true, + 'hkern': true, + 'image': true, + 'line': true, + 'linearGradient': true, + 'marker': true, + 'mask': true, + 'metadata': true, + 'missing-glyph': true, + 'mpath': true, + 'path': true, + 'pattern': true, + 'polygon': true, + 'polyline': true, + 'radialGradient': true, + 'rect': true, + 'set': true, + 'stop': true, + 'style': true, + 'svg': true, + 'switch': true, + 'symbol': true, + 'text': true, + 'textPath': true, + 'title': true, + 'tref': true, + 'tspan': true, + 'use': true, + 'view': true, + 'vkern': true +}); + +const SVG_ATTR_NAMESPACES = CONST_EXPR({'href': XLINK_NAMESPACE}); + export abstract class DomRenderer extends Renderer implements NodeFactory { abstract registerComponentTemplate(templateId: number, commands: RenderTemplateCmd[], styles: string[], nativeShadow: boolean); @@ -271,17 +357,25 @@ export class DomRenderer_ extends DomRenderer { wtfLeave(s); } createElement(name: string, attrNameAndValues: string[]): Node { - var el = DOM.createElement(name); - this._setAttributes(el, attrNameAndValues); + var isSvg = SVG_ELEMENT_NAMES[name] == true; + var el = isSvg ? DOM.createElementNS(SVG_NAMESPACE, name) : DOM.createElement(name); + this._setAttributes(el, attrNameAndValues, isSvg); return el; } mergeElement(existing: Node, attrNameAndValues: string[]) { DOM.clearNodes(existing); - this._setAttributes(existing, attrNameAndValues); + this._setAttributes(existing, attrNameAndValues, false); } - private _setAttributes(node: Node, attrNameAndValues: string[]) { + private _setAttributes(node: Node, attrNameAndValues: string[], isSvg: boolean) { for (var attrIdx = 0; attrIdx < attrNameAndValues.length; attrIdx += 2) { - DOM.setAttribute(node, attrNameAndValues[attrIdx], attrNameAndValues[attrIdx + 1]); + var attrName = attrNameAndValues[attrIdx]; + var attrValue = attrNameAndValues[attrIdx + 1]; + var attrNs = isSvg ? SVG_ATTR_NAMESPACES[attrName] : null; + if (isPresent(attrNs)) { + DOM.setAttributeNS(node, XLINK_NAMESPACE, attrName, attrValue); + } else { + DOM.setAttribute(node, attrName, attrValue); + } } } createRootContentInsertionPoint(): Node { diff --git a/modules/angular2/test/core/linker/integration_spec.ts b/modules/angular2/test/core/linker/integration_spec.ts index 910e07de5c..26f1790c7e 100644 --- a/modules/angular2/test/core/linker/integration_spec.ts +++ b/modules/angular2/test/core/linker/integration_spec.ts @@ -1741,6 +1741,29 @@ export function main() { }); })); }); + + if (DOM.supportsDOMEvents()) { + describe('svg', () => { + it('should support svg elements', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MyComp, new ViewMetadata({template: ''})) + .createAsync(MyComp) + .then((rootTC) => { + var el = rootTC.debugElement.nativeElement; + var svg = DOM.childNodes(el)[0]; + var g = DOM.childNodes(svg)[0]; + expect(DOM.getProperty(svg, 'namespaceURI')) + .toEqual('http://www.w3.org/2000/svg'); + expect(DOM.getProperty(g, 'namespaceURI')) + .toEqual('http://www.w3.org/2000/svg'); + + async.done(); + }); + })); + + }); + } }); }