/* Copyright (c) 2009 Google Inc. (Brad Neuberg, http://codinginparadise.org) Portions Copyright (c) 2009 Rick Masters Portions Copyright (c) 2009 Others (see COPYING.txt for details on third party code) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** SVG Web brings SVG to browsers that don't have it, such as on Internet Explorer, using Flash. SVG Web supports both static and dynamic SVG files scripted by JavaScript, giving the 'illusion' that SVG is truly supported by a browser. This means that JavaScript in the same page 'sees' the SVG as a real-part of the browser and can script it using the standard DOM, even when we are emulating SVG support using Flash. SVG Web targets SVG 1.1 Full. SVG Web brings SVG support from roughly a ~30% installed base to close to 100% with a library that is roughly 70K in size, giving developers a retained mode API for applications where the HTML5 Canvas tag's immediate mode API might not be appropriate, such as where DOM tracking, import/export, acessibility, and scalable vector images are needed. Retained and immediate mode graphics APIs have different tradeoffs and are appropriate for different use-cases. From a high-level SVG Web consists of two types of handlers, either the NativeHandler which uses the native SVG browser support if present (Firefox, Safari, etc.) or the FlashHandler which uses Flash and various JavaScript tricks to provide SVG support. The entry point for the system is the static JavaScript singleton 'svgweb' class. This does many things, including: ensuring SVG Web is loaded before the window onload event fires; waiting for the onDOMContentLoaded event to fire; grabbing any SVG either directly embedded into a page or embedded using the OBJECT tag; normalizing and cleaning up our SVG; and finally determining the capabilities of the platform and creating the correct handler. At this point the svgweb class hands off work to the specific handler created (NativeHandler or FlashHandler). The handlers and the svgweb singleton depend on a few support classes and methods to get their job done, including: * Utility functions such as 'extend', 'hitch', and 'mixin' to make defining JavaScript classes and callbacks be a bit more compact and readable. Other utility functions include methods such as 'parseXML', 'xpath', and 'xhrObj' to ease cross-browser XML, XPath, and XHR handling, respectively * RenderConfig class - Helps determine the rendering capabilities of the browser and whether the page itself is overriding and forcing a particular render handler, such as through a META tag or query variables. * FlashInfo class - Helps determine whether Flash is installed, and if so, which version. * FlashInserter class - Inserts our Flash into the page in a consistent way. Moving on, the NativeHandler and FlashHandler decompose as follows. Let's start with the NativeHandler, since its the most straightforward. The NativeHandler essentially shims through and uses the native browser support. For various reasons, however, the NativeHandler must still patch various parts of the browser's SVG implementation to provide a consistent SVG experience where reasonable. We are careful to do this minimally and only where absolutely necessary; we don't, for example, attempt to shim in SMIL support on Firefox as that would be overkill. Some reasons for the patching we do need include: * Firefox, for example, does not support setting SVG style values using the standard HTML idiom, such as myCircle.style.fill = 'red'. SVG Web adds this in for consistency. * Some browsers have various bugs that are serious enough that a simple patch on SVG Web's part can make life simpler for programmers. * While SVG Web mostly supports the SVG standard, some small divergences are necessary to accomodate various limitations that the FlashHandler requires; we patch the native SVG implementation to match these divergences in order to have API consistency between all the handlers for end-developers. The FlashHandler is more complicated, obviously. It essentially consists of a Flash portion plus JavaScript to simulate native support. Note that we support having the FlashHandler do its magic not only on Internet Explorer but Safari, Firefox, and Chrome as well. This is useful for two reasons: it significantly aides debugging and testing of the FlashHandler and also makes it possible to optionally use the FlashHandler to 'go beyond' the native capabilities of a browser if needed. For the FlashHandler let's begin with the Flash side. All of the Flash is written in ActionScript 3 and is located in src/org/svgweb. Much of the Flash side consists of ActionScript classes that essentially simulate and render all the various SVG node types, such as SVGCircleNode.as for the SVG Circle tag. The entry point for the Flash is the org.svgweb.SVGViewerWeb class, which mediates all interaction between the JavaScript and Flash side of things. The JavaScript invokes various methods on the SVGViewerWeb class to get things done, and the Flash messages back various things, such as rendering being done, events, etc. We use Flash's ExternalInterface to do this communication but things are more complex unfortunately due to this part of the system being one of the primary bottlenecks, requiring complicated optimizations. See the SVGViewerWeb class for details on this aspect. The FlashHandler uses the Flash side to do its rendering, but it also must handle two other significant cases: * handle disconnected nodes (i.e. nodes not attached to anything yet) * give the illusion of a real DOM and hand back SVG nodes that 'feel real' Various design constraints require that the JavaScript side be relatively sophisticated and also do tracking, rather than pushing everything to the Flash ActionScript. The first primary reason includes the fact that essentially only basic strings and types are pushed over the Flash/JS boundry, rather than object references -- this is difficult since SVG is essentially a tree, making it hard to do operations on specific nodes. The second primary reason has to do with dealing with disconnected trees, since you can build up a complicated DOM tree that is not attached to any rendered document and therefore has no Flash associated with it. Generally patching the browser is taboo. Since SVG Web is an emulation environment rather than a new API we must patch the browser to give the illusion of a real SVG implementation. We attempt to do this without impacting or slowing down non-SVG use-cases. Methods such as getElementById, getElementsByTagNameNS, or createElementNS are patched in to short-circuit for the non-SVG cases. The real magic, though, begins when these methods are called for SVG nodes. Instead of returning real DOM nodes we actually return 'fake' DOM nodes. Looking through this file you will see various 'fake' DOM implementations preceded with underscores, such as _Node, _Element, _Document, _DocumentFragment, etc. These JavaScript classes basically implement the DOM interfaces, such as nodeName, appendChild, childNodes, etc. When an external developer 'calls' on one of our fake SVG nodes, they are actually interacting with a fake JavaScript class rather than a real DOM node; we just work to give the illusion that it's a real DOM class. Inside our fake SVG DOM node classes, we track each node with a __guid that helps us do tracking and registration with the Flash side. If you change the property of an SVG Circle, for example, we would simply send the __guid over to Flash and the new values. Using the __guid essentially gives us the object references we don't get with Flash's ExternalInterface. Every fake node also keeps an internal reference to it's parsed XML so that it can change and store the values on the JavaScript side as well. Some complexity is involved in also tracking DOM TextNodes stored in our SVG tree so that we can consistently return the 'same' DOM TextNode when fetched rather than searching by text value, which would fail if there are many DOM TextNodes with the same value. To handle this we internally store DOM TextNodes as a node called '__text' and add a tracking __guid. This adds some internal complexity but allows external developers to have what feels more like a real DOM. In some ways you can think of the FlashHandler as having a 'peer node' on its side for each SVG node rendered on the Flash side. We obviously don't want to do this for every node, however, which would slow down page load and bloat memory, so we only create our FlashHandler's JavaScript fake 'peer node' on demand when fetched through the DOM, such as through getElementById or by calling childNodes or firstChild on an SVG DOM node. Once fetched the first time we cache our JavaScript fake peer class ready to be re-served on demand again. Let's look at the fake SVG nodes we return to developers to interact with. On modern browsers we can easily simulate magic getters and setters such as myCircle.style.fill = 'red' or someGroup.childNodes[0] using facilities like __defineGetter__. On those browsers when you call someGroup.childNodes[0], for example, our magic getter would get invoked; we would see if a fake peer JavaScript node exists for this and return it if so, and if not, we would create it on-demand and return it. On IE, however, we have to get our magic getters and setters and propery change events using a different mechanism, known as Microsoft Behaviors. Microsoft Behaviors, or HTCs (HTML Components) are a powerful but relatively esoteric browser technology that have been around since IE 5.0. They essentially allow JavaScript to tie directly into Internet Explorer's rendering engine and add new tags. They are defined in an HTC file, in our case svg.htc. HTCs give us the hooks we need to define magic getters and setters for IE as well as gives us something called onpropertychange necessary to support style accesses like myGroup.style.fillColor = 'green'. On IE, whenever we return a result that a developer will manipulate, such as the results of getElementsByTagNameNS, we instead return our HTC proxy node -- you will see methods such as node._getProxyNode() in the source that returns our standard JavaScript _Node or _Element class on all browsers but IE, where we return our HTC node instead. If you look at the svg.htc file you will see that it has very little code in it. This is for two reasons: * The primary performance bottleneck for HTCs is the amount of code they have; limiting their code has a huge affect on memory and performance * We want to have a similar architecture for the FlashHandler independent of the browser to ease maintenence. For this reason a given HTC node delegates all of its work to its 'fake node', which would be the _Node or _Element that it is tracking. You will see calls such as node._getFakeNode() in the source which gets our fake JavaScript class to work with. For example, if you called node.appendChild(someNode), internally we would call someNode._getFakeNode() to make sure we have our JavaScript _Node or _Element class and not the HTC node. Now we can work with our fake SVG node in a consistent way. @author Brad Neuberg (http://codinginparadise.org) */ (function(){ // hide everything externally to avoid name collisions // expose namespaces globally to ease developer authoring window.svgns = 'http://www.w3.org/2000/svg'; window.xlinkns = 'http://www.w3.org/1999/xlink'; // Firefox and Safari will incorrectly turn our internal parsed XML // for the Flash Handler into actual SVG nodes, causing issues. This is // a workaround to prevent this problem. svgnsFake = 'urn:__fake__internal__namespace'; // browser detection adapted from Dojo var isOpera = false, isSafari = false, isMoz = false, isIE = false, isAIR = false, isKhtml = false, isFF = false, isXHTML = false; function _detectBrowsers() { var n = navigator, dua = n.userAgent, dav = n.appVersion, tv = parseFloat(dav); if (dua.indexOf('Opera') >= 0) { isOpera = tv; } // safari detection derived from: // http://developer.apple.com/internet/safari/faq.html#anchor2 // http://developer.apple.com/internet/safari/uamatrix.html var index = Math.max(dav.indexOf('WebKit'), dav.indexOf('Safari'), 0); if (index) { // try to grab the explicit Safari version first. If we don't get // one, look for 419.3+ as the indication that we're on something // "Safari 3-ish". Lastly, default to "Safari 2" handling. isSafari = parseFloat(dav.split('Version/')[1]) || (parseFloat(dav.substr(index + 7)) > 419.3) ? 3 : 2; } if (dua.indexOf('AdobeAIR') >= 0) { isAIR = 1; } if (dav.indexOf('Konqueror') >= 0 || isSafari) { isKhtml = tv; } if (dua.indexOf('Gecko') >= 0 && !isKhtml) { isMoz = tv; } if (isMoz) { isFF = parseFloat(dua.split('Firefox/')[1]) || undefined; } if (document.all && !isOpera) { isIE = parseFloat(dav.split('MSIE ')[1]) || undefined; } // compatMode deprecated on IE8 in favor of documentMode if (document.documentMode) { isStandardsMode = (document.documentMode > 5); } else { isStandardsMode = (document.compatMode == 'CSS1Compat'); } // are we in an XHTML page? if (document.contentType == 'application/xhtml+xml') { /* FF */ isXHTML = true; } else if (typeof XMLDocument != 'undefined' && document.constructor == XMLDocument) { /* Safari */ isXHTML = true; } } _detectBrowsers(); // end browser detection // be able to have debug output when there is no Firebug // see if debugging is turned on function doDebugging() { var debug = false; var scripts = document.getElementsByTagName('script'); for (var i = 0; i < scripts.length; i++) { if (/svg(?:\-uncompressed)?\.js/.test(scripts[i].src)) { var debugSetting = scripts[i].getAttribute('data-debug'); debug = (debugSetting === 'true' || debugSetting === true) ? true : false; } } return debug; } var debug = doDebugging(); if (typeof console == 'undefined' || !console.log) { var queue = []; console = {}; if (!debug) { console.log = function() {}; } else { console.log = function(msg) { var body = null; var delay = false; // IE can sometimes throw an exception if document.body is accessed // before the document is fully loaded try { body = document.getElementsByTagName('body')[0]; } catch (exp) { delay = true; } // IE will sometimes have the body object but we can get the dreaded // "Operation Aborted" error if we try to do an appendChild on it; a // workaround is that the doScroll method will throw an exception before // we can truly use the body element so we can detect this before // any "Operation Aborted" errors if (isIE) { try { document.documentElement.doScroll('left'); } catch (exp) { delay = true; } } if (delay) { queue.push(msg); return; } var p; while (queue.length) { var oldMsg = queue.shift(); p = document.createElement('p'); p.appendChild(document.createTextNode(oldMsg)); body.appendChild(p); } // display new message now p = document.createElement('p'); p.appendChild(document.createTextNode(msg)); body.appendChild(p); }; // IE has an unfortunate issue; under some situations calling // document.body.appendChild can throw an Operation Aborted error, // such as when there are many SVG OBJECTs on a page. This is a workaround // to print out any queued messages until the page has truly loaded. if (isIE) { function flushQueue() { while (queue.length) { var oldMsg = queue.shift(); p = document.createElement('p'); p.appendChild(document.createTextNode(oldMsg)); document.body.appendChild(p); } } var debugInterval = window.setInterval(function() { if (document.readyState == 'complete') { flushQueue(); window.clearTimeout(debugInterval); } }, 50); } } } // end debug output methods /* Quick way to define prototypes that take up less space and result in smaller file size; much less verbose than standard foobar.prototype.someFunc = function() lists. @param f Function object/constructor to add to. @param addMe Object literal that contains the properties/methods to add to f's prototype. */ function extend(f, addMe) { for (var i in addMe) { f.prototype[i] = addMe[i]; } } /** Mixes an object literal of properties into some instance. Good for things that mimic 'static' properties. @param f Function object/contructor to add to @param addMe Object literal that contains the properties/methods to add to f. */ function mixin(f, addMe) { for (var i in addMe) { f[i] = addMe[i]; } } /** Utility function to do XPath cross browser. @param doc Either HTML or XML document to work with. @param context DOM node context to restrict the xpath executing against; can be null, which defaults to doc.documentElement. @param expr String XPath expression to execute. @param namespaces Optional; an array that contains prefix to namespace lookups; see the _getNamespaces() methods in this file for how this data structure is setup. @returns Array with results, empty array if there are none. */ function xpath(doc, context, expr, namespaces) { if (!context) { context = doc.documentElement; } if (typeof XPathEvaluator != 'undefined') { // non-IE browsers var evaluator = new XPathEvaluator(); var resolver = doc.createNSResolver(context); var result = evaluator.evaluate(expr, context, resolver, 0, null); var found = createNodeList(), current; while (current = result.iterateNext()) { found.push(current); } return found; } else { // IE doc.setProperty('SelectionLanguage', 'XPath'); if (namespaces) { var allNamespaces = ''; // IE throws an error if the same namespace is present multiple times, // so remove duplicates var foundNamespace = {}; for (var i = 0; i < namespaces.length; i++) { var namespaceURI = namespaces[i]; var prefix = namespaces['_' + namespaceURI]; // seen before? if (!foundNamespace['_' + namespaceURI]) { if (prefix == 'xmlns') { allNamespaces += 'xmlns="' + namespaceURI + '" '; } else { allNamespaces += 'xmlns:' + prefix + '="' + namespaceURI + '" '; } foundNamespace['_' + namespaceURI] = namespaceURI; } } doc.setProperty('SelectionNamespaces', allNamespaces); } var found = context.selectNodes(expr); if (found === null || typeof found == 'undefined') { found = createNodeList(); } // found is not an Array; it is a NodeList -- turn it into an Array var results = createNodeList(); for (var i = 0; i < found.length; i++) { results.push(found[i]); } return results; } } /** Parses the given XML string and returns the document object. @param xml XML String to parse. @param preserveWhiteSpace Whether to parse whitespace in the XML document into their own nodes. Defaults to false. Controls Internet Explorer's XML parser only. @returns XML DOM document node. */ var parseXMLCache = {}; function parseXML(xml, preserveWhiteSpace) { if (preserveWhiteSpace === undefined) { preserveWhiteSpace = false; } // Issue 421: Reuse XML ActiveX object on Internet Explorer // http://code.google.com/p/svgweb/issues/detail?id=421 var cachedXML = parseXMLCache[preserveWhiteSpace + xml]; if (cachedXML) { return cachedXML.cloneNode(true); } var xmlDoc; if (typeof DOMParser != 'undefined') { // non-IE browsers // parse the SVG using an XML parser var parser = new DOMParser(); try { xmlDoc = parser.parseFromString(xml, 'application/xml'); } catch (e) { throw e; } var root = xmlDoc.documentElement; if (root.nodeName == 'parsererror') { throw new Error('There is a bug in your SVG: ' + (new XMLSerializer().serializeToString(root))); } } else { // IE // only use the following two MSXML parsers: // http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx var versions = [ 'Msxml2.DOMDocument.6.0', 'Msxml2.DOMDocument.3.0' ]; var xmlDoc; for (var i = 0; i < versions.length; i++) { try { xmlDoc = new ActiveXObject(versions[i]); if (xmlDoc) { break; } } catch (e) {} } if (!xmlDoc) { throw new Error('Unable to instantiate XML parser'); } try { xmlDoc.preserveWhiteSpace = preserveWhiteSpace; // IE will attempt to resolve external DTDs (i.e. the SVG DTD) unless // we add the following two flags xmlDoc.resolveExternals = false; xmlDoc.validateOnParse = false; // MSXML 6 breaking change (Issue 138): // http://code.google.com/p/sgweb/issues/detail?id=138 xmlDoc.setProperty('ProhibitDTD', false); xmlDoc.async = 'false'; var successful = xmlDoc.loadXML(xml); if (!successful || xmlDoc.parseError.errorCode !== 0) { throw new Error(xmlDoc.parseError.reason); } } catch (e) { console.log(e.message); throw new Error('Unable to parse SVG: ' + e.message); } } // cache parsed XML to speed up performance (Issue 421) try { parseXMLCache[preserveWhiteSpace + xml] = xmlDoc.cloneNode(true); } catch (e) { // Opera at v10.10 cannot clone a Document } return xmlDoc; } /** Transforms the given node and all of its children into an XML string, suitable for us to send over to Flash for adding to the document. @param node Either a real DOM node to turn into a string or one of our fake _Node or _Elements. @param namespaces Optional. A namespace lookup table that we will use to add our namespace declarations onto the serialized XML. @returns XML String suitable for sending to Flash. */ function xmlToStr(node, namespaces) { var nodeXML = (node._nodeXML || node); var xml; if (typeof XMLSerializer != 'undefined') { // non-IE browsers xml = (new XMLSerializer().serializeToString(nodeXML)); } else { xml = nodeXML.xml; } // Firefox and Safari will incorrectly turn our internal parsed XML // for the Flash Handler into actual SVG nodes, causing issues. We added // a fake SVG namespace earlier to prevent this from happening; remove that // now xml = xml.replace(/urn\:__fake__internal__namespace/g, svgns); // add our namespace declarations var nsString = ''; if (xml.indexOf('xmlns=') == -1) { nsString = 'xmlns="' + svgns + '" '; } if (namespaces) { for (var i = 0; i < namespaces.length; i++) { var uri = namespaces[i]; var prefix = namespaces['_' + uri]; // ignore our fake SVG namespace string if (uri == svgnsFake) { uri = svgns; } var newNS; if (prefix != 'xmlns') { newNS = 'xmlns:' + prefix + '="' + uri + '"'; } else { newNS = 'xmlns' + '="' + uri + '"'; } // FIXME: Will this break if single quotes are used around namespace // declaration? if (xml.indexOf(newNS) == -1) { nsString += newNS + ' '; } } } xml = xml.replace(/<([^ ]+)/, '<$1 ' + nsString + ' '); return xml; } /* Useful for closures and event handlers. Instead of having to do this: var self = this; window.onload = function(){ self.init(); } you can do this: window.onload = hitch(this, 'init'); @param context The instance to bind this method to. @param method A string method name or function object to run on context. */ function hitch(context, method) { if (typeof method == 'string') { method = context[method]; } // this method shows up in the style string on IE's HTC object since we // use it to extend the HTC element's style object with methods like // item(), setProperty(), etc., so we want to keep it short. The performance // of an HTC/Microsoft Behavior is very sensitive to the length of its // JavaScript methods so we want to keep them short. return function() { return method.apply(context, (arguments.length) ? arguments : []); }; } /* Internet Explorer's list of standard XHR PROGIDS. */ var XHR_PROGIDS = [ 'MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP' ]; /* Standard way to grab XMLHttpRequest object. */ function xhrObj() { if (typeof XMLHttpRequest != 'undefined') { return new XMLHttpRequest(); } else if (ActiveXObject) { var xhr = null; var i; // save the good PROGID for quicker access next time for (i = 0; i < XHR_PROGIDS.length && !xhr; ++i) { try { xhr = new ActiveXObject(XHR_PROGIDS[i]); } catch(e) {} } if (!xhr) { throw new Error('XMLHttpRequest object not available on this platform'); } return xhr; } } // We just use an autoincrement counter to ensure uniqueness for our node // tracking, which is fine for our situation and produces much smaller GUIDs; // GUIDs are used to track individual SVG nodes between our JavaScript and // Flash. var guidCounter = 0; function guid() { return '_' + guidCounter++; } /** Our singleton object that acts as the primary entry point for the library. Gets exposed globally as 'svgweb'. */ function SVGWeb() { //start('SVGWeb_constructor'); // is SVG Web being hosted cross-domain? this._setXDomain(); // grab any configuration that might exist on where our library resources // are this.libraryPath = this._getLibraryPath(); // see if there is an optional HTC filename being used, such as svg-htc.php; // these are used to have the server automatically send the correct MIME // type for HTC files without having to fiddle with MIME type settings this.htcFilename = this._getHTCFilename(); // prepare IE by inserting special markup into the page to have the HTC // be available if (isIE) { FlashHandler._prepareBehavior(this.libraryPath, this.htcFilename); } // make sure we can intercept onload listener registration to delay onload // until we are done with our internal machinery this._interceptOnloadListeners(); // wait for our page's DOM content to be available this._initDOMContentLoaded(); //end('SVGWeb_constructor'); } extend(SVGWeb, { // path to find library resources libraryPath: './', // RenderConfig object of which renderer (native or Flash) to use config: null, pageLoaded: false, handlers: [], totalLoaded: 0, /** Every element (including text nodes) has a unique GUID. This lookup table allows us to go from a GUID taken from an XML node to a fake node (_Element or _Node) that might have been instantiated already. */ _guidLookup: [], /** Onload page load listeners. */ _loadListeners: [], /** A data structure that we used to keep track of removed nodes, necessary so we can clean things up and prevent memory leaks on IE on page unload. Unfortunately we have to keep track of this at the global 'svgweb' level rather than on individual handlers because a removed node might have never been associated with a real DOM or a real handler. */ _removedNodes: [], /** Used to lookup namespaces **/ _allSVGNamespaces: [], /** Adds an event listener to know when both the page, the internal SVG machinery, and any SVG SCRIPTS or OBJECTS are finished loading. @param listener Function that will get called when page and all embedded SVG is loaded and rendered. @param fromObject Optional. If true, then we are calling this from inside an SVG OBJECT file. @param objectWindow Optional. Provided when called from inside an SVG OBJECT file; this is the window object inside the SVG OBJECT. */ addOnLoad: function(listener, fromObject, objectWindow) { if (fromObject) { // addOnLoad called from an SVG file embedded with OBJECT var obj = objectWindow.frameElement; // if we are being called from an SVG OBJECT tag and are the Flash // renderer than just execute the onload listener now since we know // the SVG file is done rendering. if (fromObject && this.getHandlerType() == 'flash') { listener.apply(objectWindow); } else { // NOTE: some browsers will fire the onload of the SVG file _before_ our // NativeHandler is done (Firefox); others will do it the opposite way // (Safari). We set variables pointing between the OBJECT and its // NativeHandler to handle this. if (obj._svgHandler) { // NativeHandler already constructed obj._svgHandler._onObjectLoad(listener, objectWindow); } else { // NativeHandler not constructed yet; store a reference for later // handling obj._svgWindow = objectWindow; obj._svgFunc = listener; } } } else { // normal addOnLoad request from containing HTML page this._loadListeners.push(listener); } // fire the onsvgload event immediately if the page was done // loading earlier if (this.pageLoaded) { this._fireOnLoad(); } }, /** Returns a string for the given handler for this platform, 'flash' if flash is being used or 'native' if the native capabilities are being used. */ getHandlerType: function() { if (this.renderer == FlashHandler) { return 'flash'; } else if (this.renderer == NativeHandler) { return 'native'; } }, /** Appends a dynamically created SVG OBJECT or SVG root to the page. See the section "Dynamically Creating and Removing SVG OBJECTs and SVG Roots" in the User Guide for details. @node Either an 'object' created with document.createElement('object', true) or an SVG root created with document.createElementNS(svgns, 'svg') @parent An HTML DOM parent to attach our SVG OBJECT or SVG root to. This DOM parent must already be attached to the visible DOM. */ appendChild: function(node, parent) { //console.log('appendChild, node='+node+', parent='+parent); if (node.nodeName.toLowerCase() == 'object' && node.getAttribute('type') == 'image/svg+xml') { // dynamically created OBJECT tag for an SVG file this.totalSVG++; this._svgObjects.push(node); if (this.getHandlerType() == 'native') { node.onload = node.onsvgload; parent.appendChild(node); } var placeHolder = node; if (this.getHandlerType() == 'flash') { // register onloads if (node.onsvgload) { node.addEventListener('SVGLoad', node.onsvgload, false); } // Turn our OBJECT into a place-holder DIV attached to the DOM, // copying over our properties; this will get replaced by the // Flash OBJECT. We need to do this because we need a real element // in the DOM to 'replace' later on for IE which uses outerHTML, // and the DIV will act as that place-holder element. var div = document._createElement('div'); for (var j = 0; j < node.attributes.length; j++) { var attr = node.attributes[j]; var attrName = attr.nodeName; var attrValue = attr.nodeValue; // trim out 'empty' attributes with no value if (!attrValue && attrValue !== 'true') { continue; } div.setAttribute(attrName, attrValue); } parent.appendChild(div); // copy over internal event listener info div._onloadListeners = node._onloadListeners; placeHolder = div; } // now handle this SVG OBJECT var objID = this._processSVGObject(placeHolder); // add the ID to our original SVG OBJECT node as a private member; // we will later use this if svgweb.removeChild is called to remove // the node in order to remove the SVG OBJECT from our // handler._svgObjects array node._objID = objID; } else if (node.nodeName.toLowerCase() == 'svg') { // dynamic SVG root this.totalSVG++; // copy over any node.onsvgload listener if (node.onsvgload) { node.addEventListener('SVGLoad', node.onsvgload, false); } if (isIE && node._fakeNode) { node = node._fakeNode; } // serialize SVG into a string var svgStr = xmlToStr(node); // nest the SVG into a SCRIPT tag and add to the page; we do this // so that we hit the same code path for dynamic SVG roots as you would // get if the SCRIPT + SVG were already in the page on page load var svgScript = document.createElement('script'); svgScript.type = 'image/svg+xml'; if (!isXHTML) { // NOTE: only script.text works for IE; other ways of changing value // throws 'Unknown Runtime Error' on that wonderful browser svgScript.text = svgStr; } else { // XHTML; no innerHTML here svgScript.appendChild(document.createTextNode(svgStr)); } this._svgScripts.push(svgScript); parent.appendChild(svgScript); // preserve our SVGLoad addEventListeners on the script object svgScript._onloadListeners = node._detachedListeners /* flash renderer */ || node._onloadListeners /* native */; // now process the SVG as we would normal SVG embedded into the page // with a SCRIPT tag this._processSVGScript(svgScript); } }, /** Removes a dynamically created SVG OBJECT or SVG root to the page. See the section "Dynamically Creating and Removing SVG OBJECTs and SVG Roots" for details. @node OBJECT or EMBED tag for the SVG OBJECT to remove. @parent The parent of the node to remove. */ removeChild: function(node, parent) { //console.log('svgweb.removeChild, node='+node.nodeName+', parent='+parent.nodeName); var name = node.nodeName.toLowerCase(); var nodeID, nodeHandler; if (name == 'object' || name == 'embed' || name == 'svg') { this.totalSVG = this.totalSVG == 0 ? 0 : this.totalSVG - 1; this.totalLoaded = this.totalLoaded == 0 ? 0 : this.totalLoaded - 1; // remove from our list of handlers nodeID = node.getAttribute('id'); nodeHandler = this.handlers[nodeID]; var newHandlers = []; for (var i = 0; i < this.handlers.length; i++) { var currentHandler = this.handlers[i]; if (currentHandler != nodeHandler) { newHandlers[currentHandler.id] = currentHandler; newHandlers.push(currentHandler); } } this.handlers = newHandlers; } if (name == 'object' || name == 'embed') { // nodeHandler might not have a fake 'document' object; this can happen // if loading of the SVG OBJECT is 'interrupted' by a rapid removeChild // before it ever had a chance to even finish loading. If there is no // fake document then skip trying to remove timing functions and event // handlers below if (this.getHandlerType() == 'flash' && nodeHandler.document && nodeHandler.document.defaultView) { // remove any setTimeout or setInterval functions that might have // been registered inside this object; see _SVGWindow.setTimeout // for details var iframeWin = nodeHandler.document.defaultView; if (iframeWin._intervalIDs) { for (var i = 0; i < iframeWin._intervalIDs.length; i++) { iframeWin.clearInterval(iframeWin._intervalIDs[i]); } } if (iframeWin._timeoutIDs) { for (var i = 0; i < iframeWin._timeoutIDs.length; i++) { iframeWin.clearTimeout(iframeWin._timeoutIDs[i]); } } // remove keyboard event handlers; we added a record of these for // exactly this reason in _Node.addEventListener() for (var i = 0; i < nodeHandler._keyboardListeners.length; i++) { var l = nodeHandler._keyboardListeners[i]; if (isIE) { document.detachEvent('onkeydown', l); } else { // we aren't sure whether the event listener is a useCapture or // not; just try to remove both document.removeEventListener('keydown', l, true); document.removeEventListener('keydown', l, false); } } } // remove the original SVG OBJECT node from our handlers._svgObjects // array var objID; if (typeof node._objID != 'undefined') { // native handler objID = node._objID; } else if (typeof node.contentDocument != 'undefined') { // IE // node is a Flash node; get a reference to our fake _Document // and then use that to get our Flash Handler objID = node.contentDocument._handler.id; } else { objID = node._handler.id; } for (var i = 0; i < svgweb._svgObjects.length; i++) { if (svgweb._svgObjects[i]._objID === objID) { svgweb._svgObjects.splice(i, 1); break; } } // remove from the page parent.removeChild(node); if (this.getHandlerType() == 'flash') { // delete the HTC container and all HTC nodes that belong to this // SVG OBJECT var container = document.getElementById('__htc_container'); if (container) { for (var i = 0; i < container.childNodes.length; i++) { var child = container.childNodes[i]; if (typeof child.ownerDocument != 'undefined' && child.ownerDocument == nodeHandler._svgObject.document) { if (typeof child._fakeNode != 'undefined' && typeof child._fakeNode._htcNode != 'undefined') { child._fakeNode._htcNode = null; } child._fakeNode = null; child._handler = null; container.removeChild(child); } } } // clear out the guidLookup table for nodes that belong to this // SVG OBJECT for (var guid in svgweb._guidLookup) { var child = svgweb._guidLookup[guid]; if (child._fake && child.ownerDocument === nodeHandler.document) { delete svgweb._guidLookup[guid]; } } // remove various properties to prevent IE memory leaks nodeHandler.flash.contentDocument = null; nodeHandler.flash = null; nodeHandler._xml = null; // nodeHandler.window might not be present if this SVG OBJECT is being // removed before it was even finished loading if (nodeHandler.window) { nodeHandler.window._scope = null; nodeHandler.window = null; } var svgObj = nodeHandler._svgObject; var svgDoc = svgObj.document; svgDoc._nodeById = null; svgDoc._xml = null; svgDoc.defaultView = null; svgDoc.documentElement = null; svgDoc.rootElement = null; svgDoc.defaultView = null; svgDoc = null; svgObj._svgNode = null; svgObj._handler = null; if (iframeWin) { iframeWin._setTimeout = null; iframeWin.setTimeout = null; iframeWin._setInterval = null; iframeWin.setInterval = null; } nodeHandler._svgObject = null; svgObj = null; nodeHandler = null; iframeWin = null; } // end if (this.getHandlerType() == 'flash') } else if (name == 'svg') { // dynamicly created SVG roots // remove the original SVG SCRIPT node from our handlers._svgScripts // array for (var i = 0; i < svgweb._svgScripts.length; i++) { if (svgweb._svgScripts[i] == nodeHandler._scriptNode) { svgweb._svgScripts.splice(i, 1); break; } } if (isIE && this.getHandlerType() == 'flash' && node._fakeNode) { node = node._fakeNode; } // remove from the page var removeMe; if (this.getHandlerType() == 'native') { removeMe = node; } else { removeMe = node._handler.flash; } // IE will sometimes throw an exception if we don't do this on a timeout if (!isIE) { parent.removeChild(removeMe); } else { // FIXME: Analyze whether this will sometimes lead to race conditions; // I haven't found any and could not find another workaround on IE window.setTimeout( function(parent, removeMe) { return function() { parent.removeChild(removeMe); // IE memory leaks parent = null; removeMe = null; } }(parent, removeMe) /* prevent IE closure memory leaks */, 1); } if (this.getHandlerType() == 'flash') { // indicate we are unattached node._setUnattached(); // clear out the guidLookup table for nodes that belong to this // SVG root for (var guid in svgweb._guidLookup) { var child = svgweb._guidLookup[guid]; if (child._fake && child._getFakeNode() === nodeHandler) { delete svgweb._guidLookup[guid]; } } // remove various properties to prevent IE memory leaks nodeHandler._scriptNode = null; nodeHandler.flash.documentElement = null; nodeHandler.flash = null; nodeHandler._xml = null; nodeHandler = null; } // end if (this.getHandlerType() == 'flash') } }, /** Sets up an onContentLoaded listener */ _initDOMContentLoaded: function() { // code adapted from Dean Edwards/Matthias Miller/John Resig/others var self = this; if (document.addEventListener) { // DOMContentLoaded natively supported on Opera 9/Mozilla/Safari 3 document.addEventListener('DOMContentLoaded', function() { self._onDOMContentLoaded(); }, false); } else { // Internet Explorer // id is set to be __ie__svg__onload rather than __ie_onload so // we don't have name collisions with other scripts using this // code as well document.write(' @param win The owner window to patch. @param doc The owner document to work with. Note that there are different possible ways script code might get into a patched window.addEventListener: If it is called during onload or script tag code, then the script is likely patched to run __svgHandler.window.addEventListener where __svgHandler.window is a fake _SVGWindow object with a fake addEventListener. If the script gets a hold of the real window object, it calls in the patched 'real window' method. The following code is the code that patches the real window. */ NativeHandler._patchSvgFileAddEventListener = function(win, doc) { var _addEventListener = win.addEventListener; win.addEventListener = function(type, listener, useCapture) { if (type.toLowerCase() != 'svgload') { _addEventListener(type, listener, useCapture); } else { if (typeof listener == 'object') { listener.handleEvent.call(listener, undefined); } else { listener(); } } } win.__defineGetter__('onsvgload', function() { return this.__onsvgload; }); win.__defineSetter__('onsvgload', function(listener) { this.__onsvgload = listener; this.addEventListener('SVGLoad', listener, false); }); }; // end of static singleton functions // methods that every NativeHandler instance has extend(NativeHandler, { /** Has this handler kick off its work. */ start: function() { //console.log('start'); if (this.type == 'object') { this._handleObject(); } else if (this.type == 'script') { this._handleScript(); } }, /** Handles SVG embedded into the page with a SCRIPT tag. */ _handleScript: function() { // build up a list of namespaces, used by our patched getElementsByTagNameNS this._namespaces = this._getNamespaces(); // replace the SCRIPT node with some actual SVG this._processSVGScript(this._xml, this._svgString, this._scriptNode); // indicate that we are done this._loaded = true; svgweb._handleDone(this.id, 'script', this); }, /** Handles SVG embedded into the page with an OBJECT tag. */ _handleObject: function() { //console.log('handleObject'); // needed so that Firefox doesn't display scroll bars on SVG content // (Issue 164: http://code.google.com/p/svgweb/issues/detail?id=164) // FIXME: Will this cause issues if someone wants to override default // overflow behavior? this._objNode.style.overflow = 'hidden'; // make the object visible again this._objNode.style.visibility = 'visible'; // at this point we wait for our SVG OBJECTs to call svgweb.addOnLoad // so we can know they have loaded. Some browsers however will fire the // onload of the SVG file _before_ our NativeHandler is done depending // on whether they are loading from the cache or not; others will do it the // opposite way (Safari). If the onload was fired and therefore // svgweb.addOnLoad was called, then we stored a reference to the SVG file's // Window object there. if (this._objNode._svgWindow) { this._onObjectLoad(this._objNode._svgFunc, this._objNode._svgWindow); } else { // if SVG Window isn't there, then we need to wait for svgweb.addOnLoad // to get called by the SVG file itself. Store a reference to ourselves // to be used there. this._objNode._svgHandler = this; // if this is a purely static SVG file and it's the only one on the page // then we need to manually see when it loads for Firefox; Safari // correctly fires our onload listener but not Firefox. // Issue 219: "body.onload not fired for SVG OBJECT" // http://code.google.com/p/svgweb/issues/detail?id=219 var self = this; // Our OBJECT node could have come about in two ways: // * It was dynamically created with createElement - in this case // make sure to call the original, unpatched version of // addEventListener (notice that it is _addEventListener below)! We // patched this in NativeHandler._patchAddEventListener(). // * It was in the markup of the page on page load - use the standard // unpatched addEventListener var loadFunc = function() { // svgweb.removeChild might have been called before we are fired if (!self._objNode.contentDocument) { return; } var win = self._objNode.contentDocument.defaultView; self._onObjectLoad(self._objNode._svgFunc, win); }; if (this._objNode._addEventListener) { this._objNode._addEventListener('load', loadFunc, false); } else { this._objNode.addEventListener('load', loadFunc, false); } } }, /** Called by svgweb.addOnLoad() or our NativeHandler function constructor after an SVG OBJECT has loaded to tell us that we have loaded. We require that script writers manually tell us when they have loaded; see 'Knowing When Your SVG Is Loaded' section in the documentation. @param func The actual onload function to fire inside the SVG file (i.e. this is the function the end developer wants run when the SVG file is done loading). @param win The Window object inside the SVG OBJECT */ _onObjectLoad: function(func, win) { //console.log('onObjectLoad'); // we might have already been called before if (this._loaded) { return; // nothing to do } // flag that we are loaded this._loaded = true; // patch various browser objects to correct some browser bugs and // to have more consistency between the Flash and Native handlers var doc = win.document; NativeHandler._patchBrowserObjects(win, doc); // make the SVG root currentTranslate property work like the FlashHandler, // which slightly diverges from the standard due to limitations of IE var root = doc.rootElement; if (root) { this._patchCurrentTranslate(root); } // expose the svgns and xlinkns variables inside in the SVG files // Window object win.svgns = svgns; win.xlinkns = xlinkns; // build up list of namespaces so that getElementsByTagNameNS works with // foreign namespaces this._namespaces = this._getNamespaces(doc); // execute the actual SVG onload that the developer wants run if (func) { func.apply(win); } // execute any cached onload listeners that might been registered with // addEventListener on the SVG OBJECT for (var i = 0; this._objNode._onloadListeners && i < this._objNode._onloadListeners.length; i++) { func = this._objNode._onloadListeners[i]; func.apply(this._objNode); } // try to fire the page-level onload event; the svgweb object will check // to make sure all SVG OBJECTs are loaded svgweb._fireOnLoad(); }, /** Inserts the SVG back into the HTML page with the correct namespace. */ _processSVGScript: function(xml, svgString, scriptNode) { var importedSVG = document.importNode(xml.documentElement, true); scriptNode.parentNode.replaceChild(importedSVG, scriptNode); this._svgRoot = importedSVG; // make the SVG root currentTranslate property work like the FlashHandler, // which slightly diverges from the standard due to limitations of IE this._patchCurrentTranslate(this._svgRoot); }, /** Extracts any namespaces we might have, creating a prefix/namespaceURI lookup table. NOTE: We only support namespace declarations on the root SVG node for now. @param doc Optional. If present, then we retrieve the list of namespaces from the SVG inside of the object. This is the document object inside the SVG file. @returns An object that associates prefix to namespaceURI, and vice versa. */ _getNamespaces: function(doc) { var results = []; var attrs; if (doc) { attrs = doc.documentElement.attributes; } else { attrs = this._xml.documentElement.attributes; } for (var i = 0; i < attrs.length; i++) { var attr = attrs[i]; if (/^xmlns:?(.*)$/.test(attr.nodeName)) { var m = attr.nodeName.match(/^xmlns:?(.*)$/); var prefix = (m[1] ? m[1] : 'xmlns'); var namespaceURI = attr.nodeValue; // don't add duplicates if (!results['_' + prefix]) { results['_' + prefix] = namespaceURI; results['_' + namespaceURI] = prefix; results.push(namespaceURI); } } } return results; }, /** We patch native browsers to use our getter/setter syntax for currentTranslate (we have to use formal methods like currentTranslate.setX() for the Flash renderer instead of currentTranslate.x = 3 due to limitations in Internet Explorer) @root The SVG Root on which we are going to patch the currentTranslate property. */ _patchCurrentTranslate: function(root) { //console.log('patchCurrentTranslate, root='+root); // we have to unfortunately do this at runtime for each SVG OBJECT // since for Firefox/Native the SVGPoint prototype doesn't seem to correctly // extend the currentTranslate property; Safari likes us to extend // the prototype which DOES work there (and FF doesn't), while FF wants // us to extend the actual currentTranslate instance (which Safari // doesn't like) var t; if (typeof SVGRoot != 'undefined') { // FF t = root.currentTranslate; } else if (typeof root.currentTranslate.__proto__ != 'undefined') { // Safari t = root.currentTranslate.__proto__; } else if (typeof SVGPoint != 'undefined') { // Opera // Issue 358: // "Opera throws exception on patch to currentTranslate" // http://code.google.com/p/svgweb/issues/detail?id=358 t = SVGPoint.prototype; } t.setX = function(newValue) { return this.x = newValue; } t.getX = function() { return this.x; } t.setY = function(newValue) { return this.y = newValue; } t.getY = function() { return this.y; } // custom extension in SVG Web to aid performance for Flash renderer t.setXY = function(newValue1, newValue2) { this.x = newValue1; this.y = newValue2; } } }); /** Utility class that helps us keep track of any suspendRedraw operations that might be in effect. The FlashHandler.sendToFlash() method is the primary point at which we check to see if things are suspended; if they are, then we batch them up internally. When things are unsuspended we send them all over in one shot to Flash. @param handler The handler associated with this _RedrawManager */ function _RedrawManager(handler) { this._handler = handler; // we batch all the methods and messages into an array this._batch = []; // the next available suspend ID; we increment this each time so we don't // get duplicate suspend IDs this._nextID = 1; // array of our suspend IDs this._ids = []; // a lookup table going from suspend ID to a window.setTimeout ID this._timeoutIDs = {}; } extend(_RedrawManager, { /** Returns true if redrawing is suspended. */ isSuspended: function() { return (this._ids.length > 0); }, /** Batches up the given Flash method and message for later execution when things are unsuspended. @param method Flash method to invoke @param message Message to send to Flash method. */ batch: function(method, message) { // turn into a single string this._batch.push(method + ':' + message); }, suspendRedraw: function(ms, notifyFlash) { if (ms === undefined) { throw 'Not enough arguments to suspendRedraw'; } if (notifyFlash === undefined) { notifyFlash = true; } // generate an ID var id = this._nextID; /* technically should be unsigned long */ this._nextID++; // kick off a timer to cancel if not unsuspended by developer in time var self = this; var timeoutID = window.setTimeout(function() { self.unsuspendRedraw(id); delete self._timeoutIDs['_' + id]; }, ms); // store an entry this._ids.push(id); this._timeoutIDs['_' + id] = timeoutID; // tell Flash to stop rendering // there is a chance that suspendRedraw is called while the page // is unloading from a setTimout interval; surround everything with a // try/catch block to prevent an exception from blocking page unload if (notifyFlash) { try { this._handler.flash.jsSuspendRedraw(); } catch (exp) { console.log("suspendRedraw exception: " + exp); } } return id; }, unsuspendRedraw: function(id, notifiedFlash) { if (notifiedFlash === undefined) { notifiedFlash = true; } var idx = -1; for (var i = 0; i < this._ids.length; i++) { if (this._ids[i] == id) { idx = i; break; } } if (idx == -1) { throw 'Unknown id passed to unsuspendRedraw: ' + id; } // clear timeout if still in effect if (this._timeoutIDs['_' + id] != undefined) { window.clearTimeout(this._timeoutIDs['_' + id]); } // clear entry this._ids.splice(idx, 1); delete this._timeoutIDs['_' + id]; // other suspendRedraws in effect or nothing to do? // Even if the length is zero, if flash was notified of the suspension // then it needs to be notified of the unsuspension. If the caller // knows flash was never notified of the suspension, they pass notifyFlash=false // and we are free to exit here if there is no suspended work. if (this.isSuspended() || (this._batch.length == 0 && !notifiedFlash)) { return; } // Send over everything to Flash now. We call jsUnsuspendRedrawAll and // send over everything as a giant string. This string is setup as follows. // method:message__SVG__METHOD__DELIMIT // Basically, we have the method name, followed by a colon, followed // by the message to send to that method (which might have __SVG__DELIMITs // in it). Each method is separated by the __SVG__METHOD__DELIMIT // delimiter. var sendMe = this._batch.join('__SVG__METHOD__DELIMIT'); this._batch = []; // there is a chance that unsuspendRedraw is called while the page // is unloading from a setTimout interval; surround everything with a // try/catch block to prevent an exception from blocking page unload try { this._handler.flash.jsUnsuspendRedrawAll(sendMe); } catch (exp) { console.log('unsuspendRedraw exception: ' + exp); } }, unsuspendRedrawAll: function() { for (var i = 0; i < this._ids.length; i++) { this.unsuspendRedraw(this._ids[i]); } }, forceRedraw: function() { // not implemented } }); /* The SVG 1.1 spec requires DOM Level 2 Core and Events support. DOM Level 2 Core spec: http://www.w3.org/TR/DOM-Level-2-Core/ DOM Level 2 Events spec: http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Registration-interfaces The following DOM 2 Core interfaces are not supported: NamedNodeMap, Attr, Text, Comment, CDATASection, DocumentType, Notation, Entity, EntityReference, ProcessingInstruction We underscore our DOM interface names below so that they don't collide with the browser's implementations of these (for example, Firefox exposes the DOMException, Node, etc. interfaces as well) */ function _DOMImplementation() {} extend(_DOMImplementation, { hasFeature: function(feature /* String */, version /* String */) /* Boolean */ { // TODO } // Note: createDocumentType and createDocument left out }); // Note: Only element, text nodes, document nodes, and document fragment nodes // are supported for now. We don't parse and retain comments, processing // instructions, etc. CDATA nodes are turned into text nodes. function _Node(nodeName, nodeType, prefix, namespaceURI, nodeXML, handler, passThrough) { if (nodeName === undefined && nodeType === undefined) { // prototype subclassing return; } this.nodeName = nodeName; this._nodeXML = nodeXML; this._handler = handler; this._listeners = {}; this._detachedListeners = []; this.fake = true; // Firefox and Safari will incorrectly turn our internal parsed XML // for the Flash Handler into actual SVG nodes, causing issues. This is // a workaround to prevent this problem. if (namespaceURI == svgnsFake) { namespaceURI = svgns; } // determine whether we are attached this._attached = true; if (!this._handler) { this._attached = false; } // handle nodes that were created with createElementNS but are not yet // attached to the document yet if (nodeType == _Node.ELEMENT_NODE && !this._nodeXML && !this._handler) { // build up an empty XML node for this element var xml = '\n'; if (namespaceURI == svgns && !prefix) { // we use a fake namespace for SVG to prevent Firefox and Safari // from incorrectly making these XML nodes real SVG objects! xml += '<' + nodeName + ' xmlns="' + svgnsFake + '"/>'; } else { xml += '<' + nodeName + ' xmlns:' + prefix + '="' + namespaceURI + '"/>'; } this._nodeXML = parseXML(xml).documentElement; } else if (nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { var xml = '\n' + '<__document__fragment>'; this._nodeXML = parseXML(xml).documentElement; } // handle guid tracking if (nodeType != _Node.DOCUMENT_NODE && this._nodeXML) { if (!this._nodeXML.getAttribute('__guid')) { this._nodeXML.setAttribute('__guid', guid()); } this._guid = this._nodeXML.getAttribute('__guid'); // store a reference to the new node so that later fetching of this // node will respect object equality svgweb._guidLookup['_' + this._guid] = this; } if (nodeType == _Node.ELEMENT_NODE) { if (nodeName.indexOf(':') != -1) { this.localName = nodeName.match(/^[^:]*:(.*)$/)[1]; } else { this.localName = nodeName; } } if (nodeType) { this.nodeType = nodeType; } else { this.nodeType = _Node.ELEMENT_NODE; } if (nodeType == _Node.ELEMENT_NODE || nodeType == _Node.DOCUMENT_NODE || nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { this.prefix = prefix; this.namespaceURI = namespaceURI; this._nodeValue = null; } else if (nodeType == _Node.TEXT_NODE) { // We store the actual text node value as a child of our 'fake' DOM // Element. We have to use a DOM Element so that we have access to // setAttribute to store a fake __guid attribute to track the text node. this._nodeValue = this._nodeXML.firstChild.nodeValue; // browsers return null instead of undefined; match their behavior this.prefix = null; this.namespaceURI = null; if (this._nodeValue === undefined) { this._nodeValue = null; } } // even when not attached native behavior for ownerDocument is to be // set to 'document' this.ownerDocument = document; // if we are an SVG OBJECT set to our fake pseudo _Document if (this._attached && this._handler.type == 'object') { this.ownerDocument = this._handler.document; } if (passThrough === undefined) { passThrough = false; } this._passThrough = passThrough; // create empty stub methods for certain methods to help IE's HTC be // smaller, which has a very strong affect on performance if (isIE) { this._createEmptyMethods(); } this._childNodes = this._createChildNodes(); // we use an XML Element rather than an XML Text Node to 'track' our // text nodes; indicate as such using an attribute if (nodeType == _Node.TEXT_NODE) { this._nodeXML.setAttribute('__fakeTextNode', true); } // prepare the getter and setter magic for non-IE browsers if (!isIE) { this._defineNodeAccessors(); } else if (isIE && this.nodeType != _Node.DOCUMENT_NODE) { // If we are IE, we must use a behavior in order to get onpropertychange // and override core DOM methods. We only do this for normal SVG elements // and DocumentFragments and not for the DOCUMENT element. this._createHTC(); } } mixin(_Node, { ELEMENT_NODE: 1, TEXT_NODE: 3, DOCUMENT_NODE: 9, DOCUMENT_FRAGMENT_NODE: 11 // Note: many other node types left out here }); extend(_Node, { /* Event listeners; this is an object hashtable that keys the event name, such as 'mousedown', with an array of functions to execute when this event happens. This second level array is also used as an object hashtable to associate the function + useCapture with the listener so that we can implement removeListener at a later point. We only add to this table if the node is attached to the DOM. Example: _listeners['mousedown'] --> array of listeners _listeners['mousedown'][0] --> first mousedown listener, a function _listeners['mousedown']['_' + someListener + ':' + useCapture] --> getting listener by function reference for mouse down event */ _listeners: null, /* An array that we use to store addEventListener requests for detached nodes, where each array entry is an object literal with the following values: type - The type of the event listener - The function object to execute useCapture - Whether to use capturing or not. */ _detachedListeners: null, insertBefore: function(newChild /* _Node */, refChild /* _Node */) { //console.log('insertBefore, newChild='+newChild.id+', refChild='+refChild.id); if (this.nodeType != _Node.ELEMENT_NODE && this.nodeType != _Node.DOCUMENT_FRAGMENT_NODE) { throw 'Not supported'; } // Issue 296: existing child should not be added again if (newChild.parentNode) { newChild.parentNode.removeChild(newChild); } // if the children are DOM nodes, turn them into _Node or _Element // references newChild = this._getFakeNode(newChild); refChild = this._getFakeNode(refChild); // flag that indicates that the child is a _DocumentFragment (keeps // the overall code smaller); we have to treat these differently since // we are importing all of the DocumentFragment's children rather than // just one new child var isFragment = (newChild.nodeType == _Node.DOCUMENT_FRAGMENT_NODE); var fragmentChildren; if (isFragment) { fragmentChildren = newChild._getChildNodes(true /* get fake nodes */); } // are we an empty DocumentFragment? if (isFragment && fragmentChildren.length == 0) { // nothing to do newChild._reset(); // clean out DocumentFragment return newChild._getProxyNode(); } // get an index position for where refChild is var findResults = this._findChild(refChild); if (findResults === null) { // TODO: Throw the correct DOM error instead throw new Error('Invalid child passed to insertBefore'); } var position = findResults.position; // import the newChild or all of the _DocumentFragment children into // ourselves, insert it into our XML, and process the newChild and all // its descendants var importMe = []; if (isFragment) { for (var i = 0; i < fragmentChildren.length; i++) { importMe.push(fragmentChildren[i]); } } else { importMe.push(newChild); } for (var i = 0; i < importMe.length; i++) { var importedNode = this._importNode(importMe[i], false); this._nodeXML.insertBefore(importedNode, refChild._nodeXML); this._processAppendedChildren(importMe[i], this, this._attached, this._passThrough); } // inform Flash about the change if (this._attached && this._passThrough) { var xmlString = FlashHandler._encodeFlashData( xmlToStr(newChild, this._handler.document._namespaces)); this._handler.sendToFlash('jsInsertBefore', [ refChild._guid, this._guid, position, xmlString ]); } if (!isIE) { // _childNodes is an object literal instead of an array // to support getter/setter magic for Safari for (var i = 0; i < importMe.length; i++) { this._defineChildNodeAccessor(this._childNodes.length); this._childNodes.length++; } } // clear out the child if it is a DocumentFragment if (newChild.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { newChild._reset(); } return newChild._getProxyNode(); }, replaceChild: function(newChild /* _Node */, oldChild /* _Node */) { //console.log('replaceChild, newChild='+newChild.nodeName+', oldChild='+oldChild.nodeName); if (this.nodeType != _Node.ELEMENT_NODE && this.nodeType != _Node.DOCUMENT_FRAGMENT_NODE) { throw 'Not supported'; } // Issue 296: existing child should not be added again if (newChild.parentNode) { newChild.parentNode.removeChild(newChild); } // the children could be DOM nodes; turn them into something we can // work with, such as _Nodes or _Elements newChild = this._getFakeNode(newChild); oldChild = this._getFakeNode(oldChild); // flag that indicates that the child is a _DocumentFragment (keeps // the overall code smaller); we have to treat these differently since // we are importing all of the DocumentFragment's children rather than // just one new child var isFragment = (newChild.nodeType == _Node.DOCUMENT_FRAGMENT_NODE); var fragmentChildren; if (isFragment) { fragmentChildren = newChild._getChildNodes(true /* get fake nodes */); } // are we an empty DocumentFragment? if (isFragment && fragmentChildren.length == 0) { // nothing to do newChild._reset(); // clean out DocumentFragment return newChild._getProxyNode(); } // in our XML, find the index position of where oldChild used to be var findResults = this._findChild(oldChild); if (findResults === null) { // TODO: Throw the correct DOM error instead throw new Error('Invalid child passed to replaceChild'); } var position = findResults.position; // remove oldChild this.removeChild(oldChild); // import the newChild or all of the _DocumentFragment children into // ourselves, insert it into our XML, and process the newChild and all // its descendants var importMe = []; if (isFragment) { for (var i = 0; i < fragmentChildren.length; i++) { importMe.push(fragmentChildren[i]); } } else { importMe.push(newChild); } if (!isIE) { for (var i = 0; i < importMe.length; i++) { // _childNodes is an object literal instead of an array // to support getter/setter magic for Safari this._defineChildNodeAccessor(this._childNodes.length); this._childNodes.length++; } } var addToEnd = false; if (position >= this._nodeXML.childNodes.length) { addToEnd = true; } var insertAt = position; for (var i = 0; i < importMe.length; i++) { // import newChild into ourselves, telling importNode not to do an // appendChild since we will handle things ourselves manually later on var importedNode = this._importNode(importMe[i], false); // now bring the imported child into our XML where the oldChild used to be if (addToEnd) { // old node was at the end -- just do an appendChild this._nodeXML.appendChild(importedNode); } else { // old node is somewhere in the middle or beginning; jump one ahead // from the old position and do an insertBefore var placeBefore = this._nodeXML.childNodes[insertAt]; this._nodeXML.insertBefore(importedNode, placeBefore); insertAt++; } } // tell Flash about the newly inserted child if (this._attached && this._passThrough) { var xmlString = FlashHandler._encodeFlashData( xmlToStr(newChild, this._handler.document._namespaces)); this._handler.sendToFlash('jsAddChildAt', [ this._guid, position, xmlString ]); } // now process the newChild's node this._processAppendedChildren(newChild, this, this._attached, this._passThrough); // recursively set the removed node to be unattached and to not // pass through changes to Flash anymore oldChild._setUnattached(); // track this removed node so we can clean it up on page unload svgweb._removedNodes.push(oldChild._getProxyNode()); // clear out the child if it is a DocumentFragment if (newChild.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { newChild._reset(); } return oldChild._getProxyNode(); }, removeChild: function(child /* _Node or DOM Node */) { //console.log('removeChild, child='+child.nodeName+', this='+this.nodeName); if (this.nodeType != _Node.ELEMENT_NODE && this.nodeType != _Node.DOCUMENT_FRAGMENT_NODE) { throw 'Not supported'; } if (child.nodeType != _Node.ELEMENT_NODE && child.nodeType != _Node.TEXT_NODE) { throw 'Not supported'; } // the child could be a DOM node; turn it into something we can // work with, such as a _Node or _Element child = this._getFakeNode(child); // remove child from our list of XML var findResults = this._findChild(child); if (findResults === null) { // TODO: Throw the correct DOM error instead throw new Error('Invalid child passed to removeChild'); } var position = findResults.position; this._nodeXML.removeChild(findResults.nodeXML); // remove from our nodeById lookup table if (child.nodeType == _Node.ELEMENT_NODE) { var childID = child._getId(); if (childID && this._attached) { this._handler.document._nodeById['_' + childID] = undefined; } } // TODO: FIXME: Note that we don't remove the node from the GUID lookup // table; this is because developers might still be working with the // node while detached, and we want object equality to hold. This means // that memory will grow over time however. Find a good solution to this // issue without having to have the complex unattached child node structure // we had before. //svgweb._guidLookup['_' + child._guid] = undefined; // persist event listeners if this node is later reattached child._persistEventListeners(); // remove the getter/setter for this childNode for non-IE browsers if (!isIE) { // just remove the last getter/setter, since they all resolve // to a dynamic function anyway delete this._childNodes[this._childNodes.length - 1]; this._childNodes.length--; } else { // for IE, remove from _childNodes data structure this._childNodes.splice(position, 1); } // inform Flash about the change if (this._attached && this._passThrough) { this._handler.sendToFlash('jsRemoveChild', [ child._guid ]); } // recursively set the removed node to be unattached and to not // pass through changes to Flash anymore child._setUnattached(); // track this removed node so we can clean it up on page unload svgweb._removedNodes.push(child._getProxyNode()); return child._getProxyNode(); }, /** Appends the given child. The child can either be _Node, an actual DOM Node, or a Text DOM node created through document.createTextNode. We return either a _Node or an HTC reference depending on the browser. */ appendChild: function(child /* _Node or DOM Node */) { //console.log('appendChild, child='+child.nodeName+', this.nodeName='+this.nodeName); if (this.nodeType != _Node.ELEMENT_NODE && this.nodeType != _Node.DOCUMENT_FRAGMENT_NODE) { throw 'Not supported'; } // Issue 296: existing child should not be added again if (child.parentNode) { child.parentNode.removeChild(child); } // the child could be a DOM node; turn it into something we can // work with, such as a _Node or _Element child = this._getFakeNode(child); // flag that indicates that the child is a _DocumentFragment (keeps // the overall code smaller); we have to treat these differently since // we are importing all of the DocumentFragment's children rather than // just one new child var isFragment = (child.nodeType == _Node.DOCUMENT_FRAGMENT_NODE); var fragmentChildren; if (isFragment) { fragmentChildren = child._getChildNodes(true /* get fake nodes */); } // are we an empty DocumentFragment? if (isFragment && fragmentChildren.length == 0) { // nothing to do child._reset(); // clean out DocumentFragment return child._getProxyNode(); } // add the child's XML to our own if (isFragment) { for (var i = 0; i < fragmentChildren.length; i++) { this._importNode(fragmentChildren[i]); } } else { this._importNode(child); } if (isIE) { // _childNodes is a real array on IE rather than an object literal // like other browsers if (isFragment) { for (var i = 0; i < fragmentChildren.length; i++) { this._childNodes.push(fragmentChildren[i]._htcNode); } } else { this._childNodes.push(child._htcNode); } } else { // _childNodes is an object literal instead of an array // to support getter/setter magic for Safari if (isFragment) { for (var i = 0; i < fragmentChildren.length; i++) { this._defineChildNodeAccessor(this._childNodes.length); this._childNodes.length++; } } else { this._defineChildNodeAccessor(this._childNodes.length); this._childNodes.length++; } } // serialize this node and all its children into an XML string and // send that over to Flash if (this._attached && this._passThrough) { // note that if the child is a DocumentFragment that we simply send // the <__document__fragment> tag over to Flash so it knows what it is // dealing with var xmlString = FlashHandler._encodeFlashData( xmlToStr(child, this._handler.document._namespaces)); this._handler.sendToFlash('jsAppendChild', [ this._guid, xmlString ]); } // process the children (cache important info, add a handler, etc.) this._processAppendedChildren(child, this, this._attached, this._passThrough); // clear out the child if it is a DocumentFragment if (child.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { child._reset(); } return child._getProxyNode(); }, hasChildNodes: function() /* Boolean */ { return (this._getChildNodes().length > 0); }, // Note: cloneNode and normalize not supported isSupported: function(feature /* String */, version /* String */) { if (version == '2.0') { if (feature == 'Core') { // NOTE: There are a number of things we don't yet support in Core, // but we support the bulk of it return true; } else if (feature == 'Events' || feature == 'UIEvents' || feature == 'MouseEvents') { // NOTE: We plan on supporting most of these interfaces, but not // all of them return true; } } else { return false; } }, hasAttributes: function() /* Boolean */ { if (this.nodeType == _Node.ELEMENT_NODE) { for (var i in this._attributes) { // if this is an XMLNS declaration, don't consider it a valid // attribute for hasAttributes if (/^_xmlns/i.test(i)) { continue; } // if there is an ID attribute, but it's one of our randomly generated // ones, then don't consider this a valid attribute for hasAttributes if (i == '_id' && /^__svg__random__/.test(this._attributes[i])) { continue; } // ignore our internal __guid and __fakeTextNode attributes; // note that we have an extra _ before our attribute name when we // store it internally, so __guid becomes ___guid if (i == '___guid' && /^__guid/.test(this._attributes[i])) { continue; } if (i == '___fakeTextNode' && /^__fakeTextNode/.test(this._attributes[i])) { continue; } // our attributes start with an underscore if (/^_.*/.test(i) && this._attributes.hasOwnProperty(i)) { return true; } } return false; } else { return false; } }, /* DOM Level 2 EventTarget interface methods. Note: dispatchEvent not supported. Technically as well this interface should not appear on SVG elements that don't have any event dispatching, such as the SVG DESC element, but in our implementation they still appear. We also don't support the useCapture feature for addEventListener and removeEventListener. */ /* @param _adding Internal boolean flag used when we are adding this node to a real DOM, so that we can replay and send our addEventListener request over to Flash. */ addEventListener: function(type, listener /* Function */, useCapture, _adding /* Internal -- Boolean */) { //console.log('addEventListener, type='+type+', listener='+listener+', useCapture='+useCapture+', _adding='+_adding); // NOTE: capturing not supported if (this.nodeType != _Node.ELEMENT_NODE && this.nodeType != _Node.TEXT_NODE) { throw 'Not supported'; } if (!_adding && !this._attached) { // add to a list of event listeners that will get truly registered when // we get attached in _Node._processAppendedChildren() this._detachedListeners.push({ type: type, listener: listener, useCapture: useCapture }); return; } // add to our list of event listeners if (this._listeners[type] === undefined) { this._listeners[type] = []; } this._listeners[type].push({ type: type, listener: listener, useCapture: useCapture }); this._listeners[type]['_' + listener.toString() + ':' + useCapture] = listener; if (type == 'keydown') { // TODO: Be able to handle key events on individual SVG graphics // (g, rect, etc.) that might have focus // TODO: FIXME: do we want to be adding this listener to 'document' // when dealing with SVG OBJECTs? // prevent closure by using an inline method var wrappedListener = (function(listener) { return function(evt) { // shim in preventDefault function for IE if (!evt.preventDefault) { evt.preventDefault = function() { this.returnValue = false; evt = null; } } // call the developer's listener now if (typeof listener == 'object') { listener.handleEvent.call(listener, evt); } else { listener(evt); } } })(listener); // persist information about this listener so we can easily remove // it later wrappedListener.__type = type; wrappedListener.__listener = listener; wrappedListener.__useCapture = useCapture; // save keyboard listeners for later so we can clean them up // later if the parent SVG document is removed from the DOM this._handler._keyboardListeners.push(wrappedListener); // now actually subscribe to the event this._addEvent(document, type, wrappedListener); return; } this._handler.sendToFlash('jsAddEventListener', [ this._guid, type ]); }, removeEventListener: function(type, listener /* Function */, useCapture) { // NOTE: capturing not supported if (this.nodeType != _Node.ELEMENT_NODE && this.nodeType != _Node.TEXT_NODE) { throw 'Not supported'; } var pos; if (!this._attached) { // remove from our list of event listeners that we keep around until // _Node._processAppendedChildren() is called pos = this._findListener(this._detachedListeners, type, listener, useCapture); if (pos !== null) { delete this._detachedListeners[pos]; } return; } // remove from our list of event listeners pos = this._findListener(this._listeners, type, listener, useCapture); if (pos !== null) { // FIXME: Ensure that if identical listeners are added twice that they collapse to // just one entry or else this will fail to delete more than the first one. delete this._listeners[pos]; delete this._listeners[type]['_' + listener.toString() + ':' + useCapture]; } if (type == 'keydown') { // FIXME: We really need to remove keypress logic from being handled by us pos = this._findListener(this._keyboardListeners, type, listener, useCapture); if (pos !== null) { // FIXME: Ensure that if identical listeners are added twice that they collapse to // just one entry or else this will fail to delete more than the first one. delete this._keyboardListeners[pos]; } } this._handler.sendToFlash('jsRemoveEventListener', [ this._guid, type ]); }, getScreenCTM: function() { var msg = this._handler.sendToFlash('jsGetScreenCTM', [ this._guid ]); msg = this._handler._stringToMsg(msg); return new _SVGMatrix(new Number(msg.a), new Number(msg.b), new Number(msg.c), new Number(msg.d), new Number(msg.e), new Number(msg.f), this._handler); }, getCTM: function() { return this.getScreenCTM(); }, /** Clones the given node. @param deepClone Whether this is a shallow clone or a deep clone copying all of our children. */ cloneNode: function(deepClone) { //console.log('cloneNode, ns='+this.namespaceURI+', nodeName='+this.nodeName); var clone; // if we are a non-SVG, non-HTML node, such as a namespaced node inside // of an SVG metadata node, handle this a bit differently if (this.nodeType == _Node.ELEMENT_NODE && this.namespaceURI != svgns) { clone = new _Element(this.nodeName, this.prefix, this.namespaceURI); } else if (this.nodeType == _Node.ELEMENT_NODE) { clone = document.createElementNS(this.namespaceURI, this.nodeName); } else if (this.nodeType == _Node.TEXT_NODE) { clone = document.createTextNode(this._nodeValue, true); } else if (this.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { clone = document.createDocumentFragment(true); } else { throw 'cloneNode not supported for nodeType: ' + this.nodeType; } clone = this._getFakeNode(clone); // copy over our attributes var attrs = this._nodeXML.attributes; for (var i = 0; i < attrs.length; i++) { var attr = attrs.item(i); // IE doesn't have localName or prefix; they are munged together var m = attr.name.match(/([^:]+):?(.*)/); var ns = attr.namespaceURI; // Safari doesn't like setting xmlns declarations with createAttributeNS; // we have to do it this way unfortunately if (isSafari && attr.name.indexOf('xmlns') != -1) { clone._nodeXML.setAttribute(attr.name, attr.nodeValue); } else { // browsers other than Safari // IE doesn't have namespace aware setAttribute methods var attrNode; var doc = clone._nodeXML.ownerDocument; if (isIE) { attrNode = doc.createNode(2, attr.name, ns); } else { attrNode = doc.createAttributeNS(ns, attr.name); } attrNode.nodeValue = attr.nodeValue; if (isIE) { clone._nodeXML.setAttributeNode(attrNode); } else { clone._nodeXML.setAttributeNodeNS(attrNode); } } } // make sure our XML has our correct new cloned GUID clone._nodeXML.setAttribute('__guid', clone._guid); // for IE, copy over cached style values if (isIE) { var copyStyle = this._htcNode.style; for (var i = 0; i < copyStyle.length; i++) { var styleName = copyStyle.item(i); var styleValue = copyStyle.getPropertyValue(styleName); // bump the length on our real style object and on our fake one clone._htcNode.style.length++; clone.style.length++; // add the new style to our real style object and ignore style // changes temporarily so we don't end up in an infinite loop of // asynchronous style updates from onpropertychange events clone.style._ignoreStyleChanges = true; clone._htcNode.style[styleName] = styleValue; clone.style._ignoreStyleChanges = false; } } // update internal attributes table as well on the clone if (clone.nodeType == _Node.ELEMENT_NODE) { clone._importAttributes(clone, clone._nodeXML); } // clone each of the children and add them if (deepClone && (clone.nodeType == _Node.ELEMENT_NODE || clone.nodeType == _Node.DOCUMENT_FRAGMENT_NODE)) { var children = this._getChildNodes(); for (var i = 0; i < children.length; i++) { var childClone = children[i].cloneNode(true); clone.appendChild(childClone); } } // make sure our ownerDocument is right clone.ownerDocument = this.ownerDocument; return clone._getProxyNode(); }, toString: function() { if (this.namespaceURI == svgns) { return '[_SVG' + this.localName.charAt(0).toUpperCase() + this.localName.substring(1) + ']'; } else if (this.prefix) { return '[' + this.prefix + ':' + this.localName + ']'; } else if (this.localName) { return '[' + this.localName + ']'; } else { return '[' + this.nodeName + ']'; } }, /** Adds an event cross platform. @param obj Obj to add event to. @param type String type of event. @param fn Function to execute when event happens. */ _addEvent: function(obj, type, fn) { if (obj.addEventListener) { obj.addEventListener(type, fn, false); } else if (obj.attachEvent) { // IE obj['e'+type+fn] = fn; // do a trick to prevent closure over ourselves, which can lead to // IE memory leaks obj[type+fn] = (function(obj, type, fn) { return function(){ obj['e'+type+fn](window.event) }; })(obj, type, fn); obj.attachEvent('on'+type, obj[type+fn]); } }, // NOTE: technically the following attributes should be read-only, // raising DOMExceptions if set, but for simplicity we make them // simple JS properties instead. If set nothing will happen. nodeName: null, nodeType: null, ownerDocument: null, /* Document or _Document depending on context. */ namespaceURI: null, localName: null, prefix: null, /* Note: in the DOM 2 spec this is settable but not for us */ // getter/setter attribute methods // nodeValue defined as getter/setter // textContent and data defined as getters/setters for TEXT_NODES // childNodes defined as getter/setter _getParentNode: function() { if (this.nodeType == _Node.DOCUMENT_NODE || this.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { return null; } // are we the root SVG node when being embedded by an SVG SCRIPT? // If _handler is not set, this element is a detached svg element. if (this._handler && this._getProxyNode() == this._handler.document.rootElement) { if (this._handler.type == 'script') { return this._handler.flash.parentNode; } else if (this._handler.type == 'object') { // if we are the root SVG node and are embedded by an SVG OBJECT, then // our parent is a #document object return this._handler.document; } } var parentXML = this._nodeXML.parentNode; // unattached nodes might have an XML document as their parentNode if (parentXML === null || parentXML.nodeType == _Node.DOCUMENT_NODE) { return null; } var node = FlashHandler._getNode(parentXML, this._handler); return node; }, _getFirstChild: function() { if (this.nodeType == _Node.TEXT_NODE) { return null; } var childXML = this._nodeXML.firstChild; if (childXML === null) { return null; } var node = FlashHandler._getNode(childXML, this._handler); this._getFakeNode(node)._passThrough = this._passThrough; return node; }, _getLastChild: function() { if (this.nodeType == _Node.TEXT_NODE) { return null; } var childXML = this._nodeXML.lastChild; if (childXML === null) { return null; } var node = FlashHandler._getNode(childXML, this._handler); this._getFakeNode(node)._passThrough = this._passThrough; return node; }, _getPreviousSibling: function() { if (this.nodeType == _Node.DOCUMENT_NODE || this.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { return null; } // are we the root SVG object when being embedded by an SVG SCRIPT? // If _handler is not set, this element is a nested svg element. if (this._handler && this._getProxyNode() == this._handler.document.rootElement && this._handler.type == 'script') { var sibling = this._handler.flash.previousSibling; // is our previous sibling also an SVG object? if (sibling && sibling.nodeType == 1 && sibling.className && sibling.className.indexOf('embedssvg') != -1) { var rootID = sibling.getAttribute('id').replace('_flash', ''); var node = svgweb.handlers[rootID].document.documentElement; return node._getProxyNode(); } else { return sibling; } } var siblingXML = this._nodeXML.previousSibling; // unattached nodes will sometimes have an XML Processing Instruction // as their previous node (type=7) if (siblingXML === null || siblingXML.nodeType == 7) { return null; } var node = FlashHandler._getNode(siblingXML, this._handler); this._getFakeNode(node)._passThrough = this._passThrough; return node; }, _getNextSibling: function() { if (this.nodeType == _Node.DOCUMENT_NODE || this.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { return null; } // are we the root SVG object when being embedded by an SVG SCRIPT? // If _handler is not set, this element is a nested svg element. if (this._handler && this._getProxyNode() == this._handler.document.rootElement && this._handler.type == 'script') { var sibling = this._handler.flash.nextSibling; // is our previous sibling also an SVG object? if (sibling && sibling.nodeType == 1 && sibling.className && sibling.className.indexOf('embedssvg') != -1) { var id = sibling.getAttribute('id').replace('_flash', ''); var node = this._handler.document._nodeById['_' + id]; return node._getProxyNode(); } else { return sibling; } } var siblingXML = this._nodeXML.nextSibling; if (siblingXML === null) { return null; } var node = FlashHandler._getNode(siblingXML, this._handler); this._getFakeNode(node)._passThrough = this._passThrough; return node; }, // Note: 'attributes' property not supported since we don't support // Attribute DOM Node types // TODO: It would be nice to support the ElementTraversal spec here as well // since it cuts down on code size: // http://www.w3.org/TR/ElementTraversal/ /** The passthrough flag controls whether we 'pass through' any changes to this object to the underlying Flash viewer. For example, if a Node has been created but is not yet attached to the document, any changes to its attributes should not pass through to the Flash viewer, and this flag would therefore be false. After the Node is attached through appendChild(), passThrough would become true and everything would get passed through to Flash for rendering. */ _passThrough: false, /** The attached flag indicates whether this node is attached to a live DOM yet. For example, if you call createElementNS, you can set values on this node before actually appending it using appendChild to a node that is connected to the actual visible DOM, ready to be rendered. */ _attached: true, /** A flag we put on our _Nodes and _Elements to indicate they are fake; useful if someone wants to 'break' the abstraction and see if a node is a real DOM node or not (which won't have this flag). */ _fake: true, /** Do the getter/setter magic for our attributes for non-IE browsers. */ _defineNodeAccessors: function() { // readonly properties this.__defineGetter__('parentNode', hitch(this, this._getParentNode)); this.__defineGetter__('firstChild', hitch(this, this._getFirstChild)); this.__defineGetter__('lastChild', hitch(this, this._getLastChild)); this.__defineGetter__('previousSibling', hitch(this, this._getPreviousSibling)); this.__defineGetter__('nextSibling', hitch(this, this._getNextSibling)); // childNodes array -- define and execute an inline function so that we // only get closure over the 'self' variable rather than all the // __defineGetter__ calls above. Note that we are forced to have our // childNodes variable be an object literal rather than array, since this // is the only way we can do getter/setter magic on each indexed position // for Safari. this.__defineGetter__('childNodes', (function(self) { return function() { return self._childNodes; }; })(this)); // We represent Text nodes internally using XML Element nodes in order // to support tracking; just set our child nodes to be zero to simulate // Text nodes having no children if (this.nodeName == '#text') { this._childNodes.length = 0; } else { var children = this._nodeXML.childNodes; this._childNodes.length = children.length; for (var i = 0; i < children.length; i++) { // do the defineGetter in a different method so the closure gets // formed correctly (closures can be tricky in loops if you are not // careful); we also need the defineChildNodeAccessor method anyway // since we need the ability to individually define new accessors // at a later point (such as in insertBefore(), for example). this._defineChildNodeAccessor(i); } } // read/write properties if (this.nodeType == _Node.TEXT_NODE) { this.__defineGetter__('data', (function(self) { return function() { return self._nodeValue; }; })(this)); this.__defineSetter__('data', (function(self) { return function(newValue) { return self._setNodeValue(newValue); }; })(this)); this.__defineGetter__('textContent', (function(self) { return function() { return self._nodeValue; }; })(this)); this.__defineSetter__('textContent', (function(self) { return function(newValue) { return self._setNodeValue(newValue); }; })(this)); } else { // ELEMENT and DOCUMENT nodes // Firefox and Safari return '' for textContent for non-text nodes; // mimic this behavior this.__defineGetter__('textContent', (function() { return function() { return ''; }; })()); } this.__defineGetter__('nodeValue', (function(self) { return function() { return self._nodeValue; }; })(this)); this.__defineSetter__('nodeValue', (function(self) { return function(newValue) { return self._setNodeValue(newValue); }; })(this)); }, /** Creates a getter/setter for a childNode at the given index position. We define each one in a separate function so that we don't pull the wrong things into our closure. See _defineNodeAccessors() for details. */ _defineChildNodeAccessor: function(i) { var self = this; this._childNodes.__defineGetter__(i, function() { var childXML = self._nodeXML.childNodes[i]; var node = FlashHandler._getNode(childXML, self._handler); node._passThrough = self._passThrough; return node; }); }, /** For IE we have to do some tricks that are a bit different than the other browsers; we can't know when a particular indexed member is called, such as childNodes[1], so instead we return the entire _childNodes array; what is nice is that IE applies the indexed lookup _after_ we've returned things, so this works. This requires us to instantiate all the children, however, when childNodes is called. This method is called by the HTC file. @param returnFakeNodes Optional. If true, then we return our fake nodes; if false, then we return the HTC proxy for IE. Defaults to false. @returns An array of either the HTC proxies for our nodes if IE, or an array of _Element and _Nodes for other browsers. */ _getChildNodes: function(returnFakeNodes) { if (!isIE) { return this._childNodes; } if (returnFakeNodes === undefined) { returnFakeNodes = false; } // NOTE: for IE we return a real full Array, while for other browsers // our _childNodes array is an object literal in order to do // our __defineGetter__ magic in _defineNodeAccessors. It turns out // that on IE a full array can be returned from the getter, and _then_ // the index can get applied (i.e. our array is returned, and then // [2] might get applied to that array). var results = createNodeList(); // We represent our text nodes using an XML Element node instead of an // XML Text node in order to do tracking; we store our actual text value // as a further XML Text node child. Don't return this though. if (this.nodeName == '#text') { return results; } if (this._nodeXML.childNodes.length == this._childNodes.length && !returnFakeNodes) { // we've already processed our childNodes before return this._childNodes; } else { for (var i = 0; i < this._nodeXML.childNodes.length; i++) { var childXML = this._nodeXML.childNodes[i]; var node = FlashHandler._getNode(childXML, this._handler); node._fakeNode._passThrough = this._passThrough; if (returnFakeNodes) { node = node._fakeNode; } results.push(node); } this._childNodes = results; return results; } }, /** If we are IE, we must use an HTC behavior in order to get onpropertychange and override core DOM methods. */ _createHTC: function() { //console.log('createHTC'); // we store our HTC nodes into a hidden container located in the // BODY of the document; either get it now or create one on demand if (!this._htcContainer) { this._htcContainer = document.getElementById('__htc_container'); if (!this._htcContainer) { // NOTE: Strangely, onpropertychange does _not_ fire for HTC elements // that are in the HEAD of the document, which is where we used // to put the htc_container. Instead, we have to put it into the BODY // of the document and position it offscreen. var body = document.getElementsByTagName('body')[0]; var c = document.createElement('div'); c.id = '__htc_container'; // NOTE: style.display = 'none' does not work c.style.position = 'absolute'; c.style.top = '-5000px'; c.style.left = '-5000px'; body.appendChild(c); this._htcContainer = c; } } // now store our HTC UI node into this container; we will intercept // all calls through the HTC, and implement all the real behavior // inside ourselves (inside _Element) // Note: we do svg: even if we are dealing with a non-SVG node on IE, // such as sodipodi:namedview; this is necessary so that our svg.htc // file gets invoked for all these nodes, which is by necessity bound to // the svg: namespace var htcNode = document.createElement('svg:' + this.nodeName); htcNode._fakeNode = this; htcNode._handler = this._handler; this._htcContainer.appendChild(htcNode); this._htcNode = htcNode; }, _setNodeValue: function(newValue) { //console.log('setNodeValue, newValue='+newValue); if (this.nodeType != _Node.TEXT_NODE) { return newValue; } this._nodeValue = newValue; // we store the real text value as a child of our fake text node, // which is actually a DOM Element so that we can do tracking this._nodeXML.firstChild.nodeValue = newValue; if (this._attached && this._passThrough) { var flashStr = FlashHandler._encodeFlashData(newValue); var parentGUID = this._nodeXML.parentNode.getAttribute('__guid'); this._handler.sendToFlash('jsSetText', [ parentGUID, this._guid, flashStr ]); } return newValue; }, /** For functions like appendChild, insertBefore, removeChild, etc. outside callers can pass in DOM nodes, etc. This function turns this into something we can work with, such as a _Node or _Element. */ _getFakeNode: function(node) { if (!node) { node = this; } // Was an HTC node passed in for IE? If so, get its _Node if (isIE && node._fakeNode) { node = node._fakeNode; } return node; }, /** We do a bunch of work in this method in order to append a child to ourselves, including: Walking over the child and all of it's children; setting it's handler; setting that it is both attached and can pass through it's values; informing Flash about the newly created element; and updating our list of namespaces if there is a node with a new namespace in the appended children. This method gets called recursively for the child and all of it's children. @param child _Node to work with. @param parent The parent of this child. @param attached Boolean on whether we are attached or not yet. @param passThrough Boolean on whether to pass values through to Flash or not. */ _processAppendedChildren: function(child, parent, attached, passThrough) { //console.log('processAppendedChildren, this.nodeName='+this.nodeName+', child.nodeName='+child.nodeName+', attached='+attached+', passThrough='+passThrough); // walk the DOM from the child using an iterative algorithm, which was // found to be faster than a recursive one; for each node visited we will // store some important reference information var current; var suspendID; if (child.nodeType == _Node.DOCUMENT_FRAGMENT_NODE) { current = this._getFakeNode(child._getFirstChild()); } else { current = child; } // turn on suspendRedraw so adding our event handlers happens in one go if (attached && passThrough) { suspendID = this._handler._redrawManager.suspendRedraw(10000, false); } while (current) { //console.log('current, nodeName='+current.nodeName); // visit this node var currentXML = current._nodeXML; // set its handler current._handler = this._handler; // store a reference to our node so we can return it in the future var id = currentXML.getAttribute('id'); if (attached && current.nodeType == _Node.ELEMENT_NODE && id) { this._handler.document._nodeById['_' + id] = current; } // set the ownerDocument based on how we were embedded if (attached) { if (this._handler.type == 'script') { current.ownerDocument = document; } else if (this._handler.type == 'object') { current.ownerDocument = this._handler.document; } // register and send over any event listeners that were added while // this node was detached for (var i = 0; i < current._detachedListeners.length; i++) { var addMe = current._detachedListeners[i]; if (addMe) { current.addEventListener(addMe.type, addMe.listener, addMe.useCapture, true); } } current._detachedListeners = []; } // now continue visiting other nodes var lastVisited = current; var children = current._getChildNodes(); var next = (children && children.length > 0) ? children[0] : null; if (next) { current = next; if (isIE) { current = current._fakeNode; } } while (!next && current) { if (current != child) { next = current._getNextSibling(); if (next) { current = next; if (isIE) { current = current._fakeNode; } break; } } if (current == child) { current = null; } else { current = current._getParentNode(); if (current && isIE) { current = current._fakeNode; } // Do not traverse non-elements or retrace up past the root if (current && ((current.nodeType != 1) || (current._handler && current._getProxyNode() == current._handler.document.rootElement ))) { current = null; } } } // set our attached information lastVisited._attached = attached; lastVisited._passThrough = passThrough; } // turn off suspendRedraw. all event handlers should shoot through now if (attached && passThrough) { this._handler._redrawManager.unsuspendRedraw(suspendID, false); } }, /** Imports the given child and all it's children's XML into our XML. @param child _Node to import. @param doAppend Optional. Boolean on whether to actually append the child once it is imported. Useful for functions such as replaceChild that want to do this manually. Defaults to true if not specified. @returns The imported node. */ _importNode: function(child, doAppend) { //console.log('importNode, child='+child.nodeName+', doAppend='+doAppend); if (typeof doAppend == 'undefined') { doAppend = true; } // try to import the node into our _Document's XML object var doc; if (this._attached) { doc = this._handler.document._xml; } else { doc = this._nodeXML.ownerDocument; } // IE does not support document.importNode, even on XML documents, // so we have to define it ourselves. // Adapted from ALA article: // http://www.alistapart.com/articles/crossbrowserscripting var importedNode; if (typeof doc.importNode == 'undefined') { // import the node for IE importedNode = document._importNodeFunc(doc, child._nodeXML, true); } else { // non-IE browsers importedNode = doc.importNode(child._nodeXML, true); } // complete the import into ourselves if (doAppend) { this._nodeXML.appendChild(importedNode); } // replace all of the children's XML with our copy of it now that it // is imported child._importChildXML(importedNode); return importedNode; }, /** Recursively replaces the XML inside of our children with the given new XML to ensure that each node's reference to it's own internal _nodeXML pointer all points to the same tree, but in different locations. Called after we are importing a node into ourselves with appendChild. */ _importChildXML: function(newXML) { this._nodeXML = newXML; var children = this._getChildNodes(); for (var i = 0; i < children.length; i++) { var currentChild = children[i]; if (isIE && currentChild._fakeNode) { // IE currentChild = currentChild._fakeNode; } currentChild._nodeXML = this._nodeXML.childNodes[i]; currentChild._importChildXML(this._nodeXML.childNodes[i]); } }, /** Tries to find the given child in our list of child nodes. @param child A _Node or _Element to search for in our list of childNodes. @param ignoreTextNodes Optional, defaults to false. If true, then we ignore any text nodes when looking for the child, as if all we have are element nodes. @returns Null if nothing found, otherwise an object literal with 2 values: position The index position of where the child is located. nodeXML The found XML node. If the child is not found then null is returned instead. */ _findChild: function(child, ignoreTextNodes) { //console.log('findChild, child='+child.nodeName); if (ignoreTextNodes === undefined) { ignoreTextNodes = false; } var results = {}; var elementIndex = 0; for (var i = 0; i < this._nodeXML.childNodes.length; i++) { var currentXML = this._nodeXML.childNodes[i]; if (currentXML.nodeType != _Node.ELEMENT_NODE && currentXML.nodeType != _Node.TEXT_NODE) { // FIXME: What about CDATA nodes? // FIXME: If there are other kinds of nodes, how does this impact // our elementIndex variable? continue; } // skip text nodes? if (ignoreTextNodes && (currentXML.getAttribute('__fakeTextNode') || currentXML.nodeType == _Node.TEXT_NODE)) { continue; } if (currentXML.nodeType == _Node.ELEMENT_NODE) { elementIndex++; } if (currentXML.nodeType == _Node.ELEMENT_NODE && currentXML.getAttribute('__guid') == child._guid) { results.position = (ignoreTextNodes) ? elementIndex : i; results.nodeXML = currentXML; return results; } } return null; }, /** After a node is unattached, such as through a removeChild, this method recursively sets _attached and _passThrough to false on this node and all of its children. */ _setUnattached: function() { // set each child to be unattached var children = this._getChildNodes(); for (var i = 0; i < children.length; i++) { var child = children[i]; if (isIE) { child = child._fakeNode; } child._setUnattached(); } this._attached = false; this._passThrough = false; this._handler = null; }, /** When we return results to external callers, such as appendChild, we can return one of our fake _Node or _Elements. However, for IE, we have to return the HTC 'proxy' through which callers manipulate things. The HTC is what allows us to override core DOM methods and know when property and style changes have happened, for example. */ _getProxyNode: function() { if (!isIE) { return this; } else { // for IE, the developer will manipulate things through the UI/HTC // proxy facade so that we can know about property changes, etc. return this._htcNode; } }, /** Creates our childNodes data structure in a different way for different browsers. We have this in a separate method so that we avoid forming a closure of elements that could lead to a memory leak in IE. */ _createChildNodes: function() { var childNodes; if (!isIE) { // NOTE: we make _childNodes an object literal instead of an Array; if // it is an array we can't do __defineGetter__ on each index position on // Safari childNodes = {}; // add the item() method from NodeList to our childNodes instance childNodes.item = function(index) { if (index >= this.length) { return null; // DOM Level 2 spec says return null } else { return this[index]; } }; } else { // IE childNodes = createNodeList(); } return childNodes; }, // the following getters and setters for textContent and data are called // by the HTC; we put them here to minimize the size of the HTC which // has a very strong correlation with performance _getTextContent: function() { if (this.nodeType == _Node.TEXT_NODE) { return this._nodeValue; } else { return ''; // Firefox and Safari return empty strings for .textContent } }, _setTextContent: function(newValue) { if (this.nodeType == _Node.TEXT_NODE) { return this._setNodeValue(newValue); } else { return ''; // Firefox and Safari return empty strings for .textContent } }, _getData: function() { if (this.nodeType == _Node.TEXT_NODE) { return this._nodeValue; } else { return undefined; } }, _setData: function(newValue) { if (this.nodeType == _Node.TEXT_NODE) { return this._setNodeValue(newValue); } else { return undefined; } }, /** For Internet Explorer, the length of the script in our HTC is a major determinant in the amount of time it takes to create a new HTC element. In order to minimize the size of this code, we have many 'no-op' implementations of some methods so that we can just safely call them from the HTC without checking the type of the node inside the HTC. */ _createEmptyMethods: function() { if (this.nodeType == _Node.TEXT_NODE) { this.getAttribute = this.getAttributeNS = this.setAttribute = this.setAttributeNS = this.removeAttribute = this.removeAttributeNS = this.hasAttribute = this.hasAttributeNS = this.getElementsByTagNameNS = this._getId = this._setId = this._getX = this._getY = this._getWidth = this._getHeight = this._getCurrentScale = this._setCurrentScale = this._getCurrentTranslate = this.createSVGRect = this.createSVGPoint = function() { return undefined; }; } }, /** When a node is removed from the DOM, we make sure that all of its event listener information (and all of the event info for its children) is persisted if it is later reattached to the DOM. */ _persistEventListeners: function() { // persist all the listeners for ourselves for (var eventType in this._listeners) { for (var i = 0; i < this._listeners[eventType].length; i++) { var l = this._listeners[eventType][i]; this._detachedListeners.push({ type: l.type, listener: l.listener, useCapture: l.useCapture }); } } this._listeners = []; // visit each of our children var children = this._getChildNodes(); for (var i = 0; i < children.length; i++) { var c = children[i]; if (c._fakeNode) { // IE c = c._fakeNode; } c._persistEventListeners(); } }, /** Finds a listener in the given listenerArray using the given type, listener, and useCapture values, returning the index position. Returns null the listener is not found. */ _findListener: function(listenerArray, type, listener, useCapture) { for (var i = 0; i < listenerArray.length; i++) { var l = listenerArray[i]; if (l.listener == listener && l.type == type && l.useCapture == useCapture) { return i; } } return null; } }); /** Our DOM Element for each SVG node. @param nodeName The node name, such as 'rect' or 'sodipodi:namedview'. @param prefix The namespace prefix, such as 'svg' or 'sodipodi'. @param namespaceURI The namespace URI. If undefined, defaults to null. @param nodeXML The parsed XML DOM node for this element. @param handler The FlashHandler rendering this node. @param passThrough Optional boolean on whether any changes to this element 'pass through' and cause changes in the Flash renderer. */ function _Element(nodeName, prefix, namespaceURI, nodeXML, handler, passThrough) { if (nodeName === undefined && namespaceURI === undefined && nodeXML === undefined && handler === undefined) { // prototype subclassing return; } // superclass constructor _Node.apply(this, [nodeName, _Node.ELEMENT_NODE, prefix, namespaceURI, nodeXML, handler, passThrough]); // setup our attributes this._attributes = {}; this._attributes['_id'] = ''; // default id is empty string on FF and Safari this._importAttributes(this, this._nodeXML); // define our accessors if we are not IE; IE does this by using the HTC // file rather than doing it here if (!isIE) { this._defineAccessors(); } if (this.namespaceURI == svgns) { // track .style changes; if (isIE && this._attached && this._handler && this._handler.type == 'script' && this.nodeName == 'svg') { // do nothing now -- if we are IE and are being embedded with an // SVG SCRIPT tag, don't setup the style object for the SVG root now; we // do that later in _SVGSVGElement } else { this.style = new _Style(this); } // handle style changes for HTCs if (isIE && this._attached && this._handler && this._handler.type == 'script' && this.nodeName == 'svg') { // do nothing now - if we are IE we delay creating the style property // until later in _SVGSVGElement } else if (isIE) { this.style._ignoreStyleChanges = false; } } } // subclasses _Node _Element.prototype = new _Node; extend(_Element, { getAttribute: function(attrName) /* String */ { return this.getAttributeNS(null, attrName, true); }, /** Namespace aware function to get an attribute from a node. @param ns The namespace. @param localName The local name of the attribute, without the prefix. Note, though, that Webkit and Firefox allow the prefix form to be passed in as well, which will cause a namespace lookup to happen. @param _forceNull Internal boolean flag used by our fake getAttribute() method. Needed to match the native browser behavior of returning attributes that don't exist; see the comment near the end of the function for details. */ getAttributeNS: function(ns, localName, _forceNull) /* String */ { //console.log('getAttributeNS, ns='+ns+', localName='+localName+', this.nodeName='+this.nodeName); var value; // ignore internal __guid property if (ns == null && localName == '__guid') { return null; } // Make sure we are attached and aren't in the middle of a // suspendRedraw operation. if (this._attached && this._passThrough && !this._handler._redrawManager.isSuspended()) { value = this._handler.sendToFlash('jsGetAttribute', [ this._guid, false, false, ns, localName, true ]); } else { if (!isIE) { value = this._nodeXML.getAttributeNS(ns, localName); } else if (isIE) { // IE has no getAttributeNS if (!ns) { value = this._nodeXML.getAttribute(localName); } else { // IE has funky namespace support; we possibly have no prefix at this // point so we will have to enumerate all attributes to find the one // we want for (var i = 0; i < this._nodeXML.attributes.length; i++) { var attr = this._nodeXML.attributes.item(i); // IE has no localName property; it munges the prefix:localName // together var attrName = new String(attr.name).match(/[^:]*:?(.*)/)[1]; if (attr.namespaceURI && attr.namespaceURI == ns && attrName == localName) { value = attr.nodeValue; break; } } } } } // id property is special; we return an empty string instead of null // to mimic native behavior on Firefox and Safari if (ns == 'null' && localName == 'id' && !value) { return ''; } // Firefox and Webkit both return null when getAttribute() is called // on unknown element, but return '' when getAttributeNS() is called // on empty element; match this behavior. We pass in a boolean // '_forceNull' flag when calling getAttributeNS from our own fake // getAttribute method. if (value === undefined || value === null || /^[ ]*$/.test(value)) { return (_forceNull) ? null : ''; } return value; }, removeAttribute: function(name) /* void */ { /* throws DOMException */ this.removeAttributeNS(null, name); }, removeAttributeNS: function(ns, localName) /* void */ { /* throws DOMException */ //console.log('removeAttributeNS, ns='+ns+', localName='+localName); // if id then change node lookup table (only if we are attached to // the DOM however) if (localName == 'id' && this._attached && this.namespaceURI == svgns) { var doc = this._handler.document; var elementId = this._nodeXML.getAttribute('id'); // old lookup doc._nodeById['_' + elementId] = undefined; } // we might not be able to get a prefix to namespace mapping if we are // disconnected; loop through our attributes until we find the matching // attribute node var attrNode; if (!ns) { attrNode = this._nodeXML.getAttributeNode(localName); } else { for (var i = 0; i < this._nodeXML.attributes.length; i++) { var current = this._nodeXML.attributes.item(i); // IE has no localName property; it munges the prefix:localName // together var m = new String(current.name).match(/([^:]+:)?(.*)/); var prefix, attrName; if (current.name.indexOf(':') != -1) { prefix = m[1]; attrName = m[2]; } else { attrName = m[1]; } if (current.namespaceURI && current.namespaceURI == ns && attrName == localName) { attrNode = current; break; } } } if (!attrNode) { // JL (jamie love) Remove this warning, The way that protovis works // means it is called a lot. //console.log('No attribute node found for: ' + localName // + ' in the namespace: ' + ns); return; } // remove from our XML this._nodeXML.removeAttributeNode(attrNode); // remove from our attributes list var qName = localName; if (ns) { qName = prefix + ':' + localName; } this._attributes['_' + qName] = undefined; // send to Flash if (this._attached && this._passThrough) { this._handler.sendToFlash('jsRemoveAttribute', [ this._guid, ns, localName ]); } }, setAttribute: function(attrName, attrValue /* String */) /* void */ { //console.log('setAttribute, attrName='+attrName+', attrValue='+attrValue); this.setAttributeNS(null, attrName, attrValue); }, setAttributeNS: function(ns, qName, attrValue /* String */) /* void */ { //console.log('setAttributeNS, ns='+ns+', qName='+qName+', attrValue='+attrValue+', this.nodeName='+this.nodeName); // Issue 428: // "setAttribute gives error for undefined or null attribute value // (Flash renderer)" // http://code.google.com/p/svgweb/issues/detail?id=428 if (attrValue === null || typeof attrValue == 'undefined') { attrValue = ''; } // parse out local name of attribute var localName = qName; if (qName.indexOf(':') != -1) { localName = qName.split(':')[1]; } // if id then change node lookup table (only if we are attached to // the DOM however) if (this._attached && qName == 'id') { var doc = this._handler.document; var elementId = this._nodeXML.getAttribute('id'); // old lookup doc._nodeById['_' + elementId] = undefined; if (elementId === 0 || elementId) { // new lookup doc._nodeById['_' + attrValue] = this; } } /* Safari has a wild bug; If you have an element inside of a clipPath with a style string: Then calling setAttribute('style', '') on our nodeXML causes the browser to crash! The workaround is to temporarily remove nodes that have a clipPath parent, set their style, then reattach them (!) */ if (isSafari && localName == 'style' && this._nodeXML.parentNode !== null && this._nodeXML.parentNode.nodeName == 'clipPath') { // save our XML position information for later re-inserting var addBeforeXML = this._nodeXML.nextSibling; var origParent = this._nodeXML.parentNode; // remove the node and set style; doing this prevents crash when // setting style string this._nodeXML.parentNode.removeChild(this._nodeXML); this._nodeXML.setAttribute('style', attrValue); // re-attach ourselves before our old sibling if (addBeforeXML) { origParent.insertBefore(this._nodeXML, addBeforeXML); } else { // node was at end originally origParent.appendChild(this._nodeXML); } } else { // we are an attrname other than style, or on a non-Safari browser // update our XML if (ns && isIE) { // MSXML has its own custom funky way of dealing with namespaces, // so we have to do it this way var attrNode = this._nodeXML.ownerDocument.createNode(2, qName, ns); attrNode.nodeValue = attrValue; this._nodeXML.setAttributeNode(attrNode); } else if (isIE) { this._nodeXML.setAttribute(qName, attrValue); } else { this._nodeXML.setAttributeNS(ns, qName, attrValue); } } // If this is a namespace attribute, add it to the global // list of SVG related namespaces so that we know whether // to create fake elements or native elements for that // namespace. See Issue 507. if (/^xmlns:?(.*)$/.test(qName)) { var m = qName.match(/^xmlns:?(.*)$/); var prefix = (m[1] ? m[1] : 'xmlns'); var namespaceURI = attrValue; // don't add duplicates if (!svgweb._allSVGNamespaces['_' + prefix]) { svgweb._allSVGNamespaces['_' + prefix] = namespaceURI; svgweb._allSVGNamespaces['_' + namespaceURI] = prefix; } } // update our internal set of attributes this._attributes['_' + qName] = attrValue; // send to Flash if (this._attached && this._passThrough) { var flashStr = FlashHandler._encodeFlashData(attrValue); this._handler.sendToFlash('jsSetAttribute', [ this._guid, false, ns, localName, flashStr ]); } }, hasAttribute: function(localName) /* Boolean */ { return this.hasAttributeNS(null, localName); }, hasAttributeNS: function(ns, localName) /* Boolean */ { //console.log('hasAttributeNS, ns='+ns+', localName='+localName); if (!ns && !isIE) { return this._nodeXML.hasAttribute(localName); } else { if (!isIE) { return this._nodeXML.hasAttributeNS(ns, localName); } else { // IE doesn't have hasAttribute or hasAttributeNS var attrNode = null; for (var i = 0; i < this._nodeXML.attributes.length; i++) { var current = this._nodeXML.attributes.item(i); // IE has no localName property; it munges the prefix:localName // together var m = new String(current.name).match(/(?:[^:]+:)?(.*)/); var attrName = m[1]; var currentNS = current.namespaceURI; if (currentNS == '') { // IE returns null namespace as '' currentNS = null; } if (ns == currentNS && attrName == localName) { attrNode = current; break; } } return (attrNode != null); } } }, getElementsByTagNameNS: function(ns, localName) /* _NodeList */ { //console.log('_Element.getElementsByTagNameNS, ns='+ns+', localName='+localName); var results = createNodeList(); var matches; // DOM Level 2 spec details: // if ns is null or '', return elements that have no namespace // if ns is '*', match all namespaces // if localName is '*', match all tags in the given namespace if (ns == '') { ns = null; } // we internally have to mess with the SVG namespace a bit to avoid // an issue with Firefox and Safari if (ns == svgns) { ns = svgnsFake; } // get DOM nodes with the given tag name if (this._nodeXML.getElementsByTagNameNS) { // non-IE browsers results = this._nodeXML.getElementsByTagNameNS(ns, localName); } else { // IE // we use XPath instead of xml.getElementsByTagName because some versions // of MSXML have namespace glitches with xml.getElementsByTagName // (Issue 183: http://code.google.com/p/svgweb/issues/detail?id=183) // and the namespace aware xml.getElementsByTagNameNS is not supported var namespaces = null; if (this._attached) { namespaces = this._handler.document._namespaces; } // figure out prefix var prefix = 'xmlns'; if (ns && ns != '*' && namespaces) { prefix = namespaces['_' + ns]; if (prefix === undefined) { return createNodeList(); // empty [] } } // determine correct xpath query; // MSXML incorrectly evaluates XPath expressions on the _whole_ XML DOM // document rather than restricting things to our context. In order to // provide support for contextual getElementsByTagNameNS we use the // following 'hack': we get all of our nodes, but then do a node test // along the ancestor axis to make sure we are rooted under the // node that has the GUID of our context var query; if (ns == '*' && localName == '*') { query = "//*[ancestor::*[@__guid = '" + this._guid + "']]"; } else if (ns == '*') { // NOTE: IE does not support wild carding just the namespace; see // http://svgweb.googlecode.com/svn/trunk/docs/UserManual.html#known_issues6 // for details query = "//*[namespace-uri()='*' and local-name()='" + localName + "'" + " and ancestor::*[@__guid = '" + this._guid + "']]"; } else if (localName == '*') { query = "//*[namespace-uri()='" + ns + "'" + " and ancestor::*[@__guid = '" + this._guid + "']]"; } else { // Wonderful IE bug: some versions of MSXML don't seem to 'see' // the default XML namespace with XPath, forcing you to pretend like // an element has no namespace: '//circle' // _Other_ versions of MSXML won't work like this, and _do_ see the // default namespace, forcing you to fully specify it: // //*[namespace-uri()='http://my-namespace' and local-name()='circle'] // To accomodate these we run both and use an XPath Union Operator // to combine the results. One is the MSXML default in Windows XP, // the other is an updated MSXML component installed by // Microsoft Office. query = "//" + localName + "[ancestor::*[@__guid = '" + this._guid + "']]" + "| //*[namespace-uri()='" + ns + "' and local-name()='" + localName + "'" + " and ancestor::*[@__guid = '" + this._guid + "']]"; } matches = xpath(this._nodeXML.ownerDocument, this._nodeXML, query, namespaces); if (matches !== null && matches !== undefined && matches.length > 0) { for (var i = 0; i < matches.length; i++) { // IE will incorrectly return the context node under some // conditions; filter that out if (matches[i] === this._nodeXML) { continue; } results.push(matches[i]); } } } // When doing wildcards on local name and namespace text nodes // can also sometimes be included; filter them out if ((ns == '*' || ns == svgnsFake) && localName == '*') { var temp = []; for (var i = 0; i < results.length; i++) { if (results[i].nodeType == _Node.ELEMENT_NODE && results[i].nodeName != '__text') { temp.push(results[i]); } } results = temp; } // now create or fetch _Elements representing these DOM nodes var nodes = createNodeList(); for (var i = 0; i < results.length; i++) { var elem = FlashHandler._getNode(results[i], this._handler); elem._passThrough = true; nodes.push(elem); } return nodes; }, beginElement: function() { this.beginElementAt(0); }, endElement: function() { this.endElementAt(0); }, beginElementAt: function(offset) { if (this._attached && this._passThrough) { this._handler.sendToFlash('jsBeginElementAt', [ this._guid, offset ]); } }, endElementAt: function(offset) { if (this._attached && this._passThrough) { this._handler.sendToFlash('jsEndElementAt', [ this._guid, offset ]); } }, /* Note: DOM Level 2 getAttributeNode, setAttributeNode, removeAttributeNode, getElementsByTagName, getAttributeNodeNS, setAttributeNodeNS not supported */ // SVGStylable interface style: null, /** Note: technically should be read only; _Style instance */ _setClassName: function(className) { // TODO: Implement }, // Note: we return a normal String instead of an SVGAnimatedString // as dictated by the SVG 1.1 standard _getClassName: function() { // TODO: Implement }, // Note: getPresentationAttribute not supported // SVGTransformable; takes an _SVGTransform _setTransform: function(transform) { // TODO: Implement }, // Note: we return a JS Array of _SVGTransforms instead of an // SVGAnimatedTransformList as dictated by the SVG 1.1 standard _getTransform: function() /* readonly; returns Array */ { // TODO: Implement }, // SVGFitToViewBox // Note: only supported for root SVG element for now _getViewBox: function() { /* readonly; SVGRect */ // Note: We return an _SVGRect instead of an SVGAnimatedRect as dictated // by the SVG 1.1 standard // TODO: Implement }, // SVGElement _getId: function() { // note: all attribute names are prefixed with _ to prevent attribute names // starting numbers from being interpreted as array indexes if (this._attributes['_id']) { return this._attributes['_id']; } else { // id property is special; we return empty string instead of null // to mimic native behavior on Firefox and Safari return ''; } }, _setId: function(id) { return this.setAttribute('id', id); }, ownerSVGElement: null, /* Note: technically readonly */ // not supported: xmlbase, viewportElement // SVGSVGElement and SVGUseElement readonly _getX: function() { /* SVGAnimatedLength */ var value = this._trimMeasurement(this.getAttribute('x')); return new _SVGAnimatedLength(new _SVGLength(new Number(value))); }, _getY: function() { /* SVGAnimatedLength */ var value = this._trimMeasurement(this.getAttribute('y')); return new _SVGAnimatedLength(new _SVGLength(new Number(value))); }, _getWidth: function() { /* SVGAnimatedLength */ var value = this._trimMeasurement(this.getAttribute('width')); return new _SVGAnimatedLength(new _SVGLength(new Number(value))); }, _getHeight: function() { /* SVGAnimatedLength */ var value = this._trimMeasurement(this.getAttribute('height')); return new _SVGAnimatedLength(new _SVGLength(new Number(value))); }, _getCurrentScale: function() { /* float */ return this._currentScale; }, _setCurrentScale: function(newScale /* float */) { if (newScale !== this._currentScale) { this._currentScale = newScale; this._handler.sendToFlash('jsSetCurrentScale', [ newScale ]); } return newScale; }, _getCurrentTranslate: function() { /* SVGPoint */ return this._currentTranslate; }, createSVGPoint: function() { return new _SVGPoint(0, 0); }, createSVGRect: function() { return new _SVGRect(0, 0, 0, 0); }, /** Extracts the unit value and trims off the measurement type. For example, if you pass in 14px, this method will return 14. Null will return null. */ _trimMeasurement: function(value) { if (value !== null) { value = value.replace(/[a-z]/gi, ''); } return value; }, // many attributes and methods from these two interfaces not here // defacto non-standard attributes _getInnerHTML: function() { // TODO: Implement; NativeHandler will require this as well, since // innerHTML not natively supported there }, _setInnerHTML: function(newValue) { // TODO: Implement; NativeHandler will require this as well, since // innerHTML not natively supported there }, // SVG 1.1 inline event attributes: // http://www.w3.org/TR/SVG/script.html#EventAttributes // Note: Technically not all elements have all these events; also // technically the SVG spec requires us to support the DOM Mutation // Events, which we do not. // We use this array to build up our getters and setters . // TODO: Gauge the performance impact of making this dynamic _allEvents: [ 'onfocusin', 'onfocusout', 'onactivate', 'onclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onload', 'onunload', 'onabort', 'onerror', 'onresize', 'onscroll', 'onzoom', 'onbegin', 'onend', 'onrepeat' ], _handleEvent: function(evt) { // called from the IE HTC when an event is fired, as well as from // one of our getter/setters for non-IE browsers }, _prepareEvents: function() { // for non-IE browsers, make the getter/setter magic using the // _allEvents array }, // SVGTests, SVGLangSpace, SVGExternalResourcesRequired // not supported // contains any attribute set with setAttribute; object literal of // name/value pairs _attributes: null, // copies the attributes from the XML DOM node into target _importAttributes: function(target, nodeXML) { for (var i = 0; i < nodeXML.attributes.length; i++) { var attr = nodeXML.attributes[i]; this._attributes['_' + attr.nodeName] = attr.nodeValue; } }, /** Does all the getter/setter magic for attributes, so that external callers can do something like myElement.innerHTML = 'foobar' or myElement.id = 'test' and our getters and setters will intercept these to do the correct behavior with the Flash viewer.*/ _defineAccessors: function() { var props; var self = this; // innerHTML /* // TODO: Not implemented yet this.__defineGetter__('innerHTML', function() { return self._getInnerHTML(); }); this.__defineSetter__('innerHTML', function(newValue) { return self._setInnerHTML(newValue); }); */ // SVGSVGElement and SVGUseElement readyonly props if (this.nodeName == 'svg' || this.nodeName == 'use') { this.__defineGetter__('x', function() { return self._getX(); }); this.__defineGetter__('y', function() { return self._getY(); }); this.__defineGetter__('width', function() { return self._getWidth(); }); this.__defineGetter__('height', function() { return self._getHeight(); }); } if (this.nodeName == 'svg') { // TODO: Ensure that currentTranslate and currentScale only show up // on root SVG node and not nested SVG nodes this.__defineGetter__('currentTranslate', function() { return self._getCurrentTranslate(); }); this.__defineGetter__('currentScale', function() { return self._getCurrentScale(); }); this.__defineSetter__('currentScale', function(newScale) { return self._setCurrentScale(newScale); }); } // id property this.__defineGetter__('id', hitch(this, this._getId)); this.__defineSetter__('id', hitch(this, this._setId)); }, /** @param prop String property name, such as 'x'. @param readWrite Boolean on whether the property is both read and write; if false then read only. */ _defineAccessor: function(prop, readWrite) { var self = this; var getMethod = function() { return self.getAttribute(prop); }; this.__defineGetter__(prop, getMethod); if (readWrite) { var setMethod = function(newValue) { return self.setAttribute(prop, newValue); }; this.__defineSetter__(prop, setMethod); } } }); /** The DOM DocumentFragment API. @param doc The document that produced this _DocumentFragment. Either the global, browser native 'document' or a fake _Document if this was created in the context of an SVG OBJECT. */ function _DocumentFragment(doc) { // superclass constructor _Node.apply(this, ['#document-fragment', _Node.DOCUMENT_FRAGMENT_NODE, null, null, null, null]); this.ownerDocument = doc; } // subclasses _Node _DocumentFragment.prototype = new _Node; extend(_DocumentFragment, { /** 'Resets' a _DocumentFragment so it can be reused. Basically removes all of it's children. */ _reset: function() { // delete all the XML children; using a while() loop works better than // for() since the # of childNodes will change out from under us as we // remove children. while (this._nodeXML.firstChild) { this._nodeXML.removeChild(this._nodeXML.firstChild); } this._childNodes = this._createChildNodes(); if (!isIE) { this._defineNodeAccessors(); } // NOTE: we never remove the DocumentFragment from the svgweb._guidLookup // table or the HTC node for this DocumentFragment; this is because we // must make it possible to reuse the DocumentFragment. This could cause // a memory leak issue over time however. } }); /** Not an official DOM interface; used so that we can track changes to the CSS style property of an Element @param element The _Element that this Style is attached to. */ function _Style(element) { this._element = element; this._setup(); } // we use this array to build up getters and setters to watch any changes for // any of these styles. Note: Technically we shouldn't have all of these for // every element, since some SVG elements won't have specific kinds of // style properties, like the DESC element having a font-size. _Style._allStyles = [ 'font', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch', 'fontStyle', 'fontVariant', 'fontWeight', 'direction', 'letterSpacing', 'textDecoration', 'unicodeBidi', 'wordSpacing', 'clip', 'color', 'cursor', 'display', 'overflow', 'visibility', 'clipPath', 'clipRule', 'mask', 'opacity', 'enableBackground', 'filter', 'floodColor', 'floodOpacity', 'lightingColor', 'stopColor', 'stopOpacity', 'pointerEvents', 'colorInterpolation', 'colorInterpolationFilters', 'colorProfile', 'colorRendering', 'fill', 'fillOpacity', 'fillRule', 'imageRendering', 'marker', 'markerEnd', 'markerMid', 'markerStart', 'shapeRendering', 'stroke', 'strokeDasharray', 'strokeDashoffset', 'strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit', 'strokeOpacity', 'strokeWidth', 'textRendering', 'alignmentBaseline', 'baselineShift', 'dominantBaseline', 'glyphOrientationHorizontal', 'glyphOrientationVertical', 'kerning', 'textAnchor', 'writingMode' ]; // the root SVGSVGElement has a few extra styles possible since it is nested // into an HTML context _Style._allRootStyles = [ 'border', 'verticalAlign', 'backgroundColor', 'top', 'right', 'bottom', 'left', 'position', 'width', 'height', 'margin', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor', 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle', 'zIndex', 'overflowX', 'overflowY', 'float', 'clear' ]; extend(_Style, { /** Flag that indicates that the HTC should ignore any property changes due to style changes. Used when we are internally making style changes. */ _ignoreStyleChanges: true, /** Initializes our magic getters and setters for non-IE browsers. For IE we set our initial style values on the HTC. */ _setup: function() { // Handle an edge-condition: the SVG spec requires us to support // style="" strings that might have uppercase style names, measurements, // etc. Normalize these here before continuing. this._normalizeStyle(); // now handle browser-specific initialization if (!isIE) { // setup getter/setter magic for non-IE browsers for (var i = 0; i < _Style._allStyles.length; i++) { var styleName = _Style._allStyles[i]; this._defineAccessor(styleName); } // root SVGSVGElement nodes have some extra properties from being in an // HTML context // If _handler is not set, this element is a nested svg element. if (this._element._handler && this._element._getProxyNode() == this._element._handler.document.rootElement) { for (var i = 0; i < _Style._allRootStyles.length; i++) { var styleName = _Style._allRootStyles[i]; this._defineAccessor(styleName); } } // CSSStyleDeclaration properties this.__defineGetter__('length', hitch(this, this._getLength)); } else { // Internet Explorer setup var htcStyle = this._element._htcNode.style; // parse style string var parsedStyle = this._fromStyleString(); // loop through each one, setting it on our HTC's style object for (var i = 0; i < parsedStyle.length; i++) { var styleName = this._toCamelCase(parsedStyle[i].styleName); var styleValue = parsedStyle[i].styleValue; // Issue 485: Cannot set textAlign style on IE try { htcStyle[styleName] = styleValue; } catch (exp) { console.log('The following exception occurred setting style.' + styleName + ' on IE: ' + (exp.message || exp)); } } // set initial values for style.length htcStyle.length = 0; // expose .length property on our custom _Style object to aid it // being used internally this.length = 0; // set CSSStyleDeclaration methods to our implementation htcStyle.item = hitch(this, this.item); htcStyle.setProperty = hitch(this, this.setProperty); htcStyle.getPropertyValue = hitch(this, this.getPropertyValue); // start paying attention to style change events on the HTC node this._changeListener = hitch(this, this._onPropertyChange); this._element._htcNode.attachEvent('onpropertychange', this._changeListener); } }, /** Defines the getter and setter for a single style, such as 'display'. */ _defineAccessor: function(styleName) { var self = this; this.__defineGetter__(styleName, function() { return self._getStyleAttribute(styleName); }); this.__defineSetter__(styleName, function(styleValue) { return self._setStyleAttribute(styleName, styleValue); }); }, _setStyleAttribute: function(styleName, styleValue) { //console.log('setStyleAttribute, styleName='+styleName+', styleValue='+styleValue); // Note: .style values and XML attributes have separate values. The XML // attributes always have precedence over any style values. // convert camel casing (i.e. strokeWidth becomes stroke-width) var stylePropName = this._fromCamelCase(styleName); // change our XML style string value // parse style string first var parsedStyle = this._fromStyleString(); // is our style name there? var foundStyle = false; for (var i = 0; i < parsedStyle.length; i++) { if (parsedStyle[i].styleName === stylePropName) { parsedStyle[i].styleValue = styleValue; foundStyle = true; break; } } // if we didn't find it above add it if (!foundStyle) { parsedStyle.push({ styleName: stylePropName, styleValue: styleValue }); } // now turn the style back into a string and set it on our XML and // internal list of attribute values var styleString = this._toStyleString(parsedStyle); this._element._nodeXML.setAttribute('style', styleString); this._element._attributes['_style'] = styleString; // for IE, update our HTC style object; we can't do magic getters for // those so have to update and cache the values if (isIE) { var htcStyle = this._element._htcNode.style; // never seen before; bump our style length if (!foundStyle) { htcStyle.length++; this.length++; } // update style value on HTC node so that when the value is fetched // it is correct; ignoreStyleChanges during this or we will get into // an infinite loop this._ignoreStyleChanges = true; htcStyle[styleName] = styleValue; this._ignoreStyleChanges = false; } // tell Flash about the change if (this._element._attached && this._element._passThrough) { var flashStr = FlashHandler._encodeFlashData(styleValue); this._element._handler.sendToFlash('jsSetAttribute', [ this._element._guid, true, null, stylePropName, flashStr ]); } }, _getStyleAttribute: function(styleName) { //console.log('getStyleAttribute, styleName='+styleName); // convert camel casing (i.e. strokeWidth becomes stroke-width) var stylePropName = this._fromCamelCase(styleName); if (this._element._attached && this._element._passThrough && !this._element._handler._redrawManager.isSuspended()) { var value = this._element._handler.sendToFlash('jsGetAttribute', [ this._element._guid, true, false, null, stylePropName, false ]); return value; } else { // not attached yet; have to parse it from our local value var parsedStyle = this._fromStyleString(); for (var i = 0; i < parsedStyle.length; i++) { if (parsedStyle[i].styleName === stylePropName) { return parsedStyle[i].styleValue; } } return null; } }, /** Parses our style string into an array, where each array entry is an object literal with the 'styleName' and 'styleValue', such as: results[0].styleName results[0].styleValue etc. If there are no results an empty array is returned. */ _fromStyleString: function() { var styleValue = this._element._nodeXML.getAttribute('style'); if (styleValue === null || styleValue === undefined) { return []; } var baseStyles; if (styleValue.indexOf(';') == -1) { // only one style value given, with no trailing semicolon baseStyles = [ styleValue ]; } else { baseStyles = styleValue.split(/\s*;\s*/); // last style is empty due to split() if (!baseStyles[baseStyles.length - 1]) { baseStyles = baseStyles.slice(0, baseStyles.length - 1); } } var results = []; for (var i = 0; i < baseStyles.length; i++) { var style = baseStyles[i]; var styleSet = style.split(':'); if (styleSet.length == 2) { var attrName = styleSet[0]; var attrValue = styleSet[1]; // trim leading whitespace attrName = attrName.replace(/^\s+/, ''); attrValue = attrValue.replace(/^\s+/, ''); var entry = { styleName: attrName, styleValue: attrValue }; results.push(entry); } } return results; }, /** Turns a parsed style into a string. @param parsedStyle An array where each entry is an object literal with two values, 'styleName' and 'styleValue'. Uses same data structure returned from fromStyleString() method above. */ _toStyleString: function(parsedStyle) { var results = ''; for (var i = 0; i < parsedStyle.length; i++) { results += parsedStyle[i].styleName + ': '; results += parsedStyle[i].styleValue + ';'; if (i != (parsedStyle.length - 1)) { results += ' '; } } return results; }, /** Transforms a camel case style name, such as strokeWidth, into it's dash equivalent, such as stroke-width. */ _fromCamelCase: function(styleName) { return styleName.replace(/([A-Z])/g, '-$1').toLowerCase(); }, /** Transforms a dash style name, such as stroke-width, into it's camel case equivalent, such as strokeWidth. */ _toCamelCase: function(stylePropName) { if (stylePropName.indexOf('-') == -1) { return stylePropName; } var results = ''; var sections = stylePropName.split('-'); results += sections[0]; for (var i = 1; i < sections.length; i++) { results += sections[i].charAt(0).toUpperCase() + sections[i].substring(1); } return results; }, // CSSStyleDeclaration interface methods and properties // TODO: removeProperty not supported yet setProperty: function(stylePropName, styleValue, priority) { // TODO: priority not supported for now; not sure if it even makes // sense in this context // convert from dash style to camel casing (i.e. stroke-width becomes // strokeWidth var styleName = this._toCamelCase(stylePropName); this._setStyleAttribute(styleName, styleValue); return styleValue; }, getPropertyValue: function(stylePropName) { // convert from dash style to camel casing (i.e. stroke-width becomes // strokeWidth var styleName = this._toCamelCase(stylePropName); return this._getStyleAttribute(styleName); }, item: function(index) { // parse style string var parsedStyle = this._fromStyleString(); // TODO: Throw exception if index is greater than length of style rules return parsedStyle[index].styleName; }, // NOTE: We don't support cssText for now. The reason why is that // IE has a style.cssText property already on our HTC nodes. This // property incorrectly includes some of our custom internal code, // such as 'length' as well as both versions of certain camel cased // properties (like stroke-width and strokeWidth). There is no way // currently known to work around this. The property is not that important // anyway so it won't currently be supported. _getLength: function() { // parse style string var parsedStyle = this._fromStyleString(); return parsedStyle.length; }, /** Handles an edge-condition: the SVG spec requires us to support style="" strings that might have uppercase style names, measurements, etc. We normalize these to lower-case in this method. */ _normalizeStyle: function() { // style="" attribute? // NOTE: IE doesn't support nodeXML.hasAttribute() if (!this._element._nodeXML.getAttribute('style')) { return; } // no uppercase letters? if (!/[A-Z]/.test(this._element._nodeXML.getAttribute('style'))) { return; } // parse style into it's components var parsedStyle = this._fromStyleString(); for (var i = 0; i < parsedStyle.length; i++) { parsedStyle[i].styleName = parsedStyle[i].styleName.toLowerCase(); // don't lowercase url() values if (parsedStyle[i].styleValue.indexOf('url(') == -1) { parsedStyle[i].styleValue = parsedStyle[i].styleValue.toLowerCase(); } } // turn back into a string var results = ''; for (var i = 0; i < parsedStyle.length; i++) { results += parsedStyle[i].styleName + ': ' + parsedStyle[i].styleValue + '; '; } // remove trailing space if (results.charAt(results.length - 1) == ' ') { results = results.substring(0, results.length - 1); } // change our style value; however, don't pass this through to Flash // because Flash might not even know about our existence yet, because we // are still being run from the _Element constructor var origPassThrough = this._element._passThrough; this._element._passThrough = false; this._element.setAttribute('style', results); this._element._passThrough = origPassThrough; }, /** For Internet Explorer, this method is called whenever a propertychange event fires on the HTC. */ _onPropertyChange: function() { // watch to see when anyone changes a 'style' property so we // can mirror it in the Flash control if (this._ignoreStyleChanges) { return; } var prop = window.event.propertyName; if (prop && /^style\./.test(prop) && prop != 'style.length') { // extract the style name and value var styleName = prop.match(/^style\.(.*)$/)[1]; var styleValue = this._element._htcNode.style[styleName]; // tell Flash and our fake node about our style change this._setStyleAttribute(styleName, styleValue); } } }); /** An OBJECT tag that is embedding an SVG element. This class encapsulates how we actually do the work of embedding this into the page (such as internally transforming the SVG OBJECT into a Flash one). @param svgNode The SVG OBJECT node to work with. @param handler The FlashHandler that owns us. */ function _SVGObject(svgNode, handler) { this._handler = handler; this._svgNode = svgNode; this._scriptsToExec = []; // flags to know when the SWF file (and on IE the HTC file) are done loading this._htcLoaded = false; this._swfLoaded = false; // handle any onload event listeners that might be present for // dynamically created OBJECT tags; this._svgNode._listeners is an array // we expose through our custom document.createElement('object', true) -- // the 'true' actually flags us to do things like this for (var i = 0; this._svgNode._onloadListeners && i < this._svgNode._onloadListeners.length; i++) { // wrap each of the listeners so that its 'this' object // correctly refers to the Flash OBJECT if used inside the listener // function; we use an outer function to prevent closure from // incorrectly happening, and then return an inner function inside // of this that correctly makes the 'this' object be our Flash // OBJECT rather than the global window object var wrappedListener = (function(handler, listener) { return function() { //console.log('_SVGObject, wrappedListener, handler='+handler+', listener='+listener); listener.apply(handler.flash); }; })(this._handler, this._svgNode._onloadListeners[i]); // pass values into function svgweb.addOnLoad(wrappedListener); } // start fetching the HTC file in the background if (isIE) { this._loadHTC(); } // fetch the SVG URL now and start processing. // Note: unfortunately we must use the 'src' attribute instead of the // standard 'data' attribute for IE. On certain installations of IE // with some security patches IE will display a gold security bar indicating // that some URLs were blocked; on others IE will attempt to download the // file pointed to by the 'data' attribute. Note that using the 'src' // attribute is a divergence from the standard, but it solves both issues. this.url = this._svgNode.getAttribute('src'); if (!this.url) { this.url = this._svgNode.getAttribute('data'); } // success function var successFunc = hitch(this, function(svgStr) { // clean up and parse our SVG this._handler._origSVG = svgStr; var results = svgweb._cleanSVG(svgStr, true, false); this._svgString = results.svg; this._xml = results.xml; // create our document objects this.document = new _Document(this._xml, this._handler); this._handler.document = this.document; // insert our Flash and replace the SVG OBJECT tag var nodeXML = this._xml.documentElement; // save any custom PARAM tags that might be nested inside our SVG OBJECT this._savedParams = this._getPARAMs(this._svgNode); // now insert the Flash this._handler._inserter = new FlashInserter('object', this._xml.documentElement, this._svgNode, this._handler); // wait for Flash to finish loading; see _onFlashLoaded() in this class // for further execution after the Flash asynchronous process is done }); if (this.url.substring(0, 5) == 'data:') { successFunc(this.url.substring(this.url.indexOf(',')+1)); } else { this._fetchURL(this.url, successFunc, hitch(this, this._fallback)); } } extend(_SVGObject, { /** An array of strings, where each string is an SVG SCRIPT tag embedded in an external SVG file. This is when SVG is embedded with an OBJECT. */ _scriptsToExec: null, /** * UTF-8 data encode / decode * http://www.webtoolkit.info/ **/ _utf8encode : function (string) { string = string.replace(/\r\n/g,"\n"); var utftext = ""; for (var n = 0; n < string.length; n++) { var c = string.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if ((c > 127) && (c < 2048)) { utftext += escape(String.fromCharCode((c >> 6) | 192)); utftext += escape(String.fromCharCode((c & 63) | 128)); } else { utftext += escape(String.fromCharCode((c >> 12) | 224)); utftext += escape(String.fromCharCode(((c >> 6) & 63) | 128)); utftext += escape(String.fromCharCode((c & 63) | 128)); } } return utftext; }, _fetchURL: function(url, onSuccess, onFailure) { var req = xhrObj(); // bust the cache for IE since IE's XHR GET requests are wonky if (isIE) { url = this._utf8encode(url); url += (url.indexOf('?') == -1) ? '?' : '&'; url += new Date().getTime(); } req.onreadystatechange = function() { if (req.readyState == 4) { if (req.status == 200) { // done onSuccess(req.responseText); } else { // error onFailure(req.status + ': ' + req.statusText); } req = null; } }; req.open('GET', url, true); req.send(null); }, _fallback: function(error) { console.log('onError (fallback), error='+error); // TODO: Implement }, _loadHTC: function() { // if IE, force the HTC file to asynchronously load with a dummy element; // we want to do the async operation now so that external API users don't // get hit with the async nature of the HTC file first loading when they // make a sync call; we will send the SVG over to the Flash _after_ the // HTC file is done loading. this._dummyNode = document.createElement('svg:__force__load'); this._dummyNode._handler = this._handler; // find out when the content is ready // NOTE: we do this here instead of inside the HTC file using an // internal oncontentready event in order to make the HTC file faster // and use less memory. Note also that 'oncontentready' is not available // outside HTC files, only 'onreadystatechange' is available. this._readyStateListener = hitch(this, this._onHTCLoaded); // cleanup later this._dummyNode.attachEvent('onreadystatechange', this._readyStateListener); var head = document.getElementsByTagName('head')[0]; // NOTE: as _soon_ as we append the dummy element the HTC file will // get called, branching control, so code after this call will not // get run in the sequence expected head.appendChild(this._dummyNode); // wait for HTC to load }, _onFlashLoaded: function(msg) { //console.log('_SVGObject, onFlashLoaded, msg='+this._handler.debugMsg(msg)); // store a reference to our Flash object this._handler.flash = document.getElementById(this._handler.flashID); // copy any custom developer PARAM tags on the original SVG OBJECT // over to the Flash element so that SVG scripts can programmatically // access them; we saved these earlier in the _SVGObject constructor if (this._savedParams.length) { for (var i = 0; i < this._savedParams.length; i++) { var param = this._savedParams[i]; this._handler.flash.appendChild(param); param = null; } this._savedParams = null; } // expose top and parent attributes on Flash OBJECT this._handler.flash.top = this._handler.flash.parent = window; // flag that the SWF is loaded; if not IE, send everything over to Flash; // if IE, make sure the HTC file is done loading this._swfLoaded = true; if (!isIE || this._htcLoaded) { this._onEverythingLoaded(); } }, _onHTCLoaded: function() { //console.log('_SVGObject, onHTCLoaded'); // We added a 'dummy' HTC node to the page when we first instantiated our // _SVGObject; this was so that the HTC part of the architecture is primed // and loaded and ready to go, so later on when someone does a // synchronous createElement() to create an SVG node the HTC behavior is // already loaded and ready to do it's magic. Now that the HTC file is // loaded we want to remove the dummy element created earlier. // can't use htcNode.parentNode to get the parent and remove the child // since we override that inside svg.htc var head = document.getElementsByTagName('head')[0]; head.removeChild(this._dummyNode); // cleanup our event handler this._dummyNode.detachEvent('onreadystatechange', this._readyStateListener); // prevent IE memory leaks this._dummyNode = null; head = null; // flag that we are loaded; send things over to Flash if the SWF is loaded this._htcLoaded = true; if (this._swfLoaded) { this._onEverythingLoaded(); } }, _onEverythingLoaded: function() { //console.log('_SVGObject, onEverythingLoaded'); var size = this._handler._inserter._determineSize(); this._handler.sendToFlash('jsHandleLoad', [ /* objectURL */ this._getRelativeTo('object'), /* pageURL */ this._getRelativeTo('page'), /* objectWidth */ size.pixelsWidth, /* objectHeight */ size.pixelsHeight, /* ignoreWhiteSpace */ false, /* svgString */ this._svgString ]); }, _onRenderingFinished: function(msg) { //console.log('_SVGObject, onRenderingFinished, id='+this._handler.id // + ', msg='+this._handler.debugMsg(msg)); // we made the SVG hidden before to avoid scrollbars on IE; make visible // now this._handler.flash.style.visibility = 'visible'; // create the documentElement and rootElement and set them to our SVG // root element var rootXML = this._xml.documentElement; var rootID = rootXML.getAttribute('id'); var root = new _SVGSVGElement(rootXML, null, null, this._handler); var doc = this._handler.document; doc.documentElement = root._getProxyNode(); doc.rootElement = root._getProxyNode(); // add to our lookup tables so that fetching this node in the future works doc._nodeById['_' + rootID] = root; // add our contentDocument property // TODO: This should be doc._getProxyNode(), but Issue 227 needs to be // addressed first: // http://code.google.com/p/svgweb/issues/detail?id=227 if (isIE) { // this workaround will prevent Issue 140: // "SVG OBJECT.contentDocument does not work when DOCTYPE specified // inside of HTML file itself" // http://code.google.com/p/svgweb/issues/detail?id=140 this._handler.flash.setAttribute('contentDocument', null); } this._handler.flash.contentDocument = doc; // FIXME: NOTE: unfortunately we can't support the getSVGDocument() method; // Firefox throws an error when we try to override it: // 'Trying to add unsupported property on scriptable plugin object!' // this._handler.flash.getSVGDocument = function() { // return this.contentDocument; // }; // create a pseudo window element this._handler.window = new _SVGWindow(this._handler); // our fake document object should point to our fake window object doc.defaultView = this._handler.window; // add our onload handler to the list of scripts to execute at the // beginning var onload = rootXML.getAttribute('onload'); if (onload) { // we want 'this' inside of the onload handler to point to our // SVG root; the 'document.documentElement' will get rewritten later by // the _executeScript() method to point to our fake SVG root instead var defineEvtCode = 'var evt = { target: document.getElementById("' + root.getAttribute('id') + '") ,' + 'currentTarget: document.getElementById("' + root.getAttribute('id') + '") ,' + 'preventDefault: function() { this.returnValue=false; }' + '};'; onload = ';(function(){' + defineEvtCode + onload + '}).apply(document.documentElement);'; this._scriptsToExec.push(onload); } // now execute any scripts embedded into the SVG file; we turn all // the scripts into one giant block and run them together so that // global functions can 'see' and call each other var finalScript = ''; for (var i = 0; i < this._scriptsToExec.length; i++) { finalScript += this._scriptsToExec[i] + '\n'; } this._executeScript(finalScript); // indicate that we are done this._handler._loaded = true; this._handler.fireOnLoad(this._handler.id, 'object'); }, /** Relative URLs inside of SVG need to expand against something (i.e. such as having an SVG Audio tag with a relative URL). This method figures out what that relative URL should be. We send this over to Flash when rendering things so Flash knows what to expand against. @param toWhat - String that controls what we use for the relative URL. If "object" given, we use the URL to the SVG OBJECT; if "page" given, we determine things relative to the page itself. */ _getRelativeTo: function(toWhat) { var results = ''; if (toWhat == 'object') { // strip off scheme and hostname, then match just path portion var pathname = this.url.replace(/[^:]*:\/\/[^\/]*/).match(/\/?[^\?\#]*/)[0]; if (pathname && pathname.length > 0 && pathname.indexOf('/') != -1) { // snip off any filename after a final slash results = pathname.replace(/\/([^\/]*)$/, '/'); } } else { var pathname = window.location.pathname.toString(); if (pathname && pathname.length > 0 && pathname.indexOf('/') != -1) { // snip off any filename after a final slash results = pathname.replace(/\/([^\/]*)$/, '/'); } } return results; }, /** Executes a SCRIPT block inside of an SVG file. We essentially rewrite the references in this script to point to our Flash Handler instead, create an invisible iframe that will act as the 'global' object, and then write the script into the iframe as a new SCRIPT block. @param script String with script to execute. */ _executeScript: function(script) { // Create an iframe that we will use to 'silo' and execute our // code, which will act as a place for globals to be defined without // clobbering globals on the HTML document's window or from other // embedded SVG files. This is necessary so that setTimeouts and // setIntervals will work later on, for example. // create an iframe and attach it offscreen var iframe = document.createElement('iframe'); iframe.setAttribute('src', 'about:blank'); iframe.style.position = 'absolute'; iframe.style.top = '-1000px'; iframe.style.left = '-1000px'; var body = document.getElementsByTagName('body')[0]; body.appendChild(iframe); // get the iframes document object; IE differs on how to get this var iframeDoc = (iframe.contentDocument) ? iframe.contentDocument : iframe.contentWindow.document; // set the document.defaultView to the iframe's real Window object; // note that IE doesn't support defaultView var iframeWin = iframe.contentWindow; this._handler.document.defaultView = iframeWin; // Create a script with the proper environment. script = this._sandboxedScript(script); // Add code to set back an eval function we can use for further execution. // Code adapted from blog post by YuppY: // http://dean.edwards.name/weblog/2006/11/sandbox/ script = script + ';if (__svgHandler) __svgHandler.sandbox_eval = ' + (isIE ? 'window.eval;' : 'function(scriptCode) { return window.eval(scriptCode) };'); if (isOpera) { var _win = this; // Opera 10.53 just kills this event thread (see test_js2.html), // so we switch to a new execution context to buy more time on a // more appropriate thread. var timeoutFunc = setTimeout((function(_handlerwin, iframeWin, script) { return function() { // Opera 10.53 hangs on creating the script tag (see // test_js2.html), so try running the code this way. iframeWin.eval.apply(iframeWin, [script]); _handlerwin._fireOnload(); }; })(this._handler.window, iframeWin, script), 1); } else { // now insert the script into the iframe to execute it in a siloed way iframeDoc.write(''); iframeDoc.close(); // execute any addEventListener(onloads) that might have been // registered this._handler.window._fireOnload(); } }, _sandboxedScript: function(script) { // expose the handler as a global object at the top of the script; // expose the svgns and xlinkns variables; and override the setTimeout // and setInterval functions for the iframe where we will execute things // so we can clear out all timing functions if the SVG OBJECT is later // removed with a call to svgweb.removeChild var addToTop = 'var __svgHandler = top.svgweb.handlers["' + this._handler.id + '"];\n' + 'window.svgns = "' + svgns + '";\n' + 'window.xlinkns = "' + xlinkns + '";\n'; var timeoutOverride = 'window._timeoutIDs = [];\n' + 'window._setTimeout = window.setTimeout;\n' + 'window.setTimeout = \n' + ' (function() {\n' + ' return function(f, ms) {\n' + ' var timeID = window._setTimeout(f, ms);\n' + ' window._timeoutIDs.push(timeID);\n' + ' return timeID;\n' + ' };\n' + ' })();\n'; var intervalOverride = 'window._intervalIDs = [];\n' + 'window._setInterval = window.setInterval;\n' + 'window.setInterval = \n' + ' (function() {\n' + ' return function(f, ms) {\n' + ' var timeID = window._setInterval(f, ms);\n' + ' window._intervalIDs.push(timeID);\n' + ' return timeID;\n' + ' };\n' + ' })();\n'; script = addToTop + timeoutOverride + intervalOverride + '\n\n' + script; // change any calls to top.document or top.window, to a temporary different // string to avoid collisions when we transform next script = script.replace(/top\.document/g, 'top.DOCUMENT'); script = script.replace(/top\.window/g, 'top.WINDOW'); // intercept any calls to 'document.' or 'window.' inside of a string; // transform to this to a different temporary token so we can handle // it differently (i.e. we will put backslashes around certain portions: // top.svgweb.handlers[\"svg2\"].document for example) // change any calls to the document object to point to our Flash Handler // instead; avoid variable names that have the word document in them, // and pick up document* used with different endings script = script.replace(/(^|[^A-Za-z0-9_])document(\.|'|"|\,| |\))/g, '$1__svgHandler.document$2'); // change some calls to the window object to point to our fake window // object instead script = script.replace(/window\.(location|addEventListener|onload|frameElement)/g, '__svgHandler.window.$1'); // change back any of our top.document or top.window calls to be // their original lower case (we uppercased them earlier so that we // wouldn't incorrectly transform them) script = script.replace(/top\.DOCUMENT/g, 'top.document'); script = script.replace(/top\.WINDOW/g, 'top.window'); return script; }, /** Developers can nest custom PARAM tags inside our SVG OBJECT in order to pass parameters into their SVG file. This function gets these, and makes a clone of them suitable for us to re-attach and use in the future. @param svgNode The SVG OBJECT DOM node. @returns An array of cloned PARAM objects. If none, then the array has zero length. */ _getPARAMs: function(svgNode) { var params = []; for (var i = 0; i < svgNode.childNodes.length; i++) { var child = svgNode.childNodes[i]; if (child.nodeName.toUpperCase() == 'PARAM') { params.push(child.cloneNode(false)); } } return params; } }); /** A fake window object that we provide for SVG files to use internally. @param handler The Flash Handler assigned to this fake window object. */ function _SVGWindow(handler) { this._handler = handler; this.fake = true; // helps to detect fake abstraction this.frameElement = this._handler.flash; this.location = this._createLocation(); this.alert = window.alert; this.top = this.parent = window; this._onloadListeners = []; } extend(_SVGWindow, { addEventListener: function(type, listener, capture) { if (type.toLowerCase() == 'svgload' || type.toLowerCase() == 'load') { this._onloadListeners.push(listener); } }, _fireOnload: function() { //console.log('_SVGWindow._fireOnLoad'); for (var i = 0; i < this._onloadListeners.length; i++) { try { this._onloadListeners[i](); } catch (exp) { console.log('The following exception occurred from an SVG onload ' + 'listener: ' + (exp.message || exp)); } } // if there is an inline window.onload execute that now if (this.onload) { try { this.onload(); } catch (exp) { console.log('The following exception occurred from an SVG onload ' + 'listener: ' + (exp.message || exp)); } } }, /** Creates a fake window.location object. @param fakeLocation A full window.location object (i.e. it has .port, .hash, etc.) used for testing purposes to give a fake value to the containing HTML page's window.location value. */ _createLocation: function(fakeLocation) { var loc = {}; var url = this._handler._svgObject.url; var windowLocation; if (fakeLocation) { windowLocation = fakeLocation; } else { windowLocation = window.location; } // Bypass parsing data: URLs. if (/^data:/.test(url)) { loc.href = url; loc.toString = function() { return this.href; }; return loc; } // expand URL // first, see if this url is fully expanded already if (/^http/.test(url)) { // nothing to do } else if (url.charAt(0) == '/') { // ex: /embed1.svg url = windowLocation.protocol + '//' + windowLocation.host + url; } else { // fully relative, such as embed1.svg // get the pathname of the page we are on, clearing out everything after // the last slash if (windowLocation.pathname.indexOf('/') == -1) { url = windowLocation.protocol + '//' + windowLocation.host + '/' + url; } else { var relativeTo = windowLocation.pathname; // walk the string in reverse removing characters until we hit a slash for (var i = relativeTo.length - 1; i >= 0; i--) { if (relativeTo.charAt(i) == '/') { break; } relativeTo = relativeTo.substring(0, i); } url = windowLocation.protocol + '//' + windowLocation.host + relativeTo + url; } } // parse URL // FIXME: NOTE: href, search, and pathname should be URL-encoded; the others // should be URL-decoded // match 1 - protocol // match 2 - hostname // match 3 - port // match 4 - pathname // match 5 - search // match 6 - hash var results = url.match(/^(https?:)\/\/([^\/:]*):?([0-9]*)([^\?#]*)([^#]*)(#.*)?$/); loc.protocol = (results[1]) ? results[1] : windowLocation.href; if (loc.protocol.charAt(loc.protocol.length - 1) != ':') { loc.protocol += ':'; } loc.hostname = results[2]; // NOTE: browsers natively drop the port if its not explicitly specified loc.port = ''; if (results[3]) { loc.port = results[3]; } // is the URL and the containing page at different domains? var sameDomain = true; if (loc.protocol != windowLocation.protocol || loc.hostname != windowLocation.hostname || (loc.port && loc.port != windowLocation.port)) { sameDomain = false; } if (sameDomain && !loc.port) { loc.port = windowLocation.port; } if (loc.port) { loc.host = loc.hostname + ':' + loc.port; } else { loc.host = loc.hostname; } loc.pathname = (results[4]) ? results[4] : ''; loc.search = (results[5]) ? results[5] : ''; loc.hash = (results[6]) ? results[6] : ''; loc.href = loc.protocol + '//' + loc.host + loc.pathname + loc.search + loc.hash; loc.toString = function() { return this.protocol + '//' + this.host + this.pathname + this.search + this.hash; }; return loc; } }); /** Utility helper class that will generate the correct HTML for a Flash OBJECT and embed it into a page. Broken out so that both _SVGObject and _SVGSVGElement can use it in different contexts. @param embedType Either the string 'script' when embedding SVG into a page using the SCRIPT tag or 'object' if embedding SVG into a page using the OBJECT tag. @param nodeXML The parsed XML for the root SVG element, used for sizing, background, color, etc. @param replaceMe If embedType is 'script', this is the SCRIPT node to replace. If embedType is 'object', then this is the SVG OBJECT node. @param handler The FlashHandler that will be associated with this Flash object. */ function FlashInserter(embedType, nodeXML, replaceMe, handler) { this._embedType = embedType; this._nodeXML = nodeXML; this._replaceMe = replaceMe; this._handler = handler; this._parentNode = replaceMe.parentNode; // Get width and height from object tag, if present. if (this._embedType == 'object') { this._explicitWidth = this._replaceMe.getAttribute('width'); this._explicitHeight = this._replaceMe.getAttribute('height'); } this._setupFlash(); } extend(FlashInserter, { _setupFlash: function() { // determine various information we need to display this object var size = this._determineSize(); var background = this._determineBackground(); var style = this._determineStyle(); var className = this._determineClassName(); var customAttrs = this._determineCustomAttrs(); // setup our ID; if we are embedded with a SCRIPT tag, we use the ID from // the SVG ROOT tag; if we are embedded with an OBJECT tag, then we // simply make the Flash have the exact same ID as the OBJECT we are // replacing var elementID; if (this._embedType == 'script') { elementID = this._nodeXML.getAttribute('id'); this._handler.flashID = elementID + '_flash'; } else if (this._embedType == 'object') { elementID = this._replaceMe.getAttribute('id'); this._handler.flashID = elementID; } // get a Flash object and insert it into our document var flash = this._createFlash(size, elementID, background, style, className, customAttrs); this._insertFlash(flash); // wait for the Flash file to finish loading }, /** Inserts the Flash object into the page. @param flash Flash HTML string. If this is an XHTML page, then this is an EMBED object already instantiated and ready to insert into the page. @returns The Flash DOM object. */ _insertFlash: function(flash) { if (!isIE) { var flashObj; if (!isXHTML) { // no innerHTML in XHTML land // do a trick to turn the Flash HTML string into an actual DOM object // unfortunately this doesn't work on IE; on IE the Flash is immediately // loaded when we do div.innerHTML even though we aren't attached // to the document! var div = document.createElement('div'); div.innerHTML = flash; flashObj = div.childNodes[0]; div.removeChild(flashObj); // at this point we have the OBJECT tag; ExternalInterface communication // won't work on Firefox unless we get the EMBED tag itself for (var i = 0; i < flashObj.childNodes.length; i++) { var check = flashObj.childNodes[i]; if (check.nodeName.toUpperCase() == 'EMBED') { flashObj = check; break; } } } else if (isXHTML) { /* XHTML */ // 'flash' is an EMBED object already created for us by createFlash(); // no innerHTML in this environment so we can't instantiate from // a string: // Issue 312: Odd error when using within XHTML document: // works with Firefox, does not work with any other browser // http://code.google.com/p/svgweb/issues/detail?id=312 flashObj = flash; } // now insert the EMBED tag into the document this._replaceMe.parentNode.replaceChild(flashObj, this._replaceMe); return flashObj; } else { // IE // NOTE 1: as _soon_ as we make this call the Flash will load, even // before the rest of this method has finished. The Flash can // therefore finish loading before anything after the next statement // has run, so be careful of timing bugs. // NOTE 2: IE requires that we have this on a slight timeout, or we // will get the following issue on IE 7: when the SWF and HTC files are // loaded from the browser's cache (i.e. the second time a page loads // by using ctrl-R), loading the HTC and SWF file from the same 'thread' // of control causes an IE bug where the SWF never loads. The one // second timeout gets both files loading on separate 'threads' of // control. var self = this; window.setTimeout(function() { self._replaceMe.outerHTML = flash; self = null; // IE memory leaks }, 1); } }, /** Determines a width and height for the parsed SVG XML. Returns an object literal with two values, width and height. It is worth noting that pixels in this function and generally in javascript land refer to "unzoomed" pixels. An object has a css width value and that unit system does not change due to browser zooming. Flash knows the object size in real screen pixels, so it will account for the zoom mismatch. It does not know the javascript land units, so we must tell it. That is mainly why we are always calculating the pixel values here (from percent values, if necessary). */ _determineSize: function() { var parentWidth = this._parentNode.clientWidth; var parentHeight = this._parentNode.clientHeight; // If parentHeight is zero, then the object was sized with a % // object height and the parent height is not known. // We should use aspect ratio for sizing the object height. // Subsequent resizing of the object may result in a valid // parent height, but in this case, we should stick to our earlier // determination that the image should rely on aspect ratio to size // the object height. if (parentHeight == 0) { this.invalidParentHeight = true; } /* IE7 quirk */ if (parentWidth == 0) { parentWidth = this._parentNode.offsetWidth; } if (!isSafari) { parentWidth -= this._getMargin(this._parentNode, 'margin-left'); parentWidth -= this._getMargin(this._parentNode, 'margin-right'); parentHeight -= this._getMargin(this._parentNode, 'margin-top'); parentHeight -= this._getMargin(this._parentNode, 'margin-bottom'); } if (isStandardsMode) { return this._getStandardsSize(parentWidth, parentHeight); } else { return this._getQuirksSize(parentWidth, parentHeight); } }, _getQuirksSize: function(parentWidth, parentHeight) { var pixelsWidth, pixelsHeight; /** In the case of script or where an svg object has a height percent and the svg image has a height percent, then the height of the parent is not used and the viewBox aspect resolution is used. However, in certain circumstances, the % of the parent height is used. That circumstance is when the embed type is script and parentHeight is > 0 and if it is in a div with height. Here, we look for a parent div, and if we do not find one, then we default to clientHeight. */ if (this._embedType == 'script') { var grandParent = this._parentNode; while (grandParent && grandParent.style) { // If a grandparent is a div, the parent height is ok. if (grandParent.nodeName.toLowerCase() == 'div') { // ?? may need to check for valid height break; } // If we get to the body without div, ignore parent height. if (grandParent.nodeName.toLowerCase() == 'body') { // See Issue 276. Images with position: fixed // scale into the viewport. if (this._nodeXML.getAttribute('style') && this._nodeXML.getAttribute('style').indexOf('fixed') != -1) { if (window.innerHeight && window.innerHeight > 0) { parentHeight = window.innerHeight; } else if (document.documentElement && document.documentElement.clientHeight && document.documentElement.clientHeight > 0) { parentHeight = document.documentElement.clientHeight; } else { parentHeight = document.body.clientHeight; } this.invalidParentHeight = false; } else { // Issue 233: Default to viewBox this.invalidParentHeight = true; parentHeight = 0; } break; } grandParent = grandParent.parentNode; } } // Calculate the object width and size starting with // the width and height from the object tag. // // If this is script embed type, the algorithm will perform as // an object tag with neither width nor height specified. // var objWidth = this._explicitWidth; var objHeight = this._explicitHeight; var xmlWidth = this._nodeXML.getAttribute('width'); if (xmlWidth && xmlWidth.indexOf('%') == -1) { // strip 'px' if present xmlWidth = parseInt(xmlWidth).toString(); } var xmlHeight = this._nodeXML.getAttribute('height'); if (xmlHeight && xmlHeight.indexOf('%') == -1) { // strip 'px' if present xmlHeight = parseInt(xmlHeight).toString(); } if (objWidth && objHeight) { // calculate width in pixels if (objWidth.indexOf('%') != -1) { pixelsWidth = parentWidth * parseInt(objWidth) / 100; } else { pixelsWidth = objWidth; } // calculate height in pixels if (objHeight.indexOf('%') != -1) { if (parentHeight > 0) { pixelsHeight = parentHeight * parseInt(objHeight) / 100; } else { console.log('SVGWeb: unhandled resize scenario.'); parentHeight = 200; } } else { pixelsHeight = objHeight; } return {width: objWidth, height: pixelsHeight, pixelsWidth: pixelsWidth, pixelsHeight: pixelsHeight, clipMode: this._nodeXML.getAttribute('viewBox') ? 'neither' : 'both'}; } var viewBox, boxWidth, boxHeight; if (objWidth) { // estimate the width that will be used for percents if (objWidth.indexOf('%') != -1) { pixelsWidth = parentWidth * parseInt(objWidth) / 100; } else { pixelsWidth = objWidth; } if (this._nodeXML.getAttribute('viewBox')) { // If width and height are both specified on the SVG file in pixels, // then the height is calculated based on the object width and the // aspect ratio between the svg height and width. // The viewBox scales into resulting area (honoring preserveAspectRatio). // SVG height and width are not using in actual rendering. if (xmlWidth && xmlWidth.indexOf('%') == -1 && xmlHeight && xmlHeight.indexOf('%') == -1) { objHeight = pixelsWidth * (xmlHeight / xmlWidth); } else { viewBox = this._nodeXML.getAttribute('viewBox').split(/\s+|,/); boxWidth = viewBox[2]; boxHeight = viewBox[3]; objHeight = pixelsWidth * (boxHeight / boxWidth); } return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: 'neither'}; } else { if (xmlWidth && xmlWidth.indexOf('%') == -1 && xmlHeight && xmlHeight.indexOf('%') == -1) { objHeight = pixelsWidth * (xmlHeight / xmlWidth); } else { if (xmlHeight && xmlHeight.indexOf('%') == -1) { objHeight = xmlHeight; } else { objHeight = 150; } } return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: 'both'}; } } if (objHeight) { if (objHeight.indexOf('%') != -1) { pixelsHeight = parentHeight * parseInt(objHeight) / 100; } else { pixelsHeight = objHeight; } if (this._nodeXML.getAttribute('viewBox')) { // If width and height are both specified on the SVG file in pixels, // then the object width is calculated based on the object height and the // aspect ratio between the svg height and width. if (xmlWidth && xmlWidth.indexOf('%') == -1 && xmlHeight && xmlHeight.indexOf('%') == -1) { objWidth = pixelsHeight * (xmlWidth / xmlHeight); } else { viewBox = this._nodeXML.getAttribute('viewBox').split(/\s+|,/); boxWidth = viewBox[2]; boxHeight = viewBox[3]; objWidth = pixelsHeight * (boxWidth / boxHeight); } return {width: objWidth, height: objHeight, pixelsWidth: objWidth, pixelsHeight: pixelsHeight, clipMode: 'neither'}; } else { // No viewbox, use svg width for object size if (xmlWidth && xmlWidth.indexOf('%') == -1 && xmlHeight && xmlHeight.indexOf('%') == -1) { objWidth = pixelsHeight * (xmlWidth / xmlHeight); pixelsWidth = objWidth; } else { if (xmlWidth) { objWidth = xmlWidth; } else { objWidth = "100%"; } // estimate the width that will be used for percents if (objWidth.indexOf('%') != -1) { pixelsWidth = parentWidth * parseInt(objWidth) / 100; } else { pixelsWidth = objWidth; } } // Also use svg width for svg clipping return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: pixelsHeight, clipMode: 'both'}; } } // If we are here, neither object height nor width were specified. // Use the svg width if (xmlWidth) { objWidth = xmlWidth; } else { objWidth = "100%"; } // Calculate the width that will be used for percents. if (objWidth.indexOf('%') != -1) { pixelsWidth = parentWidth * parseInt(objWidth) / 100; } else { pixelsWidth = objWidth; } // Height pixels are used directly. if (xmlHeight && xmlHeight.indexOf('%') == -1) { objHeight = xmlHeight; return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: this._nodeXML.getAttribute('viewBox') ? 'neither' : 'both'}; } else { // The height is a % or missing. Check for viewBox. if (this._nodeXML.getAttribute('viewBox')) { // Issue 276 if (this._embedType == 'script' && (xmlHeight == null || xmlHeight.indexOf('%') != -1) && !this.invalidParentHeight) { if (xmlHeight == null) { xmlHeight = "100%"; } if (objHeight == null) { objHeight = "100%"; } pixelsHeight = parentHeight * parseInt(xmlHeight) / 100; return {width: objWidth, height: pixelsHeight, pixelsWidth: pixelsWidth, pixelsHeight: pixelsHeight, clipMode: 'neither'}; } var viewBox = this._nodeXML.getAttribute('viewBox').split(/\s+|,/); var boxWidth = viewBox[2]; var boxHeight = viewBox[3]; objHeight = pixelsWidth * (boxHeight / boxWidth); return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: 'neither'}; } else { objHeight = 150; return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: 'both'}; } } }, _getStandardsSize: function(parentWidth, parentHeight) { var pixelsWidth, pixelsHeight; // Calculate the object width and size starting with // the width and height from the object tag. // // If this is script embed type, the algorithm will perform as // an object tag with neither width nor height specified. // var objWidth = this._explicitWidth; var objHeight = this._explicitHeight; var xmlWidth = this._nodeXML.getAttribute('width'); if (xmlWidth && xmlWidth.indexOf('%') == -1) { // strip 'px' if present xmlWidth = parseInt(xmlWidth).toString(); } var xmlHeight = this._nodeXML.getAttribute('height'); if (xmlHeight && xmlHeight.indexOf('%') == -1) { // strip 'px' if present xmlHeight = parseInt(xmlHeight).toString(); } if (objWidth && !objHeight) { return this._getQuirksSize(parentWidth, parentHeight); } if (!objWidth && !objHeight) { return this._getQuirksSize(parentWidth, parentHeight); } if (!objWidth && objHeight) { if (xmlWidth) { objWidth = xmlWidth; } else { objWidth = '100%'; } // calculate width in pixels if (objWidth.indexOf('%') != -1) { pixelsWidth = parentWidth * parseInt(objWidth) / 100; } else { pixelsWidth = objWidth; } // Use object height pixels, if specified. if (objHeight.indexOf('%') == -1) { pixelsHeight = objHeight; // If width and height are both specified on the SVG file in pixels, // then the object width is calculated based on the object height and the // aspect ratio between the svg height and width. if (xmlWidth && xmlWidth.indexOf('%') == -1 && xmlHeight && xmlHeight.indexOf('%') == -1) { objWidth = objHeight * (xmlWidth / xmlHeight); pixelsWidth = objWidth; } else { if (this._nodeXML.getAttribute('viewBox')) { viewBox = this._nodeXML.getAttribute('viewBox').split(/\s+|,/); boxWidth = viewBox[2]; boxHeight = viewBox[3]; objWidth = pixelsHeight * (boxWidth / boxHeight); pixelsWidth = objWidth; } } return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: this._nodeXML.getAttribute('viewBox') ? 'neither' : 'both'}; } else { if (xmlHeight && xmlHeight.indexOf('%') == -1) { pixelsHeight = xmlHeight; } else { if (this._nodeXML.getAttribute('viewBox')) { viewBox = this._nodeXML.getAttribute('viewBox').split(/\s+|,/); boxWidth = viewBox[2]; boxHeight = viewBox[3]; pixelsHeight = pixelsWidth * (boxHeight / boxWidth); } else { pixelsHeight = 150; } } return {width: objWidth, height: pixelsHeight, pixelsWidth: pixelsWidth, pixelsHeight: pixelsHeight, clipMode: this._nodeXML.getAttribute('viewBox') ? 'neither' : 'both'}; } } if (objWidth && objHeight) { // calculate width in pixels if (objWidth.indexOf('%') != -1) { pixelsWidth = parentWidth * parseInt(objWidth) / 100; } else { pixelsWidth = objWidth; } // Use object height pixels, if specified. if (objHeight.indexOf('%') == -1) { return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: this._nodeXML.getAttribute('viewBox') ? 'neither' : 'both'}; } else { if (xmlWidth && xmlWidth.indexOf('%') == -1 && // If width and height are both specified on the SVG file in pixels, // then the height is calculated based on the object width and the // aspect ratio between the svg height and width. xmlHeight && xmlHeight.indexOf('%') == -1) { pixelsHeight = pixelsWidth * (xmlHeight / xmlWidth); return {width: objWidth, height: pixelsHeight, pixelsWidth: pixelsWidth, pixelsHeight: pixelsHeight, clipMode: 'neither'}; } else { if (!this.invalidParentHeight) { // If parent height is "valid", use it. pixelsHeight = parentHeight * parseInt(objHeight) / 100; return {width: objWidth, height: pixelsHeight, pixelsWidth: pixelsWidth, pixelsHeight: pixelsHeight, clipMode: 'neither'}; } else { // Default to viewbox aspect resolution if (this._nodeXML.getAttribute('viewBox')) { viewBox = this._nodeXML.getAttribute('viewBox').split(/\s+|,/); boxWidth = viewBox[2]; boxHeight = viewBox[3]; objHeight = pixelsWidth * (boxHeight / boxWidth); return {width: objWidth, height: objHeight, pixelsWidth: pixelsWidth, pixelsHeight: objHeight, clipMode: 'neither'}; } else { if (xmlHeight && xmlHeight.indexOf('%') == -1) { pixelsHeight = xmlHeight; } else { pixelsHeight = 150; } return {width: objWidth, height: pixelsHeight, pixelsWidth: pixelsWidth, pixelsHeight: pixelsHeight, clipMode: 'both'}; } } } } } }, // http://www.quirksmode.org/dom/getstyles.html _getMargin: function(node,styleProp) { var y; if (node.currentStyle) y = parseInt(node.currentStyle[styleProp]); else if (window.getComputedStyle) y = parseInt(document.defaultView.getComputedStyle(node,null).getPropertyValue(styleProp)); if (y) return y; else return 0; }, /** Determines the background coloring. Returns an object literal with two values, 'color' with a color or null and 'transparent' with a boolean. */ _determineBackground: function() { var transparent = false; var color = null; // NOTE: CSS 2.1 spec says background does not get inherited, and we don't // support external CSS style rules for now; we also only support // 'background-color' property and not 'background' CSS property for // setting the background color. var style = this._nodeXML.getAttribute('style'); if (style && style.indexOf('background-color') != -1) { var m = style.match(/background\-color:\s*([^;]*)/); if (m) { color = m[1]; } } if (color === null) { // no background color specified transparent = true; } return {color: color, transparent: transparent}; }, /** Determines what the style should be on the SVG root element, copying over any styles the user has placed inline and defaulting certain styles. We will bring these over to the Flash object. @returns Style string ready to copy over to Flash object. */ _determineStyle: function() { var style = this._nodeXML.getAttribute('style'); if (!style) { style = ''; } // IE sometimes leaves off trailing semicolon of style values if (style.length > 0 && style.charAt(style.length - 1) != ';') { style += ';'; } // SVG spec says default display value for SVG root element is // inline if (this._embedType == 'script' && style.indexOf('display:') == -1) { style += 'display: inline;'; } // SVG spec says SVG by default should have overflow: none if (this._embedType == 'script' && style.indexOf('overflow:') == -1) { style += 'overflow: hidden;'; } return style; }, /** Determines a class name for the Flash object; we simply copy over any class names on the SVG root object to aid in external styling. @returns Class name string. Returns '' if there is none. */ _determineClassName: function() { var className = this._nodeXML.getAttribute('class'); if (!className) { return 'embedssvg'; } else { return className + ' embedssvg'; } }, /** The developer might have added custom attributes on to the place holder we are replacing for SVG OBJECTs; copy those over and make sure they show up on the Flash OBJECT as well. */ _determineCustomAttrs: function() { var results = []; if (this._embedType == 'object') { var node = this._replaceMe; // we use a fake object to determine potential default attributes that // we don't want to copy over to our Flash object var commonObj = document._createElement('object'); for (var j = 0; j < node.attributes.length; j++) { var attr = node.attributes[j]; var attrName = attr.nodeName; var attrValue = attr.nodeValue; // trim out 'empty' attributes with no value if (!attrValue && attrValue !== 'true') { continue; } // trim out anything that is on a common OBJECT so we don't have // overlap if (commonObj.getAttribute(attrName)) { continue; } // trim out other OBJECT overrides as well as some custom attributes // we might have added earlier in the OBJECT creation process // (_listeners, addEventListener, etc.) if (/^(id|name|width|height|data|class|style|codebase|type|_listeners|addEventListener|onload)$/.test(attrName)) { continue; } results.push({attrName: attrName.toString(), attrValue: attrValue.toString()}); } } return results; }, /** Creates a Flash object that embeds the Flash SVG Viewer. @param size Object literal with width and height. @param elementID The ID of either the SVG ROOT object or of an SVG OBJECT. @param background Object literal with background color and transparent boolean. @param style Style string to copy onto Flash object. @param className The class name to copy into the Flash object. @param customAttrs Array of custom attributes that the developer might have put on their SVG OBJECT; each entry in the array is an object literal with attrName and attrValue as the keys. @returns Flash object as HTML string. If the page is an XHTML page, then we return an EMBED tag already instantiated and ready to insert; this is because we do not have innerHTML on XHTML pages. */ _createFlash: function(size, elementID, background, style, className, customAttrs) { var flashVars = 'uniqueId=' + encodeURIComponent(elementID) + '&sourceType=string' + '&clipMode=' + size.clipMode + '&debug=true' + '&svgId=' + encodeURIComponent(elementID); var src; if (this._isXDomain) { src = svgweb.xDomainURL + 'svg.swf'; } else { src = svgweb.libraryPath + 'svg.swf'; } var protocol = window.location.protocol; if (protocol.charAt(protocol.length - 1) == ':') { protocol = protocol.substring(0, protocol.length - 1); } var flash; if (isXHTML) { // XHTML environments have no innerHTML flash = document.createElement('embed'); flash.setAttribute('src', src); flash.setAttribute('quality', 'high'); // FIXME: Will this logic test break if the color is black? if (background.color) { flash.setAttribute('bgcolor', background.color); } if (background.transparent) { flash.setAttribute('wmode', 'transparent'); } flash.setAttribute('width', size.width); flash.setAttribute('height', size.height); flash.setAttribute('id', this._handler.flashID); flash.setAttribute('name', this._handler.flashID); flash.setAttribute('swLiveConnect', 'true'); flash.setAttribute('allowScriptAccess', 'always'); flash.setAttribute('type', 'application/x-shockwave-flash'); flash.setAttribute('FlashVars', flashVars); flash.setAttribute('pluginspage', protocol + '://www.macromedia.com/go/getflashplayer'); flash.setAttribute('style', style); flash.setAttribute('className', className); for (var i = 0; i < customAttrs.length; i++) { flash.setAttribute(customAttrs[i].attrName, customAttrs[i].attrValue); } } else { // normal text/html environment var customAttrStr = ''; for (var i = 0; i < customAttrs.length; i++) { customAttrStr += ' ' + customAttrs[i].attrName + '="' + customAttrs[i].attrValue + '"'; } flash = '\n ' + '\n ' + '\n ' + '\n ' + '\n ' // FIXME: Will this logic test break if the color is black? + (background.color ? '\n ' : '') + (background.transparent ? '' + '\n ' : '') + '' + ''; } return flash; } }); /** SVG Root element. @param nodeXML A parsed XML node object that is the SVG root node. @param svgString The full SVG as a string. Null if this SVG root element is being embedded by an SVG OBJECT. @param scriptNode The script node that contains this SVG. Null if this SVG root element is being embedded by an SVG OBJECT. @param handler The FlashHandler that we are a part of. */ function _SVGSVGElement(nodeXML, svgString, scriptNode, handler) { // superclass constructor _Element.apply(this, ['svg', null, svgns, nodeXML, handler, true]); this._nodeXML = nodeXML; this._svgString = svgString; this._scriptNode = scriptNode; // flash that we use to know whether the HTC and SWF files are loaded this._htcLoaded = false; this._swfLoaded = false; // add to our nodeByID lookup table so that fetching this node in the // future works if (this._handler.type == 'script') { var rootID = this._nodeXML.getAttribute('id'); var doc = this._handler.document; doc._nodeById['_' + rootID] = this; } this._currentScale = 1; this._currentTranslate = this._createCurrentTranslate(); // when being embedded by a SCRIPT element, the _SVGSVGElement class // takes over inserting the Flash and HTC elements so that we have // something visible on the screen; when being embedded by an SVG OBJECT // we don't do this since the OBJECT tag itself is what is visible on the // screen if (isIE && this._handler.type == 'script') { // slot in our suspendRedraw/unsuspendRedraw methods this._addRedrawMethods(); // track .style changes this.style = new _Style(this); // find out when the content is ready // NOTE: we do this here instead of inside the HTC file using an // internal oncontentready event in order to make the HTC file faster // and use less memory. Note also that 'oncontentready' is not available // outside HTC files, only 'onreadystatechange' is available. this._readyStateListener = hitch(this, this._onHTCLoaded); // cleanup later this._htcNode.attachEvent('onreadystatechange', this._readyStateListener); // now wait for the HTC file to load for the SVG root element; // continue inserting our Flash object below as well so that the HTC // file and SWF file load in parallel for better overall performance } else if (isIE && this._handler.type == 'object') { // slot in our suspendRedraw/unsuspendRedraw methods this._addRedrawMethods(); } if (this._handler.type == 'script') { // insert the Flash this._handler._inserter = new FlashInserter('script', this._nodeXML, this._scriptNode, this._handler); } } // subclasses _Element _SVGSVGElement.prototype = new _Element; extend(_SVGSVGElement, { // SVGSVGElement // NOTE: there are properties and methods from SVGSVGElement not defined // or implemented here; see // http://www.w3.org/TR/SVG/struct.html#InterfaceSVGSVGElement // for full list // TODO: Implement the functions below suspendRedraw: function(ms /* unsigned long */) /* unsigned long */ { return this._handler._redrawManager.suspendRedraw(ms); }, unsuspendRedraw: function(id /* unsigned long */) /* void */ /* throws DOMException */ { this._handler._redrawManager.unsuspendRedraw(id); }, unsuspendRedrawAll: function() /* void */ { this._handler._redrawManager.unsuspendRedrawAll(); }, forceRedraw: function() /* void */ { // not implemented }, // end SVGSVGElement // SVGLocatable // TODO: Implement the following properties nearestViewportElement: null, /* readonly SVGElement */ farthestViewportElement: null, /* readonly SVGElement */ // TODO: Implement the following methods getBBox: function() /* SVGRect */ {}, getTransformToElement: function(element /* SVGElement */) /* SVGMatrix */ { /* throws SVGException */ }, // end of SVGLocatable /** Called when the Microsoft Behavior HTC file is loaded. */ _onHTCLoaded: function() { //console.log('onHTCLoaded'); //end('HTCLoading'); //start('onHTCLoaded'); // cleanup our event handler this._htcNode.detachEvent('onreadystatechange', this._readyStateListener); // pay attention to style changes now in the HTC this.style._ignoreStyleChanges = false; //end('onHTCLoaded'); // indicate that the HTC is loaded; see if Flash is loaded this._htcLoaded = true; if (this._swfLoaded) { this._onEverythingLoaded(); } // TODO: we are not handling dynamically created nodes yet }, /** Called when the Flash SWF file has been loaded. Note that this doesn't include the SVG being rendered -- at this point we haven't even sent the SVG to the Flash file for rendering yet. */ _onFlashLoaded: function(msg) { //end('SWFLoading'); //start('onFlashLoaded'); // the Flash object is done loading //console.log('_onFlashLoaded'); // store a reference to our Flash object this._handler.flash = document.getElementById(this._handler.flashID); // for non-IE browsers we are ready to go; for IE, see if the HTC is done // loading yet this._swfLoaded = true; if (!isIE || this._htcLoaded) { this._onEverythingLoaded(); } //end('onFlashLoaded'); }, /** Called when the Flash is loaded initially, as well as the HTC file for IE. */ _onEverythingLoaded: function() { //console.log('_onEverythingLoaded'); // send the SVG over to Flash now //start('firstSendToFlash'); //start('jsHandleLoad'); var size = this._handler._inserter._determineSize(); this._handler.sendToFlash('jsHandleLoad', [ /* objectURL */ this._getRelativeTo('object'), /* pageURL */ this._getRelativeTo('page'), /* objectWidth */ size.pixelsWidth, /* objectHeight */ size.pixelsHeight, /* ignoreWhiteSpace */ true, /* svgString */ this._svgString ]); //end('jsHandleLoad'); }, /** The Flash is finished rendering. */ _onRenderingFinished: function(msg) { //end('firstSendToFlash'); //start('onRenderingFinished'); //console.log('onRenderingFinished'); if (this._handler.type == 'script') { // expose the root SVG element as 'documentElement' on the EMBED // or OBJECT tag for SVG SCRIPT embed as a utility property for // developers to descend down into the SVG root tag // (see Known Issues and Errata for details) this._handler.flash.documentElement = this._getProxyNode(); } // set the ownerDocument based on how we are embedded if (this._attached) { if (this._handler.type == 'script') { this.ownerDocument = document; } else if (this._handler.type == 'object') { this.ownerDocument = this._handler.document; } } this._handler.document.rootElement = this._getProxyNode(); var elementId = this._nodeXML.getAttribute('id'); this._handler._loaded = true; //end('onRenderingFinished'); this._handler.fireOnLoad(elementId, 'script'); }, /** Relative URLs inside of SVG need to expand against something (i.e. such as having an SVG Audio tag with a relative URL). This method figures out what that relative URL should be. We send this over to Flash when rendering things so Flash knows what to expand against. */ _getRelativeTo: function() { var results = ''; var pathname = window.location.pathname.toString(); if (pathname && pathname.length > 0 && pathname.indexOf('/') != -1) { // snip off any filename after a final slash results = pathname.replace(/\/([^\/]*)$/, '/'); } return results; }, /** Adds the various suspendRedraw/unsuspendRedraw methods to our HTC proxy for IE. We do it here for two reasons: so that we don't have to bloat the size of the HTC file which has a large affect on performance, and so that these methods don't show up for SVG nodes that aren't the root SVG node. */ _addRedrawMethods: function() { // add methods inside of fresh closures to prevent IE memory leaks this._htcNode.suspendRedraw = (function() { return function(ms) { return this._fakeNode.suspendRedraw(ms); }; })(); this._htcNode.unsuspendRedraw = (function() { return function(id) { return this._fakeNode.unsuspendRedraw(id); }; })(); this._htcNode.unsuspendRedrawAll = (function() { return function() { return this._fakeNode.unsuspendRedrawAll(); }; })(); this._htcNode.forceRedraw = (function() { return function() { return this._fakeNode.forceRedraw(); }; })(); }, /** Sets up our currentTranslate property to pass over any changes on the X and Y values over to Flash. */ _createCurrentTranslate: function() { var p = new _SVGPoint(0, 0, true /* formalAccessor */, hitch(this, this._updateCurrentTranslate)); return p; }, _updateCurrentTranslate: function(type, newValue1, newValue2) { if (type == 'xy') { this._handler.sendToFlash('jsSetCurrentTranslate', [ 'xy', newValue1, newValue2 ]); } else { this._handler.sendToFlash('jsSetCurrentTranslate', [ type, newValue1 ]); } } }); /** Represent a Document object for manipulating the SVG document. @param xml Parsed XML for the SVG. @param handler The FlashHandler this document is a part of. */ function _Document(xml, handler) { // superclass constructor _Node.apply(this, ['#document', _Node.DOCUMENT_NODE, null, null, xml, handler], svgns); this._xml = xml; this._handler = handler; this._nodeById = {}; this._namespaces = this._getNamespaces(); this.implementation = new _DOMImplementation(); if (this._handler.type == 'script') { this.defaultView = window; } else if (this._handler.type == 'object') { // we set the document.defaultView in _SVGObject._executeScript() once // we create the iframe that we execute our script into } } // subclasses _Node _Document.prototype = new _Node; extend(_Document, { /** Stores a lookup from a node's ID to it's _Element or _Node representation. An object literal. */ _nodeById: null, /* Note: technically these 2 properties should be read-only and throw a DOMException when set. For simplicity we make them simple JS properties; if set, nothing will happen. Also note that we don't support the 'doctype' property. */ implementation: null, documentElement: null, createElementNS: function(ns, qname) /* _Element */ { var prefix = this._namespaces['_' + ns]; if (prefix == 'xmlns' || !prefix) { // default SVG namespace // If this is a new namespace, we may have to assume the // prefix from the qname if (qname.indexOf(':') != -1) { prefix=qname.substring(0, qname.indexOf(':')) } else { prefix = null; } } var node = new _Element(qname, prefix, ns); return node._getProxyNode(); }, createTextNode: function(data /* DOM Text Node */) /* _Node */ { // We create a DOM Element node to represent this text node. We do this // so that we can track the text node over time to register changes to // it and so on. We must use a DOM Element node so that we have access // to the setAttribute method in order to store data on the XML DOM node. // This is due to a limitation on Internet Explorer where you can not // store 'expandos' on XML node objects (i.e. you can't add custom // properties). We store the actual data as a DOM Text Node of our DOM // Element. Note that since we have no handler yet we simply use a default // XML document object (_unattachedDoc) to create things for now. var doc = FlashHandler._unattachedDoc; var nodeXML; if (isIE) { // no createElementNS available nodeXML = doc.createElement('__text'); } else { nodeXML = doc.createElementNS(svgnsFake, '__text'); } nodeXML.appendChild(doc.createTextNode(data)); var textNode = new _Node('#text', _Node.TEXT_NODE, null, null, nodeXML, this._handler); textNode._nodeValue = data; textNode.ownerDocument = this; return textNode._getProxyNode(); }, createDocumentFragment: function(forSVG) { return new _DocumentFragment(this)._getProxyNode(); }, getElementById: function(id) /* _Element */ { // XML parser does not have getElementById, due to id mapping in XML // issues; use XPath instead var results = xpath(this._xml, null, '//*[@id="' + id + '"]'); var nodeXML, node; if (results.length) { nodeXML = results[0]; } else { return null; } // create or get an _Element for this XML DOM node for node node = FlashHandler._getNode(nodeXML, this._handler); node._passThrough = true; return node; }, /** NOTE: on IE we don't support calls like the following: getElementsByTagNameNS(*, 'someTag'); We do support: getElementsByTagNameNS('*', '*'); getElementsByTagNameNS('someNameSpace', '*'); getElementsByTagNameNS(null, 'someTag'); */ getElementsByTagNameNS: function(ns, localName) /* _NodeList of _Elements */ { //console.log('document.getElementsByTagNameNS, ns='+ns+', localName='+localName); // we might be a dynamically created SVG script node that is not done // loading yet if (this._handler.type == 'script' && !this._handler._loaded) { return []; } var results = this.rootElement.getElementsByTagNameNS(ns, localName); // Make sure to include root SVG node in our results if that is what // is asked for! if (ns == svgns && localName == 'svg') { results.push(this.rootElement); } return results; }, // Note: createDocumentFragment, createComment, createCDATASection, // createProcessingInstruction, createAttribute, createEntityReference, // importNode, createElement, getElementsByTagName, // createAttributeNS not supported /** Extracts any namespaces we might have, creating a prefix/namespaceURI lookup table. NOTE: We only support namespace declarations on the root SVG node for now. @returns An object that associates prefix to namespaceURI, and vice versa. */ _getNamespaces: function() { var results = []; var attrs = this._xml.documentElement.attributes; for (var i = 0; i < attrs.length; i++) { var attr = attrs[i]; if (/^xmlns:?(.*)$/.test(attr.nodeName)) { var m = attr.nodeName.match(/^xmlns:?(.*)$/); var prefix = (m[1] ? m[1] : 'xmlns'); var namespaceURI = attr.nodeValue; // don't add duplicates if (!results['_' + prefix]) { results['_' + prefix] = namespaceURI; results['_' + namespaceURI] = prefix; results.push(namespaceURI); } } } return results; } }); // We don't create a NodeList class due to the complexity of subclassing // the Array object cross browser. Instead, we simply patch in the item() // method to a normal Array object function createNodeList() { var results = []; results.item = function(i) { if (i >= this.length) { return null; // DOM Level 2 spec says return null } else { return this[i]; } } return results; } // We don't have an actual DOM CharacterData type for now. We just return // a String object with the 'data' property patched in, since that is what // is most commonly accessed function createCharacterData(data) { var results = (data !== undefined) ? new String(data) : new String(); results.data = results.toString(); return results; } // End DOM Level 2 Core/Events support // SVG DOM interfaces // Note: where the spec returns an SVGNumber or SVGString we just return // the JavaScript base type instead. Note that in general also instead of // returning the many SVG List types, such as SVGPointList, we just // return standard JavaScript Arrays. For SVGAngle we also // just return a JS Number for now. function _SVGMatrix(a /** All Numbers */, b, c, d, e, f, _handler) { this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; this._handler = _handler; } extend(_SVGMatrix, { // all functions return _SVGMatrix // TODO: Implement the following methods multiply: function(secondMatrix /* _SVGMatrix */ ) {}, inverse: function() { var msg =this._handler.sendToFlash('jsMatrixInvert', [ this.a, this.b, this.c, this.d, this.e, this.f ]); msg = this._handler._stringToMsg(msg); return new _SVGMatrix(new Number(msg.a), new Number(msg.b), new Number(msg.c), new Number(msg.d), new Number(msg.e), new Number(msg.f), this._handler); }, translate: function(x /* Number */, y /* Number */) {}, scale: function(scaleFactor /* Number */) {}, scaleNonUniform: function(scaleFactorX /* Number */, scaleFactorY /* Number */) {}, rotate: function(angle /* Number */) {}, rotateFromVector: function(x, y) {}, flipX: function() {}, flipY: function() {}, skewX: function(angle) {}, skewY: function(angle) {} }); // Note: Most of the functions on SVGLength not supported for now function _SVGLength(/* Number */ value) { this.value = value; } // Note: We only support _SVGAnimatedLength because that is what Firefox // and Safari return, and we want to have parity. Only baseVal works for now function _SVGAnimatedLength(/* _SVGLength */ value) { this.baseVal = value; this.animVal = undefined; // not supported for now } function _SVGTransform(type, matrix, angle) { this.type = type; this.matrix = matrix; this.angle = angle; } mixin(_SVGTransform, { SVG_TRANSFORM_UNKNOWN: 0, SVG_TRANSFORM_MATRIX: 1, SVG_TRANSFORM_TRANSLATE: 2, SVG_TRANSFORM_SCALE: 3, SVG_TRANSFORM_ROTATE: 4, SVG_TRANSFORM_SKEWX: 5, SVG_TRANSFORM_SKEWY: 6 }); extend(_SVGTransform, { // Note: the following 3 should technically be readonly type: null, /* one of the constants above */ matrix: null, /* _SVGMatrix */ angle: null, /* float */ // TODO: Implement the following methods setMatrix: function(matrix /* SVGMatrix */) {}, setTranslate: function(tx /* float */, ty /* float */) {}, setScale: function(sx /* float */, sy /* float */) {}, setRotate: function(angle /* float */, cx /* float */, cy /* float */) {}, setSkewX: function(angle /* float */) {}, setSkewY: function(angle /* float */) {} }); /** SVGPoint class. @formalAccessors - Optional boolean that controls whether we force the class to have formal get and set method to handle limitations in IE, such as getX. Defaults to false. @callback - Optional Function. Called when a setter is called. Given the following arguments: 'x', 'y', or 'xy' on what is being set followed by the new value(s) */ function _SVGPoint(x, y, formalAccessors, callback) { if (formalAccessors === undefined) { formalAccessors = false; } this._formalAccessors = formalAccessors; this.x = x; this.y = y; if (formalAccessors) { this.setX = hitch(this, function(newValue) { this.x = newValue; callback('x', newValue); }); this.getX = hitch(this, function() { return this.x; }); this.setY = hitch(this, function(newValue) { this.y = newValue; callback('y', newValue); }); this.getY = hitch(this, function() { return this.y; }); this.setXY = hitch(this, function(newX, newY) { this.x = newX; this.y = newY; callback('xy', newX, newY); }); } } extend(_SVGPoint, { matrixTransform: function(m) { return new _SVGPoint( m.a * this.x + m.c * this.y + m.e, m.b * this.x + m.d * this.y + m.f, this._formalAccessors); } }); // SVGRect function _SVGRect(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; } // end SVG DOM interfaces /* Other DOM interfaces specified by SVG 1.1: * SVG 1.1 spec requires DOM 2 Views support, which we do not implement: http://www.w3.org/TR/DOM-Level-2-Views/ * SVG 1.1 spec has the DOM traversal and range APIs as optional; these are not supported * Technically we need to support certain DOM Level 2 CSS interfaces: http://www.w3.org/TR/DOM-Level-2-Style/css.html We support some (anything that should be on an SVG Element), but the following interfaces are not supported: CSSStyleSheet, CSSRuleList, CSSRule, CSSStyleRule, CSSMediaRule, CSSFontFaceRule, CSSPageRule, CSSImportRule, CSSCharsetRule, CSSUnknownRule, CSSStyleDeclaration, CSSValue, CSSPrimitiveValue, CSSValueList, RGBColor, Rect, Counter, ViewCSS (getComputedStyle), DocumentCSS, DOMImplementationCSS, none of the CSS 2 Extended Interfaces * There are many SVG DOM interfaces we don't support */ window.svgweb = new SVGWeb(); // kicks things off // hide internal implementation details inside of a closure })(); // Uncomment when doing performance profiling /* window.timer = {}; function start(subject, subjectStarted) { //console.log('start('+subject+','+subjectStarted+')'); if (subjectStarted && !ifStarted(subjectStarted)) { //console.log(subjectStarted + ' not started yet so returning for ' + subject); return; } //console.log('storing time for ' + subject); window.timer[subject] = {start: new Date().getTime()}; } function end(subject, subjectStarted) { //console.log('end('+subject+','+subjectStarted+')'); if (subjectStarted && !ifStarted(subjectStarted)) { //console.log(subjectStarted + ' not started yet so returning for ' + subject); return; } if (!window.timer[subject]) { console.log('Unknown subject: ' + subject); return; } window.timer[subject].end = new Date().getTime(); //console.log('at end, storing total time: ' + total(subject)); } function increment(subject, amount) { if (!window.timer[subject]) { window.timer[subject] = {incremented: true, total: 0}; } window.timer[subject].total += amount; } function total(subject) { if (!window.timer[subject]) { console.log('Unknown subject: ' + subject); return; } var t = window.timer[subject]; if (t.incremented) { return t.total; } else if (t) { return t.end - t.start; } else { return null; } } function ifStarted(subject) { for (var i in window.timer) { var t = window.timer[i]; if (i == subject && t.start !== undefined && t.end === undefined) { return true; } } return false; } function report() { for (var i in window.timer) { var t = total(i); if (t !== null) { console.log(i + ': ' + t + 'ms'); } } } */ // End of performance profiling functions