1 /********************************************** 2 * Copyright (C) 2010 Lukas Laag 3 * This file is part of lib-gwt-svg. 4 * 5 * libgwtsvg is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU Lesser General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * libgwtsvg is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU Lesser General Public License for more details. 14 * 15 * You should have received a copy of the GNU Lesser General Public License 16 * along with libgwtsvg. If not, see http://www.gnu.org/licenses/ 17 **********************************************/ 18 package org.vectomatic.dom.svg.impl; 19 20 import org.vectomatic.dom.svg.OMNode; 21 import org.vectomatic.dom.svg.OMSVGElement; 22 import org.vectomatic.dom.svg.utils.XPathPrefixResolver; 23 24 import com.google.gwt.core.client.GWT; 25 import com.google.gwt.core.client.JavaScriptObject; 26 import com.google.gwt.core.client.impl.SchedulerImpl; 27 import com.google.gwt.dom.client.Document; 28 import com.google.gwt.dom.client.Element; 29 import com.google.gwt.dom.client.NativeEvent; 30 import com.google.gwt.dom.client.Node; 31 import com.google.gwt.dom.client.ScriptElement; 32 import com.google.gwt.event.dom.client.LoseCaptureEvent; 33 import com.google.gwt.event.dom.client.LoseCaptureHandler; 34 import com.google.gwt.event.shared.HandlerRegistration; 35 import com.google.gwt.resources.client.ClientBundle; 36 import com.google.gwt.resources.client.TextResource; 37 38 /** 39 * Implementation class for low-level GWT integration 40 * (mostly event dispatching) 41 * Xpath support for IE, based on Cameron McCormack's library 42 * (http://mcc.id.au/xpathjs) 43 * SVGPathSeg support for Chromium 48+, based on Philip Rogers's polyfill 44 * (https://github.com/progers/pathseg/blob/master/pathseg.js) 45 * @author laaglu 46 */ 47 public class DOMHelperImpl { 48 protected static boolean eventsInitialized; 49 protected OMSVGElement captureElt; 50 51 /** 52 * Initializes the event system. Positions event handlers 53 * on the window so that they can capture events early 54 * if necessary. 55 */ 56 protected native void initEventSystem() /*-{ 57 $wnd.__helperImpl = this; 58 $wnd.__svgDispatch = function(evt) { 59 $wnd.__helperImpl.@org.vectomatic.dom.svg.impl.DOMHelperImpl::dispatch(Lcom/google/gwt/dom/client/NativeEvent;Lorg/vectomatic/dom/svg/OMNode;Lcom/google/gwt/dom/client/Element;)(evt, evt.currentTarget.__wrapper, evt.currentTarget); 60 }; 61 62 $wnd.__svgCapture = function(evt) { 63 $wnd.__helperImpl.@org.vectomatic.dom.svg.impl.DOMHelperImpl::dispatchCapturedEvent(Lcom/google/gwt/dom/client/NativeEvent;Lcom/google/gwt/dom/client/Element;)(evt, evt.currentTarget); 64 }; 65 66 $wnd.addEventListener('mousedown', $wnd.__svgCapture, true); 67 $wnd.addEventListener('mouseup', $wnd.__svgCapture, true); 68 $wnd.addEventListener('mousemove', $wnd.__svgCapture, true); 69 $wnd.addEventListener('mouseover', $wnd.__svgCapture, true); 70 $wnd.addEventListener('mouseout', $wnd.__svgCapture, true); 71 $wnd.addEventListener('mousewheel', $wnd.__svgCapture, true); 72 $wnd.addEventListener('click', $wnd.__svgCapture, true); 73 $wnd.addEventListener('focusin', $wnd.__svgCapture, true); 74 $wnd.addEventListener('focusout', $wnd.__svgCapture, true); 75 $wnd.addEventListener('activate', $wnd.__svgCapture, true); 76 $wnd.addEventListener('touchstart', $wnd.__svgCapture, true); 77 $wnd.addEventListener('touchend', $wnd.__svgCapture, true); 78 $wnd.addEventListener('touchmove', $wnd.__svgCapture, true); 79 $wnd.addEventListener('touchcancel', $wnd.__svgCapture, true); 80 $wnd.addEventListener('gesturestart', $wnd.__svgCapture, true); 81 $wnd.addEventListener('gesturechange', $wnd.__svgCapture, true); 82 $wnd.addEventListener('gestureend', $wnd.__svgCapture, true); 83 $wnd.addEventListener('dragstart', $wnd.__svgCapture, true); 84 $wnd.addEventListener('drag', $wnd.__svgCapture, true); 85 $wnd.addEventListener('dragenter', $wnd.__svgCapture, true); 86 $wnd.addEventListener('dragleave', $wnd.__svgCapture, true); 87 $wnd.addEventListener('dragover', $wnd.__svgCapture, true); 88 $wnd.addEventListener('drop', $wnd.__svgCapture, true); 89 $wnd.addEventListener('dragend', $wnd.__svgCapture, true); 90 }-*/; 91 92 93 protected void init() { 94 if (!eventsInitialized) { 95 eventsInitialized = true; 96 initEventSystem(); 97 } 98 } 99 /** 100 * Makes a node sink the events emitted by the specified element 101 * @param elem The element emitting the events 102 * @param eventName The event name 103 */ 104 public void bindEventListener(Element elem, String eventName) { 105 init(); 106 sinkEvents(elem, eventName); 107 } 108 109 /** 110 * Makes a node stop sinking the events emitted by the specified element 111 * @param elem The element emitting the events 112 * @param eventName The event name 113 */ 114 public void unbindEventListener(Element elem, String eventName) { 115 init(); 116 unsinkEvents(elem, eventName); 117 } 118 119 /** 120 * Returns the element which currently captures all the 121 * events after a call to {@link org.vectomatic.dom.svg.impl.DOMHelperImpl#setCaptureElement(OMSVGElement, LoseCaptureHandler)} 122 * or null if element is set to capture events 123 * @return The event capturing element 124 */ 125 public OMSVGElement getCaptureElement() { 126 init(); 127 return captureElt; 128 } 129 130 /** 131 * Makes the specified element capture all the events, until 132 * a call to {@link org.vectomatic.dom.svg.impl.DOMHelperImpl#releaseCaptureElement()} 133 * terminates the capture 134 * @param captureElt The capturing element 135 * @param loseCaptureHandler A handler which will be invoked 136 * if the element loses capture 137 * @return {@link HandlerRegistration} used to remove this handler 138 */ 139 public HandlerRegistration setCaptureElement(OMSVGElement captureElt, LoseCaptureHandler loseCaptureHandler) { 140 init(); 141 this.captureElt = captureElt; 142 HandlerRegistration registration = null; 143 if (loseCaptureHandler != null) { 144 registration = captureElt.addHandler(loseCaptureHandler, LoseCaptureEvent.getType()); 145 } 146 return registration; 147 } 148 149 /** 150 * Stops the forwarding of all events to the capturing element 151 * specified by {@link org.vectomatic.dom.svg.impl.DOMHelperImpl#setCaptureElement(OMSVGElement, LoseCaptureHandler)} 152 */ 153 public void releaseCaptureElement() { 154 init(); 155 captureElt = null; 156 } 157 158 /** 159 * Activate the event listener for the specified 160 * event on an element 161 * @param elem The object which emits events 162 * @param eventName The event name 163 */ 164 protected native void sinkEvents(Element elem, String eventName) /*-{ 165 elem.addEventListener(eventName, $wnd.__svgDispatch, false); 166 }-*/; 167 168 /** 169 * Deactivate the event listener for the specified 170 * event on an element 171 * @param elem The object which emits events 172 * @param eventName The event name 173 */ 174 protected native void unsinkEvents(Element elem, String eventName) /*-{ 175 elem.removeEventListener(eventName, $wnd.__svgDispatch, false); 176 }-*/; 177 178 /** 179 * Central dispatching function for events emitted by DOM objects 180 * @param event The DOM event 181 * @param node The object processing the event 182 * @param elem The object emitting the event 183 */ 184 public void dispatch(NativeEvent event, OMNode node, Element elem) { 185 //Window.alert("type=" + event.getType()); 186 SchedulerImpl.INSTANCE.flushEntryCommands(); 187 String eventName = event.getType(); 188 if ("mouseover".equals(eventName) || "mouseout".equals(eventName)) { 189 // Mouseover and mouseout deserve special treatment 190 // to solve issues described in: 191 // http://www.quirksmode.org/js/events_mouse.html 192 // For SVG, it seems better to test against the tree rooted at 193 // evt.currentTarget than againt the subtree rooted at evt.target 194 if (isChildOf((Node)event.getCurrentEventTarget().cast(), (Node)event.getRelatedEventTarget().cast())) { 195 return; 196 } 197 } 198 node.dispatch(event); 199 SchedulerImpl.INSTANCE.flushFinallyCommands(); 200 } 201 202 /** 203 * Dispatching function for events which result from a call 204 * to {@link #setCaptureElement(OMSVGElement, LoseCaptureHandler)} 205 * @param event The DOM event 206 * @param elem The object emitting the event 207 */ 208 public void dispatchCapturedEvent(NativeEvent event, Element elem) { 209 if (captureElt != null) { 210 String eventName = event.getType(); 211 if ("loosecapture".equals(eventName)) { 212 captureElt = null; 213 } 214 dispatch(event, captureElt, elem); 215 event.stopPropagation(); 216 } 217 } 218 219 /** 220 * Tests if a node is part of a DOM subtree. 221 * @param root 222 * The subtree root 223 * @param node 224 * The node to be tested 225 * @return 226 * True if the node is part of the subtree, false otherwise 227 */ 228 protected native boolean isChildOf(Node root, Node node) /*-{ 229 while (node != null && node != root && node.nodeName != 'BODY') { 230 node= node.parentNode 231 } 232 if (node === root) { 233 return true; 234 } 235 return false; 236 }-*/; 237 238 /////////////////////////////////////////////////////////////// 239 // XPath support 240 /////////////////////////////////////////////////////////////// 241 242 public native JavaScriptObject evaluateNodeListXPath_(Element svgElement, String expr, XPathPrefixResolver resolver) /*-{ 243 var result, xpath; 244 if (typeof Document.prototype.evaluate !== 'function') { 245 xpath = new XPathExpression(expr, $wnd.xpr, $wnd.xpp); 246 $wnd.xpr.gwtresolver = resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null; 247 result = xpath.evaluate(svgElement, XPathResult.ORDERED_NODE_ITERATOR_TYPE); 248 } else { 249 result = svgElement.ownerDocument.evaluate(expr, svgElement, resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null, XPathResult.ORDERED_NODE_ITERATOR_TYPE , null); 250 } 251 return result; 252 }-*/; 253 254 public native Node evaluateNodeXPath_(Element svgElement, String expr, XPathPrefixResolver resolver) /*-{ 255 var result, xpath; 256 if (typeof Document.prototype.evaluate !== 'function') { 257 xpath = new XPathExpression(expr, $wnd.xpr, $wnd.xpp); 258 $wnd.xpr.gwtresolver = resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null; 259 result = xpath.evaluate(svgElement, XPathResult.ANY_UNORDERED_NODE_TYPE); 260 } else { 261 result = svgElement.ownerDocument.evaluate(expr, svgElement, resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null, XPathResult.ANY_UNORDERED_NODE_TYPE , null); 262 } 263 return result.singleNodeValue; 264 }-*/; 265 266 public native String evaluateStringXPath_(Element svgElement, String expr, XPathPrefixResolver resolver) /*-{ 267 var result, xpath; 268 if (typeof Document.prototype.evaluate !== 'function') { 269 xpath = new XPathExpression(expr, $wnd.xpr, $wnd.xpp); 270 $wnd.xpr.gwtresolver = resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null; 271 result = xpath.evaluate(svgElement, XPathResult.STRING_TYPE); 272 } else { 273 result = svgElement.ownerDocument.evaluate(expr, svgElement, resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null, XPathResult.STRING_TYPE , null); 274 } 275 return result.stringValue; 276 }-*/; 277 278 public native float evaluateNumberXPath_(Element svgElement, String expr, XPathPrefixResolver resolver) /*-{ 279 var result, xpath; 280 if (typeof Document.prototype.evaluate !== 'function') { 281 xpath = new XPathExpression(expr, $wnd.xpr, $wnd.xpp); 282 $wnd.xpr.gwtresolver = resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null; 283 result = xpath.evaluate(svgElement, XPathResult.NUMBER_TYPE); 284 } else { 285 result = svgElement.ownerDocument.evaluate(expr, svgElement, resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null, XPathResult.NUMBER_TYPE , null); 286 } 287 return result.numberValue; 288 }-*/; 289 290 public native boolean evaluateBooleanXPath_(Element svgElement, String expr, XPathPrefixResolver resolver) /*-{ 291 var result, xpath; 292 if (typeof Document.prototype.evaluate !== 'function') { 293 xpath = new XPathExpression(expr, $wnd.xpr, $wnd.xpp); 294 $wnd.xpr.gwtresolver = resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null; 295 result = xpath.evaluate(svgElement, XPathResult.BOOLEAN_TYPE); 296 } else { 297 result = svgElement.ownerDocument.evaluate(expr, svgElement, resolver ? function(prefix) { return resolver.@org.vectomatic.dom.svg.utils.XPathPrefixResolver::resolvePrefix(Ljava/lang/String;)(prefix); } : null, XPathResult.BOOLEAN_TYPE , null); 298 } 299 return result.booleanValue; 300 }-*/; 301 302 /////////////////////////////////////////////////////////////// 303 // XPath shim for IE support 304 // Pathseg polyfill for Chomium 48+ 305 // getTransformToElement polyfill for Chomium 48+ 306 /////////////////////////////////////////////////////////////// 307 308 public interface Resource extends ClientBundle { 309 static Resource INSTANCE = GWT.create(Resource.class); 310 @Source("xpath.js") 311 TextResource xpath(); 312 @Source("pathseg.js") 313 TextResource pathseg(); 314 @Source("getTransformToElement.js") 315 TextResource getTransformToElement(); 316 } 317 318 public DOMHelperImpl() { 319 if (!hasNativeXPath()) { 320 // Inject the xpath.js script in the iframe document (not 321 // in the main document, otherwise the added code will not be 322 // seen by the GWT code which lives in the iframe document) 323 Document doc = getIFrameDocument(); 324 ScriptElement scriptElem = doc.createScriptElement(Resource.INSTANCE.xpath().getText()); 325 doc.getBody().appendChild(scriptElem); 326 initXPath(); 327 } 328 if (!hasNativePathSeg()) { 329 // Inject the pathseg.js script in the main document 330 // and the iframe document 331 Document doc1 = Document.get(); 332 ScriptElement scriptElem1 = doc1.createScriptElement(Resource.INSTANCE.pathseg().getText()); 333 doc1.getBody().appendChild(scriptElem1); 334 335 Document doc2 = getIFrameDocument(); 336 ScriptElement scriptElem2 = doc2.createScriptElement(Resource.INSTANCE.pathseg().getText()); 337 doc2.getBody().appendChild(scriptElem2); 338 } 339 if (!hasGetTransformToElement()) { 340 // Inject the getTransformToElement.js script in the main document 341 // and the iframe document 342 Document doc1 = Document.get(); 343 ScriptElement scriptElem1 = doc1.createScriptElement(Resource.INSTANCE.getTransformToElement().getText()); 344 doc1.getBody().appendChild(scriptElem1); 345 346 Document doc2 = getIFrameDocument(); 347 ScriptElement scriptElem2 = doc2.createScriptElement(Resource.INSTANCE.getTransformToElement().getText()); 348 doc2.getBody().appendChild(scriptElem2); 349 } 350 } 351 352 public static native boolean hasNativeXPath() /*-{ 353 return typeof Document.prototype.evaluate === 'function'; 354 }-*/; 355 356 public static native boolean hasNativePathSeg() /*-{ 357 return typeof SVGPathSeg === 'function'; 358 }-*/; 359 360 public static native boolean hasGetTransformToElement() /*-{ 361 return 'getTransformToElement' in SVGSVGElement.prototype; 362 }-*/; 363 364 protected native void initXPath() /*-{ 365 $wnd.xpp = new XPathParser(); 366 367 // Create a custom namespace resolver 368 SvgNamespaceResolver.prototype = new NamespaceResolver(); 369 SvgNamespaceResolver.prototype.constructor = SvgNamespaceResolver; 370 SvgNamespaceResolver.superclass = NamespaceResolver.prototype; 371 function SvgNamespaceResolver() { 372 this.gwtresolver = null; 373 } 374 375 SvgNamespaceResolver.prototype.getNamespace = function(prefix, n) { 376 var ns = null; 377 if (this.gwtresolver != null) { 378 ns = this.gwtresolver(prefix); 379 } 380 if (ns == null) { 381 ns = n.namespaceURI; 382 } 383 if (ns == null) { 384 ns = SvgNamespaceResolver.superclass.getNamespace(prefix, n); 385 } 386 return ns; 387 }; 388 $wnd.xpr = new SvgNamespaceResolver(); 389 }-*/; 390 391 public static native Document getIFrameDocument() /*-{ 392 return document; 393 }-*/; 394 395 }