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 }