From: Trevor Parscal Date: Fri, 20 Nov 2009 02:24:28 +0000 (+0000) Subject: Initial check in for the SVGZoom tool. X-Git-Tag: 1.31.0-rc.0~38747 X-Git-Url: https://git.cyclocoop.org/%7B%24admin_url%7Dmembres/fiche.php?a=commitdiff_plain;h=934c7e34dedf10bccd112e7aa6e4aead1144449a;p=lhc%2Fweb%2Fwiklou.git Initial check in for the SVGZoom tool. --- diff --git a/extensions/SVGZoom/SVGZoom.hooks.php b/extensions/SVGZoom/SVGZoom.hooks.php new file mode 100644 index 0000000000..50c772c8eb --- /dev/null +++ b/extensions/SVGZoom/SVGZoom.hooks.php @@ -0,0 +1,37 @@ +getText() ends in .svg + */ + + $out->addScript( + Xml::element( + 'script', + array( + 'type' => $wgJsMimeType, + 'src' => "{$wgScriptPath}/extensions/SVGZoom/SVGZoom.js?{$wgSVGZoomScriptVersion}", + ), + '', + false + ) + ); + return true; + } +} \ No newline at end of file diff --git a/extensions/SVGZoom/SVGZoom.i18n.php b/extensions/SVGZoom/SVGZoom.i18n.php new file mode 100644 index 0000000000..514a83fe67 --- /dev/null +++ b/extensions/SVGZoom/SVGZoom.i18n.php @@ -0,0 +1,17 @@ + 'SVGZoom', + 'svgzoom-desc' => 'Adds pan and zoom capabilies to MediaWiki when viewing SVG files.', +); diff --git a/extensions/SVGZoom/SVGZoom.js b/extensions/SVGZoom/SVGZoom.js new file mode 100644 index 0000000000..9b79dc5067 --- /dev/null +++ b/extensions/SVGZoom/SVGZoom.js @@ -0,0 +1,461 @@ +(function(){ // hide everything externally to avoid name collisions + +// whether to display debugging output +var svgDebug = true; + +// whether we are locally debugging (i.e. the page is downloaded to our +// hard drive and served from a local server to ease development) +var localDebug = true; + +// the full URL to where svg.js is located +// Note: Update this before putting on production +var svgSrcURL; +if (localDebug) { + svgSrcURL = wgScriptPath + '/extensions/SVGZoom/src/svg.js'; +} else { + svgSrcURL = wgScriptPath + '/extensions/SVGZoom/src/svg.js'; +} + +// whether the pan and zoom UI is initialized +var svgUIReady = false; + +// a reference to the SVG OBJECT on the page +var svgObject; + +// a reference to our zoom and pan controls +var svgControls; + +// a reference to our SVG root tag +var svgRoot; + +var isWebkit = (Math.max(navigator.appVersion.indexOf('WebKit'), + navigator.appVersion.indexOf('Safari'), 0)); + +var isFF = false; +if (navigator.userAgent.indexOf('Gecko') >= 0) { + isFF = parseFloat(navigator.userAgent.split('Firefox/')[1]) || undefined; +} + +// the URL to the proxy from which we can fetch SVG images within the same +// domain as this page is served from +// TODO: define once a proxy or API call is setup + +// the location of our images +var imageBundle; +if (localDebug) { + // for local debugging + var imageRoot = '/images/'; + imageBundle = { + 'searchtool': imageRoot + 'searchtool.png', + 'controls-north-mini': imageRoot + 'north-mini.png', + 'controls-west-mini': imageRoot + 'west-mini.png', + 'controls-east-mini': imageRoot + 'east-mini.png', + 'controls-south-mini': imageRoot + 'south-mini.png', + 'controls-zoom-plus-mini': imageRoot + 'zoom-plus-mini.png', + 'controls-zoom-world-mini': imageRoot + 'zoom-world-mini.png', + 'controls-zoom-minus-mini': imageRoot + 'zoom-minus-mini.png' + }; +} else { + // Note: update this before putting on production + var imageRoot = wgScriptPath + '/extensions/SVGZoom/images/'; + imageBundle = { + 'searchtool': imageRoot + 'searchtool.png', + 'controls-north-mini': imageRoot + 'north-mini.png', + 'controls-west-mini': imageRoot + 'west-mini.png', + 'controls-east-mini': imageRoot + 'east-mini.png', + 'controls-south-mini': imageRoot + 'south-mini.png', + 'controls-zoom-plus-mini': imageRoot + 'zoom-plus-mini.png', + 'controls-zoom-world-mini': imageRoot + 'zoom-world-mini.png', + 'controls-zoom-minus-mini': imageRoot + 'zoom-minus-mini.png' + }; +} + +// determines if we are at a Wikimedia Commons detail page for an SVG file +function isSVGPage() { + if (wgNamespaceNumber == 6 && wgTitle && wgTitle.indexOf('.svg') != -1 + && wgAction == 'view') { + return true; + } else { + return false; + } +} + +// Determines whether this page has image annotation enabled (i.e. the +// Add Note button). If this is enabled the DOM changes slightly and we have +// to account for it. Some SVG images have this (Tux.svg); others don't +// (Commons-logo.svg, for example). +function hasAnnotation() { + if (document.getElementById('ImageAnnotationAddButton')) { + return true; + } else { + return false; + } +} + +// inserts the SVG Web library into the page +function insertSVGWeb() { + document.write(''); +} + +// adds a button that when pressed turns on the zoom and pan UI +function addStartButton() { + // are we already present? user could have hit back button on an old + // loaded page + if (document.getElementById('SVGZoom.startButton')) { + return; + } + + // insert ourselves beside the SVG thumbnail area + var info = getSVGInfo(); + var thumbnail = info.fileNode; + if (hasAnnotation()) { + thumbnail = thumbnail.childNodes[0].childNodes[0]; + } + // make the container element we will go into a bit larger to accommodate + // the icon + var infoWidth = Number(String(info.width).replace('px', '')); + thumbnail.style.width = (infoWidth + 30) + 'px'; + var img = document.createElement('img'); + img.id = 'SVGZoom.startButton'; + img.src = imageBundle['searchtool']; + img.setAttribute('width', '30px'); + img.setAttribute('height', '30px'); + img.style.position = 'absolute'; + img.style.cursor = 'pointer'; + img.onclick = initUI; + // some SVG pages have a spurious
element; add before that + if (thumbnail.lastChild.nodeType == 1 + && thumbnail.lastChild.nodeName.toLowerCase() == 'br') { + thumbnail.insertBefore(img, thumbnail.lastChild); + } else { + thumbnail.appendChild(img); + } +} + +// adds the pan and zoom UI and turns the PNG into an SVG object +function initUI() { + if (svgUIReady) { // already initialized + return; + } + + svgUIReady = true; + + // remove magnifying glass icon + var startButton = document.getElementById('SVGZoom.startButton'); + startButton.parentNode.removeChild(startButton); + + // get the thumbnail container and make it invisible + var info = getSVGInfo(); + var thumbnail = info.fileNode; + if (hasAnnotation()) { + thumbnail = thumbnail.childNodes[0].childNodes[0]; + } + var oldPNG = thumbnail.childNodes[0]; + oldPNG.style.visibility = 'hidden'; + oldPNG.style.zIndex = -1000; + + // store a reference to the SVG root to make subsequent accesses faster + svgRoot = svgObject.contentDocument.rootElement; + + // Safari/Native has a bug where it doesn't respect the height/width of + // the OBJECT when scaling the size of some objects (commons-logo.svg, + // for example). A workaround is to manually set the size inside the SVG. + if (isWebkit) { + svgRoot.setAttribute('width', info.width); + svgRoot.setAttribute('height', info.height); + } + + // reveal the SVG object and controls + svgObject.parentNode.style.zIndex = 1000; + svgControls.style.display = 'block'; + + // make the cursor a hand when over the SVG; not all browsers support + // this property yet + svgRoot.setAttribute('cursor', 'pointer'); + // TODO: Get hand cursor showing up in SVG Web's Flash renderer + + // add drag listeners on the SVG root + svgRoot.addEventListener('mousedown', mouseDown, false); + svgRoot.addEventListener('mousemove', mouseMove, false); + svgRoot.addEventListener('mouseup', mouseUp, false); +} + +// Creates the SVG OBJECT during page load so that when we swap the PNG +// thumbnail and the SVG OBJECT it happens much faster +function createSVGObject() { + var info = getSVGInfo(); + var thumbnail = info.fileNode; + if (hasAnnotation()) { + thumbnail = thumbnail.childNodes[0].childNodes[0]; + } + + // create the SVG OBJECT that will replace our thumbnail container + var obj = document.createElement('object', true); + obj.setAttribute('type', 'image/svg+xml'); + obj.setAttribute('data', info.url); + obj.setAttribute('width', info.width); + obj.setAttribute('height', info.height); + obj.addEventListener('load', function() { + // store a reference to the SVG OBJECT + svgObject = this; + + // create the controls + svgControls = createControls(); + svgControls.style.display = 'none'; + + // now place the controls on top of the SVG object + if (thumbnail.lastChild.nodeType == 1 + && thumbnail.lastChild.nodeName.toLowerCase() == 'br') { + thumbnail.insertBefore(svgControls, thumbnail.lastChild); + } else { + thumbnail.appendChild(svgControls); + } + + // add our magnification icon + addStartButton(); + + // set up the mouse scroll wheel; FF 3.5/Native has an annoying bug + // where DOMMouseScroll events do not propagate to OBJECTs under + // some situations! + if (isFF && svgweb.getHandlerType() == 'native') { + hookEvent(svgObject.contentDocument.rootElement, 'mousewheel', + MouseWheel); + } else { + hookEvent('file', 'mousewheel', MouseWheel); + } + + // prevent IE memory leaks + thumbnail = obj = null; + }, false); + // ensure that the thumbnail container has relative positioning; this will + // reset our absolutely positioned elements to be relative to our parent + // so we have correct coordinates + thumbnail.style.position = 'relative'; + // position object behind the PNG image; do it in a DIV to avoid any + // strange style + OBJECT interactions + var container = document.createElement('div'); + container.id = 'SVGZoom.container'; + container.style.zIndex = -1000; + container.style.position = 'absolute'; + // FIXME: This is a hack; figure out why the Flash version of Commons-logo.svg + // is off by one 1 pixel on x and y + if (!hasAnnotation() && svgweb.getHandlerType() == 'flash') { + container.style.top = '-1px'; + container.style.left = '-1px'; + } else { + container.style.top = '0px'; + container.style.left = '0px'; + } + if (thumbnail.lastChild.nodeType == 1 + && thumbnail.lastChild.nodeName.toLowerCase() == 'br') { + thumbnail.insertBefore(container, thumbnail.lastChild); + } else { + thumbnail.appendChild(container); + } + svgweb.appendChild(obj, container); +} + +// Returns a DIV ready to append to the page with our zoom and pan controls +function createControls() { + var controls = document.createElement('div'); + controls.id = 'SVGZoom.controls'; + controls.innerHTML = + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
'; + + // attach event handlers + controls.childNodes[0].childNodes[0].onclick = panUp; + controls.childNodes[0].childNodes[1].onclick = panLeft; + controls.childNodes[0].childNodes[2].onclick = panRight; + controls.childNodes[0].childNodes[3].onclick = panDown; + controls.childNodes[0].childNodes[4].onclick = zoomIn; + controls.childNodes[0].childNodes[5].onclick = zoomWorld; + controls.childNodes[0].childNodes[6].onclick = zoomOut; + + return controls; +} + +function panUp() { + svgRoot.currentTranslate.setY(svgRoot.currentTranslate.getY() - 25); +} + +function panLeft() { + svgRoot.currentTranslate.setX(svgRoot.currentTranslate.getX() - 25); +} + +function panRight() { + svgRoot.currentTranslate.setX(svgRoot.currentTranslate.getX() + 25); +} + +function panDown() { + svgRoot.currentTranslate.setY(svgRoot.currentTranslate.getY() + 25); +} + +function zoomIn() { + svgRoot.currentScale = svgRoot.currentScale * 1.1; +} + +function zoomWorld() { + svgRoot.currentScale = 1; + svgRoot.currentTranslate.setXY(0, 0); +} + +function zoomOut() { + svgRoot.currentScale = svgRoot.currentScale / 1.1; +} + +// variables used for dragging +var x, y, rootX, rootY; +var dragX = 0, dragY = 0; +var dragging = false; + +function mouseDown(evt) { + dragging = true; + + x = evt.clientX; + y = evt.clientY; + + rootX = svgRoot.currentTranslate.getX(); + rootY = svgRoot.currentTranslate.getY(); + + evt.preventDefault(true); +} + +function mouseMove(evt) { + if (!dragging) { + return; + } + + dragX = evt.clientX - x; + dragY = evt.clientY - y; + + // Firefox and Webkit differ on the coordinates they return; Firefox + // returns the mouse coordinates with no scaling, while Webkit and SVG Web + // scale the mouse coordinates using the currentScale + if (isWebkit || svgweb.getHandlerType() == 'flash') { + dragX /= svgRoot.currentScale; + dragY /= svgRoot.currentScale; + } + + var newX = rootX + dragX; + var newY = rootY + dragY; + + svgRoot.currentTranslate.setXY(newX, newY); + + evt.preventDefault(true); +} + +function mouseUp(evt) { + dragging = false; + + evt.preventDefault(true); +} + +// Returns a data structure that has info about the SVG file on this page, including: +// url - filename and URL necessary to fetch the SVG file +// width and height - the width and height to make the SVG file +// fileNode - the DOM node that has the top level PNG thumbnail in it to replace +// imgNode - the actual IMG tag that has the PNG thumbnail inside of it +// Note that this method returns null if there is no file node on the page. +function getSVGInfo() { + var fileNode = document.getElementById('file'); + if (!fileNode) { + return null; + } + + var url; + if (hasAnnotation()) { + url = fileNode.childNodes[0].childNodes[0].childNodes[0].href; + } else { + url = fileNode.childNodes[0].href; + } + var imgNode = fileNode.getElementsByTagName('img')[0]; + var width = imgNode.getAttribute('width'); + var height = imgNode.getAttribute('height'); + + return { url: url, fileNode: fileNode, imgNode: imgNode, + width: width, height: height }; +} + +// Mousewheel Scrolling thanks to +// http://blog.paranoidferret.com/index.php/2007/10/31/javascript-tutorial-the-scroll-wheel/ +function hookEvent(element, eventName, callback) { + if (typeof(element) == 'string') { + element = document.getElementById(element); + } + + if (element == null) { + return; + } + + if (element.addEventListener) { + if (eventName == 'mousewheel') { + element.addEventListener('DOMMouseScroll', callback, false); + } + element.addEventListener(eventName, callback, false); + } else if (element.attachEvent) { + element.attachEvent("on" + eventName, callback); + } +} + +function MouseWheel(e) { + e = e ? e : window.event; + + var wheelData = e.detail ? e.detail * -1 : e.wheelDelta; + + if (wheelData > 0) { + zoomIn(); + } else { + zoomOut(); + } + + if (e.preventDefault) { + e.preventDefault(); + } + + return false; +} + +// called when the page is loaded and ready to be manipulated +function pageLoaded() { + svgweb.addOnLoad(createSVGObject); +} + +if (isSVGPage()) { + insertSVGWeb(); + addOnloadHook(pageLoaded); +} + +// hide internal implementation details inside of a closure +})(); diff --git a/extensions/SVGZoom/SVGZoom.php b/extensions/SVGZoom/SVGZoom.php new file mode 100644 index 0000000000..f826e71c27 --- /dev/null +++ b/extensions/SVGZoom/SVGZoom.php @@ -0,0 +1,44 @@ +, Brad Neuberg + * @license ? + * @version 0.1.0 + */ + +/* Configuration */ + +// This needs to be updated before deployments +$wgSVGZoomScriptVersion = 1; + +/* Setup */ + +// Sets Credits +$wgExtensionCredits['other'][] = array( + 'path' => __FILE__, + 'name' => 'SVGZoom', + 'author' => array( 'Trevor Parscal', 'Brad Neuberg' ), + 'version' => '0.1.0', + 'url' => 'http://www.mediawiki.org/wiki/Extension:SVGZoom', + 'descriptionmsg' => 'svgzoom-desc', +); + +// Adds Autoload Classes +$wgAutoloadClasses['SVGZoomHooks'] = + dirname( __FILE__ ) . "/SVGZoom.hooks.php"; + +// Adds Internationalized Messages +$wgExtensionMessagesFiles['SVGZoom'] = + dirname( __FILE__ ) . "/SVGZoom.i18n.php"; + +// Registers Hooks +$wgHooks['BeforePageDisplay'][] = 'SVGZoomHooks::addResources'; \ No newline at end of file diff --git a/extensions/SVGZoom/images/east-mini.png b/extensions/SVGZoom/images/east-mini.png new file mode 100644 index 0000000000..0707567a7d Binary files /dev/null and b/extensions/SVGZoom/images/east-mini.png differ diff --git a/extensions/SVGZoom/images/north-mini.png b/extensions/SVGZoom/images/north-mini.png new file mode 100644 index 0000000000..a8a0b4033e Binary files /dev/null and b/extensions/SVGZoom/images/north-mini.png differ diff --git a/extensions/SVGZoom/images/searchtool.png b/extensions/SVGZoom/images/searchtool.png new file mode 100644 index 0000000000..fed38b76c0 Binary files /dev/null and b/extensions/SVGZoom/images/searchtool.png differ diff --git a/extensions/SVGZoom/images/south-mini.png b/extensions/SVGZoom/images/south-mini.png new file mode 100644 index 0000000000..6c4ac8a0f1 Binary files /dev/null and b/extensions/SVGZoom/images/south-mini.png differ diff --git a/extensions/SVGZoom/images/west-mini.png b/extensions/SVGZoom/images/west-mini.png new file mode 100644 index 0000000000..db5f420ca4 Binary files /dev/null and b/extensions/SVGZoom/images/west-mini.png differ diff --git a/extensions/SVGZoom/images/zoom-minus-mini.png b/extensions/SVGZoom/images/zoom-minus-mini.png new file mode 100644 index 0000000000..f9b63aba01 Binary files /dev/null and b/extensions/SVGZoom/images/zoom-minus-mini.png differ diff --git a/extensions/SVGZoom/images/zoom-plus-mini.png b/extensions/SVGZoom/images/zoom-plus-mini.png new file mode 100644 index 0000000000..eecf2eb4bd Binary files /dev/null and b/extensions/SVGZoom/images/zoom-plus-mini.png differ diff --git a/extensions/SVGZoom/images/zoom-world-mini.png b/extensions/SVGZoom/images/zoom-world-mini.png new file mode 100644 index 0000000000..2159dde7ba Binary files /dev/null and b/extensions/SVGZoom/images/zoom-world-mini.png differ diff --git a/extensions/SVGZoom/src/svg.htc b/extensions/SVGZoom/src/svg.htc new file mode 100644 index 0000000000..e56600e357 --- /dev/null +++ b/extensions/SVGZoom/src/svg.htc @@ -0,0 +1 @@ + diff --git a/extensions/SVGZoom/src/svg.js b/extensions/SVGZoom/src/svg.js new file mode 100644 index 0000000000..f3ce9b886d --- /dev/null +++ b/extensions/SVGZoom/src/svg.js @@ -0,0 +1 @@ +window.timer={};function start(subject,subjectStarted){if(subjectStarted&&!ifStarted(subjectStarted)){return;}window.timer[subject]={start:new Date().getTime()};}function end(subject,subjectStarted){if(subjectStarted&&!ifStarted(subjectStarted)){return;}if(!window.timer[subject]){console.log("Unknown subject: "+subject);return;}window.timer[subject].end=new Date().getTime();}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");}}}(function(){window.svgns="http://www.w3.org/2000/svg";window.xlinkns="http://www.w3.org/1999/xlink";svgnsFake="urn:__fake__internal__namespace";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;}var index=Math.max(dav.indexOf("WebKit"),dav.indexOf("Safari"),0);if(index){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;}if(document.documentMode){isStandardsMode=(document.documentMode>5);}else{isStandardsMode=(document.compatMode=="CSS1Compat");}if(document.contentType=="application/xhtml+xml"){isXHTML=true;}else{if(typeof XMLDocument!="undefined"&&document.constructor==XMLDocument){isXHTML=true;}}}_detectBrowsers();function doDebugging(){var debug=false;var scripts=document.getElementsByTagName("script");for(var i=0;i<\/script>');var script=document.getElementById("__ie__svg__onload");script.onreadystatechange=function(){if(this.readyState!="complete"&&window.onload){self._saveWindowOnload();}else{if(this.readyState=="complete"){self._onDOMContentLoaded();}}};var documentReady=function(){if(window.onload){self._saveWindowOnload();document.detachEvent("onreadystatechange",documentReady);}};document.attachEvent("onreadystatechange",documentReady);}},_setXDomain:function(){var scripts=document.getElementsByTagName("script");for(var i=0;i\s*/,"");}if(addMissing){if(/\<\?xml/m.test(svg)==false){svg='\n'+svg;}if(svg.indexOf(":svg ")==-1){if(/xmlns\=['"]http:\/\/www\.w3\.org\/2000\/svg['"]/.test(svg)==false){svg=svg.replace("\s+\<");}if(this.renderer==FlashHandler){var commentRE=/")+3;var comment="