Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / draw / engine / Svg.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
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.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.draw.engine.Svg
17  * @extends Ext.draw.Surface
18  * Provides specific methods to draw with SVG.
19  */
20 Ext.define('Ext.draw.engine.Svg', {
21
22     /* Begin Definitions */
23
24     extend: 'Ext.draw.Surface',
25
26     requires: ['Ext.draw.Draw', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.Element'],
27
28     /* End Definitions */
29
30     engine: 'Svg',
31
32     trimRe: /^\s+|\s+$/g,
33     spacesRe: /\s+/,
34     xlink: "http:/" + "/www.w3.org/1999/xlink",
35
36     translateAttrs: {
37         radius: "r",
38         radiusX: "rx",
39         radiusY: "ry",
40         path: "d",
41         lineWidth: "stroke-width",
42         fillOpacity: "fill-opacity",
43         strokeOpacity: "stroke-opacity",
44         strokeLinejoin: "stroke-linejoin"
45     },
46     
47     parsers: {},
48
49     minDefaults: {
50         circle: {
51             cx: 0,
52             cy: 0,
53             r: 0,
54             fill: "none",
55             stroke: null,
56             "stroke-width": null,
57             opacity: null,
58             "fill-opacity": null,
59             "stroke-opacity": null
60         },
61         ellipse: {
62             cx: 0,
63             cy: 0,
64             rx: 0,
65             ry: 0,
66             fill: "none",
67             stroke: null,
68             "stroke-width": null,
69             opacity: null,
70             "fill-opacity": null,
71             "stroke-opacity": null
72         },
73         rect: {
74             x: 0,
75             y: 0,
76             width: 0,
77             height: 0,
78             rx: 0,
79             ry: 0,
80             fill: "none",
81             stroke: null,
82             "stroke-width": null,
83             opacity: null,
84             "fill-opacity": null,
85             "stroke-opacity": null
86         },
87         text: {
88             x: 0,
89             y: 0,
90             "text-anchor": "start",
91             "font-family": null,
92             "font-size": null,
93             "font-weight": null,
94             "font-style": null,
95             fill: "#000",
96             stroke: null,
97             "stroke-width": null,
98             opacity: null,
99             "fill-opacity": null,
100             "stroke-opacity": null
101         },
102         path: {
103             d: "M0,0",
104             fill: "none",
105             stroke: null,
106             "stroke-width": null,
107             opacity: null,
108             "fill-opacity": null,
109             "stroke-opacity": null
110         },
111         image: {
112             x: 0,
113             y: 0,
114             width: 0,
115             height: 0,
116             preserveAspectRatio: "none",
117             opacity: null
118         }
119     },
120
121     createSvgElement: function(type, attrs) {
122         var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type),
123             key;
124         if (attrs) {
125             for (key in attrs) {
126                 el.setAttribute(key, String(attrs[key]));
127             }
128         }
129         return el;
130     },
131
132     createSpriteElement: function(sprite) {
133         // Create svg element and append to the DOM.
134         var el = this.createSvgElement(sprite.type);
135         el.id = sprite.id;
136         if (el.style) {
137             el.style.webkitTapHighlightColor = "rgba(0,0,0,0)";
138         }
139         sprite.el = Ext.get(el);
140         this.applyZIndex(sprite); //performs the insertion
141         sprite.matrix = Ext.create('Ext.draw.Matrix');
142         sprite.bbox = {
143             plain: 0,
144             transform: 0
145         };
146         sprite.fireEvent("render", sprite);
147         return el;
148     },
149
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;
155         }
156         sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
157         return sprite.bbox.transform;
158     },
159     
160     getBBoxText: function (sprite) {
161         var bbox = {},
162             bb, height, width, i, ln, el;
163
164         if (sprite && sprite.el) {
165             el = sprite.el.dom;
166             try {
167                 bbox = el.getBBox();
168                 return bbox;
169             } catch(e) {
170                 // Firefox 3.0.x plays badly here
171             }
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);
181             }
182             return bbox;
183         }
184     },
185
186     hide: function() {
187         Ext.get(this.el).hide();
188     },
189
190     show: function() {
191         Ext.get(this.el).show();
192     },
193
194     hidePrim: function(sprite) {
195         this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
196     },
197
198     showPrim: function(sprite) {
199         this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
200     },
201
202     getDefs: function() {
203         return this._defs || (this._defs = this.createSvgElement("defs"));
204     },
205
206     transform: function(sprite) {
207         var me = this,
208             matrix = Ext.create('Ext.draw.Matrix'),
209             transforms = sprite.transformations,
210             transformsLength = transforms.length,
211             i = 0,
212             transform, type;
213             
214         for (; i < transformsLength; i++) {
215             transform = transforms[i];
216             type = transform.type;
217             if (type == "translate") {
218                 matrix.translate(transform.x, transform.y);
219             }
220             else if (type == "rotate") {
221                 matrix.rotate(transform.degrees, transform.x, transform.y);
222             }
223             else if (type == "scale") {
224                 matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
225             }
226         }
227         sprite.matrix = matrix;
228         sprite.el.set({transform: matrix.toSvg()});
229     },
230
231     setSize: function(w, h) {
232         var me = this,
233             el = me.el;
234         
235         w = +w || me.width;
236         h = +h || me.height;
237         me.width = w;
238         me.height = h;
239
240         el.setSize(w, h);
241         el.set({
242             width: w,
243             height: h
244         });
245         me.callParent([w, h]);
246     },
247
248     /**
249      * Get the region for the surface's canvas area
250      * @returns {Ext.util.Region}
251      */
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(),
257             max = Math.max,
258             x = max(svgXY[0], rectXY[0]),
259             y = max(svgXY[1], rectXY[1]);
260         return {
261             left: x,
262             top: y,
263             right: x + this.width,
264             bottom: y + this.height
265         };
266     },
267
268     onRemove: function(sprite) {
269         if (sprite.el) {
270             sprite.el.remove();
271             delete sprite.el;
272         }
273         this.callParent(arguments);
274     },
275     
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(" "));
280         }
281     },
282
283     render: function (container) {
284         var me = this;
285         if (!me.el) {
286             var width = me.width || 10,
287                 height = me.height || 10,
288                 el = me.createSvgElement('svg', {
289                     xmlns: "http:/" + "/www.w3.org/2000/svg",
290                     version: 1.1,
291                     width: width,
292                     height: height
293                 }),
294                 defs = me.getDefs(),
295
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", {
301                     width: "100%",
302                     height: "100%",
303                     fill: "#000",
304                     stroke: "none",
305                     opacity: 0
306                 }),
307                 webkitRect;
308             
309                 if (Ext.isSafari3) {
310                     // Rect that we will show/hide to fix old WebKit bug with rendering issues.
311                     webkitRect = me.createSvgElement("rect", {
312                         x: -10,
313                         y: -10,
314                         width: "110%",
315                         height: "110%",
316                         fill: "none",
317                         stroke: "#000"
318                     });
319                 }
320             el.appendChild(defs);
321             if (Ext.isSafari3) {
322                 el.appendChild(webkitRect);
323             }
324             el.appendChild(bgRect);
325             container.appendChild(el);
326             me.el = Ext.get(el);
327             me.bgRect = Ext.get(bgRect);
328             if (Ext.isSafari3) {
329                 me.webkitRect = Ext.get(webkitRect);
330                 me.webkitRect.hide();
331             }
332             me.el.on({
333                 scope: me,
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,
341                 click: me.onClick
342             });
343         }
344         me.renderAll();
345     },
346
347     // private
348     onMouseEnter: function(e) {
349         if (this.el.parent().getRegion().contains(e.getPoint())) {
350             this.fireEvent('mouseenter', e);
351         }
352     },
353
354     // private
355     onMouseLeave: function(e) {
356         if (!this.el.parent().getRegion().contains(e.getPoint())) {
357             this.fireEvent('mouseleave', e);
358         }
359     },
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,
364             sprite;
365
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;
370         }
371         sprite = this.items.get(target.id);
372         if (sprite) {
373             sprite.fireEvent(name, sprite, e);
374         }
375     },
376
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.
379      */
380     tuneText: function (sprite, attrs) {
381         var el = sprite.el.dom,
382             tspans = [],
383             height, tspan, text, i, ln, texts, factor;
384
385         if (attrs.hasOwnProperty("text")) {
386            tspans = this.setText(sprite, attrs.text);
387         }
388         // Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2)
389         if (tspans.length) {
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);
396             }
397             sprite.dirty = true;
398         }
399     },
400
401     setText: function(sprite, textString) {
402          var me = this,
403              el = sprite.el.dom,
404              x = el.getAttribute("x"),
405              tspans = [],
406              height, tspan, text, i, ln, texts;
407         
408         while (el.firstChild) {
409             el.removeChild(el.firstChild);
410         }
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++) {
414             text = texts[i];
415             if (text) {
416                 tspan = me.createSvgElement("tspan");
417                 tspan.appendChild(document.createTextNode(Ext.htmlDecode(text)));
418                 tspan.setAttribute("x", x);
419                 el.appendChild(tspan);
420                 tspans[i] = tspan;
421             }
422         }
423         return tspans;
424     },
425
426     renderAll: function() {
427         this.items.each(this.renderItem, this);
428     },
429
430     renderItem: function (sprite) {
431         if (!this.el) {
432             return;
433         }
434         if (!sprite.el) {
435             this.createSpriteElement(sprite);
436         }
437         if (sprite.zIndexDirty) {
438             this.applyZIndex(sprite);
439         }
440         if (sprite.dirty) {
441             this.applyAttrs(sprite);
442             this.applyTransformations(sprite);
443         }
444     },
445
446     redraw: function(sprite) {
447         sprite.dirty = sprite.zIndexDirty = true;
448         this.renderItem(sprite);
449     },
450
451     applyAttrs: function (sprite) {
452         var me = this,
453             el = sprite.el,
454             group = sprite.group,
455             sattr = sprite.attr,
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
459             //ref: EXTJSIV-1472
460             gradientsMap = me.gradientsMap || {},
461             safariFix = Ext.isSafari && !Ext.isStrict,
462             groups, i, ln, attrs, font, key, style, name, rect;
463
464         if (group) {
465             groups = [].concat(group);
466             ln = groups.length;
467             for (i = 0; i < ln; i++) {
468                 group = groups[i];
469                 me.getGroup(group).add(sprite);
470             }
471             delete sprite.group;
472         }
473         attrs = me.scrubAttrs(sprite) || {};
474
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;
481             }
482             else if (sprite.type == "rect") {
483                 attrs.rx = attrs.ry = attrs.r;
484             }
485             else if (sprite.type == "path" && attrs.d) {
486                 attrs.d = Ext.draw.Draw.pathToString(Ext.draw.Draw.pathToAbsolute(attrs.d));
487             }
488             sprite.dirtyPath = false;
489         // }
490         // else {
491         //     delete attrs.d;
492         // }
493
494         if (attrs['clip-rect']) {
495             me.setClip(sprite, attrs);
496             delete attrs['clip-rect'];
497         }
498         if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) {
499             el.set({ style: "font: " + attrs.font});
500             sprite.dirtyFont = false;
501         }
502         if (sprite.type == "image") {
503             el.dom.setAttributeNS(me.xlink, "href", attrs.src);
504         }
505         Ext.applyIf(attrs, me.minDefaults[sprite.type]);
506
507         if (sprite.dirtyHidden) {
508             (sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
509             sprite.dirtyHidden = false;
510         }
511         for (key in attrs) {
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
515                 //ref: EXTJSIV-1472
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]];
520                 }
521                 if (key in parsers) {
522                     el.dom.setAttribute(key, parsers[key](attrs[key], sprite, me));
523                 } else {
524                     el.dom.setAttribute(key, attrs[key]);
525                 }
526             }
527         }
528         
529         if (sprite.type == 'text') {
530             me.tuneText(sprite, attrs);
531         }
532
533         //set styles
534         style = sattr.style;
535         if (style) {
536             el.setStyle(style);
537         }
538
539         sprite.dirty = false;
540
541         if (Ext.isSafari3) {
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();
546             });
547         }
548     },
549
550     setClip: function(sprite, params) {
551         var me = this,
552             rect = params["clip-rect"],
553             clipEl, clipPath;
554         if (rect) {
555             if (sprite.clip) {
556                 sprite.clip.parentNode.parentNode.removeChild(sprite.clip.parentNode);
557             }
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;
569         }
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;
575         // }
576     },
577
578     /**
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
581      */
582     applyZIndex: function(sprite) {
583         var me = this,
584             items = me.items,
585             idx = items.indexOf(sprite),
586             el = sprite.el,
587             prevEl;
588         if (me.el.dom.childNodes[idx + 2] !== el.dom) { //shift by 2 to account for defs and bg rect
589             if (idx > 0) {
590                 // Find the first previous sprite which has its DOM element created already
591                 do {
592                     prevEl = items.getAt(--idx).el;
593                 } while (!prevEl && idx > 0);
594             }
595             el.insertAfter(prevEl || me.bgRect);
596         }
597         sprite.zIndexDirty = false;
598     },
599
600     createItem: function (config) {
601         var sprite = Ext.create('Ext.draw.Sprite', config);
602         sprite.surface = this;
603         return sprite;
604     },
605
606     addGradient: function(gradient) {
607         gradient = Ext.draw.Draw.parseGradient(gradient);
608         var me = this,
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
613             //ref: EXTJSIV-1472
614             usePlain = Ext.isSafari && !Ext.isStrict,
615             gradientEl, stop, stopEl, i, gradientsMap;
616             
617         gradientsMap = me.gradientsMap || {};
618         
619         if (!usePlain) {
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]);
626             }
627             else {
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);
635                 }
636             }
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);
646             }
647         } else {
648             gradientsMap['url(#' + gradient.id + ')'] = gradient.stops[0].color;
649         }
650         me.gradientsMap = gradientsMap;
651     },
652
653     /**
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
657      */
658     hasCls: function(sprite, className) {
659         return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1;
660     },
661
662     addCls: function(sprite, className) {
663         var el = sprite.el,
664             i,
665             len,
666             v,
667             cls = [],
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 });
673             }
674         }
675         else {
676             for (i = 0, len = className.length; i < len; i++) {
677                 v = className[i];
678                 if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) {
679                     cls.push(v);
680                 }
681             }
682             if (cls.length) {
683                 el.set({ 'class': ' ' + cls.join(' ') });
684             }
685         }
686     },
687
688     removeCls: function(sprite, className) {
689         var me = this,
690             el = sprite.el,
691             curCls =  el.getAttribute('class') || '',
692             i, idx, len, cls, elClasses;
693         if (!Ext.isArray(className)){
694             className = [className];
695         }
696         if (curCls) {
697             elClasses = curCls.replace(me.trimRe, ' ').split(me.spacesRe);
698             for (i = 0, len = className.length; i < len; i++) {
699                 cls = className[i];
700                 if (typeof cls == 'string') {
701                     cls = cls.replace(me.trimRe, '');
702                     idx = Ext.Array.indexOf(elClasses, cls);
703                     if (idx != -1) {
704                         Ext.Array.erase(elClasses, idx, 1);
705                     }
706                 }
707             }
708             el.set({ 'class': elClasses.join(' ') });
709         }
710     },
711
712     destroy: function() {
713         var me = this;
714         
715         me.callParent();
716         if (me.el) {
717             me.el.remove();
718         }
719         delete me.el;
720     }
721 });