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