/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /************************************************************* * * MathJax/extensions/MathEvents.js * * Implements the event handlers needed by the output jax to perform * menu, hover, and other events. * * --------------------------------------------------------------------- * * Copyright (c) 2011-2013 The MathJax Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ (function (HUB,HTML,AJAX,CALLBACK,LOCALE,OUTPUT,INPUT) { var VERSION = "2.2"; var EXTENSION = MathJax.Extension; var ME = EXTENSION.MathEvents = {version: VERSION}; var SETTINGS = HUB.config.menuSettings; var CONFIG = { hover: 500, // time required to be considered a hover frame: { x: 3.5, y: 5, // frame padding and bwidth: 1, // frame border width (in pixels) bcolor: "#A6D", // frame border color hwidth: "15px", // haze width hcolor: "#83A" // haze color }, button: { x: -4, y: -3, // menu button offsets wx: -2, // button offset for full-width equations src: AJAX.fileURL(OUTPUT.imageDir+"/MenuArrow-15.png") // button image }, fadeinInc: .2, // increment for fade-in fadeoutInc: .05, // increment for fade-out fadeDelay: 50, // delay between fade-in or fade-out steps fadeoutStart: 400, // delay before fade-out after mouseout fadeoutDelay: 15*1000, // delay before automatic fade-out styles: { ".MathJax_Hover_Frame": { "border-radius": ".25em", // Opera 10.5 and IE9 "-webkit-border-radius": ".25em", // Safari and Chrome "-moz-border-radius": ".25em", // Firefox "-khtml-border-radius": ".25em", // Konqueror "box-shadow": "0px 0px 15px #83A", // Opera 10.5 and IE9 "-webkit-box-shadow": "0px 0px 15px #83A", // Safari and Chrome "-moz-box-shadow": "0px 0px 15px #83A", // Forefox "-khtml-box-shadow": "0px 0px 15px #83A", // Konqueror border: "1px solid #A6D ! important", display: "inline-block", position:"absolute" }, ".MathJax_Hover_Arrow": { position:"absolute", width:"15px", height:"11px", cursor:"pointer" } } }; // // Common event-handling code // var EVENT = ME.Event = { LEFTBUTTON: 0, // the event.button value for left button RIGHTBUTTON: 2, // the event.button value for right button MENUKEY: "altKey", // the event value for alternate context menu Mousedown: function (event) {return EVENT.Handler(event,"Mousedown",this)}, Mouseup: function (event) {return EVENT.Handler(event,"Mouseup",this)}, Mousemove: function (event) {return EVENT.Handler(event,"Mousemove",this)}, Mouseover: function (event) {return EVENT.Handler(event,"Mouseover",this)}, Mouseout: function (event) {return EVENT.Handler(event,"Mouseout",this)}, Click: function (event) {return EVENT.Handler(event,"Click",this)}, DblClick: function (event) {return EVENT.Handler(event,"DblClick",this)}, Menu: function (event) {return EVENT.Handler(event,"ContextMenu",this)}, // // Call the output jax's event handler or the zoom handler // Handler: function (event,type,math) { if (AJAX.loadingMathMenu) {return EVENT.False(event)} var jax = OUTPUT[math.jaxID]; if (!event) {event = window.event} event.isContextMenu = (type === "ContextMenu"); if (jax[type]) {return jax[type](event,math)} if (EXTENSION.MathZoom) {return EXTENSION.MathZoom.HandleEvent(event,type,math)} }, // // Try to cancel the event in every way we can // False: function (event) { if (!event) {event = window.event} if (event) { if (event.preventDefault) {event.preventDefault()} if (event.stopPropagation) {event.stopPropagation()} event.cancelBubble = true; event.returnValue = false; } return false; }, // // Load the contextual menu code, if needed, and post the menu // ContextMenu: function (event,math,force) { // // Check if we are showing menus // var JAX = OUTPUT[math.jaxID], jax = JAX.getJaxFromMath(math); var show = (JAX.config.showMathMenu != null ? JAX : HUB).config.showMathMenu; if (!show || (SETTINGS.context !== "MathJax" && !force)) return; // // Remove selections, remove hover fades // if (ME.msieEventBug) {event = window.event || event} EVENT.ClearSelection(); HOVER.ClearHoverTimer(); if (jax.hover) { if (jax.hover.remove) {clearTimeout(jax.hover.remove); delete jax.hover.remove} jax.hover.nofade = true; } // // If the menu code is loaded, // Check if localization needs loading; // If not, post the menu, and return. // Otherwise wait for the localization to load // Otherwse load the menu code. // Try again after the file is loaded. // var MENU = MathJax.Menu; var load, fn; if (MENU) { if (MENU.loadingDomain) {return EVENT.False(event)} load = LOCALE.loadDomain("MathMenu"); if (!load) { MENU.jax = jax; var source = MENU.menu.Find("Show Math As").menu; source.items[0].name = jax.sourceMenuTitle; source.items[0].format = (jax.sourceMenuFormat||"MathML"); source.items[1].name = INPUT[jax.inputJax].sourceMenuTitle; var MathPlayer = MENU.menu.Find("Math Settings","MathPlayer"); MathPlayer.hidden = !(jax.outputJax === "NativeMML" && HUB.Browser.hasMathPlayer); return MENU.menu.Post(event); } MENU.loadingDomain = true; fn = function () {delete MENU.loadingDomain}; } else { if (AJAX.loadingMathMenu) {return EVENT.False(event)} AJAX.loadingMathMenu = true; load = AJAX.Require("[MathJax]/extensions/MathMenu.js"); fn = function () { delete AJAX.loadingMathMenu; if (!MathJax.Menu) {MathJax.Menu = {}} } } var ev = { pageX:event.pageX, pageY:event.pageY, clientX:event.clientX, clientY:event.clientY }; CALLBACK.Queue( load, fn, // load the file and delete the marker when done ["ContextMenu",EVENT,ev,math,force] // call this function again ); return EVENT.False(event); }, // // Mousedown handler for alternate means of accessing menu // AltContextMenu: function (event,math) { var JAX = OUTPUT[math.jaxID]; var show = (JAX.config.showMathMenu != null ? JAX : HUB).config.showMathMenu; if (show) { show = (JAX.config.showMathMenuMSIE != null ? JAX : HUB).config.showMathMenuMSIE; if (SETTINGS.context === "MathJax" && !SETTINGS.mpContext && show) { if (!ME.noContextMenuBug || event.button !== EVENT.RIGHTBUTTON) return; } else { if (!event[EVENT.MENUKEY] || event.button !== EVENT.LEFTBUTTON) return; } return JAX.ContextMenu(event,math,true); } }, ClearSelection: function () { if (ME.safariContextMenuBug) {setTimeout("window.getSelection().empty()",0)} if (document.selection) {setTimeout("document.selection.empty()",0)} }, getBBox: function (span) { span.appendChild(ME.topImg); var h = ME.topImg.offsetTop, d = span.offsetHeight-h, w = span.offsetWidth; span.removeChild(ME.topImg); return {w:w, h:h, d:d}; } }; // // Handle hover "discoverability" // var HOVER = ME.Hover = { // // Check if we are moving from a non-MathJax element to a MathJax one // and either start fading in again (if it is fading out) or start the // timer for the hover // Mouseover: function (event,math) { if (SETTINGS.discoverable || SETTINGS.zoom === "Hover") { var from = event.fromElement || event.relatedTarget, to = event.toElement || event.target; if (from && to && (from.isMathJax != to.isMathJax || HUB.getJaxFor(from) !== HUB.getJaxFor(to))) { var jax = this.getJaxFromMath(math); if (jax.hover) {HOVER.ReHover(jax)} else {HOVER.HoverTimer(jax,math)} return EVENT.False(event); } } }, // // Check if we are moving from a MathJax element to a non-MathJax one // and either start fading out, or clear the timer if we haven't // hovered yet // Mouseout: function (event,math) { if (SETTINGS.discoverable || SETTINGS.zoom === "Hover") { var from = event.fromElement || event.relatedTarget, to = event.toElement || event.target; if (from && to && (from.isMathJax != to.isMathJax || HUB.getJaxFor(from) !== HUB.getJaxFor(to))) { var jax = this.getJaxFromMath(math); if (jax.hover) {HOVER.UnHover(jax)} else {HOVER.ClearHoverTimer()} return EVENT.False(event); } } }, // // Restart hover timer if the mouse moves // Mousemove: function (event,math) { if (SETTINGS.discoverable || SETTINGS.zoom === "Hover") { var jax = this.getJaxFromMath(math); if (jax.hover) return; if (HOVER.lastX == event.clientX && HOVER.lastY == event.clientY) return; HOVER.lastX = event.clientX; HOVER.lastY = event.clientY; HOVER.HoverTimer(jax,math); return EVENT.False(event); } }, // // Clear the old timer and start a new one // HoverTimer: function (jax,math) { this.ClearHoverTimer(); this.hoverTimer = setTimeout(CALLBACK(["Hover",this,jax,math]),CONFIG.hover); }, ClearHoverTimer: function () { if (this.hoverTimer) {clearTimeout(this.hoverTimer); delete this.hoverTimer} }, // // Handle putting up the hover frame // Hover: function (jax,math) { // // Check if Zoom handles the hover event // if (EXTENSION.MathZoom && EXTENSION.MathZoom.Hover({},math)) return; // // Get the hover data // var JAX = OUTPUT[jax.outputJax], span = JAX.getHoverSpan(jax,math), bbox = JAX.getHoverBBox(jax,span,math), show = (JAX.config.showMathMenu != null ? JAX : HUB).config.showMathMenu; var dx = CONFIG.frame.x, dy = CONFIG.frame.y, dd = CONFIG.frame.bwidth; // frame size if (ME.msieBorderWidthBug) {dd = 0} jax.hover = {opacity:0, id:jax.inputID+"-Hover"}; // // The frame and menu button // var frame = HTML.Element("span",{ id:jax.hover.id, isMathJax: true, style:{display:"inline-block", width:0, height:0, position:"relative"} },[["span",{ className:"MathJax_Hover_Frame", isMathJax: true, style:{ display:"inline-block", position:"absolute", top:this.Px(-bbox.h-dy-dd-(bbox.y||0)), left:this.Px(-dx-dd+(bbox.x||0)), width:this.Px(bbox.w+2*dx), height:this.Px(bbox.h+bbox.d+2*dy), opacity:0, filter:"alpha(opacity=0)" }} ]] ); var button = HTML.Element("span",{ isMathJax: true, id:jax.hover.id+"Menu", style:{display:"inline-block", "z-index": 1, width:0, height:0, position:"relative"} },[["img",{ className: "MathJax_Hover_Arrow", isMathJax: true, math: math, src: CONFIG.button.src, onclick: this.HoverMenu, jax:JAX.id, style: { left:this.Px(bbox.w+dx+dd+(bbox.x||0)+CONFIG.button.x), top:this.Px(-bbox.h-dy-dd-(bbox.y||0)-CONFIG.button.y), opacity:0, filter:"alpha(opacity=0)" } }]] ); if (bbox.width) { frame.style.width = button.style.width = bbox.width; frame.style.marginRight = button.style.marginRight = "-"+bbox.width; frame.firstChild.style.width = bbox.width; button.firstChild.style.left = ""; button.firstChild.style.right = this.Px(CONFIG.button.wx); } // // Add the frame and button // span.parentNode.insertBefore(frame,span); if (show) {span.parentNode.insertBefore(button,span)} if (span.style) {span.style.position = "relative"} // so math is on top of hover frame // // Start the hover fade-in // this.ReHover(jax); }, // // Restart the hover fade in and fade-out timers // ReHover: function (jax) { if (jax.hover.remove) {clearTimeout(jax.hover.remove)} jax.hover.remove = setTimeout(CALLBACK(["UnHover",this,jax]),CONFIG.fadeoutDelay); this.HoverFadeTimer(jax,CONFIG.fadeinInc); }, // // Start the fade-out // UnHover: function (jax) { if (!jax.hover.nofade) {this.HoverFadeTimer(jax,-CONFIG.fadeoutInc,CONFIG.fadeoutStart)} }, // // Handle the fade-in and fade-out // HoverFade: function (jax) { delete jax.hover.timer; jax.hover.opacity = Math.max(0,Math.min(1,jax.hover.opacity + jax.hover.inc)); jax.hover.opacity = Math.floor(1000*jax.hover.opacity)/1000; var frame = document.getElementById(jax.hover.id), button = document.getElementById(jax.hover.id+"Menu"); frame.firstChild.style.opacity = jax.hover.opacity; frame.firstChild.style.filter = "alpha(opacity="+Math.floor(100*jax.hover.opacity)+")"; if (button) { button.firstChild.style.opacity = jax.hover.opacity; button.firstChild.style.filter = frame.style.filter; } if (jax.hover.opacity === 1) {return} if (jax.hover.opacity > 0) {this.HoverFadeTimer(jax,jax.hover.inc); return} frame.parentNode.removeChild(frame); if (button) {button.parentNode.removeChild(button)} if (jax.hover.remove) {clearTimeout(jax.hover.remove)} delete jax.hover; }, // // Set the fade to in or out (via inc) and start the timer, if needed // HoverFadeTimer: function (jax,inc,delay) { jax.hover.inc = inc; if (!jax.hover.timer) { jax.hover.timer = setTimeout(CALLBACK(["HoverFade",this,jax]),(delay||CONFIG.fadeDelay)); } }, // // Handle a click on the menu button // HoverMenu: function (event) { if (!event) {event = window.event} return OUTPUT[this.jax].ContextMenu(event,this.math,true); }, // // Clear all hover timers // ClearHover: function (jax) { if (jax.hover.remove) {clearTimeout(jax.hover.remove)} if (jax.hover.timer) {clearTimeout(jax.hover.timer)} HOVER.ClearHoverTimer(); delete jax.hover; }, // // Make a measurement in pixels // Px: function (m) { if (Math.abs(m) < .006) {return "0px"} return m.toFixed(2).replace(/\.?0+$/,"") + "px"; }, // // Preload images so they show up with the menu // getImages: function () { var menu = new Image(); menu.src = CONFIG.button.src; } }; // // Handle touch events. // // Use double-tap-and-hold as a replacement for context menu event. // Use double-tap as a replacement for double click. // var TOUCH = ME.Touch = { last: 0, // time of last tap event delay: 500, // delay time for double-click // // Check if this is a double-tap, and if so, start the timer // for the double-tap and hold (to trigger the contextual menu) // start: function (event) { var now = new Date().getTime(); var dblTap = (now - TOUCH.last < TOUCH.delay && TOUCH.up); TOUCH.last = now; TOUCH.up = false; if (dblTap) { TOUCH.timeout = setTimeout(TOUCH.menu,TOUCH.delay,event,this); event.preventDefault(); } }, // // Check if there is a timeout pending, i.e., we have a // double-tap and were waiting to see if it is held long // enough for the menu. Since we got the end before the // timeout, it is a double-click, not a double-tap-and-hold. // Prevent the default action and issue a double click. // end: function (event) { var now = new Date().getTime(); TOUCH.up = (now - TOUCH.last < TOUCH.delay); if (TOUCH.timeout) { clearTimeout(TOUCH.timeout); delete TOUCH.timeout; TOUCH.last = 0; TOUCH.up = false; event.preventDefault(); return EVENT.Handler((event.touches[0]||event.touch),"DblClick",this); } }, // // If the timeout passes without an end event, we issue // the contextual menu event. // menu: function (event,math) { delete TOUCH.timeout; TOUCH.last = 0; TOUCH.up = false; return EVENT.Handler((event.touches[0]||event.touch),"ContextMenu",math); } }; // // Mobile screens are small, so use larger version of arrow // if (HUB.Browser.isMobile) { var arrow = CONFIG.styles[".MathJax_Hover_Arrow"]; arrow.width = "25px"; arrow.height = "18px"; CONFIG.button.x = -6; } // // Set up browser-specific values // HUB.Browser.Select({ MSIE: function (browser) { var mode = (document.documentMode || 0); var isIE8 = browser.versionAtLeast("8.0"); ME.msieBorderWidthBug = (document.compatMode === "BackCompat"); // borders are inside offsetWidth/Height ME.msieEventBug = browser.isIE9; // must get event from window even though event is passed ME.msieAlignBug = (!isIE8 || mode < 8); // inline-block spans don't rest on baseline if (mode < 9) {EVENT.LEFTBUTTON = 1} // IE < 9 has wrong event.button values }, Safari: function (browser) { ME.safariContextMenuBug = true; // selection can be started by contextmenu event }, Opera: function (browser) { ME.operaPositionBug = true; // position is wrong unless border is used }, Konqueror: function (browser) { ME.noContextMenuBug = true; // doesn't produce contextmenu event } }); // // Used in measuring zoom and hover positions // ME.topImg = (ME.msieAlignBug ? HTML.Element("img",{style:{width:0,height:0,position:"relative"},src:"about:blank"}) : HTML.Element("span",{style:{width:0,height:0,display:"inline-block"}}) ); if (ME.operaPositionBug) {ME.topImg.style.border="1px solid"} // // Get configuration from user // ME.config = CONFIG = HUB.CombineConfig("MathEvents",CONFIG); var SETFRAME = function () { var haze = CONFIG.styles[".MathJax_Hover_Frame"]; haze.border = CONFIG.frame.bwidth+"px solid "+CONFIG.frame.bcolor+" ! important"; haze["box-shadow"] = haze["-webkit-box-shadow"] = haze["-moz-box-shadow"] = haze["-khtml-box-shadow"] = "0px 0px "+CONFIG.frame.hwidth+" "+CONFIG.frame.hcolor; }; // // Queue the events needed for startup // CALLBACK.Queue( HUB.Register.StartupHook("End Config",{}), // wait until config is complete [SETFRAME], ["getImages",HOVER], ["Styles",AJAX,CONFIG.styles], ["Post",HUB.Startup.signal,"MathEvents Ready"], ["loadComplete",AJAX,"[MathJax]/extensions/MathEvents.js"] ); })(MathJax.Hub,MathJax.HTML,MathJax.Ajax,MathJax.Callback, MathJax.Localization,MathJax.OutputJax,MathJax.InputJax);