MathEvents.js (22300B)
1 /* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80: */ 3 4 /************************************************************* 5 * 6 * MathJax/extensions/MathEvents.js 7 * 8 * Implements the event handlers needed by the output jax to perform 9 * menu, hover, and other events. 10 * 11 * --------------------------------------------------------------------- 12 * 13 * Copyright (c) 2011-2015 The MathJax Consortium 14 * 15 * Licensed under the Apache License, Version 2.0 (the "License"); 16 * you may not use this file except in compliance with the License. 17 * You may obtain a copy of the License at 18 * 19 * http://www.apache.org/licenses/LICENSE-2.0 20 * 21 * Unless required by applicable law or agreed to in writing, software 22 * distributed under the License is distributed on an "AS IS" BASIS, 23 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 * See the License for the specific language governing permissions and 25 * limitations under the License. 26 */ 27 28 (function (HUB,HTML,AJAX,CALLBACK,LOCALE,OUTPUT,INPUT) { 29 var VERSION = "2.6.0"; 30 31 var EXTENSION = MathJax.Extension; 32 var ME = EXTENSION.MathEvents = {version: VERSION}; 33 34 var SETTINGS = HUB.config.menuSettings; 35 36 var CONFIG = { 37 hover: 500, // time required to be considered a hover 38 frame: { 39 x: 3.5, y: 5, // frame padding and 40 bwidth: 1, // frame border width (in pixels) 41 bcolor: "#A6D", // frame border color 42 hwidth: "15px", // haze width 43 hcolor: "#83A" // haze color 44 }, 45 button: { 46 x: -6, y: -3, // menu button offsets 47 wx: -2 // button offset for full-width equations 48 }, 49 fadeinInc: .2, // increment for fade-in 50 fadeoutInc: .05, // increment for fade-out 51 fadeDelay: 50, // delay between fade-in or fade-out steps 52 fadeoutStart: 400, // delay before fade-out after mouseout 53 fadeoutDelay: 15*1000, // delay before automatic fade-out 54 55 styles: { 56 ".MathJax_Hover_Frame": { 57 "border-radius": ".25em", // Opera 10.5 and IE9 58 "-webkit-border-radius": ".25em", // Safari and Chrome 59 "-moz-border-radius": ".25em", // Firefox 60 "-khtml-border-radius": ".25em", // Konqueror 61 62 "box-shadow": "0px 0px 15px #83A", // Opera 10.5 and IE9 63 "-webkit-box-shadow": "0px 0px 15px #83A", // Safari and Chrome 64 "-moz-box-shadow": "0px 0px 15px #83A", // Forefox 65 "-khtml-box-shadow": "0px 0px 15px #83A", // Konqueror 66 67 border: "1px solid #A6D ! important", 68 display: "inline-block", position:"absolute" 69 }, 70 71 ".MathJax_Menu_Button .MathJax_Hover_Arrow": { 72 position:"absolute", 73 cursor:"pointer", 74 display:"inline-block", 75 border:"2px solid #AAA", 76 "border-radius":"4px", 77 "-webkit-border-radius": "4px", // Safari and Chrome 78 "-moz-border-radius": "4px", // Firefox 79 "-khtml-border-radius": "4px", // Konqueror 80 "font-family":"'Courier New',Courier", 81 "font-size":"9px", 82 color:"#F0F0F0" 83 }, 84 ".MathJax_Menu_Button .MathJax_Hover_Arrow span": { 85 display:"block", 86 "background-color":"#AAA", 87 border:"1px solid", 88 "border-radius":"3px", 89 "line-height":0, 90 padding:"4px" 91 }, 92 ".MathJax_Hover_Arrow:hover": { 93 color:"white!important", 94 border:"2px solid #CCC!important" 95 }, 96 ".MathJax_Hover_Arrow:hover span": { 97 "background-color":"#CCC!important" 98 } 99 } 100 }; 101 102 103 // 104 // Common event-handling code 105 // 106 var EVENT = ME.Event = { 107 108 LEFTBUTTON: 0, // the event.button value for left button 109 RIGHTBUTTON: 2, // the event.button value for right button 110 MENUKEY: "altKey", // the event value for alternate context menu 111 112 /*************************************************************/ 113 /* 114 * Enum element for key codes. 115 */ 116 KEY: { 117 RETURN: 13, 118 ESCAPE: 27, 119 SPACE: 32, 120 LEFT: 37, 121 UP: 38, 122 RIGHT: 39, 123 DOWN: 40 124 }, 125 126 Mousedown: function (event) {return EVENT.Handler(event,"Mousedown",this)}, 127 Mouseup: function (event) {return EVENT.Handler(event,"Mouseup",this)}, 128 Mousemove: function (event) {return EVENT.Handler(event,"Mousemove",this)}, 129 Mouseover: function (event) {return EVENT.Handler(event,"Mouseover",this)}, 130 Mouseout: function (event) {return EVENT.Handler(event,"Mouseout",this)}, 131 Click: function (event) {return EVENT.Handler(event,"Click",this)}, 132 DblClick: function (event) {return EVENT.Handler(event,"DblClick",this)}, 133 Menu: function (event) {return EVENT.Handler(event,"ContextMenu",this)}, 134 135 // 136 // Call the output jax's event handler or the zoom handler 137 // 138 Handler: function (event,type,math) { 139 if (AJAX.loadingMathMenu) {return EVENT.False(event)} 140 var jax = OUTPUT[math.jaxID]; 141 if (!event) {event = window.event} 142 event.isContextMenu = (type === "ContextMenu"); 143 if (jax[type]) {return jax[type](event,math)} 144 if (EXTENSION.MathZoom) {return EXTENSION.MathZoom.HandleEvent(event,type,math)} 145 }, 146 147 // 148 // Try to cancel the event in every way we can 149 // 150 False: function (event) { 151 if (!event) {event = window.event} 152 if (event) { 153 if (event.preventDefault) {event.preventDefault()} else {event.returnValue = false} 154 if (event.stopPropagation) {event.stopPropagation()} 155 event.cancelBubble = true; 156 } 157 return false; 158 }, 159 160 // 161 // Keydown event handler. Should only fire on Space key. 162 // 163 Keydown: function (event, math) { 164 if (!event) event = window.event; 165 if (event.keyCode === EVENT.KEY.SPACE) { 166 EVENT.ContextMenu(event, this); 167 }; 168 }, 169 170 // 171 // Load the contextual menu code, if needed, and post the menu 172 // 173 ContextMenu: function (event,math,force) { 174 // 175 // Check if we are showing menus 176 // 177 var JAX = OUTPUT[math.jaxID], jax = JAX.getJaxFromMath(math); 178 var show = (JAX.config.showMathMenu != null ? JAX : HUB).config.showMathMenu; 179 if (!show || (SETTINGS.context !== "MathJax" && !force)) return; 180 181 // 182 // Remove selections, remove hover fades 183 // 184 if (ME.msieEventBug) {event = window.event || event} 185 EVENT.ClearSelection(); HOVER.ClearHoverTimer(); 186 if (jax.hover) { 187 if (jax.hover.remove) {clearTimeout(jax.hover.remove); delete jax.hover.remove} 188 jax.hover.nofade = true; 189 } 190 191 // 192 // If the menu code is loaded, 193 // Check if localization needs loading; 194 // If not, post the menu, and return. 195 // Otherwise wait for the localization to load 196 // Otherwse load the menu code. 197 // Try again after the file is loaded. 198 // 199 var MENU = MathJax.Menu; var load, fn; 200 if (MENU) { 201 if (MENU.loadingDomain) {return EVENT.False(event)} 202 load = LOCALE.loadDomain("MathMenu"); 203 if (!load) { 204 MENU.jax = jax; 205 var source = MENU.menu.Find("Show Math As").submenu; 206 source.items[0].name = jax.sourceMenuTitle; 207 source.items[0].format = (jax.sourceMenuFormat||"MathML"); 208 source.items[1].name = INPUT[jax.inputJax].sourceMenuTitle; 209 source.items[5].disabled = !INPUT[jax.inputJax].annotationEncoding; 210 211 // 212 // Try and find each known annotation format and enable the menu 213 // items accordingly. 214 // 215 var annotations = source.items[2]; annotations.disabled = true; 216 var annotationItems = annotations.submenu.items; 217 annotationList = MathJax.Hub.Config.semanticsAnnotations; 218 for (var i = 0, m = annotationItems.length; i < m; i++) { 219 var name = annotationItems[i].name[1] 220 if (jax.root && jax.root.getAnnotation(name) !== null) { 221 annotations.disabled = false; 222 annotationItems[i].hidden = false; 223 } else { 224 annotationItems[i].hidden = true; 225 } 226 } 227 228 var MathPlayer = MENU.menu.Find("Math Settings","MathPlayer"); 229 MathPlayer.hidden = !(jax.outputJax === "NativeMML" && HUB.Browser.hasMathPlayer); 230 return MENU.menu.Post(event); 231 } 232 MENU.loadingDomain = true; 233 fn = function () {delete MENU.loadingDomain}; 234 } else { 235 if (AJAX.loadingMathMenu) {return EVENT.False(event)} 236 AJAX.loadingMathMenu = true; 237 load = AJAX.Require("[MathJax]/extensions/MathMenu.js"); 238 fn = function () { 239 delete AJAX.loadingMathMenu; 240 if (!MathJax.Menu) {MathJax.Menu = {}} 241 } 242 } 243 var ev = { 244 pageX:event.pageX, pageY:event.pageY, 245 clientX:event.clientX, clientY:event.clientY 246 }; 247 CALLBACK.Queue( 248 load, fn, // load the file and delete the marker when done 249 ["ContextMenu",EVENT,ev,math,force] // call this function again 250 ); 251 return EVENT.False(event); 252 }, 253 254 // 255 // Mousedown handler for alternate means of accessing menu 256 // 257 AltContextMenu: function (event,math) { 258 var JAX = OUTPUT[math.jaxID]; 259 var show = (JAX.config.showMathMenu != null ? JAX : HUB).config.showMathMenu; 260 if (show) { 261 show = (JAX.config.showMathMenuMSIE != null ? JAX : HUB).config.showMathMenuMSIE; 262 if (SETTINGS.context === "MathJax" && !SETTINGS.mpContext && show) { 263 if (!ME.noContextMenuBug || event.button !== EVENT.RIGHTBUTTON) return; 264 } else { 265 if (!event[EVENT.MENUKEY] || event.button !== EVENT.LEFTBUTTON) return; 266 } 267 return JAX.ContextMenu(event,math,true); 268 } 269 }, 270 271 ClearSelection: function () { 272 if (ME.safariContextMenuBug) {setTimeout("window.getSelection().empty()",0)} 273 if (document.selection) {setTimeout("document.selection.empty()",0)} 274 }, 275 276 getBBox: function (span) { 277 span.appendChild(ME.topImg); 278 var h = ME.topImg.offsetTop, d = span.offsetHeight-h, w = span.offsetWidth; 279 span.removeChild(ME.topImg); 280 return {w:w, h:h, d:d}; 281 } 282 283 }; 284 285 // 286 // Handle hover "discoverability" 287 // 288 var HOVER = ME.Hover = { 289 290 // 291 // Check if we are moving from a non-MathJax element to a MathJax one 292 // and either start fading in again (if it is fading out) or start the 293 // timer for the hover 294 // 295 Mouseover: function (event,math) { 296 if (SETTINGS.discoverable || SETTINGS.zoom === "Hover") { 297 var from = event.fromElement || event.relatedTarget, 298 to = event.toElement || event.target; 299 if (from && to && (HUB.isMathJaxNode(from) !== HUB.isMathJaxNode(to) || 300 HUB.getJaxFor(from) !== HUB.getJaxFor(to))) { 301 var jax = this.getJaxFromMath(math); 302 if (jax.hover) {HOVER.ReHover(jax)} else {HOVER.HoverTimer(jax,math)} 303 return EVENT.False(event); 304 } 305 } 306 }, 307 // 308 // Check if we are moving from a MathJax element to a non-MathJax one 309 // and either start fading out, or clear the timer if we haven't 310 // hovered yet 311 // 312 Mouseout: function (event,math) { 313 if (SETTINGS.discoverable || SETTINGS.zoom === "Hover") { 314 var from = event.fromElement || event.relatedTarget, 315 to = event.toElement || event.target; 316 if (from && to && (HUB.isMathJaxNode(from) !== HUB.isMathJaxNode(to) || 317 HUB.getJaxFor(from) !== HUB.getJaxFor(to))) { 318 var jax = this.getJaxFromMath(math); 319 if (jax.hover) {HOVER.UnHover(jax)} else {HOVER.ClearHoverTimer()} 320 return EVENT.False(event); 321 } 322 } 323 }, 324 // 325 // Restart hover timer if the mouse moves 326 // 327 Mousemove: function (event,math) { 328 if (SETTINGS.discoverable || SETTINGS.zoom === "Hover") { 329 var jax = this.getJaxFromMath(math); if (jax.hover) return; 330 if (HOVER.lastX == event.clientX && HOVER.lastY == event.clientY) return; 331 HOVER.lastX = event.clientX; HOVER.lastY = event.clientY; 332 HOVER.HoverTimer(jax,math); 333 return EVENT.False(event); 334 } 335 }, 336 337 // 338 // Clear the old timer and start a new one 339 // 340 HoverTimer: function (jax,math) { 341 this.ClearHoverTimer(); 342 this.hoverTimer = setTimeout(CALLBACK(["Hover",this,jax,math]),CONFIG.hover); 343 }, 344 ClearHoverTimer: function () { 345 if (this.hoverTimer) {clearTimeout(this.hoverTimer); delete this.hoverTimer} 346 }, 347 348 // 349 // Handle putting up the hover frame 350 // 351 Hover: function (jax,math) { 352 // 353 // Check if Zoom handles the hover event 354 // 355 if (EXTENSION.MathZoom && EXTENSION.MathZoom.Hover({},math)) return; 356 // 357 // Get the hover data 358 // 359 var JAX = OUTPUT[jax.outputJax], 360 span = JAX.getHoverSpan(jax,math), 361 bbox = JAX.getHoverBBox(jax,span,math), 362 show = (JAX.config.showMathMenu != null ? JAX : HUB).config.showMathMenu; 363 var dx = CONFIG.frame.x, dy = CONFIG.frame.y, dd = CONFIG.frame.bwidth; // frame size 364 if (ME.msieBorderWidthBug) {dd = 0} 365 jax.hover = {opacity:0, id:jax.inputID+"-Hover"}; 366 // 367 // The frame and menu button 368 // 369 var frame = HTML.Element("span",{ 370 id:jax.hover.id, isMathJax: true, 371 style:{display:"inline-block", width:0, height:0, position:"relative"} 372 },[["span",{ 373 className:"MathJax_Hover_Frame", isMathJax: true, 374 style:{ 375 display:"inline-block", position:"absolute", 376 top:this.Px(-bbox.h-dy-dd-(bbox.y||0)), left:this.Px(-dx-dd+(bbox.x||0)), 377 width:this.Px(bbox.w+2*dx), height:this.Px(bbox.h+bbox.d+2*dy), 378 opacity:0, filter:"alpha(opacity=0)" 379 }} 380 ]] 381 ); 382 var button = HTML.Element("span",{ 383 isMathJax: true, id:jax.hover.id+"Menu", className:"MathJax_Menu_Button", 384 style:{display:"inline-block", "z-index": 1, width:0, height:0, position:"relative"} 385 },[["span",{ 386 className: "MathJax_Hover_Arrow", isMathJax: true, math: math, 387 onclick: this.HoverMenu, jax:JAX.id, 388 style: { 389 left:this.Px(bbox.w+dx+dd+(bbox.x||0)+CONFIG.button.x), 390 top:this.Px(-bbox.h-dy-dd-(bbox.y||0)-CONFIG.button.y), 391 opacity:0, filter:"alpha(opacity=0)" 392 } 393 },[["span",{isMathJax:true},"\u25BC"]]]] 394 ); 395 if (bbox.width) { 396 frame.style.width = button.style.width = bbox.width; 397 frame.style.marginRight = button.style.marginRight = "-"+bbox.width; 398 frame.firstChild.style.width = bbox.width; 399 button.firstChild.style.left = ""; 400 button.firstChild.style.right = this.Px(CONFIG.button.wx); 401 } 402 // 403 // Add the frame and button 404 // 405 span.parentNode.insertBefore(frame,span); 406 if (show) {span.parentNode.insertBefore(button,span)} 407 if (span.style) {span.style.position = "relative"} // so math is on top of hover frame 408 // 409 // Start the hover fade-in 410 // 411 this.ReHover(jax); 412 }, 413 // 414 // Restart the hover fade in and fade-out timers 415 // 416 ReHover: function (jax) { 417 if (jax.hover.remove) {clearTimeout(jax.hover.remove)} 418 jax.hover.remove = setTimeout(CALLBACK(["UnHover",this,jax]),CONFIG.fadeoutDelay); 419 this.HoverFadeTimer(jax,CONFIG.fadeinInc); 420 }, 421 // 422 // Start the fade-out 423 // 424 UnHover: function (jax) { 425 if (!jax.hover.nofade) {this.HoverFadeTimer(jax,-CONFIG.fadeoutInc,CONFIG.fadeoutStart)} 426 }, 427 // 428 // Handle the fade-in and fade-out 429 // 430 HoverFade: function (jax) { 431 delete jax.hover.timer; 432 jax.hover.opacity = Math.max(0,Math.min(1,jax.hover.opacity + jax.hover.inc)); 433 jax.hover.opacity = Math.floor(1000*jax.hover.opacity)/1000; 434 var frame = document.getElementById(jax.hover.id), 435 button = document.getElementById(jax.hover.id+"Menu"); 436 frame.firstChild.style.opacity = jax.hover.opacity; 437 frame.firstChild.style.filter = "alpha(opacity="+Math.floor(100*jax.hover.opacity)+")"; 438 if (button) { 439 button.firstChild.style.opacity = jax.hover.opacity; 440 button.firstChild.style.filter = frame.style.filter; 441 } 442 if (jax.hover.opacity === 1) {return} 443 if (jax.hover.opacity > 0) {this.HoverFadeTimer(jax,jax.hover.inc); return} 444 frame.parentNode.removeChild(frame); 445 if (button) {button.parentNode.removeChild(button)} 446 if (jax.hover.remove) {clearTimeout(jax.hover.remove)} 447 delete jax.hover; 448 }, 449 // 450 // Set the fade to in or out (via inc) and start the timer, if needed 451 // 452 HoverFadeTimer: function (jax,inc,delay) { 453 jax.hover.inc = inc; 454 if (!jax.hover.timer) { 455 jax.hover.timer = setTimeout(CALLBACK(["HoverFade",this,jax]),(delay||CONFIG.fadeDelay)); 456 } 457 }, 458 459 // 460 // Handle a click on the menu button 461 // 462 HoverMenu: function (event) { 463 if (!event) {event = window.event} 464 return OUTPUT[this.jax].ContextMenu(event,this.math,true); 465 }, 466 467 // 468 // Clear all hover timers 469 // 470 ClearHover: function (jax) { 471 if (jax.hover.remove) {clearTimeout(jax.hover.remove)} 472 if (jax.hover.timer) {clearTimeout(jax.hover.timer)} 473 HOVER.ClearHoverTimer(); 474 delete jax.hover; 475 }, 476 477 // 478 // Make a measurement in pixels 479 // 480 Px: function (m) { 481 if (Math.abs(m) < .006) {return "0px"} 482 return m.toFixed(2).replace(/\.?0+$/,"") + "px"; 483 }, 484 485 // 486 // Preload images so they show up with the menu 487 // 488 getImages: function () { 489 if (SETTINGS.discoverable) { 490 var menu = new Image(); 491 menu.src = CONFIG.button.src; 492 } 493 } 494 495 }; 496 497 // 498 // Handle touch events. 499 // 500 // Use double-tap-and-hold as a replacement for context menu event. 501 // Use double-tap as a replacement for double click. 502 // 503 var TOUCH = ME.Touch = { 504 505 last: 0, // time of last tap event 506 delay: 500, // delay time for double-click 507 508 // 509 // Check if this is a double-tap, and if so, start the timer 510 // for the double-tap and hold (to trigger the contextual menu) 511 // 512 start: function (event) { 513 var now = new Date().getTime(); 514 var dblTap = (now - TOUCH.last < TOUCH.delay && TOUCH.up); 515 TOUCH.last = now; TOUCH.up = false; 516 if (dblTap) { 517 TOUCH.timeout = setTimeout(TOUCH.menu,TOUCH.delay,event,this); 518 event.preventDefault(); 519 } 520 }, 521 522 // 523 // Check if there is a timeout pending, i.e., we have a 524 // double-tap and were waiting to see if it is held long 525 // enough for the menu. Since we got the end before the 526 // timeout, it is a double-click, not a double-tap-and-hold. 527 // Prevent the default action and issue a double click. 528 // 529 end: function (event) { 530 var now = new Date().getTime(); 531 TOUCH.up = (now - TOUCH.last < TOUCH.delay); 532 if (TOUCH.timeout) { 533 clearTimeout(TOUCH.timeout); 534 delete TOUCH.timeout; TOUCH.last = 0; TOUCH.up = false; 535 event.preventDefault(); 536 return EVENT.Handler((event.touches[0]||event.touch),"DblClick",this); 537 } 538 }, 539 540 // 541 // If the timeout passes without an end event, we issue 542 // the contextual menu event. 543 // 544 menu: function (event,math) { 545 delete TOUCH.timeout; TOUCH.last = 0; TOUCH.up = false; 546 return EVENT.Handler((event.touches[0]||event.touch),"ContextMenu",math); 547 } 548 549 }; 550 551 /* 552 * // 553 * // Mobile screens are small, so use larger version of arrow 554 * // 555 * if (HUB.Browser.isMobile) { 556 * var arrow = CONFIG.styles[".MathJax_Hover_Arrow"]; 557 * arrow.width = "25px"; arrow.height = "18px"; 558 * CONFIG.button.x = -6; 559 * } 560 */ 561 562 // 563 // Set up browser-specific values 564 // 565 HUB.Browser.Select({ 566 MSIE: function (browser) { 567 var mode = (document.documentMode || 0); 568 var isIE8 = browser.versionAtLeast("8.0"); 569 ME.msieBorderWidthBug = (document.compatMode === "BackCompat"); // borders are inside offsetWidth/Height 570 ME.msieEventBug = browser.isIE9; // must get event from window even though event is passed 571 ME.msieAlignBug = (!isIE8 || mode < 8); // inline-block spans don't rest on baseline 572 if (mode < 9) {EVENT.LEFTBUTTON = 1} // IE < 9 has wrong event.button values 573 }, 574 Safari: function (browser) { 575 ME.safariContextMenuBug = true; // selection can be started by contextmenu event 576 }, 577 Opera: function (browser) { 578 ME.operaPositionBug = true; // position is wrong unless border is used 579 }, 580 Konqueror: function (browser) { 581 ME.noContextMenuBug = true; // doesn't produce contextmenu event 582 } 583 }); 584 585 // 586 // Used in measuring zoom and hover positions 587 // 588 ME.topImg = (ME.msieAlignBug ? 589 HTML.Element("img",{style:{width:0,height:0,position:"relative"},src:"about:blank"}) : 590 HTML.Element("span",{style:{width:0,height:0,display:"inline-block"}}) 591 ); 592 if (ME.operaPositionBug) {ME.topImg.style.border="1px solid"} 593 594 // 595 // Get configuration from user 596 // 597 ME.config = CONFIG = HUB.CombineConfig("MathEvents",CONFIG); 598 var SETFRAME = function () { 599 var haze = CONFIG.styles[".MathJax_Hover_Frame"]; 600 haze.border = CONFIG.frame.bwidth+"px solid "+CONFIG.frame.bcolor+" ! important"; 601 haze["box-shadow"] = haze["-webkit-box-shadow"] = 602 haze["-moz-box-shadow"] = haze["-khtml-box-shadow"] = 603 "0px 0px "+CONFIG.frame.hwidth+" "+CONFIG.frame.hcolor; 604 }; 605 606 // 607 // Queue the events needed for startup 608 // 609 CALLBACK.Queue( 610 HUB.Register.StartupHook("End Config",{}), // wait until config is complete 611 [SETFRAME], 612 ["getImages",HOVER], 613 ["Styles",AJAX,CONFIG.styles], 614 ["Post",HUB.Startup.signal,"MathEvents Ready"], 615 ["loadComplete",AJAX,"[MathJax]/extensions/MathEvents.js"] 616 ); 617 618 })(MathJax.Hub,MathJax.HTML,MathJax.Ajax,MathJax.Callback, 619 MathJax.Localization,MathJax.OutputJax,MathJax.InputJax);