1 /* 2 Copyright 2008-2023 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 <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/ 33 34 /*jslint nomen: true, plusplus: true*/ 35 36 /** 37 * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods 38 * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc. 39 */ 40 41 import JXG from '../jxg'; 42 import Const from './constants'; 43 import Coords from './coords'; 44 import Options from '../options'; 45 import Numerics from '../math/numerics'; 46 import Mat from '../math/math'; 47 import Geometry from '../math/geometry'; 48 import Complex from '../math/complex'; 49 import Statistics from '../math/statistics'; 50 import JessieCode from '../parser/jessiecode'; 51 import Color from '../utils/color'; 52 import Type from '../utils/type'; 53 import EventEmitter from '../utils/event'; 54 import Env from '../utils/env'; 55 import Composition from './composition'; 56 57 /** 58 * Constructs a new Board object. 59 * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric 60 * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly. 61 * Please use {@link JXG.JSXGraph.initBoard} to initialize a board. 62 * @constructor 63 * @param {String} container The id or reference of the HTML DOM element the board is drawn in. This is usually a HTML div. 64 * @param {JXG.AbstractRenderer} renderer The reference of a renderer. 65 * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined. 66 * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates. 67 * @param {Number} zoomX Zoom factor in x-axis direction 68 * @param {Number} zoomY Zoom factor in y-axis direction 69 * @param {Number} unitX Units in x-axis direction 70 * @param {Number} unitY Units in y-axis direction 71 * @param {Number} canvasWidth The width of canvas 72 * @param {Number} canvasHeight The height of canvas 73 * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard} 74 * @borrows JXG.EventEmitter#on as this.on 75 * @borrows JXG.EventEmitter#off as this.off 76 * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers 77 * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers 78 */ 79 JXG.Board = function (container, renderer, id, 80 origin, zoomX, zoomY, unitX, unitY, 81 canvasWidth, canvasHeight, attributes) { 82 /** 83 * Board is in no special mode, objects are highlighted on mouse over and objects may be 84 * clicked to start drag&drop. 85 * @type Number 86 * @constant 87 */ 88 this.BOARD_MODE_NONE = 0x0000; 89 90 /** 91 * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in 92 * {@link JXG.Board#mouse} is updated on mouse movement. 93 * @type Number 94 * @constant 95 */ 96 this.BOARD_MODE_DRAG = 0x0001; 97 98 /** 99 * In this mode a mouse move changes the origin's screen coordinates. 100 * @type Number 101 * @constant 102 */ 103 this.BOARD_MODE_MOVE_ORIGIN = 0x0002; 104 105 /** 106 * Update is made with high quality, e.g. graphs are evaluated at much more points. 107 * @type Number 108 * @constant 109 * @see JXG.Board#updateQuality 110 */ 111 this.BOARD_MODE_ZOOM = 0x0011; 112 113 /** 114 * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points. 115 * @type Number 116 * @constant 117 * @see JXG.Board#updateQuality 118 */ 119 this.BOARD_QUALITY_LOW = 0x1; 120 121 /** 122 * Update is made with high quality, e.g. graphs are evaluated at much more points. 123 * @type Number 124 * @constant 125 * @see JXG.Board#updateQuality 126 */ 127 this.BOARD_QUALITY_HIGH = 0x2; 128 129 /** 130 * Pointer to the document element containing the board. 131 * @type Object 132 */ 133 if (Type.exists(attributes.document) && attributes.document !== false) { 134 this.document = attributes.document; 135 } else if (Env.isBrowser) { 136 this.document = document; 137 } 138 139 /** 140 * The html-id of the html element containing the board. 141 * @type String 142 */ 143 this.container = container; 144 145 /** 146 * Pointer to the html element containing the board. 147 * @type Object 148 */ 149 this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null); 150 151 if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) { 152 throw new Error('\nJSXGraph: HTML container element "' + container + '" not found.'); 153 } 154 155 /** 156 * A reference to this boards renderer. 157 * @type JXG.AbstractRenderer 158 * @name JXG.Board#renderer 159 * @private 160 * @ignore 161 */ 162 this.renderer = renderer; 163 164 /** 165 * Grids keeps track of all grids attached to this board. 166 * @type Array 167 * @private 168 */ 169 this.grids = []; 170 171 /** 172 * Some standard options 173 * @type JXG.Options 174 */ 175 this.options = Type.deepCopy(Options); 176 this.attr = attributes; 177 178 /** 179 * Dimension of the board. 180 * @default 2 181 * @type Number 182 */ 183 this.dimension = 2; 184 185 this.jc = new JessieCode(); 186 this.jc.use(this); 187 188 /** 189 * Coordinates of the boards origin. This a object with the two properties 190 * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords 191 * stores the boards origin in homogeneous screen coordinates. 192 * @type Object 193 * @private 194 */ 195 this.origin = {}; 196 this.origin.usrCoords = [1, 0, 0]; 197 this.origin.scrCoords = [1, origin[0], origin[1]]; 198 199 /** 200 * Zoom factor in X direction. It only stores the zoom factor to be able 201 * to get back to 100% in zoom100(). 202 * @name JXG.Board.zoomX 203 * @type Number 204 * @private 205 * @ignore 206 */ 207 this.zoomX = zoomX; 208 209 /** 210 * Zoom factor in Y direction. It only stores the zoom factor to be able 211 * to get back to 100% in zoom100(). 212 * @name JXG.Board.zoomY 213 * @type Number 214 * @private 215 * @ignore 216 */ 217 this.zoomY = zoomY; 218 219 /** 220 * The number of pixels which represent one unit in user-coordinates in x direction. 221 * @type Number 222 * @private 223 */ 224 this.unitX = unitX * this.zoomX; 225 226 /** 227 * The number of pixels which represent one unit in user-coordinates in y direction. 228 * @type Number 229 * @private 230 */ 231 this.unitY = unitY * this.zoomY; 232 233 /** 234 * Keep aspect ratio if bounding box is set and the width/height ratio differs from the 235 * width/height ratio of the canvas. 236 * @type Boolean 237 * @private 238 */ 239 this.keepaspectratio = false; 240 241 /** 242 * Canvas width. 243 * @type Number 244 * @private 245 */ 246 this.canvasWidth = canvasWidth; 247 248 /** 249 * Canvas Height 250 * @type Number 251 * @private 252 */ 253 this.canvasHeight = canvasHeight; 254 255 // If the given id is not valid, generate an unique id 256 if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) { 257 this.id = id; 258 } else { 259 this.id = this.generateId(); 260 } 261 262 EventEmitter.eventify(this); 263 264 this.hooks = []; 265 266 /** 267 * An array containing all other boards that are updated after this board has been updated. 268 * @type Array 269 * @see JXG.Board#addChild 270 * @see JXG.Board#removeChild 271 */ 272 this.dependentBoards = []; 273 274 /** 275 * During the update process this is set to false to prevent an endless loop. 276 * @default false 277 * @type Boolean 278 */ 279 this.inUpdate = false; 280 281 /** 282 * 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. 283 * @type Object 284 */ 285 this.objects = {}; 286 287 /** 288 * An array containing all geometric objects on the board in the order of construction. 289 * @type Array 290 */ 291 this.objectsList = []; 292 293 /** 294 * 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. 295 * @type Object 296 */ 297 this.groups = {}; 298 299 /** 300 * Stores all the objects that are currently running an animation. 301 * @type Object 302 */ 303 this.animationObjects = {}; 304 305 /** 306 * An associative array containing all highlighted elements belonging to the board. 307 * @type Object 308 */ 309 this.highlightedObjects = {}; 310 311 /** 312 * Number of objects ever created on this board. This includes every object, even invisible and deleted ones. 313 * @type Number 314 */ 315 this.numObjects = 0; 316 317 /** 318 * An associative array / dictionary 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. 319 * @type Object 320 */ 321 this.elementsByName = {}; 322 323 /** 324 * The board mode the board is currently in. Possible values are 325 * <ul> 326 * <li>JXG.Board.BOARD_MODE_NONE</li> 327 * <li>JXG.Board.BOARD_MODE_DRAG</li> 328 * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li> 329 * </ul> 330 * @type Number 331 */ 332 this.mode = this.BOARD_MODE_NONE; 333 334 /** 335 * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}. 336 * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to 337 * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of 338 * evaluation points when plotting functions. Possible values are 339 * <ul> 340 * <li>BOARD_QUALITY_LOW</li> 341 * <li>BOARD_QUALITY_HIGH</li> 342 * </ul> 343 * @type Number 344 * @see JXG.Board#mode 345 */ 346 this.updateQuality = this.BOARD_QUALITY_HIGH; 347 348 /** 349 * If true updates are skipped. 350 * @type Boolean 351 */ 352 this.isSuspendedRedraw = false; 353 354 this.calculateSnapSizes(); 355 356 /** 357 * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button. 358 * @type Number 359 * @see JXG.Board#drag_dy 360 */ 361 this.drag_dx = 0; 362 363 /** 364 * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button. 365 * @type Number 366 * @see JXG.Board#drag_dx 367 */ 368 this.drag_dy = 0; 369 370 /** 371 * The last position where a drag event has been fired. 372 * @type Array 373 * @see JXG.Board#moveObject 374 */ 375 this.drag_position = [0, 0]; 376 377 /** 378 * References to the object that is dragged with the mouse on the board. 379 * @type JXG.GeometryElement 380 * @see JXG.Board#touches 381 */ 382 this.mouse = {}; 383 384 /** 385 * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events. 386 * @type Array 387 * @see JXG.Board#mouse 388 */ 389 this.touches = []; 390 391 /** 392 * A string containing the XML text of the construction. 393 * This is set in {@link JXG.FileReader.parseString}. 394 * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File. 395 * @type String 396 */ 397 this.xmlString = ''; 398 399 /** 400 * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations. 401 * @type Array 402 */ 403 this.cPos = []; 404 405 /** 406 * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since 407 * touchStart because Android's Webkit browser fires too much of them. 408 * @type Number 409 */ 410 this.touchMoveLast = 0; 411 412 /** 413 * Contains the pointerId of the last touchMove event which was not thrown away or since 414 * touchStart because Android's Webkit browser fires too much of them. 415 * @type Number 416 */ 417 this.touchMoveLastId = Infinity; 418 419 /** 420 * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away. 421 * @type Number 422 */ 423 this.positionAccessLast = 0; 424 425 /** 426 * Collects all elements that triggered a mouse down event. 427 * @type Array 428 */ 429 this.downObjects = []; 430 431 /** 432 * Collects all elements that have keyboard focus. Should be either one or no element. 433 * Elements are stored with their id. 434 * @type Array 435 */ 436 this.focusObjects = []; 437 438 if (this.attr.showcopyright) { 439 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 440 } 441 442 /** 443 * Full updates are needed after zoom and axis translates. This saves some time during an update. 444 * @default false 445 * @type Boolean 446 */ 447 this.needsFullUpdate = false; 448 449 /** 450 * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following 451 * elements are updated during mouse move. On mouse up the whole construction is 452 * updated. This enables us to be fast even on very slow devices. 453 * @type Boolean 454 * @default false 455 */ 456 this.reducedUpdate = false; 457 458 /** 459 * The current color blindness deficiency is stored in this property. If color blindness is not emulated 460 * at the moment, it's value is 'none'. 461 */ 462 this.currentCBDef = 'none'; 463 464 /** 465 * If GEONExT constructions are displayed, then this property should be set to true. 466 * At the moment there should be no difference. But this may change. 467 * This is set in {@link JXG.GeonextReader#readGeonext}. 468 * @type Boolean 469 * @default false 470 * @see JXG.GeonextReader#readGeonext 471 */ 472 this.geonextCompatibilityMode = false; 473 474 if (this.options.text.useASCIIMathML && translateASCIIMath) { 475 init(); 476 } else { 477 this.options.text.useASCIIMathML = false; 478 } 479 480 /** 481 * A flag which tells if the board registers mouse events. 482 * @type Boolean 483 * @default false 484 */ 485 this.hasMouseHandlers = false; 486 487 /** 488 * A flag which tells if the board registers touch events. 489 * @type Boolean 490 * @default false 491 */ 492 this.hasTouchHandlers = false; 493 494 /** 495 * A flag which stores if the board registered pointer events. 496 * @type Boolean 497 * @default false 498 */ 499 this.hasPointerHandlers = false; 500 501 /** 502 * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered. 503 * @type Boolean 504 * @default false 505 */ 506 this.hasMouseUp = false; 507 508 /** 509 * A flag which tells if the board the JXG.Board#touchEndListener is currently registered. 510 * @type Boolean 511 * @default false 512 */ 513 this.hasTouchEnd = false; 514 515 /** 516 * A flag which tells us if the board has a pointerUp event registered at the moment. 517 * @type Boolean 518 * @default false 519 */ 520 this.hasPointerUp = false; 521 522 /** 523 * Offset for large coords elements like images 524 * @type Array 525 * @private 526 * @default [0, 0] 527 */ 528 this._drag_offset = [0, 0]; 529 530 /** 531 * Stores the input device used in the last down or move event. 532 * @type String 533 * @private 534 * @default 'mouse' 535 */ 536 this._inputDevice = 'mouse'; 537 538 /** 539 * Keeps a list of pointer devices which are currently touching the screen. 540 * @type Array 541 * @private 542 */ 543 this._board_touches = []; 544 545 /** 546 * A flag which tells us if the board is in the selecting mode 547 * @type Boolean 548 * @default false 549 */ 550 this.selectingMode = false; 551 552 /** 553 * A flag which tells us if the user is selecting 554 * @type Boolean 555 * @default false 556 */ 557 this.isSelecting = false; 558 559 /** 560 * A flag which tells us if the user is scrolling the viewport 561 * @type Boolean 562 * @private 563 * @default false 564 * @see JXG.Board#scrollListener 565 */ 566 this._isScrolling = false; 567 568 /** 569 * A flag which tells us if a resize is in process 570 * @type Boolean 571 * @private 572 * @default false 573 * @see JXG.Board#resizeListener 574 */ 575 this._isResizing = false; 576 577 /** 578 * A bounding box for the selection 579 * @type Array 580 * @default [ [0,0], [0,0] ] 581 */ 582 this.selectingBox = [[0, 0], [0, 0]]; 583 584 /** 585 * Array to log user activity. 586 * Entries are objects of the form '{type, id, start, end}' notifying 587 * the start time as well as the last time of a single event of type 'type' 588 * on a JSXGraph element of id 'id'. 589 * <p> 'start' and 'end' contain the amount of milliseconds elapsed between 1 January 1970 00:00:00 UTC 590 * and the time the event happened. 591 * <p> 592 * For the time being (i.e. v1.5.0) the only supported type is 'drag'. 593 * @type Array 594 */ 595 this.userLog = []; 596 597 this.mathLib = Math; // Math or JXG.Math.IntervalArithmetic 598 this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic 599 600 if (this.attr.registerevents) { 601 this.addEventHandlers(); 602 } 603 if (this.attr.registerresizeevent) { 604 this.addResizeEventHandlers(); 605 } 606 if (this.attr.registerfullscreenevent) { 607 this.addFullscreenEventHandlers(); 608 } 609 610 this.methodMap = { 611 update: 'update', 612 fullUpdate: 'fullUpdate', 613 on: 'on', 614 off: 'off', 615 trigger: 'trigger', 616 setView: 'setBoundingBox', 617 setBoundingBox: 'setBoundingBox', 618 migratePoint: 'migratePoint', 619 colorblind: 'emulateColorblindness', 620 suspendUpdate: 'suspendUpdate', 621 unsuspendUpdate: 'unsuspendUpdate', 622 clearTraces: 'clearTraces', 623 left: 'clickLeftArrow', 624 right: 'clickRightArrow', 625 up: 'clickUpArrow', 626 down: 'clickDownArrow', 627 zoomIn: 'zoomIn', 628 zoomOut: 'zoomOut', 629 zoom100: 'zoom100', 630 zoomElements: 'zoomElements', 631 remove: 'removeObject', 632 removeObject: 'removeObject' 633 }; 634 }; 635 636 JXG.extend( 637 JXG.Board.prototype, 638 /** @lends JXG.Board.prototype */ { 639 /** 640 * Generates an unique name for the given object. The result depends on the objects type, if the 641 * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line} 642 * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower 643 * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is 644 * generated using lower case characters. prefixed with k_ is used. In any other case, lower case 645 * chars prefixed with s_ is used. 646 * @param {Object} object Reference of an JXG.GeometryElement that is to be named. 647 * @returns {String} Unique name for the object. 648 */ 649 generateName: function (object) { 650 var possibleNames, i, 651 maxNameLength = this.attr.maxnamelength, 652 pre = '', 653 post = '', 654 indices = [], 655 name = ''; 656 657 if (object.type === Const.OBJECT_TYPE_TICKS) { 658 return ''; 659 } 660 661 if (Type.isPoint(object) || Type.isPoint3D(object)) { 662 // points have capital letters 663 possibleNames = [ 664 '', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' 665 ]; 666 } else if (object.type === Const.OBJECT_TYPE_ANGLE) { 667 possibleNames = [ 668 '', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 669 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω' 670 ]; 671 } else { 672 // all other elements get lowercase labels 673 possibleNames = [ 674 '', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' 675 ]; 676 } 677 678 if ( 679 !Type.isPoint(object) && 680 object.elementClass !== Const.OBJECT_CLASS_LINE && 681 object.type !== Const.OBJECT_TYPE_ANGLE 682 ) { 683 if (object.type === Const.OBJECT_TYPE_POLYGON) { 684 pre = 'P_{'; 685 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) { 686 pre = 'k_{'; 687 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) { 688 pre = 't_{'; 689 } else { 690 pre = 's_{'; 691 } 692 post = '}'; 693 } 694 695 for (i = 0; i < maxNameLength; i++) { 696 indices[i] = 0; 697 } 698 699 while (indices[maxNameLength - 1] < possibleNames.length) { 700 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) { 701 name = pre; 702 703 for (i = maxNameLength; i > 0; i--) { 704 name += possibleNames[indices[i - 1]]; 705 } 706 707 if (!Type.exists(this.elementsByName[name + post])) { 708 return name + post; 709 } 710 } 711 indices[0] = possibleNames.length; 712 713 for (i = 1; i < maxNameLength; i++) { 714 if (indices[i - 1] === possibleNames.length) { 715 indices[i - 1] = 1; 716 indices[i] += 1; 717 } 718 } 719 } 720 721 return ''; 722 }, 723 724 /** 725 * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'. 726 * @returns {String} Unique id for a board. 727 */ 728 generateId: function () { 729 var r = 1; 730 731 // as long as we don't have a unique id generate a new one 732 while (Type.exists(JXG.boards['jxgBoard' + r])) { 733 r = Math.round(Math.random() * 65535); 734 } 735 736 return 'jxgBoard' + r; 737 }, 738 739 /** 740 * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the 741 * object type. As a side effect {@link JXG.Board#numObjects} 742 * is updated. 743 * @param {Object} obj Reference of an geometry object that needs an id. 744 * @param {Number} type Type of the object. 745 * @returns {String} Unique id for an element. 746 */ 747 setId: function (obj, type) { 748 var randomNumber, 749 num = this.numObjects, 750 elId = obj.id; 751 752 this.numObjects += 1; 753 754 // If no id is provided or id is empty string, a new one is chosen 755 if (elId === '' || !Type.exists(elId)) { 756 elId = this.id + type + num; 757 while (Type.exists(this.objects[elId])) { 758 randomNumber = Math.round(Math.random() * 65535); 759 elId = this.id + type + num + '-' + randomNumber; 760 } 761 } 762 763 obj.id = elId; 764 this.objects[elId] = obj; 765 obj._pos = this.objectsList.length; 766 this.objectsList[this.objectsList.length] = obj; 767 768 return elId; 769 }, 770 771 /** 772 * After construction of the object the visibility is set 773 * and the label is constructed if necessary. 774 * @param {Object} obj The object to add. 775 */ 776 finalizeAdding: function (obj) { 777 if (Type.evaluate(obj.visProp.visible) === false) { 778 this.renderer.display(obj, false); 779 } 780 }, 781 782 finalizeLabel: function (obj) { 783 if ( 784 obj.hasLabel && 785 !Type.evaluate(obj.label.visProp.islabel) && 786 Type.evaluate(obj.label.visProp.visible) === false 787 ) { 788 this.renderer.display(obj.label, false); 789 } 790 }, 791 792 /********************************************************** 793 * 794 * Event Handler helpers 795 * 796 **********************************************************/ 797 798 /** 799 * Returns false if the event has been triggered faster than the maximum frame rate. 800 * 801 * @param {Event} evt Event object given by the browser (unused) 802 * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned. 803 * @private 804 * @see JXG.Board#pointerMoveListener 805 * @see JXG.Board#touchMoveListener 806 * @see JXG.Board#mouseMoveListener 807 */ 808 checkFrameRate: function (evt) { 809 var handleEvt = false, 810 time = new Date().getTime(); 811 812 if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) { 813 handleEvt = true; 814 this.touchMoveLastId = evt.pointerId; 815 } 816 if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) { 817 handleEvt = true; 818 } 819 if (handleEvt) { 820 this.touchMoveLast = time; 821 } 822 return handleEvt; 823 }, 824 825 /** 826 * Calculates mouse coordinates relative to the boards container. 827 * @returns {Array} Array of coordinates relative the boards container top left corner. 828 */ 829 getCoordsTopLeftCorner: function () { 830 var cPos, 831 doc, 832 crect, 833 // In ownerDoc we need the 'real' document object. 834 // The first version is used in the case of shadowDom, 835 // the second case in the 'normal' case. 836 ownerDoc = this.document.ownerDocument || this.document, 837 docElement = ownerDoc.documentElement || this.document.body.parentNode, 838 docBody = ownerDoc.body, 839 container = this.containerObj, 840 // viewport, content, 841 zoom, 842 o; 843 844 /** 845 * During drags and origin moves the container element is usually not changed. 846 * Check the position of the upper left corner at most every 1000 msecs 847 */ 848 if ( 849 this.cPos.length > 0 && 850 (this.mode === this.BOARD_MODE_DRAG || 851 this.mode === this.BOARD_MODE_MOVE_ORIGIN || 852 new Date().getTime() - this.positionAccessLast < 1000) 853 ) { 854 return this.cPos; 855 } 856 this.positionAccessLast = new Date().getTime(); 857 858 // Check if getBoundingClientRect exists. If so, use this as this covers *everything* 859 // even CSS3D transformations etc. 860 // Supported by all browsers but IE 6, 7. 861 862 if (container.getBoundingClientRect) { 863 crect = container.getBoundingClientRect(); 864 865 zoom = 1.0; 866 // Recursively search for zoom style entries. 867 // This is necessary for reveal.js on webkit. 868 // It fails if the user does zooming 869 o = container; 870 while (o && Type.exists(o.parentNode)) { 871 if ( 872 Type.exists(o.style) && 873 Type.exists(o.style.zoom) && 874 o.style.zoom !== '' 875 ) { 876 zoom *= parseFloat(o.style.zoom); 877 } 878 o = o.parentNode; 879 } 880 cPos = [crect.left * zoom, crect.top * zoom]; 881 882 // add border width 883 cPos[0] += Env.getProp(container, 'border-left-width'); 884 cPos[1] += Env.getProp(container, 'border-top-width'); 885 886 // vml seems to ignore paddings 887 if (this.renderer.type !== 'vml') { 888 // add padding 889 cPos[0] += Env.getProp(container, 'padding-left'); 890 cPos[1] += Env.getProp(container, 'padding-top'); 891 } 892 893 this.cPos = cPos.slice(); 894 return this.cPos; 895 } 896 897 // 898 // OLD CODE 899 // IE 6-7 only: 900 // 901 cPos = Env.getOffset(container); 902 doc = this.document.documentElement.ownerDocument; 903 904 if (!this.containerObj.currentStyle && doc.defaultView) { 905 // Non IE 906 // this is for hacks like this one used in wordpress for the admin bar: 907 // html { margin-top: 28px } 908 // seems like it doesn't work in IE 909 910 cPos[0] += Env.getProp(docElement, 'margin-left'); 911 cPos[1] += Env.getProp(docElement, 'margin-top'); 912 913 cPos[0] += Env.getProp(docElement, 'border-left-width'); 914 cPos[1] += Env.getProp(docElement, 'border-top-width'); 915 916 cPos[0] += Env.getProp(docElement, 'padding-left'); 917 cPos[1] += Env.getProp(docElement, 'padding-top'); 918 } 919 920 if (docBody) { 921 cPos[0] += Env.getProp(docBody, 'left'); 922 cPos[1] += Env.getProp(docBody, 'top'); 923 } 924 925 // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX 926 // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly 927 // available version so we're doing it the hacky way: Add a fixed offset. 928 // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J 929 if (typeof google === 'object' && google.translate) { 930 cPos[0] += 10; 931 cPos[1] += 25; 932 } 933 934 // add border width 935 cPos[0] += Env.getProp(container, 'border-left-width'); 936 cPos[1] += Env.getProp(container, 'border-top-width'); 937 938 // vml seems to ignore paddings 939 if (this.renderer.type !== 'vml') { 940 // add padding 941 cPos[0] += Env.getProp(container, 'padding-left'); 942 cPos[1] += Env.getProp(container, 'padding-top'); 943 } 944 945 cPos[0] += this.attr.offsetx; 946 cPos[1] += this.attr.offsety; 947 948 this.cPos = cPos.slice(); 949 return this.cPos; 950 }, 951 952 /** 953 * Get the position of the pointing device in screen coordinates, relative to the upper left corner 954 * of the host tag. 955 * @param {Event} e Event object given by the browser. 956 * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set 957 * for mouseevents. 958 * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords} 959 */ 960 getMousePosition: function (e, i) { 961 var cPos = this.getCoordsTopLeftCorner(), 962 absPos, 963 v; 964 965 // Position of cursor using clientX/Y 966 absPos = Env.getPosition(e, i, this.document); 967 968 /** 969 * In case there has been no down event before. 970 */ 971 if (!Type.exists(this.cssTransMat)) { 972 this.updateCSSTransforms(); 973 } 974 // Position relative to the top left corner 975 v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]]; 976 v = Mat.matVecMult(this.cssTransMat, v); 977 v[1] /= v[0]; 978 v[2] /= v[0]; 979 return [v[1], v[2]]; 980 981 // Method without CSS transformation 982 /* 983 return [absPos[0] - cPos[0], absPos[1] - cPos[1]]; 984 */ 985 }, 986 987 /** 988 * Initiate moving the origin. This is used in mouseDown and touchStart listeners. 989 * @param {Number} x Current mouse/touch coordinates 990 * @param {Number} y Current mouse/touch coordinates 991 */ 992 initMoveOrigin: function (x, y) { 993 this.drag_dx = x - this.origin.scrCoords[1]; 994 this.drag_dy = y - this.origin.scrCoords[2]; 995 996 this.mode = this.BOARD_MODE_MOVE_ORIGIN; 997 this.updateQuality = this.BOARD_QUALITY_LOW; 998 }, 999 1000 /** 1001 * Collects all elements below the current mouse pointer and fulfilling the following constraints: 1002 * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul> 1003 * @param {Number} x Current mouse/touch coordinates 1004 * @param {Number} y current mouse/touch coordinates 1005 * @param {Object} evt An event object 1006 * @param {String} type What type of event? 'touch', 'mouse' or 'pen'. 1007 * @returns {Array} A list of geometric elements. 1008 */ 1009 initMoveObject: function (x, y, evt, type) { 1010 var pEl, 1011 el, 1012 collect = [], 1013 offset = [], 1014 haspoint, 1015 len = this.objectsList.length, 1016 dragEl = { visProp: { layer: -10000 } }; 1017 1018 // Store status of key presses for 3D movement 1019 this._shiftKey = evt.shiftKey; 1020 this._ctrlKey = evt.ctrlKey; 1021 1022 //for (el in this.objects) { 1023 for (el = 0; el < len; el++) { 1024 pEl = this.objectsList[el]; 1025 haspoint = pEl.hasPoint && pEl.hasPoint(x, y); 1026 1027 if (pEl.visPropCalc.visible && haspoint) { 1028 pEl.triggerEventHandlers([type + 'down', 'down'], [evt]); 1029 this.downObjects.push(pEl); 1030 } 1031 1032 if (haspoint && 1033 pEl.isDraggable && 1034 pEl.visPropCalc.visible && 1035 ((this.geonextCompatibilityMode && 1036 (Type.isPoint(pEl) || pEl.elementClass === Const.OBJECT_CLASS_TEXT)) || 1037 !this.geonextCompatibilityMode) && 1038 !Type.evaluate(pEl.visProp.fixed) 1039 /*(!pEl.visProp.frozen) &&*/ 1040 ) { 1041 // Elements in the highest layer get priority. 1042 if ( 1043 pEl.visProp.layer > dragEl.visProp.layer || 1044 (pEl.visProp.layer === dragEl.visProp.layer && 1045 pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime()) 1046 ) { 1047 // If an element and its label have the focus 1048 // simultaneously, the element is taken. 1049 // This only works if we assume that every browser runs 1050 // through this.objects in the right order, i.e. an element A 1051 // added before element B turns up here before B does. 1052 if ( 1053 !this.attr.ignorelabels || 1054 !Type.exists(dragEl.label) || 1055 pEl !== dragEl.label 1056 ) { 1057 dragEl = pEl; 1058 collect.push(dragEl); 1059 // Save offset for large coords elements. 1060 if (Type.exists(dragEl.coords)) { 1061 offset.push( 1062 Statistics.subtract(dragEl.coords.scrCoords.slice(1), [ 1063 x, 1064 y 1065 ]) 1066 ); 1067 } else { 1068 offset.push([0, 0]); 1069 } 1070 1071 // we can't drop out of this loop because of the event handling system 1072 //if (this.attr.takefirst) { 1073 // return collect; 1074 //} 1075 } 1076 } 1077 } 1078 } 1079 1080 if (this.attr.drag.enabled && collect.length > 0) { 1081 this.mode = this.BOARD_MODE_DRAG; 1082 } 1083 1084 // A one-element array is returned. 1085 if (this.attr.takefirst) { 1086 collect.length = 1; 1087 this._drag_offset = offset[0]; 1088 } else { 1089 collect = collect.slice(-1); 1090 this._drag_offset = offset[offset.length - 1]; 1091 } 1092 1093 if (!this._drag_offset) { 1094 this._drag_offset = [0, 0]; 1095 } 1096 1097 // Move drag element to the top of the layer 1098 if (this.renderer.type === 'svg' && 1099 Type.exists(collect[0]) && 1100 Type.evaluate(collect[0].visProp.dragtotopoflayer) && 1101 collect.length === 1 && 1102 Type.exists(collect[0].rendNode) 1103 ) { 1104 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode); 1105 } 1106 1107 // // Init rotation angle and scale factor for two finger movements 1108 // this.previousRotation = 0.0; 1109 // this.previousScale = 1.0; 1110 1111 if (collect.length >= 1) { 1112 collect[0].highlight(true); 1113 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]); 1114 } 1115 1116 return collect; 1117 }, 1118 1119 /** 1120 * Moves an object. 1121 * @param {Number} x Coordinate 1122 * @param {Number} y Coordinate 1123 * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}. 1124 * @param {Object} evt The event object. 1125 * @param {String} type Mouse or touch event? 1126 */ 1127 moveObject: function (x, y, o, evt, type) { 1128 var newPos = new Coords( 1129 Const.COORDS_BY_SCREEN, 1130 this.getScrCoordsOfMouse(x, y), 1131 this 1132 ), 1133 drag, 1134 dragScrCoords, 1135 newDragScrCoords; 1136 1137 if (!(o && o.obj)) { 1138 return; 1139 } 1140 drag = o.obj; 1141 1142 // Avoid updates for very small movements of coordsElements, see below 1143 if (drag.coords) { 1144 dragScrCoords = drag.coords.scrCoords.slice(); 1145 } 1146 1147 this.addLogEntry('drag', drag, newPos.usrCoords.slice(1)); 1148 1149 // Store the position. 1150 this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]]; 1151 this.drag_position = Statistics.add(this.drag_position, this._drag_offset); 1152 1153 // Store status of key presses for 3D movement 1154 this._shiftKey = evt.shiftKey; 1155 this._ctrlKey = evt.ctrlKey; 1156 1157 // 1158 // We have to distinguish between CoordsElements and other elements like lines. 1159 // The latter need the difference between two move events. 1160 if (Type.exists(drag.coords)) { 1161 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position); 1162 } else { 1163 this.displayInfobox(false); 1164 // Hide infobox in case the user has touched an intersection point 1165 // and drags the underlying line now. 1166 1167 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) { 1168 drag.setPositionDirectly( 1169 Const.COORDS_BY_SCREEN, 1170 [newPos.scrCoords[1], newPos.scrCoords[2]], 1171 [o.targets[0].Xprev, o.targets[0].Yprev] 1172 ); 1173 } 1174 // Remember the actual position for the next move event. Then we are able to 1175 // compute the difference vector. 1176 o.targets[0].Xprev = newPos.scrCoords[1]; 1177 o.targets[0].Yprev = newPos.scrCoords[2]; 1178 } 1179 // This may be necessary for some gliders and labels 1180 if (Type.exists(drag.coords)) { 1181 drag.prepareUpdate().update(false).updateRenderer(); 1182 this.updateInfobox(drag); 1183 drag.prepareUpdate().update(true).updateRenderer(); 1184 } 1185 1186 if (drag.coords) { 1187 newDragScrCoords = drag.coords.scrCoords; 1188 } 1189 // No updates for very small movements of coordsElements 1190 if ( 1191 !drag.coords || 1192 dragScrCoords[1] !== newDragScrCoords[1] || 1193 dragScrCoords[2] !== newDragScrCoords[2] 1194 ) { 1195 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]); 1196 1197 this.update(); 1198 } 1199 drag.highlight(true); 1200 this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]); 1201 1202 drag.lastDragTime = new Date(); 1203 }, 1204 1205 /** 1206 * Moves elements in multitouch mode. 1207 * @param {Array} p1 x,y coordinates of first touch 1208 * @param {Array} p2 x,y coordinates of second touch 1209 * @param {Object} o The touch object that is dragged: {JXG.Board#touches}. 1210 * @param {Object} evt The event object that lead to this movement. 1211 */ 1212 twoFingerMove: function (o, id, evt) { 1213 var drag; 1214 1215 if (Type.exists(o) && Type.exists(o.obj)) { 1216 drag = o.obj; 1217 } else { 1218 return; 1219 } 1220 1221 if ( 1222 drag.elementClass === Const.OBJECT_CLASS_LINE || 1223 drag.type === Const.OBJECT_TYPE_POLYGON 1224 ) { 1225 this.twoFingerTouchObject(o.targets, drag, id); 1226 } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1227 this.twoFingerTouchCircle(o.targets, drag, id); 1228 } 1229 1230 if (evt) { 1231 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]); 1232 } 1233 }, 1234 1235 /** 1236 * Compute the transformation matrix to move an element according to the 1237 * previous and actual positions of finger 1 and finger 2. 1238 * See also https://math.stackexchange.com/questions/4010538/solve-for-2d-translation-rotation-and-scale-given-two-touch-point-movements 1239 * 1240 * @param {Object} finger1 Actual and previous position of finger 1 1241 * @param {Object} finger1 Actual and previous position of finger 1 1242 * @param {Boolean} scalable Flag if element may be scaled 1243 * @param {Boolean} rotatable Flag if element may be rotated 1244 * @returns 1245 */ 1246 getTwoFingerTransform(finger1, finger2, scalable, rotatable) { 1247 var crd, 1248 x1, y1, x2, y2, 1249 dx, dy, 1250 xx1, yy1, xx2, yy2, 1251 dxx, dyy, 1252 C, S, LL, tx, ty, lbda; 1253 1254 crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.Xprev, finger1.Yprev], this).usrCoords; 1255 x1 = crd[1]; 1256 y1 = crd[2]; 1257 crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.Xprev, finger2.Yprev], this).usrCoords; 1258 x2 = crd[1]; 1259 y2 = crd[2]; 1260 1261 crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.X, finger1.Y], this).usrCoords; 1262 xx1 = crd[1]; 1263 yy1 = crd[2]; 1264 crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.X, finger2.Y], this).usrCoords; 1265 xx2 = crd[1]; 1266 yy2 = crd[2]; 1267 1268 dx = x2 - x1; 1269 dy = y2 - y1; 1270 dxx = xx2 - xx1; 1271 dyy = yy2 - yy1; 1272 1273 LL = dx * dx + dy * dy; 1274 C = (dxx * dx + dyy * dy) / LL; 1275 S = (dyy * dx - dxx * dy) / LL; 1276 if (!scalable) { 1277 lbda = Math.sqrt(C * C + S * S); 1278 C /= lbda; 1279 S /= lbda; 1280 } 1281 if (!rotatable) { 1282 S = 0; 1283 } 1284 tx = 0.5 * (xx1 + xx2 - C * (x1 + x2) + S * (y1 + y2)); 1285 ty = 0.5 * (yy1 + yy2 - S * (x1 + x2) - C * (y1 + y2)); 1286 1287 return [1, 0, 0, 1288 tx, C, -S, 1289 ty, S, C]; 1290 }, 1291 1292 /** 1293 * Moves, rotates and scales a line or polygon with two fingers. 1294 * <p> 1295 * If one vertex of the polygon snaps to the grid or to points or is not draggable, 1296 * two-finger-movement is cancelled. 1297 * 1298 * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}. 1299 * @param {object} drag The object that is dragged: 1300 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1301 */ 1302 twoFingerTouchObject: function (tar, drag, id) { 1303 var t, T, 1304 ar, i, len, vp, 1305 snap = false; 1306 1307 if ( 1308 Type.exists(tar[0]) && 1309 Type.exists(tar[1]) && 1310 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev) 1311 ) { 1312 1313 T = this.getTwoFingerTransform( 1314 tar[0], tar[1], 1315 Type.evaluate(drag.visProp.scalable), 1316 Type.evaluate(drag.visProp.rotatable)); 1317 t = this.create('transform', T, { type: 'generic' }); 1318 t.update(); 1319 1320 if (drag.elementClass === Const.OBJECT_CLASS_LINE) { 1321 ar = []; 1322 if (drag.point1.draggable()) { 1323 ar.push(drag.point1); 1324 } 1325 if (drag.point2.draggable()) { 1326 ar.push(drag.point2); 1327 } 1328 t.applyOnce(ar); 1329 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) { 1330 len = drag.vertices.length - 1; 1331 vp = drag.visProp; 1332 snap = Type.evaluate(vp.snaptogrid) || Type.evaluate(vp.snaptopoints); 1333 for (i = 0; i < len && !snap; ++i) { 1334 vp = drag.vertices[i].visProp; 1335 snap = snap || Type.evaluate(vp.snaptogrid) || Type.evaluate(vp.snaptopoints); 1336 snap = snap || (!drag.vertices[i].draggable()) 1337 } 1338 if (!snap) { 1339 ar = []; 1340 for (i = 0; i < len; ++i) { 1341 if (drag.vertices[i].draggable()) { 1342 ar.push(drag.vertices[i]); 1343 } 1344 } 1345 t.applyOnce(ar); 1346 } 1347 } 1348 1349 this.update(); 1350 drag.highlight(true); 1351 } 1352 }, 1353 1354 /* 1355 * Moves, rotates and scales a circle with two fingers. 1356 * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}. 1357 * @param {object} drag The object that is dragged: 1358 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1359 */ 1360 twoFingerTouchCircle: function (tar, drag, id) { 1361 var fixEl, moveEl, np, op, fix, d, alpha, t1, t2, t3, t4; 1362 1363 if (drag.method === 'pointCircle' || drag.method === 'pointLine') { 1364 return; 1365 } 1366 1367 if ( 1368 Type.exists(tar[0]) && 1369 Type.exists(tar[1]) && 1370 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev) 1371 ) { 1372 if (id === tar[0].num) { 1373 fixEl = tar[1]; 1374 moveEl = tar[0]; 1375 } else { 1376 fixEl = tar[0]; 1377 moveEl = tar[1]; 1378 } 1379 1380 fix = new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this) 1381 .usrCoords; 1382 // Previous finger position 1383 op = new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this) 1384 .usrCoords; 1385 // New finger position 1386 np = new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this).usrCoords; 1387 1388 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1)); 1389 1390 // Rotate and scale by the movement of the second finger 1391 t1 = this.create('transform', [-fix[1], -fix[2]], { 1392 type: 'translate' 1393 }); 1394 t2 = this.create('transform', [alpha], { type: 'rotate' }); 1395 t1.melt(t2); 1396 if (Type.evaluate(drag.visProp.scalable)) { 1397 d = Geometry.distance(fix, np) / Geometry.distance(fix, op); 1398 t3 = this.create('transform', [d, d], { type: 'scale' }); 1399 t1.melt(t3); 1400 } 1401 t4 = this.create('transform', [fix[1], fix[2]], { 1402 type: 'translate' 1403 }); 1404 t1.melt(t4); 1405 1406 if (drag.center.draggable()) { 1407 t1.applyOnce([drag.center]); 1408 } 1409 1410 if (drag.method === 'twoPoints') { 1411 if (drag.point2.draggable()) { 1412 t1.applyOnce([drag.point2]); 1413 } 1414 } else if (drag.method === 'pointRadius') { 1415 if (Type.isNumber(drag.updateRadius.origin)) { 1416 drag.setRadius(drag.radius * d); 1417 } 1418 } 1419 1420 this.update(drag.center); 1421 drag.highlight(true); 1422 } 1423 }, 1424 1425 highlightElements: function (x, y, evt, target) { 1426 var el, 1427 pEl, 1428 pId, 1429 overObjects = {}, 1430 len = this.objectsList.length; 1431 1432 // Elements below the mouse pointer which are not highlighted yet will be highlighted. 1433 for (el = 0; el < len; el++) { 1434 pEl = this.objectsList[el]; 1435 pId = pEl.id; 1436 if ( 1437 Type.exists(pEl.hasPoint) && 1438 pEl.visPropCalc.visible && 1439 pEl.hasPoint(x, y) 1440 ) { 1441 // this is required in any case because otherwise the box won't be shown until the point is dragged 1442 this.updateInfobox(pEl); 1443 1444 if (!Type.exists(this.highlightedObjects[pId])) { 1445 // highlight only if not highlighted 1446 overObjects[pId] = pEl; 1447 pEl.highlight(); 1448 // triggers board event. 1449 this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]); 1450 } 1451 1452 if (pEl.mouseover) { 1453 pEl.triggerEventHandlers(['mousemove', 'move'], [evt]); 1454 } else { 1455 pEl.triggerEventHandlers(['mouseover', 'over'], [evt]); 1456 pEl.mouseover = true; 1457 } 1458 } 1459 } 1460 1461 for (el = 0; el < len; el++) { 1462 pEl = this.objectsList[el]; 1463 pId = pEl.id; 1464 if (pEl.mouseover) { 1465 if (!overObjects[pId]) { 1466 pEl.triggerEventHandlers(['mouseout', 'out'], [evt]); 1467 pEl.mouseover = false; 1468 } 1469 } 1470 } 1471 }, 1472 1473 /** 1474 * Helper function which returns a reasonable starting point for the object being dragged. 1475 * Formerly known as initXYstart(). 1476 * @private 1477 * @param {JXG.GeometryElement} obj The object to be dragged 1478 * @param {Array} targets Array of targets. It is changed by this function. 1479 */ 1480 saveStartPos: function (obj, targets) { 1481 var xy = [], 1482 i, 1483 len; 1484 1485 if (obj.type === Const.OBJECT_TYPE_TICKS) { 1486 xy.push([1, NaN, NaN]); 1487 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) { 1488 xy.push(obj.point1.coords.usrCoords); 1489 xy.push(obj.point2.coords.usrCoords); 1490 } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1491 xy.push(obj.center.coords.usrCoords); 1492 if (obj.method === 'twoPoints') { 1493 xy.push(obj.point2.coords.usrCoords); 1494 } 1495 } else if (obj.type === Const.OBJECT_TYPE_POLYGON) { 1496 len = obj.vertices.length - 1; 1497 for (i = 0; i < len; i++) { 1498 xy.push(obj.vertices[i].coords.usrCoords); 1499 } 1500 } else if (obj.type === Const.OBJECT_TYPE_SECTOR) { 1501 xy.push(obj.point1.coords.usrCoords); 1502 xy.push(obj.point2.coords.usrCoords); 1503 xy.push(obj.point3.coords.usrCoords); 1504 } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) { 1505 xy.push(obj.coords.usrCoords); 1506 } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) { 1507 // if (Type.exists(obj.parents)) { 1508 // len = obj.parents.length; 1509 // if (len > 0) { 1510 // for (i = 0; i < len; i++) { 1511 // xy.push(this.select(obj.parents[i]).coords.usrCoords); 1512 // } 1513 // } else 1514 // } 1515 if (obj.points.length > 0) { 1516 xy.push(obj.points[0].usrCoords); 1517 } 1518 } else { 1519 try { 1520 xy.push(obj.coords.usrCoords); 1521 } catch (e) { 1522 JXG.debug( 1523 'JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e 1524 ); 1525 } 1526 } 1527 1528 len = xy.length; 1529 for (i = 0; i < len; i++) { 1530 targets.Zstart.push(xy[i][0]); 1531 targets.Xstart.push(xy[i][1]); 1532 targets.Ystart.push(xy[i][2]); 1533 } 1534 }, 1535 1536 mouseOriginMoveStart: function (evt) { 1537 var r, pos; 1538 1539 r = this._isRequiredKeyPressed(evt, 'pan'); 1540 if (r) { 1541 pos = this.getMousePosition(evt); 1542 this.initMoveOrigin(pos[0], pos[1]); 1543 } 1544 1545 return r; 1546 }, 1547 1548 mouseOriginMove: function (evt) { 1549 var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN, 1550 pos; 1551 1552 if (r) { 1553 pos = this.getMousePosition(evt); 1554 this.moveOrigin(pos[0], pos[1], true); 1555 } 1556 1557 return r; 1558 }, 1559 1560 /** 1561 * Start moving the origin with one finger. 1562 * @private 1563 * @param {Object} evt Event from touchStartListener 1564 * @return {Boolean} returns if the origin is moved. 1565 */ 1566 touchStartMoveOriginOneFinger: function (evt) { 1567 var touches = evt[JXG.touchProperty], 1568 conditions, 1569 pos; 1570 1571 conditions = 1572 this.attr.pan.enabled && !this.attr.pan.needtwofingers && touches.length === 1; 1573 1574 if (conditions) { 1575 pos = this.getMousePosition(evt, 0); 1576 this.initMoveOrigin(pos[0], pos[1]); 1577 } 1578 1579 return conditions; 1580 }, 1581 1582 /** 1583 * Move the origin with one finger 1584 * @private 1585 * @param {Object} evt Event from touchMoveListener 1586 * @return {Boolean} returns if the origin is moved. 1587 */ 1588 touchOriginMove: function (evt) { 1589 var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN, 1590 pos; 1591 1592 if (r) { 1593 pos = this.getMousePosition(evt, 0); 1594 this.moveOrigin(pos[0], pos[1], true); 1595 } 1596 1597 return r; 1598 }, 1599 1600 /** 1601 * Stop moving the origin with one finger 1602 * @return {null} null 1603 * @private 1604 */ 1605 originMoveEnd: function () { 1606 this.updateQuality = this.BOARD_QUALITY_HIGH; 1607 this.mode = this.BOARD_MODE_NONE; 1608 }, 1609 1610 /********************************************************** 1611 * 1612 * Event Handler 1613 * 1614 **********************************************************/ 1615 1616 /** 1617 * Add all possible event handlers to the board object 1618 * which move objects, i.e. mouse, pointer and touch events. 1619 */ 1620 addEventHandlers: function () { 1621 if (Env.supportsPointerEvents()) { 1622 this.addPointerEventHandlers(); 1623 } else { 1624 this.addMouseEventHandlers(); 1625 this.addTouchEventHandlers(); 1626 } 1627 1628 // This one produces errors on IE 1629 // // Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this); 1630 // This one works on IE, Firefox and Chromium with default configurations. On some Safari 1631 // or Opera versions the user must explicitly allow the deactivation of the context menu. 1632 if (this.containerObj !== null) { 1633 this.containerObj.oncontextmenu = function (e) { 1634 if (Type.exists(e)) { 1635 e.preventDefault(); 1636 } 1637 return false; 1638 }; 1639 } 1640 1641 this.addKeyboardEventHandlers(); 1642 }, 1643 1644 /** 1645 * Add resize event handlers 1646 * 1647 */ 1648 addResizeEventHandlers: function () { 1649 if (Env.isBrowser) { 1650 try { 1651 // Supported by all new browsers 1652 // resizeObserver: triggered if size of the JSXGraph div changes. 1653 this.startResizeObserver(); 1654 } catch (err) { 1655 // Certain Safari and edge version do not support 1656 // resizeObserver, but intersectionObserver. 1657 // resize event: triggered if size of window changes 1658 Env.addEvent(window, 'resize', this.resizeListener, this); 1659 // intersectionObserver: triggered if JSXGraph becomes visible. 1660 this.startIntersectionObserver(); 1661 } 1662 // Scroll event: needs to be captured since on mobile devices 1663 // sometimes a header bar is displayed / hidden, which triggers a 1664 // resize event. 1665 Env.addEvent(window, 'scroll', this.scrollListener, this); 1666 } 1667 }, 1668 1669 /** 1670 * Remove all event handlers from the board object 1671 */ 1672 removeEventHandlers: function () { 1673 this.removeMouseEventHandlers(); 1674 this.removeTouchEventHandlers(); 1675 this.removePointerEventHandlers(); 1676 1677 this.removeFullscreenEventHandlers(); 1678 this.removeKeyboardEventHandlers(); 1679 1680 if (Env.isBrowser) { 1681 if (Type.exists(this.resizeObserver)) { 1682 this.stopResizeObserver(); 1683 } else { 1684 Env.removeEvent(window, 'resize', this.resizeListener, this); 1685 this.stopIntersectionObserver(); 1686 } 1687 Env.removeEvent(window, 'scroll', this.scrollListener, this); 1688 } 1689 }, 1690 1691 /** 1692 * Registers pointer event handlers. 1693 */ 1694 addPointerEventHandlers: function () { 1695 if (!this.hasPointerHandlers && Env.isBrowser) { 1696 var moveTarget = this.attr.movetarget || this.containerObj; 1697 1698 if (window.navigator.msPointerEnabled) { 1699 // IE10- 1700 Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1701 Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 1702 } else { 1703 Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1704 Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 1705 Env.addEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 1706 } 1707 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1708 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1709 1710 if (this.containerObj !== null) { 1711 // This is needed for capturing touch events. 1712 // It is in jsxgraph.css, for ms-touch-action... 1713 this.containerObj.style.touchAction = 'none'; 1714 } 1715 1716 this.hasPointerHandlers = true; 1717 } 1718 }, 1719 1720 /** 1721 * Registers mouse move, down and wheel event handlers. 1722 */ 1723 addMouseEventHandlers: function () { 1724 if (!this.hasMouseHandlers && Env.isBrowser) { 1725 var moveTarget = this.attr.movetarget || this.containerObj; 1726 1727 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1728 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 1729 1730 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1731 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1732 1733 this.hasMouseHandlers = true; 1734 } 1735 }, 1736 1737 /** 1738 * Register touch start and move and gesture start and change event handlers. 1739 * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers 1740 * will not be registered. 1741 * 1742 * Since iOS 13, touch events were abandoned in favour of pointer events 1743 */ 1744 addTouchEventHandlers: function (appleGestures) { 1745 if (!this.hasTouchHandlers && Env.isBrowser) { 1746 var moveTarget = this.attr.movetarget || this.containerObj; 1747 1748 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1749 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 1750 1751 /* 1752 if (!Type.exists(appleGestures) || appleGestures) { 1753 // Gesture listener are called in touchStart and touchMove. 1754 //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 1755 //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 1756 } 1757 */ 1758 1759 this.hasTouchHandlers = true; 1760 } 1761 }, 1762 1763 /** 1764 * Add fullscreen events which update the CSS transformation matrix to correct 1765 * the mouse/touch/pointer positions in case of CSS transformations. 1766 */ 1767 addFullscreenEventHandlers: function () { 1768 var i, 1769 // standard/Edge, firefox, chrome/safari, IE11 1770 events = [ 1771 'fullscreenchange', 1772 'mozfullscreenchange', 1773 'webkitfullscreenchange', 1774 'msfullscreenchange' 1775 ], 1776 le = events.length; 1777 1778 if (!this.hasFullscreenEventHandlers && Env.isBrowser) { 1779 for (i = 0; i < le; i++) { 1780 Env.addEvent(this.document, events[i], this.fullscreenListener, this); 1781 } 1782 this.hasFullscreenEventHandlers = true; 1783 } 1784 }, 1785 1786 addKeyboardEventHandlers: function () { 1787 if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) { 1788 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this); 1789 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 1790 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 1791 this.hasKeyboardHandlers = true; 1792 } 1793 }, 1794 1795 /** 1796 * Remove all registered touch event handlers. 1797 */ 1798 removeKeyboardEventHandlers: function () { 1799 if (this.hasKeyboardHandlers && Env.isBrowser) { 1800 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this); 1801 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 1802 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 1803 this.hasKeyboardHandlers = false; 1804 } 1805 }, 1806 1807 /** 1808 * Remove all registered event handlers regarding fullscreen mode. 1809 */ 1810 removeFullscreenEventHandlers: function () { 1811 var i, 1812 // standard/Edge, firefox, chrome/safari, IE11 1813 events = [ 1814 'fullscreenchange', 1815 'mozfullscreenchange', 1816 'webkitfullscreenchange', 1817 'msfullscreenchange' 1818 ], 1819 le = events.length; 1820 1821 if (this.hasFullscreenEventHandlers && Env.isBrowser) { 1822 for (i = 0; i < le; i++) { 1823 Env.removeEvent(this.document, events[i], this.fullscreenListener, this); 1824 } 1825 this.hasFullscreenEventHandlers = false; 1826 } 1827 }, 1828 1829 /** 1830 * Remove MSPointer* Event handlers. 1831 */ 1832 removePointerEventHandlers: function () { 1833 if (this.hasPointerHandlers && Env.isBrowser) { 1834 var moveTarget = this.attr.movetarget || this.containerObj; 1835 1836 if (window.navigator.msPointerEnabled) { 1837 // IE10- 1838 Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1839 Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 1840 } else { 1841 Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1842 Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 1843 Env.removeEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 1844 } 1845 1846 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1847 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1848 1849 if (this.hasPointerUp) { 1850 if (window.navigator.msPointerEnabled) { 1851 // IE10- 1852 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 1853 } else { 1854 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 1855 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 1856 } 1857 this.hasPointerUp = false; 1858 } 1859 1860 this.hasPointerHandlers = false; 1861 } 1862 }, 1863 1864 /** 1865 * De-register mouse event handlers. 1866 */ 1867 removeMouseEventHandlers: function () { 1868 if (this.hasMouseHandlers && Env.isBrowser) { 1869 var moveTarget = this.attr.movetarget || this.containerObj; 1870 1871 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1872 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 1873 1874 if (this.hasMouseUp) { 1875 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 1876 this.hasMouseUp = false; 1877 } 1878 1879 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1880 Env.removeEvent( 1881 this.containerObj, 1882 'DOMMouseScroll', 1883 this.mouseWheelListener, 1884 this 1885 ); 1886 1887 this.hasMouseHandlers = false; 1888 } 1889 }, 1890 1891 /** 1892 * Remove all registered touch event handlers. 1893 */ 1894 removeTouchEventHandlers: function () { 1895 if (this.hasTouchHandlers && Env.isBrowser) { 1896 var moveTarget = this.attr.movetarget || this.containerObj; 1897 1898 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1899 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 1900 1901 if (this.hasTouchEnd) { 1902 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 1903 this.hasTouchEnd = false; 1904 } 1905 1906 this.hasTouchHandlers = false; 1907 } 1908 }, 1909 1910 /** 1911 * Handler for click on left arrow in the navigation bar 1912 * @returns {JXG.Board} Reference to the board 1913 */ 1914 clickLeftArrow: function () { 1915 this.moveOrigin( 1916 this.origin.scrCoords[1] + this.canvasWidth * 0.1, 1917 this.origin.scrCoords[2] 1918 ); 1919 return this; 1920 }, 1921 1922 /** 1923 * Handler for click on right arrow in the navigation bar 1924 * @returns {JXG.Board} Reference to the board 1925 */ 1926 clickRightArrow: function () { 1927 this.moveOrigin( 1928 this.origin.scrCoords[1] - this.canvasWidth * 0.1, 1929 this.origin.scrCoords[2] 1930 ); 1931 return this; 1932 }, 1933 1934 /** 1935 * Handler for click on up arrow in the navigation bar 1936 * @returns {JXG.Board} Reference to the board 1937 */ 1938 clickUpArrow: function () { 1939 this.moveOrigin( 1940 this.origin.scrCoords[1], 1941 this.origin.scrCoords[2] - this.canvasHeight * 0.1 1942 ); 1943 return this; 1944 }, 1945 1946 /** 1947 * Handler for click on down arrow in the navigation bar 1948 * @returns {JXG.Board} Reference to the board 1949 */ 1950 clickDownArrow: function () { 1951 this.moveOrigin( 1952 this.origin.scrCoords[1], 1953 this.origin.scrCoords[2] + this.canvasHeight * 0.1 1954 ); 1955 return this; 1956 }, 1957 1958 /** 1959 * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. 1960 * Works on iOS/Safari and Android. 1961 * @param {Event} evt Browser event object 1962 * @returns {Boolean} 1963 */ 1964 gestureChangeListener: function (evt) { 1965 var c, 1966 dir1 = [], 1967 dir2 = [], 1968 angle, 1969 mi = 10, 1970 isPinch = false, 1971 // Save zoomFactors 1972 zx = this.attr.zoom.factorx, 1973 zy = this.attr.zoom.factory, 1974 factor, dist, theta, bound, 1975 dx, dy, cx, cy; 1976 1977 if (this.mode !== this.BOARD_MODE_ZOOM) { 1978 return true; 1979 } 1980 evt.preventDefault(); 1981 1982 dist = Geometry.distance( 1983 [evt.touches[0].clientX, evt.touches[0].clientY], 1984 [evt.touches[1].clientX, evt.touches[1].clientY], 1985 2 1986 ); 1987 1988 // Android pinch to zoom 1989 // evt.scale was available in iOS touch events (pre iOS 13) 1990 // evt.scale is undefined in Android 1991 if (evt.scale === undefined) { 1992 evt.scale = dist / this.prevDist; 1993 } 1994 1995 if (!Type.exists(this.prevCoords)) { 1996 return false; 1997 } 1998 // Compute the angle of the two finger directions 1999 dir1 = [ 2000 evt.touches[0].clientX - this.prevCoords[0][0], 2001 evt.touches[0].clientY - this.prevCoords[0][1] 2002 ]; 2003 dir2 = [ 2004 evt.touches[1].clientX - this.prevCoords[1][0], 2005 evt.touches[1].clientY - this.prevCoords[1][1] 2006 ]; 2007 2008 if ( 2009 dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi && 2010 dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi 2011 ) { 2012 return false; 2013 } 2014 2015 angle = Geometry.rad(dir1, [0, 0], dir2); 2016 if ( 2017 this.isPreviousGesture !== 'pan' && 2018 Math.abs(angle) > Math.PI * 0.2 && 2019 Math.abs(angle) < Math.PI * 1.8 2020 ) { 2021 isPinch = true; 2022 } 2023 2024 if (this.isPreviousGesture !== 'pan' && !isPinch) { 2025 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) { 2026 isPinch = true; 2027 } 2028 } 2029 2030 factor = evt.scale / this.prevScale; 2031 this.prevScale = evt.scale; 2032 this.prevCoords = [ 2033 [evt.touches[0].clientX, evt.touches[0].clientY], 2034 [evt.touches[1].clientX, evt.touches[1].clientY] 2035 ]; 2036 2037 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this); 2038 2039 if (this.attr.pan.enabled && this.attr.pan.needtwofingers && !isPinch) { 2040 // Pan detected 2041 this.isPreviousGesture = 'pan'; 2042 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true); 2043 2044 } else if (this.attr.zoom.enabled && Math.abs(factor - 1.0) < 0.5) { 2045 // Pinch detected 2046 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) { 2047 dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX); 2048 dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY); 2049 theta = Math.abs(Math.atan2(dy, dx)); 2050 bound = (Math.PI * this.attr.zoom.pinchsensitivity) / 90.0; 2051 } 2052 2053 if (this.attr.zoom.pinchhorizontal && theta < bound) { 2054 this.attr.zoom.factorx = factor; 2055 this.attr.zoom.factory = 1.0; 2056 cx = 0; 2057 cy = 0; 2058 } else if ( 2059 this.attr.zoom.pinchvertical && 2060 Math.abs(theta - Math.PI * 0.5) < bound 2061 ) { 2062 this.attr.zoom.factorx = 1.0; 2063 this.attr.zoom.factory = factor; 2064 cx = 0; 2065 cy = 0; 2066 } else { 2067 this.attr.zoom.factorx = factor; 2068 this.attr.zoom.factory = factor; 2069 cx = c.usrCoords[1]; 2070 cy = c.usrCoords[2]; 2071 } 2072 2073 this.zoomIn(cx, cy); 2074 2075 // Restore zoomFactors 2076 this.attr.zoom.factorx = zx; 2077 this.attr.zoom.factory = zy; 2078 } 2079 2080 return false; 2081 }, 2082 2083 /** 2084 * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari, 2085 * on Android we emulate it. 2086 * @param {Event} evt 2087 * @returns {Boolean} 2088 */ 2089 gestureStartListener: function (evt) { 2090 var pos; 2091 2092 evt.preventDefault(); 2093 this.prevScale = 1.0; 2094 // Android pinch to zoom 2095 this.prevDist = Geometry.distance( 2096 [evt.touches[0].clientX, evt.touches[0].clientY], 2097 [evt.touches[1].clientX, evt.touches[1].clientY], 2098 2 2099 ); 2100 this.prevCoords = [ 2101 [evt.touches[0].clientX, evt.touches[0].clientY], 2102 [evt.touches[1].clientX, evt.touches[1].clientY] 2103 ]; 2104 this.isPreviousGesture = 'none'; 2105 2106 // If pinch-to-zoom is interpreted as panning 2107 // we have to prepare move origin 2108 pos = this.getMousePosition(evt, 0); 2109 this.initMoveOrigin(pos[0], pos[1]); 2110 2111 this.mode = this.BOARD_MODE_ZOOM; 2112 return false; 2113 }, 2114 2115 /** 2116 * Test if the required key combination is pressed for wheel zoom, move origin and 2117 * selection 2118 * @private 2119 * @param {Object} evt Mouse or pen event 2120 * @param {String} action String containing the action: 'zoom', 'pan', 'selection'. 2121 * Corresponds to the attribute subobject. 2122 * @return {Boolean} true or false. 2123 */ 2124 _isRequiredKeyPressed: function (evt, action) { 2125 var obj = this.attr[action]; 2126 if (!obj.enabled) { 2127 return false; 2128 } 2129 2130 if ( 2131 ((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) && 2132 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey)) 2133 ) { 2134 return true; 2135 } 2136 2137 return false; 2138 }, 2139 2140 /* 2141 * Pointer events 2142 */ 2143 2144 /** 2145 * 2146 * Check if pointer event is already registered in {@link JXG.Board#_board_touches}. 2147 * 2148 * @param {Object} evt Event object 2149 * @return {Boolean} true if down event has already been sent. 2150 * @private 2151 */ 2152 _isPointerRegistered: function (evt) { 2153 var i, 2154 len = this._board_touches.length; 2155 2156 for (i = 0; i < len; i++) { 2157 if (this._board_touches[i].pointerId === evt.pointerId) { 2158 return true; 2159 } 2160 } 2161 return false; 2162 }, 2163 2164 /** 2165 * 2166 * Store the position of a pointer event. 2167 * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}. 2168 * Allows to follow the path of that finger on the screen. 2169 * Only two simultaneous touches are supported. 2170 * 2171 * @param {Object} evt Event object 2172 * @returns {JXG.Board} Reference to the board 2173 * @private 2174 */ 2175 _pointerStorePosition: function (evt) { 2176 var i, found; 2177 2178 for (i = 0, found = false; i < this._board_touches.length; i++) { 2179 if (this._board_touches[i].pointerId === evt.pointerId) { 2180 this._board_touches[i].clientX = evt.clientX; 2181 this._board_touches[i].clientY = evt.clientY; 2182 found = true; 2183 break; 2184 } 2185 } 2186 2187 // Restrict the number of simultaneous touches to 2 2188 if (!found && this._board_touches.length < 2) { 2189 this._board_touches.push({ 2190 pointerId: evt.pointerId, 2191 clientX: evt.clientX, 2192 clientY: evt.clientY 2193 }); 2194 } 2195 2196 return this; 2197 }, 2198 2199 /** 2200 * Deregisters a pointer event in {@link JXG.Board#_board_touches}. 2201 * It happens if a finger has been lifted from the screen. 2202 * 2203 * @param {Object} evt Event object 2204 * @returns {JXG.Board} Reference to the board 2205 * @private 2206 */ 2207 _pointerRemoveTouches: function (evt) { 2208 var i; 2209 for (i = 0; i < this._board_touches.length; i++) { 2210 if (this._board_touches[i].pointerId === evt.pointerId) { 2211 this._board_touches.splice(i, 1); 2212 break; 2213 } 2214 } 2215 2216 return this; 2217 }, 2218 2219 /** 2220 * Remove all registered fingers from {@link JXG.Board#_board_touches}. 2221 * This might be necessary if too many fingers have been registered. 2222 * @returns {JXG.Board} Reference to the board 2223 * @private 2224 */ 2225 _pointerClearTouches: function (pId) { 2226 // var i; 2227 // if (pId) { 2228 // for (i = 0; i < this._board_touches.length; i++) { 2229 // if (pId === this._board_touches[i].pointerId) { 2230 // this._board_touches.splice(i, i); 2231 // break; 2232 // } 2233 // } 2234 // } else { 2235 // } 2236 if (this._board_touches.length > 0) { 2237 this.dehighlightAll(); 2238 } 2239 this.updateQuality = this.BOARD_QUALITY_HIGH; 2240 this.mode = this.BOARD_MODE_NONE; 2241 this._board_touches = []; 2242 this.touches = []; 2243 }, 2244 2245 /** 2246 * Determine which input device is used for this action. 2247 * Possible devices are 'touch', 'pen' and 'mouse'. 2248 * This affects the precision and certain events. 2249 * In case of no browser, 'mouse' is used. 2250 * 2251 * @see JXG.Board#pointerDownListener 2252 * @see JXG.Board#pointerMoveListener 2253 * @see JXG.Board#initMoveObject 2254 * @see JXG.Board#moveObject 2255 * 2256 * @param {Event} evt The browsers event object. 2257 * @returns {String} 'mouse', 'pen', or 'touch' 2258 * @private 2259 */ 2260 _getPointerInputDevice: function (evt) { 2261 if (Env.isBrowser) { 2262 if ( 2263 evt.pointerType === 'touch' || // New 2264 (window.navigator.msMaxTouchPoints && // Old 2265 window.navigator.msMaxTouchPoints > 1) 2266 ) { 2267 return 'touch'; 2268 } 2269 if (evt.pointerType === 'mouse') { 2270 return 'mouse'; 2271 } 2272 if (evt.pointerType === 'pen') { 2273 return 'pen'; 2274 } 2275 } 2276 return 'mouse'; 2277 }, 2278 2279 /** 2280 * This method is called by the browser when a pointing device is pressed on the screen. 2281 * @param {Event} evt The browsers event object. 2282 * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter 2283 * @param {Boolean} [allowDefaultEventHandling=false] If true event is not canceled, i.e. prevent call of evt.preventDefault() 2284 * @returns {Boolean} false if the the first finger event is sent twice, or not a browser, or 2285 * or in selection mode. Otherwise returns true. 2286 */ 2287 pointerDownListener: function (evt, object, allowDefaultEventHandling) { 2288 var i, j, k, pos, 2289 elements, sel, target_obj, 2290 type = 'mouse', // Used in case of no browser 2291 found, target, ta; 2292 2293 // Fix for Firefox browser: When using a second finger, the 2294 // touch event for the first finger is sent again. 2295 if (!object && this._isPointerRegistered(evt)) { 2296 return false; 2297 } 2298 2299 if (!object && evt.isPrimary) { 2300 // First finger down. To be on the safe side this._board_touches is cleared. 2301 // this._pointerClearTouches(); 2302 } 2303 2304 if (!this.hasPointerUp) { 2305 if (window.navigator.msPointerEnabled) { 2306 // IE10- 2307 Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2308 } else { 2309 // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android 2310 Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this); 2311 Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2312 } 2313 this.hasPointerUp = true; 2314 } 2315 2316 if (this.hasMouseHandlers) { 2317 this.removeMouseEventHandlers(); 2318 } 2319 2320 if (this.hasTouchHandlers) { 2321 this.removeTouchEventHandlers(); 2322 } 2323 2324 // Prevent accidental selection of text 2325 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2326 this.document.selection.empty(); 2327 } else if (window.getSelection) { 2328 sel = window.getSelection(); 2329 if (sel.removeAllRanges) { 2330 try { 2331 sel.removeAllRanges(); 2332 } catch (e) { } 2333 } 2334 } 2335 2336 // Mouse, touch or pen device 2337 this._inputDevice = this._getPointerInputDevice(evt); 2338 type = this._inputDevice; 2339 this.options.precision.hasPoint = this.options.precision[type]; 2340 2341 // Handling of multi touch with pointer events should be easier than with touch events. 2342 // Every pointer device has its own pointerId, e.g. the mouse 2343 // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will 2344 // keep this id until a pointerUp event is fired. What we have to do here is: 2345 // 1. collect all elements under the current pointer 2346 // 2. run through the touches control structure 2347 // a. look for the object collected in step 1. 2348 // b. if an object is found, check the number of pointers. If appropriate, add the pointer. 2349 pos = this.getMousePosition(evt); 2350 2351 // selection 2352 this._testForSelection(evt); 2353 if (this.selectingMode) { 2354 this._startSelecting(pos); 2355 this.triggerEventHandlers( 2356 ['touchstartselecting', 'pointerstartselecting', 'startselecting'], 2357 [evt] 2358 ); 2359 return; // don't continue as a normal click 2360 } 2361 2362 if (this.attr.drag.enabled && object) { 2363 elements = [object]; 2364 this.mode = this.BOARD_MODE_DRAG; 2365 } else { 2366 elements = this.initMoveObject(pos[0], pos[1], evt, type); 2367 } 2368 2369 target_obj = { 2370 num: evt.pointerId, 2371 X: pos[0], 2372 Y: pos[1], 2373 Xprev: NaN, 2374 Yprev: NaN, 2375 Xstart: [], 2376 Ystart: [], 2377 Zstart: [] 2378 }; 2379 2380 // If no draggable object can be found, get out here immediately 2381 if (elements.length > 0) { 2382 // check touches structure 2383 target = elements[elements.length - 1]; 2384 found = false; 2385 2386 // Reminder: this.touches is the list of elements which 2387 // currently 'possess' a pointer (mouse, pen, finger) 2388 for (i = 0; i < this.touches.length; i++) { 2389 // An element receives a further touch, i.e. 2390 // the target is already in our touches array, add the pointer to the existing touch 2391 if (this.touches[i].obj === target) { 2392 j = i; 2393 k = this.touches[i].targets.push(target_obj) - 1; 2394 found = true; 2395 break; 2396 } 2397 } 2398 if (!found) { 2399 // An new element hae been touched. 2400 k = 0; 2401 j = 2402 this.touches.push({ 2403 obj: target, 2404 targets: [target_obj] 2405 }) - 1; 2406 } 2407 2408 this.dehighlightAll(); 2409 target.highlight(true); 2410 2411 this.saveStartPos(target, this.touches[j].targets[k]); 2412 2413 // Prevent accidental text selection 2414 // this could get us new trouble: input fields, links and drop down boxes placed as text 2415 // on the board don't work anymore. 2416 if (evt && evt.preventDefault && !allowDefaultEventHandling) { 2417 evt.preventDefault(); 2418 // All browser supporting pointer events know preventDefault() 2419 // } else if (window.event) { 2420 // window.event.returnValue = false; 2421 } 2422 } 2423 2424 if (this.touches.length > 0 && !allowDefaultEventHandling) { 2425 evt.preventDefault(); 2426 evt.stopPropagation(); 2427 } 2428 2429 if (!Env.isBrowser) { 2430 return false; 2431 } 2432 if (this._getPointerInputDevice(evt) !== 'touch') { 2433 if (this.mode === this.BOARD_MODE_NONE) { 2434 this.mouseOriginMoveStart(evt); 2435 } 2436 } else { 2437 this._pointerStorePosition(evt); 2438 evt.touches = this._board_touches; 2439 2440 // Touch events on empty areas of the board are handled here, see also touchStartListener 2441 // 1. case: one finger. If allowed, this triggers pan with one finger 2442 if ( 2443 evt.touches.length === 1 && 2444 this.mode === this.BOARD_MODE_NONE && 2445 this.touchStartMoveOriginOneFinger(evt) 2446 ) { 2447 // Empty by purpose 2448 } else if ( 2449 evt.touches.length === 2 && 2450 (this.mode === this.BOARD_MODE_NONE || 2451 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 2452 ) { 2453 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2454 // This happens when the second finger hits the device. First, the 2455 // 'one finger pan mode' has to be cancelled. 2456 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 2457 this.originMoveEnd(); 2458 } 2459 2460 this.gestureStartListener(evt); 2461 } 2462 } 2463 2464 // Allow browser scrolling 2465 // For this: pan by one finger has to be disabled 2466 ta = 'none'; // JSXGraph catches all user touch events 2467 if (this.mode === this.BOARD_MODE_NONE && 2468 Type.evaluate(this.attr.browserpan) && 2469 !(Type.evaluate(this.attr.pan.enabled) && !Type.evaluate(this.attr.pan.needtwofingers)) 2470 ) { 2471 ta = 'pan-x pan-y'; // JSXGraph allows browser scrolling 2472 } 2473 this.containerObj.style.touchAction = ta; 2474 2475 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]); 2476 2477 return true; 2478 }, 2479 2480 // /** 2481 // * Called if pointer leaves an HTML tag. It is called by the inner-most tag. 2482 // * That means, if a JSXGraph text, i.e. an HTML div, is placed close 2483 // * to the border of the board, this pointerout event will be ignored. 2484 // * @param {Event} evt 2485 // * @return {Boolean} 2486 // */ 2487 // pointerOutListener: function (evt) { 2488 // if (evt.target === this.containerObj || 2489 // (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) { 2490 // this.pointerUpListener(evt); 2491 // } 2492 // return this.mode === this.BOARD_MODE_NONE; 2493 // }, 2494 2495 /** 2496 * Called periodically by the browser while the user moves a pointing device across the screen. 2497 * @param {Event} evt 2498 * @returns {Boolean} 2499 */ 2500 pointerMoveListener: function (evt) { 2501 var i, 2502 j, 2503 pos, 2504 touchTargets, 2505 type = 'mouse'; // in case of no browser 2506 2507 if ( 2508 this._getPointerInputDevice(evt) === 'touch' && 2509 !this._isPointerRegistered(evt) 2510 ) { 2511 // Test, if there was a previous down event of this _getPointerId 2512 // (in case it is a touch event). 2513 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry. 2514 return this.BOARD_MODE_NONE; 2515 } 2516 2517 if (!this.checkFrameRate(evt)) { 2518 return false; 2519 } 2520 2521 if (this.mode !== this.BOARD_MODE_DRAG) { 2522 this.dehighlightAll(); 2523 this.displayInfobox(false); 2524 } 2525 2526 if (this.mode !== this.BOARD_MODE_NONE) { 2527 evt.preventDefault(); 2528 evt.stopPropagation(); 2529 } 2530 2531 this.updateQuality = this.BOARD_QUALITY_LOW; 2532 // Mouse, touch or pen device 2533 this._inputDevice = this._getPointerInputDevice(evt); 2534 type = this._inputDevice; 2535 this.options.precision.hasPoint = this.options.precision[type]; 2536 2537 // selection 2538 if (this.selectingMode) { 2539 pos = this.getMousePosition(evt); 2540 this._moveSelecting(pos); 2541 this.triggerEventHandlers( 2542 ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], 2543 [evt, this.mode] 2544 ); 2545 } else if (!this.mouseOriginMove(evt)) { 2546 if (this.mode === this.BOARD_MODE_DRAG) { 2547 pos = this.getMousePosition(evt); 2548 // Run through all jsxgraph elements which are touched by at least one finger. 2549 for (i = 0; i < this.touches.length; i++) { 2550 touchTargets = this.touches[i].targets; 2551 // Run through all touch events which have been started on this jsxgraph element. 2552 for (j = 0; j < touchTargets.length; j++) { 2553 if (touchTargets[j].num === evt.pointerId) { 2554 touchTargets[j].X = pos[0]; 2555 touchTargets[j].Y = pos[1]; 2556 2557 if (touchTargets.length === 1) { 2558 // Touch by one finger: this is possible for all elements that can be dragged 2559 this.moveObject(pos[0], pos[1], this.touches[i], evt, type); 2560 } else if (touchTargets.length === 2) { 2561 // Touch by two fingers: e.g. moving lines 2562 this.twoFingerMove(this.touches[i], evt.pointerId, evt); 2563 2564 touchTargets[j].Xprev = pos[0]; 2565 touchTargets[j].Yprev = pos[1]; 2566 } 2567 2568 // There is only one pointer in the evt object, so there's no point in looking further 2569 break; 2570 } 2571 } 2572 } 2573 } else { 2574 if (this._getPointerInputDevice(evt) === 'touch') { 2575 this._pointerStorePosition(evt); 2576 2577 if (this._board_touches.length === 2) { 2578 evt.touches = this._board_touches; 2579 this.gestureChangeListener(evt); 2580 } 2581 } 2582 2583 // Move event without dragging an element 2584 pos = this.getMousePosition(evt); 2585 this.highlightElements(pos[0], pos[1], evt, -1); 2586 } 2587 } 2588 2589 // Hiding the infobox is commented out, since it prevents showing the infobox 2590 // on IE 11+ on 'over' 2591 //if (this.mode !== this.BOARD_MODE_DRAG) { 2592 //this.displayInfobox(false); 2593 //} 2594 this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]); 2595 this.updateQuality = this.BOARD_QUALITY_HIGH; 2596 2597 return this.mode === this.BOARD_MODE_NONE; 2598 }, 2599 2600 /** 2601 * Triggered as soon as the user stops touching the device with at least one finger. 2602 * 2603 * @param {Event} evt 2604 * @returns {Boolean} 2605 */ 2606 pointerUpListener: function (evt) { 2607 var i, j, found, 2608 touchTargets, 2609 updateNeeded = false; 2610 2611 this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 2612 this.displayInfobox(false); 2613 2614 if (evt) { 2615 for (i = 0; i < this.touches.length; i++) { 2616 touchTargets = this.touches[i].targets; 2617 for (j = 0; j < touchTargets.length; j++) { 2618 if (touchTargets[j].num === evt.pointerId) { 2619 touchTargets.splice(j, 1); 2620 if (touchTargets.length === 0) { 2621 this.touches.splice(i, 1); 2622 } 2623 break; 2624 } 2625 } 2626 } 2627 } 2628 2629 this.originMoveEnd(); 2630 this.update(); 2631 2632 // selection 2633 if (this.selectingMode) { 2634 this._stopSelecting(evt); 2635 this.triggerEventHandlers( 2636 ['touchstopselecting', 'pointerstopselecting', 'stopselecting'], 2637 [evt] 2638 ); 2639 this.stopSelectionMode(); 2640 } else { 2641 for (i = this.downObjects.length - 1; i > -1; i--) { 2642 found = false; 2643 for (j = 0; j < this.touches.length; j++) { 2644 if (this.touches[j].obj.id === this.downObjects[i].id) { 2645 found = true; 2646 } 2647 } 2648 if (!found) { 2649 this.downObjects[i].triggerEventHandlers( 2650 ['touchend', 'up', 'pointerup', 'MSPointerUp'], 2651 [evt] 2652 ); 2653 if (!Type.exists(this.downObjects[i].coords)) { 2654 // snapTo methods have to be called e.g. for line elements here. 2655 // For coordsElements there might be a conflict with 2656 // attractors, see commit from 2022.04.08, 11:12:18. 2657 this.downObjects[i].snapToGrid(); 2658 this.downObjects[i].snapToPoints(); 2659 updateNeeded = true; 2660 } 2661 this.downObjects.splice(i, 1); 2662 } 2663 } 2664 } 2665 2666 if (this.hasPointerUp) { 2667 if (window.navigator.msPointerEnabled) { 2668 // IE10- 2669 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2670 } else { 2671 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 2672 Env.removeEvent( 2673 this.document, 2674 'pointercancel', 2675 this.pointerUpListener, 2676 this 2677 ); 2678 } 2679 this.hasPointerUp = false; 2680 } 2681 2682 // After one finger leaves the screen the gesture is stopped. 2683 this._pointerClearTouches(evt.pointerId); 2684 if (this._getPointerInputDevice(evt) !== 'touch') { 2685 this.dehighlightAll(); 2686 } 2687 2688 if (updateNeeded) { 2689 this.update(); 2690 } 2691 2692 return true; 2693 }, 2694 2695 /** 2696 * Triggered by the pointerleave event. This is needed in addition to 2697 * {@link JXG.Board#pointerUpListener} in the situation that a pen is used 2698 * and after an up event the pen leaves the hover range vertically. Here, it happens that 2699 * after the pointerup event further pointermove events are fired and elements get highlighted. 2700 * This highlighting has to be cancelled. 2701 * 2702 * @param {Event} evt 2703 * @returns {Boolean} 2704 */ 2705 pointerLeaveListener: function (evt) { 2706 this.displayInfobox(false); 2707 this.dehighlightAll(); 2708 2709 return true; 2710 }, 2711 2712 /** 2713 * Touch-Events 2714 */ 2715 2716 /** 2717 * This method is called by the browser when a finger touches the surface of the touch-device. 2718 * @param {Event} evt The browsers event object. 2719 * @returns {Boolean} ... 2720 */ 2721 touchStartListener: function (evt) { 2722 var i, 2723 pos, 2724 elements, 2725 j, 2726 k, 2727 eps = this.options.precision.touch, 2728 obj, 2729 found, 2730 targets, 2731 evtTouches = evt[JXG.touchProperty], 2732 target, 2733 touchTargets; 2734 2735 if (!this.hasTouchEnd) { 2736 Env.addEvent(this.document, 'touchend', this.touchEndListener, this); 2737 this.hasTouchEnd = true; 2738 } 2739 2740 // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen. 2741 //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); } 2742 2743 // prevent accidental selection of text 2744 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2745 this.document.selection.empty(); 2746 } else if (window.getSelection) { 2747 window.getSelection().removeAllRanges(); 2748 } 2749 2750 // multitouch 2751 this._inputDevice = 'touch'; 2752 this.options.precision.hasPoint = this.options.precision.touch; 2753 2754 // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our 2755 // previous touches. once this is done we run through the existing touches again and watch out for free touches that can be attached to our existing 2756 // touches, e.g. we translate (parallel translation) a line with one finger, now a second finger is over this line. this should change the operation to 2757 // a rotational translation. or one finger moves a circle, a second finger can be attached to the circle: this now changes the operation from translation to 2758 // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations: 2759 // * points have higher priority over other elements. 2760 // * if we find a targettouch over an element that could be transformed with more than one finger, we search the rest of the targettouches, if they are over 2761 // this element and add them. 2762 // ADDENDUM 11/10/11: 2763 // (1) run through the touches control object, 2764 // (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch 2765 // for every target in our touches objects 2766 // (3) if one of the targettouches was bound to a touches targets array, mark it 2767 // (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch: 2768 // (a) if no element could be found: mark the target touches and continue 2769 // --- in the following cases, 'init' means: 2770 // (i) check if the element is already used in another touches element, if so, mark the targettouch and continue 2771 // (ii) if not, init a new touches element, add the targettouch to the touches property and mark it 2772 // (b) if the element is a point, init 2773 // (c) if the element is a line, init and try to find a second targettouch on that line. if a second one is found, add and mark it 2774 // (d) if the element is a circle, init and try to find TWO other targettouches on that circle. if only one is found, mark it and continue. otherwise 2775 // add both to the touches array and mark them. 2776 for (i = 0; i < evtTouches.length; i++) { 2777 evtTouches[i].jxg_isused = false; 2778 } 2779 2780 for (i = 0; i < this.touches.length; i++) { 2781 touchTargets = this.touches[i].targets; 2782 for (j = 0; j < touchTargets.length; j++) { 2783 touchTargets[j].num = -1; 2784 eps = this.options.precision.touch; 2785 2786 do { 2787 for (k = 0; k < evtTouches.length; k++) { 2788 // find the new targettouches 2789 if ( 2790 Math.abs( 2791 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 2792 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 2793 ) < 2794 eps * eps 2795 ) { 2796 touchTargets[j].num = k; 2797 touchTargets[j].X = evtTouches[k].screenX; 2798 touchTargets[j].Y = evtTouches[k].screenY; 2799 evtTouches[k].jxg_isused = true; 2800 break; 2801 } 2802 } 2803 2804 eps *= 2; 2805 } while ( 2806 touchTargets[j].num === -1 && 2807 eps < this.options.precision.touchMax 2808 ); 2809 2810 if (touchTargets[j].num === -1) { 2811 JXG.debug( 2812 "i couldn't find a targettouches for target no " + 2813 j + 2814 ' on ' + 2815 this.touches[i].obj.name + 2816 ' (' + 2817 this.touches[i].obj.id + 2818 '). Removed the target.' 2819 ); 2820 JXG.debug( 2821 'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax 2822 ); 2823 touchTargets.splice(i, 1); 2824 } 2825 } 2826 } 2827 2828 // we just re-mapped the targettouches to our existing touches list. 2829 // now we have to initialize some touches from additional targettouches 2830 for (i = 0; i < evtTouches.length; i++) { 2831 if (!evtTouches[i].jxg_isused) { 2832 pos = this.getMousePosition(evt, i); 2833 // selection 2834 // this._testForSelection(evt); // we do not have shift or ctrl keys yet. 2835 if (this.selectingMode) { 2836 this._startSelecting(pos); 2837 this.triggerEventHandlers( 2838 ['touchstartselecting', 'startselecting'], 2839 [evt] 2840 ); 2841 evt.preventDefault(); 2842 evt.stopPropagation(); 2843 this.options.precision.hasPoint = this.options.precision.mouse; 2844 return this.touches.length > 0; // don't continue as a normal click 2845 } 2846 2847 elements = this.initMoveObject(pos[0], pos[1], evt, 'touch'); 2848 if (elements.length !== 0) { 2849 obj = elements[elements.length - 1]; 2850 target = { 2851 num: i, 2852 X: evtTouches[i].screenX, 2853 Y: evtTouches[i].screenY, 2854 Xprev: NaN, 2855 Yprev: NaN, 2856 Xstart: [], 2857 Ystart: [], 2858 Zstart: [] 2859 }; 2860 2861 if ( 2862 Type.isPoint(obj) || 2863 obj.elementClass === Const.OBJECT_CLASS_TEXT || 2864 obj.type === Const.OBJECT_TYPE_TICKS || 2865 obj.type === Const.OBJECT_TYPE_IMAGE 2866 ) { 2867 // It's a point, so it's single touch, so we just push it to our touches 2868 targets = [target]; 2869 2870 // For the UNDO/REDO of object moves 2871 this.saveStartPos(obj, targets[0]); 2872 2873 this.touches.push({ obj: obj, targets: targets }); 2874 obj.highlight(true); 2875 } else if ( 2876 obj.elementClass === Const.OBJECT_CLASS_LINE || 2877 obj.elementClass === Const.OBJECT_CLASS_CIRCLE || 2878 obj.elementClass === Const.OBJECT_CLASS_CURVE || 2879 obj.type === Const.OBJECT_TYPE_POLYGON 2880 ) { 2881 found = false; 2882 2883 // first check if this geometric object is already captured in this.touches 2884 for (j = 0; j < this.touches.length; j++) { 2885 if (obj.id === this.touches[j].obj.id) { 2886 found = true; 2887 // only add it, if we don't have two targets in there already 2888 if (this.touches[j].targets.length === 1) { 2889 // For the UNDO/REDO of object moves 2890 this.saveStartPos(obj, target); 2891 this.touches[j].targets.push(target); 2892 } 2893 2894 evtTouches[i].jxg_isused = true; 2895 } 2896 } 2897 2898 // we couldn't find it in touches, so we just init a new touches 2899 // IF there is a second touch targetting this line, we will find it later on, and then add it to 2900 // the touches control object. 2901 if (!found) { 2902 targets = [target]; 2903 2904 // For the UNDO/REDO of object moves 2905 this.saveStartPos(obj, targets[0]); 2906 this.touches.push({ obj: obj, targets: targets }); 2907 obj.highlight(true); 2908 } 2909 } 2910 } 2911 2912 evtTouches[i].jxg_isused = true; 2913 } 2914 } 2915 2916 if (this.touches.length > 0) { 2917 evt.preventDefault(); 2918 evt.stopPropagation(); 2919 } 2920 2921 // Touch events on empty areas of the board are handled here: 2922 // 1. case: one finger. If allowed, this triggers pan with one finger 2923 if ( 2924 evtTouches.length === 1 && 2925 this.mode === this.BOARD_MODE_NONE && 2926 this.touchStartMoveOriginOneFinger(evt) 2927 ) { 2928 } else if ( 2929 evtTouches.length === 2 && 2930 (this.mode === this.BOARD_MODE_NONE || 2931 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 2932 ) { 2933 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2934 // This happens when the second finger hits the device. First, the 2935 // 'one finger pan mode' has to be cancelled. 2936 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 2937 this.originMoveEnd(); 2938 } 2939 this.gestureStartListener(evt); 2940 } 2941 2942 this.options.precision.hasPoint = this.options.precision.mouse; 2943 this.triggerEventHandlers(['touchstart', 'down'], [evt]); 2944 2945 return false; 2946 //return this.touches.length > 0; 2947 }, 2948 2949 /** 2950 * Called periodically by the browser while the user moves his fingers across the device. 2951 * @param {Event} evt 2952 * @returns {Boolean} 2953 */ 2954 touchMoveListener: function (evt) { 2955 var i, 2956 pos1, 2957 pos2, 2958 touchTargets, 2959 evtTouches = evt[JXG.touchProperty]; 2960 2961 if (!this.checkFrameRate(evt)) { 2962 return false; 2963 } 2964 2965 if (this.mode !== this.BOARD_MODE_NONE) { 2966 evt.preventDefault(); 2967 evt.stopPropagation(); 2968 } 2969 2970 if (this.mode !== this.BOARD_MODE_DRAG) { 2971 this.dehighlightAll(); 2972 this.displayInfobox(false); 2973 } 2974 2975 this._inputDevice = 'touch'; 2976 this.options.precision.hasPoint = this.options.precision.touch; 2977 this.updateQuality = this.BOARD_QUALITY_LOW; 2978 2979 // selection 2980 if (this.selectingMode) { 2981 for (i = 0; i < evtTouches.length; i++) { 2982 if (!evtTouches[i].jxg_isused) { 2983 pos1 = this.getMousePosition(evt, i); 2984 this._moveSelecting(pos1); 2985 this.triggerEventHandlers( 2986 ['touchmoves', 'moveselecting'], 2987 [evt, this.mode] 2988 ); 2989 break; 2990 } 2991 } 2992 } else { 2993 if (!this.touchOriginMove(evt)) { 2994 if (this.mode === this.BOARD_MODE_DRAG) { 2995 // Runs over through all elements which are touched 2996 // by at least one finger. 2997 for (i = 0; i < this.touches.length; i++) { 2998 touchTargets = this.touches[i].targets; 2999 if (touchTargets.length === 1) { 3000 // Touch by one finger: this is possible for all elements that can be dragged 3001 if (evtTouches[touchTargets[0].num]) { 3002 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3003 if ( 3004 pos1[0] < 0 || 3005 pos1[0] > this.canvasWidth || 3006 pos1[1] < 0 || 3007 pos1[1] > this.canvasHeight 3008 ) { 3009 return; 3010 } 3011 touchTargets[0].X = pos1[0]; 3012 touchTargets[0].Y = pos1[1]; 3013 this.moveObject( 3014 pos1[0], 3015 pos1[1], 3016 this.touches[i], 3017 evt, 3018 'touch' 3019 ); 3020 } 3021 } else if ( 3022 touchTargets.length === 2 && 3023 touchTargets[0].num > -1 && 3024 touchTargets[1].num > -1 3025 ) { 3026 // Touch by two fingers: moving lines, ... 3027 if ( 3028 evtTouches[touchTargets[0].num] && 3029 evtTouches[touchTargets[1].num] 3030 ) { 3031 // Get coordinates of the two touches 3032 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3033 pos2 = this.getMousePosition(evt, touchTargets[1].num); 3034 if ( 3035 pos1[0] < 0 || 3036 pos1[0] > this.canvasWidth || 3037 pos1[1] < 0 || 3038 pos1[1] > this.canvasHeight || 3039 pos2[0] < 0 || 3040 pos2[0] > this.canvasWidth || 3041 pos2[1] < 0 || 3042 pos2[1] > this.canvasHeight 3043 ) { 3044 return; 3045 } 3046 3047 touchTargets[0].X = pos1[0]; 3048 touchTargets[0].Y = pos1[1]; 3049 touchTargets[1].X = pos2[0]; 3050 touchTargets[1].Y = pos2[1]; 3051 3052 this.twoFingerMove( 3053 this.touches[i], 3054 touchTargets[0].num, 3055 evt 3056 ); 3057 3058 touchTargets[0].Xprev = pos1[0]; 3059 touchTargets[0].Yprev = pos1[1]; 3060 touchTargets[1].Xprev = pos2[0]; 3061 touchTargets[1].Yprev = pos2[1]; 3062 } 3063 } 3064 } 3065 } else { 3066 if (evtTouches.length === 2) { 3067 this.gestureChangeListener(evt); 3068 } 3069 // Move event without dragging an element 3070 pos1 = this.getMousePosition(evt, 0); 3071 this.highlightElements(pos1[0], pos1[1], evt, -1); 3072 } 3073 } 3074 } 3075 3076 if (this.mode !== this.BOARD_MODE_DRAG) { 3077 this.displayInfobox(false); 3078 } 3079 3080 this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 3081 this.options.precision.hasPoint = this.options.precision.mouse; 3082 this.updateQuality = this.BOARD_QUALITY_HIGH; 3083 3084 return this.mode === this.BOARD_MODE_NONE; 3085 }, 3086 3087 /** 3088 * Triggered as soon as the user stops touching the device with at least one finger. 3089 * @param {Event} evt 3090 * @returns {Boolean} 3091 */ 3092 touchEndListener: function (evt) { 3093 var i, 3094 j, 3095 k, 3096 eps = this.options.precision.touch, 3097 tmpTouches = [], 3098 found, 3099 foundNumber, 3100 evtTouches = evt && evt[JXG.touchProperty], 3101 touchTargets, 3102 updateNeeded = false; 3103 3104 this.triggerEventHandlers(['touchend', 'up'], [evt]); 3105 this.displayInfobox(false); 3106 3107 // selection 3108 if (this.selectingMode) { 3109 this._stopSelecting(evt); 3110 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]); 3111 this.stopSelectionMode(); 3112 } else if (evtTouches && evtTouches.length > 0) { 3113 for (i = 0; i < this.touches.length; i++) { 3114 tmpTouches[i] = this.touches[i]; 3115 } 3116 this.touches.length = 0; 3117 3118 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted, 3119 // convert the operation to a simple one-finger-translation. 3120 // ADDENDUM 11/10/11: 3121 // see addendum to touchStartListener from 11/10/11 3122 // (1) run through the tmptouches 3123 // (2) check the touches.obj, if it is a 3124 // (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch. 3125 // (b) line with 3126 // (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch. 3127 // (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches 3128 // (c) circle with [proceed like in line] 3129 3130 // init the targettouches marker 3131 for (i = 0; i < evtTouches.length; i++) { 3132 evtTouches[i].jxg_isused = false; 3133 } 3134 3135 for (i = 0; i < tmpTouches.length; i++) { 3136 // could all targets of the current this.touches.obj be assigned to targettouches? 3137 found = false; 3138 foundNumber = 0; 3139 touchTargets = tmpTouches[i].targets; 3140 3141 for (j = 0; j < touchTargets.length; j++) { 3142 touchTargets[j].found = false; 3143 for (k = 0; k < evtTouches.length; k++) { 3144 if ( 3145 Math.abs( 3146 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3147 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3148 ) < 3149 eps * eps 3150 ) { 3151 touchTargets[j].found = true; 3152 touchTargets[j].num = k; 3153 touchTargets[j].X = evtTouches[k].screenX; 3154 touchTargets[j].Y = evtTouches[k].screenY; 3155 foundNumber += 1; 3156 break; 3157 } 3158 } 3159 } 3160 3161 if (Type.isPoint(tmpTouches[i].obj)) { 3162 found = touchTargets[0] && touchTargets[0].found; 3163 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) { 3164 found = 3165 (touchTargets[0] && touchTargets[0].found) || 3166 (touchTargets[1] && touchTargets[1].found); 3167 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 3168 found = foundNumber === 1 || foundNumber === 3; 3169 } 3170 3171 // if we found this object to be still dragged by the user, add it back to this.touches 3172 if (found) { 3173 this.touches.push({ 3174 obj: tmpTouches[i].obj, 3175 targets: [] 3176 }); 3177 3178 for (j = 0; j < touchTargets.length; j++) { 3179 if (touchTargets[j].found) { 3180 this.touches[this.touches.length - 1].targets.push({ 3181 num: touchTargets[j].num, 3182 X: touchTargets[j].screenX, 3183 Y: touchTargets[j].screenY, 3184 Xprev: NaN, 3185 Yprev: NaN, 3186 Xstart: touchTargets[j].Xstart, 3187 Ystart: touchTargets[j].Ystart, 3188 Zstart: touchTargets[j].Zstart 3189 }); 3190 } 3191 } 3192 } else { 3193 tmpTouches[i].obj.noHighlight(); 3194 } 3195 } 3196 } else { 3197 this.touches.length = 0; 3198 } 3199 3200 for (i = this.downObjects.length - 1; i > -1; i--) { 3201 found = false; 3202 for (j = 0; j < this.touches.length; j++) { 3203 if (this.touches[j].obj.id === this.downObjects[i].id) { 3204 found = true; 3205 } 3206 } 3207 if (!found) { 3208 this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]); 3209 if (!Type.exists(this.downObjects[i].coords)) { 3210 // snapTo methods have to be called e.g. for line elements here. 3211 // For coordsElements there might be a conflict with 3212 // attractors, see commit from 2022.04.08, 11:12:18. 3213 this.downObjects[i].snapToGrid(); 3214 this.downObjects[i].snapToPoints(); 3215 updateNeeded = true; 3216 } 3217 this.downObjects.splice(i, 1); 3218 } 3219 } 3220 3221 if (!evtTouches || evtTouches.length === 0) { 3222 if (this.hasTouchEnd) { 3223 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 3224 this.hasTouchEnd = false; 3225 } 3226 3227 this.dehighlightAll(); 3228 this.updateQuality = this.BOARD_QUALITY_HIGH; 3229 3230 this.originMoveEnd(); 3231 if (updateNeeded) { 3232 this.update(); 3233 } 3234 } 3235 3236 return true; 3237 }, 3238 3239 /** 3240 * This method is called by the browser when the mouse button is clicked. 3241 * @param {Event} evt The browsers event object. 3242 * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise. 3243 */ 3244 mouseDownListener: function (evt) { 3245 var pos, elements, result; 3246 3247 // prevent accidental selection of text 3248 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3249 this.document.selection.empty(); 3250 } else if (window.getSelection) { 3251 window.getSelection().removeAllRanges(); 3252 } 3253 3254 if (!this.hasMouseUp) { 3255 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this); 3256 this.hasMouseUp = true; 3257 } else { 3258 // In case this.hasMouseUp==true, it may be that there was a 3259 // mousedown event before which was not followed by an mouseup event. 3260 // This seems to happen with interactive whiteboard pens sometimes. 3261 return; 3262 } 3263 3264 this._inputDevice = 'mouse'; 3265 this.options.precision.hasPoint = this.options.precision.mouse; 3266 pos = this.getMousePosition(evt); 3267 3268 // selection 3269 this._testForSelection(evt); 3270 if (this.selectingMode) { 3271 this._startSelecting(pos); 3272 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]); 3273 return; // don't continue as a normal click 3274 } 3275 3276 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 3277 3278 // if no draggable object can be found, get out here immediately 3279 if (elements.length === 0) { 3280 this.mode = this.BOARD_MODE_NONE; 3281 result = true; 3282 } else { 3283 /** @ignore */ 3284 this.mouse = { 3285 obj: null, 3286 targets: [ 3287 { 3288 X: pos[0], 3289 Y: pos[1], 3290 Xprev: NaN, 3291 Yprev: NaN 3292 } 3293 ] 3294 }; 3295 this.mouse.obj = elements[elements.length - 1]; 3296 3297 this.dehighlightAll(); 3298 this.mouse.obj.highlight(true); 3299 3300 this.mouse.targets[0].Xstart = []; 3301 this.mouse.targets[0].Ystart = []; 3302 this.mouse.targets[0].Zstart = []; 3303 3304 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]); 3305 3306 // prevent accidental text selection 3307 // this could get us new trouble: input fields, links and drop down boxes placed as text 3308 // on the board don't work anymore. 3309 if (evt && evt.preventDefault) { 3310 evt.preventDefault(); 3311 } else if (window.event) { 3312 window.event.returnValue = false; 3313 } 3314 } 3315 3316 if (this.mode === this.BOARD_MODE_NONE) { 3317 result = this.mouseOriginMoveStart(evt); 3318 } 3319 3320 this.triggerEventHandlers(['mousedown', 'down'], [evt]); 3321 3322 return result; 3323 }, 3324 3325 /** 3326 * This method is called by the browser when the mouse is moved. 3327 * @param {Event} evt The browsers event object. 3328 */ 3329 mouseMoveListener: function (evt) { 3330 var pos; 3331 3332 if (!this.checkFrameRate(evt)) { 3333 return false; 3334 } 3335 3336 pos = this.getMousePosition(evt); 3337 3338 this.updateQuality = this.BOARD_QUALITY_LOW; 3339 3340 if (this.mode !== this.BOARD_MODE_DRAG) { 3341 this.dehighlightAll(); 3342 this.displayInfobox(false); 3343 } 3344 3345 // we have to check for four cases: 3346 // * user moves origin 3347 // * user drags an object 3348 // * user just moves the mouse, here highlight all elements at 3349 // the current mouse position 3350 // * the user is selecting 3351 3352 // selection 3353 if (this.selectingMode) { 3354 this._moveSelecting(pos); 3355 this.triggerEventHandlers( 3356 ['mousemoveselecting', 'moveselecting'], 3357 [evt, this.mode] 3358 ); 3359 } else if (!this.mouseOriginMove(evt)) { 3360 if (this.mode === this.BOARD_MODE_DRAG) { 3361 this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse'); 3362 } else { 3363 // BOARD_MODE_NONE 3364 // Move event without dragging an element 3365 this.highlightElements(pos[0], pos[1], evt, -1); 3366 } 3367 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]); 3368 } 3369 this.updateQuality = this.BOARD_QUALITY_HIGH; 3370 }, 3371 3372 /** 3373 * This method is called by the browser when the mouse button is released. 3374 * @param {Event} evt 3375 */ 3376 mouseUpListener: function (evt) { 3377 var i; 3378 3379 if (this.selectingMode === false) { 3380 this.triggerEventHandlers(['mouseup', 'up'], [evt]); 3381 } 3382 3383 // redraw with high precision 3384 this.updateQuality = this.BOARD_QUALITY_HIGH; 3385 3386 if (this.mouse && this.mouse.obj) { 3387 if (!Type.exists(this.mouse.obj.coords)) { 3388 // snapTo methods have to be called e.g. for line elements here. 3389 // For coordsElements there might be a conflict with 3390 // attractors, see commit from 2022.04.08, 11:12:18. 3391 // The parameter is needed for lines with snapToGrid enabled 3392 this.mouse.obj.snapToGrid(this.mouse.targets[0]); 3393 this.mouse.obj.snapToPoints(); 3394 } 3395 } 3396 3397 this.originMoveEnd(); 3398 this.dehighlightAll(); 3399 this.update(); 3400 3401 // selection 3402 if (this.selectingMode) { 3403 this._stopSelecting(evt); 3404 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]); 3405 this.stopSelectionMode(); 3406 } else { 3407 for (i = 0; i < this.downObjects.length; i++) { 3408 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]); 3409 } 3410 } 3411 3412 this.downObjects.length = 0; 3413 3414 if (this.hasMouseUp) { 3415 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 3416 this.hasMouseUp = false; 3417 } 3418 3419 // release dragged mouse object 3420 /** @ignore */ 3421 this.mouse = null; 3422 }, 3423 3424 /** 3425 * Handler for mouse wheel events. Used to zoom in and out of the board. 3426 * @param {Event} evt 3427 * @returns {Boolean} 3428 */ 3429 mouseWheelListener: function (evt) { 3430 if (!this.attr.zoom.wheel || !this._isRequiredKeyPressed(evt, 'zoom')) { 3431 return true; 3432 } 3433 3434 evt = evt || window.event; 3435 var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40, 3436 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this); 3437 3438 if (wd > 0) { 3439 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]); 3440 } else { 3441 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]); 3442 } 3443 3444 this.triggerEventHandlers(['mousewheel'], [evt]); 3445 3446 evt.preventDefault(); 3447 return false; 3448 }, 3449 3450 /** 3451 * Allow moving of JSXGraph elements with arrow keys. 3452 * The selection of the element is done with the tab key. For this, 3453 * the attribute 'tabindex' of the element has to be set to some number (default=0). 3454 * tabindex corresponds to the HTML attribute of the same name. 3455 * <p> 3456 * Panning of the construction is done with arrow keys 3457 * if the pan key (shift or ctrl - depending on the board attributes) is pressed. 3458 * <p> 3459 * Zooming is triggered with the keys +, o, -, if 3460 * the pan key (shift or ctrl - depending on the board attributes) is pressed. 3461 * <p> 3462 * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus. 3463 * 3464 * @param {Event} evt The browser's event object 3465 * 3466 * @see JXG.Board#keyboard 3467 * @see JXG.Board#keyFocusInListener 3468 * @see JXG.Board#keyFocusOutListener 3469 * 3470 */ 3471 keyDownListener: function (evt) { 3472 var id_node = evt.target.id, 3473 id, el, res, doc, 3474 sX = 0, 3475 sY = 0, 3476 // dx, dy are provided in screen units and 3477 // are converted to user coordinates 3478 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX, 3479 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY, 3480 // u = 100, 3481 doZoom = false, 3482 done = true, 3483 dir, 3484 actPos; 3485 3486 if (!this.attr.keyboard.enabled || id_node === '') { 3487 return false; 3488 } 3489 3490 // dx = Math.round(dx * u) / u; 3491 // dy = Math.round(dy * u) / u; 3492 3493 // An element of type input or textarea has foxus, get out of here. 3494 doc = this.containerObj.shadowRoot || document; 3495 if (doc.activeElement) { 3496 el = doc.activeElement; 3497 if (el.tagName === 'INPUT' || el.tagName === 'textarea') { 3498 return false; 3499 } 3500 } 3501 3502 // Get the JSXGraph id from the id of the SVG node. 3503 id = id_node.replace(this.containerObj.id + '_', ''); 3504 el = this.select(id); 3505 3506 if (Type.exists(el.coords)) { 3507 actPos = el.coords.usrCoords.slice(1); 3508 } 3509 3510 if ( 3511 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) || 3512 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey) 3513 ) { 3514 // Pan key has been pressed 3515 3516 if (Type.evaluate(this.attr.zoom.enabled) === true) { 3517 doZoom = true; 3518 } 3519 3520 // Arrow keys 3521 if (evt.keyCode === 38) { 3522 // up 3523 this.clickUpArrow(); 3524 } else if (evt.keyCode === 40) { 3525 // down 3526 this.clickDownArrow(); 3527 } else if (evt.keyCode === 37) { 3528 // left 3529 this.clickLeftArrow(); 3530 } else if (evt.keyCode === 39) { 3531 // right 3532 this.clickRightArrow(); 3533 3534 // Zoom keys 3535 } else if (doZoom && evt.keyCode === 171) { 3536 // + 3537 this.zoomIn(); 3538 } else if (doZoom && evt.keyCode === 173) { 3539 // - 3540 this.zoomOut(); 3541 } else if (doZoom && evt.keyCode === 79) { 3542 // o 3543 this.zoom100(); 3544 } else { 3545 done = false; 3546 } 3547 } else { 3548 // Adapt dx, dy to snapToGrid and attractToGrid 3549 // snapToGrid has priority. 3550 if (Type.exists(el.visProp)) { 3551 if ( 3552 Type.exists(el.visProp.snaptogrid) && 3553 el.visProp.snaptogrid && 3554 Type.evaluate(el.visProp.snapsizex) && 3555 Type.evaluate(el.visProp.snapsizey) 3556 ) { 3557 // Adapt dx, dy such that snapToGrid is possible 3558 res = el.getSnapSizes(); 3559 sX = res[0]; 3560 sY = res[1]; 3561 dx = Math.max(sX, dx); 3562 dy = Math.max(sY, dy); 3563 } else if ( 3564 Type.exists(el.visProp.attracttogrid) && 3565 el.visProp.attracttogrid && 3566 Type.evaluate(el.visProp.attractordistance) && 3567 Type.evaluate(el.visProp.attractorunit) 3568 ) { 3569 // Adapt dx, dy such that attractToGrid is possible 3570 sX = 1.1 * Type.evaluate(el.visProp.attractordistance); 3571 sY = sX; 3572 3573 if (Type.evaluate(el.visProp.attractorunit) === 'screen') { 3574 sX /= this.unitX; 3575 sY /= this.unitX; 3576 } 3577 dx = Math.max(sX, dx); 3578 dy = Math.max(sY, dy); 3579 } 3580 } 3581 3582 if (evt.keyCode === 38) { 3583 // up 3584 dir = [0, dy]; 3585 } else if (evt.keyCode === 40) { 3586 // down 3587 dir = [0, -dy]; 3588 } else if (evt.keyCode === 37) { 3589 // left 3590 dir = [-dx, 0]; 3591 } else if (evt.keyCode === 39) { 3592 // right 3593 dir = [dx, 0]; 3594 } else { 3595 done = false; 3596 } 3597 3598 if (dir && el.isDraggable && 3599 el.visPropCalc.visible && 3600 ((this.geonextCompatibilityMode && 3601 (Type.isPoint(el) || 3602 el.elementClass === Const.OBJECT_CLASS_TEXT) 3603 ) || !this.geonextCompatibilityMode) && 3604 !Type.evaluate(el.visProp.fixed) 3605 ) { 3606 3607 3608 this.mode = this.BOARD_MODE_DRAG; 3609 if (Type.exists(el.coords)) { 3610 dir[0] += actPos[0]; 3611 dir[1] += actPos[1]; 3612 } 3613 // For coordsElement setPosition has to call setPositionDirectly. 3614 // Otherwise the position is set by a translation. 3615 if (Type.exists(el.coords)) { 3616 el.setPosition(JXG.COORDS_BY_USER, dir); 3617 this.updateInfobox(el); 3618 } else { 3619 this.displayInfobox(false); 3620 el.setPositionDirectly( 3621 Const.COORDS_BY_USER, 3622 dir, 3623 [0, 0] 3624 ); 3625 } 3626 3627 this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]); 3628 el.triggerEventHandlers(['keydrag', 'drag'], [evt]); 3629 this.mode = this.BOARD_MODE_NONE; 3630 } 3631 } 3632 3633 this.update(); 3634 3635 if (done && Type.exists(evt.preventDefault)) { 3636 evt.preventDefault(); 3637 } 3638 return done; 3639 }, 3640 3641 /** 3642 * Event listener for SVG elements getting focus. 3643 * This is needed for highlighting when using keyboard control. 3644 * Only elements having the attribute 'tabindex' can receive focus. 3645 * 3646 * @see JXG.Board#keyFocusOutListener 3647 * @see JXG.Board#keyDownListener 3648 * @see JXG.Board#keyboard 3649 * 3650 * @param {Event} evt The browser's event object 3651 */ 3652 keyFocusInListener: function (evt) { 3653 var id_node = evt.target.id, 3654 id, 3655 el; 3656 3657 if (!this.attr.keyboard.enabled || id_node === '') { 3658 return false; 3659 } 3660 3661 id = id_node.replace(this.containerObj.id + '_', ''); 3662 el = this.select(id); 3663 if (Type.exists(el.highlight)) { 3664 el.highlight(true); 3665 this.focusObjects = [id]; 3666 el.triggerEventHandlers(['hit'], [evt]); 3667 } 3668 if (Type.exists(el.coords)) { 3669 this.updateInfobox(el); 3670 } 3671 }, 3672 3673 /** 3674 * Event listener for SVG elements losing focus. 3675 * This is needed for dehighlighting when using keyboard control. 3676 * Only elements having the attribute 'tabindex' can receive focus. 3677 * 3678 * @see JXG.Board#keyFocusInListener 3679 * @see JXG.Board#keyDownListener 3680 * @see JXG.Board#keyboard 3681 * 3682 * @param {Event} evt The browser's event object 3683 */ 3684 keyFocusOutListener: function (evt) { 3685 if (!this.attr.keyboard.enabled) { 3686 return false; 3687 } 3688 this.focusObjects = []; // This has to be before displayInfobox(false) 3689 this.dehighlightAll(); 3690 this.displayInfobox(false); 3691 }, 3692 3693 /** 3694 * Update the width and height of the JSXGraph container div element. 3695 * Read actual values with getBoundingClientRect(), 3696 * and call board.resizeContainer() with this values. 3697 * <p> 3698 * If necessary, also call setBoundingBox(). 3699 * 3700 * @see JXG.Board#startResizeObserver 3701 * @see JXG.Board#resizeListener 3702 * @see JXG.Board#resizeContainer 3703 * @see JXG.Board#setBoundingBox 3704 * 3705 */ 3706 updateContainerDims: function () { 3707 var w, h, 3708 bb, css, 3709 width_adjustment, height_adjustment; 3710 3711 // Get size of the board's container div 3712 bb = this.containerObj.getBoundingClientRect(); 3713 w = bb.width; 3714 h = bb.height; 3715 3716 // Subtract the border size 3717 if (window && window.getComputedStyle) { 3718 css = window.getComputedStyle(this.containerObj, null); 3719 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width')); 3720 if (!isNaN(width_adjustment)) { 3721 w -= width_adjustment; 3722 } 3723 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width')); 3724 if (!isNaN(height_adjustment)) { 3725 h -= height_adjustment; 3726 } 3727 } 3728 3729 // If div is invisible - do nothing 3730 if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { 3731 return; 3732 } 3733 3734 // If bounding box is not yet initialized, do it now. 3735 if (isNaN(this.getBoundingBox()[0])) { 3736 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep'); 3737 } 3738 3739 // Do nothing if the dimension did not change since being visible 3740 // the last time. Note that if the div had display:none in the mean time, 3741 // we did not store this._prevDim. 3742 if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) { 3743 return; 3744 } 3745 3746 // Set the size of the SVG or canvas element 3747 this.resizeContainer(w, h, true); 3748 this._prevDim = { 3749 w: w, 3750 h: h 3751 }; 3752 }, 3753 3754 /** 3755 * Start observer which reacts to size changes of the JSXGraph 3756 * container div element. Calls updateContainerDims(). 3757 * If not available, an event listener for the window-resize event is started. 3758 * On mobile devices also scrolling might trigger resizes. 3759 * However, resize events triggered by scrolling events should be ignored. 3760 * Therefore, also a scrollListener is started. 3761 * Resize can be controlled with the board attribute resize. 3762 * 3763 * @see JXG.Board#updateContainerDims 3764 * @see JXG.Board#resizeListener 3765 * @see JXG.Board#scrollListener 3766 * @see JXG.Board#resize 3767 * 3768 */ 3769 startResizeObserver: function () { 3770 var that = this; 3771 3772 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 3773 return; 3774 } 3775 3776 this.resizeObserver = new ResizeObserver(function (entries) { 3777 if (!that._isResizing) { 3778 that._isResizing = true; 3779 window.setTimeout(function () { 3780 try { 3781 that.updateContainerDims(); 3782 } catch (err) { 3783 that.stopResizeObserver(); 3784 } finally { 3785 that._isResizing = false; 3786 } 3787 }, that.attr.resize.throttle); 3788 } 3789 }); 3790 this.resizeObserver.observe(this.containerObj); 3791 }, 3792 3793 /** 3794 * Stops the resize observer. 3795 * @see JXG.Board#startResizeObserver 3796 * 3797 */ 3798 stopResizeObserver: function () { 3799 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 3800 return; 3801 } 3802 3803 if (Type.exists(this.resizeObserver)) { 3804 this.resizeObserver.unobserve(this.containerObj); 3805 } 3806 }, 3807 3808 /** 3809 * Fallback solutions if there is no resizeObserver available in the browser. 3810 * Reacts to resize events of the window (only). Otherwise similar to 3811 * startResizeObserver(). To handle changes of the visibility 3812 * of the JSXGraph container element, additionally an intersection observer is used. 3813 * which watches changes in the visibility of the JSXGraph container element. 3814 * This is necessary e.g. for register tabs or dia shows. 3815 * 3816 * @see JXG.Board#startResizeObserver 3817 * @see JXG.Board#startIntersectionObserver 3818 */ 3819 resizeListener: function () { 3820 var that = this; 3821 3822 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 3823 return; 3824 } 3825 if (!this._isScrolling && !this._isResizing) { 3826 this._isResizing = true; 3827 window.setTimeout(function () { 3828 that.updateContainerDims(); 3829 that._isResizing = false; 3830 }, this.attr.resize.throttle); 3831 } 3832 }, 3833 3834 /** 3835 * Listener to watch for scroll events. Sets board._isScrolling = true 3836 * @param {Event} evt The browser's event object 3837 * 3838 * @see JXG.Board#startResizeObserver 3839 * @see JXG.Board#resizeListener 3840 * 3841 */ 3842 scrollListener: function (evt) { 3843 var that = this; 3844 3845 if (!Env.isBrowser) { 3846 return; 3847 } 3848 if (!this._isScrolling) { 3849 this._isScrolling = true; 3850 window.setTimeout(function () { 3851 that._isScrolling = false; 3852 }, 66); 3853 } 3854 }, 3855 3856 /** 3857 * Watch for changes of the visibility of the JSXGraph container element. 3858 * 3859 * @see JXG.Board#startResizeObserver 3860 * @see JXG.Board#resizeListener 3861 * 3862 */ 3863 startIntersectionObserver: function () { 3864 var that = this, 3865 options = { 3866 root: null, 3867 rootMargin: '0px', 3868 threshold: 0.8 3869 }; 3870 3871 try { 3872 this.intersectionObserver = new IntersectionObserver(function (entries) { 3873 // If bounding box is not yet initialized, do it now. 3874 if (isNaN(that.getBoundingBox()[0])) { 3875 that.updateContainerDims(); 3876 } 3877 }, options); 3878 this.intersectionObserver.observe(that.containerObj); 3879 } catch (err) { 3880 console.log('JSXGraph: IntersectionObserver not available in this browser.'); 3881 } 3882 }, 3883 3884 /** 3885 * Stop the intersection observer 3886 * 3887 * @see JXG.Board#startIntersectionObserver 3888 * 3889 */ 3890 stopIntersectionObserver: function () { 3891 if (Type.exists(this.intersectionObserver)) { 3892 this.intersectionObserver.unobserve(this.containerObj); 3893 } 3894 }, 3895 3896 /********************************************************** 3897 * 3898 * End of Event Handlers 3899 * 3900 **********************************************************/ 3901 3902 /** 3903 * Initialize the info box object which is used to display 3904 * the coordinates of points near the mouse pointer, 3905 * @returns {JXG.Board} Reference to the board 3906 */ 3907 initInfobox: function (attributes) { 3908 var attr = Type.copyAttributes(attributes, this.options, 'infobox'); 3909 3910 attr.id = this.id + '_infobox'; 3911 3912 /** 3913 * Infobox close to points in which the points' coordinates are displayed. 3914 * This is simply a JXG.Text element. Access through board.infobox. 3915 * Uses CSS class .JXGinfobox. 3916 * 3917 * @namespace 3918 * @name JXG.Board.infobox 3919 * @type JXG.Text 3920 * 3921 * @example 3922 * const board = JXG.JSXGraph.initBoard(BOARDID, { 3923 * boundingbox: [-0.5, 0.5, 0.5, -0.5], 3924 * intl: { 3925 * enabled: false, 3926 * locale: 'de-DE' 3927 * }, 3928 * keepaspectratio: true, 3929 * axis: true, 3930 * infobox: { 3931 * distanceY: 40, 3932 * intl: { 3933 * enabled: true, 3934 * options: { 3935 * minimumFractionDigits: 1, 3936 * maximumFractionDigits: 2 3937 * } 3938 * } 3939 * } 3940 * }); 3941 * var p = board.create('point', [0.1, 0.1], {}); 3942 * 3943 * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div> 3944 * <script type="text/javascript"> 3945 * (function() { 3946 * const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', { 3947 * boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false, 3948 * intl: { 3949 * enabled: false, 3950 * locale: 'de-DE' 3951 * }, 3952 * keepaspectratio: true, 3953 * axis: true, 3954 * infobox: { 3955 * distanceY: 40, 3956 * intl: { 3957 * enabled: true, 3958 * options: { 3959 * minimumFractionDigits: 1, 3960 * maximumFractionDigits: 2 3961 * } 3962 * } 3963 * } 3964 * }); 3965 * var p = board.create('point', [0.1, 0.1], {}); 3966 * })(); 3967 * 3968 * </script><pre> 3969 * 3970 */ 3971 this.infobox = this.create('text', [0, 0, '0,0'], attr); 3972 // this.infobox.needsUpdateSize = false; // That is not true, but it speeds drawing up. 3973 this.infobox.dump = false; 3974 3975 this.displayInfobox(false); 3976 return this; 3977 }, 3978 3979 /** 3980 * Updates and displays a little info box to show coordinates of current selected points. 3981 * @param {JXG.GeometryElement} el A GeometryElement 3982 * @returns {JXG.Board} Reference to the board 3983 * @see JXG.Board#displayInfobox 3984 * @see JXG.Board#showInfobox 3985 * @see Point#showInfobox 3986 * 3987 */ 3988 updateInfobox: function (el) { 3989 var x, y, xc, yc, 3990 vpinfoboxdigits, 3991 distX, distY, 3992 vpsi = Type.evaluate(el.visProp.showinfobox); 3993 3994 if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) { 3995 return this; 3996 } 3997 3998 if (Type.isPoint(el)) { 3999 xc = el.coords.usrCoords[1]; 4000 yc = el.coords.usrCoords[2]; 4001 distX = Type.evaluate(this.infobox.visProp.distancex); 4002 distY = Type.evaluate(this.infobox.visProp.distancey); 4003 4004 this.infobox.setCoords( 4005 xc + distX / this.unitX, 4006 yc + distY / this.unitY 4007 ); 4008 4009 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits); 4010 if (typeof el.infoboxText !== 'string') { 4011 if (vpinfoboxdigits === 'auto') { 4012 if (this.infobox.useLocale()) { 4013 x = this.infobox.formatNumberLocale(xc); 4014 y = this.infobox.formatNumberLocale(yc); 4015 } else { 4016 x = Type.autoDigits(xc); 4017 y = Type.autoDigits(yc); 4018 } 4019 } else if (Type.isNumber(vpinfoboxdigits)) { 4020 if (this.infobox.useLocale()) { 4021 x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits); 4022 y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits); 4023 } else { 4024 x = Type.toFixed(xc, vpinfoboxdigits); 4025 y = Type.toFixed(yc, vpinfoboxdigits); 4026 } 4027 4028 } else { 4029 x = xc; 4030 y = yc; 4031 } 4032 4033 this.highlightInfobox(x, y, el); 4034 } else { 4035 this.highlightCustomInfobox(el.infoboxText, el); 4036 } 4037 4038 this.displayInfobox(true); 4039 } 4040 return this; 4041 }, 4042 4043 /** 4044 * Set infobox visible / invisible. 4045 * 4046 * It uses its property hiddenByParent to memorize its status. 4047 * In this way, many DOM access can be avoided. 4048 * 4049 * @param {Boolean} val true for visible, false for invisible 4050 * @returns {JXG.Board} Reference to the board. 4051 * @see JXG.Board#updateInfobox 4052 * 4053 */ 4054 displayInfobox: function (val) { 4055 if (!val && this.focusObjects.length > 0 && 4056 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) { 4057 // If an element has focus we do not hide its infobox 4058 return this; 4059 } 4060 if (this.infobox.hiddenByParent === val) { 4061 this.infobox.hiddenByParent = !val; 4062 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer(); 4063 } 4064 return this; 4065 }, 4066 4067 // Alias for displayInfobox to be backwards compatible. 4068 // The method showInfobox clashes with the board attribute showInfobox 4069 showInfobox: function (val) { 4070 return this.displayInfobox(val); 4071 }, 4072 4073 /** 4074 * Changes the text of the info box to show the given coordinates. 4075 * @param {Number} x 4076 * @param {Number} y 4077 * @param {JXG.GeometryElement} [el] The element the mouse is pointing at 4078 * @returns {JXG.Board} Reference to the board. 4079 */ 4080 highlightInfobox: function (x, y, el) { 4081 this.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 4082 return this; 4083 }, 4084 4085 /** 4086 * Changes the text of the info box to what is provided via text. 4087 * @param {String} text 4088 * @param {JXG.GeometryElement} [el] 4089 * @returns {JXG.Board} Reference to the board. 4090 */ 4091 highlightCustomInfobox: function (text, el) { 4092 this.infobox.setText(text); 4093 return this; 4094 }, 4095 4096 /** 4097 * Remove highlighting of all elements. 4098 * @returns {JXG.Board} Reference to the board. 4099 */ 4100 dehighlightAll: function () { 4101 var el, 4102 pEl, 4103 stillHighlighted = {}, 4104 needsDeHighlight = false; 4105 4106 for (el in this.highlightedObjects) { 4107 if (this.highlightedObjects.hasOwnProperty(el)) { 4108 4109 pEl = this.highlightedObjects[el]; 4110 if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus 4111 if (this.hasMouseHandlers || this.hasPointerHandlers) { 4112 pEl.noHighlight(); 4113 } 4114 needsDeHighlight = true; 4115 } else { 4116 stillHighlighted[el] = pEl; 4117 } 4118 // In highlightedObjects should only be objects which fulfill all these conditions 4119 // And in case of complex elements, like a turtle based fractal, it should be faster to 4120 // just de-highlight the element instead of checking hasPoint... 4121 // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible) 4122 } 4123 } 4124 4125 this.highlightedObjects = stillHighlighted; 4126 4127 // We do not need to redraw during dehighlighting in CanvasRenderer 4128 // because we are redrawing anyhow 4129 // -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until 4130 // another object is highlighted. 4131 if (this.renderer.type === 'canvas' && needsDeHighlight) { 4132 this.prepareUpdate(); 4133 this.renderer.suspendRedraw(this); 4134 this.updateRenderer(); 4135 this.renderer.unsuspendRedraw(); 4136 } 4137 4138 return this; 4139 }, 4140 4141 /** 4142 * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose 4143 * once. 4144 * @private 4145 * @param {Number} x X coordinate in screen coordinates 4146 * @param {Number} y Y coordinate in screen coordinates 4147 * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates. 4148 * @see JXG.Board#getUsrCoordsOfMouse 4149 */ 4150 getScrCoordsOfMouse: function (x, y) { 4151 return [x, y]; 4152 }, 4153 4154 /** 4155 * This method calculates the user coords of the current mouse coordinates. 4156 * @param {Event} evt Event object containing the mouse coordinates. 4157 * @returns {Array} Coordinates [x, y] of the mouse in user coordinates. 4158 * @example 4159 * board.on('up', function (evt) { 4160 * var a = board.getUsrCoordsOfMouse(evt), 4161 * x = a[0], 4162 * y = a[1], 4163 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4164 * // Shorter version: 4165 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4166 * }); 4167 * 4168 * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div> 4169 * <script type='text/javascript'> 4170 * (function() { 4171 * var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746', 4172 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 4173 * board.on('up', function (evt) { 4174 * var a = board.getUsrCoordsOfMouse(evt), 4175 * x = a[0], 4176 * y = a[1], 4177 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4178 * // Shorter version: 4179 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4180 * }); 4181 * 4182 * })(); 4183 * 4184 * </script><pre> 4185 * 4186 * @see JXG.Board#getScrCoordsOfMouse 4187 * @see JXG.Board#getAllUnderMouse 4188 */ 4189 getUsrCoordsOfMouse: function (evt) { 4190 var cPos = this.getCoordsTopLeftCorner(), 4191 absPos = Env.getPosition(evt, null, this.document), 4192 x = absPos[0] - cPos[0], 4193 y = absPos[1] - cPos[1], 4194 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this); 4195 4196 return newCoords.usrCoords.slice(1); 4197 }, 4198 4199 /** 4200 * Collects all elements under current mouse position plus current user coordinates of mouse cursor. 4201 * @param {Event} evt Event object containing the mouse coordinates. 4202 * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse. 4203 * @see JXG.Board#getUsrCoordsOfMouse 4204 * @see JXG.Board#getAllObjectsUnderMouse 4205 */ 4206 getAllUnderMouse: function (evt) { 4207 var elList = this.getAllObjectsUnderMouse(evt); 4208 elList.push(this.getUsrCoordsOfMouse(evt)); 4209 4210 return elList; 4211 }, 4212 4213 /** 4214 * Collects all elements under current mouse position. 4215 * @param {Event} evt Event object containing the mouse coordinates. 4216 * @returns {Array} Array of elements at the current mouse position. 4217 * @see JXG.Board#getAllUnderMouse 4218 */ 4219 getAllObjectsUnderMouse: function (evt) { 4220 var cPos = this.getCoordsTopLeftCorner(), 4221 absPos = Env.getPosition(evt, null, this.document), 4222 dx = absPos[0] - cPos[0], 4223 dy = absPos[1] - cPos[1], 4224 elList = [], 4225 el, 4226 pEl, 4227 len = this.objectsList.length; 4228 4229 for (el = 0; el < len; el++) { 4230 pEl = this.objectsList[el]; 4231 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) { 4232 elList[elList.length] = pEl; 4233 } 4234 } 4235 4236 return elList; 4237 }, 4238 4239 /** 4240 * Update the coords object of all elements which possess this 4241 * property. This is necessary after changing the viewport. 4242 * @returns {JXG.Board} Reference to this board. 4243 **/ 4244 updateCoords: function () { 4245 var el, 4246 ob, 4247 len = this.objectsList.length; 4248 4249 for (ob = 0; ob < len; ob++) { 4250 el = this.objectsList[ob]; 4251 4252 if (Type.exists(el.coords)) { 4253 if (Type.evaluate(el.visProp.frozen)) { 4254 if (el.is3D) { 4255 el.element2D.coords.screen2usr(); 4256 } else { 4257 el.coords.screen2usr(); 4258 } 4259 } else { 4260 if (el.is3D) { 4261 el.element2D.coords.usr2screen(); 4262 } else { 4263 el.coords.usr2screen(); 4264 } 4265 } 4266 } 4267 } 4268 return this; 4269 }, 4270 4271 /** 4272 * Moves the origin and initializes an update of all elements. 4273 * @param {Number} x 4274 * @param {Number} y 4275 * @param {Boolean} [diff=false] 4276 * @returns {JXG.Board} Reference to this board. 4277 */ 4278 moveOrigin: function (x, y, diff) { 4279 var ox, oy, ul, lr; 4280 if (Type.exists(x) && Type.exists(y)) { 4281 ox = this.origin.scrCoords[1]; 4282 oy = this.origin.scrCoords[2]; 4283 4284 this.origin.scrCoords[1] = x; 4285 this.origin.scrCoords[2] = y; 4286 4287 if (diff) { 4288 this.origin.scrCoords[1] -= this.drag_dx; 4289 this.origin.scrCoords[2] -= this.drag_dy; 4290 } 4291 4292 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords; 4293 lr = new Coords( 4294 Const.COORDS_BY_SCREEN, 4295 [this.canvasWidth, this.canvasHeight], 4296 this 4297 ).usrCoords; 4298 if ( 4299 ul[1] < this.maxboundingbox[0] || 4300 ul[2] > this.maxboundingbox[1] || 4301 lr[1] > this.maxboundingbox[2] || 4302 lr[2] < this.maxboundingbox[3] 4303 ) { 4304 this.origin.scrCoords[1] = ox; 4305 this.origin.scrCoords[2] = oy; 4306 } 4307 } 4308 4309 this.updateCoords().clearTraces().fullUpdate(); 4310 this.triggerEventHandlers(['boundingbox']); 4311 4312 return this; 4313 }, 4314 4315 /** 4316 * Add conditional updates to the elements. 4317 * @param {String} str String containing coniditional update in geonext syntax 4318 */ 4319 addConditions: function (str) { 4320 var term, 4321 m, 4322 left, 4323 right, 4324 name, 4325 el, 4326 property, 4327 functions = [], 4328 // plaintext = 'var el, x, y, c, rgbo;\n', 4329 i = str.indexOf('<data>'), 4330 j = str.indexOf('<' + '/data>'), 4331 xyFun = function (board, el, f, what) { 4332 return function () { 4333 var e, t; 4334 4335 e = board.select(el.id); 4336 t = e.coords.usrCoords[what]; 4337 4338 if (what === 2) { 4339 e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]); 4340 } else { 4341 e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]); 4342 } 4343 e.prepareUpdate().update(); 4344 }; 4345 }, 4346 visFun = function (board, el, f) { 4347 return function () { 4348 var e, v; 4349 4350 e = board.select(el.id); 4351 v = f(); 4352 4353 e.setAttribute({ visible: v }); 4354 }; 4355 }, 4356 colFun = function (board, el, f, what) { 4357 return function () { 4358 var e, v; 4359 4360 e = board.select(el.id); 4361 v = f(); 4362 4363 if (what === 'strokewidth') { 4364 e.visProp.strokewidth = v; 4365 } else { 4366 v = Color.rgba2rgbo(v); 4367 e.visProp[what + 'color'] = v[0]; 4368 e.visProp[what + 'opacity'] = v[1]; 4369 } 4370 }; 4371 }, 4372 posFun = function (board, el, f) { 4373 return function () { 4374 var e = board.select(el.id); 4375 4376 e.position = f(); 4377 }; 4378 }, 4379 styleFun = function (board, el, f) { 4380 return function () { 4381 var e = board.select(el.id); 4382 4383 e.setStyle(f()); 4384 }; 4385 }; 4386 4387 if (i < 0) { 4388 return; 4389 } 4390 4391 while (i >= 0) { 4392 term = str.slice(i + 6, j); // throw away <data> 4393 m = term.indexOf('='); 4394 left = term.slice(0, m); 4395 right = term.slice(m + 1); 4396 m = left.indexOf('.'); // Dies erzeugt Probleme bei Variablennamen der Form ' Steuern akt.' 4397 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace) 4398 el = this.elementsByName[Type.unescapeHTML(name)]; 4399 4400 property = left 4401 .slice(m + 1) 4402 .replace(/\s+/g, '') 4403 .toLowerCase(); // remove whitespace in property 4404 right = Type.createFunction(right, this, '', true); 4405 4406 // Debug 4407 if (!Type.exists(this.elementsByName[name])) { 4408 JXG.debug('debug conditions: |' + name + '| undefined'); 4409 } else { 4410 // plaintext += 'el = this.objects[\'' + el.id + '\'];\n'; 4411 4412 switch (property) { 4413 case 'x': 4414 functions.push(xyFun(this, el, right, 2)); 4415 break; 4416 case 'y': 4417 functions.push(xyFun(this, el, right, 1)); 4418 break; 4419 case 'visible': 4420 functions.push(visFun(this, el, right)); 4421 break; 4422 case 'position': 4423 functions.push(posFun(this, el, right)); 4424 break; 4425 case 'stroke': 4426 functions.push(colFun(this, el, right, 'stroke')); 4427 break; 4428 case 'style': 4429 functions.push(styleFun(this, el, right)); 4430 break; 4431 case 'strokewidth': 4432 functions.push(colFun(this, el, right, 'strokewidth')); 4433 break; 4434 case 'fill': 4435 functions.push(colFun(this, el, right, 'fill')); 4436 break; 4437 case 'label': 4438 break; 4439 default: 4440 JXG.debug( 4441 'property "' + 4442 property + 4443 '" in conditions not yet implemented:' + 4444 right 4445 ); 4446 break; 4447 } 4448 } 4449 str = str.slice(j + 7); // cut off '</data>' 4450 i = str.indexOf('<data>'); 4451 j = str.indexOf('<' + '/data>'); 4452 } 4453 4454 this.updateConditions = function () { 4455 var i; 4456 4457 for (i = 0; i < functions.length; i++) { 4458 functions[i](); 4459 } 4460 4461 this.prepareUpdate().updateElements(); 4462 return true; 4463 }; 4464 this.updateConditions(); 4465 }, 4466 4467 /** 4468 * Computes the commands in the conditions-section of the gxt file. 4469 * It is evaluated after an update, before the unsuspendRedraw. 4470 * The function is generated in 4471 * @see JXG.Board#addConditions 4472 * @private 4473 */ 4474 updateConditions: function () { 4475 return false; 4476 }, 4477 4478 /** 4479 * Calculates adequate snap sizes. 4480 * @returns {JXG.Board} Reference to the board. 4481 */ 4482 calculateSnapSizes: function () { 4483 var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this), 4484 p2 = new Coords( 4485 Const.COORDS_BY_USER, 4486 [this.options.grid.gridX, this.options.grid.gridY], 4487 this 4488 ), 4489 x = p1.scrCoords[1] - p2.scrCoords[1], 4490 y = p1.scrCoords[2] - p2.scrCoords[2]; 4491 4492 this.options.grid.snapSizeX = this.options.grid.gridX; 4493 while (Math.abs(x) > 25) { 4494 this.options.grid.snapSizeX *= 2; 4495 x /= 2; 4496 } 4497 4498 this.options.grid.snapSizeY = this.options.grid.gridY; 4499 while (Math.abs(y) > 25) { 4500 this.options.grid.snapSizeY *= 2; 4501 y /= 2; 4502 } 4503 4504 return this; 4505 }, 4506 4507 /** 4508 * Apply update on all objects with the new zoom-factors. Clears all traces. 4509 * @returns {JXG.Board} Reference to the board. 4510 */ 4511 applyZoom: function () { 4512 this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate(); 4513 4514 return this; 4515 }, 4516 4517 /** 4518 * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 4519 * The zoom operation is centered at x, y. 4520 * @param {Number} [x] 4521 * @param {Number} [y] 4522 * @returns {JXG.Board} Reference to the board 4523 */ 4524 zoomIn: function (x, y) { 4525 var bb = this.getBoundingBox(), 4526 zX = this.attr.zoom.factorx, 4527 zY = this.attr.zoom.factory, 4528 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX), 4529 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY), 4530 lr = 0.5, 4531 tr = 0.5, 4532 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 4533 4534 if ( 4535 (this.zoomX > this.attr.zoom.max && zX > 1.0) || 4536 (this.zoomY > this.attr.zoom.max && zY > 1.0) || 4537 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices 4538 (this.zoomY < mi && zY < 1.0) 4539 ) { 4540 return this; 4541 } 4542 4543 if (Type.isNumber(x) && Type.isNumber(y)) { 4544 lr = (x - bb[0]) / (bb[2] - bb[0]); 4545 tr = (bb[1] - y) / (bb[1] - bb[3]); 4546 } 4547 4548 this.setBoundingBox( 4549 [ 4550 bb[0] + dX * lr, 4551 bb[1] - dY * tr, 4552 bb[2] - dX * (1 - lr), 4553 bb[3] + dY * (1 - tr) 4554 ], 4555 this.keepaspectratio, 4556 'update' 4557 ); 4558 return this.applyZoom(); 4559 }, 4560 4561 /** 4562 * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 4563 * The zoom operation is centered at x, y. 4564 * 4565 * @param {Number} [x] 4566 * @param {Number} [y] 4567 * @returns {JXG.Board} Reference to the board 4568 */ 4569 zoomOut: function (x, y) { 4570 var bb = this.getBoundingBox(), 4571 zX = this.attr.zoom.factorx, 4572 zY = this.attr.zoom.factory, 4573 dX = (bb[2] - bb[0]) * (1.0 - zX), 4574 dY = (bb[1] - bb[3]) * (1.0 - zY), 4575 lr = 0.5, 4576 tr = 0.5, 4577 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 4578 4579 if (this.zoomX < mi || this.zoomY < mi) { 4580 return this; 4581 } 4582 4583 if (Type.isNumber(x) && Type.isNumber(y)) { 4584 lr = (x - bb[0]) / (bb[2] - bb[0]); 4585 tr = (bb[1] - y) / (bb[1] - bb[3]); 4586 } 4587 4588 this.setBoundingBox( 4589 [ 4590 bb[0] + dX * lr, 4591 bb[1] - dY * tr, 4592 bb[2] - dX * (1 - lr), 4593 bb[3] + dY * (1 - tr) 4594 ], 4595 this.keepaspectratio, 4596 'update' 4597 ); 4598 4599 return this.applyZoom(); 4600 }, 4601 4602 /** 4603 * Reset the zoom level to the original zoom level from initBoard(); 4604 * Additionally, if the board as been initialized with a boundingBox (which is the default), 4605 * restore the viewport to the original viewport during initialization. Otherwise, 4606 * (i.e. if the board as been initialized with unitX/Y and originX/Y), 4607 * just set the zoom level to 100%. 4608 * 4609 * @returns {JXG.Board} Reference to the board 4610 */ 4611 zoom100: function () { 4612 var bb, dX, dY; 4613 4614 if (Type.exists(this.attr.boundingbox)) { 4615 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset'); 4616 } else { 4617 // Board has been set up with unitX/Y and originX/Y 4618 bb = this.getBoundingBox(); 4619 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5; 4620 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5; 4621 this.setBoundingBox( 4622 [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], 4623 this.keepaspectratio, 4624 'reset' 4625 ); 4626 } 4627 return this.applyZoom(); 4628 }, 4629 4630 /** 4631 * Zooms the board so every visible point is shown. Keeps aspect ratio. 4632 * @returns {JXG.Board} Reference to the board 4633 */ 4634 zoomAllPoints: function () { 4635 var el, 4636 border, 4637 borderX, 4638 borderY, 4639 pEl, 4640 minX = 0, 4641 maxX = 0, 4642 minY = 0, 4643 maxY = 0, 4644 len = this.objectsList.length; 4645 4646 for (el = 0; el < len; el++) { 4647 pEl = this.objectsList[el]; 4648 4649 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) { 4650 if (pEl.coords.usrCoords[1] < minX) { 4651 minX = pEl.coords.usrCoords[1]; 4652 } else if (pEl.coords.usrCoords[1] > maxX) { 4653 maxX = pEl.coords.usrCoords[1]; 4654 } 4655 if (pEl.coords.usrCoords[2] > maxY) { 4656 maxY = pEl.coords.usrCoords[2]; 4657 } else if (pEl.coords.usrCoords[2] < minY) { 4658 minY = pEl.coords.usrCoords[2]; 4659 } 4660 } 4661 } 4662 4663 border = 50; 4664 borderX = border / this.unitX; 4665 borderY = border / this.unitY; 4666 4667 this.setBoundingBox( 4668 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], 4669 this.keepaspectratio, 4670 'update' 4671 ); 4672 4673 return this.applyZoom(); 4674 }, 4675 4676 /** 4677 * Reset the bounding box and the zoom level to 100% such that a given set of elements is 4678 * within the board's viewport. 4679 * @param {Array} elements A set of elements given by id, reference, or name. 4680 * @returns {JXG.Board} Reference to the board. 4681 */ 4682 zoomElements: function (elements) { 4683 var i, 4684 e, 4685 box, 4686 newBBox = [Infinity, -Infinity, -Infinity, Infinity], 4687 cx, 4688 cy, 4689 dx, 4690 dy, 4691 d; 4692 4693 if (!Type.isArray(elements) || elements.length === 0) { 4694 return this; 4695 } 4696 4697 for (i = 0; i <