1 /* 2 Copyright 2008-2016 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/> 29 and <http://opensource.org/licenses/MIT/>. 30 */ 31 32 33 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/ 34 35 /*jslint nomen: true, plusplus: true*/ 36 37 /* depends: 38 jxg 39 base/constants 40 base/coords 41 options 42 math/numerics 43 math/math 44 math/geometry 45 math/complex 46 parser/jessiecode 47 parser/geonext 48 utils/color 49 utils/type 50 utils/event 51 utils/env 52 elements: 53 transform 54 point 55 line 56 text 57 grid 58 */ 59 60 /** 61 * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods 62 * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc. 63 */ 64 65 define([ 66 'jxg', 'base/constants', 'base/coords', 'options', 'math/numerics', 'math/math', 'math/geometry', 'math/complex', 67 'math/statistics', 68 'parser/jessiecode', 'parser/geonext', 'utils/color', 'utils/type', 'utils/event', 'utils/env', 'base/transformation', 69 'base/point', 'base/line', 'base/text', 'element/composition', 'base/composition' 70 ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, GeonextParser, Color, Type, 71 EventEmitter, Env, Transform, Point, Line, Text, Composition, EComposition) { 72 73 'use strict'; 74 75 /** 76 * Constructs a new Board object. 77 * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric 78 * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly. 79 * Please use {@link JXG.JSXGraph#initBoard} to initialize a board. 80 * @constructor 81 * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div. 82 * @param {JXG.AbstractRenderer} renderer The reference of a renderer. 83 * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined. 84 * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates. 85 * @param {Number} zoomX Zoom factor in x-axis direction 86 * @param {Number} zoomY Zoom factor in y-axis direction 87 * @param {Number} unitX Units in x-axis direction 88 * @param {Number} unitY Units in y-axis direction 89 * @param {Number} canvasWidth The width of canvas 90 * @param {Number} canvasHeight The height of canvas 91 * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph#initBoard} 92 * @borrows JXG.EventEmitter#on as this.on 93 * @borrows JXG.EventEmitter#off as this.off 94 * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers 95 * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers 96 */ 97 JXG.Board = function (container, renderer, id, origin, zoomX, zoomY, unitX, unitY, canvasWidth, canvasHeight, attributes) { 98 /** 99 * Board is in no special mode, objects are highlighted on mouse over and objects may be 100 * clicked to start drag&drop. 101 * @type Number 102 * @constant 103 */ 104 this.BOARD_MODE_NONE = 0x0000; 105 106 /** 107 * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in 108 * {JXG.Board#mouse} is updated on mouse movement. 109 * @type Number 110 * @constant 111 * @see JXG.Board#drag_obj 112 */ 113 this.BOARD_MODE_DRAG = 0x0001; 114 115 /** 116 * In this mode a mouse move changes the origin's screen coordinates. 117 * @type Number 118 * @constant 119 */ 120 this.BOARD_MODE_MOVE_ORIGIN = 0x0002; 121 122 /** 123 * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points. 124 * @type Number 125 * @constant 126 * @see JXG.Board#updateQuality 127 */ 128 this.BOARD_QUALITY_LOW = 0x1; 129 130 /** 131 * Update is made with high quality, e.g. graphs are evaluated at much more points. 132 * @type Number 133 * @constant 134 * @see JXG.Board#updateQuality 135 */ 136 this.BOARD_QUALITY_HIGH = 0x2; 137 138 /** 139 * Update is made with high quality, e.g. graphs are evaluated at much more points. 140 * @type Number 141 * @constant 142 * @see JXG.Board#updateQuality 143 */ 144 this.BOARD_MODE_ZOOM = 0x0011; 145 146 /** 147 * Pointer to the document element containing the board. 148 * @type Object 149 */ 150 // Former version: 151 // this.document = attributes.document || document; 152 if (Type.exists(attributes.document) && attributes.document !== false) { 153 this.document = attributes.document; 154 } else if (typeof document !== 'undefined' && Type.isObject(document)) { 155 this.document = document; 156 } 157 158 /** 159 * The html-id of the html element containing the board. 160 * @type String 161 */ 162 this.container = container; 163 164 /** 165 * Pointer to the html element containing the board. 166 * @type Object 167 */ 168 this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null); 169 170 if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) { 171 throw new Error("\nJSXGraph: HTML container element '" + container + "' not found."); 172 } 173 174 /** 175 * A reference to this boards renderer. 176 * @type JXG.AbstractRenderer 177 */ 178 this.renderer = renderer; 179 180 /** 181 * Grids keeps track of all grids attached to this board. 182 */ 183 this.grids = []; 184 185 /** 186 * Some standard options 187 * @type JXG.Options 188 */ 189 this.options = Type.deepCopy(Options); 190 this.attr = attributes; 191 192 /** 193 * Dimension of the board. 194 * @default 2 195 * @type Number 196 */ 197 this.dimension = 2; 198 199 this.jc = new JessieCode(); 200 this.jc.use(this); 201 202 /** 203 * Coordinates of the boards origin. This a object with the two properties 204 * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords 205 * stores the boards origin in homogeneous screen coordinates. 206 * @type Object 207 */ 208 this.origin = {}; 209 this.origin.usrCoords = [1, 0, 0]; 210 this.origin.scrCoords = [1, origin[0], origin[1]]; 211 212 /** 213 * Zoom factor in X direction. It only stores the zoom factor to be able 214 * to get back to 100% in zoom100(). 215 * @type Number 216 */ 217 this.zoomX = zoomX; 218 219 /** 220 * Zoom factor in Y direction. It only stores the zoom factor to be able 221 * to get back to 100% in zoom100(). 222 * @type Number 223 */ 224 this.zoomY = zoomY; 225 226 /** 227 * The number of pixels which represent one unit in user-coordinates in x direction. 228 * @type Number 229 */ 230 this.unitX = unitX * this.zoomX; 231 232 /** 233 * The number of pixels which represent one unit in user-coordinates in y direction. 234 * @type Number 235 */ 236 this.unitY = unitY * this.zoomY; 237 238 /** 239 * Keep aspect ratio if bounding box is set and the width/height ratio differs from the 240 * width/height ratio of the canvas. 241 */ 242 this.keepaspectratio = false; 243 244 /** 245 * Canvas width. 246 * @type Number 247 */ 248 this.canvasWidth = canvasWidth; 249 250 /** 251 * Canvas Height 252 * @type Number 253 */ 254 this.canvasHeight = canvasHeight; 255 256 // If the given id is not valid, generate an unique id 257 if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) { 258 this.id = id; 259 } else { 260 this.id = this.generateId(); 261 } 262 263 EventEmitter.eventify(this); 264 265 this.hooks = []; 266 267 /** 268 * An array containing all other boards that are updated after this board has been updated. 269 * @type Array 270 * @see JXG.Board#addChild 271 * @see JXG.Board#removeChild 272 */ 273 this.dependentBoards = []; 274 275 /** 276 * During the update process this is set to false to prevent an endless loop. 277 * @default false 278 * @type Boolean 279 */ 280 this.inUpdate = false; 281 282 /** 283 * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object. 284 * @type Object 285 */ 286 this.objects = {}; 287 288 /** 289 * An array containing all geometric objects on the board in the order of construction. 290 * @type {Array} 291 */ 292 this.objectsList = []; 293 294 /** 295 * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object. 296 * @type Object 297 */ 298 this.groups = {}; 299 300 /** 301 * Stores all the objects that are currently running an animation. 302 * @type Object 303 */ 304 this.animationObjects = {}; 305 306 /** 307 * An associative array containing all highlighted elements belonging to the board. 308 * @type Object 309 */ 310 this.highlightedObjects = {}; 311 312 /** 313 * Number of objects ever created on this board. This includes every object, even invisible and deleted ones. 314 * @type Number 315 */ 316 this.numObjects = 0; 317 318 /** 319 * An associative array to store the objects of the board by name. the name of the object is the key and value is a reference to the object. 320 * @type Object 321 */ 322 this.elementsByName = {}; 323 324 /** 325 * The board mode the board is currently in. Possible values are 326 * <ul> 327 * <li>JXG.Board.BOARD_MODE_NONE</li> 328 * <li>JXG.Board.BOARD_MODE_DRAG</li> 329 * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li> 330 * </ul> 331 * @type Number 332 */ 333 this.mode = this.BOARD_MODE_NONE; 334 335 /** 336 * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}. 337 * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to 338 * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of 339 * evaluation points when plotting functions. Possible values are 340 * <ul> 341 * <li>BOARD_QUALITY_LOW</li> 342 * <li>BOARD_QUALITY_HIGH</li> 343 * </ul> 344 * @type Number 345 * @see JXG.Board#mode 346 */ 347 this.updateQuality = this.BOARD_QUALITY_HIGH; 348 349 /** 350 * If true updates are skipped. 351 * @type Boolean 352 */ 353 this.isSuspendedRedraw = false; 354 355 this.calculateSnapSizes(); 356 357 /** 358 * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button. 359 * @type Number 360 * @see JXG.Board#drag_dy 361 * @see JXG.Board#drag_obj 362 */ 363 this.drag_dx = 0; 364 365 /** 366 * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button. 367 * @type Number 368 * @see JXG.Board#drag_dx 369 * @see JXG.Board#drag_obj 370 */ 371 this.drag_dy = 0; 372 373 /** 374 * The last position where a drag event has been fired. 375 * @type Array 376 * @see JXG.Board#moveObject 377 */ 378 this.drag_position = [0, 0]; 379 380 /** 381 * References to the object that is dragged with the mouse on the board. 382 * @type {@link JXG.GeometryElement}. 383 * @see {JXG.Board#touches} 384 */ 385 this.mouse = {}; 386 387 /** 388 * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events. 389 * @type Array 390 * @see {JXG.Board#mouse} 391 */ 392 this.touches = []; 393 394 /** 395 * A string containing the XML text of the construction. This is set in {@link JXG.FileReader#parseString}. 396 * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File. 397 * @type String 398 */ 399 this.xmlString = ''; 400 401 /** 402 * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations. 403 * @type Array 404 */ 405 this.cPos = []; 406 407 /** 408 * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since 409 * touchStart because Android's Webkit browser fires too much of them. 410 * @type Number 411 */ 412 // this.touchMoveLast = 0; 413 414 /** 415 * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away. 416 * @type Number 417 */ 418 this.positionAccessLast = 0; 419 420 /** 421 * Collects all elements that triggered a mouse down event. 422 * @type Array 423 */ 424 this.downObjects = []; 425 426 if (this.attr.showcopyright) { 427 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 428 } 429 430 /** 431 * Full updates are needed after zoom and axis translates. This saves some time during an update. 432 * @default false 433 * @type Boolean 434 */ 435 this.needsFullUpdate = false; 436 437 /** 438 * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following 439 * elements are updated during mouse move. On mouse up the whole construction is 440 * updated. This enables us to be fast even on very slow devices. 441 * @type Boolean 442 * @default false 443 */ 444 this.reducedUpdate = false; 445 446 /** 447 * The current color blindness deficiency is stored in this property. If color blindness is not emulated 448 * at the moment, it's value is 'none'. 449 */ 450 this.currentCBDef = 'none'; 451 452 /** 453 * If GEONExT constructions are displayed, then this property should be set to true. 454 * At the moment there should be no difference. But this may change. 455 * This is set in {@link JXG.GeonextReader#readGeonext}. 456 * @type Boolean 457 * @default false 458 * @see JXG.GeonextReader#readGeonext 459 */ 460 this.geonextCompatibilityMode = false; 461 462 if (this.options.text.useASCIIMathML && translateASCIIMath) { 463 init(); 464 } else { 465 this.options.text.useASCIIMathML = false; 466 } 467 468 /** 469 * A flag which tells if the board registers mouse events. 470 * @type Boolean 471 * @default false 472 */ 473 this.hasMouseHandlers = false; 474 475 /** 476 * A flag which tells if the board registers touch events. 477 * @type Boolean 478 * @default false 479 */ 480 this.hasTouchHandlers = false; 481 482 /** 483 * A flag which stores if the board registered pointer events. 484 * @type {Boolean} 485 * @default false 486 */ 487 this.hasPointerHandlers = false; 488 489 /** 490 * This bool flag stores the current state of the mobile Safari specific gesture event handlers. 491 * @type {boolean} 492 * @default false 493 */ 494 this.hasGestureHandlers = false; 495 496 /** 497 * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered. 498 * @type Boolean 499 * @default false 500 */ 501 this.hasMouseUp = false; 502 503 /** 504 * A flag which tells if the board the JXG.Board#touchEndListener is currently registered. 505 * @type Boolean 506 * @default false 507 */ 508 this.hasTouchEnd = false; 509 510 /** 511 * A flag which tells us if the board has a pointerUp event registered at the moment. 512 * @type {Boolean} 513 * @default false 514 */ 515 this.hasPointerUp = false; 516 517 /** 518 * Offset for large coords elements like images 519 * @type {Array} 520 * @private 521 * @default [0, 0] 522 */ 523 this._drag_offset = [0, 0]; 524 525 /** 526 * A flag which tells us if the board is in the selecting mode 527 * @type {Boolean} 528 * @default false 529 */ 530 this.selectingMode = false; 531 532 /** 533 * A flag which tells us if the user is selecting 534 * @type {Boolean} 535 * @default false 536 */ 537 this.isSelecting = false; 538 539 /** 540 * A bounding box for the selection 541 * @type {Array} 542 * @default [ [0,0], [0,0] ] 543 */ 544 this.selectingBox = [[0, 0], [0, 0]]; 545 546 547 if (this.attr.registerevents) { 548 this.addEventHandlers(); 549 } 550 551 this.methodMap = { 552 update: 'update', 553 fullUpdate: 'fullUpdate', 554 on: 'on', 555 off: 'off', 556 trigger: 'trigger', 557 setView: 'setBoundingBox', 558 setBoundingBox: 'setBoundingBox', 559 migratePoint: 'migratePoint', 560 colorblind: 'emulateColorblindness', 561 suspendUpdate: 'suspendUpdate', 562 unsuspendUpdate: 'unsuspendUpdate', 563 clearTraces: 'clearTraces', 564 left: 'clickLeftArrow', 565 right: 'clickRightArrow', 566 up: 'clickUpArrow', 567 down: 'clickDownArrow', 568 zoomIn: 'zoomIn', 569 zoomOut: 'zoomOut', 570 zoom100: 'zoom100', 571 zoomElements: 'zoomElements', 572 remove: 'removeObject', 573 removeObject: 'removeObject' 574 }; 575 }; 576 577 JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ { 578 579 /** 580 * Generates an unique name for the given object. The result depends on the objects type, if the 581 * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line} 582 * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower 583 * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is 584 * generated using lower case characters. prefixed with k_ is used. In any other case, lower case 585 * chars prefixed with s_ is used. 586 * @param {Object} object Reference of an JXG.GeometryElement that is to be named. 587 * @returns {String} Unique name for the object. 588 */ 589 generateName: function (object) { 590 var possibleNames, i, 591 maxNameLength = this.attr.maxnamelength, 592 pre = '', 593 post = '', 594 indices = [], 595 name = ''; 596 597 if (object.type === Const.OBJECT_TYPE_TICKS) { 598 return ''; 599 } 600 601 if (Type.isPoint(object)) { 602 // points have capital letters 603 possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 604 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 605 } else if (object.type === Const.OBJECT_TYPE_ANGLE) { 606 possibleNames = ['', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 607 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 608 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω']; 609 } else { 610 // all other elements get lowercase labels 611 possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 612 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; 613 } 614 615 if (!Type.isPoint(object) && 616 object.elementClass !== Const.OBJECT_CLASS_LINE && 617 object.type !== Const.OBJECT_TYPE_ANGLE) { 618 if (object.type === Const.OBJECT_TYPE_POLYGON) { 619 pre = 'P_{'; 620 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) { 621 pre = 'k_{'; 622 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) { 623 pre = 't_{'; 624 } else { 625 pre = 's_{'; 626 } 627 post = '}'; 628 } 629 630 for (i = 0; i < maxNameLength; i++) { 631 indices[i] = 0; 632 } 633 634 while (indices[maxNameLength - 1] < possibleNames.length) { 635 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) { 636 name = pre; 637 638 for (i = maxNameLength; i > 0; i--) { 639 name += possibleNames[indices[i - 1]]; 640 } 641 642 if (!Type.exists(this.elementsByName[name + post])) { 643 return name + post; 644 } 645 646 } 647 indices[0] = possibleNames.length; 648 649 for (i = 1; i < maxNameLength; i++) { 650 if (indices[i - 1] === possibleNames.length) { 651 indices[i - 1] = 1; 652 indices[i] += 1; 653 } 654 } 655 } 656 657 return ''; 658 }, 659 660 /** 661 * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'. 662 * @returns {String} Unique id for a board. 663 */ 664 generateId: function () { 665 var r = 1; 666 667 // as long as we don't have a unique id generate a new one 668 while (Type.exists(JXG.boards['jxgBoard' + r])) { 669 r = Math.round(Math.random() * 65535); 670 } 671 672 return ('jxgBoard' + r); 673 }, 674 675 /** 676 * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the 677 * object type. Additionally, the id of the label is set. As a side effect {@link JXG.Board#numObjects} 678 * is updated. 679 * @param {Object} obj Reference of an geometry object that needs an id. 680 * @param {Number} type Type of the object. 681 * @returns {String} Unique id for an element. 682 */ 683 setId: function (obj, type) { 684 var num = this.numObjects, 685 elId = obj.id; 686 687 this.numObjects += 1; 688 689 // Falls Id nicht vorgegeben, eine Neue generieren: 690 if (elId === '' || !Type.exists(elId)) { 691 elId = this.id + type + num; 692 } 693 694 obj.id = elId; 695 this.objects[elId] = obj; 696 obj._pos = this.objectsList.length; 697 this.objectsList[this.objectsList.length] = obj; 698 699 return elId; 700 }, 701 702 /** 703 * After construction of the object the visibility is set 704 * and the label is constructed if necessary. 705 * @param {Object} obj The object to add. 706 */ 707 finalizeAdding: function (obj) { 708 if (!obj.visProp.visible) { 709 this.renderer.hide(obj); 710 } 711 }, 712 713 finalizeLabel: function (obj) { 714 if (obj.hasLabel && !obj.label.visProp.islabel && !obj.label.visProp.visible) { 715 this.renderer.hide(obj.label); 716 } 717 }, 718 719 /********************************************************** 720 * 721 * Event Handler helpers 722 * 723 **********************************************************/ 724 725 /** 726 * Calculates mouse coordinates relative to the boards container. 727 * @returns {Array} Array of coordinates relative the boards container top left corner. 728 */ 729 getCoordsTopLeftCorner: function () { 730 var cPos, doc, crect, 731 docElement = this.document.documentElement || this.document.body.parentNode, 732 docBody = this.document.body, 733 container = this.containerObj, 734 viewport, content; 735 736 /** 737 * During drags and origin moves the container element is usually not changed. 738 * Check the position of the upper left corner at most every 1000 msecs 739 */ 740 if (this.cPos.length > 0 && 741 (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN || 742 (new Date()).getTime() - this.positionAccessLast < 1000)) { 743 return this.cPos; 744 } 745 this.positionAccessLast = (new Date()).getTime(); 746 747 // Check if getBoundingClientRect exists. If so, use this as this covers *everything* 748 // even CSS3D transformations etc. 749 // Supported by all browsers but IE 6, 7. 750 if (container.getBoundingClientRect) { 751 crect = container.getBoundingClientRect(); 752 cPos = [crect.left, crect.top]; 753 754 // add border width 755 cPos[0] += Env.getProp(container, 'border-left-width'); 756 cPos[1] += Env.getProp(container, 'border-top-width'); 757 758 // vml seems to ignore paddings 759 if (this.renderer.type !== 'vml') { 760 // add padding 761 cPos[0] += Env.getProp(container, 'padding-left'); 762 cPos[1] += Env.getProp(container, 'padding-top'); 763 } 764 765 this.cPos = cPos.slice(); 766 return this.cPos; 767 } 768 769 // 770 // OLD CODE 771 // IE 6-7 only: 772 // 773 cPos = Env.getOffset(container); 774 doc = this.document.documentElement.ownerDocument; 775 776 if (!this.containerObj.currentStyle && doc.defaultView) { // Non IE 777 // this is for hacks like this one used in wordpress for the admin bar: 778 // html { margin-top: 28px } 779 // seems like it doesn't work in IE 780 781 cPos[0] += Env.getProp(docElement, 'margin-left'); 782 cPos[1] += Env.getProp(docElement, 'margin-top'); 783 784 cPos[0] += Env.getProp(docElement, 'border-left-width'); 785 cPos[1] += Env.getProp(docElement, 'border-top-width'); 786 787 cPos[0] += Env.getProp(docElement, 'padding-left'); 788 cPos[1] += Env.getProp(docElement, 'padding-top'); 789 } 790 791 if (docBody) { 792 cPos[0] += Env.getProp(docBody, 'left'); 793 cPos[1] += Env.getProp(docBody, 'top'); 794 } 795 796 // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX 797 // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly 798 // available version so we're doing it the hacky way: Add a fixed offset. 799 // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J 800 if (typeof google === 'object' && google.translate) { 801 cPos[0] += 10; 802 cPos[1] += 25; 803 } 804 805 // add border width 806 cPos[0] += Env.getProp(container, 'border-left-width'); 807 cPos[1] += Env.getProp(container, 'border-top-width'); 808 809 // vml seems to ignore paddings 810 if (this.renderer.type !== 'vml') { 811 // add padding 812 cPos[0] += Env.getProp(container, 'padding-left'); 813 cPos[1] += Env.getProp(container, 'padding-top'); 814 } 815 816 cPos[0] += this.attr.offsetx; 817 cPos[1] += this.attr.offsety; 818 819 this.cPos = cPos.slice(); 820 return this.cPos; 821 }, 822 823 /** 824 * Get the position of the mouse in screen coordinates, relative to the upper left corner 825 * of the host tag. 826 * @param {Event} e Event object given by the browser. 827 * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set 828 * for mouseevents. 829 * @returns {Array} Contains the mouse coordinates in user coordinates, ready for {@link JXG.Coords} 830 */ 831 getMousePosition: function (e, i) { 832 var cPos = this.getCoordsTopLeftCorner(), 833 absPos, 834 v; 835 836 // position of mouse cursor relative to containers position of container 837 absPos = Env.getPosition(e, i, this.document); 838 839 /** 840 * In case there has been no down event before. 841 */ 842 if (!Type.exists(this.cssTransMat)) { 843 this.updateCSSTransforms(); 844 } 845 v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]]; 846 v = Mat.matVecMult(this.cssTransMat, v); 847 v[1] /= v[0]; 848 v[2] /= v[0]; 849 return [v[1], v[2]]; 850 851 // Method without CSS transformation 852 /* 853 return [absPos[0] - cPos[0], absPos[1] - cPos[1]]; 854 */ 855 }, 856 857 /** 858 * Initiate moving the origin. This is used in mouseDown and touchStart listeners. 859 * @param {Number} x Current mouse/touch coordinates 860 * @param {Number} y Current mouse/touch coordinates 861 */ 862 initMoveOrigin: function (x, y) { 863 this.drag_dx = x - this.origin.scrCoords[1]; 864 this.drag_dy = y - this.origin.scrCoords[2]; 865 866 this.mode = this.BOARD_MODE_MOVE_ORIGIN; 867 this.updateQuality = this.BOARD_QUALITY_LOW; 868 }, 869 870 /** 871 * Collects all elements below the current mouse pointer and fulfilling the following constraints: 872 * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul> 873 * @param {Number} x Current mouse/touch coordinates 874 * @param {Number} y current mouse/touch coordinates 875 * @param {Object} evt An event object 876 * @param {String} type What type of event? 'touch' or 'mouse'. 877 * @returns {Array} A list of geometric elements. 878 */ 879 initMoveObject: function (x, y, evt, type) { 880 var pEl, 881 el, 882 collect = [], 883 offset = [], 884 haspoint, 885 len = this.objectsList.length, 886 dragEl = {visProp: {layer: -10000}}; 887 888 //for (el in this.objects) { 889 for (el = 0; el < len; el++) { 890 pEl = this.objectsList[el]; 891 haspoint = pEl.hasPoint && pEl.hasPoint(x, y); 892 893 if (pEl.visProp.visible && haspoint) { 894 pEl.triggerEventHandlers([type + 'down', 'down'], [evt]); 895 this.downObjects.push(pEl); 896 } 897 898 if (((this.geonextCompatibilityMode && 899 (Type.isPoint(pEl) || 900 pEl.elementClass === Const.OBJECT_CLASS_TEXT)) || 901 !this.geonextCompatibilityMode) && 902 pEl.isDraggable && 903 pEl.visProp.visible && 904 (!pEl.visProp.fixed) && /*(!pEl.visProp.frozen) &&*/ 905 haspoint) { 906 // Elements in the highest layer get priority. 907 if (pEl.visProp.layer > dragEl.visProp.layer || 908 (pEl.visProp.layer === dragEl.visProp.layer && 909 pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime() 910 )) { 911 // If an element and its label have the focus 912 // simultaneously, the element is taken. 913 // This only works if we assume that every browser runs 914 // through this.objects in the right order, i.e. an element A 915 // added before element B turns up here before B does. 916 if (!this.attr.ignorelabels || (!Type.exists(dragEl.label) || pEl !== dragEl.label)) { 917 dragEl = pEl; 918 collect.push(dragEl); 919 920 // Save offset for large coords elements. 921 if (Type.exists(dragEl.coords)) { 922 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y])); 923 } else { 924 offset.push([0, 0]); 925 } 926 927 // we can't drop out of this loop because of the event handling system 928 //if (this.attr.takefirst) { 929 // return collect; 930 //} 931 } 932 } 933 } 934 } 935 936 if (collect.length > 0) { 937 this.mode = this.BOARD_MODE_DRAG; 938 } 939 940 // A one-element array is returned. 941 if (this.attr.takefirst) { 942 collect.length = 1; 943 this._drag_offset = offset[0]; 944 } else { 945 collect = collect.slice(-1); 946 this._drag_offset = offset[offset.length - 1]; 947 } 948 949 if (!this._drag_offset) { 950 this._drag_offset = [0, 0]; 951 } 952 953 return collect; 954 }, 955 956 /** 957 * Moves an object. 958 * @param {Number} x Coordinate 959 * @param {Number} y Coordinate 960 * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}. 961 * @param {Object} evt The event object. 962 * @param {String} type Mouse or touch event? 963 */ 964 moveObject: function (x, y, o, evt, type) { 965 var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this), 966 drag; 967 968 if (!(o && o.obj)) { 969 return; 970 } 971 drag = o.obj; 972 973 /* 974 * Save the position. 975 */ 976 this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]]; 977 this.drag_position = Statistics.add(this.drag_position, this._drag_offset); 978 // 979 // We have to distinguish between CoordsElements and other elements like lines. 980 // The latter need the difference between two move events. 981 if (Type.exists(drag.coords)) { 982 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position); 983 } else { 984 this.renderer.hide(this.infobox); // Hide infobox in case the user has touched an intersection point 985 // and drags the underlying line now. 986 987 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) { 988 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, 989 [newPos.scrCoords[1], newPos.scrCoords[2]], 990 [o.targets[0].Xprev, o.targets[0].Yprev] 991 ); 992 } 993 // Remember the actual position for the next move event. Then we are able to 994 // compute the difference vector. 995 o.targets[0].Xprev = newPos.scrCoords[1]; 996 o.targets[0].Yprev = newPos.scrCoords[2]; 997 } 998 // This may be necessary for some gliders 999 drag.prepareUpdate().update(false).updateRenderer(); 1000 1001 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]); 1002 1003 this.updateInfobox(drag); 1004 this.update(); 1005 drag.highlight(true); 1006 1007 drag.lastDragTime = new Date(); 1008 }, 1009 1010 /** 1011 * Moves elements in multitouch mode. 1012 * @param {Array} p1 x,y coordinates of first touch 1013 * @param {Array} p2 x,y coordinates of second touch 1014 * @param {Object} o The touch object that is dragged: {JXG.Board#touches}. 1015 * @param {Object} evt The event object that lead to this movement. 1016 */ 1017 twoFingerMove: function (p1, p2, o, evt) { 1018 var np1c, np2c, drag; 1019 1020 if (Type.exists(o) && Type.exists(o.obj)) { 1021 drag = o.obj; 1022 } else { 1023 return; 1024 } 1025 1026 // New finger position 1027 np1c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this); 1028 np2c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this); 1029 1030 if (drag.elementClass === Const.OBJECT_CLASS_LINE || 1031 drag.type === Const.OBJECT_TYPE_POLYGON) { 1032 this.twoFingerTouchObject(np1c, np2c, o, drag); 1033 } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1034 this.twoFingerTouchCircle(np1c, np2c, o, drag); 1035 } 1036 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]); 1037 1038 o.targets[0].Xprev = np1c.scrCoords[1]; 1039 o.targets[0].Yprev = np1c.scrCoords[2]; 1040 o.targets[1].Xprev = np2c.scrCoords[1]; 1041 o.targets[1].Yprev = np2c.scrCoords[2]; 1042 }, 1043 1044 /** 1045 * Moves a line or polygon with two fingers 1046 * @param {JXG.Coords} np1c x,y coordinates of first touch 1047 * @param {JXG.Coords} np2c x,y coordinates of second touch 1048 * @param {object} o The touch object that is dragged: {JXG.Board#touches}. 1049 * @param {object} drag The object that is dragged: 1050 */ 1051 twoFingerTouchObject: function (np1c, np2c, o, drag) { 1052 var np1, np2, op1, op2, 1053 nmid, omid, nd, od, 1054 d, 1055 S, alpha, t1, t2, t3, t4, t5, 1056 ar, i, len; 1057 1058 if (Type.exists(o.targets[0]) && 1059 Type.exists(o.targets[1]) && 1060 !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) { 1061 np1 = np1c.usrCoords; 1062 np2 = np2c.usrCoords; 1063 // Previous finger position 1064 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords; 1065 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords; 1066 1067 // Affine mid points of the old and new positions 1068 omid = [1, (op1[1] + op2[1]) * 0.5, (op1[2] + op2[2]) * 0.5]; 1069 nmid = [1, (np1[1] + np2[1]) * 0.5, (np1[2] + np2[2]) * 0.5]; 1070 1071 // Old and new directions 1072 od = Mat.crossProduct(op1, op2); 1073 nd = Mat.crossProduct(np1, np2); 1074 S = Mat.crossProduct(od, nd); 1075 1076 // If parallel, translate otherwise rotate 1077 if (Math.abs(S[0]) < Mat.eps) { 1078 return; 1079 } 1080 1081 S[1] /= S[0]; 1082 S[2] /= S[0]; 1083 alpha = Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1)); 1084 t1 = this.create('transform', [alpha, S[1], S[2]], {type: 'rotate'}); 1085 1086 // Old midpoint of fingers after first transformation: 1087 t1.update(); 1088 omid = Mat.matVecMult(t1.matrix, omid); 1089 omid[1] /= omid[0]; 1090 omid[2] /= omid[0]; 1091 1092 // Shift to the new mid point 1093 t2 = this.create('transform', [nmid[1] - omid[1], nmid[2] - omid[2]], {type: 'translate'}); 1094 t2.update(); 1095 //omid = Mat.matVecMult(t2.matrix, omid); 1096 1097 t1.melt(t2); 1098 if (drag.visProp.scalable) { 1099 // Scale 1100 d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2); 1101 t3 = this.create('transform', [-nmid[1], -nmid[2]], {type: 'translate'}); 1102 t4 = this.create('transform', [d, d], {type: 'scale'}); 1103 t5 = this.create('transform', [nmid[1], nmid[2]], {type: 'translate'}); 1104 t1.melt(t3).melt(t4).melt(t5); 1105 } 1106 1107 1108 if (drag.elementClass === Const.OBJECT_CLASS_LINE) { 1109 ar = []; 1110 if (drag.point1.draggable()) { 1111 ar.push(drag.point1); 1112 } 1113 if (drag.point2.draggable()) { 1114 ar.push(drag.point2); 1115 } 1116 t1.applyOnce(ar); 1117 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) { 1118 ar = []; 1119 len = drag.vertices.length - 1; 1120 for (i = 0; i < len; ++i) { 1121 if (drag.vertices[i].draggable()) { 1122 ar.push(drag.vertices[i]); 1123 } 1124 } 1125 t1.applyOnce(ar); 1126 } 1127 1128 this.update(); 1129 drag.highlight(true); 1130 } 1131 }, 1132 1133 /* 1134 * Moves a circle with two fingers 1135 * @param {JXG.Coords} np1c x,y coordinates of first touch 1136 * @param {JXG.Coords} np2c x,y coordinates of second touch 1137 * @param {object} o The touch object that is dragged: {JXG.Board#touches}. 1138 * @param {object} drag The object that is dragged: 1139 */ 1140 twoFingerTouchCircle: function (np1c, np2c, o, drag) { 1141 var np1, np2, op1, op2, 1142 d, alpha, t1, t2, t3, t4, t5; 1143 1144 if (drag.method === 'pointCircle' || 1145 drag.method === 'pointLine') { 1146 return; 1147 } 1148 1149 if (Type.exists(o.targets[0]) && 1150 Type.exists(o.targets[1]) && 1151 !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) { 1152 1153 np1 = np1c.usrCoords; 1154 np2 = np2c.usrCoords; 1155 // Previous finger position 1156 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords; 1157 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords; 1158 1159 // Shift by the movement of the first finger 1160 t1 = this.create('transform', [np1[1] - op1[1], np1[2] - op1[2]], {type: 'translate'}); 1161 alpha = Geometry.rad(op2.slice(1), np1.slice(1), np2.slice(1)); 1162 1163 // Rotate and scale by the movement of the second finger 1164 t2 = this.create('transform', [-np1[1], -np1[2]], {type: 'translate'}); 1165 t3 = this.create('transform', [alpha], {type: 'rotate'}); 1166 t1.melt(t2).melt(t3); 1167 1168 if (drag.visProp.scalable) { 1169 d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2); 1170 t4 = this.create('transform', [d, d], {type: 'scale'}); 1171 t1.melt(t4); 1172 } 1173 t5 = this.create('transform', [np1[1], np1[2]], {type: 'translate'}); 1174 t1.melt(t5); 1175 1176 if (drag.center.draggable()) { 1177 t1.applyOnce([drag.center]); 1178 } 1179 1180 if (drag.method === 'twoPoints') { 1181 if (drag.point2.draggable()) { 1182 t1.applyOnce([drag.point2]); 1183 } 1184 } else if (drag.method === 'pointRadius') { 1185 if (Type.isNumber(drag.updateRadius.origin)) { 1186 drag.setRadius(drag.radius * d); 1187 } 1188 } 1189 this.update(drag.center); 1190 drag.highlight(true); 1191 } 1192 }, 1193 1194 highlightElements: function (x, y, evt, target) { 1195 var el, pEl, pId, 1196 overObjects = {}, 1197 len = this.objectsList.length; 1198 1199 // Elements below the mouse pointer which are not highlighted yet will be highlighted. 1200 for (el = 0; el < len; el++) { 1201 pEl = this.objectsList[el]; 1202 pId = pEl.id; 1203 if (Type.exists(pEl.hasPoint) && pEl.visProp.visible && pEl.hasPoint(x, y)) { 1204 // this is required in any case because otherwise the box won't be shown until the point is dragged 1205 this.updateInfobox(pEl); 1206 1207 if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted 1208 overObjects[pId] = pEl; 1209 pEl.highlight(); 1210 this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]); 1211 } 1212 1213 if (pEl.mouseover) { 1214 pEl.triggerEventHandlers(['mousemove', 'move'], [evt]); 1215 } else { 1216 pEl.triggerEventHandlers(['mouseover', 'over'], [evt]); 1217 pEl.mouseover = true; 1218 } 1219 } 1220 } 1221 1222 for (el = 0; el < len; el++) { 1223 pEl = this.objectsList[el]; 1224 pId = pEl.id; 1225 if (pEl.mouseover) { 1226 if (!overObjects[pId]) { 1227 pEl.triggerEventHandlers(['mouseout', 'out'], [evt]); 1228 pEl.mouseover = false; 1229 } 1230 } 1231 } 1232 }, 1233 1234 /** 1235 * Helper function which returns a reasonable starting point for the object being dragged. 1236 * Formerly known as initXYstart(). 1237 * @private 1238 * @param {JXG.GeometryElement} obj The object to be dragged 1239 * @param {Array} targets Array of targets. It is changed by this function. 1240 */ 1241 saveStartPos: function (obj, targets) { 1242 var xy = [], i, len; 1243 1244 if (obj.type === Const.OBJECT_TYPE_TICKS) { 1245 xy.push([1, NaN, NaN]); 1246 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) { 1247 xy.push(obj.point1.coords.usrCoords); 1248 xy.push(obj.point2.coords.usrCoords); 1249 } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1250 xy.push(obj.center.coords.usrCoords); 1251 if (obj.method === 'twoPoints') { 1252 xy.push(obj.point2.coords.usrCoords); 1253 } 1254 } else if (obj.type === Const.OBJECT_TYPE_POLYGON) { 1255 len = obj.vertices.length - 1; 1256 for (i = 0; i < len; i++) { 1257 xy.push(obj.vertices[i].coords.usrCoords); 1258 } 1259 } else if (obj.type === Const.OBJECT_TYPE_SECTOR) { 1260 xy.push(obj.point1.coords.usrCoords); 1261 xy.push(obj.point2.coords.usrCoords); 1262 xy.push(obj.point3.coords.usrCoords); 1263 } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) { 1264 xy.push(obj.coords.usrCoords); 1265 } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) { 1266 if (JXG.exists(obj.parents)) { 1267 len = obj.parents.length; 1268 for (i = 0; i < len; i++) { 1269 xy.push(this.select(obj.parents[i]).coords.usrCoords); 1270 } 1271 } 1272 } else { 1273 try { 1274 xy.push(obj.coords.usrCoords); 1275 } catch (e) { 1276 JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e); 1277 } 1278 } 1279 1280 len = xy.length; 1281 for (i = 0; i < len; i++) { 1282 targets.Zstart.push(xy[i][0]); 1283 targets.Xstart.push(xy[i][1]); 1284 targets.Ystart.push(xy[i][2]); 1285 } 1286 }, 1287 1288 mouseOriginMoveStart: function (evt) { 1289 var r = this.attr.pan.enabled && (!this.attr.pan.needshift || evt.shiftKey), 1290 pos; 1291 1292 if (r) { 1293 pos = this.getMousePosition(evt); 1294 this.initMoveOrigin(pos[0], pos[1]); 1295 } 1296 1297 return r; 1298 }, 1299 1300 mouseOriginMove: function (evt) { 1301 var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN), 1302 pos; 1303 1304 if (r) { 1305 pos = this.getMousePosition(evt); 1306 this.moveOrigin(pos[0], pos[1], true); 1307 } 1308 1309 return r; 1310 }, 1311 1312 touchOriginMoveStart: function (evt) { 1313 var touches = evt[JXG.touchProperty], 1314 twoFingersCondition = (touches.length === 2 && Geometry.distance([touches[0].screenX, touches[0].screenY], [touches[1].screenX, touches[1].screenY]) < 120), 1315 r = this.attr.pan.enabled && (!this.attr.pan.needtwofingers || twoFingersCondition), 1316 pos; 1317 1318 if (r) { 1319 pos = this.getMousePosition(evt, 0); 1320 this.initMoveOrigin(pos[0], pos[1]); 1321 } 1322 1323 return r; 1324 }, 1325 1326 touchOriginMove: function (evt) { 1327 var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN), 1328 pos; 1329 1330 if (r) { 1331 pos = this.getMousePosition(evt, 0); 1332 this.moveOrigin(pos[0], pos[1], true); 1333 } 1334 1335 return r; 1336 }, 1337 1338 originMoveEnd: function () { 1339 this.updateQuality = this.BOARD_QUALITY_HIGH; 1340 this.mode = this.BOARD_MODE_NONE; 1341 }, 1342 1343 /********************************************************** 1344 * 1345 * Event Handler 1346 * 1347 **********************************************************/ 1348 1349 /** 1350 * Add all possible event handlers to the board object 1351 */ 1352 addEventHandlers: function () { 1353 if (Env.supportsPointerEvents()) { 1354 this.addPointerEventHandlers(); 1355 } else { 1356 this.addMouseEventHandlers(); 1357 this.addTouchEventHandlers(); 1358 } 1359 //if (Env.isBrowser) { 1360 //Env.addEvent(window, 'resize', this.update, this); 1361 //} 1362 }, 1363 1364 /** 1365 * Registers the MSPointer* event handlers. 1366 */ 1367 addPointerEventHandlers: function () { 1368 if (!this.hasPointerHandlers && Env.isBrowser) { 1369 if (window.navigator.pointerEnabled) { // IE11+ 1370 Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1371 Env.addEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this); 1372 } else { 1373 Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1374 Env.addEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this); 1375 } 1376 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1377 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1378 1379 this.hasPointerHandlers = true; 1380 } 1381 }, 1382 1383 /** 1384 * Registers mouse move, down and wheel event handlers. 1385 */ 1386 addMouseEventHandlers: function () { 1387 if (!this.hasMouseHandlers && Env.isBrowser) { 1388 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1389 Env.addEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this); 1390 1391 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1392 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1393 1394 this.hasMouseHandlers = true; 1395 1396 // This one produces errors on IE 1397 // Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this); 1398 1399 // This one works on IE, Firefox and Chromium with default configurations. On some Safari 1400 // or Opera versions the user must explicitly allow the deactivation of the context menu. 1401 if (this.containerObj !== null) { 1402 this.containerObj.oncontextmenu = function (e) { 1403 if (Type.exists(e)) { 1404 e.preventDefault(); 1405 } 1406 1407 return false; 1408 }; 1409 } 1410 } 1411 }, 1412 1413 /** 1414 * Register touch start and move and gesture start and change event handlers. 1415 * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers 1416 * will not be registered. 1417 */ 1418 addTouchEventHandlers: function (appleGestures) { 1419 if (!this.hasTouchHandlers && Env.isBrowser) { 1420 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1421 Env.addEvent(this.containerObj, 'touchmove', this.touchMoveListener, this); 1422 1423 if (!Type.exists(appleGestures) || appleGestures) { 1424 Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 1425 Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 1426 this.hasGestureHandlers = true; 1427 } 1428 1429 this.hasTouchHandlers = true; 1430 } 1431 }, 1432 1433 /** 1434 * Remove MSPointer* Event handlers. 1435 */ 1436 removePointerEventHandlers: function () { 1437 if (this.hasPointerHandlers && Env.isBrowser) { 1438 if (window.navigator.pointerEnabled) { // IE11+ 1439 Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1440 Env.removeEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this); 1441 } else { 1442 Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1443 Env.removeEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this); 1444 } 1445 1446 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1447 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1448 1449 if (this.hasPointerUp) { 1450 if (window.navigator.pointerEnabled) { // IE11+ 1451 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 1452 } else { 1453 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 1454 } 1455 this.hasPointerUp = false; 1456 } 1457 1458 this.hasPointerHandlers = false; 1459 } 1460 }, 1461 1462 /** 1463 * De-register mouse event handlers. 1464 */ 1465 removeMouseEventHandlers: function () { 1466 if (this.hasMouseHandlers && Env.isBrowser) { 1467 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1468 Env.removeEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this); 1469 1470 if (this.hasMouseUp) { 1471 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 1472 this.hasMouseUp = false; 1473 } 1474 1475 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1476 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1477 1478 this.hasMouseHandlers = false; 1479 } 1480 }, 1481 1482 /** 1483 * Remove all registered touch event handlers. 1484 */ 1485 removeTouchEventHandlers: function () { 1486 if (this.hasTouchHandlers && Env.isBrowser) { 1487 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1488 Env.removeEvent(this.containerObj, 'touchmove', this.touchMoveListener, this); 1489 1490 if (this.hasTouchEnd) { 1491 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 1492 this.hasTouchEnd = false; 1493 } 1494 1495 if (this.hasGestureHandlers) { 1496 Env.removeEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 1497 Env.removeEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 1498 this.hasGestureHandlers = false; 1499 } 1500 1501 this.hasTouchHandlers = false; 1502 } 1503 }, 1504 1505 /** 1506 * Remove all event handlers from the board object 1507 */ 1508 removeEventHandlers: function () { 1509 this.removeMouseEventHandlers(); 1510 this.removeTouchEventHandlers(); 1511 this.removePointerEventHandlers(); 1512 }, 1513 1514 /** 1515 * Handler for click on left arrow in the navigation bar 1516 * @returns {JXG.Board} Reference to the board 1517 */ 1518 clickLeftArrow: function () { 1519 this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]); 1520 return this; 1521 }, 1522 1523 /** 1524 * Handler for click on right arrow in the navigation bar 1525 * @returns {JXG.Board} Reference to the board 1526 */ 1527 clickRightArrow: function () { 1528 this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]); 1529 return this; 1530 }, 1531 1532 /** 1533 * Handler for click on up arrow in the navigation bar 1534 * @returns {JXG.Board} Reference to the board 1535 */ 1536 clickUpArrow: function () { 1537 this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1); 1538 return this; 1539 }, 1540 1541 /** 1542 * Handler for click on down arrow in the navigation bar 1543 * @returns {JXG.Board} Reference to the board 1544 */ 1545 clickDownArrow: function () { 1546 this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1); 1547 return this; 1548 }, 1549 1550 /** 1551 * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. 1552 * Works on iOS/Safari and Android. 1553 * @param {Event} evt Browser event object 1554 * @returns {Boolean} 1555 */ 1556 gestureChangeListener: function (evt) { 1557 var c, 1558 zx = this.attr.zoom.factorx, 1559 zy = this.attr.zoom.factory, 1560 dist; 1561 1562 if (!this.attr.zoom.wheel) { 1563 return true; 1564 } 1565 1566 evt.preventDefault(); 1567 1568 if (this.mode === this.BOARD_MODE_ZOOM) { 1569 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this); 1570 1571 if (evt.scale === undefined) { 1572 // Android pinch to zoom 1573 dist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY], 1574 [evt.touches[1].clientX, evt.touches[1].clientY], 2); 1575 1576 // evt.scale is undefined in Android 1577 evt.scale = dist / this.prevDist; 1578 } 1579 this.attr.zoom.factorx = evt.scale / this.prevScale; 1580 this.attr.zoom.factory = evt.scale / this.prevScale; 1581 1582 this.zoomIn(c.usrCoords[1], c.usrCoords[2]); 1583 this.prevScale = evt.scale; 1584 1585 this.attr.zoom.factorx = zx; 1586 this.attr.zoom.factory = zy; 1587 } 1588 1589 return false; 1590 }, 1591 1592 /** 1593 * Called by iOS/Safari as soon as the user starts a gesture (only works on iOS/Safari). 1594 * @param {Event} evt 1595 * @returns {Boolean} 1596 */ 1597 gestureStartListener: function (evt) { 1598 1599 if (!this.attr.zoom.wheel) { 1600 return true; 1601 } 1602 1603 evt.preventDefault(); 1604 this.prevScale = 1; 1605 1606 // Android pinch to zoom 1607 if (evt.scale === undefined) { 1608 this.prevDist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY], 1609 [evt.touches[1].clientX, evt.touches[1].clientY], 2); 1610 } 1611 1612 if (this.mode === this.BOARD_MODE_NONE) { 1613 this.mode = this.BOARD_MODE_ZOOM; 1614 } 1615 return false; 1616 }, 1617 1618 /** 1619 * pointer-Events 1620 */ 1621 1622 /** 1623 * This method is called by the browser when a pointing device is pressed on the screen. 1624 * @param {Event} evt The browsers event object. 1625 * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter 1626 * @returns {Boolean} ... 1627 */ 1628 pointerDownListener: function (evt, object) { 1629 var i, j, k, pos, elements, sel, 1630 eps = this.options.precision.touch, 1631 found, target, result; 1632 1633 if (!this.hasPointerUp) { 1634 if (window.navigator.pointerEnabled) { // IE11+ 1635 Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this); 1636 } else { 1637 Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 1638 } 1639 this.hasPointerUp = true; 1640 } 1641 1642 if (this.hasMouseHandlers) { 1643 this.removeMouseEventHandlers(); 1644 } 1645 1646 if (this.hasTouchHandlers) { 1647 this.removeTouchEventHandlers(); 1648 } 1649 1650 // prevent accidental selection of text 1651 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 1652 this.document.selection.empty(); 1653 } else if (window.getSelection) { 1654 sel = window.getSelection(); 1655 if (sel.removeAllRanges) { 1656 try { 1657 sel.removeAllRanges(); 1658 } catch (e) {} 1659 } 1660 } 1661 1662 // Touch or pen device 1663 if (JXG.isBrowser && (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) { 1664 this.options.precision.hasPoint = eps; 1665 } 1666 1667 // This should be easier than the touch events. Every pointer device gets its own pointerId, e.g. the mouse 1668 // always has id 1, fingers and pens get unique ids every time a pointerDown event is fired and they will 1669 // keep this id until a pointerUp event is fired. What we have to do here is: 1670 // 1. collect all elements under the current pointer 1671 // 2. run through the touches control structure 1672 // a. look for the object collected in step 1. 1673 // b. if an object is found, check the number of pointers. if appropriate, add the pointer. 1674 1675 pos = this.getMousePosition(evt); 1676 1677 // selection 1678 this._testForSelection(evt); 1679 if (this.selectingMode) { 1680 this._startSelecting(pos); 1681 this.triggerEventHandlers(['touchstartselecting', 'pointerstartselecting', 'startselecting'], [evt]); 1682 return; // don't continue as a normal click 1683 } 1684 1685 if (object) { 1686 elements = [ object ]; 1687 this.mode = this.BOARD_MODE_DRAG; 1688 } else { 1689 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 1690 } 1691 1692 // if no draggable object can be found, get out here immediately 1693 if (elements.length > 0) { 1694 // check touches structure 1695 target = elements[elements.length - 1]; 1696 found = false; 1697 1698 for (i = 0; i < this.touches.length; i++) { 1699 // the target is already in our touches array, try to add the pointer to the existing touch 1700 if (this.touches[i].obj === target) { 1701 j = i; 1702 k = this.touches[i].targets.push({ 1703 num: evt.pointerId, 1704 X: pos[0], 1705 Y: pos[1], 1706 Xprev: NaN, 1707 Yprev: NaN, 1708 Xstart: [], 1709 Ystart: [], 1710 Zstart: [] 1711 }) - 1; 1712 1713 found = true; 1714 break; 1715 } 1716 } 1717 1718 if (!found) { 1719 k = 0; 1720 j = this.touches.push({ 1721 obj: target, 1722 targets: [{ 1723 num: evt.pointerId, 1724 X: pos[0], 1725 Y: pos[1], 1726 Xprev: NaN, 1727 Yprev: NaN, 1728 Xstart: [], 1729 Ystart: [], 1730 Zstart: [] 1731 }] 1732 }) - 1; 1733 } 1734 1735 this.dehighlightAll(); 1736 target.highlight(true); 1737 1738 this.saveStartPos(target, this.touches[j].targets[k]); 1739 1740 // prevent accidental text selection 1741 // this could get us new trouble: input fields, links and drop down boxes placed as text 1742 // on the board don't work anymore. 1743 if (evt && evt.preventDefault) { 1744 evt.preventDefault(); 1745 } else if (window.event) { 1746 window.event.returnValue = false; 1747 } 1748 } 1749 1750 if (this.touches.length > 0) { 1751 evt.preventDefault(); 1752 evt.stopPropagation(); 1753 } 1754 1755 // move origin - but only if we're not in drag mode 1756 if (this.mode === this.BOARD_MODE_NONE && this.mouseOriginMoveStart(evt)) { 1757 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]