3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.draw.engine.Svg
17 * @extends Ext.draw.Surface
18 * Provides specific methods to draw with SVG.
20 Ext.define('Ext.draw.engine.Svg', {
22 /* Begin Definitions */
24 extend: 'Ext.draw.Surface',
26 requires: ['Ext.draw.Draw', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.Element'],
34 xlink: "http:/" + "/www.w3.org/1999/xlink",
41 lineWidth: "stroke-width",
42 fillOpacity: "fill-opacity",
43 strokeOpacity: "stroke-opacity",
44 strokeLinejoin: "stroke-linejoin"
59 "stroke-opacity": null
71 "stroke-opacity": null
85 "stroke-opacity": null
90 "text-anchor": "start",
100 "stroke-opacity": null
106 "stroke-width": null,
108 "fill-opacity": null,
109 "stroke-opacity": null
116 preserveAspectRatio: "none",
121 createSvgElement: function(type, attrs) {
122 var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type),
126 el.setAttribute(key, String(attrs[key]));
132 createSpriteElement: function(sprite) {
133 // Create svg element and append to the DOM.
134 var el = this.createSvgElement(sprite.type);
137 el.style.webkitTapHighlightColor = "rgba(0,0,0,0)";
139 sprite.el = Ext.get(el);
140 this.applyZIndex(sprite); //performs the insertion
141 sprite.matrix = Ext.create('Ext.draw.Matrix');
146 sprite.fireEvent("render", sprite);
150 getBBox: function (sprite, isWithoutTransform) {
151 var realPath = this["getPath" + sprite.type](sprite);
152 if (isWithoutTransform) {
153 sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
154 return sprite.bbox.plain;
156 sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
157 return sprite.bbox.transform;
160 getBBoxText: function (sprite) {
162 bb, height, width, i, ln, el;
164 if (sprite && sprite.el) {
170 // Firefox 3.0.x plays badly here
172 bbox = {x: bbox.x, y: Infinity, width: 0, height: 0};
173 ln = el.getNumberOfChars();
174 for (i = 0; i < ln; i++) {
175 bb = el.getExtentOfChar(i);
176 bbox.y = Math.min(bb.y, bbox.y);
177 height = bb.y + bb.height - bbox.y;
178 bbox.height = Math.max(bbox.height, height);
179 width = bb.x + bb.width - bbox.x;
180 bbox.width = Math.max(bbox.width, width);
187 Ext.get(this.el).hide();
191 Ext.get(this.el).show();
194 hidePrim: function(sprite) {
195 this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
198 showPrim: function(sprite) {
199 this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
202 getDefs: function() {
203 return this._defs || (this._defs = this.createSvgElement("defs"));
206 transform: function(sprite) {
208 matrix = Ext.create('Ext.draw.Matrix'),
209 transforms = sprite.transformations,
210 transformsLength = transforms.length,
214 for (; i < transformsLength; i++) {
215 transform = transforms[i];
216 type = transform.type;
217 if (type == "translate") {
218 matrix.translate(transform.x, transform.y);
220 else if (type == "rotate") {
221 matrix.rotate(transform.degrees, transform.x, transform.y);
223 else if (type == "scale") {
224 matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
227 sprite.matrix = matrix;
228 sprite.el.set({transform: matrix.toSvg()});
231 setSize: function(w, h) {
245 me.callParent([w, h]);
249 * Get the region for the surface's canvas area
250 * @returns {Ext.util.Region}
252 getRegion: function() {
253 // Mozilla requires using the background rect because the svg element returns an
254 // incorrect region. Webkit gives no region for the rect and must use the svg element.
255 var svgXY = this.el.getXY(),
256 rectXY = this.bgRect.getXY(),
258 x = max(svgXY[0], rectXY[0]),
259 y = max(svgXY[1], rectXY[1]);
263 right: x + this.width,
264 bottom: y + this.height
268 onRemove: function(sprite) {
273 this.callParent(arguments);
276 setViewBox: function(x, y, width, height) {
277 if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
278 this.callParent(arguments);
279 this.el.dom.setAttribute("viewBox", [x, y, width, height].join(" "));
283 render: function (container) {
286 var width = me.width || 10,
287 height = me.height || 10,
288 el = me.createSvgElement('svg', {
289 xmlns: "http:/" + "/www.w3.org/2000/svg",
296 // Create a rect that is always the same size as the svg root; this serves 2 purposes:
297 // (1) It allows mouse events to be fired over empty areas in Webkit, and (2) we can
298 // use it rather than the svg element for retrieving the correct client rect of the
299 // surface in Mozilla (see https://bugzilla.mozilla.org/show_bug.cgi?id=530985)
300 bgRect = me.createSvgElement("rect", {
310 // Rect that we will show/hide to fix old WebKit bug with rendering issues.
311 webkitRect = me.createSvgElement("rect", {
320 el.appendChild(defs);
322 el.appendChild(webkitRect);
324 el.appendChild(bgRect);
325 container.appendChild(el);
327 me.bgRect = Ext.get(bgRect);
329 me.webkitRect = Ext.get(webkitRect);
330 me.webkitRect.hide();
334 mouseup: me.onMouseUp,
335 mousedown: me.onMouseDown,
336 mouseover: me.onMouseOver,
337 mouseout: me.onMouseOut,
338 mousemove: me.onMouseMove,
339 mouseenter: me.onMouseEnter,
340 mouseleave: me.onMouseLeave,
348 onMouseEnter: function(e) {
349 if (this.el.parent().getRegion().contains(e.getPoint())) {
350 this.fireEvent('mouseenter', e);
355 onMouseLeave: function(e) {
356 if (!this.el.parent().getRegion().contains(e.getPoint())) {
357 this.fireEvent('mouseleave', e);
360 // @private - Normalize a delegated single event from the main container to each sprite and sprite group
361 processEvent: function(name, e) {
362 var target = e.getTarget(),
363 surface = this.surface,
366 this.fireEvent(name, e);
367 // We wrap text types in a tspan, sprite is the parent.
368 if (target.nodeName == "tspan" && target.parentNode) {
369 target = target.parentNode;
371 sprite = this.items.get(target.id);
373 sprite.fireEvent(name, sprite, e);
377 /* @private - Wrap SVG text inside a tspan to allow for line wrapping. In addition this normallizes
378 * the baseline for text the vertical middle of the text to be the same as VML.
380 tuneText: function (sprite, attrs) {
381 var el = sprite.el.dom,
383 height, tspan, text, i, ln, texts, factor;
385 if (attrs.hasOwnProperty("text")) {
386 tspans = this.setText(sprite, attrs.text);
388 // Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2)
390 height = this.getBBoxText(sprite).height;
391 for (i = 0, ln = tspans.length; i < ln; i++) {
392 // The text baseline for FireFox 3.0 and 3.5 is different than other SVG implementations
393 // so we are going to normalize that here
394 factor = (Ext.isFF3_0 || Ext.isFF3_5) ? 2 : 4;
395 tspans[i].setAttribute("dy", i ? height * 1.2 : height / factor);
401 setText: function(sprite, textString) {
404 x = el.getAttribute("x"),
406 height, tspan, text, i, ln, texts;
408 while (el.firstChild) {
409 el.removeChild(el.firstChild);
411 // Wrap each row into tspan to emulate rows
412 texts = String(textString).split("\n");
413 for (i = 0, ln = texts.length; i < ln; i++) {
416 tspan = me.createSvgElement("tspan");
417 tspan.appendChild(document.createTextNode(Ext.htmlDecode(text)));
418 tspan.setAttribute("x", x);
419 el.appendChild(tspan);
426 renderAll: function() {
427 this.items.each(this.renderItem, this);
430 renderItem: function (sprite) {
435 this.createSpriteElement(sprite);
437 if (sprite.zIndexDirty) {
438 this.applyZIndex(sprite);
441 this.applyAttrs(sprite);
442 this.applyTransformations(sprite);
446 redraw: function(sprite) {
447 sprite.dirty = sprite.zIndexDirty = true;
448 this.renderItem(sprite);
451 applyAttrs: function (sprite) {
454 group = sprite.group,
456 parsers = me.parsers,
457 //Safari does not handle linear gradients correctly in quirksmode
458 //ref: https://bugs.webkit.org/show_bug.cgi?id=41952
460 gradientsMap = me.gradientsMap || {},
461 safariFix = Ext.isSafari && !Ext.isStrict,
462 groups, i, ln, attrs, font, key, style, name, rect;
465 groups = [].concat(group);
467 for (i = 0; i < ln; i++) {
469 me.getGroup(group).add(sprite);
473 attrs = me.scrubAttrs(sprite) || {};
475 // if (sprite.dirtyPath) {
476 sprite.bbox.plain = 0;
477 sprite.bbox.transform = 0;
478 if (sprite.type == "circle" || sprite.type == "ellipse") {
479 attrs.cx = attrs.cx || attrs.x;
480 attrs.cy = attrs.cy || attrs.y;
482 else if (sprite.type == "rect") {
483 attrs.rx = attrs.ry = attrs.r;
485 else if (sprite.type == "path" && attrs.d) {
486 attrs.d = Ext.draw.Draw.pathToString(Ext.draw.Draw.pathToAbsolute(attrs.d));
488 sprite.dirtyPath = false;
494 if (attrs['clip-rect']) {
495 me.setClip(sprite, attrs);
496 delete attrs['clip-rect'];
498 if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) {
499 el.set({ style: "font: " + attrs.font});
500 sprite.dirtyFont = false;
502 if (sprite.type == "image") {
503 el.dom.setAttributeNS(me.xlink, "href", attrs.src);
505 Ext.applyIf(attrs, me.minDefaults[sprite.type]);
507 if (sprite.dirtyHidden) {
508 (sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
509 sprite.dirtyHidden = false;
512 if (attrs.hasOwnProperty(key) && attrs[key] != null) {
513 //Safari does not handle linear gradients correctly in quirksmode
514 //ref: https://bugs.webkit.org/show_bug.cgi?id=41952
516 //if we're Safari in QuirksMode and we're applying some color attribute and the value of that
517 //attribute is a reference to a gradient then assign a plain color to that value instead of the gradient.
518 if (safariFix && ('color|stroke|fill'.indexOf(key) > -1) && (attrs[key] in gradientsMap)) {
519 attrs[key] = gradientsMap[attrs[key]];
521 if (key in parsers) {
522 el.dom.setAttribute(key, parsers[key](attrs[key], sprite, me));
524 el.dom.setAttribute(key, attrs[key]);
529 if (sprite.type == 'text') {
530 me.tuneText(sprite, attrs);
539 sprite.dirty = false;
542 // Refreshing the view to fix bug EXTJSIV-1: rendering issue in old Safari 3
543 me.webkitRect.show();
544 setTimeout(function () {
545 me.webkitRect.hide();
550 setClip: function(sprite, params) {
552 rect = params["clip-rect"],
556 sprite.clip.parentNode.parentNode.removeChild(sprite.clip.parentNode);
558 clipEl = me.createSvgElement('clipPath');
559 clipPath = me.createSvgElement('rect');
560 clipEl.id = Ext.id(null, 'ext-clip-');
561 clipPath.setAttribute("x", rect.x);
562 clipPath.setAttribute("y", rect.y);
563 clipPath.setAttribute("width", rect.width);
564 clipPath.setAttribute("height", rect.height);
565 clipEl.appendChild(clipPath);
566 me.getDefs().appendChild(clipEl);
567 sprite.el.dom.setAttribute("clip-path", "url(#" + clipEl.id + ")");
568 sprite.clip = clipPath;
570 // if (!attrs[key]) {
571 // var clip = Ext.getDoc().dom.getElementById(sprite.el.getAttribute("clip-path").replace(/(^url\(#|\)$)/g, ""));
572 // clip && clip.parentNode.removeChild(clip);
573 // sprite.el.setAttribute("clip-path", "");
574 // delete attrss.clip;
579 * Insert or move a given sprite's element to the correct place in the DOM list for its zIndex
580 * @param {Ext.draw.Sprite} sprite
582 applyZIndex: function(sprite) {
585 idx = items.indexOf(sprite),
588 if (me.el.dom.childNodes[idx + 2] !== el.dom) { //shift by 2 to account for defs and bg rect
590 // Find the first previous sprite which has its DOM element created already
592 prevEl = items.getAt(--idx).el;
593 } while (!prevEl && idx > 0);
595 el.insertAfter(prevEl || me.bgRect);
597 sprite.zIndexDirty = false;
600 createItem: function (config) {
601 var sprite = Ext.create('Ext.draw.Sprite', config);
602 sprite.surface = this;
606 addGradient: function(gradient) {
607 gradient = Ext.draw.Draw.parseGradient(gradient);
609 ln = gradient.stops.length,
610 vector = gradient.vector,
611 //Safari does not handle linear gradients correctly in quirksmode
612 //ref: https://bugs.webkit.org/show_bug.cgi?id=41952
614 usePlain = Ext.isSafari && !Ext.isStrict,
615 gradientEl, stop, stopEl, i, gradientsMap;
617 gradientsMap = me.gradientsMap || {};
620 if (gradient.type == "linear") {
621 gradientEl = me.createSvgElement("linearGradient");
622 gradientEl.setAttribute("x1", vector[0]);
623 gradientEl.setAttribute("y1", vector[1]);
624 gradientEl.setAttribute("x2", vector[2]);
625 gradientEl.setAttribute("y2", vector[3]);
628 gradientEl = me.createSvgElement("radialGradient");
629 gradientEl.setAttribute("cx", gradient.centerX);
630 gradientEl.setAttribute("cy", gradient.centerY);
631 gradientEl.setAttribute("r", gradient.radius);
632 if (Ext.isNumber(gradient.focalX) && Ext.isNumber(gradient.focalY)) {
633 gradientEl.setAttribute("fx", gradient.focalX);
634 gradientEl.setAttribute("fy", gradient.focalY);
637 gradientEl.id = gradient.id;
638 me.getDefs().appendChild(gradientEl);
639 for (i = 0; i < ln; i++) {
640 stop = gradient.stops[i];
641 stopEl = me.createSvgElement("stop");
642 stopEl.setAttribute("offset", stop.offset + "%");
643 stopEl.setAttribute("stop-color", stop.color);
644 stopEl.setAttribute("stop-opacity",stop.opacity);
645 gradientEl.appendChild(stopEl);
648 gradientsMap['url(#' + gradient.id + ')'] = gradient.stops[0].color;
650 me.gradientsMap = gradientsMap;
654 * Checks if the specified CSS class exists on this element's DOM node.
655 * @param {String} className The CSS class to check for
656 * @return {Boolean} True if the class exists, else false
658 hasCls: function(sprite, className) {
659 return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1;
662 addCls: function(sprite, className) {
668 curCls = el.getAttribute('class') || '';
669 // Separate case is for speed
670 if (!Ext.isArray(className)) {
671 if (typeof className == 'string' && !this.hasCls(sprite, className)) {
672 el.set({ 'class': curCls + ' ' + className });
676 for (i = 0, len = className.length; i < len; i++) {
678 if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) {
683 el.set({ 'class': ' ' + cls.join(' ') });
688 removeCls: function(sprite, className) {
691 curCls = el.getAttribute('class') || '',
692 i, idx, len, cls, elClasses;
693 if (!Ext.isArray(className)){
694 className = [className];
697 elClasses = curCls.replace(me.trimRe, ' ').split(me.spacesRe);
698 for (i = 0, len = className.length; i < len; i++) {
700 if (typeof cls == 'string') {
701 cls = cls.replace(me.trimRe, '');
702 idx = Ext.Array.indexOf(elClasses, cls);
704 Ext.Array.erase(elClasses, idx, 1);
708 el.set({ 'class': elClasses.join(' ') });
712 destroy: function() {