View Javadoc

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 }