/** * @class Ext.draw.engine.Svg * @extends Ext.draw.Surface * Provides specific methods to draw with SVG. */ Ext.define('Ext.draw.engine.Svg', { /* Begin Definitions */ extend: 'Ext.draw.Surface', requires: ['Ext.draw.Draw', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.core.Element'], /* End Definitions */ engine: 'Svg', trimRe: /^\s+|\s+$/g, spacesRe: /\s+/, xlink: "http:/" + "/www.w3.org/1999/xlink", translateAttrs: { radius: "r", radiusX: "rx", radiusY: "ry", path: "d", lineWidth: "stroke-width", fillOpacity: "fill-opacity", strokeOpacity: "stroke-opacity", strokeLinejoin: "stroke-linejoin" }, minDefaults: { circle: { cx: 0, cy: 0, r: 0, fill: "none", stroke: null, "stroke-width": null, opacity: null, "fill-opacity": null, "stroke-opacity": null }, ellipse: { cx: 0, cy: 0, rx: 0, ry: 0, fill: "none", stroke: null, "stroke-width": null, opacity: null, "fill-opacity": null, "stroke-opacity": null }, rect: { x: 0, y: 0, width: 0, height: 0, rx: 0, ry: 0, fill: "none", stroke: null, "stroke-width": null, opacity: null, "fill-opacity": null, "stroke-opacity": null }, text: { x: 0, y: 0, "text-anchor": "start", "font-family": null, "font-size": null, "font-weight": null, "font-style": null, fill: "#000", stroke: null, "stroke-width": null, opacity: null, "fill-opacity": null, "stroke-opacity": null }, path: { d: "M0,0", fill: "none", stroke: null, "stroke-width": null, opacity: null, "fill-opacity": null, "stroke-opacity": null }, image: { x: 0, y: 0, width: 0, height: 0, preserveAspectRatio: "none", opacity: null } }, createSvgElement: function(type, attrs) { var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type), key; if (attrs) { for (key in attrs) { el.setAttribute(key, String(attrs[key])); } } return el; }, createSpriteElement: function(sprite) { // Create svg element and append to the DOM. var el = this.createSvgElement(sprite.type); el.id = sprite.id; if (el.style) { el.style.webkitTapHighlightColor = "rgba(0,0,0,0)"; } sprite.el = Ext.get(el); this.applyZIndex(sprite); //performs the insertion sprite.matrix = Ext.create('Ext.draw.Matrix'); sprite.bbox = { plain: 0, transform: 0 }; sprite.fireEvent("render", sprite); return el; }, getBBox: function (sprite, isWithoutTransform) { var realPath = this["getPath" + sprite.type](sprite); if (isWithoutTransform) { sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath); return sprite.bbox.plain; } sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix)); return sprite.bbox.transform; }, getBBoxText: function (sprite) { var bbox = {}, bb, height, width, i, ln, el; if (sprite && sprite.el) { el = sprite.el.dom; try { bbox = el.getBBox(); return bbox; } catch(e) { // Firefox 3.0.x plays badly here } bbox = {x: bbox.x, y: Infinity, width: 0, height: 0}; ln = el.getNumberOfChars(); for (i = 0; i < ln; i++) { bb = el.getExtentOfChar(i); bbox.y = Math.min(bb.y, bbox.y); height = bb.y + bb.height - bbox.y; bbox.height = Math.max(bbox.height, height); width = bb.x + bb.width - bbox.x; bbox.width = Math.max(bbox.width, width); } return bbox; } }, hide: function() { Ext.get(this.el).hide(); }, show: function() { Ext.get(this.el).show(); }, hidePrim: function(sprite) { this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility'); }, showPrim: function(sprite) { this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility'); }, getDefs: function() { return this._defs || (this._defs = this.createSvgElement("defs")); }, transform: function(sprite) { var me = this, matrix = Ext.create('Ext.draw.Matrix'), transforms = sprite.transformations, transformsLength = transforms.length, i = 0, transform, type; for (; i < transformsLength; i++) { transform = transforms[i]; type = transform.type; if (type == "translate") { matrix.translate(transform.x, transform.y); } else if (type == "rotate") { matrix.rotate(transform.degrees, transform.x, transform.y); } else if (type == "scale") { matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY); } } sprite.matrix = matrix; sprite.el.set({transform: matrix.toSvg()}); }, setSize: function(w, h) { var me = this, el = me.el; w = +w || me.width; h = +h || me.height; me.width = w; me.height = h; el.setSize(w, h); el.set({ width: w, height: h }); me.callParent([w, h]); }, /** * Get the region for the surface's canvas area * @returns {Ext.util.Region} */ getRegion: function() { // Mozilla requires using the background rect because the svg element returns an // incorrect region. Webkit gives no region for the rect and must use the svg element. var svgXY = this.el.getXY(), rectXY = this.bgRect.getXY(), max = Math.max, x = max(svgXY[0], rectXY[0]), y = max(svgXY[1], rectXY[1]); return { left: x, top: y, right: x + this.width, bottom: y + this.height }; }, onRemove: function(sprite) { if (sprite.el) { sprite.el.remove(); delete sprite.el; } this.callParent(arguments); }, setViewBox: function(x, y, width, height) { if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) { this.callParent(arguments); this.el.dom.setAttribute("viewBox", [x, y, width, height].join(" ")); } }, render: function (container) { var me = this; if (!me.el) { var width = me.width || 10, height = me.height || 10, el = me.createSvgElement('svg', { xmlns: "http:/" + "/www.w3.org/2000/svg", version: 1.1, width: width, height: height }), defs = me.getDefs(), // Create a rect that is always the same size as the svg root; this serves 2 purposes: // (1) It allows mouse events to be fired over empty areas in Webkit, and (2) we can // use it rather than the svg element for retrieving the correct client rect of the // surface in Mozilla (see https://bugzilla.mozilla.org/show_bug.cgi?id=530985) bgRect = me.createSvgElement("rect", { width: "100%", height: "100%", fill: "#000", stroke: "none", opacity: 0 }), webkitRect; if (Ext.isSafari3) { // Rect that we will show/hide to fix old WebKit bug with rendering issues. webkitRect = me.createSvgElement("rect", { x: -10, y: -10, width: "110%", height: "110%", fill: "none", stroke: "#000" }); } el.appendChild(defs); if (Ext.isSafari3) { el.appendChild(webkitRect); } el.appendChild(bgRect); container.appendChild(el); me.el = Ext.get(el); me.bgRect = Ext.get(bgRect); if (Ext.isSafari3) { me.webkitRect = Ext.get(webkitRect); me.webkitRect.hide(); } me.el.on({ scope: me, mouseup: me.onMouseUp, mousedown: me.onMouseDown, mouseover: me.onMouseOver, mouseout: me.onMouseOut, mousemove: me.onMouseMove, mouseenter: me.onMouseEnter, mouseleave: me.onMouseLeave, click: me.onClick }); } me.renderAll(); }, // private onMouseEnter: function(e) { if (this.el.parent().getRegion().contains(e.getPoint())) { this.fireEvent('mouseenter', e); } }, // private onMouseLeave: function(e) { if (!this.el.parent().getRegion().contains(e.getPoint())) { this.fireEvent('mouseleave', e); } }, // @private - Normalize a delegated single event from the main container to each sprite and sprite group processEvent: function(name, e) { var target = e.getTarget(), surface = this.surface, sprite; this.fireEvent(name, e); // We wrap text types in a tspan, sprite is the parent. if (target.nodeName == "tspan" && target.parentNode) { target = target.parentNode; } sprite = this.items.get(target.id); if (sprite) { sprite.fireEvent(name, sprite, e); } }, /* @private - Wrap SVG text inside a tspan to allow for line wrapping. In addition this normallizes * the baseline for text the vertical middle of the text to be the same as VML. */ tuneText: function (sprite, attrs) { var el = sprite.el.dom, tspans = [], height, tspan, text, i, ln, texts, factor; if (attrs.hasOwnProperty("text")) { tspans = this.setText(sprite, attrs.text); } // Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2) if (tspans.length) { height = this.getBBoxText(sprite).height; for (i = 0, ln = tspans.length; i < ln; i++) { // The text baseline for FireFox 3.0 and 3.5 is different than other SVG implementations // so we are going to normalize that here factor = (Ext.isFF3_0 || Ext.isFF3_5) ? 2 : 4; tspans[i].setAttribute("dy", i ? height * 1.2 : height / factor); } sprite.dirty = true; } }, setText: function(sprite, textString) { var me = this, el = sprite.el.dom, x = el.getAttribute("x"), tspans = [], height, tspan, text, i, ln, texts; while (el.firstChild) { el.removeChild(el.firstChild); } // Wrap each row into tspan to emulate rows texts = String(textString).split("\n"); for (i = 0, ln = texts.length; i < ln; i++) { text = texts[i]; if (text) { tspan = me.createSvgElement("tspan"); tspan.appendChild(document.createTextNode(Ext.htmlDecode(text))); tspan.setAttribute("x", x); el.appendChild(tspan); tspans[i] = tspan; } } return tspans; }, renderAll: function() { this.items.each(this.renderItem, this); }, renderItem: function (sprite) { if (!this.el) { return; } if (!sprite.el) { this.createSpriteElement(sprite); } if (sprite.zIndexDirty) { this.applyZIndex(sprite); } if (sprite.dirty) { this.applyAttrs(sprite); this.applyTransformations(sprite); } }, redraw: function(sprite) { sprite.dirty = sprite.zIndexDirty = true; this.renderItem(sprite); }, applyAttrs: function (sprite) { var me = this, el = sprite.el, group = sprite.group, sattr = sprite.attr, groups, i, ln, attrs, font, key, style, name, rect; if (group) { groups = [].concat(group); ln = groups.length; for (i = 0; i < ln; i++) { group = groups[i]; me.getGroup(group).add(sprite); } delete sprite.group; } attrs = me.scrubAttrs(sprite) || {}; // if (sprite.dirtyPath) { sprite.bbox.plain = 0; sprite.bbox.transform = 0; if (sprite.type == "circle" || sprite.type == "ellipse") { attrs.cx = attrs.cx || attrs.x; attrs.cy = attrs.cy || attrs.y; } else if (sprite.type == "rect") { attrs.rx = attrs.ry = attrs.r; } else if (sprite.type == "path" && attrs.d) { attrs.d = Ext.draw.Draw.pathToAbsolute(attrs.d); } sprite.dirtyPath = false; // } if (attrs['clip-rect']) { me.setClip(sprite, attrs); delete attrs['clip-rect']; } if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) { el.set({ style: "font: " + attrs.font}); sprite.dirtyFont = false; } if (sprite.type == "image") { el.dom.setAttributeNS(me.xlink, "href", attrs.src); } Ext.applyIf(attrs, me.minDefaults[sprite.type]); if (sprite.dirtyHidden) { (sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite); sprite.dirtyHidden = false; } for (key in attrs) { if (attrs.hasOwnProperty(key) && attrs[key] != null) { el.dom.setAttribute(key, String(attrs[key])); } } if (sprite.type == 'text') { me.tuneText(sprite, attrs); } //set styles style = sattr.style; if (style) { el.setStyle(style); } sprite.dirty = false; if (Ext.isSafari3) { // Refreshing the view to fix bug EXTJSIV-1: rendering issue in old Safari 3 me.webkitRect.show(); setTimeout(function () { me.webkitRect.hide(); }); } }, setClip: function(sprite, params) { var me = this, rect = params["clip-rect"], clipEl, clipPath; if (rect) { if (sprite.clip) { sprite.clip.parentNode.parentNode.removeChild(sprite.clip.parentNode); } clipEl = me.createSvgElement('clipPath'); clipPath = me.createSvgElement('rect'); clipEl.id = Ext.id(null, 'ext-clip-'); clipPath.setAttribute("x", rect.x); clipPath.setAttribute("y", rect.y); clipPath.setAttribute("width", rect.width); clipPath.setAttribute("height", rect.height); clipEl.appendChild(clipPath); me.getDefs().appendChild(clipEl); sprite.el.dom.setAttribute("clip-path", "url(#" + clipEl.id + ")"); sprite.clip = clipPath; } // if (!attrs[key]) { // var clip = Ext.getDoc().dom.getElementById(sprite.el.getAttribute("clip-path").replace(/(^url\(#|\)$)/g, "")); // clip && clip.parentNode.removeChild(clip); // sprite.el.setAttribute("clip-path", ""); // delete attrss.clip; // } }, /** * Insert or move a given sprite's element to the correct place in the DOM list for its zIndex * @param {Ext.draw.Sprite} sprite */ applyZIndex: function(sprite) { var idx = this.normalizeSpriteCollection(sprite), el = sprite.el, prevEl; if (this.el.dom.childNodes[idx + 2] !== el.dom) { //shift by 2 to account for defs and bg rect if (idx > 0) { // Find the first previous sprite which has its DOM element created already do { prevEl = this.items.getAt(--idx).el; } while (!prevEl && idx > 0); } el.insertAfter(prevEl || this.bgRect); } sprite.zIndexDirty = false; }, createItem: function (config) { var sprite = Ext.create('Ext.draw.Sprite', config); sprite.surface = this; return sprite; }, addGradient: function(gradient) { gradient = Ext.draw.Draw.parseGradient(gradient); var ln = gradient.stops.length, vector = gradient.vector, gradientEl, stop, stopEl, i; if (gradient.type == "linear") { gradientEl = this.createSvgElement("linearGradient"); gradientEl.setAttribute("x1", vector[0]); gradientEl.setAttribute("y1", vector[1]); gradientEl.setAttribute("x2", vector[2]); gradientEl.setAttribute("y2", vector[3]); } else { gradientEl = this.createSvgElement("radialGradient"); gradientEl.setAttribute("cx", gradient.centerX); gradientEl.setAttribute("cy", gradient.centerY); gradientEl.setAttribute("r", gradient.radius); if (Ext.isNumber(gradient.focalX) && Ext.isNumber(gradient.focalY)) { gradientEl.setAttribute("fx", gradient.focalX); gradientEl.setAttribute("fy", gradient.focalY); } } gradientEl.id = gradient.id; this.getDefs().appendChild(gradientEl); for (i = 0; i < ln; i++) { stop = gradient.stops[i]; stopEl = this.createSvgElement("stop"); stopEl.setAttribute("offset", stop.offset + "%"); stopEl.setAttribute("stop-color", stop.color); stopEl.setAttribute("stop-opacity",stop.opacity); gradientEl.appendChild(stopEl); } }, /** * Checks if the specified CSS class exists on this element's DOM node. * @param {String} className The CSS class to check for * @return {Boolean} True if the class exists, else false */ hasCls: function(sprite, className) { return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1; }, addCls: function(sprite, className) { var el = sprite.el, i, len, v, cls = [], curCls = el.getAttribute('class') || ''; // Separate case is for speed if (!Ext.isArray(className)) { if (typeof className == 'string' && !this.hasCls(sprite, className)) { el.set({ 'class': curCls + ' ' + className }); } } else { for (i = 0, len = className.length; i < len; i++) { v = className[i]; if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) { cls.push(v); } } if (cls.length) { el.set({ 'class': ' ' + cls.join(' ') }); } } }, removeCls: function(sprite, className) { var me = this, el = sprite.el, curCls = el.getAttribute('class') || '', i, idx, len, cls, elClasses; if (!Ext.isArray(className)){ className = [className]; } if (curCls) { elClasses = curCls.replace(me.trimRe, ' ').split(me.spacesRe); for (i = 0, len = className.length; i < len; i++) { cls = className[i]; if (typeof cls == 'string') { cls = cls.replace(me.trimRe, ''); idx = Ext.Array.indexOf(elClasses, cls); if (idx != -1) { elClasses.splice(idx, 1); } } } el.set({ 'class': elClasses.join(' ') }); } }, destroy: function() { var me = this; me.callParent(); if (me.el) { me.el.remove(); } delete me.el; } });