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