You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
9156 lines
331 KiB
9156 lines
331 KiB
/* |
|
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('<script id="__ie__svg__onload" defer ' |
|
+ 'src=//0><\/script>'); |
|
var script = document.getElementById('__ie__svg__onload'); |
|
script.onreadystatechange = function() { |
|
if (this.readyState == 'complete') { |
|
// all the DOM content is finished loading -- continue our internal |
|
// execution now |
|
self._onDOMContentLoaded(); |
|
} |
|
} |
|
|
|
// needed to intercept window.onload when page loaded first time |
|
// (i.e. not in cache); see details above in script.onreadystatechange |
|
var documentReady = function() { |
|
if (window.onload) { |
|
self._saveWindowOnload(); |
|
document.detachEvent('onreadystatechange', documentReady); |
|
} |
|
}; |
|
document.attachEvent('onreadystatechange', documentReady); |
|
} |
|
}, |
|
|
|
/** Determines whether SVG Web is being hosted cross-domain (Issue 285, |
|
"For Wikipedia: Be able to host bulk of SVG Web on a different domain", |
|
http://code.google.com/p/svgweb/issues/detail?id=285). We determine |
|
this by seeing if the svg.js or svg-uncompressed.js files are being |
|
hosted on a different domain than the current page. */ |
|
_setXDomain: function() { |
|
var scripts = document.getElementsByTagName('script'); |
|
for (var i = 0; i < scripts.length; i++) { |
|
if (/svg(?:\-uncompressed)?\.js/.test(scripts[i].src) |
|
&& /^https?/.test(scripts[i].src)) { |
|
// strip out svg.js filename |
|
var url = scripts[i].src.replace(/svg(?:\-uncompressed)?\.js/, ''); |
|
|
|
// get just protocol/hostname/port portion |
|
var loc = url.match(/https?\:\/\/[^\/]*/)[0]; |
|
|
|
// get the protocol/hostname/port portion of our current web page |
|
var ourLoc = window.location.protocol.replace(/:|\//g, '') + '://' |
|
+ window.location.host; |
|
|
|
// are they different? |
|
if (loc != ourLoc) { |
|
this.xDomainURL = url; |
|
this._isXDomain = true; |
|
return; |
|
} |
|
} |
|
} |
|
|
|
this._isXDomain = false; |
|
}, |
|
|
|
/** Gets any data-path value that might exist on the SCRIPT tag |
|
that pulls in our svg.js or svg-uncompressed.js library to configure |
|
where to find library resources like SWF files, HTC files, etc. |
|
You can also use a META tag with the name 'svg.config.data-path' |
|
and the content property set to the data path. */ |
|
_getLibraryPath: function() { |
|
// determine the path to our HTC and Flash files |
|
var libraryPath = './'; |
|
|
|
var meta = document.getElementsByTagName('meta'); |
|
for (var i = 0; i < meta.length; i++) { |
|
if (meta[i].name == 'svg.config.data-path' |
|
&& meta[i].content.length > 0) { |
|
libraryPath = meta[i].content; |
|
} |
|
} |
|
|
|
var scripts = document.getElementsByTagName('script'); |
|
for (var i = 0; i < scripts.length; i++) { |
|
if (/svg(?:\-uncompressed)?\.js/.test(scripts[i].src) |
|
&& scripts[i].getAttribute('data-path')) { |
|
libraryPath = scripts[i].getAttribute('data-path'); |
|
break; |
|
} |
|
} |
|
|
|
if (libraryPath.charAt(libraryPath.length - 1) != '/') { |
|
libraryPath += '/'; |
|
} |
|
|
|
return libraryPath; |
|
}, |
|
|
|
/** Gets an optional data-htc-filename value that might exist on the SCRIPT |
|
tag. If present, this holds a different filename for where to grab the |
|
HTC file from, such as svg-htc.php or svg-htc.asp. This is a trick to |
|
help support those in environments where they can't manually add new |
|
MIME types, so we have an ASP, JSP, or PHP file set the MIME type for the |
|
HTC file automatically. */ |
|
_getHTCFilename: function() { |
|
var htcFilename = 'svg.htc'; |
|
|
|
// see if one of our three predefined file names is given in the query |
|
// string as the query parameter 'svg.htcFilename'; we do whitelisting |
|
// rather than directly copying this value in to prevent XSS attacks |
|
var loc = window.location.toString(); |
|
if (loc.indexOf('svg.htcFilename=svg-htc.php') != -1) { |
|
return 'svg-htc.php'; |
|
} else if (loc.indexOf('svg.htcFilename=svg-htc.jsp') != -1) { |
|
return 'svg-htc.jsp'; |
|
} else if (loc.indexOf('svg.htcFilename=svg-htc.asp') != -1) { |
|
return 'svg-htc.asp'; |
|
} |
|
|
|
var scripts = document.getElementsByTagName('script'); |
|
for (var i = 0; i < scripts.length; i++) { |
|
if (/svg(?:\-uncompressed)?\.js/.test(scripts[i].src) |
|
&& scripts[i].getAttribute('data-htc-filename')) { |
|
htcFilename = scripts[i].getAttribute('data-htc-filename'); |
|
break; |
|
} |
|
} |
|
|
|
return htcFilename; |
|
}, |
|
|
|
/** Fires when the DOM content of the page is ready to be worked with. */ |
|
_onDOMContentLoaded: function() { |
|
//console.log('onDOMContentLoaded'); |
|
//start('DOMContentLoaded'); |
|
|
|
// quit if this function has already been called |
|
if (arguments.callee.done) { |
|
return; |
|
} |
|
|
|
// flag this function so we don't do the same thing twice |
|
arguments.callee.done = true; |
|
|
|
// start tracking total startup time |
|
this._startTime = new Date().getTime(); |
|
|
|
// cleanup onDOMContentLoaded handler to prevent memory leaks on IE |
|
var listener = document.getElementById('__ie__svg__onload'); |
|
if (listener) { |
|
listener.parentNode.removeChild(listener); |
|
listener.onreadystatechange = null; |
|
listener = null; |
|
} |
|
|
|
// save any window.onsvgload listener that might be present |
|
this._saveWindowOnload(); |
|
|
|
// determine what renderers (native or Flash) to use for which browsers |
|
this.config = new RenderConfig(); |
|
|
|
// sign up for the onunload event on IE to clean up references that |
|
// can cause memory leaks |
|
if (isIE) { |
|
this._watchUnload(); |
|
} |
|
|
|
// extract any SVG SCRIPTs or OBJECTs from the page |
|
this._svgScripts = this._getSVGScripts(); |
|
this._svgObjects = this._getSVGObjects(); |
|
|
|
this.totalSVG = this._svgScripts.length + this._svgObjects.length; |
|
|
|
// do various things we must do early on in the page load process |
|
// around cleaning up SVG OBJECTs on the page |
|
this._cleanupSVGObjects(); |
|
|
|
// handle a peculiarity for Safari (see method for details) |
|
this._handleHTMLTitleBug(); |
|
|
|
// see if we can even support SVG in any way |
|
if (!this.config.supported) { |
|
// no ability to use SVG in any way |
|
this._displayNotSupported(this.config.reason); |
|
this._fireOnLoad(); |
|
return; |
|
} |
|
|
|
// setup which renderer we will use |
|
this.renderer; |
|
if (this.config.use == 'flash') { |
|
this.renderer = FlashHandler; |
|
} else if (this.config.use == 'native') { |
|
this.renderer = NativeHandler; |
|
} |
|
|
|
// patch the document and style objects with bug fixes for the |
|
// NativeHandler and actual implementations for the FlashHandler |
|
this.renderer._patchBrowserObjects(window, document); |
|
|
|
// there may be objects added later, so add our resize listener before |
|
// checking for whether there is any SVG content |
|
if (this.config.use == 'flash') { |
|
// Attach window resize listener to adjust SVG size when % is used |
|
// on the SVG width and height |
|
this._createResizeListener(); |
|
this._attachResizeListener(); |
|
} |
|
|
|
// no SVG - we're done |
|
if (this.totalSVG === 0) { |
|
this._fireOnLoad(); |
|
return; |
|
} |
|
|
|
// now process each of the SVG SCRIPTs and SVG OBJECTs |
|
var self = this; |
|
for (var i = 0; i < this._svgScripts.length; i++) { |
|
this._processSVGScript(this._svgScripts[i]); |
|
} |
|
|
|
for (var i = 0; i < this._svgObjects.length; i++) { |
|
var objID = this._processSVGObject(this._svgObjects[i]); |
|
|
|
// 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 |
|
this._svgObjects[i]._objID = objID; |
|
} |
|
//end('DOMContentLoaded'); |
|
|
|
// wait until all of them have done their work, then fire onload |
|
}, |
|
|
|
_createResizeListener: function() { |
|
var self = this; |
|
if (isIE) { |
|
this._resizeFunc = |
|
(function(self) { |
|
return function() { |
|
self._onWindowResize(); |
|
}; |
|
})(this); // prevent IE memory leaks |
|
} else { |
|
this._resizeFunc = hitch(this, function() { |
|
this._onWindowResize(); |
|
}); |
|
} |
|
}, |
|
|
|
_attachResizeListener: function() { |
|
if (isIE) { |
|
window.attachEvent('onresize', this._resizeFunc); |
|
} else { |
|
window.addEventListener('resize', this._resizeFunc, false); |
|
} |
|
}, |
|
|
|
_detachResizeListener: function() { |
|
if (isIE) { |
|
window.detachEvent('onresize', this._resizeFunc); |
|
} else { |
|
window.removeEventListener('resize', this._resizeFunc, false); |
|
} |
|
}, |
|
|
|
_onWindowResize: function() { |
|
if (!this.pageLoaded) { |
|
return; |
|
} |
|
this._detachResizeListener(); |
|
for (var i = 0; i < this.handlers.length; i++) { |
|
var handler = this.handlers[i]; |
|
if (!handler._inserter || !handler.flash) { |
|
// Flash still being rendered |
|
continue; |
|
} |
|
|
|
var size = handler._inserter._determineSize(); |
|
//console.log("svg #"+i+": flash resize: " |
|
// + size.width + "," + size.height + " " |
|
// + size.pixelsWidth + "," + size.pixelsHeight); |
|
handler.flash.width = size.width; |
|
handler.flash.height = size.height; |
|
handler.sendToFlash('jsHandleResize', |
|
[ /* objectWidth */ size.pixelsWidth, |
|
/* objectHeight */ size.pixelsHeight ]); |
|
} |
|
this._attachResizeListener(); |
|
}, |
|
|
|
/** Gets any SVG SCRIPT blocks on the page. */ |
|
_getSVGScripts: function() { |
|
var scripts = document.getElementsByTagName('script'); |
|
var results = []; |
|
for (var i = 0; i < scripts.length; i++) { |
|
if (scripts[i].type == 'image/svg+xml') { |
|
results.push(scripts[i]); |
|
} |
|
} |
|
|
|
return results; |
|
}, |
|
|
|
/** Gets any SVG OBJECTs on the page. */ |
|
_getSVGObjects: function() { |
|
// Note for IE: Unfortunately we have to use @classid to carry our MIME |
|
// type instead of @type for IE. Here's why: on IE, you must have |
|
// either @classid or @type present on your OBJECT tag. If there is |
|
// _any_ fallback content inside of the OBJECT tag, IE will erase the |
|
// OBJECT from the DOM and replace it with the fallback content if the |
|
// @type attribute is set to an unknown MIME type; this makes it |
|
// impossible for us to then get the OBJECT from the DOM below. If we |
|
// don't have a @classid this will also happen, so we just set it to |
|
// the string 'image/svg+xml' which is arbitrary. If both @type and |
|
// @classid are present, IE will still look at the type first and |
|
// repeat the same incorrect fallback behavior for our purposes. |
|
var objs = document.getElementsByTagName('object'); |
|
var results = []; |
|
for (var i = 0; i < objs.length; i++) { |
|
if (objs[i].getAttribute('classid') == 'image/svg+xml') { |
|
results.push(objs[i]); |
|
} else if (objs[i].getAttribute('type') == 'image/svg+xml') { |
|
results.push(objs[i]); |
|
} |
|
} |
|
|
|
return results; |
|
}, |
|
|
|
/** Displays not supported messages. |
|
|
|
@param reason String containing why this browser is not supported. */ |
|
_displayNotSupported: function(reason) { |
|
// write the reason into the OBJECT tags if nothing is already present |
|
for (var i = 0; i < this._svgObjects.length; i++) { |
|
var obj = this._svgObjects[i]; |
|
// ignore whitespace children |
|
if (!obj.childNodes.length || |
|
(obj.childNodes.length == 1 && obj.childNodes[0].nodeType == 3 |
|
&& /^[ ]*$/m.test(obj.childNodes[0].nodeValue))) { |
|
var span = document.createElement('span'); |
|
span.className = 'svg-noscript'; |
|
span.appendChild(document.createTextNode(reason)); |
|
obj.parentNode.replaceChild(span, obj); |
|
} |
|
} |
|
|
|
// surface any adjacent NOSCRIPTs that might be adjacent to our SVG |
|
// SCRIPTs; if none present, write out our reason |
|
for (var i = 0; i < this._svgScripts.length; i++) { |
|
var script = this._svgScripts[i]; |
|
var output = document.createElement('span'); |
|
output.className = 'svg-noscript'; |
|
|
|
var sibling = script.nextSibling; |
|
// jump past everything until we hit our first Element |
|
while (sibling && sibling.nodeType != 1) { |
|
sibling = sibling.nextSibling; |
|
} |
|
|
|
if (sibling && sibling.nodeName.toLowerCase() == 'noscript') { |
|
var noscript = sibling; |
|
output.innerHTML = noscript.innerHTML; |
|
} else { |
|
output.appendChild(document.createTextNode(reason)); |
|
} |
|
|
|
script.parentNode.insertBefore(output, script); |
|
} |
|
}, |
|
|
|
/** Fires any addOnLoad() listeners that were registered by a developer. */ |
|
_fireOnLoad: function() { |
|
//console.log('svgweb._fireOnLoad'); |
|
// see if all SVG OBJECTs are done loading; if so, fire final onload |
|
// event for any externally registered SVG |
|
if (this.handlers.length < this._svgObjects.length) { |
|
// not even done constructing all our Native Handlers yet |
|
return; |
|
} |
|
|
|
var allLoaded = true; |
|
for (var i = 0; i < this.handlers.length; i++) { |
|
var h = this.handlers[i]; |
|
if (!h._loaded) { |
|
allLoaded = false; |
|
break; |
|
} |
|
} |
|
|
|
if (!allLoaded) { |
|
return; |
|
} |
|
|
|
// the page is truly finished loading |
|
this.pageLoaded = true; |
|
|
|
// report total startup time |
|
this._endTime = new Date().getTime(); |
|
// FIXME: Get these times to be more accurate; I think they are off |
|
//console.log('Total JS plus Flash startup time: ' |
|
// + (this._endTime - this._startTime) + 'ms'); |
|
|
|
if (this._loadListeners.length) { |
|
// we do a slight timeout so that if exceptions get thrown inside the |
|
// developers onload methods they will correctly show up and get reported |
|
// to the developer; otherwise since the fireOnLoad method is called |
|
// from Flash and an exception gets called it can get 'squelched' |
|
var self = this; |
|
window.setTimeout(function() { |
|
//console.log('svgweb._fireOnLoad timeout'); |
|
// make a copy of our listeners and then clear them out _before_ looping |
|
// and calling each one; this is to handle the following edge condition: |
|
// one of the listeners might dynamically create an SVG OBJECT _inside_ |
|
// of it, which would then add a new listener, and we would then |
|
// incorrectly get it in our list of listeners as we loop below! |
|
var listeners = self._loadListeners; |
|
self._loadListeners = []; |
|
this.totalLoaded = 0; |
|
for (var i = 0; i < listeners.length; i++) { |
|
try { |
|
listeners[i](); |
|
} catch (exp) { |
|
console.log('Error while firing onload: ' + (exp.message || exp)); |
|
} |
|
} |
|
}, 1); |
|
} |
|
}, |
|
|
|
/** Cleans up some SVG in various ways (adding IDs, etc.) |
|
|
|
@param svg SVG string to clean up. |
|
@param addMissing If true, we add missing |
|
XML doctypes, SVG namespace, etc. to make working with SVG a bit easier |
|
when doing direct embed; if false, we require the XML to be |
|
well-formed and correct (primarily for independent .svg files). |
|
@param normalizeWhitespace If true, we try to remove whitespace between |
|
nodes to make the DOM more similar to Internet Explorer's |
|
ignoreWhiteSpace, mostly used when doing direct embed of SVG into an |
|
HTML page; if false, we leave things alone (primarily for independent |
|
.svg files). |
|
|
|
@returns Returns an object with two values: |
|
svg: cleaned up SVG as a String |
|
xml: parsed SVG as an XML object |
|
*/ |
|
_cleanSVG: function(svg, addMissing, normalizeWhitespace) { |
|
// if this was directly embedded SVG into a SCRIPT block on an XHTML page, |
|
// but on IE, then it will be surrounded with a CDATA block; remove this |
|
if (/^\s*<\!\[CDATA\[/.test(svg)) { |
|
svg = svg.replace(/^\s*<\!\[CDATA\[/, ''); |
|
svg = svg.replace(/\]\]>\s*/, ''); |
|
} |
|
|
|
if (addMissing) { |
|
// add any missing things (XML declaration, SVG namespace, etc.) |
|
if (/\<\?xml/m.test(svg) == false) { // XML declaration |
|
svg = '<?xml version="1.0"?>\n' + svg; |
|
} |
|
|
|
// add SVG namespace declaration |
|
|
|
if (/xmlns\=['"]http:\/\/www\.w3\.org\/2000\/svg['"]/.test(svg) == false) { |
|
svg = svg.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"'); |
|
} |
|
|
|
// add xlink namespace if it is not present |
|
if (/xmlns:[^=]+=['"]http:\/\/www\.w3\.org\/1999\/xlink['"]/.test(svg) == false) { |
|
svg = svg.replace('<svg', '<svg xmlns:xlink="http://www.w3.org/1999/xlink"'); |
|
} |
|
} |
|
|
|
// remove leading whitespace before XML declaration |
|
if (svg.charAt(0) != '<') { |
|
svg = svg.replace(/\s*<\?xml/, '<?xml'); |
|
} |
|
|
|
// expand ENTITY definitions |
|
// Issue 221: "DOCTYPE ENTITYs not expanded on certain |
|
// browsers (safari, opera)" |
|
// http://code.google.com/p/svgweb/issues/detail?id=221 |
|
// NOTE: entity expansion is performance sensitive; see |
|
// Issue 218 for details |
|
// (http://code.google.com/p/svgweb/issues/detail?id=218) |
|
RegExp.lastIndex = 0; |
|
var match; |
|
var entityRE = /<!ENTITY\s+(\S+)\s+"([^"]*)"/g; |
|
while ((match = entityRE.exec(svg)) != null) { |
|
var entityName = RegExp.$1; |
|
var entityValue = RegExp.$2; |
|
svg = svg.split('&' + entityName + ';').join(entityValue); |
|
} |
|
|
|
if (normalizeWhitespace) { |
|
// remove whitespace between tags to normalize the DOM between IE |
|
// and other browsers |
|
svg = svg.replace(/\>\s+\</gm, '><'); |
|
} |
|
|
|
// transform text nodes into 'fake' elements so that we can track them |
|
// with a GUID |
|
if (this.renderer == FlashHandler) { |
|
// strip out <!-- --> style comments; these cause a variety of problems: |
|
// 1) We don't parse comments into our DOM, 2) when we add our |
|
// <__text> sections below they can get incorrectly nested into multi |
|
// line comments; stripping them out is a simple solution for now. |
|
|
|
// this is preferable and more readable but most browsers and JavaScript |
|
// do not support a Single Line Mode (i.e. .* matches everything |
|
// _including_ new lines) |
|
//svg = svg.replace(/<!\-\-.*?\-\->/gm, ''); |
|
svg = svg.replace(/<!\-\-[\s\S]*?\-\->/g, ''); |
|
|
|
// We might have nested <svg> elements; we want to make sure we don't |
|
// incorrectly think these are SVG root elements. To do this, temporarily |
|
// rename the SVG root element, then rename nested <svg> root elements |
|
// to a temporary token that we will restore at the end of this method |
|
svg = svg.replace(/<svg/, '<SVGROOT'); // root <svg> element |
|
svg = svg.replace(/<svg/g, '<NESTEDSVG'); // nested <svg> |
|
svg = svg.replace(/<SVGROOT/, '<svg'); |
|
|
|
// break SVG string into pieces so that we don't incorrectly add our |
|
// <__text> fake text nodes outside the SVG root tag |
|
var separator = svg.match(/<svg/)[0]; |
|
var pieces = svg.split(/<svg/); |
|
|
|
// extract CDATA sections temporarily so that we don't end up |
|
// adding double <__text> blocks |
|
var hasCData = (pieces[1].indexOf('<![CDATA[') != -1); |
|
if (hasCData) { |
|
RegExp.lastIndex = 0; // reset global exec() |
|
var cdataRE = /<\!\[CDATA\[/g; |
|
match = cdataRE.exec(pieces[1]); |
|
var cdataBlocks = []; |
|
i = 0; |
|
while (match && RegExp.rightContext) { |
|
var startIdx = cdataRE.lastIndex - '<![CDATA['.length; |
|
var context = RegExp.rightContext; |
|
var endIdx = cdataRE.lastIndex + context.indexOf(']]>') + 2; |
|
var section = context.substring(0, context.indexOf(']]>')); |
|
|
|
// save this CDATA section for later |
|
section = '<![CDATA[' + section + ']]>'; |
|
cdataBlocks.push(section); |
|
|
|
// change the CDATA section into a token that we will replace later |
|
var before = pieces[1].substring(0, startIdx); |
|
var after = pieces[1].substring(endIdx + 1, pieces[1].length); |
|
pieces[1] = before + '__SVG_CDATA_TOKEN_' + i + after; |
|
|
|
// find next match |
|
match = cdataRE.exec(pieces[1]); |
|
i++; |
|
} |
|
} |
|
|
|
// capture anything between > and < tags |
|
pieces[1] = pieces[1].replace(/>([^>]+)</g, '><__text>$1</__text><'); |
|
|
|
// re-assemble our CDATA blocks |
|
if (hasCData) { |
|
for (var i = 0; i < cdataBlocks.length; i++) { |
|
pieces[1] = pieces[1].replace('__SVG_CDATA_TOKEN_' + i, cdataBlocks[i]); |
|
} |
|
} |
|
|
|
// paste the pieces back together |
|
svg = pieces[0] + separator + pieces[1]; |
|
for (var i = 2; i < pieces.length; i++) { |
|
svg = svg + pieces[i]; |
|
} |
|
} |
|
|
|
// earlier we turned nested <svg> elements into a temporary token; restore |
|
// them |
|
svg = svg.replace(/<NESTEDSVG/g, '<svg'); |
|
|
|
if (this.renderer == FlashHandler) { |
|
// handle Flash encoding issues |
|
svg = FlashHandler._encodeFlashData(svg); |
|
|
|
// Firefox and Safari will parse any nodes in the SVG namespace into 'real' |
|
// SVG nodes when using the Flash handler, which we don't want |
|
// (they take up more memory, and can mess up our return results in some |
|
// cases). To get around this, we change the SVG namespace in our XML into |
|
// a temporary different one to prevent this from happening. |
|
svg = svg.replace(/xmlns(\:[^=]*)?=['"]http\:\/\/www\.w3\.org\/2000\/svg['"]/g, |
|
"xmlns$1='" + svgnsFake + "'"); |
|
} |
|
|
|
// add guids and IDs to all elements and get the root SVG elements ID; |
|
// this also has the side effect of also parsing our SVG into an actual |
|
// XML tree we can use later; we do it here so that we don't have to |
|
// parse the XML twice for performance reasons |
|
var xml = this._addTracking(svg, normalizeWhitespace); |
|
if (typeof XMLSerializer != 'undefined') { // non-IE browsers |
|
svg = (new XMLSerializer()).serializeToString(xml); |
|
} else { // IE |
|
svg = xml.xml; |
|
} |
|
|
|
// remove the fake SVG namespace we added as a workaround right above now |
|
// that we are parsed |
|
if (this.renderer == FlashHandler) { |
|
svg = svg.replace(new RegExp(svgnsFake, 'g'), svgns); |
|
} |
|
|
|
return {svg: svg, xml: xml}; |
|
}, |
|
|
|
/** Extracts SVG from the script, cleans it up, adds GUIDs to all elements, |
|
and then creates the correct Flash or Native handler to do |
|
the hard work. |
|
|
|
@param script SCRIPT node to get the SVG from. */ |
|
_processSVGScript: function(script) { |
|
//console.log('processSVGScript, script='+script); |
|
var origSVG; |
|
if (!isXHTML) { |
|
origSVG = script.innerHTML; |
|
} else { // XHTML document |
|
// Safari/Native has an unusual bug; sometimes, when fetching the |
|
// SVG text content inside of a SCRIPT tag, it will break it up into |
|
// multiple CDATA sections, corrupting the source if we fetch it |
|
// with innerHTML above. Instead loop through and get the CDATA text. |
|
origSVG = ''; |
|
for (var i = 0; i < script.childNodes.length; i++) { |
|
origSVG += script.childNodes[i].textContent; |
|
} |
|
} |
|
|
|
var results = this._cleanSVG(origSVG, true, true); |
|
var svg = results.svg; |
|
var xml = results.xml; |
|
var rootID = xml.documentElement.getAttribute('id'); |
|
var rootOnload = xml.documentElement.getAttribute('onload'); |
|
if (rootOnload) { |
|
// turn the textual onload handler into a real function |
|
var defineEvtCode = |
|
'var evt = { target: document.getElementById("' + rootID + '") ,' + |
|
'currentTarget: document.getElementById("' + rootID + '") ,' + |
|
'preventDefault: function() { this.returnValue=false; }' + |
|
'};'; |
|
rootOnload = new Function(defineEvtCode + rootOnload); |
|
|
|
// return a function that makes the 'this' keyword apply to |
|
// the SVG root; wrap in another anonymous closure as well to prevent |
|
// IE memory leaks |
|
var f = (function(rootOnload, rootID) { |
|
return function() { |
|
// get our SVG root so we can set the 'this' reference correctly |
|
var handler = svgweb.handlers[rootID]; |
|
var root; |
|
if (svgweb.getHandlerType() == 'flash') { |
|
root = handler.document.documentElement._getProxyNode(); |
|
} else { // native |
|
// Safari/Native has a bizarre bug; earlier we save a reference |
|
// to our handler._svgRoot variable, but it won't match what |
|
// is returned by document.getElementById(rootID). We use that |
|
// instead to have correct object identity. |
|
root = document.getElementById(rootID); |
|
} |
|
|
|
return rootOnload.apply(root); |
|
}; |
|
})(rootOnload, rootID); |
|
|
|
// add to our list of page-level onload listeners |
|
this._loadListeners.push(f); |
|
} |
|
|
|
// create the correct handler |
|
var handler = new this.renderer({type: 'script', |
|
svgID: rootID, |
|
xml: xml, |
|
svgString: svg, |
|
origSVG: origSVG, |
|
scriptNode: script}); |
|
|
|
// NOTE: FIXME: If someone chooses a rootID that starts with a number |
|
// this will break |
|
this.handlers[rootID] = handler; |
|
this.handlers.push(handler); |
|
|
|
// have the handler do its thing |
|
handler.start(); |
|
}, |
|
|
|
/** Extracts or autogenerates an ID for the object and then creates the |
|
correct Flash or Native handler to do the hard work. |
|
|
|
@returns The ID for the object, either what was specified or what was |
|
autogenerated. */ |
|
_processSVGObject: function(obj) { |
|
//console.log('processSVGObject'); |
|
var objID = obj.getAttribute('id'); |
|
// extract an ID from the OBJECT tag; generate a random ID if needed |
|
if (!objID) { |
|
obj.setAttribute('id', svgweb._generateID('__svg__random__', '__object')); |
|
objID = obj.getAttribute('id'); |
|
} |
|
|
|
// create the correct handler |
|
var handler = new this.renderer({type: 'object', |
|
objID: objID, |
|
objNode: obj}); |
|
|
|
// NOTE: FIXME: If someone chooses an objID that starts with a number |
|
// this will break |
|
this.handlers[objID] = handler; |
|
this.handlers.push(handler); |
|
|
|
// have the handler do its thing |
|
handler.start(); |
|
|
|
return objID; |
|
}, |
|
|
|
/** Generates a random SVG ID. It is recommended that you use the prefix |
|
and postfix. |
|
|
|
@param prefix An optional string to add to the beginning of the ID. |
|
@param postfix An optional string to add to the end of the ID. */ |
|
_generateID: function(prefix, postfix) { |
|
// generate an ID for this element |
|
if (!postfix) { |
|
postfix = ''; |
|
} |
|
|
|
if (!prefix) { |
|
prefix = ''; |
|
} |
|
|
|
return prefix + guid() + postfix; |
|
}, |
|
|
|
/** Walks the SVG DOM, adding automatic generated GUIDs to all elements. |
|
Generates an ID for the SVG root if one is not present. |
|
|
|
@param normalizeWhitespace If true, then when parsing we ignore |
|
whitespace, acting more like normal HTML; if false, then we keep |
|
whitespace as independent nodes more like XML. |
|
@returns Parsed DOM XML Document of the SVG with all elements having |
|
an ID and a GUID. */ |
|
_addTracking: function(svg, normalizeWhitespace) { |
|
var parseWhitespace = !normalizeWhitespace; |
|
|
|
// parse the SVG |
|
var xmlDoc = parseXML(svg, parseWhitespace); |
|
var root = xmlDoc.documentElement; |
|
|
|
// make sure the root has an ID |
|
if (root && !root.getAttribute('id')) { |
|
root.setAttribute('id', this._generateID('__svg__random__', null)); |
|
} |
|
|
|
if (this.getHandlerType() != 'flash') { |
|
return xmlDoc; |
|
} |
|
|
|
// now walk the parsed DOM; we do this iteratively rather than |
|
// recursively as this was found to be a performance bottleneck and |
|
// 'unrolling' the recursive algorithm sped things up. |
|
var current = root; |
|
while (current) { |
|
if (current.nodeType == _Node.ELEMENT_NODE) { |
|
current.setAttribute('__guid', guid()); |
|
} |
|
|
|
if (current.nodeType == _Node.ELEMENT_NODE |
|
&& !current.getAttribute('id')) { |
|
// generate a random ID, since the Flash backend needs IDs for certain |
|
// scenarios (such as tracking dependencies around redraws for USE |
|
// nodes, for example) |
|
// FIXME: TODO: can we eliminate having to generate these for all nodes |
|
// by changing the Flash backend to use GUIDs for these scenarios? |
|
current.setAttribute('id', svgweb._generateID('__svg__random__', null)); |
|
} |
|
|
|
var next = current.firstChild; |
|
if (next) { |
|
current = next; |
|
continue; |
|
} |
|
|
|
while (current) { |
|
if (current != root) { |
|
next = current.nextSibling; |
|
if (next) { |
|
current = next; |
|
break; |
|
} |
|
} |
|
if (current == root) { |
|
current = null; |
|
} else { |
|
current = current.parentNode; |
|
if (current.nodeType != 1) { |
|
current = null; |
|
} |
|
} |
|
} |
|
} |
|
|
|
return xmlDoc; |
|
}, |
|
|
|
/** Called when an SVG SCRIPT or OBJECT is done loading. If we are finished |
|
loading every SVG item then we fire window onload and also indicate to |
|
each handler that the page is finished loading so that handlers can |
|
take further action, such as executing any SVG scripts that might be |
|
inside of an SVG file loaded in an SVG OBJECT. |
|
|
|
@param ID of either the SVG root element inside of an SVG SCRIPT or |
|
the SVG OBJECT that has finished loading. |
|
@param type Either 'script' for an SVG SCRIPT or 'object' for an |
|
SVG OBJECT. |
|
@param handler The Flash or Native Handler that is done loading. |
|
*/ |
|
_handleDone: function(id, type, handler) { |
|
//console.log('svgweb._handleDone, id='+id+', type='+type); |
|
this.totalLoaded++; |
|
|
|
// fire any onload listeners that were registered with a dynamically |
|
// created SVG root |
|
if (type == 'script' && handler._scriptNode._onloadListeners) { |
|
for (var i = 0; i < handler._scriptNode._onloadListeners.length; i++) { |
|
var f = handler._scriptNode._onloadListeners[i]; |
|
if (svgweb.getHandlerType() == 'flash') { |
|
f = f.listener; |
|
} else { |
|
// Firefox has a frustrating bug around addEventListener (see |
|
// NativeHandler._patchAddEventListener for details). Repatch |
|
// addEventListener on our SVG Root element if our changes have |
|
// been lost. |
|
var methodStr = handler._svgRoot.addEventListener.toString(); |
|
if (methodStr.indexOf('[native code]') != -1) { |
|
NativeHandler._patchAddEventListener(handler._svgRoot); |
|
} |
|
} |
|
|
|
try { |
|
// every root has an ID, whether autogenerated or not; |
|
// fetch the root so that our 'this' context correctly |
|
// points to the root node inside of our onload function |
|
var root = document.getElementById(handler.id); |
|
if (isOpera) { |
|
// Opera 10.53 does not like this thread, probably because |
|
// it originated from flash. Strange problems occur, like the |
|
// thread just stops in various places. setTimeout seems to |
|
// set up a better thread. This is the same workaround as |
|
// in _SVGObject._executeScript(). |
|
setTimeout(function() { f.apply(root);f=null;root=null; }, 1); |
|
} else { |
|
f.apply(root); |
|
} |
|
} catch (exp) { |
|
console.log('Error while firing onload listener: ' |
|
+ exp.message || exp); |
|
} |
|
} |
|
handler._scriptNode._onloadListeners = []; |
|
} |
|
|
|
// page-level onload listeners or dynamically created SVG OBJECTs |
|
if (this.totalLoaded >= this.totalSVG) { |
|
// we are finished |
|
this._fireOnLoad(); |
|
} |
|
}, |
|
|
|
/** Safari 3 has a strange bug where if you have no HTML TITLE element, |
|
it will interpret the first SVG TITLE as the HTML TITLE and change |
|
the browser's title at the top of the title bar; this only happens |
|
with the native handler, but for consistency we insert an empty |
|
HTML TITLE into the page if none is present for all handlers |
|
which solves the issue. */ |
|
_handleHTMLTitleBug: function() { |
|
var head = document.getElementsByTagName('head')[0]; |
|
var title = head.getElementsByTagName('title'); |
|
if (title.length === 0) { |
|
title = document.createElement('title'); |
|
head.appendChild(title); |
|
} |
|
}, |
|
|
|
/** This method is a hook useful for unit testing; unit testing can |
|
override it to be informed if an error occurs inside the Flash |
|
so that we can stop the unit test and report the error. */ |
|
_fireFlashError: function(logString) { |
|
}, |
|
|
|
/** Add an .id attribute for non-SVG and non-HTML nodes for the Native |
|
Handler, which don't have them by default in order to have parity with the |
|
Flash viewer. We have this here instead of on the Native Handlers |
|
themselves because the method is called by our patched |
|
document.getElementByTagNameNS and we don't want to do any closure |
|
magic there to prevent memory leaks. */ |
|
_exportID: function(node) { |
|
node.__defineGetter__('id', function() { |
|
return node.getAttribute('id'); |
|
}); |
|
node.__defineSetter__('id', function(newValue) { |
|
return node.setAttribute('id', newValue); |
|
}); |
|
}, |
|
|
|
/** Sign up for the onunload event on IE to clean up references that |
|
can cause memory leaks. */ |
|
_watchUnload: function() { |
|
window.attachEvent('onunload', function(evt) { |
|
// detach this anonymous listener |
|
window.detachEvent('onunload', arguments.callee); |
|
|
|
// do all the onunload work now |
|
svgweb._fireUnload(); |
|
}); |
|
}, |
|
|
|
/** Called when window unload event fires; putting this in a separate |
|
function helps us with unit testing. */ |
|
_fireUnload: function() { |
|
if (!isIE) { // helps with unit testing |
|
return; |
|
} |
|
|
|
// clean up SVG OBJECTs |
|
for (var i = 0; i < svgweb.handlers.length; i++) { |
|
if (svgweb.handlers[i].type == 'object') { |
|
var removeMe = svgweb.handlers[i].flash; |
|
if (removeMe.parentNode) { // attachment may have been interrupted |
|
svgweb.removeChild(removeMe, removeMe.parentNode); |
|
} |
|
} else { |
|
// null out reference to root |
|
svgweb.handlers[i].document.documentElement = null; |
|
} |
|
} |
|
|
|
// delete the HTC container and all HTC nodes |
|
var container = document.getElementById('__htc_container'); |
|
if (container) { |
|
for (var i = 0; i < container.childNodes.length; i++) { |
|
var child = container.childNodes[i]; |
|
// remove style handling |
|
if (child.nodeType == 1 && child.namespaceURI == svgns) { |
|
child.detachEvent('onpropertychange', |
|
child._fakeNode.style._changeListener); |
|
child.style.item = null; |
|
child.style.setProperty = null; |
|
child.style.getPropertyValue = null; |
|
} |
|
|
|
// remove other references |
|
if (child._fakeNode) { |
|
child._fakeNode._htcNode = null; |
|
} |
|
child._fakeNode = null; |
|
child._handler = null; |
|
} |
|
container.parentNode.removeChild(container); |
|
container = null; |
|
} |
|
|
|
// for all the handlers, remove their reference to the Flash object |
|
for (var i = 0; i < svgweb.handlers.length; i++) { |
|
var handler = svgweb.handlers[i]; |
|
handler.flash = null; |
|
} |
|
svgweb.handlers = null; |
|
|
|
// clean up any nodes that were removed in the past |
|
for (var i = 0; i < svgweb._removedNodes.length; i++) { |
|
var node = svgweb._removedNodes[i]; |
|
if (node._fakeNode) { |
|
node._fakeNode._htcNode = null; |
|
} |
|
node._fakeNode = null; |
|
node._handler = null; |
|
} |
|
svgweb._removedNodes = null; |
|
|
|
// cleanup document patching |
|
document.getElementById = document._getElementById; |
|
document._getElementById = null; |
|
|
|
document.getElementsByTagNameNS = document._getElementsByTagNameNS; |
|
document._getElementsByTagNameNS = null; |
|
|
|
document.createElementNS = document._createElementNS; |
|
document._createElementNS = null; |
|
|
|
document.createElement = document._createElement; |
|
document._createElement = null; |
|
|
|
document.createTextNode = document._createTextNode; |
|
document._createTextNode = null; |
|
|
|
document._importNodeFunc = null; |
|
|
|
document.createDocumentFragment = document._createDocumentFragment; |
|
document._createDocumentFragment = null; |
|
|
|
window.addEventListener = null; |
|
window._addEventListener = null; |
|
|
|
window.attachEvent = window._attachEvent; |
|
window._attachEvent = null; |
|
|
|
// cleanup parsed XML cache |
|
parseXMLCache = null; |
|
}, |
|
|
|
/** Does certain things early on in the page load process to cleanup |
|
any SVG objects on our page, such as making them hidden, etc. */ |
|
_cleanupSVGObjects: function() { |
|
// if this browser has native SVG support, do some tricks to take away |
|
// control from it for the Flash renderer early in the process |
|
if (this.config.use == 'flash' && this.config.hasNativeSVG()) { |
|
for (var i = 0; i < this._svgObjects.length; i++) { |
|
// replace the SVG OBJECT with a DIV |
|
var obj = this._svgObjects[i]; |
|
var div = document.createElement('div'); |
|
for (var j = 0; j < obj.attributes.length; j++) { |
|
var attr = obj.attributes[j]; |
|
div.setAttribute(attr.nodeName, attr.nodeValue); |
|
} |
|
// bring over fallback content |
|
var fallback = obj.innerHTML; |
|
div.innerHTML = fallback; |
|
obj.parentNode.replaceChild(div, obj); |
|
this._svgObjects[i] = div; |
|
} |
|
} |
|
|
|
// make any SVG objects have visibility hidden early in the process |
|
// to prevent IE from showing scroll bars |
|
for (var i = 0; i < this._svgObjects.length; i++) { |
|
this._svgObjects[i].style.visibility = 'hidden'; |
|
} |
|
}, |
|
|
|
/** Intercepts setting up window.onload events so we can delay firing |
|
them until we are done with our internal work. */ |
|
_interceptOnloadListeners: function() { |
|
if (window.addEventListener) { |
|
window._addEventListener = window.addEventListener; |
|
window.addEventListener = function(type, f, useCapture) { |
|
if (type.toLowerCase() != 'svgload') { |
|
return window._addEventListener(type, f, useCapture); |
|
} else { |
|
svgweb.addOnLoad(f); |
|
} |
|
} |
|
} else { |
|
// patch in addEventListener just for the svgload event |
|
window.addEventListener = function(type, f, useCapture) { |
|
if (type.toLowerCase() == 'svgload') { |
|
svgweb.addOnLoad(f); |
|
} else { |
|
if (isIE && window.attachEvent) { |
|
return window.attachEvent('on' + type, f); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (isIE && window.attachEvent) { |
|
window._attachEvent = window.attachEvent; |
|
window.attachEvent = function(type, f) { |
|
if (type.toLowerCase() != 'onsvgload') { |
|
return window._attachEvent(type, f); |
|
} else { |
|
svgweb.addOnLoad(f); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
_saveWindowOnload: function() { |
|
// intercept and save window.onsvgload or <body onsvgload=""> |
|
var onsvgload = window.onsvgload; |
|
// browsers will replace any previous window.onload listeners |
|
// with a <body onload=""> listener; simulate this for onsvgload |
|
if (document.getElementsByTagName('body')) { |
|
var body = document.getElementsByTagName('body')[0]; |
|
if (body.getAttribute('onsvgload')) { |
|
callbackStr = body.getAttribute('onsvgload'); |
|
onsvgload = (function(callbackStr) { |
|
// FIXME: What should 'this' refer to when body.onload |
|
// is simulated? The body tag? The window object? |
|
return function() { |
|
eval(callbackStr); |
|
} |
|
})(callbackStr); |
|
} |
|
} |
|
|
|
if (onsvgload) { |
|
// preserve IE's different behavior of firing window.onload |
|
// behavior _before_ everything else; other browsers don't necessarily |
|
// give preferential treatment to window.onload. Even though we |
|
// now use window.onsvgload instead of window.onload preserve |
|
// this behavior. |
|
if (isIE) { |
|
this._loadListeners.splice(0, 0, onsvgload); |
|
} else { |
|
this._loadListeners.push(onsvgload); |
|
} |
|
window.onsvgload = onsvgload = null; |
|
} |
|
} |
|
}); |
|
|
|
|
|
/** A class that sees if there is a META tag to force Flash rendering |
|
for all browsers. Also determines if the browser supports native SVG or |
|
Flash and the correct Flash version. Determines the best renderer |
|
to use. */ |
|
function RenderConfig() { |
|
// see if there is a META tag for 'svg.render.forceflash' or a query |
|
// value in the URL |
|
if (!this._forceFlash()) { |
|
// if not, see if this browser natively supports SVG |
|
if (this.hasNativeSVG()) { |
|
this.supported = true; |
|
this.use = 'native'; |
|
return; |
|
} |
|
} else { |
|
console.log('Forcing Flash SVG viewer for this browser'); |
|
} |
|
|
|
// if not, see if this browser has Flash and the correct Flash version (9+) |
|
var info = new FlashInfo(); |
|
if (info.capable) { |
|
if (info.isVersionOrAbove(9, 0, 0)) { |
|
this.supported = true; |
|
this.use = 'flash'; |
|
} else { // has Flash but wrong version |
|
this.supported = false; |
|
this.reason = 'Flash 9+ required'; |
|
} |
|
} else { // no Flash present |
|
this.supported = false; |
|
this.reason = 'Flash 9+ or a different browser required'; |
|
} |
|
} |
|
|
|
extend(RenderConfig, { |
|
/** Boolean on whether the given browser is supported. */ |
|
supported: false, |
|
|
|
/* String on why the given browser is not supported. */ |
|
reason: null, |
|
|
|
/** String on which renderer to use: flash or native. */ |
|
use: null, |
|
|
|
/** Determines if there is the META tag 'svg.render.forceflash' set to |
|
true or a URL query value with 'svg.render.forceflash' given. */ |
|
_forceFlash: function() { |
|
var results = false; |
|
var hasMeta = false; |
|
|
|
var meta = document.getElementsByTagName('meta'); |
|
for (var i = 0; i < meta.length; i++) { |
|
if (meta[i].name == 'svg.render.forceflash' && |
|
meta[i].content.toLowerCase() == 'true') { |
|
results = true; |
|
hasMeta = true; |
|
} |
|
} |
|
|
|
if (window.location.search.indexOf('svg.render.forceflash=true') != -1) { |
|
results = true; |
|
} else if (hasMeta |
|
&& window.location.search.indexOf( |
|
'svg.render.forceflash=false') != -1) { |
|
// URL takes precedence |
|
results = false; |
|
} |
|
|
|
return results; |
|
}, |
|
|
|
/** Determines whether this browser supports native SVG. */ |
|
hasNativeSVG: function() { |
|
if (document.implementation && document.implementation.hasFeature) { |
|
return document.implementation.hasFeature( |
|
'http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1'); |
|
} else { |
|
return false; |
|
} |
|
} |
|
}); |
|
|
|
|
|
// adapted from Dojo Flash dojox.flash.Info |
|
function FlashInfo(){ |
|
// summary: A class that helps us determine whether Flash is available. |
|
// description: |
|
// A class that helps us determine whether Flash is available, |
|
// it's major and minor versions, and what Flash version features should |
|
// be used for Flash/JavaScript communication. Parts of this code |
|
// are adapted from the automatic Flash plugin detection code autogenerated |
|
// by the Macromedia Flash 8 authoring environment. |
|
|
|
this._detectVersion(); |
|
} |
|
|
|
FlashInfo.prototype = { |
|
// version: String |
|
// The full version string, such as "8r22". |
|
version: -1, |
|
|
|
// versionMajor, versionMinor, versionRevision: String |
|
// The major, minor, and revisions of the plugin. For example, if the |
|
// plugin is 8r22, then the major version is 8, the minor version is 0, |
|
// and the revision is 22. |
|
versionMajor: -1, |
|
versionMinor: -1, |
|
versionRevision: -1, |
|
|
|
// capable: Boolean |
|
// Whether this platform has Flash already installed. |
|
capable: false, |
|
|
|
isVersionOrAbove: function( |
|
/* int */ reqMajorVer, |
|
/* int */ reqMinorVer, |
|
/* int */ reqVer){ /* Boolean */ |
|
// summary: |
|
// Asserts that this environment has the given major, minor, and revision |
|
// numbers for the Flash player. |
|
// description: |
|
// Asserts that this environment has the given major, minor, and revision |
|
// numbers for the Flash player. |
|
// |
|
// Example- To test for Flash Player 7r14: |
|
// |
|
// info.isVersionOrAbove(7, 0, 14) |
|
// returns: |
|
// Returns true if the player is equal |
|
// or above the given version, false otherwise. |
|
|
|
// make the revision a decimal (i.e. transform revision 14 into |
|
// 0.14 |
|
reqVer = parseFloat("." + reqVer); |
|
|
|
if (this.versionMajor >= reqMajorVer && this.versionMinor >= reqMinorVer |
|
&& this.versionRevision >= reqVer) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}, |
|
|
|
_detectVersion: function(){ |
|
var versionStr; |
|
|
|
// loop backwards through the versions until we find the newest version |
|
for (var testVersion = 25; testVersion > 0; testVersion--) { |
|
if (isIE) { |
|
var axo; |
|
try { |
|
if (testVersion > 6) { |
|
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash." |
|
+ testVersion); |
|
} else { |
|
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); |
|
} |
|
if (typeof axo == "object") { |
|
if (testVersion == 6) { |
|
axo.AllowScriptAccess = "always"; |
|
} |
|
versionStr = axo.GetVariable("$version"); |
|
} |
|
} catch(e) { |
|
continue; |
|
} |
|
} else { |
|
versionStr = this._JSFlashInfo(testVersion); |
|
} |
|
|
|
if (versionStr == -1 ) { |
|
this.capable = false; |
|
return; |
|
} else if (versionStr !== 0) { |
|
var versionArray; |
|
if (isIE) { |
|
var tempArray = versionStr.split(" "); |
|
var tempString = tempArray[1]; |
|
versionArray = tempString.split(","); |
|
} else { |
|
versionArray = versionStr.split("."); |
|
} |
|
|
|
this.versionMajor = versionArray[0]; |
|
this.versionMinor = versionArray[1]; |
|
this.versionRevision = versionArray[2]; |
|
|
|
// 7.0r24 == 7.24 |
|
var versionString = this.versionMajor + "." + this.versionRevision; |
|
this.version = parseFloat(versionString); |
|
|
|
this.capable = true; |
|
|
|
break; |
|
} |
|
} |
|
}, |
|
|
|
// JavaScript helper required to detect Flash Player PlugIn version |
|
// information. |
|
_JSFlashInfo: function(testVersion){ |
|
// NS/Opera version >= 3 check for Flash plugin in plugin array |
|
if (navigator.plugins !== null && navigator.plugins.length > 0) { |
|
if (navigator.plugins["Shockwave Flash 2.0"] || |
|
navigator.plugins["Shockwave Flash"]) { |
|
var swVer2 = navigator.plugins["Shockwave Flash 2.0"] ? " 2.0" : ""; |
|
var flashDescription = navigator.plugins["Shockwave Flash" + swVer2].description; |
|
var descArray = flashDescription.split(" "); |
|
var tempArrayMajor = descArray[2].split("."); |
|
var versionMajor = tempArrayMajor[0]; |
|
var versionMinor = tempArrayMajor[1]; |
|
var tempArrayMinor = (descArray[3] || descArray[4]).split("r"); |
|
var versionRevision = tempArrayMinor[1] > 0 ? tempArrayMinor[1] : 0; |
|
var version = versionMajor + "." + versionMinor + "." + versionRevision; |
|
|
|
return version; |
|
} |
|
} |
|
|
|
return -1; |
|
} |
|
}; |
|
|
|
|
|
/** Creates a FlashHandler that will embed the given SVG into the page using |
|
Flash. Pass in an object literal with the correct arguments. Once the |
|
handler is setup call start() to have it kick off doing its work. |
|
|
|
If dealing with an SVG SCRIPT tag these arguments are: |
|
|
|
type - The string 'script'. |
|
svgID - A unique ID for the SVG root tag. |
|
xml - XML Document object for parsed SVG. |
|
svgString - The SVG content as a String. |
|
origSVG - The original, pre-cleaned up SVG. Useful so that we can |
|
provide the original SVG for 'View Source' functionality. Only used |
|
by the FlashHandler. |
|
scriptNode - The DOM element for the SVG SCRIPT block. |
|
|
|
If dealing with an SVG OBJECT tag these arguments are: |
|
|
|
type - The string 'object'. |
|
objID - A unique ID for the SVG OBJECT tag. |
|
objNode - DOM OBJECT pointing to an SVG URL to handle. |
|
*/ |
|
function FlashHandler(args) { |
|
this.type = args.type; |
|
|
|
// we keep a record of all keyboard listeners added by any of our nodes; |
|
// this is necessary so that if the containing SVG document is removed from |
|
// the DOM we can clean up keyboard listeners, which are actually registered |
|
// on the document object |
|
this._keyboardListeners = []; |
|
|
|
// helps us with suspendRedraw operations |
|
this._redrawManager = new _RedrawManager(this); |
|
|
|
if (this.type == 'script') { |
|
this.id = args.svgID; |
|
this._xml = args.xml; |
|
this._svgString = args.svgString; |
|
this._origSVG = args.origSVG; |
|
this._scriptNode = args.scriptNode; |
|
} else if (this.type == 'object') { |
|
this.id = args.objID; |
|
this._objNode = args.objNode; |
|
} |
|
} |
|
|
|
// start of 'static' singleton functions and properties |
|
|
|
// when someone calls createElementNS or createTextNode we are not attached |
|
// to a handler yet; we need an XML document object in order to generate things |
|
// though, so this single unattached XML document object serves that purpose |
|
FlashHandler._unattachedDoc = parseXML('<?xml version="1.0"?>\n' |
|
+ '<svg xmlns="' + svgns + '"></svg>', |
|
false); |
|
|
|
/** Prepares the svg.htc behavior for IE. */ |
|
FlashHandler._prepareBehavior = function(libraryPath, htcFilename) { |
|
// Adapted from Mark Finkle's SVG using VML project |
|
|
|
// add the SVG namespace to the page in a way IE can use |
|
var ns = null; |
|
for (var i = 0; i < document.namespaces.length; i++) { |
|
if (document.namespaces.item(i).name == 'svg') { |
|
ns = document.namespaces.item(i); |
|
break; |
|
} |
|
} |
|
|
|
if (ns === null) { |
|
ns = document.namespaces.add('svg', svgns); |
|
} |
|
|
|
// attach SVG behavior to the page |
|
ns.doImport(libraryPath + htcFilename); |
|
}; |
|
|
|
/** Fetches an _Element or _Node or creates a new one on demand using the |
|
given parsed XML and handler. |
|
|
|
@param nodeXML XML or HTML DOM node for the element to use when |
|
constructing the _Element or _Node. |
|
@param handler Optional. A FlashHandler to associate with this node if |
|
the node is attached to a real DOM. |
|
|
|
@returns If IE, returns the HTC proxy for the node (i.e. node._htcNode) so |
|
that external callers can manipulate it and have getter/setter magic happen; |
|
if other browsers, returns the _Node or _Element itself. */ |
|
FlashHandler._getNode = function(nodeXML, handler) { |
|
//console.log('getNode, nodeXML='+nodeXML+', nodeName='+nodeXML.nodeName+', handler='+handler); |
|
var node; |
|
|
|
// if we've created an _Element or _Node for this XML before, we |
|
// stored a reference to it by GUID so we could get it later |
|
node = svgweb._guidLookup['_' + nodeXML.getAttribute('__guid')]; |
|
|
|
// NOTE: We represent text nodes using an XML Element node in order to do |
|
// tracking, so we have to catch this fact below |
|
var fakeTextNode = false; |
|
if (!node && nodeXML.nodeName == '__text') { |
|
fakeTextNode = true; |
|
} |
|
|
|
if (!node && !fakeTextNode && nodeXML.nodeType == _Node.ELEMENT_NODE) { |
|
// never seen before -- we'll have to create a new _Element now |
|
node = new _Element(nodeXML.nodeName, nodeXML.prefix, |
|
nodeXML.namespaceURI, nodeXML, handler, true); |
|
} else if (!node && (nodeXML.nodeType == _Node.TEXT_NODE || fakeTextNode)) { |
|
node = new _Node('#text', _Node.TEXT_NODE, null, null, nodeXML, |
|
handler, false); |
|
} else if (!node) { |
|
throw new Error('Unknown node type given to _getNode: ' |
|
+ nodeXML.nodeType); |
|
} |
|
|
|
return node._getProxyNode(); |
|
}; |
|
|
|
/** Patches the document object to also use the Flash backend. */ |
|
FlashHandler._patchBrowserObjects = function(win, doc) { |
|
if (doc._getElementById) { // already patched |
|
return; |
|
} |
|
|
|
// We don't capture the original document functions as a closure, |
|
// as Firefox doesn't like this and will fail to run the original. |
|
// Instead, we capture the original versions on the document object |
|
// itself but with a _ prefix. |
|
|
|
document._getElementById = document.getElementById; |
|
document.getElementById = FlashHandler._getElementById; |
|
|
|
document._getElementsByTagNameNS = document.getElementsByTagNameNS; |
|
document.getElementsByTagNameNS = FlashHandler._getElementsByTagNameNS; |
|
|
|
document._createElementNS = document.createElementNS; |
|
document.createElementNS = FlashHandler._createElementNS; |
|
|
|
document._createElement = document.createElement; |
|
document.createElement = FlashHandler._createElement; |
|
|
|
document._createTextNode = document.createTextNode; |
|
document.createTextNode = FlashHandler._createTextNode; |
|
|
|
document._importNodeFunc = FlashHandler._importNodeFunc; |
|
|
|
document._createDocumentFragment = document.createDocumentFragment; |
|
document.createDocumentFragment = FlashHandler._createDocumentFragment; |
|
}; |
|
|
|
/** Our implementation of getElementById, which we patch into the |
|
document object. We do it here to prevent a closure and therefore |
|
a memory leak on IE. Note that this function runs in the global |
|
scope, so 'this' will not refer to our object instance but rather |
|
the window object. */ |
|
FlashHandler._getElementById = function(id) { |
|
var result = document._getElementById(id); |
|
if (result !== null) { // Firefox doesn't like 'if (result)' |
|
return result; |
|
} |
|
|
|
// loop through each of the handlers and see if they have the element with |
|
// this ID |
|
for (var i = 0; i < svgweb.handlers.length; i++) { |
|
if (svgweb.handlers[i].type == 'script') { |
|
result = svgweb.handlers[i].document.getElementById(id); |
|
} |
|
|
|
if (result) { |
|
return result; |
|
} |
|
} |
|
|
|
return null; |
|
}; |
|
|
|
/** Our implementation of getElementsByTagNameNS, which we patch into the |
|
document object. We do it here to prevent a closure and therefore |
|
a memory leak on IE. Note that this function runs in the global |
|
scope, so 'this' will not refer to our object instance but rather |
|
the window object. */ |
|
FlashHandler._getElementsByTagNameNS = function(ns, localName) { |
|
//console.log('FlashHandler._getElementsByTagNameNS, ns='+ns+', localName='+localName); |
|
var results = createNodeList(); |
|
|
|
// NOTE: can't use Array.concat to combine our arrays below because |
|
// document._getElementsByTagNameNS results aren't a real Array, they |
|
// are DOM NodeLists |
|
|
|
if (document._getElementsByTagNameNS) { |
|
var matches = document._getElementsByTagNameNS(ns, localName); |
|
|
|
for (var j = 0; j < matches.length; j++) { |
|
results.push(matches[j]); |
|
} |
|
} |
|
|
|
for (var i = 0; i < svgweb.handlers.length; i++) { |
|
if (svgweb.handlers[i].type == 'script') { |
|
var doc = svgweb.handlers[i].document; |
|
var matches = doc.getElementsByTagNameNS(ns, localName); |
|
for (var j = 0; j < matches.length; j++) { |
|
results.push(matches[j]); |
|
} |
|
} |
|
} |
|
|
|
return results; |
|
}; |
|
|
|
/** Our implementation of createElementNS, which we patch into the |
|
document object. We do it here to prevent a closure and therefore |
|
a memory leak on IE. Note that this function runs in the global |
|
scope, so 'this' will not refer to our object instance but rather |
|
the window object. */ |
|
FlashHandler._createElementNS = function(ns, qname, forSVG) { |
|
//console.log('createElementNS, ns='+ns+', qname='+qname); |
|
if (forSVG === undefined) { |
|
forSVG = false; |
|
} |
|
if (ns === null || ns == 'http://www.w3.org/1999/xhtml') { |
|
if (isIE) { |
|
return document.createElement(qname); |
|
} else { |
|
return document._createElementNS(ns, qname); |
|
} |
|
} |
|
|
|
var namespaceFound = false; |
|
|
|
// 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 (ns == svgns) { |
|
ns = svgnsFake; |
|
namespaceFound = true; |
|
} |
|
|
|
// someone might be using this library on an XHTML page; |
|
// only use our overridden createElementNS if they are using |
|
// a namespace we have never seen before |
|
if (!isIE && !forSVG) { |
|
// Check namespaces from unattached svg elements |
|
if (svgweb._allSVGNamespaces['_' + ns]) { |
|
namespaceFound = true; |
|
} |
|
for (var i = 0; !namespaceFound && i < svgweb.handlers.length; i++) { |
|
if (svgweb.handlers[i].type == 'script' |
|
&& svgweb.handlers[i].document._namespaces['_' + ns]) { |
|
namespaceFound = true; |
|
break; |
|
} |
|
} |
|
|
|
if (!namespaceFound) { |
|
return document._createElementNS(ns, qname); |
|
} |
|
} |
|
|
|
var prefix; |
|
// Check namespaces from unattached svg elements |
|
if (svgweb._allSVGNamespaces['_' + ns]) { |
|
prefix = svgweb._allSVGNamespaces['_' + ns]; |
|
} else { |
|
// Check attached svg elements |
|
for (var i = 0; i < svgweb.handlers.length; i++) { |
|
if (svgweb.handlers[i].type == 'script') { |
|
prefix = svgweb.handlers[i].document._namespaces['_' + ns]; |
|
if (prefix) { |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
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(); |
|
}; |
|
|
|
/** Our implementation of createElement, which we patch into the |
|
document object. We do it here to prevent a closure and therefore |
|
a memory leak on IE. Note that this function runs in the global |
|
scope, so 'this' will not refer to our object instance but rather |
|
the window object. |
|
|
|
We patch createElement to have a second boolean argument that controls |
|
how we handle the nodeName, in particular for 'object'. This flags to |
|
us that this object will be used as an SVG object so that we can |
|
keep track of onload listeners added through addEventListener. |
|
|
|
@param nodeName The node name, such as 'div' or 'object'. |
|
@param forSVG Optional boolean on whether the node is an OBJECT that |
|
will be used as an SVG OBJECT. Defaults to false. */ |
|
FlashHandler._createElement = function(nodeName, forSVG) { |
|
if (!forSVG) { |
|
return document._createElement(nodeName); |
|
} else if (forSVG && nodeName.toLowerCase() == 'object') { |
|
var obj = document._createElement('object'); |
|
obj._onloadListeners = []; |
|
|
|
// capture any original addEventListener method |
|
var addEventListener = obj.addEventListener; |
|
// Do a trick to form a mini-closure here so that we don't capture |
|
// the objects above and form memory leaks on IE. We basically patch |
|
// addEventListener for just this object to build up our list of |
|
// onload listeners; for other event types we delegate to the browser's |
|
// native way to attach event listeners. |
|
(function(_obj, _addEventListener){ |
|
_obj.addEventListener = function(type, listener, useCapture) { |
|
// handle onloads special |
|
// NOTE: 'this' == our SVG OBJECT |
|
if (type.toLowerCase() == 'svgload') { |
|
this._onloadListeners.push(listener); |
|
} else if (!addEventListener) { // IE |
|
this.attachEvent('on' + type, listener); |
|
} else { // W3C |
|
_addEventListener(type, listener, useCapture); |
|
} |
|
}; |
|
})(obj, addEventListener); // pass in object and native addEventListener |
|
|
|
return obj; |
|
} |
|
}; |
|
|
|
/** Our implementation of createTextNode, which we patch into the |
|
document object. We do it here to prevent a closure and therefore |
|
a memory leak on IE. Note that this function runs in the global |
|
scope, so 'this' will not refer to our object instance but rather |
|
the window object. |
|
|
|
We patch createTextNode to have a second boolean argument that controls |
|
whether the resulting text node will be appended within our SVG tree. |
|
We need this so we can return one of our magic _Nodes instead of a native |
|
DOM node for later appending and tracking. |
|
|
|
@param data Text String. |
|
@param forSVG Optional boolean on whether node will be attached to |
|
SVG sub-tree. Defaults to false. */ |
|
FlashHandler._createTextNode = function(data, forSVG) { |
|
if (!forSVG) { |
|
return document._createTextNode(data); |
|
} else { |
|
// we create a DOM Element instead of a DOM Text Node so that we can |
|
// assign it a _guid and do tracking on it; we assign the data value |
|
// to a DOM Text Node that is a child of our fake DOM Element. Note |
|
// that since we are unattached we use an XML document object we |
|
// created earlier (FlashHandler._unattachedDoc) in order to |
|
// generate things. |
|
var doc = FlashHandler._unattachedDoc; |
|
var nodeXML; |
|
if (isIE) { |
|
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); |
|
textNode._nodeValue = data; |
|
textNode.ownerDocument = document; |
|
|
|
return textNode._getProxyNode(); |
|
} |
|
}; |
|
|
|
/** IE doesn't support the importNode function. We define it on the |
|
document object as _importNodeFunc. Unfortunately we need it there |
|
since it is a recursive function and needs to call itself, and we |
|
don't want to do this on an object instance to avoid memory leaks |
|
from closures on IE. Note that this function runs in the global scope |
|
so 'this' will point to the Window object. |
|
|
|
@param doc The document object to work with. |
|
@param node An XML node to import |
|
@param allChildren Whether to import the node's children as well. */ |
|
FlashHandler._importNodeFunc = function(doc, node, allChildren) { |
|
switch (node.nodeType) { |
|
case 1: // ELEMENT NODE |
|
var newNode = doc.createElement(node.nodeName); |
|
|
|
// does the node have any attributes to add? |
|
if (node.attributes && node.attributes.length > 0) { |
|
for (var i = 0; i < node.attributes.length; i++) { |
|
var attrName = node.attributes[i].nodeName; |
|
var attrValue = node.getAttribute(attrName); |
|
newNode.setAttribute(attrName, attrValue); |
|
} |
|
} |
|
|
|
// are we going after children too, and does the node have any? |
|
if (allChildren && node.childNodes && node.childNodes.length > 0) { |
|
for (var i = 0; i < node.childNodes.length; i++) { |
|
newNode.appendChild( |
|
document._importNodeFunc(doc, node.childNodes[i], allChildren)); |
|
} |
|
} |
|
|
|
return newNode; |
|
break; |
|
case 3: // TEXT NODE |
|
return doc.createTextNode(node.nodeValue); |
|
break; |
|
} |
|
}; |
|
|
|
/** Our implementation of createDocumentFragment, which we patch into the |
|
document object. We do it here to prevent a closure and therefore |
|
a memory leak on IE. Note that this function runs in the global |
|
scope, so 'this' will not refer to our object instance but rather |
|
the window object. |
|
|
|
@param forSVG Optional boolean value. If true, then we return a fake |
|
_DocumentFragment suitable for adding SVG nodes into. If false or not |
|
present, then this is a normal browser native DocumentFragment. */ |
|
FlashHandler._createDocumentFragment = function(forSVG) { |
|
if (forSVG) { |
|
return new _DocumentFragment(document)._getProxyNode(); |
|
} else { |
|
return document._createDocumentFragment(); |
|
} |
|
}; |
|
|
|
/** Flash has a number of encoding issues when talking over the Flash/JS |
|
boundry. This method encapsulates fixing these issues. |
|
|
|
@param str String before fixing encoding issues. |
|
|
|
@returns String suitable for sending to Flash. */ |
|
FlashHandler._encodeFlashData = function(str) { |
|
// Flash has a surprising bug: backslashing certain characters will |
|
// cause an 'Illegal Character' error. For example, if I have a SCRIPT |
|
// inside my SVG OBJECT as follows: var temp = "\"\"" then I will get |
|
// this exception. To handle this we double encode back slashes. |
|
str = str.toString().replace(/\\/g, '\\\\'); |
|
|
|
// Flash barfs on entities, such as ". To get around this, tokenize |
|
// our & characters into our own special string which we will then |
|
// replace on the Flash side inside SVGViewerWeb. |
|
str = str.replace(/&/g, '__SVG__AMPERSAND'); |
|
|
|
return str; |
|
}; |
|
|
|
// end static singleton functions |
|
|
|
// methods that every FlashHandler instance will have |
|
extend(FlashHandler, { |
|
/** The Flash object's ID; set by _SVGSVGElement. */ |
|
flashID: null, |
|
|
|
/** The Flash object; set by _SVGSVGElement. */ |
|
flash: null, |
|
|
|
/** Has this handler kick off doing its work. */ |
|
start: function() { |
|
if (this.type == 'script') { |
|
this._handleScript(); |
|
} else if (this.type == 'object') { |
|
this._handleObject(); |
|
} |
|
}, |
|
|
|
/** Turns the string results from Flash back into an Object. The HTC, unlike |
|
our Flash, returns an Object, so we detect that and simply return |
|
it unchanged if so. */ |
|
_stringToMsg: function(msg) { |
|
if (msg == null || typeof msg != 'string') { |
|
return msg; |
|
} |
|
|
|
var results = {}; |
|
|
|
// our delimiter is a custom token: __SVG__DELIMIT |
|
var tokens = msg.split(/__SVG__DELIMIT/g); |
|
for (var i = 0; i < tokens.length; i++) { |
|
// each token is a propname:propvalue pair |
|
var cutAt = tokens[i].indexOf(':'); |
|
var propName = tokens[i].substring(0, cutAt); |
|
var propValue = tokens[i].substring(cutAt + 1); |
|
if (propValue === 'true') { |
|
propValue = true; |
|
} else if (propValue === 'false') { |
|
propValue = false; |
|
} else if (propValue === 'null') { |
|
propValue = null; |
|
} else if (propValue === 'undefined') { |
|
propValue = undefined; |
|
} |
|
|
|
results[propName] = propValue; |
|
} |
|
|
|
return results; |
|
}, |
|
|
|
/** |
|
Stringifies the msg object sent back from the Flash SVG renderer or |
|
from the HTC file to help with debugging. |
|
*/ |
|
debugMsg: function(msg) { |
|
if (msg === undefined) { |
|
return 'undefined'; |
|
} else if (msg === null) { |
|
return 'null'; |
|
} |
|
|
|
var result = []; |
|
for (var i in msg) { |
|
result.push(i + ':' + msg[i]); |
|
} |
|
result = result.join(', '); |
|
|
|
return '{' + result + '}'; |
|
}, |
|
|
|
/** Sends a message to the Flash object rendering this SVG. |
|
|
|
@param invoke Flash method to invoke, such as jsSetAttribute. |
|
@param args Array of values to pass to the Flash method. */ |
|
sendToFlash: function(invoke, args) { |
|
//console.log('sendToFlash, invoke='+invoke); |
|
// Performance testing found that Flash/JS communication is one of the |
|
// primary bottlenecks. Two workarounds were found to make this faster: |
|
// 1) Send over giant strings instead of Objects and 2) minimize |
|
// our own custom marshaling code. To accomodate this, we take all |
|
// of our arguments and turn them into a giant string, delimited with |
|
// __SVG__DELIMIT. We then call individual Flash methods exposed through |
|
// ExternalInterface on the Flash side for each method (given by 'invoke'); |
|
// each of these methods knows how to deal with their arguments, so we |
|
// can minimize our marshaling code and keep it from being too generic |
|
// and slow |
|
|
|
// note that 'this.flash' is set by the FlashInserter class after we |
|
// create a Flash object there |
|
|
|
var message = args.join('__SVG__DELIMIT'); |
|
|
|
// batch things up if we are in the middle of a suspendRedraw operation |
|
if (this._redrawManager.isSuspended()) { |
|
this._redrawManager.batch(invoke, message); |
|
} else { |
|
// send things over to Flash |
|
try { |
|
return this.flash[invoke](message); |
|
} catch(exp) { |
|
// This code is for crashing exception in Opera |
|
// that occurs with scripts running in relation |
|
// to an object that is unloaded. |
|
console.log("Call to flash but flash is not present! " + |
|
invoke + ": " + this.debugMsg(message) + ": "+exp); |
|
} |
|
} |
|
}, |
|
|
|
/** The method is the primary entry-point called by Flash when it has results |
|
ready for us to use. This method then dispatches the message to other |
|
methods based on the type of the message (whether it is an event, |
|
logging, a script encountered in an SVG OBJECT, etc.). |
|
|
|
Note that this method is also called by the HTC file to tell us when |
|
events have happened, such as the HTC file being finished loading. |
|
|
|
@param msg The HTC sends us an Object populated with various values; |
|
for Flash, we send over string values instead since we found that |
|
performance is roughly twice as fast when passing strings. */ |
|
onMessage: function(msg) { |
|
msg = this._stringToMsg(msg); |
|
//console.log('onMessage, msg='+this.debugMsg(msg)); |
|
if (msg.type == 'event') { |
|
this._onEvent(msg); |
|
return; |
|
} else if (msg.type == 'log') { |
|
this._onLog(msg); |
|
return; |
|
} else if (msg.type == 'script') { |
|
this._onObjectScript(msg); |
|
return; |
|
} else if (msg.type == 'viewsource') { |
|
this._onViewSource(); |
|
return; |
|
} else if (msg.type == 'viewsourceDynamic') { |
|
this._onViewSourceDynamic(msg); |
|
return; |
|
} else if (msg.type == 'error') { |
|
this._onFlashError(msg); |
|
} |
|
}, |
|
|
|
/** Called by _SVGSVGElement or _SVGObject when we are loaded and rendered. |
|
|
|
@param id The ID of the SVG element. |
|
@param type The type of element that is finished loading, |
|
either 'script' or 'object'. */ |
|
fireOnLoad: function(id, type) { |
|
//console.log('FlashHandler.fireOnLoad'); |
|
|
|
// indicate that we are done with this handler |
|
svgweb._handleDone(id, type, this); |
|
}, |
|
|
|
/** Handles SVG embedded into the page with a SCRIPT tag. */ |
|
_handleScript: function() { |
|
// create proxy objects representing the Document and SVG root; these |
|
// kick off creating the Flash internally |
|
this.document = new _Document(this._xml, this); |
|
// Note that documentElement starts off as a fake node and transforms |
|
// to a proxy node in onRenderingFinished. |
|
this.document.documentElement = |
|
new _SVGSVGElement(this._xml.documentElement, this._svgString, |
|
this._scriptNode, this); |
|
}, |
|
|
|
/** Handles SVG embedded into the page with an OBJECT tag. */ |
|
_handleObject: function() { |
|
// transform the SVG OBJECT into a Flash one; the _SVGObject class |
|
// will handle embedding the Flash asychronously; see there for |
|
// where the code continues after the Flash is done loading |
|
this._svgObject = new _SVGObject(this._objNode, this); |
|
this._objNode = null; |
|
}, |
|
|
|
_onLog: function(msg) { |
|
console.log('FLASH: ' + msg.logString); |
|
}, |
|
|
|
_onEvent: function(msg) { |
|
//console.log('onEvent, msg='+this.debugMsg(msg)); |
|
if (msg.eventType.substr(0, 5) == 'mouse' || msg.eventType == 'click') { |
|
this._onMouseEvent(msg); |
|
return; |
|
} else if (msg.eventType == 'onRenderingFinished') { |
|
if (this.type == 'script') { |
|
this.document.documentElement._onRenderingFinished(msg); |
|
} else if (this.type == 'object') { |
|
this._svgObject._onRenderingFinished(msg); |
|
} |
|
return; |
|
} else if (msg.eventType == 'onFlashLoaded') { |
|
if (this.type == 'script') { |
|
this.document.documentElement._onFlashLoaded(msg); |
|
} else if (this.type == 'object') { |
|
this._svgObject._onFlashLoaded(msg); |
|
} |
|
return; |
|
} |
|
}, |
|
|
|
_onMouseEvent: function(msg) { |
|
//console.log('_onMouseEvent, msg='+this.debugMsg(msg)); |
|
var target = this._getElementByGuid(msg.targetGUID); |
|
var currentTarget = this._getElementByGuid(msg.currentTargetGUID); |
|
|
|
// TODO: FIXME: |
|
// The stageX,Y mouse coordinates delivered from flash are |
|
// relative to the flash object. |
|
// In native implementations, mouse event coordinates are |
|
// relative to the browser content viewport (clientX, clientY) |
|
// and to the actual physical screen pixels (screenX, screenY). |
|
// AFAICT, flash does not have access to this positioning information, |
|
// so for now we provide mouse coordinates which assume the flash |
|
// object is located at the viewport origin and that the viewport |
|
// origin is at the screen origin, which of course is not accurate. |
|
// |
|
// However, SVG developers who take no interest as to what |
|
// location the SVG file has been placed within the browser viewport |
|
// or screen should be OK. |
|
// |
|
// This logic should remain consistent with the implementation |
|
// of getScreenCTM. Native implementations implement getScreenCTM |
|
// relative to the browser viewport, not the actual screen, |
|
// which is confusing because that differs from screenX,Y in |
|
// mouse events. In fact, getScreenCTM must be implemented consistent |
|
// with clientX,clientY mouse event coordinates! There are SVG |
|
// scripting examples which depend on this. |
|
// In our case, that means getScreenCTM should return a transform |
|
// assuming the flash object is located at the browser origin, |
|
// which is what flash provides as node.transform.concatenatedMatrix. |
|
var evt = { target: target._getProxyNode(), |
|
currentTarget: currentTarget._getProxyNode(), |
|
type: msg.eventType, |
|
clientX: new Number(msg.stageX), |
|
clientY: new Number(msg.stageY), |
|
screenX: new Number(msg.stageX), |
|
screenY: new Number(msg.stageY), |
|
altKey: msg.altKey, |
|
ctrlKey: msg.ctrlKey, |
|
shiftKey: msg.shiftKey, |
|
button: 0, // flash only supports left button |
|
preventDefault: function() { this.returnValue=false; }, |
|
stopPropagation: function() { /* TODO */ } |
|
}; |
|
|
|
var handlers = currentTarget._listeners[msg.eventType]; |
|
if (handlers) { |
|
for (var i = 0; i < handlers.length; i++) { |
|
var handler = handlers[i]; |
|
var listener = handler.listener; |
|
// TODO: See Issue 208 |
|
// If the element is in an svg in an object, |
|
// then the function needs to be called in the |
|
// proper sandbox (see below). |
|
// See tests/browser-tests/test_events.html tests 10, 38 |
|
if (typeof listener == 'object') { |
|
listener.handleEvent.call(listener, evt); |
|
} else { |
|
listener.call(evt.currentTarget, evt); |
|
} |
|
} |
|
} |
|
if (msg.scriptCode != null) { |
|
if (this.type == 'object') { |
|
var defineEvtCode = |
|
'var evt = { target: document.getElementById("' + |
|
target._getProxyNode().getAttribute('id') + '") ,\n' + |
|
'currentTarget:document.getElementById("' + |
|
currentTarget._getProxyNode().getAttribute('id') + '") ,\n' + |
|
'type: "' + msg.eventType + '",\n' + |
|
'clientX: ' + new Number(msg.stageX) + ',\n' + |
|
'clientY: ' + new Number(msg.stageY) + ',\n' + |
|
'screenX: ' + new Number(msg.stageX) + ',\n' + |
|
'screenY: ' + new Number(msg.stageY) + ',\n' + |
|
'altKey: ' + msg.altKey + ',\n' + |
|
'ctrlKey: ' + msg.ctrlKey + ',\n' + |
|
'shiftKey: ' + msg.shiftKey + ',\n' + |
|
'button: 0,\n' + |
|
'preventDefault: function() { this.returnValue=false; },\n' + |
|
'stopPropagation: function() { }\n' + |
|
'};\n'; |
|
|
|
// prepare the code for the correct object context. |
|
var executeInContext = ';(function (evt) { ' + msg.scriptCode + '; }' + |
|
').call(evt.currentTarget, evt);\n'; |
|
// execute the code within the correct window context. |
|
this.sandbox_eval(this._svgObject._sandboxedScript(defineEvtCode + executeInContext)); |
|
} else { |
|
var eventFunc = new Function(msg.scriptCode); |
|
eventFunc.call(evt.currentTarget, evt); |
|
} |
|
} |
|
}, |
|
|
|
_getElementByGuid: function(guid) { |
|
var node = svgweb._guidLookup['_' + guid]; |
|
if (node) { |
|
return node; |
|
} |
|
|
|
var results; |
|
if (this.type == 'script') { |
|
results = xpath(this._xml, null, '//*[@__guid="' + guid + '"]'); |
|
} else if (this.type == 'object') { |
|
results = xpath(this._svgObject._xml, null, '//*[@__guid="' + guid + '"]'); |
|
} |
|
|
|
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); |
|
|
|
// _guidLookup holds _Nodes, so if this is an HTC node, get the _Node instead |
|
if(isIE && node._fakeNode) { |
|
node = node._fakeNode; |
|
} |
|
node._passThrough = true; |
|
|
|
return node; |
|
}, |
|
|
|
/** Calls if the Flash encounters an error. */ |
|
_onFlashError: function(msg) { |
|
this._onLog(msg); |
|
svgweb._fireFlashError('FLASH: ' + msg.logString); |
|
throw new Error('FLASH: ' + msg.logString); |
|
}, |
|
|
|
/** Stores any SCRIPT that might be inside an SVG file embedded through |
|
an SVG OBJECT to be executed at a later time when are done |
|
loading the Flash and HTC infrastructure. */ |
|
_onObjectScript: function(msg) { |
|
//console.log('onObjectScript, msg='+this.debugMsg(msg)); |
|
|
|
// batch for later execution |
|
this._svgObject._scriptsToExec.push(msg.script); |
|
}, |
|
|
|
/** View XML source for svg. Invoked from flash context menu. */ |
|
_onViewSource: function() { |
|
var origSVG = this._origSVG; |
|
|
|
if (!origSVG) { // dynamically created SVG objects and roots |
|
origSVG = 'SVG Source Not Available'; |
|
} |
|
|
|
// escape tags |
|
origSVG = origSVG.replace(/>/g,'>').replace(/</g,'<'); |
|
|
|
// place source in a new window |
|
var w = window.open('', '_blank'); |
|
w.document.write('<html><body><pre>' + origSVG + '</pre></body></html>'); |
|
w.document.close(); |
|
}, |
|
|
|
_onViewSourceDynamic: function(msg) { |
|
// add xml tag if not present |
|
if (msg.source.indexOf('<?xml') == -1) { |
|
msg.source='<?xml version="1.0"?>\n' + msg.source; |
|
} |
|
|
|
// remove svg web artifacts |
|
msg.source=msg.source.replace(/<svg:([^ ]+) /g, '<$1 '); |
|
msg.source=msg.source.replace(/<\/svg:([^>]+)>/g, '<\/$1>'); |
|
msg.source=msg.source.replace(/\n\s*<__text[^\/]*\/>/gm, ''); |
|
msg.source=msg.source.replace(/<__text[^>]*>([^<]*)<\/__text>/gm, '$1'); |
|
msg.source=msg.source.replace(/<__text[^>]*>/g, ''); |
|
msg.source=msg.source.replace(/<\/__text>/g, ''); |
|
msg.source=msg.source.replace(/\s*__guid="[^"]*"/g, ''); |
|
msg.source=msg.source.replace(/ id="__svg__random__[^"]*"/g, ''); |
|
msg.source=msg.source.replace(/>\n\n/g, '>\n'); |
|
|
|
// escape tags |
|
msg.source=msg.source.replace(/>/g, '>'); |
|
msg.source=msg.source.replace(/</g, '<'); |
|
|
|
// place source in a new window |
|
var w = window.open('', '_blank'); |
|
w.document.write('<body><pre>' + msg.source + '</pre></body>'); |
|
w.document.close(); |
|
} |
|
|
|
}); |
|
|
|
/** Creates a NativeHandler that will embed the given SVG into the page using |
|
native SVG support. Pass in an object literal with the correct arguments. |
|
Once the handler is setup call start() to have it kick off doing its work. |
|
|
|
If dealing with an SVG SCRIPT tag these arguments are: |
|
|
|
type - The string 'script'. |
|
svgID - A unique ID for the SVG root tag. |
|
xml - XML Document object for parsed SVG. |
|
svgString - The SVG content as a String. |
|
origSVG - The original, pre-cleaned up SVG. Useful so that we can |
|
provide the original SVG for 'View Source' functionality. Only used |
|
by the FlashHandler. |
|
scriptNode - The DOM element for the SVG SCRIPT block. |
|
|
|
If dealing with an SVG OBJECT tag these arguments are: |
|
|
|
type - The string 'object'. |
|
objID - A unique ID for the SVG OBJECT tag. |
|
objNode - DOM OBJECT pointing to an SVG URL to handle. |
|
*/ |
|
function NativeHandler(args) { |
|
this.type = args.type; |
|
|
|
this._xml = args.xml; |
|
|
|
if (this.type == 'object') { |
|
// these are mostly handled by the browser |
|
this.id = args.objID; |
|
this._objNode = args.objNode; |
|
} else if (this.type == 'script') { |
|
this.id = args.svgID; |
|
this._svgString = args.svgString; |
|
this._scriptNode = args.scriptNode; |
|
} |
|
} |
|
|
|
// start of 'static' singleton functions, mostly around patching the |
|
// document object with some bug fixes |
|
NativeHandler._patchBrowserObjects = function(win, doc) { |
|
if (doc._getElementById) { |
|
// already defined before |
|
return; |
|
} |
|
|
|
// we have to patch getElementById because getting a node by ID |
|
// if it is namespaced to something that is not XHTML or SVG does |
|
// not work natively; we build up a lookup table in _processSVGScript |
|
// that we can work with later |
|
// FIXME: explore actually removing support for this, as it's 'correct' |
|
// behavior according to the XML specification and would save file size |
|
// and simplify the NativeHandler code significantly |
|
|
|
// getElementById |
|
doc._getElementById = doc.getElementById; |
|
doc.getElementById = function(id) { |
|
var result = doc._getElementById(id); |
|
if (result !== null) { // Firefox doesn't like 'if (result)' |
|
// This is to solve an edge bug on Safari 3; |
|
// if you do a replaceChild on a non-SVG, non-HTML node, |
|
// the element is still returned by getElementById! |
|
// The element has a null parentNode. |
|
// TODO: FIXME: Track down whether this is caused by a memory |
|
// leak of some kind |
|
if (result.parentNode === null) { |
|
return null; |
|
} else { |
|
return result; |
|
} |
|
} |
|
|
|
// The id attribute for namespaced, non-SVG and non-HTML nodes |
|
// does not get picked up by getElementById, such as |
|
// <sodipodi:namedview id="someID"/>, so we have to use an XPath |
|
// expression |
|
result = xpath(doc, null, '//*[@id="' + id + '"]'); |
|
if (result.length) { |
|
var node = result[0]; |
|
|
|
// add an .id attribute for non-SVG and non-HTML nodes, which |
|
// don't have them by default in order to have parity with the |
|
// Flash viewer; note Firefox doesn't like if (node.namespaceURI) |
|
// rather than (node.namespaceURI !== null) |
|
if (node.namespaceURI !== null && node.namespaceURI != svgns |
|
&& node.namespaceURI != 'http://www.w3.org/1999/xhtml') { |
|
svgweb._exportID(node); |
|
} |
|
|
|
return node; |
|
} else { |
|
return null; |
|
} |
|
}; |
|
|
|
// we also have to patch getElementsByTagNameNS because it does |
|
// not seem to work consistently with namepaced content in an HTML |
|
// context, I believe due to casing issues (i.e. if the local name |
|
// were RDF rather than rdf it won't work) |
|
|
|
// getElementsByTagNameNS |
|
doc._getElementsByTagNameNS = doc.getElementsByTagNameNS; |
|
doc.getElementsByTagNameNS = function(ns, localName) { |
|
var result = doc._getElementsByTagNameNS(ns, localName); |
|
|
|
// firefox doesn't like if (result) |
|
if (result !== null && result.length !== 0) { |
|
if (ns !== null && ns != 'http://www.w3.org/1999/xhtml' && ns != svgns) { |
|
// add an .id attribute for non-SVG and non-HTML nodes, which |
|
// don't have them by default in order to have parity with the |
|
// Flash viewer |
|
for (var i = 0; i < result.length; i++) { |
|
var node = result[i]; |
|
svgweb._exportID(node); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
if (result === null || result.length === 0) { |
|
result = createNodeList(); |
|
} |
|
|
|
var xpathResults; |
|
for (var i = 0; i < svgweb.handlers.length; i++) { |
|
var handler = svgweb.handlers[i]; |
|
|
|
if (handler.type == 'object') { |
|
continue; |
|
} |
|
|
|
var prefix = handler._namespaces['_' + ns]; |
|
if (!prefix) { |
|
continue; |
|
} |
|
|
|
var expr; |
|
if (prefix == 'xmlns') { // default SVG namespace |
|
expr = "//*[namespace-uri()='" + svgns + "' and name()='" |
|
+ localName + "']"; |
|
} else if (prefix) { |
|
expr = '//' + prefix + ':' + localName; |
|
} else { |
|
expr = '//' + localName; |
|
} |
|
|
|
xpathResults = xpath(doc, handler._svgRoot, expr, handler._namespaces); |
|
if (xpathResults !== null && xpathResults !== undefined |
|
&& xpathResults.length > 0) { |
|
for (var j = 0; j < xpathResults.length; j++) { |
|
var node = xpathResults[j]; |
|
|
|
// add an .id attribute for non-SVG and non-HTML nodes, which |
|
// don't have them by default in order to have parity with the |
|
// Flash viewer; note Firefox doesn't like if (node.namespaceURI) |
|
// rather than (node.namespaceURI !== null) |
|
if (node.namespaceURI !== null && node.namespaceURI != svgns |
|
&& node.namespaceURI != 'http://www.w3.org/1999/xhtml') { |
|
svgweb._exportID(node); |
|
} |
|
|
|
result.push(node); |
|
} |
|
|
|
return result; |
|
} |
|
} |
|
|
|
return createNodeList(); |
|
}; |
|
|
|
// When dynamically creating SVG roots that we add to a page, we need to |
|
// have them fire an onload event to handle the asynchronous nature of the |
|
// Flash handler. In order to have similar code we patch the Native Handler |
|
// as well. In a sane world we could just change |
|
// SVGSVGElement.prototype.addEventListener when working with this, but |
|
// Firefox doesn't seem to allow us to over ride that (Safari does). To |
|
// get around this we do a small patch to createElementNS to slightly |
|
// patch addEventListener. |
|
doc._createElementNS = doc.createElementNS; |
|
doc.createElementNS = function(ns, localName) { |
|
if (ns != svgns || localName != 'svg') { |
|
return doc._createElementNS(ns, localName); |
|
} |
|
|
|
// svg root |
|
var svg = doc._createElementNS(ns, localName); |
|
|
|
// patch addEventListener |
|
svg = NativeHandler._patchAddEventListener(svg); |
|
|
|
return svg; |
|
} |
|
|
|
// Native browsers will fire the load event for SVG OBJECTs; we need to |
|
// intercept event listeners for these so that they don't get fired in |
|
// the wrong order |
|
doc._createElement = doc.createElement; |
|
doc.createElement = function(name, forSVG) { |
|
if (!forSVG) { |
|
return doc._createElement(name); |
|
} |
|
|
|
if (forSVG && name == 'object') { |
|
// patch addEventListener |
|
var obj = doc._createElement(name); |
|
obj = NativeHandler._patchAddEventListener(obj); |
|
return obj; |
|
} else { |
|
throw 'Unknown createElement() call for SVG: ' + name; |
|
} |
|
} |
|
|
|
// cloneNode needs some help or it loses our reference to the patched |
|
// addEventListener |
|
NativeHandler._patchCloneNode(); |
|
|
|
// Firefox/Native needs some help around svgElement.style.* access; see |
|
// NativeHandler._patchStyleObject for details |
|
if (isFF) { |
|
NativeHandler._patchStyleObject(win); |
|
} |
|
|
|
// make sure that calls to window.addEventListener('SVGLoad', ...) or |
|
// window.onsvgload made from external SVG files works if done _after_ |
|
// the normal window.onload call has fired |
|
var rootElement = doc.rootElement; |
|
if (rootElement && rootElement.localName == 'svg' |
|
&& rootElement.namespaceURI == svgns) { |
|
NativeHandler._patchSvgFileAddEventListener(win, doc); |
|
} |
|
}; |
|
|
|
/** If someone calls cloneNode, our patched addEventListener method goes away; |
|
we need to ensure this doesn't happen. */ |
|
NativeHandler._patchCloneNode = function() { |
|
var proto; |
|
if (typeof SVGSVGElement != 'undefined') { // Firefox |
|
proto = SVGSVGElement.prototype; |
|
} else { // Webkit |
|
proto = document.createElementNS(svgns, 'svg').__proto__; |
|
} |
|
|
|
if (proto._cloneNode) { // already patched |
|
return; |
|
} |
|
|
|
proto._cloneNode = proto.cloneNode; |
|
proto.cloneNode = function(deepClone) { |
|
var results = this._cloneNode(deepClone); |
|
NativeHandler._patchAddEventListener(results); |
|
return results; |
|
} |
|
} |
|
|
|
/** Adds a bit of magic we need on addEventListener so we can |
|
fire SVGLoad events for dynamically created SVG nodes and load events |
|
for SVG OBJECTS. Unfortunately Firefox has a frustrating bug where we |
|
can't simple override the prototype for addEventListener: |
|
https://bugzilla.mozilla.org/show_bug.cgi?id=456151 |
|
As a workaround, we have to patch each individual SVG root instance or |
|
SVG OBJECT instance. |
|
*/ |
|
NativeHandler._patchAddEventListener = function(root) { |
|
//console.log('patchAddEventListener, root='+root.getAttribute('id')); |
|
|
|
// Firefox has a bizare bug; if I call createElement('object') then |
|
// replace it's addEventListener method with our own, _then_ call |
|
// createElement('object') to create another object, _that_ object's |
|
// addEventListener is changed as well! This bug is tracked here in Firefox's |
|
// bugzilla: |
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=528392 |
|
// Even though the addEventListener method is 'shared', inside the method it |
|
// is correctly bound to each separate instance, so 'this' is correct. |
|
// In order to get around the bug, we 'cache' the real addEventListener once |
|
// as an instance on NativeHandler. Interestingly, this truly is a shared |
|
// method so pre-patched obj1.addEventListener === obj2.addEventListener, |
|
// so we can re-use this method (this is probably the source of the issue). |
|
// We later use this NativeHandler._objectAddEventListener cached instance |
|
// inside of our custom addEventListener. |
|
if (root.nodeName == 'object' && !NativeHandler._objectAddEventListener) { |
|
NativeHandler._objectAddEventListener = root.addEventListener; |
|
} |
|
if (root.nodeName == 'object') { |
|
root._addEventListener = NativeHandler._objectAddEventListener; |
|
} else { |
|
root._addEventListener = root.addEventListener |
|
} |
|
root._onloadListeners = []; |
|
root.addEventListener = (function(self) { |
|
return function(type, f, useCapture) { |
|
if (type.toLowerCase() == 'svgload') { |
|
this._onloadListeners.push(f); |
|
} else { |
|
root._addEventListener(type, f, useCapture); |
|
} |
|
} |
|
})(); |
|
|
|
return root; |
|
}; |
|
|
|
/** Surprisingly, Firefox doesn't work when setting svgElement.style.property! |
|
For example, if you set myCircle.style.fill = 'red', nothing happens. You |
|
have to do myCircle.style.setProperty('fill', 'red', null). This issue is |
|
independent of the fact that we are running in a text/html situation, |
|
and happens in self-contained SVG files as well. To fix this, we have |
|
to patch in the ability to use the svgElement.style.* syntax. Note that |
|
Safari, Opera, Chrome, and others all natively support the |
|
svgElement.style.* syntax so we don't have to patch anything there. |
|
|
|
@param window The owner window to patch. |
|
*/ |
|
NativeHandler._patchStyleObject = function(win) { |
|
// Unfortunately, trying to set SVGElement.prototype.style = to our own |
|
// custom object that then defines all of our getters and setters doesn't |
|
// work; somehow that is a 'magical' prototype that doesn't stick. Instead, |
|
// the trick we have to use is to modify the CSSStyleDeclaration prototype. |
|
|
|
// TODO: Test whether adding extra members to CSSStyleDeclaration has |
|
// a memory impact because it also affects HTML elements; probably not since |
|
// prototypes are singletons shared by all instances |
|
|
|
// prototype definitions are 'window' specific |
|
var patchMe = win.CSSStyleDeclaration; |
|
|
|
// define getters and setters for SVG CSS property names |
|
for (var i = 0; i < _Style._allStyles.length; i++) { |
|
var styleName = _Style._allStyles[i]; |
|
|
|
// convert camel casing (i.e. strokeWidth becomes stroke-width) |
|
var stylePropName = styleName.replace(/([A-Z])/g, '-$1').toLowerCase(); |
|
|
|
// Do a trick so that we can have a separate closure for each |
|
// iteration of the loop and therefore each separate style name; |
|
// closures are function-level, so doing an anonymous inline function |
|
// will force the closure into just being what we pass into it. If we |
|
// don't do this then the closure will actually always simply be the final |
|
// index position when the for loop finishes. |
|
(function(styleName, stylePropName) { |
|
patchMe.prototype.__defineSetter__(styleName, |
|
function(styleValue) { |
|
return this.setProperty(stylePropName, styleValue, null); |
|
} |
|
); |
|
patchMe.prototype.__defineGetter__(styleName, |
|
function() { |
|
return this.getPropertyValue(stylePropName); |
|
} |
|
); |
|
})(styleName, stylePropName); // pass closure values |
|
} |
|
}; |
|
|
|
/** |
|
This method is meant to address the following edge condition. If we |
|
have an external SVG file that we have brought in using an SVG |
|
OBJECT tag, such as foobar.svg, inside _that_ external SVG file |
|
there might be nested calls to know when SVG Web is done loading |
|
_after_ the page has loaded: |
|
|
|
foobar.svg: |
|
<svg> |
|
<script> |
|
window.addEventListener('load', function() { |
|
// be able to handle this! |
|
window.addEventListener('SVGLoad', function() { |
|
// this should fire |
|
}, false); |
|
}, false); |
|
</script> |
|
</svg> |
|
|
|
@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 = '<?xml version="1.0"?>\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 = '<?xml version="1.0"?>\n' |
|
+ '<__document__fragment></__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: |
|
|
|
<clipPath id="clipPath11119"> |
|
<rect width="36.416" height="36.416" ry="0" x="247" y="-157" |
|
style="opacity:1;fill:#6b6b6b;" |
|
id="rect11121" /> |
|
</clipPath> |
|
|
|
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('<script>' + script + '</script>'); |
|
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 = |
|
'<object\n ' |
|
+ 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"\n ' |
|
+ 'codebase="' |
|
+ protocol |
|
+ '://fpdownload.macromedia.com/pub/shockwave/cabs/flash/' |
|
+ 'swflash.cab#version=9,0,0,0"\n ' |
|
+ 'width="' + size.width + '"\n ' |
|
+ 'height="' + size.height + '"\n ' |
|
+ 'id="' + this._handler.flashID + '"\n ' |
|
+ 'name="' + this._handler.flashID + '"\n ' |
|
+ 'style="' + style + '"\n ' |
|
+ 'class="' + className + '"\n ' |
|
+ customAttrStr + '\n' |
|
+ '>\n ' |
|
+ '<param name="allowScriptAccess" value="always"></param>\n ' |
|
+ '<param name="movie" value="' + src + '"></param>\n ' |
|
+ '<param name="quality" value="high"></param>\n ' |
|
+ '<param name="FlashVars" value="' + flashVars + '"></param>\n ' |
|
// FIXME: Will this logic test break if the color is black? |
|
+ (background.color ? '<param name="bgcolor" value="' |
|
+ background.color + '"></param>\n ' : '') |
|
+ (background.transparent ? |
|
'<param name="wmode" value="transparent">' |
|
+ '</param>\n ' : '') |
|
+ '<embed ' |
|
+ 'src="' + src + '" ' |
|
+ 'quality="high" ' |
|
+ (background.color ? 'bgcolor="' + background.color |
|
+ '" \n' : '') |
|
+ (background.transparent ? 'wmode="transparent" \n' : '') |
|
+ 'width="' + size.width + '" ' |
|
+ 'height="' + size.height + '" ' |
|
+ 'id="' + this._handler.flashID + '" ' |
|
+ 'name="' + this._handler.flashID + '" ' |
|
+ 'swLiveConnect="true" ' |
|
+ 'allowScriptAccess="always" ' |
|
+ 'type="application/x-shockwave-flash" ' |
|
+ 'FlashVars="' + flashVars + '" ' |
|
+ 'pluginspage="' |
|
+ protocol |
|
+ '://www.macromedia.com/go/getflashplayer" ' |
|
+ 'style="' + style + '"\n ' |
|
+ 'class="' + className + '"\n ' |
|
+ customAttrStr + '\n' |
|
+ ' />' |
|
+ '</object>'; |
|
} |
|
|
|
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 |
|
|
|
|