Initial check in for the SVGZoom tool.
[lhc/web/wiklou.git] / extensions / SVGZoom / SVGZoom.js
1 (function(){ // hide everything externally to avoid name collisions
2
3 // whether to display debugging output
4 var svgDebug = true;
5
6 // whether we are locally debugging (i.e. the page is downloaded to our
7 // hard drive and served from a local server to ease development)
8 var localDebug = true;
9
10 // the full URL to where svg.js is located
11 // Note: Update this before putting on production
12 var svgSrcURL;
13 if (localDebug) {
14 svgSrcURL = wgScriptPath + '/extensions/SVGZoom/src/svg.js';
15 } else {
16 svgSrcURL = wgScriptPath + '/extensions/SVGZoom/src/svg.js';
17 }
18
19 // whether the pan and zoom UI is initialized
20 var svgUIReady = false;
21
22 // a reference to the SVG OBJECT on the page
23 var svgObject;
24
25 // a reference to our zoom and pan controls
26 var svgControls;
27
28 // a reference to our SVG root tag
29 var svgRoot;
30
31 var isWebkit = (Math.max(navigator.appVersion.indexOf('WebKit'),
32 navigator.appVersion.indexOf('Safari'), 0));
33
34 var isFF = false;
35 if (navigator.userAgent.indexOf('Gecko') >= 0) {
36 isFF = parseFloat(navigator.userAgent.split('Firefox/')[1]) || undefined;
37 }
38
39 // the URL to the proxy from which we can fetch SVG images within the same
40 // domain as this page is served from
41 // TODO: define once a proxy or API call is setup
42
43 // the location of our images
44 var imageBundle;
45 if (localDebug) {
46 // for local debugging
47 var imageRoot = '/images/';
48 imageBundle = {
49 'searchtool': imageRoot + 'searchtool.png',
50 'controls-north-mini': imageRoot + 'north-mini.png',
51 'controls-west-mini': imageRoot + 'west-mini.png',
52 'controls-east-mini': imageRoot + 'east-mini.png',
53 'controls-south-mini': imageRoot + 'south-mini.png',
54 'controls-zoom-plus-mini': imageRoot + 'zoom-plus-mini.png',
55 'controls-zoom-world-mini': imageRoot + 'zoom-world-mini.png',
56 'controls-zoom-minus-mini': imageRoot + 'zoom-minus-mini.png'
57 };
58 } else {
59 // Note: update this before putting on production
60 var imageRoot = wgScriptPath + '/extensions/SVGZoom/images/';
61 imageBundle = {
62 'searchtool': imageRoot + 'searchtool.png',
63 'controls-north-mini': imageRoot + 'north-mini.png',
64 'controls-west-mini': imageRoot + 'west-mini.png',
65 'controls-east-mini': imageRoot + 'east-mini.png',
66 'controls-south-mini': imageRoot + 'south-mini.png',
67 'controls-zoom-plus-mini': imageRoot + 'zoom-plus-mini.png',
68 'controls-zoom-world-mini': imageRoot + 'zoom-world-mini.png',
69 'controls-zoom-minus-mini': imageRoot + 'zoom-minus-mini.png'
70 };
71 }
72
73 // determines if we are at a Wikimedia Commons detail page for an SVG file
74 function isSVGPage() {
75 if (wgNamespaceNumber == 6 && wgTitle && wgTitle.indexOf('.svg') != -1
76 && wgAction == 'view') {
77 return true;
78 } else {
79 return false;
80 }
81 }
82
83 // Determines whether this page has image annotation enabled (i.e. the
84 // Add Note button). If this is enabled the DOM changes slightly and we have
85 // to account for it. Some SVG images have this (Tux.svg); others don't
86 // (Commons-logo.svg, for example).
87 function hasAnnotation() {
88 if (document.getElementById('ImageAnnotationAddButton')) {
89 return true;
90 } else {
91 return false;
92 }
93 }
94
95 // inserts the SVG Web library into the page
96 function insertSVGWeb() {
97 document.write('<script type="text/javascript" '
98 + 'src="' + svgSrcURL + '" '
99 + 'data-path="../../../../src" ' /* Note: remove before production */
100 + 'data-debug="' + svgDebug + '"></script>');
101 }
102
103 // adds a button that when pressed turns on the zoom and pan UI
104 function addStartButton() {
105 // are we already present? user could have hit back button on an old
106 // loaded page
107 if (document.getElementById('SVGZoom.startButton')) {
108 return;
109 }
110
111 // insert ourselves beside the SVG thumbnail area
112 var info = getSVGInfo();
113 var thumbnail = info.fileNode;
114 if (hasAnnotation()) {
115 thumbnail = thumbnail.childNodes[0].childNodes[0];
116 }
117 // make the container element we will go into a bit larger to accommodate
118 // the icon
119 var infoWidth = Number(String(info.width).replace('px', ''));
120 thumbnail.style.width = (infoWidth + 30) + 'px';
121 var img = document.createElement('img');
122 img.id = 'SVGZoom.startButton';
123 img.src = imageBundle['searchtool'];
124 img.setAttribute('width', '30px');
125 img.setAttribute('height', '30px');
126 img.style.position = 'absolute';
127 img.style.cursor = 'pointer';
128 img.onclick = initUI;
129 // some SVG pages have a spurious <br/> element; add before that
130 if (thumbnail.lastChild.nodeType == 1
131 && thumbnail.lastChild.nodeName.toLowerCase() == 'br') {
132 thumbnail.insertBefore(img, thumbnail.lastChild);
133 } else {
134 thumbnail.appendChild(img);
135 }
136 }
137
138 // adds the pan and zoom UI and turns the PNG into an SVG object
139 function initUI() {
140 if (svgUIReady) { // already initialized
141 return;
142 }
143
144 svgUIReady = true;
145
146 // remove magnifying glass icon
147 var startButton = document.getElementById('SVGZoom.startButton');
148 startButton.parentNode.removeChild(startButton);
149
150 // get the thumbnail container and make it invisible
151 var info = getSVGInfo();
152 var thumbnail = info.fileNode;
153 if (hasAnnotation()) {
154 thumbnail = thumbnail.childNodes[0].childNodes[0];
155 }
156 var oldPNG = thumbnail.childNodes[0];
157 oldPNG.style.visibility = 'hidden';
158 oldPNG.style.zIndex = -1000;
159
160 // store a reference to the SVG root to make subsequent accesses faster
161 svgRoot = svgObject.contentDocument.rootElement;
162
163 // Safari/Native has a bug where it doesn't respect the height/width of
164 // the OBJECT when scaling the size of some objects (commons-logo.svg,
165 // for example). A workaround is to manually set the size inside the SVG.
166 if (isWebkit) {
167 svgRoot.setAttribute('width', info.width);
168 svgRoot.setAttribute('height', info.height);
169 }
170
171 // reveal the SVG object and controls
172 svgObject.parentNode.style.zIndex = 1000;
173 svgControls.style.display = 'block';
174
175 // make the cursor a hand when over the SVG; not all browsers support
176 // this property yet
177 svgRoot.setAttribute('cursor', 'pointer');
178 // TODO: Get hand cursor showing up in SVG Web's Flash renderer
179
180 // add drag listeners on the SVG root
181 svgRoot.addEventListener('mousedown', mouseDown, false);
182 svgRoot.addEventListener('mousemove', mouseMove, false);
183 svgRoot.addEventListener('mouseup', mouseUp, false);
184 }
185
186 // Creates the SVG OBJECT during page load so that when we swap the PNG
187 // thumbnail and the SVG OBJECT it happens much faster
188 function createSVGObject() {
189 var info = getSVGInfo();
190 var thumbnail = info.fileNode;
191 if (hasAnnotation()) {
192 thumbnail = thumbnail.childNodes[0].childNodes[0];
193 }
194
195 // create the SVG OBJECT that will replace our thumbnail container
196 var obj = document.createElement('object', true);
197 obj.setAttribute('type', 'image/svg+xml');
198 obj.setAttribute('data', info.url);
199 obj.setAttribute('width', info.width);
200 obj.setAttribute('height', info.height);
201 obj.addEventListener('load', function() {
202 // store a reference to the SVG OBJECT
203 svgObject = this;
204
205 // create the controls
206 svgControls = createControls();
207 svgControls.style.display = 'none';
208
209 // now place the controls on top of the SVG object
210 if (thumbnail.lastChild.nodeType == 1
211 && thumbnail.lastChild.nodeName.toLowerCase() == 'br') {
212 thumbnail.insertBefore(svgControls, thumbnail.lastChild);
213 } else {
214 thumbnail.appendChild(svgControls);
215 }
216
217 // add our magnification icon
218 addStartButton();
219
220 // set up the mouse scroll wheel; FF 3.5/Native has an annoying bug
221 // where DOMMouseScroll events do not propagate to OBJECTs under
222 // some situations!
223 if (isFF && svgweb.getHandlerType() == 'native') {
224 hookEvent(svgObject.contentDocument.rootElement, 'mousewheel',
225 MouseWheel);
226 } else {
227 hookEvent('file', 'mousewheel', MouseWheel);
228 }
229
230 // prevent IE memory leaks
231 thumbnail = obj = null;
232 }, false);
233 // ensure that the thumbnail container has relative positioning; this will
234 // reset our absolutely positioned elements to be relative to our parent
235 // so we have correct coordinates
236 thumbnail.style.position = 'relative';
237 // position object behind the PNG image; do it in a DIV to avoid any
238 // strange style + OBJECT interactions
239 var container = document.createElement('div');
240 container.id = 'SVGZoom.container';
241 container.style.zIndex = -1000;
242 container.style.position = 'absolute';
243 // FIXME: This is a hack; figure out why the Flash version of Commons-logo.svg
244 // is off by one 1 pixel on x and y
245 if (!hasAnnotation() && svgweb.getHandlerType() == 'flash') {
246 container.style.top = '-1px';
247 container.style.left = '-1px';
248 } else {
249 container.style.top = '0px';
250 container.style.left = '0px';
251 }
252 if (thumbnail.lastChild.nodeType == 1
253 && thumbnail.lastChild.nodeName.toLowerCase() == 'br') {
254 thumbnail.insertBefore(container, thumbnail.lastChild);
255 } else {
256 thumbnail.appendChild(container);
257 }
258 svgweb.appendChild(obj, container);
259 }
260
261 // Returns a DIV ready to append to the page with our zoom and pan controls
262 function createControls() {
263 var controls = document.createElement('div');
264 controls.id = 'SVGZoom.controls';
265 controls.innerHTML =
266 '<div style="position: absolute; left: 4px; top: 4px; z-index: 1004;" unselectable="on">'
267 + '<div id="SVGZoom.panup" style="position: absolute; left: 13px; top: 4px; width: 18px; height: 18px;">'
268 + ' <img id="SVGZoom.panup.innerImage" style="position: relative; width: 18px; height: 18px;" '
269 + 'src="' + imageBundle['controls-north-mini'] + '"/>'
270 + '</div>'
271 + '<div id="SVGZoom.panleft" style="position: absolute; left: 4px; top: 22px; width: 18px; height: 18px;">'
272 + ' <img id="SVGZoom.panleft.innerImage" style="position: relative; width: 18px; height: 18px;" '
273 + 'src="' + imageBundle['controls-west-mini'] + '"/>'
274 + '</div>'
275 + '<div id="SVGZoom.panright" style="position: absolute; left: 22px; top: 22px; width: 18px; height: 18px;">'
276 + ' <img id="SVGZoom.panright.innerImage" style="position: relative; width: 18px; height: 18px;" '
277 + 'src="' + imageBundle['controls-east-mini'] + '"/>'
278 + '</div>'
279 + '<div id="SVGZoom.pandown" style="position: absolute; left: 13px; top: 40px; width: 18px; height: 18px;">'
280 + ' <img id="SVGZoom.pandown.innerImage" style="position: relative; width: 18px; height: 18px;" '
281 + 'src="' + imageBundle['controls-south-mini'] + '"/>'
282 + '</div>'
283 + '<div id="SVGZoom.zoomin" style="position: absolute; left: 13px; top: 63px; width: 18px; height: 18px;">'
284 + ' <img id="SVGZoom.zoomin.innerImage" style="position: relative; width: 18px; height: 18px;" '
285 + 'src="' + imageBundle['controls-zoom-plus-mini'] + '"/>'
286 + '</div>'
287 + '<div id="SVGZoom.zoomworld" style="position: absolute; left: 13px; top: 81px; width: 18px; height: 18px;">'
288 + ' <img id="SVGZoom.zoomworld.innerImage" style="position: relative; width: 18px; height: 18px;" '
289 + 'src="' + imageBundle['controls-zoom-world-mini'] + '"/>'
290 + '</div>'
291 + '<div id="SVGZoom.zoomout" style="position: absolute; left: 13px; top: 99px; width: 18px; height: 18px;">'
292 + ' <img id="SVGZoom.zoomout.innerImage" style="position: relative; width: 18px; height: 18px;" '
293 + 'src="' + imageBundle['controls-zoom-minus-mini'] + '"/>'
294 + '</div>'
295 + '</div>';
296
297 // attach event handlers
298 controls.childNodes[0].childNodes[0].onclick = panUp;
299 controls.childNodes[0].childNodes[1].onclick = panLeft;
300 controls.childNodes[0].childNodes[2].onclick = panRight;
301 controls.childNodes[0].childNodes[3].onclick = panDown;
302 controls.childNodes[0].childNodes[4].onclick = zoomIn;
303 controls.childNodes[0].childNodes[5].onclick = zoomWorld;
304 controls.childNodes[0].childNodes[6].onclick = zoomOut;
305
306 return controls;
307 }
308
309 function panUp() {
310 svgRoot.currentTranslate.setY(svgRoot.currentTranslate.getY() - 25);
311 }
312
313 function panLeft() {
314 svgRoot.currentTranslate.setX(svgRoot.currentTranslate.getX() - 25);
315 }
316
317 function panRight() {
318 svgRoot.currentTranslate.setX(svgRoot.currentTranslate.getX() + 25);
319 }
320
321 function panDown() {
322 svgRoot.currentTranslate.setY(svgRoot.currentTranslate.getY() + 25);
323 }
324
325 function zoomIn() {
326 svgRoot.currentScale = svgRoot.currentScale * 1.1;
327 }
328
329 function zoomWorld() {
330 svgRoot.currentScale = 1;
331 svgRoot.currentTranslate.setXY(0, 0);
332 }
333
334 function zoomOut() {
335 svgRoot.currentScale = svgRoot.currentScale / 1.1;
336 }
337
338 // variables used for dragging
339 var x, y, rootX, rootY;
340 var dragX = 0, dragY = 0;
341 var dragging = false;
342
343 function mouseDown(evt) {
344 dragging = true;
345
346 x = evt.clientX;
347 y = evt.clientY;
348
349 rootX = svgRoot.currentTranslate.getX();
350 rootY = svgRoot.currentTranslate.getY();
351
352 evt.preventDefault(true);
353 }
354
355 function mouseMove(evt) {
356 if (!dragging) {
357 return;
358 }
359
360 dragX = evt.clientX - x;
361 dragY = evt.clientY - y;
362
363 // Firefox and Webkit differ on the coordinates they return; Firefox
364 // returns the mouse coordinates with no scaling, while Webkit and SVG Web
365 // scale the mouse coordinates using the currentScale
366 if (isWebkit || svgweb.getHandlerType() == 'flash') {
367 dragX /= svgRoot.currentScale;
368 dragY /= svgRoot.currentScale;
369 }
370
371 var newX = rootX + dragX;
372 var newY = rootY + dragY;
373
374 svgRoot.currentTranslate.setXY(newX, newY);
375
376 evt.preventDefault(true);
377 }
378
379 function mouseUp(evt) {
380 dragging = false;
381
382 evt.preventDefault(true);
383 }
384
385 // Returns a data structure that has info about the SVG file on this page, including:
386 // url - filename and URL necessary to fetch the SVG file
387 // width and height - the width and height to make the SVG file
388 // fileNode - the DOM node that has the top level PNG thumbnail in it to replace
389 // imgNode - the actual IMG tag that has the PNG thumbnail inside of it
390 // Note that this method returns null if there is no file node on the page.
391 function getSVGInfo() {
392 var fileNode = document.getElementById('file');
393 if (!fileNode) {
394 return null;
395 }
396
397 var url;
398 if (hasAnnotation()) {
399 url = fileNode.childNodes[0].childNodes[0].childNodes[0].href;
400 } else {
401 url = fileNode.childNodes[0].href;
402 }
403 var imgNode = fileNode.getElementsByTagName('img')[0];
404 var width = imgNode.getAttribute('width');
405 var height = imgNode.getAttribute('height');
406
407 return { url: url, fileNode: fileNode, imgNode: imgNode,
408 width: width, height: height };
409 }
410
411 // Mousewheel Scrolling thanks to
412 // http://blog.paranoidferret.com/index.php/2007/10/31/javascript-tutorial-the-scroll-wheel/
413 function hookEvent(element, eventName, callback) {
414 if (typeof(element) == 'string') {
415 element = document.getElementById(element);
416 }
417
418 if (element == null) {
419 return;
420 }
421
422 if (element.addEventListener) {
423 if (eventName == 'mousewheel') {
424 element.addEventListener('DOMMouseScroll', callback, false);
425 }
426 element.addEventListener(eventName, callback, false);
427 } else if (element.attachEvent) {
428 element.attachEvent("on" + eventName, callback);
429 }
430 }
431
432 function MouseWheel(e) {
433 e = e ? e : window.event;
434
435 var wheelData = e.detail ? e.detail * -1 : e.wheelDelta;
436
437 if (wheelData > 0) {
438 zoomIn();
439 } else {
440 zoomOut();
441 }
442
443 if (e.preventDefault) {
444 e.preventDefault();
445 }
446
447 return false;
448 }
449
450 // called when the page is loaded and ready to be manipulated
451 function pageLoaded() {
452 svgweb.addOnLoad(createSVGObject);
453 }
454
455 if (isSVGPage()) {
456 insertSVGWeb();
457 addOnloadHook(pageLoaded);
458 }
459
460 // hide internal implementation details inside of a closure
461 })();