2 * @class Ext.draw.engine.Svg
3 * @extends Ext.draw.Surface
4 * Provides specific methods to draw with SVG.
6 Ext.define('Ext.draw.engine.Svg', {
8 /* Begin Definitions */
10 extend: 'Ext.draw.Surface',
12 requires: ['Ext.draw.Draw', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.core.Element'],
20 xlink: "http:/" + "/www.w3.org/1999/xlink",
27 lineWidth: "stroke-width",
28 fillOpacity: "fill-opacity",
29 strokeOpacity: "stroke-opacity",
30 strokeLinejoin: "stroke-linejoin"
43 "stroke-opacity": null
55 "stroke-opacity": null
69 "stroke-opacity": null
74 "text-anchor": "start",
84 "stroke-opacity": null
93 "stroke-opacity": null
100 preserveAspectRatio: "none",
105 createSvgElement: function(type, attrs) {
106 var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type),
110 el.setAttribute(key, String(attrs[key]));
116 createSpriteElement: function(sprite) {
117 // Create svg element and append to the DOM.
118 var el = this.createSvgElement(sprite.type);
121 el.style.webkitTapHighlightColor = "rgba(0,0,0,0)";
123 sprite.el = Ext.get(el);
124 this.applyZIndex(sprite); //performs the insertion
125 sprite.matrix = Ext.create('Ext.draw.Matrix');
130 sprite.fireEvent("render", sprite);
134 getBBox: function (sprite, isWithoutTransform) {
135 var realPath = this["getPath" + sprite.type](sprite);
136 if (isWithoutTransform) {
137 sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
138 return sprite.bbox.plain;
140 sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
141 return sprite.bbox.transform;
144 getBBoxText: function (sprite) {
146 bb, height, width, i, ln, el;
148 if (sprite && sprite.el) {
154 // Firefox 3.0.x plays badly here
156 bbox = {x: bbox.x, y: Infinity, width: 0, height: 0};
157 ln = el.getNumberOfChars();
158 for (i = 0; i < ln; i++) {
159 bb = el.getExtentOfChar(i);
160 bbox.y = Math.min(bb.y, bbox.y);
161 height = bb.y + bb.height - bbox.y;
162 bbox.height = Math.max(bbox.height, height);
163 width = bb.x + bb.width - bbox.x;
164 bbox.width = Math.max(bbox.width, width);
171 Ext.get(this.el).hide();
175 Ext.get(this.el).show();
178 hidePrim: function(sprite) {
179 this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
182 showPrim: function(sprite) {
183 this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
186 getDefs: function() {
187 return this._defs || (this._defs = this.createSvgElement("defs"));
190 transform: function(sprite) {
192 matrix = Ext.create('Ext.draw.Matrix'),
193 transforms = sprite.transformations,
194 transformsLength = transforms.length,
198 for (; i < transformsLength; i++) {
199 transform = transforms[i];
200 type = transform.type;
201 if (type == "translate") {
202 matrix.translate(transform.x, transform.y);
204 else if (type == "rotate") {
205 matrix.rotate(transform.degrees, transform.x, transform.y);
207 else if (type == "scale") {
208 matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
211 sprite.matrix = matrix;
212 sprite.el.set({transform: matrix.toSvg()});
215 setSize: function(w, h) {
229 me.callParent([w, h]);
233 * Get the region for the surface's canvas area
234 * @returns {Ext.util.Region}
236 getRegion: function() {
237 // Mozilla requires using the background rect because the svg element returns an
238 // incorrect region. Webkit gives no region for the rect and must use the svg element.
239 var svgXY = this.el.getXY(),
240 rectXY = this.bgRect.getXY(),
242 x = max(svgXY[0], rectXY[0]),
243 y = max(svgXY[1], rectXY[1]);
247 right: x + this.width,
248 bottom: y + this.height
252 onRemove: function(sprite) {
257 this.callParent(arguments);
260 setViewBox: function(x, y, width, height) {
261 if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
262 this.callParent(arguments);
263 this.el.dom.setAttribute("viewBox", [x, y, width, height].join(" "));
267 render: function (container) {
270 var width = me.width || 10,
271 height = me.height || 10,
272 el = me.createSvgElement('svg', {
273 xmlns: "http:/" + "/www.w3.org/2000/svg",
280 // Create a rect that is always the same size as the svg root; this serves 2 purposes:
281 // (1) It allows mouse events to be fired over empty areas in Webkit, and (2) we can
282 // use it rather than the svg element for retrieving the correct client rect of the
283 // surface in Mozilla (see https://bugzilla.mozilla.org/show_bug.cgi?id=530985)
284 bgRect = me.createSvgElement("rect", {
294 // Rect that we will show/hide to fix old WebKit bug with rendering issues.
295 webkitRect = me.createSvgElement("rect", {
304 el.appendChild(defs);
306 el.appendChild(webkitRect);
308 el.appendChild(bgRect);
309 container.appendChild(el);
311 me.bgRect = Ext.get(bgRect);
313 me.webkitRect = Ext.get(webkitRect);
314 me.webkitRect.hide();
318 mouseup: me.onMouseUp,
319 mousedown: me.onMouseDown,
320 mouseover: me.onMouseOver,
321 mouseout: me.onMouseOut,
322 mousemove: me.onMouseMove,
323 mouseenter: me.onMouseEnter,
324 mouseleave: me.onMouseLeave,
332 onMouseEnter: function(e) {
333 if (this.el.parent().getRegion().contains(e.getPoint())) {
334 this.fireEvent('mouseenter', e);
339 onMouseLeave: function(e) {
340 if (!this.el.parent().getRegion().contains(e.getPoint())) {
341 this.fireEvent('mouseleave', e);
344 // @private - Normalize a delegated single event from the main container to each sprite and sprite group
345 processEvent: function(name, e) {
346 var target = e.getTarget(),
347 surface = this.surface,
350 this.fireEvent(name, e);
351 // We wrap text types in a tspan, sprite is the parent.
352 if (target.nodeName == "tspan" && target.parentNode) {
353 target = target.parentNode;
355 sprite = this.items.get(target.id);
357 sprite.fireEvent(name, sprite, e);
361 /* @private - Wrap SVG text inside a tspan to allow for line wrapping. In addition this normallizes
362 * the baseline for text the vertical middle of the text to be the same as VML.
364 tuneText: function (sprite, attrs) {
365 var el = sprite.el.dom,
367 height, tspan, text, i, ln, texts, factor;
369 if (attrs.hasOwnProperty("text")) {
370 tspans = this.setText(sprite, attrs.text);
372 // Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2)
374 height = this.getBBoxText(sprite).height;
375 for (i = 0, ln = tspans.length; i < ln; i++) {
376 // The text baseline for FireFox 3.0 and 3.5 is different than other SVG implementations
377 // so we are going to normalize that here
378 factor = (Ext.isFF3_0 || Ext.isFF3_5) ? 2 : 4;
379 tspans[i].setAttribute("dy", i ? height * 1.2 : height / factor);
385 setText: function(sprite, textString) {
388 x = el.getAttribute("x"),
390 height, tspan, text, i, ln, texts;
392 while (el.firstChild) {
393 el.removeChild(el.firstChild);
395 // Wrap each row into tspan to emulate rows
396 texts = String(textString).split("\n");
397 for (i = 0, ln = texts.length; i < ln; i++) {
400 tspan = me.createSvgElement("tspan");
401 tspan.appendChild(document.createTextNode(Ext.htmlDecode(text)));
402 tspan.setAttribute("x", x);
403 el.appendChild(tspan);
410 renderAll: function() {
411 this.items.each(this.renderItem, this);
414 renderItem: function (sprite) {
419 this.createSpriteElement(sprite);
421 if (sprite.zIndexDirty) {
422 this.applyZIndex(sprite);
425 this.applyAttrs(sprite);
426 this.applyTransformations(sprite);
430 redraw: function(sprite) {
431 sprite.dirty = sprite.zIndexDirty = true;
432 this.renderItem(sprite);
435 applyAttrs: function (sprite) {
438 group = sprite.group,
440 groups, i, ln, attrs, font, key, style, name, rect;
443 groups = [].concat(group);
445 for (i = 0; i < ln; i++) {
447 me.getGroup(group).add(sprite);
451 attrs = me.scrubAttrs(sprite) || {};
453 // if (sprite.dirtyPath) {
454 sprite.bbox.plain = 0;
455 sprite.bbox.transform = 0;
456 if (sprite.type == "circle" || sprite.type == "ellipse") {
457 attrs.cx = attrs.cx || attrs.x;
458 attrs.cy = attrs.cy || attrs.y;
460 else if (sprite.type == "rect") {
461 attrs.rx = attrs.ry = attrs.r;
463 else if (sprite.type == "path" && attrs.d) {
464 attrs.d = Ext.draw.Draw.pathToString(Ext.draw.Draw.pathToAbsolute(attrs.d));
467 sprite.dirtyPath = false;
473 if (attrs['clip-rect']) {
474 me.setClip(sprite, attrs);
475 delete attrs['clip-rect'];
477 if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) {
478 el.set({ style: "font: " + attrs.font});
479 sprite.dirtyFont = false;
481 if (sprite.type == "image") {
482 el.dom.setAttributeNS(me.xlink, "href", attrs.src);
484 Ext.applyIf(attrs, me.minDefaults[sprite.type]);
486 if (sprite.dirtyHidden) {
487 (sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
488 sprite.dirtyHidden = false;
491 if (attrs.hasOwnProperty(key) && attrs[key] != null) {
492 el.dom.setAttribute(key, attrs[key]);
495 if (sprite.type == 'text') {
496 me.tuneText(sprite, attrs);
505 sprite.dirty = false;
508 // Refreshing the view to fix bug EXTJSIV-1: rendering issue in old Safari 3
509 me.webkitRect.show();
510 setTimeout(function () {
511 me.webkitRect.hide();
516 setClip: function(sprite, params) {
518 rect = params["clip-rect"],
522 sprite.clip.parentNode.parentNode.removeChild(sprite.clip.parentNode);
524 clipEl = me.createSvgElement('clipPath');
525 clipPath = me.createSvgElement('rect');
526 clipEl.id = Ext.id(null, 'ext-clip-');
527 clipPath.setAttribute("x", rect.x);
528 clipPath.setAttribute("y", rect.y);
529 clipPath.setAttribute("width", rect.width);
530 clipPath.setAttribute("height", rect.height);
531 clipEl.appendChild(clipPath);
532 me.getDefs().appendChild(clipEl);
533 sprite.el.dom.setAttribute("clip-path", "url(#" + clipEl.id + ")");
534 sprite.clip = clipPath;
536 // if (!attrs[key]) {
537 // var clip = Ext.getDoc().dom.getElementById(sprite.el.getAttribute("clip-path").replace(/(^url\(#|\)$)/g, ""));
538 // clip && clip.parentNode.removeChild(clip);
539 // sprite.el.setAttribute("clip-path", "");
540 // delete attrss.clip;
545 * Insert or move a given sprite's element to the correct place in the DOM list for its zIndex
546 * @param {Ext.draw.Sprite} sprite
548 applyZIndex: function(sprite) {
549 var idx = this.normalizeSpriteCollection(sprite),
552 if (this.el.dom.childNodes[idx + 2] !== el.dom) { //shift by 2 to account for defs and bg rect
554 // Find the first previous sprite which has its DOM element created already
556 prevEl = this.items.getAt(--idx).el;
557 } while (!prevEl && idx > 0);
559 el.insertAfter(prevEl || this.bgRect);
561 sprite.zIndexDirty = false;
564 createItem: function (config) {
565 var sprite = Ext.create('Ext.draw.Sprite', config);
566 sprite.surface = this;
570 addGradient: function(gradient) {
571 gradient = Ext.draw.Draw.parseGradient(gradient);
572 var ln = gradient.stops.length,
573 vector = gradient.vector,
578 if (gradient.type == "linear") {
579 gradientEl = this.createSvgElement("linearGradient");
580 gradientEl.setAttribute("x1", vector[0]);
581 gradientEl.setAttribute("y1", vector[1]);
582 gradientEl.setAttribute("x2", vector[2]);
583 gradientEl.setAttribute("y2", vector[3]);
586 gradientEl = this.createSvgElement("radialGradient");
587 gradientEl.setAttribute("cx", gradient.centerX);
588 gradientEl.setAttribute("cy", gradient.centerY);
589 gradientEl.setAttribute("r", gradient.radius);
590 if (Ext.isNumber(gradient.focalX) && Ext.isNumber(gradient.focalY)) {
591 gradientEl.setAttribute("fx", gradient.focalX);
592 gradientEl.setAttribute("fy", gradient.focalY);
595 gradientEl.id = gradient.id;
596 this.getDefs().appendChild(gradientEl);
598 for (i = 0; i < ln; i++) {
599 stop = gradient.stops[i];
600 stopEl = this.createSvgElement("stop");
601 stopEl.setAttribute("offset", stop.offset + "%");
602 stopEl.setAttribute("stop-color", stop.color);
603 stopEl.setAttribute("stop-opacity",stop.opacity);
604 gradientEl.appendChild(stopEl);
609 * Checks if the specified CSS class exists on this element's DOM node.
610 * @param {String} className The CSS class to check for
611 * @return {Boolean} True if the class exists, else false
613 hasCls: function(sprite, className) {
614 return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1;
617 addCls: function(sprite, className) {
623 curCls = el.getAttribute('class') || '';
624 // Separate case is for speed
625 if (!Ext.isArray(className)) {
626 if (typeof className == 'string' && !this.hasCls(sprite, className)) {
627 el.set({ 'class': curCls + ' ' + className });
631 for (i = 0, len = className.length; i < len; i++) {
633 if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) {
638 el.set({ 'class': ' ' + cls.join(' ') });
643 removeCls: function(sprite, className) {
646 curCls = el.getAttribute('class') || '',
647 i, idx, len, cls, elClasses;
648 if (!Ext.isArray(className)){
649 className = [className];
652 elClasses = curCls.replace(me.trimRe, ' ').split(me.spacesRe);
653 for (i = 0, len = className.length; i < len; i++) {
655 if (typeof cls == 'string') {
656 cls = cls.replace(me.trimRe, '');
657 idx = Ext.Array.indexOf(elClasses, cls);
659 elClasses.splice(idx, 1);
663 el.set({ 'class': elClasses.join(' ') });
667 destroy: function() {